From ed4be76a9279d27a90a6037a6fdb661f2752ad02 Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Thu, 5 Jun 2025 11:32:09 +0100 Subject: [PATCH 001/321] interop with docbrown --- Cargo.lock | 205 ++- Cargo.toml | 38 +- db4-common/Cargo.toml | 18 + db4-common/src/lib.rs | 78 + db4-storage/Cargo.toml | 50 + db4-storage/src/lib.rs | 236 +++ db4-storage/src/loaders/mod.rs | 534 +++++++ db4-storage/src/pages/edge_page/edge_entry.rs | 235 +++ .../src/pages/edge_page/edge_page_view.rs | 73 + db4-storage/src/pages/edge_page/mod.rs | 1 + db4-storage/src/pages/edge_page/writer.rs | 116 ++ db4-storage/src/pages/edge_store.rs | 351 +++++ db4-storage/src/pages/locked/edges.rs | 78 + db4-storage/src/pages/locked/mod.rs | 2 + db4-storage/src/pages/locked/nodes.rs | 77 + db4-storage/src/pages/mod.rs | 1401 +++++++++++++++++ db4-storage/src/pages/node_page/mod.rs | 1 + .../src/pages/node_page/node_page_view.rs | 105 ++ db4-storage/src/pages/node_page/writer.rs | 188 +++ db4-storage/src/pages/node_store.rs | 248 +++ db4-storage/src/pages/session.rs | 219 +++ db4-storage/src/pages/test_utils.rs | 869 ++++++++++ db4-storage/src/properties/mod.rs | 326 ++++ .../src/properties/props_meta_writer.rs | 230 +++ db4-storage/src/segments/additions.rs | 60 + db4-storage/src/segments/edge.rs | 249 +++ db4-storage/src/segments/edge_entry.rs | 80 + db4-storage/src/segments/mod.rs | 244 +++ db4-storage/src/segments/node.rs | 315 ++++ db4-storage/src/segments/node_entry.rs | 88 ++ python/Cargo.toml | 5 +- raphtory-api/src/core/entities/layers.rs | 2 +- raphtory-api/src/core/entities/mod.rs | 2 +- .../src/core/entities/properties/meta.rs | 172 +- .../entities/properties/prop/prop_type.rs | 81 + .../src/core/entities/properties/tprop.rs | 41 +- raphtory-api/src/core/storage/dict_mapper.rs | 117 +- .../src/entities/graph/logical_to_physical.rs | 7 + .../src/entities/nodes/node_store.rs | 14 +- .../src/entities/nodes/structure/adj.rs | 21 +- .../src/entities/properties/tprop.rs | 278 ++-- raphtory-core/src/storage/lazy_vec.rs | 2 +- raphtory-core/src/storage/mod.rs | 275 ++-- .../src/disk/storage_interface/edges.rs | 3 +- .../src/graph/edges/edge_entry.rs | 34 - raphtory-storage/src/graph/edges/edge_ref.rs | 26 - .../src/graph/edges/edge_storage_ops.rs | 70 +- raphtory-storage/src/graph/graph.rs | 2 +- .../src/graph/nodes/node_additions.rs | 6 +- .../src/graph/variants/storage_variants2.rs | 15 +- .../src/graph/variants/storage_variants3.rs | 15 +- .../storage/graph/storage_ops/time_props.rs | 4 +- .../graph/storage_ops/time_semantics.rs | 36 +- .../time_semantics/base_time_semantics.rs | 25 +- .../time_semantics/event_semantics.rs | 32 +- .../internal/time_semantics/filtered_edge.rs | 58 +- .../api/view/internal/time_semantics/mod.rs | 41 +- .../time_semantics/persistent_semantics.rs | 55 +- .../internal/time_semantics/time_semantics.rs | 23 +- .../time_semantics/time_semantics_ops.rs | 19 +- .../time_semantics/window_time_semantics.rs | 27 +- raphtory/src/db/graph/node.rs | 7 +- raphtory/src/db/graph/views/deletion_graph.rs | 37 +- raphtory/src/db/graph/views/window_graph.rs | 17 +- raphtory/src/errors.rs | 2 + raphtory/src/graphgen/mod.rs | 2 +- raphtory/src/io/arrow/mod.rs | 4 +- raphtory/src/io/arrow/node_col.rs | 244 ++- raphtory/src/io/arrow/prop_handler.rs | 182 ++- raphtory/src/io/mod.rs | 2 +- 70 files changed, 8157 insertions(+), 563 deletions(-) create mode 100644 db4-common/Cargo.toml create mode 100644 db4-common/src/lib.rs create mode 100644 db4-storage/Cargo.toml create mode 100644 db4-storage/src/lib.rs create mode 100644 db4-storage/src/loaders/mod.rs create mode 100644 db4-storage/src/pages/edge_page/edge_entry.rs create mode 100644 db4-storage/src/pages/edge_page/edge_page_view.rs create mode 100644 db4-storage/src/pages/edge_page/mod.rs create mode 100644 db4-storage/src/pages/edge_page/writer.rs create mode 100644 db4-storage/src/pages/edge_store.rs create mode 100644 db4-storage/src/pages/locked/edges.rs create mode 100644 db4-storage/src/pages/locked/mod.rs create mode 100644 db4-storage/src/pages/locked/nodes.rs create mode 100644 db4-storage/src/pages/mod.rs create mode 100644 db4-storage/src/pages/node_page/mod.rs create mode 100644 db4-storage/src/pages/node_page/node_page_view.rs create mode 100644 db4-storage/src/pages/node_page/writer.rs create mode 100644 db4-storage/src/pages/node_store.rs create mode 100644 db4-storage/src/pages/session.rs create mode 100644 db4-storage/src/pages/test_utils.rs create mode 100644 db4-storage/src/properties/mod.rs create mode 100644 db4-storage/src/properties/props_meta_writer.rs create mode 100644 db4-storage/src/segments/additions.rs create mode 100644 db4-storage/src/segments/edge.rs create mode 100644 db4-storage/src/segments/edge_entry.rs create mode 100644 db4-storage/src/segments/mod.rs create mode 100644 db4-storage/src/segments/node.rs create mode 100644 db4-storage/src/segments/node_entry.rs diff --git a/Cargo.lock b/Cargo.lock index 642c03c068..11479c2cf2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "Inflector" @@ -231,14 +231,14 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "arrow" -version = "53.2.0" +version = "53.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4caf25cdc4a985f91df42ed9e9308e1adbcd341a31a72605c697033fcef163e3" +checksum = "d3a3ec4fe573f9d1f59d99c085197ef669b00b088ba1d7bb75224732d9357a74" dependencies = [ "arrow-arith", "arrow-array", "arrow-buffer", - "arrow-cast", + "arrow-cast 53.4.1 (registry+https://github.com/rust-lang/crates.io-index)", "arrow-csv", "arrow-data", "arrow-ipc", @@ -246,15 +246,15 @@ dependencies = [ "arrow-ord", "arrow-row", "arrow-schema", - "arrow-select", + "arrow-select 53.4.1 (registry+https://github.com/rust-lang/crates.io-index)", "arrow-string", ] [[package]] name = "arrow-arith" -version = "53.2.0" +version = "53.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91f2dfd1a7ec0aca967dfaa616096aec49779adc8eccec005e2f5e4111b1192a" +checksum = "6dcf19f07792d8c7f91086c67b574a79301e367029b17fcf63fb854332246a10" dependencies = [ "arrow-array", "arrow-buffer", @@ -267,8 +267,8 @@ dependencies = [ [[package]] name = "arrow-array" -version = "53.2.0" -source = "git+https://github.com/apache/arrow-rs.git?tag=53.2.0#10c4059b40f838bb8f7bac5259cb499e6eceec88" +version = "53.4.1" +source = "git+https://github.com/apache/arrow-rs.git?tag=53.4.1#962558546b9f441b5b7d039624d068c36a3ef69e" dependencies = [ "ahash", "arrow-buffer", @@ -277,14 +277,14 @@ dependencies = [ "chrono", "chrono-tz 0.10.3", "half", - "hashbrown 0.14.5", + "hashbrown 0.15.3", "num", ] [[package]] name = "arrow-buffer" -version = "53.2.0" -source = "git+https://github.com/apache/arrow-rs.git?tag=53.2.0#10c4059b40f838bb8f7bac5259cb499e6eceec88" +version = "53.4.1" +source = "git+https://github.com/apache/arrow-rs.git?tag=53.4.1#962558546b9f441b5b7d039624d068c36a3ef69e" dependencies = [ "bytes", "half", @@ -293,15 +293,15 @@ dependencies = [ [[package]] name = "arrow-cast" -version = "53.2.0" +version = "53.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d09aea56ec9fa267f3f3f6cdab67d8a9974cbba90b3aa38c8fe9d0bb071bd8c1" +checksum = "6365f8527d4f87b133eeb862f9b8093c009d41a210b8f101f91aa2392f61daac" dependencies = [ "arrow-array", "arrow-buffer", "arrow-data", "arrow-schema", - "arrow-select", + "arrow-select 53.4.1 (registry+https://github.com/rust-lang/crates.io-index)", "atoi", "base64 0.22.1", "chrono", @@ -312,15 +312,33 @@ dependencies = [ "ryu", ] +[[package]] +name = "arrow-cast" +version = "53.4.1" +source = "git+https://github.com/apache/arrow-rs.git?tag=53.4.1#962558546b9f441b5b7d039624d068c36a3ef69e" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "arrow-select 53.4.1 (git+https://github.com/apache/arrow-rs.git?tag=53.4.1)", + "atoi", + "base64 0.22.1", + "chrono", + "half", + "lexical-core", + "num", + "ryu", +] + [[package]] name = "arrow-csv" -version = "53.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c07b5232be87d115fde73e32f2ca7f1b353bff1b44ac422d3c6fc6ae38f11f0d" +version = "53.4.1" +source = "git+https://github.com/apache/arrow-rs.git?tag=53.4.1#962558546b9f441b5b7d039624d068c36a3ef69e" dependencies = [ "arrow-array", "arrow-buffer", - "arrow-cast", + "arrow-cast 53.4.1 (git+https://github.com/apache/arrow-rs.git?tag=53.4.1)", "arrow-data", "arrow-schema", "chrono", @@ -333,8 +351,8 @@ dependencies = [ [[package]] name = "arrow-data" -version = "53.2.0" -source = "git+https://github.com/apache/arrow-rs.git?tag=53.2.0#10c4059b40f838bb8f7bac5259cb499e6eceec88" +version = "53.4.1" +source = "git+https://github.com/apache/arrow-rs.git?tag=53.4.1#962558546b9f441b5b7d039624d068c36a3ef69e" dependencies = [ "arrow-buffer", "arrow-schema", @@ -344,13 +362,13 @@ dependencies = [ [[package]] name = "arrow-ipc" -version = "53.2.0" +version = "53.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ed91bdeaff5a1c00d28d8f73466bcb64d32bbd7093b5a30156b4b9f4dba3eee" +checksum = "c3527365b24372f9c948f16e53738eb098720eea2093ae73c7af04ac5e30a39b" dependencies = [ "arrow-array", "arrow-buffer", - "arrow-cast", + "arrow-cast 53.4.1 (registry+https://github.com/rust-lang/crates.io-index)", "arrow-data", "arrow-schema", "flatbuffers", @@ -359,13 +377,13 @@ dependencies = [ [[package]] name = "arrow-json" -version = "53.2.0" +version = "53.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0471f51260a5309307e5d409c9dc70aede1cd9cf1d4ff0f0a1e8e1a2dd0e0d3c" +checksum = "acdec0024749fc0d95e025c0b0266d78613727b3b3a5d4cf8ea47eb6d38afdd1" dependencies = [ "arrow-array", "arrow-buffer", - "arrow-cast", + "arrow-cast 53.4.1 (registry+https://github.com/rust-lang/crates.io-index)", "arrow-data", "arrow-schema", "chrono", @@ -379,24 +397,24 @@ dependencies = [ [[package]] name = "arrow-ord" -version = "53.2.0" +version = "53.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2883d7035e0b600fb4c30ce1e50e66e53d8656aa729f2bfa4b51d359cf3ded52" +checksum = "79af2db0e62a508d34ddf4f76bfd6109b6ecc845257c9cba6f939653668f89ac" dependencies = [ "arrow-array", "arrow-buffer", "arrow-data", "arrow-schema", - "arrow-select", + "arrow-select 53.4.1 (registry+https://github.com/rust-lang/crates.io-index)", "half", "num", ] [[package]] name = "arrow-row" -version = "53.2.0" +version = "53.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "552907e8e587a6fde4f8843fd7a27a576a260f65dab6c065741ea79f633fc5be" +checksum = "da30e9d10e9c52f09ea0cf15086d6d785c11ae8dcc3ea5f16d402221b6ac7735" dependencies = [ "ahash", "arrow-array", @@ -408,17 +426,30 @@ dependencies = [ [[package]] name = "arrow-schema" -version = "53.2.0" -source = "git+https://github.com/apache/arrow-rs.git?tag=53.2.0#10c4059b40f838bb8f7bac5259cb499e6eceec88" +version = "53.4.1" +source = "git+https://github.com/apache/arrow-rs.git?tag=53.4.1#962558546b9f441b5b7d039624d068c36a3ef69e" dependencies = [ "bitflags 2.9.0", ] [[package]] name = "arrow-select" -version = "53.2.0" +version = "53.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6259e566b752da6dceab91766ed8b2e67bf6270eb9ad8a6e07a33c1bede2b125" +checksum = "92fc337f01635218493c23da81a364daf38c694b05fc20569c3193c11c561984" +dependencies = [ + "ahash", + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "num", +] + +[[package]] +name = "arrow-select" +version = "53.4.1" +source = "git+https://github.com/apache/arrow-rs.git?tag=53.4.1#962558546b9f441b5b7d039624d068c36a3ef69e" dependencies = [ "ahash", "arrow-array", @@ -430,15 +461,15 @@ dependencies = [ [[package]] name = "arrow-string" -version = "53.2.0" +version = "53.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3179ccbd18ebf04277a095ba7321b93fd1f774f18816bd5f6b3ce2f594edb6c" +checksum = "d596a9fc25dae556672d5069b090331aca8acb93cae426d8b7dcdf1c558fa0ce" dependencies = [ "arrow-array", "arrow-buffer", "arrow-data", "arrow-schema", - "arrow-select", + "arrow-select 53.4.1 (registry+https://github.com/rust-lang/crates.io-index)", "memchr", "num", "regex", @@ -837,6 +868,18 @@ dependencies = [ "crunchy", ] +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "blake2" version = "0.10.6" @@ -868,6 +911,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "boxcar" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66bb12751a83493ef4b8da1120451a262554e216a247f14b48cb5e8fe7ed8bdf" + [[package]] name = "brotli" version = "6.0.0" @@ -1904,6 +1953,47 @@ dependencies = [ "strum", ] +[[package]] +name = "db4-common" +version = "0.15.1" +dependencies = [ + "arrow-schema", + "parquet", + "raphtory", + "serde_json", + "thiserror 2.0.12", +] + +[[package]] +name = "db4-storage" +version = "0.15.1" +dependencies = [ + "arrow", + "arrow-array", + "arrow-csv", + "arrow-schema", + "bigdecimal", + "bitvec", + "boxcar", + "bytemuck", + "chrono", + "db4-common", + "either", + "iter-enum", + "itertools 0.13.0", + "parking_lot", + "parquet", + "polars-arrow", + "proptest", + "raphtory", + "raphtory-api", + "rayon", + "rustc-hash 2.1.1", + "serde", + "serde_json", + "tempfile", +] + [[package]] name = "deadpool" version = "0.9.5" @@ -2311,6 +2401,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.31" @@ -4031,18 +4127,18 @@ dependencies = [ [[package]] name = "parquet" -version = "53.2.0" +version = "53.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dea02606ba6f5e856561d8d507dba8bac060aefca2a6c0f1aa1d361fed91ff3e" +checksum = "2f8cf58b29782a7add991f655ff42929e31a7859f5319e53db9e39a714cb113c" dependencies = [ "ahash", "arrow-array", "arrow-buffer", - "arrow-cast", + "arrow-cast 53.4.1 (registry+https://github.com/rust-lang/crates.io-index)", "arrow-data", "arrow-ipc", "arrow-schema", - "arrow-select", + "arrow-select 53.4.1 (registry+https://github.com/rust-lang/crates.io-index)", "base64 0.22.1", "brotli 7.0.0", "bytes", @@ -4050,7 +4146,7 @@ dependencies = [ "flate2", "futures", "half", - "hashbrown 0.14.5", + "hashbrown 0.15.3", "lz4_flex", "num", "num-bigint", @@ -4929,6 +5025,12 @@ version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.8.5" @@ -6339,6 +6441,12 @@ dependencies = [ "serde", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "target-features" version = "0.1.6" @@ -7513,6 +7621,15 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "xxhash-rust" version = "0.8.15" diff --git a/Cargo.toml b/Cargo.toml index bf7a2d3635..a467fc8a14 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,8 +11,10 @@ members = [ "raphtory-api", "raphtory-core", "raphtory-storage", + "db4-common", + "db4-storage", ] -default-members = ["raphtory"] +default-members = ["raphtory", "db4-common", "db4-storage"] resolver = "2" [workspace.package] @@ -52,9 +54,11 @@ incremental = false #[public-storage] pometry-storage = { version = ">=0.8.1", path = "pometry-storage" } #[private-storage] -# pometry-storage = { path = "pometry-storage-private", package = "pometry-storage-private" } +#pometry-storage = { path = "pometry-storage-private", package = "pometry-storage-private" } async-graphql = { version = "7.0.16", features = ["dynamic-schema"] } bincode = "1.3.3" +bitvec = "1.0.1" +boxcar = "0.2.8" async-graphql-poem = "7.0.16" dynamic-graphql = "0.10.1" reqwest = { version = "0.12.8", default-features = false, features = [ @@ -158,22 +162,30 @@ heed = "0.22.0" sysinfo = "0.35.1" sqlparser = "0.51.0" futures = "0.3" -arrow = { version = "=53.2.0" } -parquet = { version = "=53.2.0" } -arrow-json = { version = "=53.2.0" } -arrow-buffer = { version = "=53.2.0" } -arrow-schema = { version = "=53.2.0" } -arrow-array = { version = "=53.2.0" } -arrow-ipc = { version = "=53.2.0" } +arrow = { version = "=53.4.1" } +parquet = { version = "=53.4.1" } +arrow-json = { version = "=53.4.1" } +arrow-buffer = { version = "=53.4.1" } +arrow-schema = { version = "=53.4.1" } +arrow-array = { version = "=53.4.1" } +arrow-ipc = { version = "=53.4.1" } +arrow-csv = { version = "=53.4.1" } + moka = { version = "0.12.7", features = ["sync"] } indexmap = { version = "2.7.0", features = ["rayon"] } fake = { version = "3.1.0", features = ["chrono"] } strsim = { version = "0.11.1" } uuid = { version = "1.16.0", features = ["v4"] } +raphtory ={ version = "0.15.1", path = "./raphtory", default-features = false} +raphtory-api ={ version = "0.15.1", path = "./raphtory-api", default-features = false} +#raphtory-core ={ version = "0.15.1", path = "./raphtory-core", default-features = false} +raphtory-graphql ={ version = "0.15.1", path = "./raphtory-graphql", default-features = false} + # Make sure that transitive dependencies stick to disk_graph 50 [patch.crates-io] -arrow-buffer = { git = "https://github.com/apache/arrow-rs.git", tag = "53.2.0" } -arrow-schema = { git = "https://github.com/apache/arrow-rs.git", tag = "53.2.0" } -arrow-data = { git = "https://github.com/apache/arrow-rs.git", tag = "53.2.0" } -arrow-array = { git = "https://github.com/apache/arrow-rs.git", tag = "53.2.0" } +arrow-buffer = { git = "https://github.com/apache/arrow-rs.git", tag = "53.4.1" } +arrow-schema = { git = "https://github.com/apache/arrow-rs.git", tag = "53.4.1" } +arrow-data = { git = "https://github.com/apache/arrow-rs.git", tag = "53.4.1" } +arrow-array = { git = "https://github.com/apache/arrow-rs.git", tag = "53.4.1" } +arrow-csv = { git = "https://github.com/apache/arrow-rs.git", tag = "53.4.1" } diff --git a/db4-common/Cargo.toml b/db4-common/Cargo.toml new file mode 100644 index 0000000000..b342de445a --- /dev/null +++ b/db4-common/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "db4-common" +version.workspace = true +documentation.workspace = true +repository.workspace = true +readme.workspace = true +homepage.workspace = true +keywords.workspace = true +authors.workspace = true +rust-version.workspace = true +edition = "2024" + +[dependencies] +raphtory.workspace = true +thiserror.workspace = true +serde_json.workspace = true +arrow-schema.workspace = true +parquet.workspace = true diff --git a/db4-common/src/lib.rs b/db4-common/src/lib.rs new file mode 100644 index 0000000000..f73b972e98 --- /dev/null +++ b/db4-common/src/lib.rs @@ -0,0 +1,78 @@ +use std::path::Path; + +use raphtory::core::entities::{EID, VID}; + +pub mod error { + use std::{path::PathBuf, sync::Arc}; + + use raphtory::{ + api::core::entities::properties::prop::PropError, core::utils::time::ParseTimeError, + errors::LoadError, + }; + + #[derive(thiserror::Error, Debug)] + pub enum DBV4Error { + #[error("External Storage Error {0}")] + External(#[from] Arc), + #[error("IO error: {0}")] + IO(#[from] std::io::Error), + #[error("Serde error: {0}")] + Serde(#[from] serde_json::Error), + #[error("Load error: {0}")] + LoadError(#[from] LoadError), + #[error("Arrow-rs error: {0}")] + ArrowRS(#[from] arrow_schema::ArrowError), + #[error("Parquet error: {0}")] + Parquet(#[from] parquet::errors::ParquetError), + + #[error("Property error: {0}")] + PropError(#[from] PropError), + #[error("Empty Graph: {0}")] + EmptyGraphDir(PathBuf), + #[error("Failed to parse time string")] + ParseTime { + #[from] + source: ParseTimeError, + }, + #[error("Unnamed Failure: {0}")] + GenericFailure(String), + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[repr(transparent)] +pub struct LocalPOS(pub usize); + +impl LocalPOS { + pub fn as_vid(self, page_id: usize, max_page_len: usize) -> VID { + VID(page_id * max_page_len + self.0) + } + + pub fn as_eid(self, page_id: usize, max_page_len: usize) -> EID { + EID(page_id * max_page_len + self.0) + } +} + +impl From for LocalPOS { + fn from(pos: usize) -> Self { + Self(pos) + } +} + +pub fn calculate_size_recursive(path: &Path) -> Result { + let mut size = 0; + if path.is_dir() { + for entry in std::fs::read_dir(path)? { + let entry = entry?; + let path = entry.path(); + if path.is_dir() { + size += calculate_size_recursive(&path)?; + } else { + size += path.metadata()?.len() as usize; + } + } + } else { + size += path.metadata()?.len() as usize; + } + Ok(size) +} diff --git a/db4-storage/Cargo.toml b/db4-storage/Cargo.toml new file mode 100644 index 0000000000..846b569ab8 --- /dev/null +++ b/db4-storage/Cargo.toml @@ -0,0 +1,50 @@ +[package] +name = "db4-storage" +version.workspace = true +documentation.workspace = true +repository.workspace = true +readme.workspace = true +homepage.workspace = true +keywords.workspace = true +authors.workspace = true +rust-version.workspace = true +edition = "2024" + +[dependencies] +raphtory-api.workspace = true +raphtory = {workspace = true, features = ["arrow", "io"]} +db4-common = {path = "../db4-common"} + +bitvec.workspace = true +bigdecimal.workspace = true +polars-arrow.workspace = true +rustc-hash.workspace = true +either.workspace = true +parking_lot.workspace = true +serde.workspace = true +boxcar.workspace = true +serde_json.workspace = true +arrow.workspace = true +arrow-array.workspace = true +arrow-csv.workspace = true +arrow-schema.workspace = true +parquet.workspace = true +bytemuck.workspace = true +rayon.workspace = true +iter-enum = {workspace = true, features = ["rayon"]} + +proptest = {workspace = true, optional = true} +tempfile = {workspace = true, optional = true} +itertools = {workspace = true, optional = true} +chrono = {workspace = true, optional = true} + +[dev-dependencies] +proptest.workspace = true +tempfile.workspace = true +itertools.workspace = true +chrono.workspace = true +rayon.workspace = true + +[features] +test-utils = ["proptest", "tempfile", "itertools", "chrono"] +default = ["test-utils"] \ No newline at end of file diff --git a/db4-storage/src/lib.rs b/db4-storage/src/lib.rs new file mode 100644 index 0000000000..6a3f80b347 --- /dev/null +++ b/db4-storage/src/lib.rs @@ -0,0 +1,236 @@ +use std::{ + ops::{Deref, DerefMut}, + path::Path, + sync::Arc, +}; + +use crate::{ + pages::GraphStore, + segments::{edge::EdgeSegmentView, node::NodeSegmentView}, +}; +use db4_common::{LocalPOS, error::DBV4Error}; +use parking_lot::{RwLockReadGuard, RwLockWriteGuard}; +use raphtory::{ + core::entities::{EID, VID}, + prelude::Prop, +}; +use raphtory_api::core::{ + entities::properties::{meta::Meta, tprop::TPropOps}, + storage::timeindex::{TimeIndexEntry, TimeIndexOps}, +}; +use segments::{edge::MemEdgeSegment, node::MemNodeSegment}; + +pub mod loaders; +pub mod pages; +pub mod properties; +pub mod segments; + +pub type Layer = GraphStore; + +pub trait EdgeSegmentOps: Send + Sync { + type Extension; + + type Entry<'a>: EdgeEntryOps<'a> + where + Self: 'a; + + fn latest(&self) -> Option; + fn earliest(&self) -> Option; + + fn t_len(&self) -> usize; + + fn load( + page_id: usize, + max_page_len: usize, + meta: Arc, + path: impl AsRef, + ext: Self::Extension, + ) -> Result + where + Self: Sized; + + fn new( + page_id: usize, + max_page_len: usize, + meta: Arc, + path: impl AsRef, + ext: Self::Extension, + ) -> Self; + + fn segment_id(&self) -> usize; + + fn num_edges(&self) -> usize; + + fn head(&self) -> RwLockReadGuard; + + fn head_mut(&self) -> RwLockWriteGuard; + + fn try_head_mut(&self) -> Option>; + + fn notify_write( + &self, + head_lock: impl DerefMut, + ) -> Result<(), DBV4Error>; + + fn increment_num_edges(&self) -> usize; + + fn contains_edge( + &self, + edge_pos: LocalPOS, + locked_head: impl Deref, + ) -> bool; + + fn get_edge( + &self, + edge_pos: LocalPOS, + locked_head: impl Deref, + ) -> Option<(VID, VID)>; + + fn entry<'a, LP: Into>(&'a self, edge_pos: LP) -> Self::Entry<'a>; +} + +pub trait EdgeEntryOps<'a> { + type Ref<'b>: EdgeRefOps<'b> + where + 'a: 'b, + Self: 'b; + + fn as_ref<'b>(&'b self) -> Self::Ref<'b> + where + 'a: 'b; +} + +pub trait EdgeRefOps<'a>: Copy + Clone + Send + Sync { + type Additions: TimeIndexOps<'a>; + type TProps: TPropOps<'a>; + + fn edge(self) -> Option<(VID, VID)>; + + fn additions(self) -> Self::Additions; + + fn c_prop(self, prop_id: usize) -> Option; + + fn t_prop(self, prop_id: usize) -> Self::TProps; +} + +pub trait NodeSegmentOps: Send + Sync { + type Extension; + + type Entry<'a>: NodeEntryOps<'a> + where + Self: 'a; + + fn latest(&self) -> Option; + fn earliest(&self) -> Option; + + fn t_len(&self) -> usize; + + fn load( + page_id: usize, + max_page_len: usize, + meta: Arc, + path: impl AsRef, + ext: Self::Extension, + ) -> Result + where + Self: Sized; + fn new( + page_id: usize, + max_page_len: usize, + meta: Arc, + path: impl AsRef, + ext: Self::Extension, + ) -> Self; + + fn segment_id(&self) -> usize; + + fn head(&self) -> RwLockReadGuard; + + fn head_mut(&self) -> RwLockWriteGuard; + + fn num_nodes(&self) -> usize; + + fn increment_num_nodes(&self) -> usize; + + fn notify_write( + &self, + head_lock: impl DerefMut, + ) -> Result<(), DBV4Error>; + + fn check_node(&self, pos: LocalPOS) -> bool; + + fn get_out_edge( + &self, + pos: LocalPOS, + dst: impl Into, + locked_head: impl Deref, + ) -> Option; + + fn get_inb_edge( + &self, + pos: LocalPOS, + src: impl Into, + locked_head: impl Deref, + ) -> Option; + + fn entry<'a>(&'a self, pos: impl Into) -> Self::Entry<'a>; +} + +pub trait NodeEntryOps<'a> { + type Ref<'b>: NodeRefOps<'b> + where + 'a: 'b, + Self: 'b; + + fn as_ref<'b>(&'b self) -> Self::Ref<'b> + where + 'a: 'b; +} + +pub trait NodeRefOps<'a>: Copy + Clone + Send + Sync { + type Additions: TimeIndexOps<'a>; + + type TProps: TPropOps<'a>; + + fn out_edges(self) -> impl Iterator + 'a; + + fn inb_edges(self) -> impl Iterator + 'a; + + fn out_edges_sorted(self) -> impl Iterator + 'a; + + fn inb_edges_sorted(self) -> impl Iterator + 'a; + + fn out_nbrs(self) -> impl Iterator + 'a + where + Self: Sized, + { + self.out_edges().map(|(v, _)| v) + } + + fn inb_nbrs(self) -> impl Iterator + 'a + where + Self: Sized, + { + self.inb_edges().map(|(v, _)| v) + } + + fn out_nbrs_sorted(self) -> impl Iterator + 'a + where + Self: Sized, + { + self.out_edges_sorted().map(|(v, _)| v) + } + + fn inb_nbrs_sorted(self) -> impl Iterator + 'a + where + Self: Sized, + { + self.inb_edges_sorted().map(|(v, _)| v) + } + + fn additions(self) -> Self::Additions; + + fn c_prop(self, prop_id: usize) -> Option; + + fn t_prop(self, prop_id: usize) -> Self::TProps; +} diff --git a/db4-storage/src/loaders/mod.rs b/db4-storage/src/loaders/mod.rs new file mode 100644 index 0000000000..e2f928dead --- /dev/null +++ b/db4-storage/src/loaders/mod.rs @@ -0,0 +1,534 @@ +use crate::{EdgeSegmentOps, NodeSegmentOps, pages::GraphStore}; +use arrow::buffer::ScalarBuffer; +use arrow_array::{ + Array, PrimitiveArray, RecordBatch, TimestampMicrosecondArray, TimestampMillisecondArray, + TimestampNanosecondArray, types::Int64Type, +}; +use arrow_csv::reader::Format; +use arrow_schema::{ArrowError, DataType, Schema, TimeUnit}; +use bytemuck::checked::cast_slice_mut; +use db4_common::error::DBV4Error; +use either::Either; +use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder; +use raphtory::{ + atomic_extra::atomic_usize_from_mut_slice, + core::entities::{EID, VID, graph::logical_to_physical::Mapping}, + errors::LoadError, + io::arrow::{ + node_col::NodeCol, + prop_handler::{PropCols, combine_properties_arrow}, + }, +}; +use raphtory_api::core::{ + entities::properties::prop::PropType, + storage::{dict_mapper::MaybeNew, timeindex::TimeIndexEntry}, +}; +use rayon::prelude::*; +use std::{ + fs::File, + path::{Path, PathBuf}, + sync::{ + Arc, + atomic::{self, AtomicBool, AtomicUsize}, + }, +}; + +pub struct Loader<'a> { + path: PathBuf, + src_col: Either<&'a str, usize>, + dst_col: Either<&'a str, usize>, + time_col: Either<&'a str, usize>, + format: FileFormat, +} + +pub enum FileFormat { + CSV { + delimiter: u8, + has_header: bool, + sample_records: usize, + }, + Parquet, +} + +pub struct Rows { + rb: RecordBatch, + src: usize, + dst: usize, + t_properties: Vec, + t_indices: Vec, + time_col: ScalarBuffer, +} + +impl Rows { + pub fn srcs(&self) -> Result { + let arr = self.rb.column(self.src); + let arr = arr.as_ref(); + let srcs = NodeCol::try_from(arr)?; + Ok(srcs) + } + + pub fn dsts(&self) -> Result { + let arr = self.rb.column(self.dst); + let arr = arr.as_ref(); + let dsts = NodeCol::try_from(arr)?; + Ok(dsts) + } + + pub fn time(&self) -> &[i64] { + &self.time_col + } + + pub fn properties( + &self, + prop_id_resolver: impl Fn(&str, PropType) -> Result, DBV4Error>, + ) -> Result { + combine_properties_arrow( + &self.t_properties, + &self.t_indices, + self.rb.columns(), + prop_id_resolver, + ) + } + + fn new(rb: RecordBatch, src: usize, dst: usize, time: usize) -> Result { + let (t_indices, t_properties): (Vec<_>, Vec<_>) = rb + .schema() + .fields() + .iter() + .enumerate() + .filter_map(|(id, f)| { + if id == src || id == dst || id == time { + None + } else { + Some((id, f.name().to_owned())) + } + }) + .unzip(); + + let time_arr = rb.column(time); + let values = if let Some(arr) = time_arr + .as_any() + .downcast_ref::>() + { + arr.values().clone() + } else if let Some(arr) = time_arr.as_any().downcast_ref::() { + let arr_to_millis = + arrow::compute::cast(&arr, &DataType::Timestamp(TimeUnit::Millisecond, None))?; + let arr = arr_to_millis + .as_any() + .downcast_ref::() + .unwrap(); + arr.values().clone() + } else if let Some(arr) = time_arr + .as_any() + .downcast_ref::() + { + let arr_to_millis = + arrow::compute::cast(&arr, &DataType::Timestamp(TimeUnit::Millisecond, None))?; + let arr = arr_to_millis + .as_any() + .downcast_ref::() + .unwrap(); + arr.values().clone() + } else if let Some(arr) = time_arr + .as_any() + .downcast_ref::() + { + arr.values().clone() + } else { + return Err(DBV4Error::ArrowRS(ArrowError::CastError(format!( + "failed to cast time column {} to i64", + time_arr.data_type() + )))); + }; + + Ok(Self { + rb, + src, + dst, + t_indices, + t_properties, + time_col: values, + }) + } + + fn num_rows(&self) -> usize { + self.rb.num_rows() + } +} + +impl<'a> Loader<'a> { + pub fn new( + path: &Path, + src_col: Either<&'a str, usize>, + dst_col: Either<&'a str, usize>, + time_col: Either<&'a str, usize>, + format: FileFormat, + ) -> Result { + Ok(Self { + path: path.to_owned(), + src_col, + dst_col, + time_col, + format, + }) + } + + pub fn iter_file( + &self, + path: &Path, + rows_per_batch: usize, + ) -> Result> + Send>, DBV4Error> { + match &self.format { + FileFormat::CSV { + delimiter, + has_header, + sample_records, + } => { + let file = File::open(path).unwrap(); + let (schema, _) = Format::default() + .with_header(*has_header) + .with_delimiter(*delimiter) + .infer_schema(file, Some(*sample_records))?; + let schema = Arc::new(schema); + + let (src, dst, time) = self.src_dst_time_cols(&schema)?; + + let file = File::open(path)?; + + let reader = arrow_csv::reader::ReaderBuilder::new(schema.clone()) + .with_header(*has_header) + .with_delimiter(*delimiter) + .with_batch_size(rows_per_batch) + .build(file)?; + Ok(Box::new(reader.map(move |rb| { + rb.map_err(DBV4Error::from) + .and_then(|rb| Rows::new(rb, src, dst, time)) + }))) + } + FileFormat::Parquet => { + let file = File::open(path)?; + let builder = + ParquetRecordBatchReaderBuilder::try_new(file)?.with_batch_size(rows_per_batch); + + let (src, dst, time) = self.src_dst_time_cols(&builder.schema())?; + let reader = builder.build()?; + Ok(Box::new(reader.map(move |rb| { + rb.map_err(DBV4Error::from) + .and_then(|rb| Rows::new(rb, src, dst, time)) + }))) + } + } + } + + pub fn iter( + &self, + rows_per_batch: usize, + ) -> Result> + Send>, DBV4Error> { + if self.path.is_dir() { + let mut files = vec![]; + for entry in std::fs::read_dir(&self.path)? { + let entry = entry?; + if entry.file_type()?.is_file() { + files.push(entry.path()); + } + } + let iterators: Vec<_> = files + .into_iter() + .map(|path| self.iter_file(&path, rows_per_batch)) + .collect::, _>>()?; + Ok(Box::new(iterators.into_iter().flatten())) + } else { + Ok(self.iter_file(&self.path, rows_per_batch)?) + } + } + + fn src_dst_time_cols(&self, schema: &Schema) -> Result<(usize, usize, usize), DBV4Error> { + let src_field = match self.src_col { + Either::Left(name) => schema.index_of(name)?, + Either::Right(idx) => idx, + }; + let dst_field = match self.dst_col { + Either::Left(name) => schema.index_of(name)?, + Either::Right(idx) => idx, + }; + + let time_field = match self.time_col { + Either::Left(name) => schema.index_of(name)?, + Either::Right(idx) => idx, + }; + + Ok((src_field, dst_field, time_field)) + } + + pub fn load_into< + NS: NodeSegmentOps, + ES: EdgeSegmentOps, + EXT: Clone + Default + Send + Sync, + >( + &self, + graph: &GraphStore, + rows_per_batch: usize, + ) -> Result { + let mut src_col_resolved: Vec = vec![]; + let mut dst_col_resolved: Vec = vec![]; + let mut eid_col_resolved: Vec = vec![]; + let mut eids_exist: Vec = vec![]; // exists or needs to be created + + let max_edge_id = AtomicUsize::new(graph.edges().num_edges().saturating_sub(1)); + + let resolver = Mapping::new(); + + let next_id = AtomicUsize::new(0); + let mut offset = 0; + + let now = std::time::Instant::now(); + for chunk in self.iter(rows_per_batch)? { + let now_chunk = std::time::Instant::now(); + let rb = chunk?; + + let props = rb.properties(|name, p_type| { + graph + .edge_meta() + .resolve_prop_id(name, p_type, false) + .map_err(DBV4Error::from) + })?; + + let srcs = rb.srcs()?; + let dsts = rb.dsts()?; + + src_col_resolved.resize_with(rb.num_rows(), Default::default); + srcs.par_iter() + .zip(src_col_resolved.par_iter_mut()) + .try_for_each(|(gid, resolved)| { + let gid = gid.ok_or_else(|| LoadError::MissingSrcError)?; + let id = resolver + .get_or_init(gid, || VID(next_id.fetch_add(1, atomic::Ordering::Relaxed))) + .unwrap() + .inner(); + *resolved = id; + Ok::<(), DBV4Error>(()) + })?; + + dst_col_resolved.resize_with(rb.num_rows(), Default::default); + dsts.par_iter() + .zip(dst_col_resolved.par_iter_mut()) + .try_for_each(|(gid, resolved)| { + let gid = gid.ok_or_else(|| LoadError::MissingDstError)?; + let id = resolver + .get_or_init(gid, || VID(next_id.fetch_add(1, atomic::Ordering::Relaxed))) + .unwrap() + .inner(); + *resolved = id; + Ok::<(), DBV4Error>(()) + })?; + + eid_col_resolved.resize_with(rb.num_rows(), Default::default); + eids_exist.resize_with(rb.num_rows(), Default::default); + let eid_col_shared = atomic_usize_from_mut_slice(cast_slice_mut(&mut eid_col_resolved)); + + let num_pages = + next_id.load(atomic::Ordering::Relaxed) / graph.nodes().max_page_len() + 1; + graph.nodes().grow(num_pages); + + let mut node_writers = graph.nodes().locked(); + + node_writers.par_iter_mut().try_for_each(|locked_page| { + for (row, (&src, &dst)) in src_col_resolved + .iter() + .zip(dst_col_resolved.iter()) + .enumerate() + { + if let Some(src_pos) = locked_page.resolve_pos(src) { + let mut writer = locked_page.writer(); + if let Some(edge_id) = writer.get_out_edge(src_pos, dst) { + eid_col_shared[row].store(edge_id.0, atomic::Ordering::Relaxed); + eids_exist[row].store(true, atomic::Ordering::Relaxed); + } else { + let edge_id = EID(max_edge_id.fetch_add(1, atomic::Ordering::Relaxed)); + writer.add_outbound_edge(0, src_pos, dst, edge_id.with_layer(0), 0); // FIXME: when we update this to work with layers use the correct layer + eid_col_shared[row].store(edge_id.0, atomic::Ordering::Relaxed); + eids_exist[row].store(false, atomic::Ordering::Relaxed); + } + } + } + + Ok::<_, DBV4Error>(()) + })?; + + node_writers.par_iter_mut().try_for_each(|locked_page| { + for (&edge_id, (&src, &dst)) in eid_col_resolved + .iter() + .zip(src_col_resolved.iter().zip(&dst_col_resolved)) + { + if let Some(dst_pos) = locked_page.resolve_pos(dst) { + let mut writer = locked_page.writer(); + if !writer.get_inb_edge(dst_pos, src).is_some() { + let edge_id = EID(edge_id.0); + writer.add_inbound_edge(0, dst_pos, src, edge_id.with_layer(0), 0); // FIXME: when we update this to work with layers use the correct layer + } + } + } + + Ok::<_, DBV4Error>(()) + })?; + + // now edges + + let num_pages = + max_edge_id.load(atomic::Ordering::Relaxed) / graph.edges().max_page_len() + 1; + + graph.edges().grow(num_pages); + + let mut edge_writers = graph.edges().locked(); + + let time_col = rb.time(); + + edge_writers.iter_mut().try_for_each(|edge_writer| { + for (row_idx, ((((&src, &dst), &eid), edge_exists), time)) in src_col_resolved + .iter() + .zip(&dst_col_resolved) + .zip(&eid_col_resolved) + .zip( + eids_exist + .iter() + .map(|exists| exists.load(atomic::Ordering::Relaxed)), + ) + .zip(time_col) + .enumerate() + { + if let Some(local_pos) = edge_writer.resolve_pos(eid) { + let mut writer = edge_writer.writer(); + let time = TimeIndexEntry::new(*time, offset + row_idx); + writer.add_edge( + time, + Some(local_pos), + src, + dst, + props.iter_row(row_idx), + 0, + Some(edge_exists), + )?; + } + } + Ok::<_, DBV4Error>(()) + })?; + + src_col_resolved.clear(); + dst_col_resolved.clear(); + eid_col_resolved.clear(); + eids_exist.clear(); + offset += rb.num_rows(); + + // println!( + // "Loaded {} events in {:?}. Average {} events/s. Batch time: {:?}", + // offset, + // now.elapsed(), + // offset as f64 / now.elapsed().as_secs_f64(), + // now_chunk.elapsed(), + // ); + } + + Ok(resolver) + } +} + +#[cfg(test)] +mod test { + use crate::{Layer, pages::test_utils::check_load_support}; + use proptest::{collection::vec, prelude::*}; + + fn check_load(edges: &[(i64, u64, u64)], max_page_len: usize) { + check_load_support(edges, false, |path| { + Layer::<()>::new(path, max_page_len, max_page_len) + }); + } + + #[test] + fn test_one_edge() { + check_load(&[(0, 0, 1)], 32); + } + + #[test] + fn test_load_graph_from_csv() { + let edge_strat = (1u64..100).prop_flat_map(|num_nodes| { + (1usize..100).prop_flat_map(move |num_edges| { + vec(((0i64..100), (0..num_nodes), (0..num_nodes)), num_edges) + }) + }); + + proptest!(|(edges in edge_strat, max_page_len in 1usize .. 100)| { + check_load(&edges, max_page_len); + }); + } + + #[test] + fn teas_load_graph_from_csv_5() { + let edges = [ + (42, 16, 24), + (96, 41, 8), + (37, 9, 9), + (62, 37, 57), + (12, 49, 23), + (8, 60, 44), + (56, 35, 0), + (9, 48, 58), + (59, 20, 37), + (36, 17, 46), + ]; + let max_page_len = 7; + check_load(&edges, max_page_len); + } + + #[test] + fn test_load_graph_from_csv_4() { + let edges = [ + (27, 20, 85), + (2, 29, 77), + (55, 59, 22), + (72, 47, 73), + (26, 66, 36), + (22, 39, 37), + (5, 49, 88), + (2, 48, 13), + (97, 23, 57), + ]; + let max_page_len = 8; + check_load(&edges, max_page_len); + } + + #[test] + fn test_load_graph_from_csv_1() { + let edges = [(0, 33, 31), (1, 12, 20), (2, 22, 32)]; + + check_load(&edges, 32); + } + + #[test] + fn test_load_graph_from_csv_2() { + let edges = [ + (0, 23, 61), + (1, 52, 14), + (2, 62, 62), + (3, 13, 9), + (4, 29, 6), + (5, 13, 7), + ]; + + check_load(&edges, 5); + } + + #[test] + fn test_load_graph_from_csv_3() { + let edges = [(0, 0, 32)]; + + check_load(&edges, 51); + } + + #[test] + fn test_edges_1() { + let edges = [(0, 1, 0), (0, 0, 0), (0, 0, 0)]; + + check_load(&edges, 32); + } +} diff --git a/db4-storage/src/pages/edge_page/edge_entry.rs b/db4-storage/src/pages/edge_page/edge_entry.rs new file mode 100644 index 0000000000..fd0af23621 --- /dev/null +++ b/db4-storage/src/pages/edge_page/edge_entry.rs @@ -0,0 +1,235 @@ +use std::{ + ops::{Deref, Range}, + sync::Arc, +}; + +use crate::{avoid_k_merge_with_iterators, LocalPOS}; +use arc_swap::Guard; +use parking_lot::RwLockReadGuard; +use raphtory::{ + core::storage::timeindex::{TimeIndexEntry, TimeIndexIntoOps, TimeIndexOps}, + db::api::storage::graph::tprop_storage_ops::TPropOps, + prelude::Prop, +}; +use raphtory_api::core::entities::{EID, VID}; +use raphtory_api::iter::BoxedLIter; + +use super::{ + edge_page_view::{EdgePageView, TProp, TimeCell}, + frozen_pages::{DiskEdgeSegments, ExactSizeChain}, + mem_edge_page::MemEdgeSegment, + EdgeSegment, +}; + +#[derive(Debug, Clone, Copy)] +pub struct EdgeEntry<'a, MP: 'a, DP: 'a> { + edge_pos: LocalPOS, + mem_seg: Option, + disk_pages: DP, + seg: &'a EdgeSegment, +} + +pub type EdgeStorageRef<'a> = EdgeEntry<'a, &'a MemEdgeSegment, &'a DiskEdgeSegments>; + +impl<'a, MP: 'a, DP: 'a> EdgeEntry<'a, MP, DP> { + pub fn page_id(&self) -> usize { + self.seg.page_id() + } +} + +impl<'a> EdgeEntry<'a, RwLockReadGuard<'a, MemEdgeSegment>, Guard>> { + pub fn read(edge_pos: LocalPOS, page: &'a EdgeSegment) -> Self { + let mem_page = (!page.head_is_empty()).then(|| page.head.read()); + let disk_pages = page.immut(); + Self { + edge_pos, + mem_seg: mem_page, + disk_pages, + seg: page, + } + } + + pub fn as_ref(&'a self) -> EdgeStorageRef<'a> { + let disk_pages = self.disk_pages.deref().as_ref(); + EdgeStorageRef { + edge_pos: self.edge_pos, + mem_seg: self.mem_seg.as_deref(), + disk_pages, + seg: self.seg, + } + } +} + +impl<'a> EdgeStorageRef<'a> { + pub fn eid(self) -> EID { + self.edge_pos + .as_eid(self.page_id(), self.disk_pages.max_page_len()) + } + + pub fn edge(self) -> (VID, VID) { + let edge_pos = self.edge_pos; + self.mem_seg + .and_then(|mp| mp.get_edge(edge_pos)) + .or_else(|| self.disk_pages.get_edge(edge_pos)) + .expect("Internal error: edge not found") + } + + fn pages(self) -> impl ExactSizeIterator { + ExactSizeChain::new( + self.mem_seg.into_iter().map(|p| p as &dyn EdgePageView), + self.disk_pages.iter(), + ) + } + + pub fn additions(self) -> EdgeAdditions<'a> { + EdgeAdditions { + edge: self, + range: None, + } + } + + pub fn t_prop(self, prop_id: usize) -> EdgeTProps<'a> { + EdgeTProps { + edge: self, + prop_id, + } + } + + pub fn c_prop(self, prop_id: usize) -> Option { + self.mem_seg + .and_then(|mp| mp.as_ref().c_prop(self.edge_pos, prop_id)) + .or_else(|| self.disk_pages.c_prop(self.edge_pos, prop_id)) + } +} + +#[derive(Debug, Clone, Copy)] +pub struct EdgeAdditions<'a> { + range: Option<(TimeIndexEntry, TimeIndexEntry)>, + edge: EdgeStorageRef<'a>, +} + +impl<'a> EdgeAdditions<'a> { + pub fn time_cells(self) -> impl ExactSizeIterator> + 'a { + let edge_pos = self.edge.edge_pos; + self.edge + .pages() + .map(move |page| page.additions(edge_pos)) + .map(move |tc| match self.range { + Some((start, end)) => tc.into_range(start..end), + None => tc, + }) + } + + fn into_iter(self) -> impl Iterator + 'a { + let iters = self.time_cells(); + avoid_k_merge_with_iterators(iters, |t_cell| t_cell.into_iter(), |a, b| a < b) + } +} + +#[derive(Debug, Clone, Copy)] +pub struct EdgeTProps<'a> { + edge: EdgeStorageRef<'a>, + prop_id: usize, +} + +impl<'a> EdgeTProps<'a> { + fn tprops(self, prop_id: usize) -> impl ExactSizeIterator> + 'a { + let edge_pos = self.edge.edge_pos; + self.edge + .pages() + .map(move |page| page.t_prop(edge_pos, prop_id)) + } +} + +impl<'a> TPropOps<'a> for EdgeTProps<'a> { + fn last_before(&self, t: TimeIndexEntry) -> Option<(TimeIndexEntry, Prop)> { + self.tprops(self.prop_id) + .map(|t_props| t_props.last_before(t)) + .flatten() + .max_by_key(|(t, _)| *t) + } + + fn iter_inner( + self, + w: Option>, + ) -> impl Iterator + Send + Sync + 'a { + let w = w.map(|w| (w.start, w.end)); + avoid_k_merge_with_iterators( + self.tprops(self.prop_id), + move |t_cell| t_cell.iter_inner(w.map(|(start, end)| start..end)), + |a, b| a < b, + ) + } + + fn iter_inner_rev( + self, + range: Option>, + ) -> impl Iterator + Send + Sync + 'a { + let w = range.map(|r| (r.start, r.end)); + avoid_k_merge_with_iterators( + self.tprops(self.prop_id), + move |t_cell| t_cell.iter_inner_rev(w.map(|(start, end)| start..end)), + |a, b| a > b, + ) + } + + fn at(self, ti: &TimeIndexEntry) -> Option { + self.tprops(self.prop_id) + .flat_map(|t_props| t_props.at(ti)) + .next() //TODO: need to figure out how to handle this + } +} + +// FIXME: write some tests with windows, this is wrong in a the current state +impl TimeIndexOps for EdgeAdditions<'_> { + type IndexType = TimeIndexEntry; + + type RangeType<'b> + = EdgeAdditions<'b> + where + Self: 'b; + + fn active(&self, w: Range) -> bool { + self.time_cells().any(|tc| tc.active(w.clone())) + } + + fn range(&self, w: Range) -> Self::RangeType<'_> { + EdgeAdditions { + edge: self.edge, + range: Some((w.start, w.end)), + } + } + + fn first(&self) -> Option { + self.time_cells().filter_map(|tc| tc.first()).min() + } + + fn last(&self) -> Option { + self.time_cells().filter_map(|tc| tc.last()).max() + } + + fn iter(&self) -> BoxedLIter { + Box::new(self.into_iter()) + } + + fn len(&self) -> usize { + self.time_cells().map(|tc| tc.len()).sum() + } +} + +impl TimeIndexIntoOps for EdgeAdditions<'_> { + type IndexType = TimeIndexEntry; + + type RangeType = Self; + + fn into_range(self, w: Range) -> Self::RangeType { + EdgeAdditions { + edge: self.edge, + range: Some((w.start, w.end)), + } + } + + fn into_iter(self) -> impl Iterator + Send + Sync { + self.into_iter() + } +} diff --git a/db4-storage/src/pages/edge_page/edge_page_view.rs b/db4-storage/src/pages/edge_page/edge_page_view.rs new file mode 100644 index 0000000000..cbdba773fa --- /dev/null +++ b/db4-storage/src/pages/edge_page/edge_page_view.rs @@ -0,0 +1,73 @@ +use std::{ + cmp::{max, min}, + ops::Range, +}; + +use itertools::Itertools; +use raphtory::{ + core::{ + entities::{nodes::node_store::PropTimestamps, properties::tprop::TPropCell}, + storage::timeindex::{TimeIndexEntry, TimeIndexIntoOps, TimeIndexOps, TimeIndexWindow}, + }, + db::api::storage::graph::tprop_storage_ops::{TPropOps, TPropRef}, + prelude::Prop, +}; +use raphtory_api::iter::BoxedLIter; + +use crate::LocalPOS; + +pub trait EdgePageView { + fn additions(&self, edge_pos: LocalPOS) -> TimeCell; + fn t_prop(&self, edge_pos: LocalPOS, prop_id: usize) -> TProp; +} + +#[derive(Debug, Clone, Copy, Default)] +pub enum TProp<'a> { + #[default] + Empty, + Mem(TPropCell<'a>), + Disk(TPropRef<'a>), +} + +impl<'a> TPropOps<'a> for TProp<'a> { + fn last_before(&self, t: TimeIndexEntry) -> Option<(TimeIndexEntry, Prop)> { + match self { + TProp::Mem(cell) => cell.last_before(t), + TProp::Disk(cell) => cell.last_before(t), + TProp::Empty => None, + } + } + + fn iter_inner( + self, + range: Option>, + ) -> impl Iterator + Send + Sync + 'a { + let iter: BoxedLIter<_> = match self { + TProp::Mem(cell) => Box::new(cell.iter_inner(range)), + TProp::Disk(cell) => Box::new(cell.iter_inner(range)), + TProp::Empty => Box::new(std::iter::empty()), + }; + iter + } + + fn iter_inner_rev( + self, + range: Option>, + ) -> impl Iterator + Send + Sync + 'a { + let iter: BoxedLIter<_> = match self { + TProp::Mem(cell) => Box::new(cell.iter_inner_rev(range)), + TProp::Disk(cell) => Box::new(cell.iter_inner_rev(range)), + TProp::Empty => Box::new(std::iter::empty()), + }; + iter + } + + fn at(self, ti: &TimeIndexEntry) -> Option { + match self { + TProp::Mem(cell) => cell.at(ti), + TProp::Disk(cell) => cell.at(ti), + TProp::Empty => None, + } + } +} + diff --git a/db4-storage/src/pages/edge_page/mod.rs b/db4-storage/src/pages/edge_page/mod.rs new file mode 100644 index 0000000000..d3baa81782 --- /dev/null +++ b/db4-storage/src/pages/edge_page/mod.rs @@ -0,0 +1 @@ +pub mod writer; diff --git a/db4-storage/src/pages/edge_page/writer.rs b/db4-storage/src/pages/edge_page/writer.rs new file mode 100644 index 0000000000..106aefe975 --- /dev/null +++ b/db4-storage/src/pages/edge_page/writer.rs @@ -0,0 +1,116 @@ +use std::{ops::DerefMut, sync::atomic::AtomicUsize}; + +use crate::{EdgeSegmentOps, segments::edge::MemEdgeSegment}; +use db4_common::{LocalPOS, error::DBV4Error}; +use raphtory::{core::storage::timeindex::AsTime, prelude::Prop}; +use raphtory_api::core::entities::VID; + +pub struct EdgeWriter<'a, MP: DerefMut, ES: EdgeSegmentOps> { + pub page: &'a ES, + pub writer: MP, + pub global_num_edges: &'a AtomicUsize, +} + +impl<'a, MP: DerefMut, ES: EdgeSegmentOps> EdgeWriter<'a, MP, ES> { + pub fn new(global_num_edges: &'a AtomicUsize, page: &'a ES, writer: MP) -> Self { + Self { + page, + writer, + global_num_edges, + } + } + + fn new_local_pos(&self) -> LocalPOS { + let new_pos = LocalPOS(self.page.increment_num_edges()); + self.increment_global_num_edges(); + new_pos + } + + pub fn add_edge( + &mut self, + t: T, + edge_pos: Option, + src: impl Into, + dst: impl Into, + props: impl IntoIterator, + lsn: u64, + exists_hint: Option, // used when edge_pos is Some but the is not counted, this is used in the bulk loader + ) -> Result { + self.writer.as_mut().set_lsn(lsn); + + if exists_hint == Some(false) && edge_pos.is_some() { + self.new_local_pos(); // increment the counts, this is triggered from the bulk loader + } + + let edge_pos = edge_pos.unwrap_or_else(|| self.new_local_pos()); + self.writer + .insert_edge_internal(t, edge_pos, src, dst, props); + // self.est_size = self.page.increment_size(size_of::<(VID, VID)>()) + // + self.writer.as_ref().t_prop_est_size(); + Ok(edge_pos) + } + + pub fn add_static_edge( + &mut self, + edge_pos: Option, + src: impl Into, + dst: impl Into, + lsn: u64, + exists_hint: Option, // used when edge_pos is Some but the is not counted, this is used in the bulk loader + ) -> Result { + self.writer.as_mut().set_lsn(lsn); + + if exists_hint == Some(false) && edge_pos.is_some() { + self.new_local_pos(); // increment the counts, this is triggered from the bulk loader + } + + let edge_pos = edge_pos.unwrap_or_else(|| self.new_local_pos()); + self.writer.insert_static_edge_internal(edge_pos, src, dst); + // self.est_size = self.page.increment_size(size_of::<(VID, VID)>()) + // + self.writer.as_ref().t_prop_est_size(); + Ok(edge_pos) + } + + pub fn segment_id(&self) -> usize { + self.page.segment_id() + } + + fn increment_global_num_edges(&self) { + self.global_num_edges + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + } + + pub fn contains_edge(&self, pos: LocalPOS) -> bool { + // self.writer.contains_edge(pos) || self.page.disk_contains_edge(pos) + self.page.contains_edge(pos, self.writer.deref()) + } + + pub fn get_edge(&self, edge_pos: LocalPOS) -> Option<(VID, VID)> { + // self.writer + // .get_edge(edge_pos) + // .or_else(|| self.page.get_disk_edge(edge_pos)) + self.page.get_edge(edge_pos, self.writer.deref()) + } + + pub fn update_c_props( + &mut self, + edge_pos: LocalPOS, + src: impl Into, + dst: impl Into, + props: impl IntoIterator, + ) { + // self.page.increment_size(size_of::<(VID, VID)>()); + self.writer + .update_const_properties(edge_pos, src, dst, props); + } +} + +impl<'a, MP: DerefMut, ES: EdgeSegmentOps> Drop + for EdgeWriter<'a, MP, ES> +{ + fn drop(&mut self) { + if let Err(err) = self.page.notify_write(self.writer.deref_mut()) { + println!("Failed to persist {}, err: {}", self.segment_id(), err) + } + } +} diff --git a/db4-storage/src/pages/edge_store.rs b/db4-storage/src/pages/edge_store.rs new file mode 100644 index 0000000000..a0aec33c27 --- /dev/null +++ b/db4-storage/src/pages/edge_store.rs @@ -0,0 +1,351 @@ +use std::{ + collections::HashMap, + path::{Path, PathBuf}, + sync::{ + Arc, + atomic::{self, AtomicUsize}, + }, +}; + +use super::{edge_page::writer::EdgeWriter, resolve_pos}; +use crate::{ + EdgeSegmentOps, + pages::locked::edges::{LockedEdgePage, WriteLockedEdgePages}, + segments::edge::MemEdgeSegment, +}; +use db4_common::{LocalPOS, error::DBV4Error}; +use parking_lot::{RwLock, RwLockWriteGuard}; +use raphtory::core::storage::timeindex::TimeIndexEntry; +use raphtory_api::core::entities::{EID, VID, properties::meta::Meta}; + +const N: usize = 32; + +#[derive(Debug)] +pub struct EdgeStorageInner { + pages: boxcar::Vec>, + num_edges: AtomicUsize, + free_pages: Box<[RwLock; N]>, + edges_path: PathBuf, + max_page_len: usize, + prop_meta: Arc, + ext: EXT, +} + +impl, EXT: Clone> EdgeStorageInner { + pub fn layer( + edges_path: impl AsRef, + max_page_len: usize, + meta: &Arc, + ext: EXT, + ) -> Self { + let free_pages = (0..N).map(RwLock::new).collect::>(); + Self { + pages: boxcar::Vec::new(), + num_edges: AtomicUsize::new(0), + free_pages: free_pages.try_into().unwrap(), + edges_path: edges_path.as_ref().to_path_buf(), + max_page_len, + prop_meta: meta.clone(), + ext, + } + } + + pub fn new(edges_path: impl AsRef, max_page_len: usize, ext: EXT) -> Self { + let free_pages = (0..N).map(RwLock::new).collect::>(); + Self { + pages: boxcar::Vec::new(), + num_edges: AtomicUsize::new(0), + free_pages: free_pages.try_into().unwrap(), + edges_path: edges_path.as_ref().to_path_buf(), + max_page_len, + prop_meta: Arc::new(Meta::new()), + ext, + } + } + + pub fn pages(&self) -> &boxcar::Vec> { + &self.pages + } + + pub fn edges_path(&self) -> &Path { + &self.edges_path + } + + pub fn earliest(&self) -> Option { + Iterator::min(self.pages.iter().filter_map(|(_, page)| page.earliest())) + // see : https://github.com/rust-lang/rust-analyzer/issues/10653 + } + + pub fn latest(&self) -> Option { + Iterator::max(self.pages.iter().filter_map(|(_, page)| page.latest())) + } + + pub fn t_len(&self) -> usize { + self.pages.iter().map(|(_, page)| page.t_len()).sum() + } + + pub fn prop_meta(&self) -> &Arc { + &self.prop_meta + } + + #[inline(always)] + pub fn resolve_pos(&self, e_id: EID) -> (usize, LocalPOS) { + resolve_pos(e_id, self.max_page_len) + } + + pub fn load( + edges_path: impl AsRef, + max_page_len: usize, + ext: EXT, + ) -> Result { + let edges_path = edges_path.as_ref(); + + let meta = Arc::new(Meta::new()); + if !edges_path.exists() { + return Ok(Self::new(edges_path, max_page_len, ext.clone())); + } + let mut pages = std::fs::read_dir(edges_path)? + .filter(|entry| { + entry + .as_ref() + .ok() + .and_then(|entry| entry.file_type().ok().map(|ft| ft.is_dir())) + .unwrap_or_default() + }) + .filter_map(|entry| { + let entry = entry.ok()?; + let page_id = entry + .path() + .file_stem() + .and_then(|name| name.to_str().and_then(|name| name.parse::().ok()))?; + let page = ES::load(page_id, max_page_len, meta.clone(), edges_path, ext.clone()) + .map(|page| (page_id, page)); + Some(page) + }) + .collect::, _>>()?; + + if pages.is_empty() { + return Err(DBV4Error::EmptyGraphDir(edges_path.to_path_buf())); + } + + let max_page = Iterator::max(pages.keys().copied()).unwrap(); + + let pages: boxcar::Vec> = (0..=max_page) + .map(|page_id| { + let np = pages.remove(&page_id).unwrap_or_else(|| { + ES::new(page_id, max_page_len, meta.clone(), edges_path, ext.clone()) + }); + Arc::new(np) + }) + .collect::>(); + + let first_page = pages.iter().next().unwrap().1; + let first_p_id = first_page.segment_id(); + + if first_p_id != 0 { + return Err(DBV4Error::GenericFailure(format!( + "First page id is not 0 in {:?}", + edges_path + ))); + } + + let mut free_pages = pages + .iter() + .filter_map(|(_, page)| { + let len = page.num_edges(); + if len < max_page_len { + Some(RwLock::new(page.segment_id())) + } else { + None + } + }) + .collect::>(); + let mut next_free_page = free_pages + .last() + .map(|page| *(page.read())) + .map(|last| last + 1) + .unwrap_or_else(|| pages.count()); + free_pages.resize_with(N, || { + let lock = RwLock::new(next_free_page); + next_free_page += 1; + lock + }); + + let num_edges = pages.iter().map(|(_, page)| page.num_edges()).sum(); + + Ok(Self { + pages, + edges_path: edges_path.to_path_buf(), + max_page_len, + num_edges: AtomicUsize::new(num_edges), + free_pages: free_pages.try_into().unwrap(), + prop_meta: meta, + ext, + }) + } + + pub fn grow(&self, size: usize) { + self.get_or_create_segment(size - 1); + } + + pub fn push_new_page(&self) -> usize { + let segment_id = self.pages.push_with(|segment_id| { + Arc::new(ES::new( + segment_id, + self.max_page_len, + self.prop_meta.clone(), + self.edges_path.clone(), + self.ext.clone(), + )) + }); + + while self.pages.get(segment_id).is_none() { + // wait + } + segment_id + } + + pub fn get_or_create_segment(&self, segment_id: usize) -> &Arc { + if let Some(segment) = self.pages.get(segment_id) { + return segment; + } + let count = self.pages.count(); + if count >= segment_id + 1 { + // something has allocated the segment, wait for it to be added + loop { + if let Some(segment) = self.pages.get(segment_id) { + return segment; + } else { + // wait for the segment to be created + std::thread::yield_now(); + } + } + } else { + // we need to create the segment + self.pages.reserve(segment_id + 1 - count); + + loop { + let new_segment_id = self.pages.push_with(|segment_id| { + Arc::new(ES::new( + segment_id, + self.max_page_len, + self.prop_meta.clone(), + self.edges_path.clone(), + self.ext.clone(), + )) + }); + + if new_segment_id >= segment_id { + loop { + if let Some(segment) = self.pages.get(segment_id) { + return segment; + } else { + // wait for the segment to be created + std::thread::yield_now(); + } + } + } + } + } + } + + pub fn max_page_len(&self) -> usize { + self.max_page_len + } + + pub fn locked<'a>(&'a self) -> WriteLockedEdgePages<'a, ES> { + WriteLockedEdgePages::new( + self.pages + .iter() + .map(|(page_id, page)| { + LockedEdgePage::new( + page_id, + self.max_page_len, + page.as_ref(), + &self.num_edges, + page.head_mut(), + ) + }) + .collect(), + ) + } + + pub fn get_edge(&self, e_id: EID) -> Option<(VID, VID)> { + let (chunk, local_edge) = resolve_pos(e_id, self.max_page_len); + let page = self.pages.get(chunk)?; + page.get_edge(local_edge, page.head()) + } + + pub fn edge(&self, e_id: impl Into) -> ES::Entry<'_> { + let e_id = e_id.into(); + let (page_id, local_edge) = resolve_pos(e_id, self.max_page_len); + let page = self + .pages + .get(page_id) + .expect("Internal error: page not found"); + page.entry(local_edge) + } + + pub fn num_edges(&self) -> usize { + self.num_edges.load(atomic::Ordering::Relaxed) + } + + pub fn get_writer<'a>( + &'a self, + e_id: EID, + ) -> EdgeWriter<'a, RwLockWriteGuard<'a, MemEdgeSegment>, ES> { + let (chunk, _) = resolve_pos(e_id, self.max_page_len); + let page = self.get_or_create_segment(chunk); + EdgeWriter::new(&self.num_edges, page, page.head_mut()) + } + + pub fn try_get_writer<'a>( + &'a self, + e_id: EID, + ) -> Result, ES>, DBV4Error> { + let (segment_id, _) = resolve_pos(e_id, self.max_page_len); + let page = self.get_or_create_segment(segment_id); + let writer = page.head_mut(); + Ok(EdgeWriter::new(&self.num_edges, page, writer)) + } + + pub fn get_free_writer<'a>( + &'a self, + ) -> EdgeWriter<'a, RwLockWriteGuard<'a, MemEdgeSegment>, ES> { + // optimistic first try to get a free page 3 times + let num_edges = self.num_edges(); + let slot_idx = num_edges % N; + let maybe_free_page = self.free_pages[slot_idx..] + .iter() + .cycle() + .take(3) + .filter_map(|lock| lock.try_read()) + .filter_map(|page_id| { + let page = self.pages.get(*page_id)?; + let guard = page.try_head_mut()?; + if page.num_edges() < self.max_page_len { + Some((page, guard)) + } else { + None + } + }) + .next(); + + if let Some((edge_page, writer)) = maybe_free_page { + EdgeWriter::new(&self.num_edges, edge_page, writer) + } else { + // not lucky, go wait on your slot + loop { + let mut slot = self.free_pages[slot_idx].write(); + match self.pages.get(*slot).map(|page| (page, page.head_mut())) { + Some((edge_page, writer)) if edge_page.num_edges() < self.max_page_len => { + return EdgeWriter::new(&self.num_edges, edge_page, writer); + } + _ => { + *slot = self.push_new_page(); + } + } + } + } + } +} diff --git a/db4-storage/src/pages/locked/edges.rs b/db4-storage/src/pages/locked/edges.rs new file mode 100644 index 0000000000..6723e8a7c3 --- /dev/null +++ b/db4-storage/src/pages/locked/edges.rs @@ -0,0 +1,78 @@ +use std::{ops::DerefMut, sync::atomic::AtomicUsize}; + +use crate::{ + EdgeSegmentOps, + pages::{edge_page::writer::EdgeWriter, resolve_pos}, + segments::edge::MemEdgeSegment, +}; +use db4_common::LocalPOS; +use parking_lot::RwLockWriteGuard; +use raphtory::core::entities::EID; +use rayon::prelude::*; + +pub struct LockedEdgePage<'a, ES> { + page_id: usize, + max_page_len: usize, + page: &'a ES, + num_edges: &'a AtomicUsize, + lock: RwLockWriteGuard<'a, MemEdgeSegment>, +} + +impl<'a, EXT, ES: EdgeSegmentOps> LockedEdgePage<'a, ES> { + pub fn new( + page_id: usize, + max_page_len: usize, + page: &'a ES, + num_edges: &'a AtomicUsize, + lock: RwLockWriteGuard<'a, MemEdgeSegment>, + ) -> Self { + Self { + page_id, + max_page_len, + page, + num_edges, + lock, + } + } + + #[inline(always)] + pub fn writer(&mut self) -> EdgeWriter<'_, &mut MemEdgeSegment, ES> { + EdgeWriter::new(self.num_edges, self.page, self.lock.deref_mut()) + } + + #[inline(always)] + pub fn page_id(&self) -> usize { + self.page_id + } + + #[inline(always)] + pub fn resolve_pos(&self, edge_id: EID) -> Option { + let (page, pos) = resolve_pos(edge_id, self.max_page_len); + if page == self.page_id { + Some(pos) + } else { + None + } + } +} +pub struct WriteLockedEdgePages<'a, ES> { + writers: Vec>, +} + +impl<'a, EXT, ES: EdgeSegmentOps> WriteLockedEdgePages<'a, ES> { + pub fn new(writers: Vec>) -> Self { + Self { writers } + } + + pub fn par_iter_mut(&mut self) -> rayon::slice::IterMut<'_, LockedEdgePage<'a, ES>> { + self.writers.par_iter_mut() + } + + pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, LockedEdgePage<'a, ES>> { + self.writers.iter_mut() + } + + pub fn into_par_iter(self) -> impl ParallelIterator> + 'a { + self.writers.into_par_iter() + } +} diff --git a/db4-storage/src/pages/locked/mod.rs b/db4-storage/src/pages/locked/mod.rs new file mode 100644 index 0000000000..bd58cef13c --- /dev/null +++ b/db4-storage/src/pages/locked/mod.rs @@ -0,0 +1,2 @@ +pub mod edges; +pub mod nodes; diff --git a/db4-storage/src/pages/locked/nodes.rs b/db4-storage/src/pages/locked/nodes.rs new file mode 100644 index 0000000000..49904c51a7 --- /dev/null +++ b/db4-storage/src/pages/locked/nodes.rs @@ -0,0 +1,77 @@ +use crate::{ + NodeSegmentOps, + pages::{node_page::writer::NodeWriter, resolve_pos}, + segments::node::MemNodeSegment, +}; +use db4_common::LocalPOS; +use parking_lot::RwLockWriteGuard; +use raphtory::core::entities::VID; +use rayon::prelude::*; +use std::{ops::DerefMut, sync::atomic::AtomicUsize}; + +pub struct LockedNodePage<'a, NS> { + page_id: usize, + max_page_len: usize, + num_nodes: &'a AtomicUsize, + page: &'a NS, + lock: RwLockWriteGuard<'a, MemNodeSegment>, +} + +impl<'a, EXT, NS: NodeSegmentOps> LockedNodePage<'a, NS> { + pub fn new( + page_id: usize, + num_nodes: &'a AtomicUsize, + max_page_len: usize, + page: &'a NS, + lock: RwLockWriteGuard<'a, MemNodeSegment>, + ) -> Self { + Self { + page_id, + num_nodes, + max_page_len, + page, + lock, + } + } + + #[inline(always)] + pub fn writer(&mut self) -> NodeWriter<'_, &mut MemNodeSegment, NS> { + NodeWriter::new(self.page, self.num_nodes, self.lock.deref_mut()) + } + + #[inline(always)] + pub fn page_id(&self) -> usize { + self.page_id + } + + #[inline(always)] + pub fn resolve_pos(&self, node_id: VID) -> Option { + let (page, pos) = resolve_pos(node_id, self.max_page_len); + if page == self.page_id { + Some(pos) + } else { + None + } + } +} +pub struct WriteLockedNodePages<'a, NS> { + writers: Vec>, +} + +impl<'a, EXT, NS: NodeSegmentOps> WriteLockedNodePages<'a, NS> { + pub fn new(writers: Vec>) -> Self { + Self { writers } + } + + pub fn par_iter_mut(&mut self) -> rayon::slice::IterMut<'_, LockedNodePage<'a, NS>> { + self.writers.par_iter_mut() + } + + pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, LockedNodePage<'a, NS>> { + self.writers.iter_mut() + } + + pub fn into_par_iter(self) -> impl ParallelIterator> + 'a { + self.writers.into_par_iter() + } +} diff --git a/db4-storage/src/pages/mod.rs b/db4-storage/src/pages/mod.rs new file mode 100644 index 0000000000..d82951a74f --- /dev/null +++ b/db4-storage/src/pages/mod.rs @@ -0,0 +1,1401 @@ +use std::{ + ops::DerefMut, + path::Path, + sync::{ + Arc, + atomic::{self, AtomicI64, AtomicUsize}, + }, +}; + +use crate::{ + EdgeSegmentOps, NodeSegmentOps, + properties::props_meta_writer::PropsMetaWriter, + segments::{edge::MemEdgeSegment, node::MemNodeSegment}, +}; +use db4_common::{LocalPOS, error::DBV4Error}; +use edge_page::writer::EdgeWriter; +use edge_store::EdgeStorageInner; +use node_page::writer::{NodeWriter, WriterPair}; +use node_store::NodeStorageInner; +use raphtory::{ + core::{ + entities::{EID, ELID, VID}, + storage::timeindex::{AsTime, TimeIndexEntry}, + utils::time::{InputTime, TryIntoInputTime}, + }, + prelude::Prop, +}; +use raphtory_api::core::{entities::properties::meta::Meta, storage::dict_mapper::MaybeNew}; +use serde::{Deserialize, Serialize}; +use session::WriteSession; + +pub mod edge_page; +pub mod edge_store; +pub mod locked; +pub mod node_page; +pub mod node_store; +pub mod session; +#[cfg(feature = "test-utils")] +pub mod test_utils; + +#[derive(Debug)] +pub struct GraphStore { + nodes: Arc>, + edges: Arc>, + edge_meta: Arc, + node_meta: Arc, + earliest: AtomicI64, + latest: AtomicI64, + event_id: AtomicUsize, + _ext: EXT, +} + +impl, ES: EdgeSegmentOps, EXT: Clone + Default> + GraphStore +{ + pub fn nodes(&self) -> &NodeStorageInner { + &self.nodes + } + + pub fn edges(&self) -> &EdgeStorageInner { + &self.edges + } + + pub fn edge_meta(&self) -> &Meta { + &self.edge_meta + } + + pub fn node_meta(&self) -> &Meta { + &self.node_meta + } + + pub fn earliest(&self) -> i64 { + self.earliest.load(atomic::Ordering::Relaxed) + } + + pub fn latest(&self) -> i64 { + self.latest.load(atomic::Ordering::Relaxed) + } + + pub fn load(graph_dir: impl AsRef) -> Result { + let nodes_path = graph_dir.as_ref().join("nodes"); + let edges_path = graph_dir.as_ref().join("edges"); + + let GraphMeta { + max_page_len_nodes, + max_page_len_edges, + } = read_graph_meta(graph_dir.as_ref())?; + + let ext = EXT::default(); + + let nodes = Arc::new(NodeStorageInner::load( + nodes_path, + max_page_len_nodes, + ext.clone(), + )?); + let edges = Arc::new(EdgeStorageInner::load( + edges_path, + max_page_len_edges, + ext.clone(), + )?); + let edge_meta = edges.prop_meta().clone(); + let node_meta = nodes.prop_meta().clone(); + + let earliest = AtomicI64::new(edges.earliest().map(|t| t.t()).unwrap_or_default()); + let latest = AtomicI64::new(edges.latest().map(|t| t.t()).unwrap_or_default()); + let t_len = edges.t_len(); + + Ok(Self { + nodes, + edges, + edge_meta, + node_meta, + earliest, + latest, + event_id: AtomicUsize::new(t_len), + _ext: ext, + }) + } + + pub fn layer( + graph_dir: impl AsRef, + max_page_len_nodes: usize, + max_page_len_edges: usize, + node_meta: Arc, + edge_meta: Arc, + ) -> Self { + let nodes_path = graph_dir.as_ref().join("nodes"); + let edges_path = graph_dir.as_ref().join("edges"); + let ext = EXT::default(); + + let nodes = Arc::new(NodeStorageInner::layer( + nodes_path, + max_page_len_nodes, + &node_meta, + ext.clone(), + )); + let edges = Arc::new(EdgeStorageInner::layer( + edges_path, + max_page_len_edges, + &edge_meta, + ext.clone(), + )); + + Self { + nodes: nodes.clone(), + edges: edges.clone(), + edge_meta, + node_meta, + earliest: AtomicI64::new(0), + latest: AtomicI64::new(0), + event_id: AtomicUsize::new(0), + _ext: ext, + } + } + + pub fn new( + graph_dir: impl AsRef, + max_page_len_nodes: usize, + max_page_len_edges: usize, + ) -> Self { + let nodes_path = graph_dir.as_ref().join("nodes"); + let edges_path = graph_dir.as_ref().join("edges"); + let ext = EXT::default(); + + let nodes = Arc::new(NodeStorageInner::new( + nodes_path, + max_page_len_nodes, + ext.clone(), + )); + let edges = Arc::new(EdgeStorageInner::new( + edges_path, + max_page_len_edges, + ext.clone(), + )); + let edge_meta = edges.prop_meta(); + let node_meta = nodes.prop_meta(); + let graph_meta = GraphMeta { + max_page_len_nodes, + max_page_len_edges, + }; + + write_graph_meta(&graph_dir, graph_meta); + + Self { + nodes: nodes.clone(), + edges: edges.clone(), + edge_meta: edge_meta.clone(), + node_meta: node_meta.clone(), + earliest: AtomicI64::new(0), + latest: AtomicI64::new(0), + event_id: AtomicUsize::new(0), + _ext: ext, + } + } + + pub fn add_edge( + &self, + t: T, + src: impl Into, + dst: impl Into, + ) -> Result, DBV4Error> { + let t = self.as_time_index_entry(t)?; + self.internal_add_edge(t, src, dst, 0, []) + } + + pub fn add_edge_props, T: TryIntoInputTime>( + &self, + t: T, + src: impl Into, + dst: impl Into, + props: Vec<(PN, Prop)>, + _lsn: u64, + ) -> Result, DBV4Error> { + let t = self.as_time_index_entry(t)?; + let prop_writer = PropsMetaWriter::temporal(&self.edge_meta, props.into_iter())?; + self.internal_add_edge(t, src, dst, 0, prop_writer.into_props_temporal()?) + } + + pub fn internal_add_edge( + &self, + t: TimeIndexEntry, + src: impl Into, + dst: impl Into, + lsn: u64, + props: impl IntoIterator, + ) -> Result, DBV4Error> { + let src = src.into(); + let dst = dst.into(); + let mut session = self.write_session(src, dst, None); + session.internal_add_edge(t, src, dst, lsn, 0, props) + } + + /// Adds an edge if it doesn't exist yet, does nothing if the edge is there + // pub fn internal_add_edge( + // &self, + // t: T, + // src: impl Into, + // dst: impl Into, + // lsn: u64, + // props: impl IntoIterator, + // ) -> Result, DBV4Error> { + // let src = src.into(); + // let dst = dst.into(); + + // let (src_chunk, src_pos) = self.nodes.resolve_pos(src); + // let (dst_chunk, dst_pos) = self.nodes.resolve_pos(dst); + + // self.nodes.grow(src_chunk.max(dst_chunk) + 1); + + // let src_page = &self.nodes.pages()[src_chunk]; + // // let dst_page = &self.nodes.pages()[dst_chunk]; + + // let acquire_node_writers = || { + // // let writer_pair = if src_chunk < dst_chunk { + // // let src_writer = src_page.writer::(); + // // let dst_writer = dst_page.writer::(); + // // WriterPair::Different { + // // writer_i: src_writer, + // // writer_j: dst_writer, + // // } + // // } else if src_chunk > dst_chunk { + // // let dst_writer = dst_page.writer::(); + // // let src_writer = src_page.writer::(); + // // WriterPair::Different { + // // writer_i: src_writer, + // // writer_j: dst_writer, + // // } + // // } else { + // // let writer = src_page.writer::(); + // // WriterPair::Same { writer } + // // }; + // // writer_pair + + // // let mut loop_count = 0; + // loop { + // if src_chunk == dst_chunk { + // if let Some(writer) = self + // .nodes() + // .try_writer(src_chunk) + // { + // return WriterPair::Same { writer }; + // } + // } else { + // if let Some(writer_i) = self + // .nodes + // .try_writer(src_chunk, self.persistence.strategy()) + // { + // if let Some(writer_j) = self + // .nodes + // .try_writer(dst_chunk, self.persistence.strategy()) + // { + // return WriterPair::Different { writer_i, writer_j }; + // } + // } + // } + // } + // }; + + // if let Some(e_id) = src_page.disk_get_out_edge(src_pos, dst) { + // let mut edge_writer = self.edges.get_writer(e_id); + // let (_, edge_pos) = self.edges.resolve_pos(e_id); + // edge_writer.add_edge(t, Some(edge_pos), src, dst, props, lsn, None)?; + + // let mut node_writers = acquire_node_writers(); + // node_writers + // .get_mut_i() + // .update_timestamp(t, src_pos, e_id, lsn); + // node_writers + // .get_mut_j() + // .update_timestamp(t, dst_pos, e_id, lsn); + + // Ok(MaybeNew::Existing(e_id)) + // } else { + // let mut node_writers = acquire_node_writers(); + + // if let Some(e_id) = node_writers.get_mut_i().get_out_edge(src_pos, dst) { + // let mut edge_writer = self.edges.get_writer(e_id); + // let (_, edge_pos) = self.edges.resolve_pos(e_id); + + // edge_writer.add_edge(t, Some(edge_pos), src, dst, props, lsn, None)?; + // node_writers + // .get_mut_i() + // .update_timestamp(t, src_pos, e_id, lsn); + // node_writers + // .get_mut_j() + // .update_timestamp(t, dst_pos, e_id, lsn); + + // Ok(MaybeNew::Existing(e_id)) + // } else { + // let mut edge_writer = self.get_free_writer(); + // let edge_id = edge_writer.add_edge(t, None, src, dst, props, lsn, None)?; + // let edge_id = edge_id.as_eid(edge_writer.segment_id(), self.edges.max_page_len()); + + // node_writers + // .get_mut_i() + // .add_outbound_edge(t, src_pos, dst, edge_id, lsn); + // node_writers + // .get_mut_j() + // .add_inbound_edge(t, dst_pos, src, edge_id, lsn); + + // Ok(MaybeNew::New(edge_id)) + // } + // } + // } + + fn as_time_index_entry(&self, t: T) -> Result { + let input_time = t.try_into_input_time()?; + let t = match input_time { + InputTime::Indexed(t, i) => TimeIndexEntry::new(t, i), + InputTime::Simple(t) => { + let i = self.event_id.fetch_add(1, atomic::Ordering::Relaxed); + TimeIndexEntry::new(t, i) + } + }; + Ok(t) + } + + pub fn update_edge_const_props>( + &self, + eid: impl Into, + props: Vec<(PN, Prop)>, + ) -> Result<(), DBV4Error> { + let eid = eid.into(); + let (_, edge_pos) = self.edges.resolve_pos(eid); + let mut edge_writer = self.edges.try_get_writer(eid)?; + let (src, dst) = edge_writer + .get_edge(edge_pos) + .expect("Internal Error, EID should be checked at this point!"); + let prop_writer = PropsMetaWriter::constant(&self.edge_meta, props.into_iter())?; + edge_writer.update_c_props(edge_pos, src, dst, prop_writer.into_props_const()?); + Ok(()) + } + + pub fn update_node_const_props>( + &self, + node: impl Into, + props: Vec<(PN, Prop)>, + ) -> Result<(), DBV4Error> { + let node = node.into(); + let (segment, node_pos) = self.nodes.resolve_pos(node); + let mut node_writer = self.nodes.writer(segment); + let prop_writer = PropsMetaWriter::constant(&self.node_meta, props.into_iter())?; + node_writer.update_c_props(node_pos, prop_writer.into_props_const()?, 0); // TODO: LSN + Ok(()) + } + + pub fn add_node_props>( + &self, + t: impl TryIntoInputTime, + node: impl Into, + props: Vec<(PN, Prop)>, + ) -> Result<(), DBV4Error> { + let node = node.into(); + let (segment, node_pos) = self.nodes.resolve_pos(node); + + let t = self.as_time_index_entry(t)?; + + let mut node_writer = self.nodes.writer(segment); + let prop_writer = PropsMetaWriter::temporal(&self.node_meta, props.into_iter())?; + node_writer.add_props(t, node_pos, prop_writer.into_props_temporal()?, 0); // TODO: LSN + Ok(()) + } + + pub fn write_session( + &self, + src: VID, + dst: VID, + e_id: Option, + ) -> WriteSession< + '_, + impl DerefMut, + impl DerefMut, + NS, + ES, + EXT, + > { + let (src_chunk, _) = self.nodes.resolve_pos(src); + let (dst_chunk, _) = self.nodes.resolve_pos(dst); + + let acquire_node_writers = || { + let writer_pair = if src_chunk < dst_chunk { + let src_writer = self.node_writer(src_chunk); + let dst_writer = self.node_writer(dst_chunk); + WriterPair::Different { + src_writer, + dst_writer, + } + } else if src_chunk > dst_chunk { + let dst_writer = self.node_writer(dst_chunk); + let src_writer = self.node_writer(src_chunk); + WriterPair::Different { + src_writer, + dst_writer, + } + } else { + let writer = self.node_writer(src_chunk); + WriterPair::Same { writer } + }; + writer_pair + }; + + let node_writers = acquire_node_writers(); + + let edge_writer = e_id.map(|e_id| self.edge_writer(e_id)); + + WriteSession::new(node_writers, edge_writer, self) + } + + fn node_writer( + &self, + node_segment: usize, + ) -> NodeWriter, NS> { + self.nodes().writer(node_segment) + } + + pub fn edge_writer(&self, eid: EID) -> EdgeWriter, ES> { + self.edges().get_writer(eid) + } + + pub fn get_free_writer(&self) -> EdgeWriter, ES> { + self.edges.get_free_writer() + } +} + +fn write_graph_meta(graph_dir: impl AsRef, graph_meta: GraphMeta) -> Result<(), DBV4Error> { + let meta_file = graph_dir.as_ref().join("graph_meta.json"); + let meta_file = std::fs::File::create(meta_file).unwrap(); + serde_json::to_writer_pretty(meta_file, &graph_meta)?; + Ok(()) +} + +fn read_graph_meta(graph_dir: impl AsRef) -> Result { + let meta_file = graph_dir.as_ref().join("graph_meta.json"); + let meta_file = std::fs::File::open(meta_file).unwrap(); + let meta = serde_json::from_reader(meta_file)?; + Ok(meta) +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +struct GraphMeta { + max_page_len_nodes: usize, + max_page_len_edges: usize, +} + +#[inline(always)] +pub fn resolve_pos>(i: I, max_page_len: usize) -> (usize, LocalPOS) { + let chunk = i.into() / max_page_len; + let pos = i.into() % max_page_len; + (chunk, pos.into()) +} + +#[cfg(test)] +mod test { + use super::GraphStore; + use crate::{ + Layer, NodeEntryOps, NodeRefOps, + pages::test_utils::{ + AddEdge, Fixture, NodeFixture, check_edges_support, check_graph_with_nodes_support, + check_graph_with_props_support, edges_strat, make_edges, make_nodes, + }, + }; + use chrono::{DateTime, NaiveDateTime, Utc}; + use core::panic; + use proptest::prelude::*; + use raphtory::{ + core::{entities::VID, storage::timeindex::TimeIndexOps}, + prelude::Prop, + }; + + fn check_edges( + edges: Vec<(impl Into, impl Into)>, + chunk_size: usize, + par_load: bool, + ) { + check_edges_support(edges, par_load, false, |graph_dir| { + Layer::new(graph_dir, chunk_size, chunk_size) + }) + } + + #[test] + fn test_storage() { + let edges_strat = edges_strat(10); + proptest!(|(edges in edges_strat, chunk_size in 1usize .. 100)|{ + check_edges(edges, chunk_size, false); + }); + } + + #[test] + fn test_storage_par() { + let edges_strat = edges_strat(15); + proptest!(|(edges in edges_strat, chunk_size in 1usize..100)|{ + check_edges(edges, chunk_size, true); + }); + } + + #[test] + fn test_storage_par_1024_x2() { + let edges_strat = edges_strat(50); + proptest!(|(edges in edges_strat, chunk_size in 1usize..100)|{ + check_edges(edges, chunk_size, true); + }); + } + + #[test] + fn test_storage_par_1024() { + let edges_strat = edges_strat(50); + proptest!(|(edges in edges_strat, chunk_size in 2usize..100)|{ + check_edges(edges, chunk_size, false); + }); + } + + #[test] + fn test_storage_issue1() { + let edges = vec![(0, 1), (1, 0), (0, 0)]; + check_edges(edges, 2, false); + } + + #[test] + fn test_storage_empty() { + let edges = Vec::<(VID, VID)>::new(); + check_edges(edges, 32, false); + } + + #[test] + fn test_one_edge() { + let edges = vec![(2, 2)]; + check_edges(edges, 2, false); + } + + #[test] + fn test_add_one_edge_get_num_nodes() { + let graph_dir = tempfile::tempdir().unwrap(); + let g = Layer::new(graph_dir.path(), 32, 32); + g.add_edge(4, 7, 3).unwrap(); + assert_eq!(g.nodes().num_nodes(), 2); + } + + #[test] + fn test_node_additions_1() { + let graph_dir = tempfile::tempdir().unwrap(); + let g = GraphStore::new(graph_dir.path(), 32, 32); + g.add_edge(4, 7, 3).unwrap(); + + let check = |g: &Layer<()>| { + assert_eq!(g.nodes().num_nodes(), 2); + + let node = g.nodes().node(3); + let node_entry = node.as_ref(); + let actual: Vec<_> = node_entry.additions().iter_t().collect(); + assert_eq!(actual, vec![4]); + }; + + check(&g); + // drop(g); + // + // let g = GraphStore::::load(graph_dir.path()).unwrap(); + // check(&g); + } + + #[test] + fn test_one_edge_par() { + let edges = vec![(2, 2)]; + check_edges(edges, 2, true); + } + + #[test] + fn test_multiple_edges_par() { + let edges = vec![(2, 2), (2, 3), (3, 2), (3, 3), (3, 4), (4, 3)]; + check_edges(edges, 2, false); + } + + #[test] + fn test_multiple_edges_par_x2() { + let edges = vec![(2, 2), (2, 3), (3, 2), (3, 3), (3, 4), (4, 3)]; + check_edges(edges, 2, true); + } + + #[test] + fn some_edges() { + let edges = vec![(1, 1), (0, 0), (1, 0), (1, 1)]; + check_edges(edges, 89, false); + } + + #[test] + fn add_one_edge_with_props() { + let edges = make_edges(1, 1); + proptest!(|(edges in edges, node_page_len in 1usize..100, edge_page_len in 1usize .. 100)|{ + check_graph_with_props(node_page_len, edge_page_len, &edges); + }); + } + + #[test] + fn add_one_edge_with_decimal() { + let edges = vec![( + VID(0), + VID(0), + 0, + vec![ + ( + "957".to_owned(), + Prop::DTime(DateTime::from_timestamp_millis(0).unwrap()), + ), + ("920".to_owned(), Prop::I32(0)), + ], + vec![ + ("920".to_owned(), Prop::I32(0)), + ( + "957".to_owned(), + Prop::DTime(DateTime::from_timestamp_millis(0).unwrap()), + ), + ], + Some("b"), + )]; + check_graph_with_props(89, 1, &edges.into()); + } + + #[test] + fn add_one_edge_with_time_props_and_decimal() { + let edges: Vec = vec![( + VID(0), + VID(0), + 0, + vec![ + ( + "767".to_owned(), + Prop::DTime(DateTime::from_timestamp_millis(-2208988800000).unwrap()), + ), + ("123".to_owned(), Prop::Decimal(123425879.into())), + ], + vec![ + ( + "140".to_owned(), + Prop::NDTime( + DateTime::from_timestamp_millis(-2208988800001) + .unwrap() + .naive_utc(), + ), + ), + ("321".to_owned(), Prop::Decimal(7654321.into())), + ], + Some("b"), + )]; + + check_graph_with_props(31, 50, &edges.into()); + } + + #[test] + fn add_one_node_with_props() { + let nodes = make_nodes(1); + proptest!(|(nodes in nodes, node_page_len in 1usize..100, edge_page_len in 1usize .. 100)|{ + check_graph_with_nodes(node_page_len, edge_page_len, &nodes); + }); + } + + #[test] + fn add_multiple_node_with_props() { + let nodes = make_nodes(20); + proptest!(|(nodes in nodes, node_page_len in 1usize..100, edge_page_len in 1usize .. 100)|{ + check_graph_with_nodes(node_page_len, edge_page_len, &nodes); + }); + } + + #[test] + fn add_multiple_node_with_props_4() { + let node_fixture = NodeFixture { + temp_props: vec![(VID(0), 0, vec![])], + const_props: vec![( + VID(0), + vec![ + ("399".to_owned(), Prop::I64(498)), + ("831".to_owned(), Prop::str("898")), + ("857".to_owned(), Prop::F64(2.56)), + ( + "296".to_owned(), + Prop::NDTime(NaiveDateTime::from_timestamp(1334043671, 0)), + ), + ( + "92".to_owned(), + Prop::DTime(DateTime::::from_utc( + NaiveDateTime::from_timestamp(994032315, 0), + Utc, + )), + ), + ], + )], + }; + + check_graph_with_nodes(90, 60, &node_fixture); + } + + #[test] + fn add_multiple_node_with_props_3() { + let node_fixture = NodeFixture { + temp_props: vec![ + (VID(0), 0, vec![]), + (VID(0), 0, vec![]), + (VID(0), 0, vec![]), + (VID(0), 0, vec![]), + (VID(0), 0, vec![]), + (VID(0), 0, vec![]), + ], + const_props: vec![(VID(0), vec![]), (VID(0), vec![]), (VID(0), vec![])], + }; + check_graph_with_nodes(1, 1, &node_fixture); + } + + #[test] + fn add_multiple_node_with_props_1() { + let node_fixture = NodeFixture { + temp_props: vec![(VID(0), 0, vec![])], + const_props: vec![ + (VID(0), vec![]), + (VID(8), vec![("422".to_owned(), Prop::U8(0))]), + (VID(8), vec![("422".to_owned(), Prop::U8(30))]), + ], + }; + check_graph_with_nodes(43, 94, &node_fixture); + } + + #[test] + fn add_multiple_node_with_props_2() { + let node_fixture = NodeFixture { + temp_props: vec![(VID(0), 0, vec![])], + const_props: vec![ + ( + VID(0), + vec![ + ("441".to_owned(), Prop::I64(-3856368215564042936)), + ("225".to_owned(), Prop::F64(-202423261.6280773)), + ("290".to_owned(), Prop::str("15")), + ("54".to_owned(), Prop::U8(226)), + ("953".to_owned(), Prop::Bool(false)), + ("771".to_owned(), Prop::I64(-6507648222238880768)), + ("955".to_owned(), Prop::Bool(true)), + ("346".to_owned(), Prop::F64(-1.608025857001021e-308)), + ], + ), + (VID(1), vec![("953".to_owned(), Prop::Bool(false))]), + (VID(1), vec![]), + ], + }; + check_graph_with_nodes(8, 57, &node_fixture); + } + + #[test] + fn add_one_node_with_props_0() { + let node_fixture = NodeFixture { + temp_props: vec![(VID(0), 0, vec![])], + const_props: vec![ + ( + VID(1), + vec![("574".to_owned(), Prop::I64(-28802842553584714))], + ), + ( + VID(1), + vec![ + ("571".to_owned(), Prop::U8(30)), + ("618".to_owned(), Prop::Bool(true)), + ("431".to_owned(), Prop::F64(-2.7522071060615837e-76)), + ("68".to_owned(), Prop::F64(-2.32248037343811e44)), + ("620".to_owned(), Prop::I64(1574788428164567343)), + ("574".to_owned(), Prop::I64(-6212197184834902986)), + ], + ), + ], + }; + + check_graph_with_nodes(85, 34, &node_fixture); + } + + #[test] + fn add_one_node_with_props_1() { + let node_fixture = NodeFixture { + temp_props: vec![( + VID(1), + 2, + vec![ + ("611".to_owned(), Prop::U8(25)), + ("590".to_owned(), Prop::str("294")), + ("63".to_owned(), Prop::Bool(true)), + ("789".to_owned(), Prop::I64(-245071354050338754)), + ], + )], + const_props: vec![(VID(1), vec![("801".to_owned(), Prop::U8(32))])], + }; + + check_graph_with_nodes(85, 34, &node_fixture); + } + + #[test] + fn add_one_edge_with_props_0() { + let edges = vec![( + VID(0), + VID(0), + 0, + vec![("1".to_owned(), Prop::str("0"))], + vec![], + Some("a"), + )]; + check_graph_with_props(82, 82, &edges.into()); + } + + #[test] + fn add_one_edge_with_props_1() { + let edges = vec![( + VID(0), + VID(0), + 0, + vec![], + vec![("877".to_owned(), Prop::F64(0.0))], + None, + )]; + check_graph_with_props(82, 82, &edges.into()); + } + + #[test] + fn add_one_edge_with_props_2() { + let edges = vec![( + VID(0), + VID(0), + 0, + vec![("0".to_owned(), Prop::str("0"))], + vec![("1".to_owned(), Prop::str("0"))], + Some("a"), + )]; + check_graph_with_props(82, 82, &edges.into()); + } + + #[test] + fn add_one_edge_with_props_3() { + let edges = vec![( + VID(0), + VID(0), + 0, + vec![("962".to_owned(), Prop::I64(0))], + vec![("324".to_owned(), Prop::U8(0))], + Some("a"), + )]; + check_graph_with_props(98, 16, &edges.into()); + } + + #[test] + fn add_multiple_edges_with_props() { + let edges = make_edges(20, 20); + proptest!(|(edges in edges, node_page_len in 1usize..100, edge_page_len in 1usize .. 100)|{ + check_graph_with_props(node_page_len, edge_page_len, &edges); + }); + } + + #[test] + fn add_multiple_edges_with_props_13() { + for _ in 0..10 { + let edges = vec![ + ( + VID(12), + VID(3), + 64, + vec![("659".to_owned(), Prop::Bool(true))], + vec![ + ("429".to_owned(), Prop::U8(13)), + ("991".to_owned(), Prop::F64(9.431610844495756)), + ("792".to_owned(), Prop::str("44")), + ], + Some("a"), + ), + ( + VID(8), + VID(0), + 45, + vec![ + ("374".to_owned(), Prop::F64(-3.2891291943257276)), + ("659".to_owned(), Prop::Bool(true)), + ("649".to_owned(), Prop::U8(72)), + ("877".to_owned(), Prop::F64(5.505566002056544)), + ("561".to_owned(), Prop::str("289")), + ], + vec![ + ("991".to_owned(), Prop::F64(4.4758924307224585)), + ("792".to_owned(), Prop::str("594")), + ], + None, + ), + ( + VID(14), + VID(16), + 30, + vec![ + ("374".to_owned(), Prop::F64(-2.4044297575008132)), + ("561".to_owned(), Prop::str("964")), + ], + vec![ + ("899".to_owned(), Prop::F64(4.491626971132711)), + ("868".to_owned(), Prop::Bool(true)), + ("962".to_owned(), Prop::I64(3133919197295275594)), + ("840".to_owned(), Prop::str("578")), + ], + None, + ), + ]; + check_graph_with_props(33, 39, &edges.into()); + } + } + + #[test] + fn add_multiple_edges_with_props_11() { + let edges = vec![ + ( + VID(10), + VID(7), + 63, + vec![ + ("649".to_owned(), Prop::U8(54)), + ("868".to_owned(), Prop::Bool(false)), + ("361".to_owned(), Prop::I64(6798507933589465750)), + ("561".to_owned(), Prop::str("800")), + ], + vec![("877".to_owned(), Prop::F64(-4.4595346573113036e-48))], + Some("b"), + ), + ( + VID(7), + VID(3), + 56, + vec![], + vec![ + ("877".to_owned(), Prop::F64(-9.826757828363747e44)), + ("899".to_owned(), Prop::F64(1.6798428870674542e-256)), + ("991".to_owned(), Prop::F64(2.246204753092509e144)), + ("374".to_owned(), Prop::F64(1.1547300396496702e131)), + ], + Some("b"), + ), + ( + VID(9), + VID(9), + 28, + vec![], + vec![ + ("792".to_owned(), Prop::str("426")), + ("877".to_owned(), Prop::F64(-1.2304916849909104e-297)), + ("899".to_owned(), Prop::F64(2.8623367224991785e75)), + ("840".to_owned(), Prop::str("309")), + ("991".to_owned(), Prop::F64(-2.1336000912955556e-308)), + ("962".to_owned(), Prop::I64(-3475626455764953092)), + ("374".to_owned(), Prop::F64(-0.0)), + ], + Some("a"), + ), + ( + VID(4), + VID(14), + 10, + vec![ + ("868".to_owned(), Prop::Bool(false)), + ("361".to_owned(), Prop::I64(-6751088942916859396)), + ], + vec![], + Some("b"), + ), + ]; + + check_graph_with_props(33, 69, &edges.into()); + // check_graph_with_props::>(33, 69, &edges.into()); different problem + } + + #[test] + fn add_multiple_edges_with_props_12() { + let edges = vec![ + (VID(13), VID(11), 47, vec![], vec![], None), + ( + VID(2), + VID(10), + 61, + vec![ + ("991".to_owned(), Prop::F64(1.783602448650279e-300)), + ("361".to_owned(), Prop::I64(-6635533919809359722)), + ("659".to_owned(), Prop::Bool(false)), + ], + vec![ + ("868".to_owned(), Prop::Bool(false)), + ("561".to_owned(), Prop::str("443")), + ], + None, + ), + ( + VID(16), + VID(7), + 63, + vec![("962".to_owned(), Prop::I64(-5795311055328182913))], + vec![ + ("429".to_owned(), Prop::U8(173)), + ("561".to_owned(), Prop::str("821")), + ("649".to_owned(), Prop::U8(177)), + ], + Some("a"), + ), + ( + VID(16), + VID(6), + 56, + vec![ + ("792".to_owned(), Prop::str("551")), + ("962".to_owned(), Prop::I64(123378859162979696)), + ("361".to_owned(), Prop::I64(-324898360063869285)), + ("659".to_owned(), Prop::Bool(true)), + ], + vec![], + None, + ), + ]; + check_graph_with_props(24, 31, &edges.into()); + } + + // #[test] + // #[ignore = "Time index entry can be overwritten"] + // fn add_multiple_edges_with_props_9() { + // let graph_dir = tempfile::tempdir().unwrap(); + // let gs = Layer::new(graph_dir.path(), 32, 32); + + // gs.internal_add_edge(TimeIndexEntry(1, 0), 0, 0, 0, vec![("a", Prop::str("b"))]) + // .unwrap(); + // gs.internal_add_edge(TimeIndexEntry(1, 0), 0, 0, 0, vec![("c", Prop::str("d"))]) + // .unwrap(); + + // let edge = gs.edges().edge(0); + // let props = edge.as_ref().t_prop(0).iter().collect::>(); + // assert_eq!(props, vec![(TimeIndexEntry(1, 0), Prop::str("b")),]); + // let props = edge.as_ref().t_prop(1).iter().collect::>(); + // assert_eq!(props, vec![(TimeIndexEntry(1, 0), Prop::str("d")),]); + // } + + // #[test] + // #[ignore = "Time index entry can be overwritten"] + // fn add_multiple_edges_with_props_10() { + // let graph_dir = tempfile::tempdir().unwrap(); + // let gs = GraphStore::>::new(graph_dir.path(), 32, 32); + + // gs.add_edge_props(TimeIndexEntry(1, 0), 0, 0, vec![("a", Prop::str("b"))], 0) + // .unwrap(); + // gs.add_edge_props(TimeIndexEntry(1, 0), 0, 0, vec![("a", Prop::str("d"))], 0) + // .unwrap(); + + // let edge = gs.edges().edge(0); + // let props = edge.as_ref().t_prop(0).iter().collect::>(); + // assert_eq!( + // props, + // vec![ + // (TimeIndexEntry(1, 0), Prop::str("b")), + // (TimeIndexEntry(1, 0), Prop::str("d")) + // ] + // ); + // } + + #[test] + fn add_multiple_edges_with_props_8() { + let edges = vec![ + (VID(7), VID(8), 0, vec![], vec![], Some("a")), + (VID(0), VID(0), 0, vec![], vec![], Some("a")), + (VID(1), VID(0), 0, vec![], vec![], Some("a")), + (VID(7), VID(8), 66, vec![], vec![], Some("b")), + ( + VID(7), + VID(3), + 31, + vec![("52".to_string(), Prop::U8(202))], + vec![], + None, + ), + (VID(4), VID(8), 40, vec![], vec![], Some("a")), + ( + VID(3), + VID(10), + 9, + vec![("52".to_string(), Prop::U8(169))], + vec![], + None, + ), + ( + VID(13), + VID(4), + 3, + vec![("52".to_string(), Prop::U8(72))], + vec![], + Some("a"), + ), + ( + VID(2), + VID(4), + 9, + vec![("52".to_string(), Prop::U8(131))], + vec![], + Some("b"), + ), + ( + VID(2), + VID(1), + 47, + vec![("52".to_string(), Prop::U8(55))], + vec![], + Some("a"), + ), + ( + VID(14), + VID(3), + 13, + vec![("52".to_string(), Prop::U8(70))], + vec![], + None, + ), + ( + VID(8), + VID(10), + 11, + vec![("52".to_string(), Prop::U8(47))], + vec![], + Some("b"), + ), + ]; + + check_graph_with_props(88, 83, &edges.into()); + } + + #[test] + fn add_multiple_edges_with_props_7() { + let edges = vec![ + (VID(0), VID(0), 1, vec![], vec![], Some("a")), + (VID(0), VID(1), 2, vec![], vec![], Some("a")), + (VID(3), VID(3), 3, vec![], vec![], Some("a")), + ( + VID(3), + VID(3), + 4, + vec![("9".to_string(), Prop::I64(0))], + vec![], + Some("a"), + ), + ]; + check_graph_with_props(90, 2, &edges.into()); + } + + #[test] + fn add_multiple_edges_with_props_6() { + let edges = vec![ + (VID(5), VID(6), 0, vec![], vec![], Some("a")), + (VID(0), VID(0), 0, vec![], vec![], Some("a")), + (VID(0), VID(1), 0, vec![], vec![], Some("a")), + (VID(1), VID(0), 0, vec![], vec![], Some("a")), + (VID(4), VID(7), 0, vec![], vec![], Some("a")), + (VID(4), VID(7), 0, vec![], vec![], Some("a")), + ( + VID(5), + VID(6), + 1, + vec![("100".to_string(), Prop::Bool(false))], + vec![], + Some("a"), + ), + ]; + check_graph_with_props(10, 19, &edges.into()); + } + + #[test] + fn add_multiple_edges_with_props_5() { + let edges = vec![ + (VID(2), VID(0), 0, vec![], vec![], Some("a")), + ( + VID(0), + VID(0), + 0, + vec![("382".to_string(), Prop::U8(90))], + vec![], + Some("a"), + ), + ( + VID(3), + VID(1), + 3, + vec![("382".to_string(), Prop::U8(227))], + vec![], + Some("a"), + ), + (VID(2), VID(2), 18, vec![], vec![], None), + ( + VID(0), + VID(2), + 15, + vec![("195".to_string(), Prop::Bool(false))], + vec![], + Some("b"), + ), + ( + VID(0), + VID(2), + 12, + vec![ + ("287".to_string(), Prop::I64(-5621124784932591697)), + ("382".to_string(), Prop::U8(95)), + ], + vec![], + None, + ), + ]; + check_graph_with_props(10, 10, &edges.into()); + } + + #[test] + fn add_multiple_edges_with_props_3() { + let edges = vec![ + ( + VID(0), + VID(0), + 0, + vec![("419".to_string(), Prop::F64(6.839180078867341e80))], + vec![], + Some("b"), + ), + ( + VID(0), + VID(0), + 3, + vec![], + vec![("419".to_string(), Prop::F64(-0.0))], + None, + ), + (VID(0), VID(0), 4, Vec::new(), Vec::new(), None), + (VID(0), VID(0), 0, Vec::new(), Vec::new(), Some("b")), + ( + VID(0), + VID(0), + 4, + Vec::new(), + vec![("419".to_string(), Prop::F64(1.0562500054688134e-99))], + Some("b"), + ), + ]; + check_graph_with_props(43, 86, &edges.into()); + } + + #[test] + fn add_multiple_edges_with_props_4() { + let edges = vec![ + ( + VID(0), + VID(0), + 2, + vec![("419".to_string(), Prop::F64(0.0))], + vec![("533".to_string(), Prop::F64(7.22))], + Some("a"), + ), + ( + VID(0), + VID(0), + 2, + vec![("419".to_string(), Prop::F64(-4.522))], + vec![], + Some("b"), + ), + ]; + check_graph_with_props(5, 5, &edges.into()); + } + + #[test] + fn add_multiple_edges_with_props_2() { + let edges: Vec = vec![ + ( + VID(1), + VID(0), + 5, + vec![("195".to_string(), Prop::Bool(false))], + Vec::new(), + Some("b"), + ), + ( + VID(1), + VID(0), + 16, + vec![ + ("921".to_string(), Prop::U8(41)), + ("195".to_string(), Prop::Bool(true)), + ("287".to_string(), Prop::I64(6720004553605012498)), + ], + Vec::new(), + Some("a"), + ), + ( + VID(3), + VID(1), + 3, + vec![("287".to_string(), Prop::I64(846481219119638755))], + Vec::new(), + Some("a"), + ), + (VID(2), VID(2), 18, Vec::new(), Vec::new(), None), + ( + VID(0), + VID(2), + 15, + vec![("921".to_string(), Prop::U8(109))], + Vec::new(), + Some("b"), + ), + ( + VID(0), + VID(2), + 12, + vec![ + ("195".to_string(), Prop::Bool(false)), + ("287".to_string(), Prop::I64(92928934764462282)), + ], + Vec::new(), + None, + ), + ]; + check_graph_with_props(10, 10, &edges.into()); + } + + #[test] + fn add_multiple_edges_with_props_1() { + let edges = vec![ + ( + VID(0), + VID(0), + 0i64, + vec![("607".to_owned(), Prop::Bool(true))], + vec![ + ("688".to_owned(), Prop::str("791")), + ("59".to_owned(), Prop::I64(-570315263996158600)), + ("340".to_owned(), Prop::F64(-3.651023008388272e-78)), + ], + None, + ), + ( + VID(4), + VID(4), + 15, + vec![ + ("811".to_owned(), Prop::str("24")), + ("607".to_owned(), Prop::Bool(false)), + ], + vec![ + ("59".to_owned(), Prop::I64(4022071530038561966)), + ("340".to_owned(), Prop::F64(-4.79337077061449e-296)), + ], + Some("b"), + ), + ]; + check_graph_with_props(10, 10, &edges.into()); + } + + fn check_graph_with_nodes(node_page_len: usize, edge_page_len: usize, fixture: &NodeFixture) { + check_graph_with_nodes_support(fixture, false, |path| { + Layer::<()>::new(path, node_page_len, edge_page_len) + }); + } + + fn check_graph_with_props(node_page_len: usize, edge_page_len: usize, fixture: &Fixture) { + check_graph_with_props_support(fixture, false, |path| { + Layer::<()>::new(path, node_page_len, edge_page_len) + }); + } +} diff --git a/db4-storage/src/pages/node_page/mod.rs b/db4-storage/src/pages/node_page/mod.rs new file mode 100644 index 0000000000..d3baa81782 --- /dev/null +++ b/db4-storage/src/pages/node_page/mod.rs @@ -0,0 +1 @@ +pub mod writer; diff --git a/db4-storage/src/pages/node_page/node_page_view.rs b/db4-storage/src/pages/node_page/node_page_view.rs new file mode 100644 index 0000000000..3d36b3e2fb --- /dev/null +++ b/db4-storage/src/pages/node_page/node_page_view.rs @@ -0,0 +1,105 @@ +use crate::pages::edge_page::edge_page_view::TProp; +use crate::pages::node_page::mem_node_page::MemNodeSegment; +use crate::pages::{ + edge_page::edge_page_view::TimeCell, node_page::disk_node_page::DiskNodeSegment, +}; +use crate::LocalPOS; +use either::Either; +use raphtory_api::core::entities::{EID, VID}; + +#[derive(Debug, Clone, Copy)] +pub enum NodePageView<'a> { + Mem(&'a MemNodeSegment), + Disk(&'a DiskNodeSegment), +} + +impl<'a> NodePageView<'a> { + #[inline] + pub fn out_nbrs(self, n: LocalPOS) -> impl Iterator + 'a { + match self { + NodePageView::Mem(page) => Either::Left(page.out_nbrs(n)), + NodePageView::Disk(page) => Either::Right(page.out_nbrs(n)), + } + } + + #[inline] + pub fn inb_nbrs(self, n: LocalPOS) -> impl Iterator + 'a { + match self { + NodePageView::Mem(page) => Either::Left(page.inb_nbrs(n)), + NodePageView::Disk(page) => Either::Right(page.inb_nbrs(n)), + } + } + + #[inline] + pub fn out_edges(self, n: LocalPOS) -> impl Iterator + 'a { + match self { + NodePageView::Mem(page) => Either::Left(page.out_edges(n)), + NodePageView::Disk(page) => Either::Right(page.out_edges(n)), + } + } + + #[inline] + pub fn inb_edges(self, n: LocalPOS) -> impl Iterator + 'a { + match self { + NodePageView::Mem(page) => Either::Left(page.inb_edges(n)), + NodePageView::Disk(page) => Either::Right(page.inb_edges(n)), + } + } + + #[inline] + pub fn contains_out(self, n: LocalPOS, dst: VID) -> bool { + match self { + NodePageView::Mem(page) => page.contains_out(n, dst), + NodePageView::Disk(page) => page.contains_out(n, dst), + } + } + + #[inline] + pub fn contains_in(self, n: LocalPOS, dst: VID) -> bool { + match self { + NodePageView::Mem(page) => page.contains_in(n, dst), + NodePageView::Disk(page) => page.contains_in(n, dst), + } + } + + #[inline] + pub fn get_out_edge(self, n: LocalPOS, dst: VID) -> Option { + match self { + NodePageView::Mem(page) => page.get_out_edge(n, dst), + NodePageView::Disk(page) => page.get_out_edge(n, dst), + } + } + + #[inline] + pub fn get_in_edge(self, n: LocalPOS, dst: VID) -> Option { + match self { + NodePageView::Mem(page) => page.get_in_edge(n, dst), + NodePageView::Disk(page) => page.get_in_edge(n, dst), + } + } + + #[inline] + pub fn has_node(self, n: LocalPOS) -> bool { + match self { + NodePageView::Mem(page) => page.has_node(n), + NodePageView::Disk(page) => page.has_node(n), + } + } + + pub fn additions(self, n: LocalPOS) -> TimeCell<'a> { + match self { + NodePageView::Mem(page) => page.as_ref().additions(n), + NodePageView::Disk(page) => TimeCell::Disk { + props: page.prop_additions(n), + additions: page.edge_additions(n), + }, + } + } + + pub(crate) fn t_prop(self, edge_pos: LocalPOS, prop_id: usize) -> TProp<'a> { + match self { + NodePageView::Mem(page) => page.t_prop(edge_pos, prop_id), + NodePageView::Disk(page) => page.t_prop(edge_pos, prop_id, 0), + } + } +} diff --git a/db4-storage/src/pages/node_page/writer.rs b/db4-storage/src/pages/node_page/writer.rs new file mode 100644 index 0000000000..272276ea99 --- /dev/null +++ b/db4-storage/src/pages/node_page/writer.rs @@ -0,0 +1,188 @@ +use crate::{NodeSegmentOps, segments::node::MemNodeSegment}; +use db4_common::LocalPOS; +use raphtory::{ + core::{entities::ELID, storage::timeindex::AsTime}, + prelude::Prop, +}; +use raphtory_api::core::entities::{EID, VID}; +use std::{ops::DerefMut, sync::atomic::AtomicUsize}; + +#[derive(Debug)] +pub struct NodeWriter<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> { + pub page: &'a NS, + pub writer: MP, // TODO: rename to m_segment + pub global_num_nodes: &'a AtomicUsize, +} + +impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> NodeWriter<'a, MP, NS> { + pub fn new(page: &'a NS, global_num_nodes: &'a AtomicUsize, writer: MP) -> Self { + Self { + page, + writer, + global_num_nodes, + } + } + + pub fn add_outbound_edge( + &mut self, + t: T, + src_pos: impl Into, + dst: impl Into, + e_id: impl Into, + lsn: u64, + ) { + self.add_outbound_edge_inner(Some(t), src_pos, dst, e_id, lsn); + } + + pub fn add_static_outbound_edge( + &mut self, + src_pos: LocalPOS, + dst: impl Into, + e_id: impl Into, + lsn: u64, + ) { + self.add_outbound_edge_inner::(None, src_pos, dst, e_id, lsn); + } + + fn add_outbound_edge_inner( + &mut self, + t: Option, + src_pos: impl Into, + dst: impl Into, + e_id: impl Into, + lsn: u64, + ) { + let src_pos = src_pos.into(); + self.writer.as_mut().set_lsn(lsn); + + let e_id = e_id.into(); + let is_new_node = self.writer.add_outbound_edge(t, src_pos, dst, e_id); + + if is_new_node && !self.page.check_node(src_pos) { + self.page.increment_num_nodes(); + self.global_num_nodes + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + } + } + + pub fn add_inbound_edge( + &mut self, + t: T, + dst_pos: impl Into, + src: impl Into, + e_id: impl Into, + lsn: u64, + ) { + self.add_inbound_edge_inner(Some(t), dst_pos, src, e_id, lsn); + } + + pub fn add_static_inbound_edge( + &mut self, + dst_pos: LocalPOS, + src: impl Into, + e_id: impl Into, + lsn: u64, + ) { + self.add_inbound_edge_inner::(None, dst_pos, src, e_id, lsn); + } + + fn add_inbound_edge_inner( + &mut self, + t: Option, + dst_pos: impl Into, + src: impl Into, + e_id: impl Into, + lsn: u64, + ) { + self.writer.as_mut().set_lsn(lsn); + let e_id = e_id.into(); + let dst_pos = dst_pos.into(); + let is_new_node = self.writer.add_inbound_edge(t, dst_pos, src, e_id); + + if is_new_node && !self.page.check_node(dst_pos) { + self.page.increment_num_nodes(); + self.global_num_nodes + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + } + } + + pub fn add_props( + &mut self, + t: T, + pos: LocalPOS, + props: impl IntoIterator, + lsn: u64, + ) { + self.writer.as_mut().set_lsn(lsn); + self.writer.add_props(t, pos, props); + // self.est_size = self.page.increment_size(size_of::<(i64, i64)>()); + } + + pub fn update_c_props( + &mut self, + pos: LocalPOS, + props: impl IntoIterator, + lsn: u64, + ) { + self.writer.as_mut().set_lsn(lsn); + self.writer.update_c_props(pos, props); + // self.est_size = self.page.increment_size(size_of::<(i64, i64)>()); + } + + pub fn update_timestamp(&mut self, t: T, pos: LocalPOS, e_id: ELID, lsn: u64) { + self.writer.as_mut().set_lsn(lsn); + self.writer.update_timestamp(t, pos, e_id); + // self.est_size = self.page.increment_size(size_of::<(i64, i64)>()); + } + + pub fn get_out_edge(&self, pos: LocalPOS, dst: VID) -> Option { + self.page.get_out_edge(pos, dst, self.writer.deref()) + } + + pub fn get_inb_edge(&self, pos: LocalPOS, src: VID) -> Option { + self.page.get_inb_edge(pos, src, self.writer.deref()) + } +} + +impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> Drop + for NodeWriter<'a, MP, NS> +{ + fn drop(&mut self) { + // S::persist_node_page(self.est_size, self.page, self.writer.deref_mut()); + self.page + .notify_write(self.writer.deref_mut()) + .expect("Failed to persist node page"); + } +} + +pub enum WriterPair<'a, MP: DerefMut, NS: NodeSegmentOps> { + Same { + writer: NodeWriter<'a, MP, NS>, + }, + Different { + src_writer: NodeWriter<'a, MP, NS>, + dst_writer: NodeWriter<'a, MP, NS>, + }, +} + +impl<'a, MP: DerefMut, NS: NodeSegmentOps> WriterPair<'a, MP, NS> { + pub fn get_mut_src(&mut self) -> &mut NodeWriter<'a, MP, NS> { + match self { + WriterPair::Same { writer, .. } => writer, + WriterPair::Different { + src_writer: writer_i, + .. + } => writer_i, + } + } + + pub fn get_mut_dst(&mut self) -> &mut NodeWriter<'a, MP, NS> { + match self { + WriterPair::Same { writer, .. } => writer, + WriterPair::Different { + dst_writer: writer_j, + .. + } => writer_j, + } + } +} diff --git a/db4-storage/src/pages/node_store.rs b/db4-storage/src/pages/node_store.rs new file mode 100644 index 0000000000..bc3657195e --- /dev/null +++ b/db4-storage/src/pages/node_store.rs @@ -0,0 +1,248 @@ +use super::{node_page::writer::NodeWriter, resolve_pos}; +use crate::{ + NodeSegmentOps, + pages::locked::nodes::{LockedNodePage, WriteLockedNodePages}, + segments::node::MemNodeSegment, +}; +use db4_common::{LocalPOS, error::DBV4Error}; +use raphtory::core::entities::{EID, VID}; +use raphtory_api::core::entities::properties::meta::Meta; +use std::{ + collections::HashMap, + ops::DerefMut, + path::{Path, PathBuf}, + sync::{ + Arc, + atomic::{self, AtomicUsize}, + }, +}; + +#[derive(Debug)] +pub struct NodeStorageInner { + pages: boxcar::Vec>, + num_nodes: AtomicUsize, + nodes_path: PathBuf, + max_page_len: usize, + prop_meta: Arc, + ext: EXT, +} + +impl, EXT: Clone> NodeStorageInner { + pub fn layer( + nodes_path: impl AsRef, + max_page_len: usize, + meta: &Arc, + ext: EXT, + ) -> Self { + Self { + pages: boxcar::Vec::new(), + num_nodes: AtomicUsize::new(0), + nodes_path: nodes_path.as_ref().to_path_buf(), + max_page_len, + prop_meta: meta.clone(), + ext, + } + } + pub fn new(nodes_path: impl AsRef, max_page_len: usize, ext: EXT) -> Self { + Self { + pages: boxcar::Vec::new(), + num_nodes: AtomicUsize::new(0), + nodes_path: nodes_path.as_ref().to_path_buf(), + max_page_len, + prop_meta: Arc::new(Meta::new()), + ext, + } + } + + pub fn locked<'a>(&'a self) -> WriteLockedNodePages<'a, NS> { + WriteLockedNodePages::new( + self.pages + .iter() + .map(|(page_id, page)| { + LockedNodePage::new( + page_id, + &self.num_nodes, + self.max_page_len, + page.as_ref(), + page.head_mut(), + ) + }) + .collect(), + ) + } + + pub fn node<'a>(&'a self, node: impl Into) -> NS::Entry<'a> { + let (page_id, pos) = self.resolve_pos(node); + let node_page = self + .pages + .get(page_id) + .expect("Internal error: page not found"); + node_page.entry(pos) + } + + pub fn prop_meta(&self) -> &Arc { + &self.prop_meta + } + + #[inline(always)] + pub fn writer<'a>( + &'a self, + segment_id: usize, + ) -> NodeWriter<'a, impl DerefMut + 'a, NS> { + let segment = self.get_or_create_segment(segment_id); + let head = segment.head_mut(); + NodeWriter::new(segment, &self.num_nodes, head) + } + + pub fn num_nodes(&self) -> usize { + self.num_nodes.load(atomic::Ordering::Relaxed) + } + + pub fn pages(&self) -> &boxcar::Vec> { + &self.pages + } + + pub fn nodes_path(&self) -> &Path { + &self.nodes_path + } + + // pub fn iter<'a>(&'a self) -> impl Iterator> + 'a { + // self.pages() + // .iter() + // .flat_map(|(_, node_segment)| node_segment.iter(self.max_page_len)) + // } + + pub fn load( + nodes_path: impl AsRef, + max_page_len: usize, + ext: EXT, + ) -> Result { + let nodes_path = nodes_path.as_ref(); + + let meta = Arc::new(Meta::new()); + let mut pages = std::fs::read_dir(nodes_path)? + .filter(|entry| { + entry + .as_ref() + .ok() + .and_then(|entry| entry.file_type().ok().map(|ft| ft.is_dir())) + .unwrap_or_default() + }) + .filter_map(|entry| { + let entry = entry.ok()?; + let page_id = entry + .path() + .file_stem() + .and_then(|name| name.to_str().and_then(|name| name.parse::().ok()))?; + let page = NS::load(page_id, max_page_len, meta.clone(), nodes_path, ext.clone()) + .map(|page| (page_id, page)); + Some(page) + }) + .collect::, _>>()?; + + if pages.is_empty() { + return Err(DBV4Error::EmptyGraphDir(nodes_path.to_path_buf())); + } + + let max_page = Iterator::max(pages.keys().copied()).unwrap(); + + let pages = (0..=max_page) + .map(|page_id| { + let np = pages.remove(&page_id).unwrap_or_else(|| { + NS::new(page_id, max_page_len, meta.clone(), nodes_path, ext.clone()) + }); + Arc::new(np) + }) + .collect::>(); + + let first_page = pages.iter().next().unwrap().1; + let first_p_id = first_page.segment_id(); + + if first_p_id != 0 { + return Err(DBV4Error::GenericFailure(format!( + "First page id is not 0 in {:?}", + nodes_path + ))); + } + + let num_nodes = pages + .iter() + .map(|(_, page)| page.num_nodes()) + .sum::(); + + Ok(Self { + pages, + nodes_path: nodes_path.to_path_buf(), + max_page_len, + num_nodes: AtomicUsize::new(num_nodes), + prop_meta: meta, + ext, + }) + } + + /// Return the position of the chunk and the position within the chunk + pub fn resolve_pos(&self, i: impl Into) -> (usize, LocalPOS) { + resolve_pos(i.into(), self.max_page_len) + } + + pub fn get_edge(&self, src: VID, dst: VID) -> Option { + let (src_chunk, src_pos) = self.resolve_pos(src); + if src_chunk >= self.pages.count() { + return None; + } + let src_page = &self.pages[src_chunk]; + src_page.get_out_edge(src_pos, dst, src_page.head()) + } + + pub fn grow(&self, new_len: usize) { + self.get_or_create_segment(new_len - 1); + } + + pub fn get_or_create_segment(&self, segment_id: usize) -> &Arc { + if let Some(segment) = self.pages.get(segment_id) { + return segment; + } + let count = self.pages.count(); + if count >= segment_id + 1 { + // something has allocated the segment, wait for it to be added + loop { + if let Some(segment) = self.pages.get(segment_id) { + return segment; + } else { + // wait for the segment to be created + std::thread::yield_now(); + } + } + } else { + // we need to create the segment + self.pages.reserve(segment_id + 1 - count); + + loop { + let new_segment_id = self.pages.push_with(|segment_id| { + Arc::new(NS::new( + segment_id, + self.max_page_len, + self.prop_meta.clone(), + self.nodes_path.clone(), + self.ext.clone(), + )) + }); + + if new_segment_id >= segment_id { + loop { + if let Some(segment) = self.pages.get(segment_id) { + return segment; + } else { + // wait for the segment to be created + std::thread::yield_now(); + } + } + } + } + } + } + + pub fn max_page_len(&self) -> usize { + self.max_page_len + } +} diff --git a/db4-storage/src/pages/session.rs b/db4-storage/src/pages/session.rs new file mode 100644 index 0000000000..ebbb3768c5 --- /dev/null +++ b/db4-storage/src/pages/session.rs @@ -0,0 +1,219 @@ +use std::ops::DerefMut; + +use super::{ + GraphStore, edge_page::writer::EdgeWriter, node_page::writer::WriterPair, resolve_pos, +}; +use crate::{ + EdgeSegmentOps, NodeSegmentOps, + segments::{edge::MemEdgeSegment, node::MemNodeSegment}, +}; +use db4_common::error::DBV4Error; +use raphtory::{ + core::{ + entities::{EID, ELID, VID}, + storage::timeindex::AsTime, + }, + prelude::Prop, +}; +use raphtory_api::core::storage::dict_mapper::MaybeNew; + +pub struct WriteSession< + 'a, + MNS: DerefMut + 'a, + MES: DerefMut + 'a, + NS: NodeSegmentOps, + ES: EdgeSegmentOps, + EXT, +> { + node_writers: WriterPair<'a, MNS, NS>, + edge_writer: Option>, + graph: &'a GraphStore, +} + +impl< + 'a, + MNS: DerefMut + 'a, + MES: DerefMut + 'a, + NS: NodeSegmentOps, + ES: EdgeSegmentOps, + EXT: Clone + Default, +> WriteSession<'a, MNS, MES, NS, ES, EXT> +{ + pub fn new( + node_writers: WriterPair<'a, MNS, NS>, + edge_writer: Option>, + graph: &'a GraphStore, + ) -> Self { + Self { + node_writers, + edge_writer, + graph, + } + } + + pub fn add_edge_into_layer( + &mut self, + t: T, + src: impl Into, + dst: impl Into, + edge: MaybeNew, + lsn: u64, + props: impl IntoIterator, + ) -> Result<(), DBV4Error> { + let src = src.into(); + let dst = dst.into(); + let e_id = edge.inner(); + + let edge_writer = self + .edge_writer + .as_mut() + .expect("Internal Error: Edge writer is not set"); + + let node_max_page_len = self + .node_writers + .get_mut_src() + .writer + .as_ref() + .max_page_len(); + let edge_max_page_len = edge_writer.writer.as_ref().max_page_len(); + + let (_, src_pos) = resolve_pos(src, node_max_page_len); + let (_, dst_pos) = resolve_pos(dst, node_max_page_len); + let (_, edge_pos) = resolve_pos(e_id.edge, edge_max_page_len); + + let exists = Some(!edge.is_new()); + edge_writer.add_edge(t, Some(edge_pos), src, dst, props, lsn, exists)?; + + edge.if_new(|edge_id| { + self.node_writers + .get_mut_src() + .add_outbound_edge(t, src_pos, dst, edge_id, lsn); + self.node_writers + .get_mut_dst() + .add_inbound_edge(t, dst_pos, src, edge_id, lsn); + edge_id + }); + + self.node_writers + .get_mut_src() + .update_timestamp(t, src_pos, e_id, lsn); + self.node_writers + .get_mut_dst() + .update_timestamp(t, dst_pos, e_id, lsn); + + Ok(()) + } + + pub fn add_static_edge( + &mut self, + src: impl Into, + dst: impl Into, + lsn: u64, + ) -> Result, DBV4Error> { + let src = src.into(); + let dst = dst.into(); + + let (_, src_pos) = self.graph.nodes().resolve_pos(src); + let (_, dst_pos) = self.graph.nodes().resolve_pos(dst); + + if let Some(e_id) = self.node_writers.get_mut_src().get_out_edge(src_pos, dst) { + let mut edge_writer = self.graph.edge_writer(e_id); + let (_, edge_pos) = self.graph.edges().resolve_pos(e_id); + edge_writer.add_static_edge(Some(edge_pos), src, dst, lsn, None)?; + + Ok(MaybeNew::Existing(e_id)) + } else { + if let Some(e_id) = self.node_writers.get_mut_src().get_out_edge(src_pos, dst) { + let mut edge_writer = self.graph.edge_writer(e_id); + let (_, edge_pos) = self.graph.edges().resolve_pos(e_id); + + edge_writer.add_static_edge(Some(edge_pos), src, dst, lsn, None)?; + + Ok(MaybeNew::Existing(e_id)) + } else { + let mut edge_writer = self.graph.get_free_writer(); + let edge_id = edge_writer.add_static_edge(None, src, dst, lsn, None)?; + let edge_id = + edge_id.as_eid(edge_writer.segment_id(), self.graph.edges().max_page_len()); + + self.node_writers.get_mut_src().add_static_outbound_edge( + src_pos, + dst, + edge_id.with_layer(0), + lsn, + ); + self.node_writers.get_mut_dst().add_static_inbound_edge( + dst_pos, + src, + edge_id.with_layer(0), + lsn, + ); + + Ok(MaybeNew::New(edge_id)) + } + } + } + + pub fn internal_add_edge( + &mut self, + t: T, + src: impl Into, + dst: impl Into, + lsn: u64, + layer: usize, + props: impl IntoIterator, + ) -> Result, DBV4Error> { + let src = src.into(); + let dst = dst.into(); + + let (_, src_pos) = self.graph.nodes().resolve_pos(src); + let (_, dst_pos) = self.graph.nodes().resolve_pos(dst); + + if let Some(e_id) = self.node_writers.get_mut_src().get_out_edge(src_pos, dst) { + let mut edge_writer = self.graph.edge_writer(e_id); + let (_, edge_pos) = self.graph.edges().resolve_pos(e_id); + edge_writer.add_edge(t, Some(edge_pos), src, dst, props, lsn, None)?; + let e_id = e_id.with_layer(layer); + + self.node_writers + .get_mut_src() + .update_timestamp(t, src_pos, e_id, lsn); + self.node_writers + .get_mut_dst() + .update_timestamp(t, dst_pos, e_id, lsn); + + Ok(MaybeNew::Existing(e_id)) + } else { + if let Some(e_id) = self.node_writers.get_mut_src().get_out_edge(src_pos, dst) { + let mut edge_writer = self.graph.edge_writer(e_id); + let (_, edge_pos) = self.graph.edges().resolve_pos(e_id); + let e_id = e_id.with_layer(layer); + + edge_writer.add_edge(t, Some(edge_pos), src, dst, props, lsn, None)?; + self.node_writers + .get_mut_src() + .update_timestamp(t, src_pos, e_id, lsn); + self.node_writers + .get_mut_dst() + .update_timestamp(t, dst_pos, e_id, lsn); + + Ok(MaybeNew::Existing(e_id)) + } else { + let mut edge_writer = self.graph.get_free_writer(); + let edge_id = edge_writer.add_edge(t, None, src, dst, props, lsn, None)?; + let edge_id = + edge_id.as_eid(edge_writer.segment_id(), self.graph.edges().max_page_len()); + let edge_id = edge_id.with_layer(layer); + + self.node_writers + .get_mut_src() + .add_outbound_edge(t, src_pos, dst, edge_id, lsn); + self.node_writers + .get_mut_dst() + .add_inbound_edge(t, dst_pos, src, edge_id, lsn); + + Ok(MaybeNew::New(edge_id)) + } + } + } +} diff --git a/db4-storage/src/pages/test_utils.rs b/db4-storage/src/pages/test_utils.rs new file mode 100644 index 0000000000..9f0e6b082c --- /dev/null +++ b/db4-storage/src/pages/test_utils.rs @@ -0,0 +1,869 @@ +use std::{ + collections::{HashMap, HashSet}, + fs::File, + io::Write, + path::Path, +}; + +use bigdecimal::BigDecimal; +use chrono::{DateTime, NaiveDateTime, Utc}; +use db4_common::error::DBV4Error; +use either::Either::Left; +use itertools::Itertools; +use proptest::{collection, prelude::*}; +use raphtory::{ + core::{ + entities::{VID, graph::logical_to_physical::Mapping}, + storage::timeindex::TimeIndexOps, + }, + prelude::Prop, +}; +use raphtory_api::core::entities::properties::{ + prop::{DECIMAL_MAX, PropType}, + tprop::TPropOps, +}; +use rayon::prelude::*; + +use crate::{ + EdgeEntryOps, EdgeRefOps, EdgeSegmentOps, NodeEntryOps, NodeRefOps, NodeSegmentOps, + loaders::{FileFormat, Loader}, + pages::GraphStore, +}; + +pub fn check_edges_support< + NS: NodeSegmentOps, + ES: EdgeSegmentOps, + EXT: Clone + Default + Send + Sync, +>( + edges: Vec<(impl Into, impl Into)>, + par_load: bool, + check_load: bool, + make_graph: impl FnOnce(&Path) -> GraphStore, +) { + let mut edges = edges + .into_iter() + .map(|(src, dst)| (src.into(), dst.into())) + .collect::>(); + + let graph_dir = tempfile::tempdir().unwrap(); + let graph = make_graph(graph_dir.path()); + let mut nodes = HashSet::new(); + + for (src, dst) in &edges { + nodes.insert(*src); + nodes.insert(*dst); + } + + if par_load { + edges + .par_iter() + .try_for_each(|(src, dst)| { + let _ = graph.add_edge(0, *src, *dst)?; + Ok::<_, DBV4Error>(()) + }) + .expect("Failed to add edge"); + } else { + edges + .iter() + .try_for_each(|(src, dst)| { + let _ = graph.add_edge(0, *src, *dst)?; + Ok::<_, DBV4Error>(()) + }) + .expect("Failed to add edge"); + } + + let actual_num_nodes = graph.nodes().num_nodes(); + assert_eq!(actual_num_nodes, nodes.len()); + + edges.sort_unstable(); + + fn check< + NS: NodeSegmentOps, + ES: EdgeSegmentOps, + EXT: Clone + Default, + >( + stage: &str, + es: &[(VID, VID)], + graph: &GraphStore, + ) { + let nodes = graph.nodes(); + let edges = graph.edges(); + + if !es.is_empty() { + assert!(nodes.pages().count() > 0, "{stage}"); + } + + let mut expected_graph: HashMap, Vec)> = es + .iter() + .chunk_by(|(src, _)| *src) + .into_iter() + .map(|(src, edges)| { + let mut out: Vec<_> = edges.map(|(_, dst)| *dst).collect(); + out.sort_unstable(); + out.dedup(); + (src, (out, vec![])) + }) + .collect::>(); + + let mut edges_sorted_by_dest = es.to_vec(); + edges_sorted_by_dest.sort_unstable_by_key(|(_, dst)| *dst); + + // now inbounds + edges_sorted_by_dest + .iter() + .chunk_by(|(_, dst)| *dst) + .into_iter() + .for_each(|(dst, edges)| { + let mut edges: Vec<_> = edges.map(|(src, _)| *src).collect(); + edges.sort_unstable(); + edges.dedup(); + let (_, inb) = expected_graph.entry(dst).or_default(); + *inb = edges; + }); + + for (n, (exp_out, exp_inb)) in expected_graph { + let entry = nodes.node(n); + + let adj = entry.as_ref(); + let out_nbrs: Vec<_> = adj.out_nbrs_sorted().collect(); + assert_eq!(out_nbrs, exp_out, "{stage} node: {:?}", n); + + let in_nbrs: Vec<_> = adj.inb_nbrs_sorted().collect(); + assert_eq!(in_nbrs, exp_inb, "{stage} node: {:?}", n); + + for (exp_dst, eid) in adj.out_edges() { + let (src, dst) = edges.get_edge(eid).unwrap(); + assert_eq!(src, n, "{stage}"); + assert_eq!(dst, exp_dst, "{stage}"); + } + + for (exp_src, eid) in adj.inb_edges() { + let (src, dst) = edges.get_edge(eid).unwrap(); + assert_eq!(src, exp_src, "{stage}"); + assert_eq!(dst, n, "{stage}"); + } + } + } + + check("pre-drop", &edges, &graph); + if check_load { + drop(graph); + + let maybe_ns = GraphStore::::load(graph_dir.path()); + if edges.is_empty() { + assert!(maybe_ns.is_err()); + } else { + match maybe_ns { + Ok(graph) => { + check("post-drop", &edges, &graph); + } + Err(e) => { + panic!("Failed to load graph: {:?}", e); + } + } + } + } +} + +pub fn check_graph_with_nodes_support< + EXT: Clone + Default + Send + Sync, + NS: NodeSegmentOps, + ES: EdgeSegmentOps, +>( + fixture: &NodeFixture, + check_load: bool, + make_graph: impl FnOnce(&Path) -> GraphStore, +) { + let NodeFixture { + temp_props, + const_props, + } = fixture; + + let graph_dir = tempfile::tempdir().unwrap(); + let graph = make_graph(graph_dir.path()); + + for (node, t, t_props) in temp_props { + let err = graph.add_node_props(*t, *node, t_props.clone()); + + assert!(err.is_ok(), "Failed to add node: {:?}", err); + } + + for (node, const_props) in const_props { + let err = graph.update_node_const_props(*node, const_props.clone()); + + assert!(err.is_ok(), "Failed to add node: {:?}", err); + } + + let check_fn = |temp_props: &[(VID, i64, Vec<(String, Prop)>)], + const_props: &[(VID, Vec<(String, Prop)>)], + graph: &GraphStore| { + let mut ts_for_nodes = HashMap::new(); + for (node, t, _) in temp_props { + ts_for_nodes.entry(*node).or_insert_with(|| vec![]).push(*t); + } + ts_for_nodes.iter_mut().for_each(|(_, ts)| { + ts.sort_unstable(); + }); + + for (node, ts_expected) in ts_for_nodes { + let ne = graph.nodes().node(node); + let node_entry = ne.as_ref(); + let actual: Vec<_> = node_entry.additions().iter_t().collect(); + assert_eq!( + actual, ts_expected, + "Expected node additions for node ({node:?})", + ); + } + + let mut const_props_values = HashMap::new(); + for (node, const_props) in const_props { + let node = *node; + for (name, prop) in const_props { + const_props_values + .entry((node, name)) + .or_insert_with(|| HashSet::new()) + .insert(prop.clone()); + } + } + + for ((node, name), const_props) in const_props_values { + let ne = graph.nodes().node(node); + let node_entry = ne.as_ref(); + + let prop_id = graph + .node_meta() + .const_prop_meta() + .get_id(&name) + .unwrap_or_else(|| panic!("Failed to get prop id for {}", name)); + let actual_props = node_entry.c_prop(prop_id); + + if !const_props.is_empty() { + let actual_prop = actual_props + .unwrap_or_else(|| panic!("Failed to get prop {name} for {node:?}")); + assert!( + const_props.contains(&actual_prop), + "failed to get const prop {name} for {node:?}, expected {:?}, got {:?}", + const_props, + actual_prop + ); + } + } + + let mut nod_t_prop_groups = HashMap::new(); + for (node, t, t_props) in temp_props { + let node = *node; + let t = *t; + + for (prop_name, prop) in t_props { + let prop_values = nod_t_prop_groups + .entry((node, prop_name)) + .or_insert_with(|| vec![]); + prop_values.push((t, prop.clone())); + } + } + + nod_t_prop_groups.iter_mut().for_each(|(_, props)| { + props.sort_unstable_by_key(|(t, _)| *t); + }); + + for ((node, prop_name), props) in nod_t_prop_groups { + let prop_id = graph + .node_meta() + .temporal_prop_meta() + .get_id(&prop_name) + .unwrap_or_else(|| panic!("Failed to get prop id for {}", prop_name)); + + let ne = graph.nodes().node(node); + let node_entry = ne.as_ref(); + let actual_props = node_entry.t_prop(prop_id).iter_t().collect::>(); + + assert_eq!( + actual_props, props, + "Expected properties for node ({:?}) to be {:?}, but got {:?}", + node, props, actual_props + ); + } + }; + + check_fn(temp_props, const_props, &graph); + + if check_load { + drop(graph); + let graph = GraphStore::::load(graph_dir.path()).unwrap(); + check_fn(temp_props, const_props, &graph); + } +} + +pub fn check_graph_with_props_support< + EXT: Clone + Default + Send + Sync, + NS: NodeSegmentOps, + ES: EdgeSegmentOps, +>( + fixture: &Fixture, + check_load: bool, + make_graph: impl FnOnce(&Path) -> GraphStore, +) { + let Fixture { edges, const_props } = fixture; + let graph_dir = tempfile::tempdir().unwrap(); + let graph = make_graph(graph_dir.path()); + + // Add edges + for (src, dst, t, t_props, _, _) in edges { + let err = graph.add_edge_props(*t, *src, *dst, t_props.clone(), 0); + + assert!(err.is_ok(), "Failed to add edge: {:?}", err); + } + + // Add const props + for ((src, dst), const_props) in const_props { + let eid = graph + .nodes() + .get_edge(*src, *dst) + .unwrap_or_else(|| panic!("Failed to get edge ({:?}, {:?}) from graph", src, dst)); + let res = graph.update_edge_const_props(eid, const_props.clone()); + + assert!( + res.is_ok(), + "Failed to update edge const props: {:?} {src:?} -> {dst:?}", + res + ); + } + + assert!(graph.edges().num_edges() > 0); + + let check_fn = |edges: &[AddEdge], graph: &GraphStore| { + let mut edge_groups = HashMap::new(); + let mut node_groups: HashMap> = HashMap::new(); + + // Group temporal edge props and their timestamps + for (src, dst, t, t_props, _, _) in edges { + let src = *src; + let dst = *dst; + let t = *t; + + for (prop_name, prop) in t_props { + let prop_values = edge_groups + .entry((src, dst, prop_name)) + .or_insert_with(|| vec![]); + prop_values.push((t, prop.clone())); + } + } + + edge_groups.iter_mut().for_each(|(_, props)| { + props.sort_unstable_by_key(|(t, _)| *t); + }); + + // Group node additions and their timestamps + for (src, dst, t, _, _, _) in edges { + let src = *src; + let dst = *dst; + let t = *t; + + // Include src additions + node_groups.entry(src).or_insert_with(|| vec![]).push(t); + + // Self-edges don't have dst additions, so skip + if src == dst { + continue; + } + + // Include dst additions + node_groups.entry(dst).or_insert_with(|| vec![]).push(t); + } + + node_groups.iter_mut().for_each(|(_, ts)| { + ts.sort_unstable(); + }); + + for ((src, dst, prop_name), props) in edge_groups { + // Check temporal props + let prop_id = graph + .edge_meta() + .temporal_prop_meta() + .get_id(&prop_name) + .unwrap_or_else(|| panic!("Failed to get prop id for {}", prop_name)); + + let edge = graph + .nodes() + .get_edge(src, dst) + .unwrap_or_else(|| panic!("Failed to get edge ({:?}, {:?}) from graph", src, dst)); + let edge = graph.edges().edge(edge); + let e = edge.as_ref(); + let actual_props = e.t_prop(prop_id).iter_t().collect::>(); + + assert_eq!( + actual_props, props, + "Expected properties for edge ({:?}, {:?}) to be {:?}, but got {:?}", + src, dst, props, actual_props + ); + + // Check const props + if let Some(exp_const_props) = const_props.get(&(src, dst)) { + for (name, prop) in exp_const_props { + let prop_id = graph + .edge_meta() + .const_prop_meta() + .get_id(name) + .unwrap_or_else(|| panic!("Failed to get prop id for {}", name)); + let actual_props = e.c_prop(prop_id); + assert_eq!( + actual_props.as_ref(), + Some(prop), + "Expected const properties for edge ({:?}, {:?}) to be {:?}, but got {:?}", + src, + dst, + prop, + actual_props + ); + } + } + } + + // Check node additions and their timestamps + for (node_id, ts) in node_groups { + let node = graph.nodes().node(node_id); + let node_entry = node.as_ref(); + let actual_additions_ts = node_entry.additions().iter_t().collect::>(); + + assert_eq!( + actual_additions_ts, ts, + "Expected node additions for node ({:?}) to be {:?}, but got {:?}", + node_id, ts, actual_additions_ts + ); + } + }; + + check_fn(edges, &graph); + + if check_load { + // Load the graph from disk and check again + drop(graph); + + let graph = GraphStore::::load(graph_dir.path()).unwrap(); + check_fn(edges, &graph); + } +} + +pub fn check_load_support< + EXT: Clone + Default + Send + Sync, + NS: NodeSegmentOps, + ES: EdgeSegmentOps, +>( + edges: &[(i64, u64, u64)], + check_load: bool, + make_graph: impl FnOnce(&Path) -> GraphStore, +) { + // Create temporary directory and CSV file + let temp_dir = tempfile::tempdir().unwrap(); + let csv_path = temp_dir.path().join("edges.csv"); + + // Write edges to CSV file + let mut file = File::create(&csv_path).unwrap(); + writeln!(file, "src,time,dst,test").unwrap(); + for (time, src, dst) in edges { + writeln!(file, "{},{},{},a", src, time, dst).unwrap(); + } + file.flush().unwrap(); + + // Create graph store + let graph_dir = temp_dir.path().join("graph"); + std::fs::create_dir_all(&graph_dir).unwrap(); + let graph = make_graph(&graph_dir); + + // Create loader and load data + let loader = Loader::new( + &csv_path, + Left("src"), + Left("dst"), + Left("time"), + FileFormat::CSV { + delimiter: b',', + has_header: true, + sample_records: 10, + }, + ) + .unwrap(); + + let resolver = loader.load_into(&graph, 1024).unwrap(); + + fn check_graph< + NS: NodeSegmentOps, + ES: EdgeSegmentOps, + EXT: Clone + Default + Send + Sync, + >( + edges: &[(i64, u64, u64)], + graph: &GraphStore, + resolver: &Mapping, + label: &str, + ) { + // Create expected adjacency data + let mut expected_out_edges: HashMap> = HashMap::new(); + let mut expected_in_edges: HashMap> = HashMap::new(); + let mut reverse_resolver = vec![0; resolver.len()]; + + for &(_, src, dst) in edges { + expected_out_edges.entry(src).or_default().push(dst); + expected_in_edges.entry(dst).or_default().push(src); + let id = resolver + .get_u64(src) + .unwrap_or_else(|| panic!("Missing src node {}", src)); + reverse_resolver[id.0] = src; + let id = resolver + .get_u64(dst) + .unwrap_or_else(|| panic!("Missing dst node {}", dst)); + reverse_resolver[id.0] = dst; + } + + // Deduplicate expected edges + for values in expected_out_edges.values_mut() { + values.sort_unstable(); + values.dedup(); + } + + for values in expected_in_edges.values_mut() { + values.sort_unstable(); + values.dedup(); + } + + let expected_num_edges = expected_out_edges.values().map(Vec::len).sum::(); + // let expected_num_nodes = expected_out_edges.keys().chain(expected_in_edges.keys()).collect::>().len(); + + // Verify graph structure + let nodes = graph.nodes(); + let edges_store = graph.edges(); + + // assert_eq!(nodes.num_nodes(), expected_num_nodes); + assert_eq!( + edges_store.num_edges(), + expected_num_edges, + "Bad number of edges {label}" + ); + + for (exp_src, expected_outs) in expected_out_edges { + for &exp_dst in &expected_outs { + let src_vid = resolver.get_u64(exp_src).unwrap(); + let dst_vid = resolver.get_u64(exp_dst).unwrap(); + + let edge_id = graph + .nodes() + .get_edge(src_vid, dst_vid) + .expect("Edge not found"); + let edge = edges_store.edge(edge_id); + let (src, dst) = edge.as_ref().edge().unwrap(); + let (src_act, dst_act) = (reverse_resolver[src.0], reverse_resolver[dst.0]); + + assert_eq!( + (src_act, dst_act), + (exp_src, exp_dst), + "{label} Bad Edge {} -> {}", + exp_src, + exp_dst, + ); + } + + let adj = graph.nodes().node(resolver.get_u64(exp_src).unwrap()); + let adj = adj.as_ref(); + + let mut out_neighbours: Vec<_> = adj + .out_nbrs_sorted() + .map(|VID(id)| reverse_resolver[id]) + .collect(); + out_neighbours.sort_unstable(); + let mut expected_outs: Vec<_> = expected_outs.iter().copied().collect(); + expected_outs.sort_unstable(); + + assert_eq!( + out_neighbours, expected_outs, + "{label} Outbound edges don't match for node {}", + exp_src + ); + + // Check edge lookup works and edge_id points to the right (src, dst) + for (exp_dst, edge_id) in adj.out_edges() { + let (VID(src), dst) = edges_store.get_edge(edge_id).unwrap(); + assert_eq!(reverse_resolver[src], exp_src); + assert_eq!(dst, exp_dst); + } + } + + for (exp_dst, expected_ins) in expected_in_edges { + let adj = nodes.node(resolver.get_u64(exp_dst).unwrap()); + let adj = adj.as_ref(); + + let mut in_neighbours: Vec<_> = adj + .inb_nbrs_sorted() + .map(|VID(id)| reverse_resolver[id]) + .collect(); + in_neighbours.sort_unstable(); + let mut expected_ins: Vec<_> = expected_ins.iter().copied().collect(); + expected_ins.sort_unstable(); + + assert_eq!( + in_neighbours, expected_ins, + "Inbound edges don't match for node {}", + exp_dst + ); + + // Check edge lookup works + for (exp_src, edge_id) in adj.inb_edges() { + let (src, VID(dst)) = edges_store.get_edge(edge_id).unwrap(); + assert_eq!(reverse_resolver[dst], exp_dst); + assert_eq!(src, exp_src); + } + } + } + + check_graph(edges, &graph, &resolver, "pre-drop"); + if check_load { + drop(graph); + + // Reload graph and check again + let graph = GraphStore::::load(&graph_dir).unwrap(); + check_graph(edges, &graph, &resolver, "post-drop"); + } +} + +pub fn edges_strat(size: usize) -> impl Strategy> { + (1..=size).prop_flat_map(|num_nodes| { + let num_edges = 0..(num_nodes * num_nodes); + let srcs = (0usize..num_nodes).prop_map(VID); + let dsts = (0usize..num_nodes).prop_map(VID); + num_edges.prop_flat_map(move |num_edges| { + collection::vec((srcs.clone(), dsts.clone()), num_edges as usize) + }) + }) +} + +pub type AddEdge = ( + VID, + VID, + i64, + Vec<(String, Prop)>, + Vec<(String, Prop)>, + Option<&'static str>, +); + +#[derive(Debug)] +pub struct NodeFixture { + pub temp_props: Vec<(VID, i64, Vec<(String, Prop)>)>, + pub const_props: Vec<(VID, Vec<(String, Prop)>)>, +} + +#[derive(Debug)] +pub struct Fixture { + pub edges: Vec, + pub const_props: HashMap<(VID, VID), Vec<(String, Prop)>>, +} + +impl From> for Fixture { + fn from(edges: Vec) -> Self { + let mut const_props = HashMap::new(); + for (src, dst, _, _, c_props, _) in &edges { + for (k, v) in c_props { + const_props + .entry((*src, *dst)) + .or_insert_with(|| vec![]) + .push((k.clone(), v.clone())); + } + } + const_props.iter_mut().for_each(|(_, v)| { + v.sort_by(|a, b| a.0.cmp(&b.0)); + v.dedup_by(|a, b| a.0 == b.0); + }); + Self { edges, const_props } + } +} + +pub fn make_edges(num_edges: usize, num_nodes: usize) -> impl Strategy { + assert!(num_edges > 0); + assert!(num_nodes > 0); + (1..=num_edges, 1..=num_nodes) + .prop_flat_map(|(len, num_nodes)| build_raw_edges(len, num_nodes)) + .prop_map(|edges| edges.into()) +} + +pub fn make_nodes(num_nodes: usize) -> impl Strategy { + assert!(num_nodes > 0); + let schema = proptest::collection::hash_map( + (0i32..1000).prop_map(|i| i.to_string()), + prop_type(), + 0..30, + ); + + schema.prop_flat_map(move |schema| { + let (t_props, c_props) = make_props(&schema); + let temp_props = proptest::collection::vec( + ((0..num_nodes).prop_map(VID), 0i64..1000, t_props), + 1..=num_nodes, + ); + + let const_props = + proptest::collection::vec(((0..num_nodes).prop_map(VID), c_props), 1..=num_nodes); + + (temp_props, const_props).prop_map(|(temp_props, const_props)| NodeFixture { + temp_props, + const_props, + }) + }) +} + +pub fn build_raw_edges( + len: usize, + num_nodes: usize, +) -> impl Strategy< + Value = Vec<( + VID, + VID, + i64, + Vec<(String, Prop)>, + Vec<(String, Prop)>, + Option<&'static str>, + )>, +> { + proptest::collection::hash_map((0i32..1000).prop_map(|i| i.to_string()), prop_type(), 0..20) + .prop_flat_map(move |schema| { + let (t_props, c_props) = make_props(&schema); + + proptest::collection::vec( + ( + (0..num_nodes).prop_map(VID), + (0..num_nodes).prop_map(VID), + 0i64..(num_nodes as i64 * 5), + t_props, + c_props, + proptest::sample::select(vec![Some("a"), Some("b"), None]), + ), + 1..=len, + ) + }) +} + +pub fn prop_type() -> impl Strategy { + let leaf = proptest::sample::select(&[ + PropType::Str, + PropType::I64, + PropType::F64, + PropType::F32, + PropType::I32, + PropType::U8, + PropType::Bool, + PropType::DTime, + PropType::NDTime, + PropType::Decimal { scale: 7 }, // decimal breaks the tests because of polars-parquet + ]); + + // leaf.prop_recursive(3, 10, 10, |inner| { + // let dict = proptest::collection::hash_map(r"\w{1,10}", inner.clone(), 1..10) + // .prop_map(|map| PropType::map(map)); + // let list = inner + // .clone() + // .prop_map(|p_type| PropType::List(Box::new(p_type))); + // prop_oneof![inner, list, dict] + // }) + leaf +} + +pub fn make_props( + schema: &HashMap, +) -> ( + BoxedStrategy>, + BoxedStrategy>, +) { + let mut iter = schema.iter(); + + // split in half, one temporal one constant + let t_prop_s = (&mut iter) + .take(schema.len() / 2) + .map(|(k, v)| (k.clone(), v.clone())) + .collect::>(); + let c_prop_s = iter + .map(|(k, v)| (k.clone(), v.clone())) + .collect::>(); + + let num_tprops = t_prop_s.len(); + let num_cprops = c_prop_s.len(); + + let t_props = proptest::sample::subsequence(t_prop_s, 0..=num_tprops).prop_flat_map(|schema| { + schema + .into_iter() + .map(|(k, v)| prop(&v).prop_map(move |prop| (k.clone(), prop))) + .collect::>() + }); + let c_props = proptest::sample::subsequence(c_prop_s, 0..=num_cprops).prop_flat_map(|schema| { + schema + .into_iter() + .map(|(k, v)| prop(&v).prop_map(move |prop| (k.clone(), prop))) + .collect::>() + }); + (t_props.boxed(), c_props.boxed()) +} + +pub(crate) fn prop(p_type: &PropType) -> impl Strategy + use<> { + match p_type { + PropType::Str => (0i32..1000).prop_map(|s| Prop::str(s.to_string())).boxed(), + PropType::I64 => any::().prop_map(Prop::I64).boxed(), + PropType::I32 => any::().prop_map(Prop::I32).boxed(), + PropType::F64 => any::().prop_map(Prop::F64).boxed(), + PropType::F32 => any::().prop_map(Prop::F32).boxed(), + PropType::U8 => any::().prop_map(Prop::U8).boxed(), + PropType::Bool => any::().prop_map(Prop::Bool).boxed(), + PropType::DTime => (1900..2024, 1..=12, 1..28, 0..24, 0..60, 0..60) + .prop_map(|(year, month, day, h, m, s)| { + Prop::DTime( + format!( + "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z", + year, month, day, h, m, s + ) + .parse::>() + .unwrap(), + ) + }) + .boxed(), + PropType::NDTime => (1970..2024, 1..=12, 1..28, 0..24, 0..60, 0..60) + .prop_map(|(year, month, day, h, m, s)| { + // 2015-09-18T23:56:04 + Prop::NDTime( + format!( + "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}", + year, month, day, h, m, s + ) + .parse::() + .unwrap(), + ) + }) + .boxed(), + PropType::List(p_type) => proptest::collection::vec(prop(p_type), 0..10) + .prop_map(|props| Prop::List(props.into())) + .boxed(), + PropType::Map(p_types) => { + let prop_types: Vec> = p_types + .iter() + .map(|(a, b)| (a.clone(), b.clone())) + .collect::>() + .into_iter() + .map(|(name, p_type)| { + let pt_strat = prop(&p_type) + .prop_map(move |prop| (name.clone(), prop.clone())) + .boxed(); + pt_strat + }) + .collect_vec(); + + let props = proptest::sample::select(prop_types).prop_flat_map(|prop| prop); + + proptest::collection::vec(props, 1..10) + .prop_map(|props| Prop::map(props)) + .boxed() + } + PropType::Decimal { scale } => { + let scale = *scale; + let dec_max = DECIMAL_MAX; + ((scale as i128)..dec_max) + .prop_map(move |int| Prop::Decimal(BigDecimal::new(int.into(), scale))) + .boxed() + } + pt => { + panic!("Unsupported prop type: {:?}", pt); + } + } +} diff --git a/db4-storage/src/properties/mod.rs b/db4-storage/src/properties/mod.rs new file mode 100644 index 0000000000..900e8cd565 --- /dev/null +++ b/db4-storage/src/properties/mod.rs @@ -0,0 +1,326 @@ +use bigdecimal::ToPrimitive; +use polars_arrow::array::{Array, BooleanArray, PrimitiveArray, Utf8ViewArray}; +use raphtory::{ + core::{ + entities::{ + ELID, + nodes::node_store::PropTimestamps, + properties::{tcell::TCell, tprop::TPropCell}, + }, + storage::{PropColumn, TColumns, timeindex::TimeIndexEntry}, + }, + prelude::Prop, +}; +use raphtory_api::core::entities::properties::{meta::PropMapper, prop::PropType}; + +pub mod props_meta_writer; + +#[derive(Debug, Default)] +pub struct Properties { + c_properties: Vec, + t_index: Vec, + t_properties: TColumns, + earliest: Option, + latest: Option, + has_node_additions: bool, + has_node_properties: bool, +} + +pub(crate) struct PropMutEntry<'a> { + row: usize, + properties: &'a mut Properties, +} + +#[derive(Debug, Clone, Copy)] +pub struct PropEntry<'a> { + row: usize, + properties: &'a Properties, +} + +impl Properties { + pub fn est_size(&self) -> usize { + self.t_properties.len() + self.c_properties.len() + } + + pub(crate) fn get_mut_entry(&mut self, row: usize) -> PropMutEntry { + PropMutEntry { + row, + properties: self, + } + } + + pub(crate) fn get_entry(&self, row: usize) -> PropEntry { + PropEntry { + row, + properties: self, + } + } + + pub fn earliest(&self) -> Option { + self.earliest + } + + pub fn latest(&self) -> Option { + self.latest + } + + pub fn t_column(&self, prop_id: usize) -> Option<&PropColumn> { + self.t_properties.get(prop_id) + } + + pub fn c_column(&self, prop_id: usize) -> Option<&PropColumn> { + self.c_properties.get(prop_id) + } + + pub fn num_t_columns(&self) -> usize { + self.t_properties.num_columns() + } + + pub fn num_c_columns(&self) -> usize { + self.c_properties.len() + } + + pub(crate) fn temporal_index(&self, row: usize) -> Option<&PropTimestamps> { + self.t_index.get(row) + } + + pub fn has_node_properties(&self) -> bool { + self.has_node_properties + } + + pub fn has_node_additions(&self) -> bool { + self.has_node_additions + } + + pub(crate) fn column_as_array( + &self, + column: &PropColumn, + col_id: usize, + meta: &PropMapper, + indices: impl ExactSizeIterator, + ) -> Option> { + match column { + PropColumn::Empty(_) => None, + PropColumn::U32(lazy_vec) => Some( + unsafe { + PrimitiveArray::from_trusted_len_iter_unchecked( + indices.map(|i| lazy_vec.get_opt(i).copied()), + ) + } + .boxed(), + ), + PropColumn::Bool(lazy_vec) => Some(unsafe { + BooleanArray::from_trusted_len_iter_unchecked( + indices.map(|i| lazy_vec.get_opt(i).copied()), + ) + .boxed() + }), + PropColumn::U8(lazy_vec) => Some(unsafe { + PrimitiveArray::from_trusted_len_iter_unchecked( + indices.map(|i| lazy_vec.get_opt(i).copied()), + ) + .boxed() + }), + PropColumn::U16(lazy_vec) => Some(unsafe { + PrimitiveArray::from_trusted_len_iter_unchecked( + indices.map(|i| lazy_vec.get_opt(i).copied()), + ) + .boxed() + }), + PropColumn::U64(lazy_vec) => Some(unsafe { + PrimitiveArray::from_trusted_len_iter_unchecked( + indices.map(|i| lazy_vec.get_opt(i).copied()), + ) + .boxed() + }), + PropColumn::I32(lazy_vec) => Some(unsafe { + PrimitiveArray::from_trusted_len_iter_unchecked( + indices.map(|i| lazy_vec.get_opt(i).copied()), + ) + .boxed() + }), + PropColumn::I64(lazy_vec) => Some(unsafe { + PrimitiveArray::from_trusted_len_iter_unchecked( + indices.map(|i| lazy_vec.get_opt(i).copied()), + ) + .boxed() + }), + PropColumn::F32(lazy_vec) => Some(unsafe { + PrimitiveArray::from_trusted_len_iter_unchecked( + indices.map(|i| lazy_vec.get_opt(i).copied()), + ) + .boxed() + }), + PropColumn::F64(lazy_vec) => Some(unsafe { + PrimitiveArray::from_trusted_len_iter_unchecked( + indices.map(|i| lazy_vec.get_opt(i).copied()), + ) + .boxed() + }), + PropColumn::Str(lazy_vec) => { + let vec = indices + .map(|i| lazy_vec.get_opt(i).map(|str| str.as_ref())) + .collect::>(); + + Some(Utf8ViewArray::from_slice(&vec).boxed()) + } + PropColumn::DTime(lazy_vec) => Some(unsafe { + PrimitiveArray::from_trusted_len_iter_unchecked( + indices.map(|i| lazy_vec.get_opt(i).copied().map(|dt| dt.timestamp_millis())), + ) + .to(polars_arrow::datatypes::ArrowDataType::Timestamp( + polars_arrow::datatypes::TimeUnit::Millisecond, + Some("UTC".to_string()), + )) + .boxed() + }), + PropColumn::NDTime(lazy_vec) => Some(unsafe { + PrimitiveArray::from_trusted_len_iter_unchecked(indices.map(|i| { + lazy_vec + .get_opt(i) + .copied() + .map(|dt| dt.and_utc().timestamp_millis()) + })) + .to(polars_arrow::datatypes::ArrowDataType::Timestamp( + polars_arrow::datatypes::TimeUnit::Millisecond, + None, + )) + .boxed() + }), + PropColumn::Decimal(lazy_vec) => { + let scale = meta + .get_dtype(col_id) + .and_then(|dtype| match dtype { + PropType::Decimal { scale } => Some(scale as usize), + _ => None, + }) + .unwrap(); + Some(unsafe { + PrimitiveArray::from_trusted_len_iter_unchecked(indices.map(|i| { + lazy_vec.get_opt(i).and_then(|bd| { + let (num, _) = bd.as_bigint_and_scale(); + num.to_i128() + }) + })) + .to(polars_arrow::datatypes::ArrowDataType::Decimal(38, scale)) + .boxed() + }) + } + // PropColumn::Array(lazy_vec) => todo!(), + // PropColumn::List(lazy_vec) => todo!(), + // PropColumn::Map(lazy_vec) => todo!(), + // PropColumn::NDTime(lazy_vec) => todo!(), + // PropColumn::DTime(lazy_vec) => todo!(), + // PropColumn::Decimal(lazy_vec) => todo!(), + _ => todo!("Unsupported column type"), + } + } + + pub fn take_t_column( + &self, + col_id: usize, + meta: &PropMapper, + indices: impl ExactSizeIterator, + ) -> Option> { + let column = self.t_properties.get(col_id)?; + self.column_as_array(column, col_id, meta, indices) + } + + pub fn take_c_column( + &self, + col: usize, + meta: &PropMapper, + indices: impl ExactSizeIterator, + ) -> Option> { + let column = self.c_properties.get(col)?; + self.column_as_array(column, col, meta, indices) + } + + fn update_earliest_latest(&mut self, t: TimeIndexEntry) { + let earliest = self.earliest.get_or_insert(t); + if t < *earliest { + *earliest = t; + } + let latest = self.latest.get_or_insert(t); + if t > *latest { + *latest = t; + } + } + + pub(crate) fn t_len(&self) -> usize { + self.t_properties.len() + } +} + +impl<'a> PropMutEntry<'a> { + pub(crate) fn append_t_props( + &mut self, + t: TimeIndexEntry, + props: impl IntoIterator, + ) { + let t_prop_row = if let Some(t_prop_row) = self + .properties + .t_properties + .push(props) + .expect("Internal error: properties should be validated at this point") + { + t_prop_row + } else { + let row = self.properties.t_properties.push_null(); + row + }; + + if self.properties.t_index.len() <= self.row { + self.properties + .t_index + .resize_with(self.row + 1, Default::default); + } + let prop_timestamps = &mut self.properties.t_index[self.row]; + prop_timestamps.props_ts.set(t, Some(t_prop_row)); + + self.properties.has_node_properties = true; + self.properties.update_earliest_latest(t); + } + + pub(crate) fn append_edge_ts(&mut self, t: TimeIndexEntry, edge_id: ELID) { + if self.properties.t_index.len() <= self.row { + self.properties + .t_index + .resize_with(self.row + 1, Default::default); + } + + self.properties.has_node_additions = true; + let prop_timestamps = &mut self.properties.t_index[self.row]; + prop_timestamps.edge_ts.set(t, edge_id); + + self.properties.update_earliest_latest(t); + } + + pub(crate) fn append_const_props(&mut self, props: impl IntoIterator) { + for (prop_id, prop) in props { + if self.properties.c_properties.len() <= prop_id { + self.properties + .c_properties + .resize_with(prop_id + 1, Default::default); + } + let const_props = &mut self.properties.c_properties[prop_id]; + let _ = const_props.set(self.row, prop); + } + } +} + +impl<'a> PropEntry<'a> { + pub fn timestamps(self) -> Option<&'a PropTimestamps> { + self.properties.t_index.get(self.row) + } + + pub(crate) fn prop(self, prop_id: usize) -> Option> { + let t_cell = self + .properties + .t_index + .get(self.row) + .map_or(&TCell::Empty, |ts| &ts.props_ts); + + Some(TPropCell::new(t_cell, self.properties.t_column(prop_id))) + } +} diff --git a/db4-storage/src/properties/props_meta_writer.rs b/db4-storage/src/properties/props_meta_writer.rs new file mode 100644 index 0000000000..7899ec4135 --- /dev/null +++ b/db4-storage/src/properties/props_meta_writer.rs @@ -0,0 +1,230 @@ +use db4_common::error::DBV4Error; +use either::Either; +use raphtory::prelude::Prop; +use raphtory_api::core::entities::properties::{ + meta::{LockedPropMapper, Meta, PropMapper}, + prop::unify_types, +}; + +pub enum PropsMetaWriter<'a, PN: AsRef> { + Change { + props: Vec>, + mapper: LockedPropMapper<'a>, + meta: &'a Meta, + }, + NoChange { + props: Vec<(usize, Prop)>, + }, +} + +pub enum PropEntry<'a, PN: AsRef + 'a> { + Change { + name: PN, + prop_id: Option, + prop: Prop, + _phantom: &'a (), + }, + NoChange(PN, usize, Prop), +} + +impl<'a, PN: AsRef> PropsMetaWriter<'a, PN> { + pub fn temporal( + meta: &'a Meta, + props: impl ExactSizeIterator, + ) -> Result { + Self::new(meta, meta.temporal_prop_meta(), props) + } + + pub fn constant( + meta: &'a Meta, + props: impl ExactSizeIterator, + ) -> Result { + Self::new(meta, meta.const_prop_meta(), props) + } + + pub fn new( + meta: &'a Meta, + prop_mapper: &'a PropMapper, + props: impl ExactSizeIterator, + ) -> Result { + let locked_meta = prop_mapper.locked(); + + let mut in_props = Vec::with_capacity(props.len()); + + let mut no_type_changes = true; + for (prop_name, prop) in props { + let dtype = prop.dtype(); + let outcome @ (_, _, type_check) = locked_meta + .fast_proptype_check(prop_name.as_ref(), dtype) + .map(|outcome| (prop_name, prop, outcome))?; + let nothing_to_do = type_check.map(|x| x.is_right()).unwrap_or_default(); + no_type_changes &= nothing_to_do; + in_props.push(outcome); + } + + if no_type_changes { + return Ok(Self::NoChange { + props: in_props + .into_iter() + .filter_map(|(prop_name, prop, _)| { + Some((locked_meta.get_id(prop_name.as_ref())?, prop)) + }) + .collect(), + }); + } + + let mut props = vec![]; + for (prop_name, prop, outcome) in in_props { + props.push(Self::as_prop_entry(prop_name, prop, outcome)); + } + Ok(Self::Change { + props, + mapper: locked_meta, + meta, + }) + } + + fn as_prop_entry( + prop_name: PN, + prop: Prop, + outcome: Option>, + ) -> PropEntry<'a, PN> { + match outcome { + Some(Either::Right(prop_id)) => PropEntry::NoChange(prop_name, prop_id, prop), + Some(Either::Left(prop_id)) => PropEntry::Change { + name: prop_name, + prop_id: Some(prop_id), + prop, + _phantom: &(), + }, + None => { + // prop id doesn't exist so we grab the entry + PropEntry::Change { + name: prop_name, + prop_id: None, + prop, + _phantom: &(), + } + } + } + } + + pub fn into_props_temporal(self) -> Result, DBV4Error> { + self.into_props_inner(|mapper| mapper.temporal_prop_meta()) + } + + pub fn into_props_const(self) -> Result, DBV4Error> { + self.into_props_inner(|mapper| mapper.const_prop_meta()) + } + + pub fn into_props_inner( + self, + mapper_fn: impl Fn(&Meta) -> &PropMapper, + ) -> Result, DBV4Error> { + match self { + Self::NoChange { props } => Ok(props), + Self::Change { + props, + mapper, + meta, + } => { + let mut prop_with_ids = vec![]; + drop(mapper); + let mut mapper = mapper_fn(meta).write_locked(); + + // revalidate + let props = props + .into_iter() + .map(|entry| match entry { + PropEntry::NoChange(name, _, prop) => { + let new_entry = mapper + .fast_proptype_check(name.as_ref(), prop.dtype()) + .map(|outcome| Self::as_prop_entry(name, prop, outcome))?; + Ok(new_entry) + } + PropEntry::Change { name, prop, .. } => { + let new_entry = mapper + .fast_proptype_check(name.as_ref(), prop.dtype()) + .map(|outcome| Self::as_prop_entry(name, prop, outcome))?; + Ok(new_entry) + } + }) + .collect::, DBV4Error>>()?; + + for entry in props { + match entry { + PropEntry::NoChange(_, prop_id, prop) => { + prop_with_ids.push((prop_id, prop)); + } + PropEntry::Change { + name, + prop_id: Some(prop_id), + prop, + .. + } => { + let new_prop_type = prop.dtype(); + let existing_type = mapper.get_dtype(prop_id).unwrap(); + let new_prop_type = + unify_types(&new_prop_type, existing_type, &mut false)?; + mapper.set_id_and_dtype(name.as_ref(), prop_id, new_prop_type); + prop_with_ids.push((prop_id, prop)); + } + PropEntry::Change { name, prop, .. } => { + let new_prop_type = prop.dtype(); + let prop_id = mapper.new_id_and_dtype(name.as_ref(), new_prop_type); + prop_with_ids.push((prop_id, prop)); + } + } + } + Ok(prop_with_ids) + } + } + } +} + +#[cfg(test)] +mod test { + + use raphtory_api::core::storage::arc_str::ArcStr; + + use super::*; + + #[test] + fn test_props_meta_writer() { + let meta = Meta::new(); + let props = vec![ + (ArcStr::from("prop1"), Prop::U32(0)), + (ArcStr::from("prop2"), Prop::U32(1)), + ]; + let writer = PropsMetaWriter::temporal(&meta, props.into_iter()).unwrap(); + let props = writer.into_props_temporal().unwrap(); + assert_eq!(props.len(), 2); + + assert_eq!(props, vec![(0, Prop::U32(0)), (1, Prop::U32(1))]); + + assert_eq!(meta.temporal_prop_meta().len(), 2); + } + + #[test] + fn test_fail_typecheck() { + let meta = Meta::new(); + let prop1 = Prop::U32(0); + let prop2 = Prop::U64(1); + + let writer = + PropsMetaWriter::temporal(&meta, vec![(ArcStr::from("prop1"), prop1)].into_iter()) + .unwrap(); + let props = writer.into_props_temporal().unwrap(); + assert_eq!(props.len(), 1); + + assert!(meta.temporal_prop_meta().len() == 1); + assert!(meta.temporal_prop_meta().get_id("prop1").is_some()); + + let writer = + PropsMetaWriter::temporal(&meta, vec![(ArcStr::from("prop1"), prop2)].into_iter()); + + assert!(writer.is_err()); + assert!(meta.temporal_prop_meta().len() == 1); + assert!(meta.temporal_prop_meta().get_id("prop1").is_some()); + } +} diff --git a/db4-storage/src/segments/additions.rs b/db4-storage/src/segments/additions.rs new file mode 100644 index 0000000000..0370071cc7 --- /dev/null +++ b/db4-storage/src/segments/additions.rs @@ -0,0 +1,60 @@ +use std::ops::Range; + +use iter_enum::{DoubleEndedIterator, ExactSizeIterator, FusedIterator, Iterator}; +use raphtory::core::{ + entities::nodes::node_store::PropTimestamps, + storage::timeindex::{TimeIndexEntry, TimeIndexOps, TimeIndexWindow}, +}; + +#[derive(Clone, Debug)] +pub enum MemAdditions<'a> { + Props(&'a PropTimestamps), + Window(TimeIndexWindow<'a, TimeIndexEntry, PropTimestamps>), +} + +impl<'a> TimeIndexOps<'a> for MemAdditions<'a> { + type IndexType = TimeIndexEntry; + + type RangeType = Self; + + fn active(&self, w: Range) -> bool { + match self { + MemAdditions::Props(props) => props.active(w), + MemAdditions::Window(window) => window.active(w), + } + } + + fn range(&self, w: Range) -> Self::RangeType { + match self { + MemAdditions::Props(props) => MemAdditions::Window(props.range(w)), + MemAdditions::Window(window) => MemAdditions::Window(window.range(w)), + } + } + + fn iter(self) -> impl Iterator + Send + Sync + 'a { + match self { + MemAdditions::Props(props) => Iter2::I1(props.iter()), + MemAdditions::Window(window) => Iter2::I2(window.iter()), + } + } + + fn iter_rev(self) -> impl Iterator + Send + Sync + 'a { + match self { + MemAdditions::Props(props) => Iter2::I1(props.iter_rev()), + MemAdditions::Window(window) => Iter2::I2(window.iter_rev()), + } + } + + fn len(&self) -> usize { + match self { + MemAdditions::Props(props) => props.len(), + MemAdditions::Window(window) => window.len(), + } + } +} + +#[derive(Clone, Debug, Iterator, DoubleEndedIterator, ExactSizeIterator, FusedIterator)] +pub enum Iter2 { + I1(I1), + I2(I2), +} diff --git a/db4-storage/src/segments/edge.rs b/db4-storage/src/segments/edge.rs new file mode 100644 index 0000000000..44f54b8d22 --- /dev/null +++ b/db4-storage/src/segments/edge.rs @@ -0,0 +1,249 @@ +use std::{ + ops::{Deref, DerefMut}, + sync::{ + Arc, + atomic::{self, AtomicUsize}, + }, +}; + +use db4_common::LocalPOS; +use raphtory::{ + core::storage::timeindex::{AsTime, TimeIndexEntry}, + prelude::Prop, +}; +use raphtory_api::core::entities::{VID, properties::meta::Meta}; + +use crate::{EdgeSegmentOps, properties::PropMutEntry}; + +use super::{HasRow, SegmentContainer, edge_entry::MemEdgeEntry}; + +#[derive(Debug, Default)] +pub struct MemPageEntry { + pub src: VID, + pub dst: VID, + pub row: usize, +} + +impl HasRow for MemPageEntry { + fn row(&self) -> usize { + self.row + } + + fn row_mut(&mut self) -> &mut usize { + &mut self.row + } +} + +#[derive(Debug)] +pub struct MemEdgeSegment { + inner: SegmentContainer, +} + +impl AsRef> for MemEdgeSegment { + fn as_ref(&self) -> &SegmentContainer { + &self.inner + } +} + +impl AsMut> for MemEdgeSegment { + fn as_mut(&mut self) -> &mut SegmentContainer { + &mut self.inner + } +} + +impl MemEdgeSegment { + pub fn new(segment_id: usize, max_page_len: usize, meta: Arc) -> Self { + Self { + inner: SegmentContainer::new(segment_id, max_page_len, meta), + } + } + + pub fn get_edge(&self, edge_pos: impl Into) -> Option<(VID, VID)> { + let edge_pos = edge_pos.into(); + self.inner + .get(&edge_pos) + .map(|entry| (entry.src, entry.dst)) + } + + pub fn insert_edge_internal( + &mut self, + t: T, + edge_pos: impl Into, + src: impl Into, + dst: impl Into, + props: impl IntoIterator, + ) { + let edge_pos = edge_pos.into(); + let src = src.into(); + let dst = dst.into(); + let local_row = self.reserve_local_row(edge_pos, src, dst); + + let mut prop_entry: PropMutEntry<'_> = self.inner.properties_mut().get_mut_entry(local_row); + let ts = TimeIndexEntry::new(t.t(), t.i()); + prop_entry.append_t_props(ts, props) + } + + pub fn insert_static_edge_internal( + &mut self, + edge_pos: LocalPOS, + src: impl Into, + dst: impl Into, + ) { + let src = src.into(); + let dst = dst.into(); + self.reserve_local_row(edge_pos, src, dst); + } + + fn reserve_local_row( + &mut self, + edge_pos: LocalPOS, + src: impl Into, + dst: impl Into, + ) -> usize { + let src = src.into(); + let dst = dst.into(); + let row = self.inner.reserve_local_row(edge_pos).map_either( + |row| { + row.src = src; + row.dst = dst; + row.row() + }, + |row| { + row.src = src; + row.dst = dst; + row.row() + }, + ); + row.either(|a| a, |a| a) + } + + pub fn update_const_properties( + &mut self, + edge_pos: impl Into, + src: impl Into, + dst: impl Into, + props: impl IntoIterator, + ) { + let edge_pos = edge_pos.into(); + let src = src.into(); + let dst = dst.into(); + let local_row = self.reserve_local_row(edge_pos, src, dst); + let mut prop_entry: PropMutEntry<'_> = self.inner.properties_mut().get_mut_entry(local_row); + prop_entry.append_const_props(props) + } + + pub fn insert_edge(&mut self, edge_pos: LocalPOS, src: impl Into, dst: impl Into) { + self.insert_edge_internal(0, edge_pos, src, dst, []); + } + + pub fn contains_edge(&self, edge_pos: LocalPOS) -> bool { + self.inner + .items() + .get::(edge_pos.0) + .map(|b| *b) + .unwrap_or_default() + } +} + +pub struct EdgeSegmentView { + segment: parking_lot::RwLock, + segment_id: usize, + num_edges: AtomicUsize, +} + +impl EdgeSegmentOps for EdgeSegmentView { + type Extension = (); + + type Entry<'a> = MemEdgeEntry<'a, parking_lot::RwLockReadGuard<'a, MemEdgeSegment>>; + + fn latest(&self) -> Option { + self.head().as_ref().latest() + } + + fn earliest(&self) -> Option { + self.head().as_ref().earliest() + } + + fn t_len(&self) -> usize { + self.head().as_ref().t_len() + } + + fn load( + _page_id: usize, + _max_page_len: usize, + _meta: Arc, + _path: impl AsRef, + _ext: Self::Extension, + ) -> Result + where + Self: Sized, + { + todo!() + } + + fn new( + page_id: usize, + max_page_len: usize, + meta: Arc, + _path: impl AsRef, + _ext: Self::Extension, + ) -> Self { + Self { + segment: parking_lot::RwLock::new(MemEdgeSegment::new(page_id, max_page_len, meta)), + segment_id: page_id, + num_edges: AtomicUsize::new(0), + } + } + + fn segment_id(&self) -> usize { + self.segment_id + } + + fn num_edges(&self) -> usize { + self.num_edges.load(atomic::Ordering::Relaxed) + } + + fn head(&self) -> parking_lot::RwLockReadGuard { + self.segment.read_recursive() + } + + fn head_mut(&self) -> parking_lot::RwLockWriteGuard { + self.segment.write() + } + + fn try_head_mut(&self) -> Option> { + self.segment.try_write() + } + + fn notify_write( + &self, + _head_lock: impl DerefMut, + ) -> Result<(), db4_common::error::DBV4Error> { + Ok(()) + } + + fn increment_num_edges(&self) -> usize { + self.num_edges.fetch_add(1, atomic::Ordering::Relaxed) + } + + fn contains_edge( + &self, + edge_pos: LocalPOS, + locked_head: impl Deref, + ) -> bool { + locked_head.contains_edge(edge_pos) + } + + fn get_edge( + &self, + edge_pos: LocalPOS, + locked_head: impl Deref, + ) -> Option<(VID, VID)> { + locked_head.get_edge(edge_pos) + } + + fn entry<'a, LP: Into>(&'a self, edge_pos: LP) -> Self::Entry<'a> { + let edge_pos = edge_pos.into(); + MemEdgeEntry::new(edge_pos, self.head()) + } +} diff --git a/db4-storage/src/segments/edge_entry.rs b/db4-storage/src/segments/edge_entry.rs new file mode 100644 index 0000000000..47cbcbe9dc --- /dev/null +++ b/db4-storage/src/segments/edge_entry.rs @@ -0,0 +1,80 @@ +use db4_common::LocalPOS; +use raphtory::core::entities::{VID, properties::tprop::TPropCell}; + +use crate::{EdgeEntryOps, EdgeRefOps}; + +use super::{additions::MemAdditions, edge::MemEdgeSegment}; + +pub struct MemEdgeEntry<'a, MES> { + pos: LocalPOS, + es: MES, + __marker: std::marker::PhantomData<&'a ()>, +} + +impl<'a, MES: std::ops::Deref> MemEdgeEntry<'a, MES> { + pub fn new(pos: LocalPOS, es: MES) -> Self { + Self { + pos, + es, + __marker: std::marker::PhantomData, + } + } +} + +impl<'a, MES: std::ops::Deref> EdgeEntryOps<'a> for MemEdgeEntry<'a, MES> { + type Ref<'b> + = MemEdgeRef<'b> + where + 'a: 'b, + MES: 'b; + + fn as_ref<'b>(&'b self) -> Self::Ref<'b> + where + 'a: 'b, + { + MemEdgeRef { + pos: self.pos, + es: &self.es, + } + } +} + +#[derive(Copy, Clone)] +pub struct MemEdgeRef<'a> { + pos: LocalPOS, + es: &'a MemEdgeSegment, +} + +impl<'a> MemEdgeRef<'a> { + pub fn new(pos: LocalPOS, es: &'a MemEdgeSegment) -> Self { + Self { pos, es } + } +} + +impl<'a> EdgeRefOps<'a> for MemEdgeRef<'a> { + type Additions = MemAdditions<'a>; + + type TProps = TPropCell<'a>; + + fn edge(self) -> Option<(VID, VID)> { + self.es + .as_ref() + .get(&self.pos) + .map(|entry| (entry.src, entry.dst)) + } + + fn additions(self) -> Self::Additions { + MemAdditions::Props(self.es.as_ref().additions(self.pos)) + } + + fn c_prop(self, prop_id: usize) -> Option { + self.es.as_ref().c_prop(self.pos, prop_id) + } + + fn t_prop(self, prop_id: usize) -> Self::TProps { + self.es + .as_ref() + .t_prop(self.pos, prop_id, 0) + .unwrap_or_default() + } +} diff --git a/db4-storage/src/segments/mod.rs b/db4-storage/src/segments/mod.rs new file mode 100644 index 0000000000..c37af06a69 --- /dev/null +++ b/db4-storage/src/segments/mod.rs @@ -0,0 +1,244 @@ +use std::{collections::hash_map::Entry, fmt::Debug, sync::Arc}; + +use bitvec::{order::Msb0, vec::BitVec}; +use db4_common::LocalPOS; +use either::Either; +use raphtory::{ + core::{ + entities::{ + nodes::node_store::PropTimestamps, + properties::{tcell::TCell, tprop::TPropCell}, + }, + storage::timeindex::TimeIndexEntry, + }, + prelude::Prop, +}; +use raphtory_api::core::entities::properties::meta::Meta; +use rustc_hash::FxHashMap; + +use super::properties::{PropEntry, Properties}; + +pub mod edge; +pub mod node; + +pub mod additions; +pub mod edge_entry; +pub mod node_entry; + +pub struct SegmentContainer { + page_id: usize, + items: BitVec, + data: FxHashMap, + max_page_len: usize, + properties: Properties, + meta: Arc, + lsn: u64, +} + +impl Debug for SegmentContainer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let items = self + .items + .iter() + .map(|x| if *x { 1 } else { 0 }) + .collect::>(); + let mut data = self.data.iter().map(|(k, v)| (k, v)).collect::>(); + data.sort_by(|a, b| a.0.cmp(&b.0)); + + f.debug_struct("SegmentContainer") + .field("page_id", &self.page_id) + .field("items", &items as &dyn Debug) + .field("data", &data) + .field("max_page_len", &self.max_page_len) + .field("properties", &self.properties) + .finish() + } +} + +pub trait HasRow: Default { + fn row(&self) -> usize; + fn row_mut(&mut self) -> &mut usize; +} + +impl SegmentContainer { + pub fn new(page_id: usize, max_page_len: usize, meta: Arc) -> Self { + assert!(max_page_len > 0, "max_page_len must be greater than 0"); + Self { + page_id, + items: BitVec::repeat(false, max_page_len), + data: Default::default(), + max_page_len, + properties: Default::default(), + meta, + lsn: 0, + } + } + + #[inline] + pub fn est_size(&self) -> usize { + //FIXME: this is a rough estimate and should be improved + let data_size = (self.data.len() as f64 * std::mem::size_of::() as f64 * 1.5) as usize; // Estimate size of data + data_size + self.t_prop_est_size() + self.c_prop_est_size() + } + + pub fn get(&self, item_pos: &LocalPOS) -> Option<&T> { + self.data.get(item_pos) + } + + pub fn set_item(&mut self, item_pos: LocalPOS) { + self.items.set(item_pos.0 as usize, true); + } + + pub fn max_page_len(&self) -> usize { + self.max_page_len + } + + pub fn is_full(&self) -> bool { + self.data.len() == self.max_page_len + } + + pub fn t_len(&self) -> usize { + self.properties.t_len() + } + + pub(crate) fn reserve_local_row(&mut self, item_pos: LocalPOS) -> Either<&mut T, &mut T> { + let local_row = self.data.len(); + self.set_item(item_pos); + match self.data.entry(item_pos) { + Entry::Occupied(occupied_entry) => Either::Left(occupied_entry.into_mut()), + Entry::Vacant(vacant_entry) => { + let vacant_entry = vacant_entry.insert(T::default()); + *vacant_entry.row_mut() = local_row; + Either::Right(vacant_entry) + } + } + } + + #[inline] + pub fn t_prop_est_size(&self) -> usize { + let row_size = self.meta.temporal_est_row_size(); + let row_count = self.properties.t_len(); + + row_size * row_count + } + + pub(crate) fn c_prop_est_size(&self) -> usize { + self.meta.const_est_row_size() * self.len() + } + + pub fn properties(&self) -> &Properties { + &self.properties + } + + pub fn properties_mut(&mut self) -> &mut Properties { + &mut self.properties + } + + pub fn meta(&self) -> &Arc { + &self.meta + } + + pub fn items(&self) -> &BitVec { + &self.items + } + + #[inline(always)] + pub fn segment_id(&self) -> usize { + self.page_id + } + + #[inline(always)] + pub fn lsn(&self) -> u64 { + self.lsn + } + + #[inline(always)] + pub fn set_lsn(&mut self, lsn: u64) { + self.lsn = lsn; + } + + pub fn len(&self) -> usize { + self.data.len() + } + + pub fn row_entries(&self) -> impl Iterator { + self.items.iter_ones().filter_map(move |l_pos| { + let entry = self.data.get(&LocalPOS(l_pos))?; + Some(( + LocalPOS(l_pos), + entry, + self.properties().get_entry(entry.row()), + )) + }) + } + + pub fn all_entries( + &self, + ) -> impl ExactSizeIterator)> { + self.items.iter().enumerate().map(move |(l_pos, exists)| { + let l_pos = LocalPOS(l_pos); + let entry = (*exists).then(|| { + let entry = self.data.get(&l_pos).unwrap(); + (entry, self.properties().get_entry(entry.row())) + }); + (l_pos, entry) + }) + } + + pub fn earliest(&self) -> Option { + self.properties.earliest() + } + + pub fn latest(&self) -> Option { + self.properties.latest() + } + + pub fn temporal_index(&self) -> Vec { + self.row_entries() + .flat_map(|(_, mp, _)| { + let row = mp.row(); + self.properties() + .temporal_index(row) + .into_iter() + .flat_map(|entry| entry.props_ts.iter()) + .filter_map(|(_, &v)| v) + }) + .collect::>() + } + + pub fn t_prop( + &self, + item_id: impl Into, + prop_id: usize, + _layer_id: usize, + ) -> Option> { + let item_id = item_id.into(); + self.data.get(&item_id).and_then(|entry| { + let prop_entry = self.properties.get_entry(entry.row()); + prop_entry.prop(prop_id) + }) + } + + pub fn c_prop(&self, item_id: impl Into, prop_id: usize) -> Option { + let item_id = item_id.into(); + self.data.get(&item_id).and_then(|entry| { + let prop_entry = self.properties.c_column(prop_id)?; + prop_entry.get(entry.row()) + }) + } + + pub fn additions(&self, item_pos: LocalPOS) -> &PropTimestamps { + self.data + .get(&item_pos) + .and_then(|entry| { + let prop_entry = self.properties.get_entry(entry.row()); + prop_entry.timestamps() + }) + .unwrap_or(&EMPTY_PROP_TIMESTAMPS) + } +} + +const EMPTY_PROP_TIMESTAMPS: PropTimestamps = PropTimestamps { + edge_ts: TCell::Empty, + props_ts: TCell::Empty, +}; diff --git a/db4-storage/src/segments/node.rs b/db4-storage/src/segments/node.rs new file mode 100644 index 0000000000..bf1801f2ba --- /dev/null +++ b/db4-storage/src/segments/node.rs @@ -0,0 +1,315 @@ +use db4_common::{LocalPOS, error::DBV4Error}; +use either::Either; +use raphtory::{ + core::{ + entities::{ELID, nodes::structure::adj::Adj}, + storage::timeindex::{AsTime, TimeIndexEntry}, + }, + prelude::Prop, +}; +use raphtory_api::core::{ + Direction, + entities::{EID, VID, properties::meta::Meta}, +}; +use std::{ + ops::{Deref, DerefMut}, + sync::{Arc, atomic, atomic::AtomicUsize}, +}; + +use super::{HasRow, SegmentContainer}; +use crate::{NodeSegmentOps, segments::node_entry::MemNodeEntry}; + +#[derive(Debug)] +pub struct MemNodeSegment { + inner: SegmentContainer, +} + +#[derive(Debug, Default)] +pub struct AdjEntry { + row: usize, + adj: Adj, +} + +impl AdjEntry { + pub fn degree(&self, d: Direction) -> usize { + self.adj.degree(d) + } + + pub fn edges(&self, d: Direction) -> impl Iterator + '_ { + match d { + Direction::IN => Either::Left(self.adj.inb_iter()), + Direction::OUT => Either::Right(self.adj.out_iter()), + Direction::BOTH => panic!("AdjEntry::edges: BOTH direction is not supported"), + } + } +} + +impl HasRow for AdjEntry { + fn row(&self) -> usize { + self.row + } + + fn row_mut(&mut self) -> &mut usize { + &mut self.row + } +} + +impl AsRef> for MemNodeSegment { + fn as_ref(&self) -> &SegmentContainer { + &self.inner + } +} + +impl AsMut> for MemNodeSegment { + fn as_mut(&mut self) -> &mut SegmentContainer { + &mut self.inner + } +} + +impl MemNodeSegment { + #[inline(always)] + fn get_adj(&self, n: LocalPOS) -> Option<&Adj> { + self.inner.get(&n).map(|AdjEntry { adj, .. }| adj) + } + + pub fn has_node(&self, n: LocalPOS) -> bool { + self.inner.get(&n).is_some() + } + + // pub(crate) fn contains_out(&self, n: LocalPOS, dst: VID) -> bool { + // self.get_out_edge(n, dst).is_some() + // } + + // pub(crate) fn contains_in(&self, n: LocalPOS, src: VID) -> bool { + // self.get_in_edge(n, src).is_some() + // } + + pub fn get_out_edge(&self, n: LocalPOS, dst: VID) -> Option { + self.get_adj(n) + .and_then(|adj| adj.get_edge(dst, Direction::OUT)) + } + + pub fn get_inb_edge(&self, n: LocalPOS, src: VID) -> Option { + self.get_adj(n) + .and_then(|adj| adj.get_edge(src, Direction::IN)) + } + + pub fn out_edges(&self, n: LocalPOS) -> impl Iterator + '_ { + self.get_adj(n).into_iter().flat_map(|adj| adj.out_iter()) + } + + pub fn inb_edges(&self, n: LocalPOS) -> impl Iterator + '_ { + self.get_adj(n).into_iter().flat_map(|adj| adj.inb_iter()) + } +} + +impl MemNodeSegment { + pub fn new(segment_id: usize, max_page_len: usize, meta: Arc) -> Self { + Self { + inner: SegmentContainer::new(segment_id, max_page_len, meta), + } + } + + pub fn add_outbound_edge( + &mut self, + t: Option, + src_pos: LocalPOS, + dst: impl Into, + e_id: impl Into, + ) -> bool { + let dst = dst.into(); + let e_id = e_id.into(); + let add_out = self.inner.reserve_local_row(src_pos).map_either( + |row| { + row.adj.add_edge_out(dst, e_id.edge); + row.row() + }, + |row| { + row.adj.add_edge_out(dst, e_id.edge); + row.row() + }, + ); + + let new_entry = add_out.is_right(); + if let Some(t) = t { + self.update_timestamp_inner(t, add_out.either(|a| a, |a| a), e_id); + } + new_entry + } + + pub fn add_inbound_edge( + &mut self, + t: Option, + dst_pos: impl Into, + src: impl Into, + e_id: impl Into, + ) -> bool { + let src = src.into(); + let e_id = e_id.into(); + let dst_pos = dst_pos.into(); + + let add_in = self.inner.reserve_local_row(dst_pos).map_either( + |row| { + row.adj.add_edge_into(src, e_id.edge); + row.row() + }, + |row| { + row.adj.add_edge_into(src, e_id.edge); + row.row() + }, + ); + let new_entry = add_in.is_right(); + if let Some(t) = t { + self.update_timestamp_inner(t, add_in.either(|a| a, |a| a), e_id); + } + new_entry + } + + fn update_timestamp_inner(&mut self, t: T, row: usize, e_id: ELID) { + let mut prop_mut_entry = self.inner.properties_mut().get_mut_entry(row); + let ts = TimeIndexEntry::new(t.t(), t.i()); + + prop_mut_entry.append_edge_ts(ts, e_id); + } + + pub fn update_timestamp(&mut self, t: T, node_pos: LocalPOS, e_id: ELID) { + let row = self + .inner + .reserve_local_row(node_pos) + .either(|a| a.row, |a| a.row); + self.update_timestamp_inner(t, row, e_id); + } + + pub fn add_props( + &mut self, + t: T, + node_pos: LocalPOS, + props: impl IntoIterator, + ) { + let row = self + .inner + .reserve_local_row(node_pos) + .either(|a| a.row, |a| a.row); + let mut prop_mut_entry = self.inner.properties_mut().get_mut_entry(row); + let ts = TimeIndexEntry::new(t.t(), t.i()); + prop_mut_entry.append_t_props(ts, props); + } + + pub fn update_c_props( + &mut self, + node_pos: LocalPOS, + props: impl IntoIterator, + ) { + let row = self + .inner + .reserve_local_row(node_pos) + .either(|a| a.row, |a| a.row); + let mut prop_mut_entry = self.inner.properties_mut().get_mut_entry(row); + prop_mut_entry.append_const_props(props); + } +} + +pub struct NodeSegmentView { + inner: parking_lot::RwLock, + segment_id: usize, + num_nodes: AtomicUsize, +} + +impl NodeSegmentOps for NodeSegmentView { + type Extension = (); + + type Entry<'a> = MemNodeEntry<'a, parking_lot::RwLockReadGuard<'a, MemNodeSegment>>; + + fn latest(&self) -> Option { + self.head().as_ref().latest() + } + + fn earliest(&self) -> Option { + self.head().as_ref().earliest() + } + + fn t_len(&self) -> usize { + self.head().as_ref().t_len() + } + + fn load( + _page_id: usize, + _max_page_len: usize, + _meta: Arc, + _path: impl AsRef, + _ext: Self::Extension, + ) -> Result + where + Self: Sized, + { + todo!() + } + + fn new( + page_id: usize, + max_page_len: usize, + meta: Arc, + _path: impl AsRef, + _ext: Self::Extension, + ) -> Self { + Self { + inner: parking_lot::RwLock::new(MemNodeSegment::new(page_id, max_page_len, meta)), + segment_id: page_id, + num_nodes: AtomicUsize::new(0), + } + } + + fn segment_id(&self) -> usize { + self.segment_id + } + + fn head(&self) -> parking_lot::RwLockReadGuard { + self.inner.read() + } + + fn head_mut(&self) -> parking_lot::RwLockWriteGuard { + self.inner.write() + } + + fn num_nodes(&self) -> usize { + self.num_nodes.load(atomic::Ordering::Relaxed) + } + + fn increment_num_nodes(&self) -> usize { + self.num_nodes.fetch_add(1, atomic::Ordering::Relaxed) + } + + fn notify_write( + &self, + _head_lock: impl DerefMut, + ) -> Result<(), DBV4Error> { + Ok(()) + } + + fn check_node(&self, _pos: LocalPOS) -> bool { + false + } + + fn get_out_edge( + &self, + pos: LocalPOS, + dst: impl Into, + locked_head: impl Deref, + ) -> Option { + locked_head.get_out_edge(pos, dst.into()) + } + + fn get_inb_edge( + &self, + pos: LocalPOS, + src: impl Into, + locked_head: impl Deref, + ) -> Option { + locked_head.get_inb_edge(pos, src.into()) + } + + fn entry<'a>(&'a self, pos: impl Into) -> Self::Entry<'a> { + let pos = pos.into(); + MemNodeEntry::new(pos, self.head()) + } +} diff --git a/db4-storage/src/segments/node_entry.rs b/db4-storage/src/segments/node_entry.rs new file mode 100644 index 0000000000..616a689788 --- /dev/null +++ b/db4-storage/src/segments/node_entry.rs @@ -0,0 +1,88 @@ +use crate::{NodeEntryOps, NodeRefOps, segments::node::MemNodeSegment}; +use db4_common::LocalPOS; +use raphtory::{core::entities::properties::tprop::TPropCell, prelude::Prop}; +use raphtory_api::core::entities::{EID, VID}; +use std::ops::Deref; + +use super::additions::MemAdditions; + +pub struct MemNodeEntry<'a, MNS> { + pos: LocalPOS, + ns: MNS, + __marker: std::marker::PhantomData<&'a ()>, +} + +impl<'a, MNS: Deref> MemNodeEntry<'a, MNS> { + pub fn new(pos: LocalPOS, ns: MNS) -> Self { + Self { + pos, + ns, + __marker: std::marker::PhantomData, + } + } +} + +impl<'a, MNS: Deref> NodeEntryOps<'a> for MemNodeEntry<'a, MNS> { + type Ref<'b> + = MemNodeRef<'b> + where + 'a: 'b, + MNS: 'b; + + fn as_ref<'b>(&'b self) -> Self::Ref<'b> + where + 'a: 'b, + { + MemNodeRef { + pos: self.pos, + ns: self.ns.deref(), + } + } +} +#[derive(Copy, Clone)] +pub struct MemNodeRef<'a> { + pos: LocalPOS, + ns: &'a MemNodeSegment, +} + +impl<'a> MemNodeRef<'a> { + pub fn new(pos: LocalPOS, ns: &'a MemNodeSegment) -> Self { + Self { pos, ns } + } +} + +impl<'a> NodeRefOps<'a> for MemNodeRef<'a> { + type Additions = MemAdditions<'a>; + type TProps = TPropCell<'a>; + + fn out_edges(self) -> impl Iterator + 'a { + self.ns.out_edges(self.pos) + } + + fn inb_edges(self) -> impl Iterator + 'a { + self.ns.inb_edges(self.pos) + } + + fn out_edges_sorted(self) -> impl Iterator + 'a { + self.ns.out_edges(self.pos) + } + + fn inb_edges_sorted(self) -> impl Iterator + 'a { + self.ns.inb_edges(self.pos) + } + + fn additions(self) -> Self::Additions { + MemAdditions::Props(self.ns.as_ref().additions(self.pos)) + } + + fn c_prop(self, prop_id: usize) -> Option { + self.ns.as_ref().c_prop(self.pos, prop_id) + } + + fn t_prop(self, prop_id: usize) -> Self::TProps { + self.ns + .as_ref() + .t_prop(self.pos, prop_id, 0) + .unwrap_or_default() + } +} diff --git a/python/Cargo.toml b/python/Cargo.toml index 4e4f8e9f96..a4c79e14a6 100644 --- a/python/Cargo.toml +++ b/python/Cargo.toml @@ -26,9 +26,8 @@ raphtory_core = { path = "../raphtory", version = "0.15.1", features = [ "vectors", "proto", ], package = "raphtory" } -raphtory-graphql = { path = "../raphtory-graphql", version = "0.15.1", features = [ - "python", -] } + +raphtory-graphql = { workspace = true, features = ["python"] } [features] default = ["extension-module"] diff --git a/raphtory-api/src/core/entities/layers.rs b/raphtory-api/src/core/entities/layers.rs index bd9db86510..fe20cc0408 100644 --- a/raphtory-api/src/core/entities/layers.rs +++ b/raphtory-api/src/core/entities/layers.rs @@ -150,7 +150,7 @@ impl Multiple { } #[inline] - pub fn into_iter(&self) -> impl Iterator { + pub fn into_iter(self) -> impl Iterator { let ids = self.0.clone(); (0..ids.len()).map(move |i| ids[i]) } diff --git a/raphtory-api/src/core/entities/mod.rs b/raphtory-api/src/core/entities/mod.rs index abb6bea21c..8529734f26 100644 --- a/raphtory-api/src/core/entities/mod.rs +++ b/raphtory-api/src/core/entities/mod.rs @@ -465,7 +465,7 @@ impl LayerIds { matches!(self, LayerIds::One(_)) } - pub fn iter(&self, num_layers: usize) -> impl Iterator { + pub fn iter(&self, num_layers: usize) -> impl Iterator + use<'_> { match self { LayerIds::None => iter::empty().into_dyn_boxed(), LayerIds::All => (0..num_layers).into_dyn_boxed(), diff --git a/raphtory-api/src/core/entities/properties/meta.rs b/raphtory-api/src/core/entities/properties/meta.rs index 5c6b541649..01b3d536bc 100644 --- a/raphtory-api/src/core/entities/properties/meta.rs +++ b/raphtory-api/src/core/entities/properties/meta.rs @@ -1,13 +1,21 @@ -use std::{ops::Deref, sync::Arc}; +use std::{ + ops::{Deref, DerefMut}, + sync::{ + atomic::{self, AtomicUsize}, + Arc, + }, +}; -use parking_lot::RwLock; +use itertools::Either; +use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard}; +use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; use crate::core::{ - entities::properties::prop::{unify_types, PropError, PropType}, + entities::properties::prop::{check_for_unification, unify_types, PropError, PropType}, storage::{ arc_str::ArcStr, - dict_mapper::{DictMapper, MaybeNew}, + dict_mapper::{DictMapper, LockedDictMapper, MaybeNew, WriteLockedDictMapper}, locked_vec::ArcReadLockedVec, }, }; @@ -49,6 +57,16 @@ impl Meta { &self.meta_node_type } + #[inline] + pub fn temporal_est_row_size(&self) -> usize { + self.meta_prop_temporal.row_size() + } + + #[inline] + pub fn const_est_row_size(&self) -> usize { + self.meta_prop_constant.row_size() + } + pub fn new() -> Self { let meta_layer = DictMapper::default(); let meta_node_type = DictMapper::default(); @@ -166,6 +184,7 @@ impl Meta { #[derive(Default, Debug, Serialize, Deserialize)] pub struct PropMapper { id_mapper: DictMapper, + row_size: AtomicUsize, dtypes: Arc>>, } @@ -183,10 +202,16 @@ impl PropMapper { let dtypes = self.dtypes.read().clone(); Self { id_mapper: self.id_mapper.deep_clone(), + row_size: AtomicUsize::new(self.row_size.load(std::sync::atomic::Ordering::Relaxed)), dtypes: Arc::new(RwLock::new(dtypes)), } } + #[inline] + pub fn row_size(&self) -> usize { + self.row_size.load(atomic::Ordering::Relaxed) + } + pub fn get_and_validate( &self, prop: &str, @@ -197,7 +222,14 @@ impl PropMapper { let existing_dtype = self .get_dtype(id) .expect("Existing id should always have a dtype"); - if existing_dtype == dtype { + + let fast_check = check_for_unification(&dtype, &existing_dtype); + if fast_check.is_none() { + // means nothing to do + return Ok(Some(id)); + } + let can_unify = fast_check.unwrap(); + if can_unify { Ok(Some(id)) } else { Err(PropError::PropertyTypeError { @@ -210,6 +242,7 @@ impl PropMapper { None => Ok(None), } } + pub fn get_or_create_and_validate( &self, prop: &str, @@ -220,7 +253,7 @@ impl PropMapper { let dtype_read = self.dtypes.read_recursive(); if let Some(old_type) = dtype_read.get(id) { let mut unified = false; - if unify_types(&dtype, old_type, &mut unified).is_ok() { + if let Ok(_) = unify_types(&dtype, old_type, &mut unified) { if !unified { // means the types were equal, no change needed return Ok(wrapped_id); @@ -251,6 +284,8 @@ impl PropMapper { None => { // vector not resized yet, resize it and set the dtype and return id dtype_write.resize(id + 1, PropType::Empty); + self.row_size + .fetch_add(dtype.est_size(), atomic::Ordering::Relaxed); dtype_write[id] = dtype; Ok(wrapped_id) } @@ -263,6 +298,8 @@ impl PropMapper { if dtypes.len() <= id { dtypes.resize(id + 1, PropType::Empty); } + self.row_size + .fetch_add(dtype.est_size(), atomic::Ordering::Relaxed); dtypes[id] = dtype; } @@ -273,6 +310,129 @@ impl PropMapper { pub fn dtypes(&self) -> impl Deref> + '_ { self.dtypes.read_recursive() } + + pub fn locked_dtypes(&self) -> &RwLock> { + self.dtypes.as_ref() + } + + pub fn locked(&self) -> LockedPropMapper { + LockedPropMapper { + dict_mapper: self.id_mapper.read(), + d_types: self.dtypes.read_recursive(), + } + } + + pub fn write_locked(&self) -> WriteLockedPropMapper { + WriteLockedPropMapper { + dict_mapper: self.id_mapper.write(), + d_types: self.dtypes.write(), + } + } +} + +pub struct LockedPropMapper<'a> { + dict_mapper: LockedDictMapper<'a>, + d_types: RwLockReadGuard<'a, Vec>, +} + +pub struct WriteLockedPropMapper<'a> { + dict_mapper: WriteLockedDictMapper<'a>, + d_types: RwLockWriteGuard<'a, Vec>, +} + +impl<'a> WriteLockedPropMapper<'a> { + pub fn get_dtype(&'a self, prop_id: usize) -> Option<&'a PropType> { + self.d_types.get(prop_id) + } + + /// Fast check for property type without unifying the types + /// Returns: + /// - `Some(Either::Left(id))` if the property type can be unified + /// - `Some(Either::Right(id))` if the property type is already set and no unification is needed + /// - `None` if the property type is not set + /// - `Err(PropError::PropertyTypeError)` if the property type cannot be unified + pub fn fast_proptype_check( + &mut self, + prop: &str, + dtype: PropType, + ) -> Result>, PropError> { + fast_proptype_check(self.dict_mapper.map(), &self.d_types, prop, dtype) + } + + pub fn set_id_and_dtype(&mut self, key: impl Into, id: usize, dtype: PropType) { + self.dict_mapper.set_id(key, id); + let dtypes = self.d_types.deref_mut(); + if dtypes.len() <= id { + dtypes.resize(id + 1, PropType::Empty); + } + dtypes[id] = dtype; + } + + pub fn new_id_and_dtype(&mut self, key: impl Into, dtype: PropType) -> usize { + let id = self.dict_mapper.get_or_create_id(&key.into()); + let dtypes = self.d_types.deref_mut(); + if dtypes.len() <= id.inner() { + dtypes.resize(id.inner() + 1, PropType::Empty); + } + dtypes[id.inner()] = dtype; + id.inner() + } +} + +impl<'a> LockedPropMapper<'a> { + pub fn get_id(&self, prop: &str) -> Option { + self.dict_mapper.get_id(prop) + } + + pub fn get_dtype(&'a self, prop_id: usize) -> Option<&'a PropType> { + self.d_types.get(prop_id) + } + + /// Fast check for property type without unifying the types + /// Returns: + /// - `Some(Either::Left(id))` if the property type can be unified + /// - `Some(Either::Right(id))` if the property type is already set and no unification is needed + /// - `None` if the property type is not set + /// - `Err(PropError::PropertyTypeError)` if the property type cannot be unified + pub fn fast_proptype_check( + &self, + prop: &str, + dtype: PropType, + ) -> Result>, PropError> { + fast_proptype_check(self.dict_mapper.map(), &self.d_types, prop, dtype) + } +} + +fn fast_proptype_check( + mapper: &FxHashMap, + d_types: &[PropType], + prop: &str, + dtype: PropType, +) -> Result>, PropError> { + match mapper.get(prop) { + Some(&id) => { + let existing_dtype = d_types + .get(id) + .expect("Existing id should always have a dtype"); + + let fast_check = check_for_unification(&dtype, existing_dtype); + if fast_check.is_none() { + // means nothing to do + return Ok(Some(Either::Right(id))); + } + let can_unify = fast_check.unwrap(); + if can_unify { + Ok(Some(Either::Left(id))) + } else { + Err(PropError::PropertyTypeError { + name: prop.to_string(), + expected: existing_dtype.clone(), + actual: dtype, + }) + } + } + None => Ok(None), + } } #[cfg(test)] diff --git a/raphtory-api/src/core/entities/properties/prop/prop_type.rs b/raphtory-api/src/core/entities/properties/prop/prop_type.rs index 77b59547af..2f31ea4c62 100644 --- a/raphtory-api/src/core/entities/properties/prop/prop_type.rs +++ b/raphtory-api/src/core/entities/properties/prop/prop_type.rs @@ -117,6 +117,26 @@ impl PropType { pub fn has_cmp(&self) -> bool { self.is_bool() || self.is_numeric() || self.is_str() || self.is_date() } + + // This is the best guess for the size of one row of properties + pub fn est_size(&self) -> usize { + const CONTAINER_SIZE: usize = 8; + match self { + PropType::Str => CONTAINER_SIZE, + PropType::U8 | PropType::Bool => 1, + PropType::U16 => 2, + PropType::I32 | PropType::F32 | PropType::U32 => 4, + PropType::I64 | PropType::F64 | PropType::U64 => 8, + PropType::NDTime | PropType::DTime => 8, + PropType::List(p_type) => p_type.est_size() * CONTAINER_SIZE, + PropType::Map(p_map) => { + p_map.iter().map(|(_, v)| v.est_size()).sum::() * CONTAINER_SIZE + } + PropType::Array(p_type) => p_type.est_size() * CONTAINER_SIZE, + PropType::Decimal { .. } => 16, + PropType::Empty => 0, + } + } } #[cfg(feature = "storage")] @@ -244,6 +264,67 @@ pub fn unify_types(l: &PropType, r: &PropType, unified: &mut bool) -> Result Option { + match (l, r) { + (PropType::Empty, _) => Some(true), + (_, PropType::Empty) => Some(true), + (PropType::Str, PropType::Str) => None, + (PropType::U8, PropType::U8) => None, + (PropType::U16, PropType::U16) => None, + (PropType::I32, PropType::I32) => None, + (PropType::I64, PropType::I64) => None, + (PropType::U32, PropType::U32) => None, + (PropType::U64, PropType::U64) => None, + (PropType::F32, PropType::F32) => None, + (PropType::F64, PropType::F64) => None, + (PropType::Bool, PropType::Bool) => None, + (PropType::NDTime, PropType::NDTime) => None, + (PropType::DTime, PropType::DTime) => None, + (PropType::List(l_type), PropType::List(r_type)) => check_for_unification(l_type, r_type), + (PropType::Array(l_type), PropType::Array(r_type)) => check_for_unification(l_type, r_type), + (PropType::Map(l_map), PropType::Map(r_map)) => { + let keys_check = l_map + .keys() + .any(|k| !r_map.contains_key(k)) + .then_some(true) + .or_else(|| r_map.keys().any(|k| !l_map.contains_key(k)).then_some(true)); + + // check for unification of the values + let inner_checks = l_map + .iter() + .filter_map(|(l_key, l_d_type)| { + r_map + .get(l_key) + .and_then(|r_d_type| check_for_unification(r_d_type, l_d_type)) + }) + .chain(r_map.iter().filter_map(|(r_key, r_d_type)| { + l_map + .get(r_key) + .and_then(|l_d_type| check_for_unification(r_d_type, l_d_type)) + })); + for check in inner_checks { + if !check { + return Some(false); + } else { + return Some(true); + } + } + keys_check + } + (PropType::Decimal { scale: l_scale }, PropType::Decimal { scale: r_scale }) + if l_scale == r_scale => + { + None + } + _ => Some(false), + } +} + #[cfg(test)] mod test { use super::*; diff --git a/raphtory-api/src/core/entities/properties/tprop.rs b/raphtory-api/src/core/entities/properties/tprop.rs index b987adb498..f30398492c 100644 --- a/raphtory-api/src/core/entities/properties/tprop.rs +++ b/raphtory-api/src/core/entities/properties/tprop.rs @@ -15,24 +15,47 @@ pub trait TPropOps<'a>: Clone + Send + Sync + Sized + 'a { } fn last_before(&self, t: TimeIndexEntry) -> Option<(TimeIndexEntry, Prop)> { - self.clone().iter_window(TimeIndexEntry::MIN..t).next_back() + self.clone() + .iter_inner_rev(Some(TimeIndexEntry::MIN..t)) + .next() } - fn iter(self) -> impl DoubleEndedIterator + Send + Sync + 'a; + fn iter_inner( + self, + range: Option>, + ) -> impl Iterator + Send + Sync + 'a; - fn iter_t(self) -> impl DoubleEndedIterator + Send + Sync + 'a { - self.iter().map(|(t, v)| (t.t(), v)) + fn iter_inner_rev( + self, + range: Option>, + ) -> impl Iterator + Send + Sync + 'a; + + fn iter(self) -> impl Iterator + Send + Sync + 'a { + self.iter_inner(None) } + fn iter_rev(self) -> impl Iterator + Send + Sync + 'a { + self.iter_inner_rev(None) + } fn iter_window( self, r: Range, - ) -> impl DoubleEndedIterator + Send + Sync + 'a; + ) -> impl Iterator + Send + Sync + 'a { + self.iter_inner(Some(r)) + } - fn iter_window_t( + fn iter_window_rev( self, - r: Range, - ) -> impl DoubleEndedIterator + Send + Sync + 'a { + r: Range, + ) -> impl Iterator + Send + Sync + 'a { + self.iter_inner_rev(Some(r)) + } + + fn iter_t(self) -> impl Iterator + Send + Sync + 'a { + self.iter().map(|(t, v)| (t.t(), v)) + } + + fn iter_window_t(self, r: Range) -> impl Iterator + Send + Sync + 'a { self.iter_window(TimeIndexEntry::range(r)) .map(|(t, v)| (t.t(), v)) } @@ -40,7 +63,7 @@ pub trait TPropOps<'a>: Clone + Send + Sync + Sized + 'a { fn iter_window_te( self, r: Range, - ) -> impl DoubleEndedIterator + Send + Sync + 'a { + ) -> impl Iterator + Send + Sync + 'a { self.iter_window(r).map(|(t, v)| (t.t(), v)) } diff --git a/raphtory-api/src/core/storage/dict_mapper.rs b/raphtory-api/src/core/storage/dict_mapper.rs index 8fe4f1ee22..6f4c6814c2 100644 --- a/raphtory-api/src/core/storage/dict_mapper.rs +++ b/raphtory-api/src/core/storage/dict_mapper.rs @@ -1,16 +1,18 @@ -use crate::core::storage::{arc_str::ArcStr, locked_vec::ArcReadLockedVec, FxDashMap}; -use dashmap::mapref::entry::Entry; -use parking_lot::RwLock; +use crate::core::storage::{arc_str::ArcStr, locked_vec::ArcReadLockedVec}; +use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard}; +use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; use std::{ borrow::{Borrow, BorrowMut}, + collections::hash_map::Entry, hash::Hash, + ops::DerefMut, sync::Arc, }; #[derive(Serialize, Deserialize, Default, Debug)] pub struct DictMapper { - map: FxDashMap, + map: Arc>>, reverse_map: Arc>>, //FIXME: a boxcar vector would be a great fit if it was serializable... } @@ -31,6 +33,11 @@ where } impl MaybeNew { + #[inline] + pub fn is_new(&self) -> bool { + matches!(self, MaybeNew::New(_)) + } + #[inline] pub fn inner(self) -> Index { match self { @@ -97,6 +104,61 @@ impl BorrowMut for MaybeNew { } } +pub struct LockedDictMapper<'a> { + map: RwLockReadGuard<'a, FxHashMap>, + reverse_map: RwLockReadGuard<'a, Vec>, +} + +pub struct WriteLockedDictMapper<'a> { + map: RwLockWriteGuard<'a, FxHashMap>, + reverse_map: RwLockWriteGuard<'a, Vec>, +} + +impl LockedDictMapper<'_> { + pub fn get_id(&self, name: &str) -> Option { + self.map.get(name).copied() + } + + pub fn map(&self) -> &FxHashMap { + &self.map + } +} + +impl WriteLockedDictMapper<'_> { + pub fn get_or_create_id(&mut self, name: &Q) -> MaybeNew + where + Q: Hash + Eq + ?Sized + ToOwned + Borrow, + T: Into, + { + let name = name.to_owned().into(); + let new_id = match self.map.entry(name.clone()) { + Entry::Occupied(entry) => MaybeNew::Existing(*entry.get()), + Entry::Vacant(entry) => { + let id = self.reverse_map.len(); + self.reverse_map.push(name); + entry.insert(id); + MaybeNew::New(id) + } + }; + new_id + } + + pub fn set_id(&mut self, name: impl Into, id: usize) { + let arc_name = name.into(); + let map_entry = self.map.entry(arc_name.clone()); + let keys = self.reverse_map.deref_mut(); + if keys.len() <= id { + keys.resize(id + 1, Default::default()) + } + keys[id] = arc_name; + map_entry.insert_entry(id); + } + + pub fn map(&self) -> &FxHashMap { + &self.map + } +} + impl DictMapper { pub fn deep_clone(&self) -> Self { let reverse_map = self.reverse_map.read().clone(); @@ -106,17 +168,36 @@ impl DictMapper { reverse_map: Arc::new(RwLock::new(reverse_map)), } } + + pub fn read(&self) -> LockedDictMapper { + LockedDictMapper { + map: self.map.read(), + reverse_map: self.reverse_map.read(), + } + } + + pub fn write(&self) -> WriteLockedDictMapper { + WriteLockedDictMapper { + map: self.map.write(), + reverse_map: self.reverse_map.write(), + } + } + pub fn get_or_create_id(&self, name: &Q) -> MaybeNew where Q: Hash + Eq + ?Sized + ToOwned + Borrow, T: Into, { - if let Some(existing_id) = self.map.get(name.borrow()) { + let map = self.map.read(); + if let Some(existing_id) = map.get(name.borrow()) { return MaybeNew::Existing(*existing_id); } + drop(map); + + let mut map = self.map.write(); let name = name.to_owned().into(); - let new_id = match self.map.entry(name.clone()) { + let new_id = match map.entry(name.clone()) { Entry::Occupied(entry) => MaybeNew::Existing(*entry.get()), Entry::Vacant(entry) => { let mut reverse = self.reverse_map.write(); @@ -130,19 +211,28 @@ impl DictMapper { } pub fn get_id(&self, name: &str) -> Option { - self.map.get(name).map(|id| *id) + self.map.read().get(name).map(|id| *id) } /// Explicitly set the id for a key (useful for initialising the map in parallel) pub fn set_id(&self, name: impl Into, id: usize) { + let mut map = self.map.write(); let arc_name = name.into(); - let map_entry = self.map.entry(arc_name.clone()); + let map_entry = map.entry(arc_name.clone()); let mut keys = self.reverse_map.write(); if keys.len() <= id { keys.resize(id + 1, Default::default()) } keys[id] = arc_name; - map_entry.insert(id); + map_entry.insert_entry(id); + } + + pub fn set_reverse_id(&self, id: usize, name: impl Into) { + let mut keys = self.reverse_map.write(); + if keys.len() <= id { + keys.resize(id + 1, Default::default()) + } + keys[id] = name.into(); } pub fn has_name(&self, id: usize) -> bool { @@ -152,10 +242,9 @@ impl DictMapper { pub fn get_name(&self, id: usize) -> ArcStr { let guard = self.reverse_map.read(); - guard - .get(id) - .cloned() - .expect("internal ids should always be mapped to a name") + guard.get(id).cloned().expect(&format!( + "internal ids should always be mapped to a name {id}" + )) } pub fn get_keys(&self) -> ArcReadLockedVec { @@ -165,7 +254,7 @@ impl DictMapper { } pub fn get_values(&self) -> Vec { - self.map.iter().map(|entry| *entry.value()).collect() + self.map.read().iter().map(|(_, &entry)| entry).collect() } pub fn len(&self) -> usize { diff --git a/raphtory-core/src/entities/graph/logical_to_physical.rs b/raphtory-core/src/entities/graph/logical_to_physical.rs index cf4d1afe93..6c0754ecb2 100644 --- a/raphtory-core/src/entities/graph/logical_to_physical.rs +++ b/raphtory-core/src/entities/graph/logical_to_physical.rs @@ -55,6 +55,13 @@ pub struct Mapping { } impl Mapping { + pub fn len(&self) -> usize { + self.map.get().map_or(0, |map| match map { + Map::U64(map) => map.len(), + Map::Str(map) => map.len(), + }) + } + pub fn dtype(&self) -> Option { self.map.get().map(|map| match map { Map::U64(_) => GidType::U64, diff --git a/raphtory-core/src/entities/nodes/node_store.rs b/raphtory-core/src/entities/nodes/node_store.rs index ea8afd50bd..e32d40752f 100644 --- a/raphtory-core/src/entities/nodes/node_store.rs +++ b/raphtory-core/src/entities/nodes/node_store.rs @@ -37,17 +37,17 @@ pub struct NodeStore { pub node_type: usize, /// For every property id keep a hash map of timestamps to values pointing to the property entries in the props vector - timestamps: NodeTimestamps, + timestamps: PropTimestamps, } #[derive(Serialize, Deserialize, Debug, Default, PartialEq)] -pub struct NodeTimestamps { +pub struct PropTimestamps { // all the timestamps that have been seen by this node pub edge_ts: TCell, pub props_ts: TCell>, } -impl NodeTimestamps { +impl PropTimestamps { pub fn edge_ts(&self) -> &TCell { &self.edge_ts } @@ -57,9 +57,9 @@ impl NodeTimestamps { } } -impl<'a> TimeIndexOps<'a> for &'a NodeTimestamps { +impl<'a> TimeIndexOps<'a> for &'a PropTimestamps { type IndexType = TimeIndexEntry; - type RangeType = TimeIndexWindow<'a, TimeIndexEntry, NodeTimestamps>; + type RangeType = TimeIndexWindow<'a, TimeIndexEntry, PropTimestamps>; #[inline] fn active(&self, w: Range) -> bool { @@ -114,7 +114,7 @@ impl<'a> TimeIndexOps<'a> for &'a NodeTimestamps { } } -impl<'a> TimeIndexLike<'a> for &'a NodeTimestamps { +impl<'a> TimeIndexLike<'a> for &'a PropTimestamps { fn range_iter( self, w: Range, @@ -206,7 +206,7 @@ impl NodeStore { &self.global_id } - pub fn timestamps(&self) -> &NodeTimestamps { + pub fn timestamps(&self) -> &PropTimestamps { &self.timestamps } diff --git a/raphtory-core/src/entities/nodes/structure/adj.rs b/raphtory-core/src/entities/nodes/structure/adj.rs index ec1e27b7b9..8dba2bbdbe 100644 --- a/raphtory-core/src/entities/nodes/structure/adj.rs +++ b/raphtory-core/src/entities/nodes/structure/adj.rs @@ -1,4 +1,5 @@ use crate::entities::{edges::edge_ref::Dir, nodes::structure::adjset::AdjSet, EID, VID}; +use either::Either; use itertools::Itertools; use raphtory_api::{ core::{Direction, DirectionVariants}, @@ -18,7 +19,7 @@ pub enum Adj { } impl Adj { - pub(crate) fn get_edge(&self, v: VID, dir: Direction) -> Option { + pub fn get_edge(&self, v: VID, dir: Direction) -> Option { match self { Adj::Solo => None, Adj::List { out, into } => match dir { @@ -45,14 +46,14 @@ impl Adj { } } - pub(crate) fn add_edge_into(&mut self, v: VID, e: EID) { + pub fn add_edge_into(&mut self, v: VID, e: EID) { match self { Adj::Solo => *self = Self::new_into(v, e), Adj::List { into, .. } => into.push(v, e), } } - pub(crate) fn add_edge_out(&mut self, v: VID, e: EID) { + pub fn add_edge_out(&mut self, v: VID, e: EID) { match self { Adj::Solo => *self = Self::new_out(v, e), Adj::List { out, .. } => out.push(v, e), @@ -70,6 +71,20 @@ impl Adj { } } + pub fn out_iter(&self) -> impl Iterator + Send + Sync + '_ { + match self { + Adj::Solo => Either::Left(std::iter::empty()), + Adj::List { out, .. } => Either::Right(out.iter()), + } + } + + pub fn inb_iter(&self) -> impl Iterator + Send + Sync + '_ { + match self { + Adj::Solo => Either::Left(std::iter::empty()), + Adj::List { into, .. } => Either::Right(into.iter()), + } + } + pub fn node_iter(&self, dir: Direction) -> impl Iterator + Send + '_ { let iter = self.iter(dir).map(|(v, _)| v); match dir { diff --git a/raphtory-core/src/entities/properties/tprop.rs b/raphtory-core/src/entities/properties/tprop.rs index cb9635e5e9..786798fe01 100644 --- a/raphtory-core/src/entities/properties/tprop.rs +++ b/raphtory-core/src/entities/properties/tprop.rs @@ -1,9 +1,10 @@ use crate::{ entities::properties::tcell::TCell, - storage::{timeindex::TimeIndexEntry, TPropColumn}, + storage::{timeindex::TimeIndexEntry, PropColumn}, }; use bigdecimal::BigDecimal; use chrono::{DateTime, NaiveDateTime, Utc}; +use either::Either; use iter_enum::{DoubleEndedIterator, ExactSizeIterator, FusedIterator, Iterator}; #[cfg(feature = "arrow")] use raphtory_api::core::entities::properties::prop::PropArray; @@ -89,41 +90,72 @@ pub enum TPropVariants< Decimal(Decimal), } -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, Default)] pub struct TPropCell<'a> { t_cell: Option<&'a TCell>>, - log: Option<&'a TPropColumn>, + log: Option<&'a PropColumn>, } impl<'a> TPropCell<'a> { - pub(crate) fn new(t_cell: &'a TCell>, log: Option<&'a TPropColumn>) -> Self { + pub fn new(t_cell: &'a TCell>, log: Option<&'a PropColumn>) -> Self { Self { t_cell: Some(t_cell), log, } } -} -impl<'a> TPropOps<'a> for TPropCell<'a> { - fn iter(self) -> impl DoubleEndedIterator + Send + Sync + 'a { - let log = self.log; + fn iter_window_inner( + self, + r: Range, + ) -> impl DoubleEndedIterator + Send + 'a { self.t_cell.into_iter().flat_map(move |t_cell| { t_cell - .iter() - .filter_map(move |(t, &id)| log?.get(id?).map(|prop| (*t, prop))) + .iter_window(r.clone()) + .filter_map(move |(t, &id)| self.log?.get(id?).map(|prop| (*t, prop))) }) } - fn iter_window( - self, - r: Range, - ) -> impl DoubleEndedIterator + Send + Sync + 'a { + fn iter_inner(self) -> impl DoubleEndedIterator + Send + 'a { self.t_cell.into_iter().flat_map(move |t_cell| { t_cell - .iter_window(r.clone()) + .iter() .filter_map(move |(t, &id)| self.log?.get(id?).map(|prop| (*t, prop))) }) } +} + +impl<'a> TPropOps<'a> for TPropCell<'a> { + fn iter_inner( + self, + range: Option>, + ) -> impl Iterator + Send + Sync + 'a { + match range { + Some(w) => { + let iter = self.iter_window_inner(w); + Either::Right(iter) + } + None => { + let iter = self.iter_inner(); + Either::Left(iter) + } + } + } + + fn iter_inner_rev( + self, + range: Option>, + ) -> impl Iterator + Send + Sync + 'a { + match range { + Some(w) => { + let iter = self.iter_window_inner(w).rev(); + Either::Right(iter) + } + None => { + let iter = self.iter_inner().rev(); + Either::Left(iter) + } + } + } fn at(&self, ti: &TimeIndexEntry) -> Option { self.t_cell?.at(ti).and_then(|&id| self.log?.get(id?)) @@ -242,98 +274,11 @@ impl TProp { } Ok(()) } -} - -impl<'a> TPropOps<'a> for &'a TProp { - fn last_before(&self, t: TimeIndexEntry) -> Option<(TimeIndexEntry, Prop)> { - match self { - TProp::Empty => None, - TProp::Str(cell) => cell.last_before(t).map(|(t, v)| (t, Prop::Str(v.clone()))), - TProp::I32(cell) => cell.last_before(t).map(|(t, v)| (t, Prop::I32(*v))), - TProp::I64(cell) => cell.last_before(t).map(|(t, v)| (t, Prop::I64(*v))), - TProp::U8(cell) => cell.last_before(t).map(|(t, v)| (t, Prop::U8(*v))), - TProp::U16(cell) => cell.last_before(t).map(|(t, v)| (t, Prop::U16(*v))), - TProp::U32(cell) => cell.last_before(t).map(|(t, v)| (t, Prop::U32(*v))), - TProp::U64(cell) => cell.last_before(t).map(|(t, v)| (t, Prop::U64(*v))), - TProp::F32(cell) => cell.last_before(t).map(|(t, v)| (t, Prop::F32(*v))), - TProp::F64(cell) => cell.last_before(t).map(|(t, v)| (t, Prop::F64(*v))), - TProp::Bool(cell) => cell.last_before(t).map(|(t, v)| (t, Prop::Bool(*v))), - TProp::DTime(cell) => cell.last_before(t).map(|(t, v)| (t, Prop::DTime(*v))), - TProp::NDTime(cell) => cell.last_before(t).map(|(t, v)| (t, Prop::NDTime(*v))), - #[cfg(feature = "arrow")] - TProp::Array(cell) => cell - .last_before(t) - .map(|(t, v)| (t, Prop::Array(v.clone()))), - TProp::List(cell) => cell.last_before(t).map(|(t, v)| (t, Prop::List(v.clone()))), - TProp::Map(cell) => cell.last_before(t).map(|(t, v)| (t, Prop::Map(v.clone()))), - TProp::Decimal(cell) => cell - .last_before(t) - .map(|(t, v)| (t, Prop::Decimal(v.clone()))), - } - } - - fn iter(self) -> impl DoubleEndedIterator + Send + Sync + 'a { - match self { - TProp::Empty => TPropVariants::Empty(iter::empty()), - TProp::Str(cell) => { - TPropVariants::Str(cell.iter().map(|(t, value)| (*t, Prop::Str(value.clone())))) - } - TProp::I32(cell) => { - TPropVariants::I32(cell.iter().map(|(t, value)| (*t, Prop::I32(*value)))) - } - TProp::I64(cell) => { - TPropVariants::I64(cell.iter().map(|(t, value)| (*t, Prop::I64(*value)))) - } - TProp::U8(cell) => { - TPropVariants::U8(cell.iter().map(|(t, value)| (*t, Prop::U8(*value)))) - } - TProp::U16(cell) => { - TPropVariants::U16(cell.iter().map(|(t, value)| (*t, Prop::U16(*value)))) - } - TProp::U32(cell) => { - TPropVariants::U32(cell.iter().map(|(t, value)| (*t, Prop::U32(*value)))) - } - TProp::U64(cell) => { - TPropVariants::U64(cell.iter().map(|(t, value)| (*t, Prop::U64(*value)))) - } - TProp::F32(cell) => { - TPropVariants::F32(cell.iter().map(|(t, value)| (*t, Prop::F32(*value)))) - } - TProp::F64(cell) => { - TPropVariants::F64(cell.iter().map(|(t, value)| (*t, Prop::F64(*value)))) - } - TProp::Bool(cell) => { - TPropVariants::Bool(cell.iter().map(|(t, value)| (*t, Prop::Bool(*value)))) - } - TProp::DTime(cell) => { - TPropVariants::DTime(cell.iter().map(|(t, value)| (*t, Prop::DTime(*value)))) - } - TProp::NDTime(cell) => { - TPropVariants::NDTime(cell.iter().map(|(t, value)| (*t, Prop::NDTime(*value)))) - } - #[cfg(feature = "arrow")] - TProp::Array(cell) => TPropVariants::Array( - cell.iter() - .map(|(t, value)| (*t, Prop::Array(value.clone()))), - ), - TProp::List(cell) => TPropVariants::List( - cell.iter() - .map(|(t, value)| (*t, Prop::List(value.clone()))), - ), - TProp::Map(cell) => { - TPropVariants::Map(cell.iter().map(|(t, value)| (*t, Prop::Map(value.clone())))) - } - TProp::Decimal(cell) => TPropVariants::Decimal( - cell.iter() - .map(|(t, value)| (*t, Prop::Decimal(value.clone()))), - ), - } - } - fn iter_window( - self, + pub(crate) fn iter_window_inner( + &self, r: Range, - ) -> impl DoubleEndedIterator + Send + Sync + 'a { + ) -> impl DoubleEndedIterator + Send + Sync + '_ { match self { TProp::Empty => TPropVariants::Empty(iter::empty()), TProp::Str(cell) => TPropVariants::Str( @@ -403,6 +348,95 @@ impl<'a> TPropOps<'a> for &'a TProp { } } + pub(crate) fn iter_inner( + &self, + ) -> impl DoubleEndedIterator + Send + Sync + '_ { + match self { + TProp::Empty => TPropVariants::Empty(iter::empty()), + TProp::Str(cell) => { + TPropVariants::Str(cell.iter().map(|(t, value)| (*t, Prop::Str(value.clone())))) + } + TProp::I32(cell) => { + TPropVariants::I32(cell.iter().map(|(t, value)| (*t, Prop::I32(*value)))) + } + TProp::I64(cell) => { + TPropVariants::I64(cell.iter().map(|(t, value)| (*t, Prop::I64(*value)))) + } + TProp::U8(cell) => { + TPropVariants::U8(cell.iter().map(|(t, value)| (*t, Prop::U8(*value)))) + } + TProp::U16(cell) => { + TPropVariants::U16(cell.iter().map(|(t, value)| (*t, Prop::U16(*value)))) + } + TProp::U32(cell) => { + TPropVariants::U32(cell.iter().map(|(t, value)| (*t, Prop::U32(*value)))) + } + TProp::U64(cell) => { + TPropVariants::U64(cell.iter().map(|(t, value)| (*t, Prop::U64(*value)))) + } + TProp::F32(cell) => { + TPropVariants::F32(cell.iter().map(|(t, value)| (*t, Prop::F32(*value)))) + } + TProp::F64(cell) => { + TPropVariants::F64(cell.iter().map(|(t, value)| (*t, Prop::F64(*value)))) + } + TProp::Bool(cell) => { + TPropVariants::Bool(cell.iter().map(|(t, value)| (*t, Prop::Bool(*value)))) + } + TProp::DTime(cell) => { + TPropVariants::DTime(cell.iter().map(|(t, value)| (*t, Prop::DTime(*value)))) + } + TProp::NDTime(cell) => { + TPropVariants::NDTime(cell.iter().map(|(t, value)| (*t, Prop::NDTime(*value)))) + } + #[cfg(feature = "arrow")] + TProp::Array(cell) => TPropVariants::Array( + cell.iter() + .map(|(t, value)| (*t, Prop::Array(value.clone()))), + ), + TProp::List(cell) => TPropVariants::List( + cell.iter() + .map(|(t, value)| (*t, Prop::List(value.clone()))), + ), + TProp::Map(cell) => { + TPropVariants::Map(cell.iter().map(|(t, value)| (*t, Prop::Map(value.clone())))) + } + TProp::Decimal(cell) => TPropVariants::Decimal( + cell.iter() + .map(|(t, value)| (*t, Prop::Decimal(value.clone()))), + ), + } + } +} + +impl<'a> TPropOps<'a> for &'a TProp { + fn last_before(&self, t: TimeIndexEntry) -> Option<(TimeIndexEntry, Prop)> { + match self { + TProp::Empty => None, + TProp::Str(cell) => cell.last_before(t).map(|(t, v)| (t, Prop::Str(v.clone()))), + TProp::I32(cell) => cell.last_before(t).map(|(t, v)| (t, Prop::I32(*v))), + TProp::I64(cell) => cell.last_before(t).map(|(t, v)| (t, Prop::I64(*v))), + TProp::U8(cell) => cell.last_before(t).map(|(t, v)| (t, Prop::U8(*v))), + TProp::U16(cell) => cell.last_before(t).map(|(t, v)| (t, Prop::U16(*v))), + TProp::U32(cell) => cell.last_before(t).map(|(t, v)| (t, Prop::U32(*v))), + TProp::U64(cell) => cell.last_before(t).map(|(t, v)| (t, Prop::U64(*v))), + TProp::F32(cell) => cell.last_before(t).map(|(t, v)| (t, Prop::F32(*v))), + TProp::F64(cell) => cell.last_before(t).map(|(t, v)| (t, Prop::F64(*v))), + TProp::Bool(cell) => cell.last_before(t).map(|(t, v)| (t, Prop::Bool(*v))), + TProp::DTime(cell) => cell.last_before(t).map(|(t, v)| (t, Prop::DTime(*v))), + TProp::NDTime(cell) => cell.last_before(t).map(|(t, v)| (t, Prop::NDTime(*v))), + #[cfg(feature = "arrow")] + TProp::Array(cell) => cell + .last_before(t) + .map(|(t, v)| (t, Prop::Array(v.clone()))), + TProp::List(cell) => cell.last_before(t).map(|(t, v)| (t, Prop::List(v.clone()))), + TProp::Map(cell) => cell.last_before(t).map(|(t, v)| (t, Prop::Map(v.clone()))), + TProp::Decimal(cell) => cell + .last_before(t) + .map(|(t, v)| (t, Prop::Decimal(v.clone()))), + } + } + fn at(&self, ti: &TimeIndexEntry) -> Option { match self { TProp::Empty => None, @@ -425,6 +459,38 @@ impl<'a> TPropOps<'a> for &'a TProp { TProp::Decimal(cell) => cell.at(ti).map(|v| Prop::Decimal(v.clone())), } } + + fn iter_inner( + self, + range: Option>, + ) -> impl Iterator + Send + Sync + 'a { + match range { + Some(w) => { + let iter = self.iter_window_inner(w); + Either::Right(iter) + } + None => { + let iter = self.iter_inner(); + Either::Left(iter) + } + } + } + + fn iter_inner_rev( + self, + range: Option>, + ) -> impl Iterator + Send + Sync + 'a { + match range { + Some(w) => { + let iter = self.iter_window_inner(w).rev(); + Either::Right(iter) + } + None => { + let iter = self.iter_inner().rev(); + Either::Left(iter) + } + } + } } #[cfg(test)] @@ -435,7 +501,7 @@ mod tprop_tests { #[test] fn t_prop_cell() { - let col = TPropColumn::Bool(LazyVec::from(0, true)); + let col = PropColumn::Bool(LazyVec::from(0, true)); assert_eq!(col.get(0), Some(Prop::Bool(true))); let t_prop = TPropCell::new(&TCell::TCell1(TimeIndexEntry(0, 0), Some(0)), Some(&col)); diff --git a/raphtory-core/src/storage/lazy_vec.rs b/raphtory-core/src/storage/lazy_vec.rs index d3e78e44c3..e0b24cd578 100644 --- a/raphtory-core/src/storage/lazy_vec.rs +++ b/raphtory-core/src/storage/lazy_vec.rs @@ -293,7 +293,7 @@ where } } - pub(crate) fn get_opt(&self, id: usize) -> Option<&A> { + pub fn get_opt(&self, id: usize) -> Option<&A> { match self { LazyVec::LazyVec1(_, tuples) => tuples.get(id), LazyVec::LazyVecN(_, vec) => vec.get(id), diff --git a/raphtory-core/src/storage/mod.rs b/raphtory-core/src/storage/mod.rs index 3dcecb8343..edf9d3e767 100644 --- a/raphtory-core/src/storage/mod.rs +++ b/raphtory-core/src/storage/mod.rs @@ -81,7 +81,7 @@ pub struct NodeSlot { #[derive(Debug, Serialize, Deserialize, PartialEq, Default)] pub struct TColumns { - t_props_log: Vec, + t_props_log: Vec, num_rows: usize, } @@ -97,9 +97,9 @@ impl TColumns { match self.t_props_log.get_mut(prop_id) { Some(col) => col.push(prop)?, None => { - let col: TPropColumn = TPropColumn::new(self.num_rows, prop); + let col: PropColumn = PropColumn::new(self.num_rows, prop); self.t_props_log - .resize_with(prop_id + 1, || TPropColumn::Empty(id)); + .resize_with(prop_id + 1, || PropColumn::Empty(id)); self.t_props_log[prop_id] = col; } } @@ -117,7 +117,20 @@ impl TColumns { } } - pub(crate) fn get(&self, prop_id: usize) -> Option<&TPropColumn> { + pub fn push_null(&mut self) -> usize { + let id = self.num_rows; + for col in self.t_props_log.iter_mut() { + col.push_null(); + } + self.num_rows += 1; + id + } + + pub fn get(&self, prop_id: usize) -> Option<&PropColumn> { + self.t_props_log.get(prop_id) + } + + pub fn getx(&self, prop_id: usize) -> Option<&PropColumn> { self.t_props_log.get(prop_id) } @@ -129,13 +142,17 @@ impl TColumns { self.num_rows == 0 } - pub fn iter(&self) -> impl Iterator { + pub fn iter(&self) -> impl Iterator { self.t_props_log.iter() } + + pub fn num_columns(&self) -> usize { + self.t_props_log.len() + } } #[derive(Debug, Serialize, Deserialize, PartialEq)] -pub enum TPropColumn { +pub enum PropColumn { Empty(usize), Bool(LazyVec), U8(LazyVec), @@ -195,39 +212,39 @@ pub enum TPropColumnError { IllegalPropType(#[from] IllegalPropType), } -impl Default for TPropColumn { +impl Default for PropColumn { fn default() -> Self { - TPropColumn::Empty(0) + PropColumn::Empty(0) } } -impl TPropColumn { +impl PropColumn { pub(crate) fn new(idx: usize, prop: Prop) -> Self { - let mut col = TPropColumn::default(); + let mut col = PropColumn::default(); col.set(idx, prop).unwrap(); col } pub(crate) fn dtype(&self) -> PropType { match self { - TPropColumn::Empty(_) => PropType::Empty, - TPropColumn::Bool(_) => PropType::Bool, - TPropColumn::U8(_) => PropType::U8, - TPropColumn::U16(_) => PropType::U16, - TPropColumn::U32(_) => PropType::U32, - TPropColumn::U64(_) => PropType::U64, - TPropColumn::I32(_) => PropType::I32, - TPropColumn::I64(_) => PropType::I64, - TPropColumn::F32(_) => PropType::F32, - TPropColumn::F64(_) => PropType::F64, - TPropColumn::Str(_) => PropType::Str, + PropColumn::Empty(_) => PropType::Empty, + PropColumn::Bool(_) => PropType::Bool, + PropColumn::U8(_) => PropType::U8, + PropColumn::U16(_) => PropType::U16, + PropColumn::U32(_) => PropType::U32, + PropColumn::U64(_) => PropType::U64, + PropColumn::I32(_) => PropType::I32, + PropColumn::I64(_) => PropType::I64, + PropColumn::F32(_) => PropType::F32, + PropColumn::F64(_) => PropType::F64, + PropColumn::Str(_) => PropType::Str, #[cfg(feature = "arrow")] - TPropColumn::Array(_) => PropType::Array(Box::new(PropType::Empty)), - TPropColumn::List(_) => PropType::List(Box::new(PropType::Empty)), - TPropColumn::Map(_) => PropType::Map(HashMap::new().into()), - TPropColumn::NDTime(_) => PropType::NDTime, - TPropColumn::DTime(_) => PropType::DTime, - TPropColumn::Decimal(_) => PropType::Decimal { scale: 0 }, + PropColumn::Array(_) => PropType::Array(Box::new(PropType::Empty)), + PropColumn::List(_) => PropType::List(Box::new(PropType::Empty)), + PropColumn::Map(_) => PropType::Map(HashMap::new().into()), + PropColumn::NDTime(_) => PropType::NDTime, + PropColumn::DTime(_) => PropType::DTime, + PropColumn::Decimal(_) => PropType::Decimal { scale: 0 }, } } @@ -237,26 +254,26 @@ impl TPropColumn { } } - pub(crate) fn set(&mut self, index: usize, prop: Prop) -> Result<(), TPropColumnError> { + pub fn set(&mut self, index: usize, prop: Prop) -> Result<(), TPropColumnError> { self.init_empty_col(&prop); match (self, prop) { - (TPropColumn::Bool(col), Prop::Bool(v)) => col.set(index, v)?, - (TPropColumn::I64(col), Prop::I64(v)) => col.set(index, v)?, - (TPropColumn::U32(col), Prop::U32(v)) => col.set(index, v)?, - (TPropColumn::U64(col), Prop::U64(v)) => col.set(index, v)?, - (TPropColumn::F32(col), Prop::F32(v)) => col.set(index, v)?, - (TPropColumn::F64(col), Prop::F64(v)) => col.set(index, v)?, - (TPropColumn::Str(col), Prop::Str(v)) => col.set(index, v)?, + (PropColumn::Bool(col), Prop::Bool(v)) => col.set(index, v)?, + (PropColumn::I64(col), Prop::I64(v)) => col.set(index, v)?, + (PropColumn::U32(col), Prop::U32(v)) => col.set(index, v)?, + (PropColumn::U64(col), Prop::U64(v)) => col.set(index, v)?, + (PropColumn::F32(col), Prop::F32(v)) => col.set(index, v)?, + (PropColumn::F64(col), Prop::F64(v)) => col.set(index, v)?, + (PropColumn::Str(col), Prop::Str(v)) => col.set(index, v)?, #[cfg(feature = "arrow")] - (TPropColumn::Array(col), Prop::Array(v)) => col.set(index, v)?, - (TPropColumn::U8(col), Prop::U8(v)) => col.set(index, v)?, - (TPropColumn::U16(col), Prop::U16(v)) => col.set(index, v)?, - (TPropColumn::I32(col), Prop::I32(v)) => col.set(index, v)?, - (TPropColumn::List(col), Prop::List(v)) => col.set(index, v)?, - (TPropColumn::Map(col), Prop::Map(v)) => col.set(index, v)?, - (TPropColumn::NDTime(col), Prop::NDTime(v)) => col.set(index, v)?, - (TPropColumn::DTime(col), Prop::DTime(v)) => col.set(index, v)?, - (TPropColumn::Decimal(col), Prop::Decimal(v)) => col.set(index, v)?, + (PropColumn::Array(col), Prop::Array(v)) => col.set(index, v)?, + (PropColumn::U8(col), Prop::U8(v)) => col.set(index, v)?, + (PropColumn::U16(col), Prop::U16(v)) => col.set(index, v)?, + (PropColumn::I32(col), Prop::I32(v)) => col.set(index, v)?, + (PropColumn::List(col), Prop::List(v)) => col.set(index, v)?, + (PropColumn::Map(col), Prop::Map(v)) => col.set(index, v)?, + (PropColumn::NDTime(col), Prop::NDTime(v)) => col.set(index, v)?, + (PropColumn::DTime(col), Prop::DTime(v)) => col.set(index, v)?, + (PropColumn::Decimal(col), Prop::Decimal(v)) => col.set(index, v)?, (col, prop) => { Err(IllegalPropType { expected: col.dtype(), @@ -270,23 +287,23 @@ impl TPropColumn { pub(crate) fn push(&mut self, prop: Prop) -> Result<(), IllegalPropType> { self.init_empty_col(&prop); match (self, prop) { - (TPropColumn::Bool(col), Prop::Bool(v)) => col.push(Some(v)), - (TPropColumn::U8(col), Prop::U8(v)) => col.push(Some(v)), - (TPropColumn::I64(col), Prop::I64(v)) => col.push(Some(v)), - (TPropColumn::U32(col), Prop::U32(v)) => col.push(Some(v)), - (TPropColumn::U64(col), Prop::U64(v)) => col.push(Some(v)), - (TPropColumn::F32(col), Prop::F32(v)) => col.push(Some(v)), - (TPropColumn::F64(col), Prop::F64(v)) => col.push(Some(v)), - (TPropColumn::Str(col), Prop::Str(v)) => col.push(Some(v)), + (PropColumn::Bool(col), Prop::Bool(v)) => col.push(Some(v)), + (PropColumn::U8(col), Prop::U8(v)) => col.push(Some(v)), + (PropColumn::I64(col), Prop::I64(v)) => col.push(Some(v)), + (PropColumn::U32(col), Prop::U32(v)) => col.push(Some(v)), + (PropColumn::U64(col), Prop::U64(v)) => col.push(Some(v)), + (PropColumn::F32(col), Prop::F32(v)) => col.push(Some(v)), + (PropColumn::F64(col), Prop::F64(v)) => col.push(Some(v)), + (PropColumn::Str(col), Prop::Str(v)) => col.push(Some(v)), #[cfg(feature = "arrow")] - (TPropColumn::Array(col), Prop::Array(v)) => col.push(Some(v)), - (TPropColumn::U16(col), Prop::U16(v)) => col.push(Some(v)), - (TPropColumn::I32(col), Prop::I32(v)) => col.push(Some(v)), - (TPropColumn::List(col), Prop::List(v)) => col.push(Some(v)), - (TPropColumn::Map(col), Prop::Map(v)) => col.push(Some(v)), - (TPropColumn::NDTime(col), Prop::NDTime(v)) => col.push(Some(v)), - (TPropColumn::DTime(col), Prop::DTime(v)) => col.push(Some(v)), - (TPropColumn::Decimal(col), Prop::Decimal(v)) => col.push(Some(v)), + (PropColumn::Array(col), Prop::Array(v)) => col.push(Some(v)), + (PropColumn::U16(col), Prop::U16(v)) => col.push(Some(v)), + (PropColumn::I32(col), Prop::I32(v)) => col.push(Some(v)), + (PropColumn::List(col), Prop::List(v)) => col.push(Some(v)), + (PropColumn::Map(col), Prop::Map(v)) => col.push(Some(v)), + (PropColumn::NDTime(col), Prop::NDTime(v)) => col.push(Some(v)), + (PropColumn::DTime(col), Prop::DTime(v)) => col.push(Some(v)), + (PropColumn::Decimal(col), Prop::Decimal(v)) => col.push(Some(v)), (col, prop) => { return Err(IllegalPropType { expected: col.dtype(), @@ -298,53 +315,53 @@ impl TPropColumn { } fn init_empty_col(&mut self, prop: &Prop) { - if let TPropColumn::Empty(len) = self { + if let PropColumn::Empty(len) = self { match prop { - Prop::Bool(_) => *self = TPropColumn::Bool(LazyVec::with_len(*len)), - Prop::I64(_) => *self = TPropColumn::I64(LazyVec::with_len(*len)), - Prop::U32(_) => *self = TPropColumn::U32(LazyVec::with_len(*len)), - Prop::U64(_) => *self = TPropColumn::U64(LazyVec::with_len(*len)), - Prop::F32(_) => *self = TPropColumn::F32(LazyVec::with_len(*len)), - Prop::F64(_) => *self = TPropColumn::F64(LazyVec::with_len(*len)), - Prop::Str(_) => *self = TPropColumn::Str(LazyVec::with_len(*len)), + Prop::Bool(_) => *self = PropColumn::Bool(LazyVec::with_len(*len)), + Prop::I64(_) => *self = PropColumn::I64(LazyVec::with_len(*len)), + Prop::U32(_) => *self = PropColumn::U32(LazyVec::with_len(*len)), + Prop::U64(_) => *self = PropColumn::U64(LazyVec::with_len(*len)), + Prop::F32(_) => *self = PropColumn::F32(LazyVec::with_len(*len)), + Prop::F64(_) => *self = PropColumn::F64(LazyVec::with_len(*len)), + Prop::Str(_) => *self = PropColumn::Str(LazyVec::with_len(*len)), #[cfg(feature = "arrow")] - Prop::Array(_) => *self = TPropColumn::Array(LazyVec::with_len(*len)), - Prop::U8(_) => *self = TPropColumn::U8(LazyVec::with_len(*len)), - Prop::U16(_) => *self = TPropColumn::U16(LazyVec::with_len(*len)), - Prop::I32(_) => *self = TPropColumn::I32(LazyVec::with_len(*len)), - Prop::List(_) => *self = TPropColumn::List(LazyVec::with_len(*len)), - Prop::Map(_) => *self = TPropColumn::Map(LazyVec::with_len(*len)), - Prop::NDTime(_) => *self = TPropColumn::NDTime(LazyVec::with_len(*len)), - Prop::DTime(_) => *self = TPropColumn::DTime(LazyVec::with_len(*len)), - Prop::Decimal(_) => *self = TPropColumn::Decimal(LazyVec::with_len(*len)), + Prop::Array(_) => *self = PropColumn::Array(LazyVec::with_len(*len)), + Prop::U8(_) => *self = PropColumn::U8(LazyVec::with_len(*len)), + Prop::U16(_) => *self = PropColumn::U16(LazyVec::with_len(*len)), + Prop::I32(_) => *self = PropColumn::I32(LazyVec::with_len(*len)), + Prop::List(_) => *self = PropColumn::List(LazyVec::with_len(*len)), + Prop::Map(_) => *self = PropColumn::Map(LazyVec::with_len(*len)), + Prop::NDTime(_) => *self = PropColumn::NDTime(LazyVec::with_len(*len)), + Prop::DTime(_) => *self = PropColumn::DTime(LazyVec::with_len(*len)), + Prop::Decimal(_) => *self = PropColumn::Decimal(LazyVec::with_len(*len)), } } } fn is_empty(&self) -> bool { - matches!(self, TPropColumn::Empty(_)) + matches!(self, PropColumn::Empty(_)) } pub(crate) fn push_null(&mut self) { match self { - TPropColumn::Bool(col) => col.push(None), - TPropColumn::I64(col) => col.push(None), - TPropColumn::U32(col) => col.push(None), - TPropColumn::U64(col) => col.push(None), - TPropColumn::F32(col) => col.push(None), - TPropColumn::F64(col) => col.push(None), - TPropColumn::Str(col) => col.push(None), + PropColumn::Bool(col) => col.push(None), + PropColumn::I64(col) => col.push(None), + PropColumn::U32(col) => col.push(None), + PropColumn::U64(col) => col.push(None), + PropColumn::F32(col) => col.push(None), + PropColumn::F64(col) => col.push(None), + PropColumn::Str(col) => col.push(None), #[cfg(feature = "arrow")] - TPropColumn::Array(col) => col.push(None), - TPropColumn::U8(col) => col.push(None), - TPropColumn::U16(col) => col.push(None), - TPropColumn::I32(col) => col.push(None), - TPropColumn::List(col) => col.push(None), - TPropColumn::Map(col) => col.push(None), - TPropColumn::NDTime(col) => col.push(None), - TPropColumn::DTime(col) => col.push(None), - TPropColumn::Decimal(col) => col.push(None), - TPropColumn::Empty(count) => { + PropColumn::Array(col) => col.push(None), + PropColumn::U8(col) => col.push(None), + PropColumn::U16(col) => col.push(None), + PropColumn::I32(col) => col.push(None), + PropColumn::List(col) => col.push(None), + PropColumn::Map(col) => col.push(None), + PropColumn::NDTime(col) => col.push(None), + PropColumn::DTime(col) => col.push(None), + PropColumn::Decimal(col) => col.push(None), + PropColumn::Empty(count) => { *count += 1; } } @@ -352,47 +369,47 @@ impl TPropColumn { pub fn get(&self, index: usize) -> Option { match self { - TPropColumn::Bool(col) => col.get_opt(index).map(|prop| (*prop).into()), - TPropColumn::I64(col) => col.get_opt(index).map(|prop| (*prop).into()), - TPropColumn::U32(col) => col.get_opt(index).map(|prop| (*prop).into()), - TPropColumn::U64(col) => col.get_opt(index).map(|prop| (*prop).into()), - TPropColumn::F32(col) => col.get_opt(index).map(|prop| (*prop).into()), - TPropColumn::F64(col) => col.get_opt(index).map(|prop| (*prop).into()), - TPropColumn::Str(col) => col.get_opt(index).map(|prop| prop.into()), + PropColumn::Bool(col) => col.get_opt(index).map(|prop| (*prop).into()), + PropColumn::I64(col) => col.get_opt(index).map(|prop| (*prop).into()), + PropColumn::U32(col) => col.get_opt(index).map(|prop| (*prop).into()), + PropColumn::U64(col) => col.get_opt(index).map(|prop| (*prop).into()), + PropColumn::F32(col) => col.get_opt(index).map(|prop| (*prop).into()), + PropColumn::F64(col) => col.get_opt(index).map(|prop| (*prop).into()), + PropColumn::Str(col) => col.get_opt(index).map(|prop| prop.into()), #[cfg(feature = "arrow")] - TPropColumn::Array(col) => col.get_opt(index).map(|prop| Prop::Array(prop.clone())), - TPropColumn::U8(col) => col.get_opt(index).map(|prop| (*prop).into()), - TPropColumn::U16(col) => col.get_opt(index).map(|prop| (*prop).into()), - TPropColumn::I32(col) => col.get_opt(index).map(|prop| (*prop).into()), - TPropColumn::List(col) => col.get_opt(index).map(|prop| Prop::List(prop.clone())), - TPropColumn::Map(col) => col.get_opt(index).map(|prop| Prop::Map(prop.clone())), - TPropColumn::NDTime(col) => col.get_opt(index).map(|prop| Prop::NDTime(*prop)), - TPropColumn::DTime(col) => col.get_opt(index).map(|prop| Prop::DTime(*prop)), - TPropColumn::Decimal(col) => col.get_opt(index).map(|prop| Prop::Decimal(prop.clone())), - TPropColumn::Empty(_) => None, + PropColumn::Array(col) => col.get_opt(index).map(|prop| Prop::Array(prop.clone())), + PropColumn::U8(col) => col.get_opt(index).map(|prop| (*prop).into()), + PropColumn::U16(col) => col.get_opt(index).map(|prop| (*prop).into()), + PropColumn::I32(col) => col.get_opt(index).map(|prop| (*prop).into()), + PropColumn::List(col) => col.get_opt(index).map(|prop| Prop::List(prop.clone())), + PropColumn::Map(col) => col.get_opt(index).map(|prop| Prop::Map(prop.clone())), + PropColumn::NDTime(col) => col.get_opt(index).map(|prop| Prop::NDTime(*prop)), + PropColumn::DTime(col) => col.get_opt(index).map(|prop| Prop::DTime(*prop)), + PropColumn::Decimal(col) => col.get_opt(index).map(|prop| Prop::Decimal(prop.clone())), + PropColumn::Empty(_) => None, } } pub(crate) fn len(&self) -> usize { match self { - TPropColumn::Bool(col) => col.len(), - TPropColumn::I64(col) => col.len(), - TPropColumn::U32(col) => col.len(), - TPropColumn::U64(col) => col.len(), - TPropColumn::F32(col) => col.len(), - TPropColumn::F64(col) => col.len(), - TPropColumn::Str(col) => col.len(), + PropColumn::Bool(col) => col.len(), + PropColumn::I64(col) => col.len(), + PropColumn::U32(col) => col.len(), + PropColumn::U64(col) => col.len(), + PropColumn::F32(col) => col.len(), + PropColumn::F64(col) => col.len(), + PropColumn::Str(col) => col.len(), #[cfg(feature = "arrow")] - TPropColumn::Array(col) => col.len(), - TPropColumn::U8(col) => col.len(), - TPropColumn::U16(col) => col.len(), - TPropColumn::I32(col) => col.len(), - TPropColumn::List(col) => col.len(), - TPropColumn::Map(col) => col.len(), - TPropColumn::NDTime(col) => col.len(), - TPropColumn::DTime(col) => col.len(), - TPropColumn::Decimal(col) => col.len(), - TPropColumn::Empty(count) => *count, + PropColumn::Array(col) => col.len(), + PropColumn::U8(col) => col.len(), + PropColumn::U16(col) => col.len(), + PropColumn::I32(col) => col.len(), + PropColumn::List(col) => col.len(), + PropColumn::Map(col) => col.len(), + PropColumn::NDTime(col) => col.len(), + PropColumn::DTime(col) => col.len(), + PropColumn::Decimal(col) => col.len(), + PropColumn::Empty(count) => *count, } } } diff --git a/raphtory-storage/src/disk/storage_interface/edges.rs b/raphtory-storage/src/disk/storage_interface/edges.rs index 898108c439..42dbf08675 100644 --- a/raphtory-storage/src/disk/storage_interface/edges.rs +++ b/raphtory-storage/src/disk/storage_interface/edges.rs @@ -75,7 +75,8 @@ impl DiskEdges { .into_par_iter() .map(EID) .filter(move |e| { - ids.into_iter() + ids.clone() + .into_iter() .any(|layer_id| self.graph.inner.edge(*e).has_layer_inner(layer_id)) }), ), diff --git a/raphtory-storage/src/graph/edges/edge_entry.rs b/raphtory-storage/src/graph/edges/edge_entry.rs index bec02dc2c1..3ae2d9fb78 100644 --- a/raphtory-storage/src/graph/edges/edge_entry.rs +++ b/raphtory-storage/src/graph/edges/edge_entry.rs @@ -7,7 +7,6 @@ use raphtory_api::core::entities::{ LayerIds, EID, VID, }; use raphtory_core::{entities::edges::edge_store::MemEdge, storage::raw_edges::EdgeRGuard}; -use rayon::prelude::*; use std::ops::Range; #[cfg(feature = "storage")] @@ -58,10 +57,6 @@ impl<'a, 'b: 'a> EdgeStorageOps<'a> for &'a EdgeStorageEntry<'b> { self.as_ref().layer_ids_iter(layer_ids) } - fn layer_ids_par_iter(self, layer_ids: &LayerIds) -> impl ParallelIterator + 'a { - self.as_ref().layer_ids_par_iter(layer_ids) - } - fn additions_iter( self, layer_ids: &'a LayerIds, @@ -69,13 +64,6 @@ impl<'a, 'b: 'a> EdgeStorageOps<'a> for &'a EdgeStorageEntry<'b> { self.as_ref().additions_iter(layer_ids) } - fn additions_par_iter( - self, - layer_ids: &LayerIds, - ) -> impl ParallelIterator)> + 'a { - self.as_ref().additions_par_iter(layer_ids) - } - fn deletions_iter( self, layer_ids: &'a LayerIds, @@ -83,13 +71,6 @@ impl<'a, 'b: 'a> EdgeStorageOps<'a> for &'a EdgeStorageEntry<'b> { self.as_ref().deletions_iter(layer_ids) } - fn deletions_par_iter( - self, - layer_ids: &LayerIds, - ) -> impl ParallelIterator)> + 'a { - self.as_ref().deletions_par_iter(layer_ids) - } - fn updates_iter( self, layer_ids: &'a LayerIds, @@ -97,13 +78,6 @@ impl<'a, 'b: 'a> EdgeStorageOps<'a> for &'a EdgeStorageEntry<'b> { self.as_ref().updates_iter(layer_ids) } - fn updates_par_iter( - self, - layer_ids: &LayerIds, - ) -> impl ParallelIterator, TimeIndexRef<'a>)> + 'a { - self.as_ref().updates_par_iter(layer_ids) - } - fn additions(self, layer_id: usize) -> TimeIndexRef<'a> { self.as_ref().additions(layer_id) } @@ -124,14 +98,6 @@ impl<'a, 'b: 'a> EdgeStorageOps<'a> for &'a EdgeStorageEntry<'b> { self.as_ref().temporal_prop_iter(layer_ids, prop_id) } - fn temporal_prop_par_iter( - self, - layer_ids: &LayerIds, - prop_id: usize, - ) -> impl ParallelIterator)> + 'a { - self.as_ref().temporal_prop_par_iter(layer_ids, prop_id) - } - fn constant_prop_layer(self, layer_id: usize, prop_id: usize) -> Option { self.as_ref().constant_prop_layer(layer_id, prop_id) } diff --git a/raphtory-storage/src/graph/edges/edge_ref.rs b/raphtory-storage/src/graph/edges/edge_ref.rs index bed37c9b97..dfbc9c3e03 100644 --- a/raphtory-storage/src/graph/edges/edge_ref.rs +++ b/raphtory-storage/src/graph/edges/edge_ref.rs @@ -4,7 +4,6 @@ use raphtory_api::core::entities::{ LayerIds, EID, VID, }; use raphtory_core::entities::edges::edge_store::MemEdge; -use rayon::prelude::*; use std::ops::Range; #[cfg(feature = "storage")] @@ -71,10 +70,6 @@ impl<'a> EdgeStorageOps<'a> for EdgeStorageRef<'a> { for_all_iter!(self, edge => EdgeStorageOps::layer_ids_iter(edge, layer_ids)) } - fn layer_ids_par_iter(self, layer_ids: &LayerIds) -> impl ParallelIterator + 'a { - for_all_iter!(self, edge => EdgeStorageOps::layer_ids_par_iter(edge, layer_ids)) - } - fn additions_iter( self, layer_ids: &'a LayerIds, @@ -82,13 +77,6 @@ impl<'a> EdgeStorageOps<'a> for EdgeStorageRef<'a> { for_all_iter!(self, edge => EdgeStorageOps::additions_iter(edge, layer_ids)) } - fn additions_par_iter( - self, - layer_ids: &LayerIds, - ) -> impl ParallelIterator)> + 'a { - for_all_iter!(self, edge => EdgeStorageOps::additions_par_iter(edge, layer_ids)) - } - fn deletions_iter( self, layer_ids: &'a LayerIds, @@ -96,13 +84,6 @@ impl<'a> EdgeStorageOps<'a> for EdgeStorageRef<'a> { for_all_iter!(self, edge => EdgeStorageOps::deletions_iter(edge, layer_ids)) } - fn deletions_par_iter( - self, - layer_ids: &LayerIds, - ) -> impl ParallelIterator)> + 'a { - for_all_iter!(self, edge => EdgeStorageOps::deletions_par_iter(edge, layer_ids)) - } - fn updates_iter( self, layer_ids: &'a LayerIds, @@ -110,13 +91,6 @@ impl<'a> EdgeStorageOps<'a> for EdgeStorageRef<'a> { for_all_iter!(self, edge => EdgeStorageOps::updates_iter(edge, layer_ids)) } - fn updates_par_iter( - self, - layer_ids: &LayerIds, - ) -> impl ParallelIterator, TimeIndexRef<'a>)> + 'a { - for_all_iter!(self, edge => EdgeStorageOps::updates_par_iter(edge, layer_ids)) - } - fn additions(self, layer_id: usize) -> TimeIndexRef<'a> { for_all!(self, edge => EdgeStorageOps::additions(edge, layer_id)) } diff --git a/raphtory-storage/src/graph/edges/edge_storage_ops.rs b/raphtory-storage/src/graph/edges/edge_storage_ops.rs index 3206beb332..58b9f26be2 100644 --- a/raphtory-storage/src/graph/edges/edge_storage_ops.rs +++ b/raphtory-storage/src/graph/edges/edge_storage_ops.rs @@ -13,7 +13,6 @@ use raphtory_core::{ entities::{edges::edge_store::MemEdge, properties::tprop::TProp}, storage::timeindex::{TimeIndex, TimeIndexWindow}, }; -use rayon::prelude::*; use std::ops::Range; #[derive(Clone)] @@ -40,36 +39,36 @@ impl<'a> TimeIndexOps<'a> for TimeIndexRef<'a> { fn active(&self, w: Range) -> bool { match self { TimeIndexRef::Ref(t) => t.active(w), - TimeIndexRef::Range(ref t) => t.active(w), + TimeIndexRef::Range(t) => t.active(w), #[cfg(feature = "storage")] - TimeIndexRef::External(ref t) => t.active(w), + TimeIndexRef::External(t) => t.active(w), } } fn range(&self, w: Range) -> Self { match self { TimeIndexRef::Ref(t) => TimeIndexRef::Range(t.range(w)), - TimeIndexRef::Range(ref t) => TimeIndexRef::Range(t.range(w)), + TimeIndexRef::Range(t) => TimeIndexRef::Range(t.range(w)), #[cfg(feature = "storage")] - TimeIndexRef::External(ref t) => TimeIndexRef::External(t.range(w)), + TimeIndexRef::External(t) => TimeIndexRef::External(t.range(w)), } } fn first(&self) -> Option { match self { TimeIndexRef::Ref(t) => t.first(), - TimeIndexRef::Range(ref t) => t.first(), + TimeIndexRef::Range(t) => t.first(), #[cfg(feature = "storage")] - TimeIndexRef::External(ref t) => t.first(), + TimeIndexRef::External(t) => t.first(), } } fn last(&self) -> Option { match self { TimeIndexRef::Ref(t) => t.last(), - TimeIndexRef::Range(ref t) => t.last(), + TimeIndexRef::Range(t) => t.last(), #[cfg(feature = "storage")] - TimeIndexRef::External(ref t) => t.last(), + TimeIndexRef::External(t) => t.last(), } } @@ -96,7 +95,7 @@ impl<'a> TimeIndexOps<'a> for TimeIndexRef<'a> { TimeIndexRef::Ref(ts) => ts.len(), TimeIndexRef::Range(ts) => ts.len(), #[cfg(feature = "storage")] - TimeIndexRef::External(ref t) => t.len(), + TimeIndexRef::External(t) => t.len(), } } } @@ -128,8 +127,6 @@ pub trait EdgeStorageOps<'a>: Copy + Sized + Send + Sync + 'a { layer_ids: &'a LayerIds, ) -> impl Iterator + Send + Sync + 'a; - fn layer_ids_par_iter(self, layer_ids: &LayerIds) -> impl ParallelIterator + 'a; - fn additions_iter( self, layer_ids: &'a LayerIds, @@ -138,13 +135,6 @@ pub trait EdgeStorageOps<'a>: Copy + Sized + Send + Sync + 'a { .map(move |id| (id, self.additions(id))) } - fn additions_par_iter( - self, - layer_ids: &LayerIds, - ) -> impl ParallelIterator)> + 'a { - self.layer_ids_par_iter(layer_ids) - .map(move |id| (id, self.additions(id))) - } fn deletions_iter( self, layer_ids: &'a LayerIds, @@ -153,14 +143,6 @@ pub trait EdgeStorageOps<'a>: Copy + Sized + Send + Sync + 'a { .map(move |id| (id, self.deletions(id))) } - fn deletions_par_iter( - self, - layer_ids: &LayerIds, - ) -> impl ParallelIterator)> + 'a { - self.layer_ids_par_iter(layer_ids) - .map(move |id| (id, self.deletions(id))) - } - fn updates_iter( self, layer_ids: &'a LayerIds, @@ -169,14 +151,6 @@ pub trait EdgeStorageOps<'a>: Copy + Sized + Send + Sync + 'a { .map(move |id| (id, self.additions(id), self.deletions(id))) } - fn updates_par_iter( - self, - layer_ids: &LayerIds, - ) -> impl ParallelIterator, TimeIndexRef<'a>)> + 'a { - self.layer_ids_par_iter(layer_ids) - .map(move |id| (id, self.additions(id), self.deletions(id))) - } - fn additions(self, layer_id: usize) -> TimeIndexRef<'a>; fn deletions(self, layer_id: usize) -> TimeIndexRef<'a>; @@ -192,15 +166,6 @@ pub trait EdgeStorageOps<'a>: Copy + Sized + Send + Sync + 'a { .map(move |id| (id, self.temporal_prop_layer(id, prop_id))) } - fn temporal_prop_par_iter( - self, - layer_ids: &LayerIds, - prop_id: usize, - ) -> impl ParallelIterator)> + 'a { - self.layer_ids_par_iter(layer_ids) - .map(move |id| (id, self.temporal_prop_layer(id, prop_id))) - } - fn constant_prop_layer(self, layer_id: usize, prop_id: usize) -> Option; fn constant_prop_iter( @@ -266,23 +231,6 @@ impl<'a> EdgeStorageOps<'a> for MemEdge<'a> { } } - fn layer_ids_par_iter(self, layer_ids: &LayerIds) -> impl ParallelIterator + 'a { - match layer_ids { - LayerIds::None => LayerVariants::None(rayon::iter::empty()), - LayerIds::All => LayerVariants::All( - (0..self.internal_num_layers()) - .into_par_iter() - .filter(move |&l| self.has_layer_inner(l)), - ), - LayerIds::One(id) => { - LayerVariants::One(self.has_layer_inner(*id).then_some(*id).into_par_iter()) - } - LayerIds::Multiple(ids) => { - LayerVariants::Multiple(ids.par_iter().filter(move |&id| self.has_layer_inner(id))) - } - } - } - fn additions(self, layer_id: usize) -> TimeIndexRef<'a> { TimeIndexRef::Ref(self.get_additions(layer_id).unwrap_or(&TimeIndex::Empty)) } diff --git a/raphtory-storage/src/graph/graph.rs b/raphtory-storage/src/graph/graph.rs index da0d511f7e..5af606a01c 100644 --- a/raphtory-storage/src/graph/graph.rs +++ b/raphtory-storage/src/graph/graph.rs @@ -247,7 +247,7 @@ impl GraphStorage { LayerIds::None => LayerVariants::None(iter::empty()), LayerIds::All => LayerVariants::All(0..self.unfiltered_num_layers()), LayerIds::One(id) => LayerVariants::One(iter::once(*id)), - LayerIds::Multiple(ids) => LayerVariants::Multiple(ids.into_iter()), + LayerIds::Multiple(ids) => LayerVariants::Multiple(ids.clone().into_iter()), } } // diff --git a/raphtory-storage/src/graph/nodes/node_additions.rs b/raphtory-storage/src/graph/nodes/node_additions.rs index 3c7901af9f..fdb3ad585f 100644 --- a/raphtory-storage/src/graph/nodes/node_additions.rs +++ b/raphtory-storage/src/graph/nodes/node_additions.rs @@ -4,7 +4,7 @@ use raphtory_api::core::{ storage::timeindex::{TimeIndexEntry, TimeIndexOps}, }; use raphtory_core::{ - entities::nodes::node_store::NodeTimestamps, + entities::nodes::node_store::PropTimestamps, storage::timeindex::{TimeIndexWindow, TimeIndexWindowVariants}, }; use std::{iter, ops::Range}; @@ -14,8 +14,8 @@ use {itertools::Itertools, pometry_storage::timestamps::LayerAdditions}; #[derive(Clone, Debug)] pub enum NodeAdditions<'a> { - Mem(&'a NodeTimestamps), - Range(TimeIndexWindow<'a, TimeIndexEntry, NodeTimestamps>), + Mem(&'a PropTimestamps), + Range(TimeIndexWindow<'a, TimeIndexEntry, PropTimestamps>), #[cfg(feature = "storage")] Col(LayerAdditions<'a>), } diff --git a/raphtory-storage/src/graph/variants/storage_variants2.rs b/raphtory-storage/src/graph/variants/storage_variants2.rs index 8b63fbd3d4..a736cb0426 100644 --- a/raphtory-storage/src/graph/variants/storage_variants2.rs +++ b/raphtory-storage/src/graph/variants/storage_variants2.rs @@ -76,15 +76,18 @@ impl<'a, Mem: TPropOps<'a> + 'a, #[cfg(feature = "storage")] Disk: TPropOps<'a> for_all!(self, props => props.last_before(t)) } - fn iter(self) -> impl DoubleEndedIterator + Send + Sync + 'a { - for_all_iter!(self, props => props.iter()) + fn iter_inner( + self, + range: Option>, + ) -> impl Iterator + Send + Sync + 'a { + for_all_iter!(self, props => props.iter_inner(range)) } - fn iter_window( + fn iter_inner_rev( self, - r: Range, - ) -> impl DoubleEndedIterator + Send + Sync + 'a { - for_all_iter!(self, props => props.iter_window(r)) + range: Option>, + ) -> impl Iterator + Send + Sync + 'a { + for_all_iter!(self, props => props.iter_inner_rev(range)) } fn at(&self, ti: &TimeIndexEntry) -> Option { diff --git a/raphtory-storage/src/graph/variants/storage_variants3.rs b/raphtory-storage/src/graph/variants/storage_variants3.rs index 7b4e4242f8..2ac2d41e20 100644 --- a/raphtory-storage/src/graph/variants/storage_variants3.rs +++ b/raphtory-storage/src/graph/variants/storage_variants3.rs @@ -73,15 +73,18 @@ impl< for_all!(self, props => props.last_before(t)) } - fn iter(self) -> impl DoubleEndedIterator + Send + Sync + 'a { - for_all_iter!(self, props => props.iter()) + fn iter_inner( + self, + range: Option>, + ) -> impl Iterator + Send + Sync + 'a { + for_all_iter!(self, props => props.iter_inner(range)) } - fn iter_window( + fn iter_inner_rev( self, - r: Range, - ) -> impl DoubleEndedIterator + Send + Sync + 'a { - for_all_iter!(self, props => props.iter_window(r)) + range: Option>, + ) -> impl Iterator + Send + Sync + 'a { + for_all_iter!(self, props => props.iter_inner_rev(range)) } fn at(&self, ti: &TimeIndexEntry) -> Option { diff --git a/raphtory/src/db/api/storage/graph/storage_ops/time_props.rs b/raphtory/src/db/api/storage/graph/storage_ops/time_props.rs index bd8f78df63..118006149b 100644 --- a/raphtory/src/db/api/storage/graph/storage_ops/time_props.rs +++ b/raphtory/src/db/api/storage/graph/storage_ops/time_props.rs @@ -35,7 +35,9 @@ impl TemporalPropertyViewOps for GraphStorage { .get_temporal_prop(id) .into_iter() .flat_map(|prop| { - GenLockedIter::from(prop, |prop| prop.deref().iter().rev().into_dyn_boxed()) + GenLockedIter::from(prop, |prop| { + prop.deref().iter_inner_rev(None).into_dyn_boxed() + }) }) .into_dyn_boxed() } diff --git a/raphtory/src/db/api/storage/graph/storage_ops/time_semantics.rs b/raphtory/src/db/api/storage/graph/storage_ops/time_semantics.rs index 9584a2ae5f..75f566a26e 100644 --- a/raphtory/src/db/api/storage/graph/storage_ops/time_semantics.rs +++ b/raphtory/src/db/api/storage/graph/storage_ops/time_semantics.rs @@ -11,8 +11,9 @@ use raphtory_api::{ entities::{properties::tprop::TPropOps, EID, VID}, storage::timeindex::{AsTime, TimeIndexEntry}, }, - iter::{BoxedLDIter, IntoDynDBoxed}, + iter::{BoxedLDIter, BoxedLIter, IntoDynBoxed, IntoDynDBoxed}, }; +use raphtory_core::utils::iter::GenLockedIter; use raphtory_storage::{ core_ops::CoreGraphOps, graph::{edges::edge_storage_ops::EdgeStorageOps, nodes::node_storage_ops::NodeStorageOps}, @@ -75,14 +76,14 @@ impl GraphTimeSemanticsOps for GraphStorage { prop_id < self.graph_meta().temporal_prop_meta().len() } - fn temporal_prop_iter(&self, prop_id: usize) -> BoxedLDIter<(TimeIndexEntry, Prop)> { + fn temporal_prop_iter(&self, prop_id: usize) -> BoxedLIter<(TimeIndexEntry, Prop)> { self.graph_meta() .get_temporal_prop(prop_id) .into_iter() .flat_map(move |prop| { - GenLockedDIter::from(prop, |prop| prop.deref().iter().into_dyn_dboxed()) + GenLockedIter::from(prop, |prop| prop.deref().iter().into_dyn_boxed()) }) - .into_dyn_dboxed() + .into_dyn_boxed() } fn has_temporal_prop_window(&self, prop_id: usize, w: Range) -> bool { @@ -97,18 +98,37 @@ impl GraphTimeSemanticsOps for GraphStorage { prop_id: usize, start: i64, end: i64, - ) -> BoxedLDIter<(TimeIndexEntry, Prop)> { + ) -> BoxedLIter<(TimeIndexEntry, Prop)> { self.graph_meta() .get_temporal_prop(prop_id) .into_iter() .flat_map(move |prop| { - GenLockedDIter::from(prop, |prop| { + GenLockedIter::from(prop, |prop| { prop.deref() .iter_window(TimeIndexEntry::range(start..end)) - .into_dyn_dboxed() + .into_dyn_boxed() }) }) - .into_dyn_dboxed() + .into_dyn_boxed() + } + + fn temporal_prop_iter_window_rev( + &self, + prop_id: usize, + start: i64, + end: i64, + ) -> BoxedLIter<(TimeIndexEntry, Prop)> { + self.graph_meta() + .get_temporal_prop(prop_id) + .into_iter() + .flat_map(move |prop| { + GenLockedIter::from(prop, |prop| { + prop.deref() + .iter_window_rev(TimeIndexEntry::range(start..end)) + .into_dyn_boxed() + }) + }) + .into_dyn_boxed() } fn temporal_prop_last_at( diff --git a/raphtory/src/db/api/view/internal/time_semantics/base_time_semantics.rs b/raphtory/src/db/api/view/internal/time_semantics/base_time_semantics.rs index 6fd7a9dcb0..08f9a3b22a 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/base_time_semantics.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/base_time_semantics.rs @@ -147,10 +147,20 @@ impl NodeTimeSemanticsOps for BaseTimeSemantics { node: NodeStorageRef<'graph>, view: G, prop_id: usize, - ) -> impl DoubleEndedIterator + Send + Sync + 'graph { + ) -> impl Iterator + Send + Sync + 'graph { for_all_iter!(self, semantics => semantics.node_tprop_iter(node, view, prop_id)) } + #[inline] + fn node_tprop_iter_rev<'graph, G: GraphView + 'graph>( + &self, + node: NodeStorageRef<'graph>, + view: G, + prop_id: usize, + ) -> impl Iterator + Send + Sync + 'graph { + for_all_iter!(self, semantics => semantics.node_tprop_iter_rev(node, view, prop_id)) + } + #[inline] fn node_tprop_iter_window<'graph, G: GraphView + 'graph>( &self, @@ -158,10 +168,21 @@ impl NodeTimeSemanticsOps for BaseTimeSemantics { view: G, prop_id: usize, w: Range, - ) -> impl DoubleEndedIterator + Send + Sync + 'graph { + ) -> impl Iterator + Send + Sync + 'graph { for_all_iter!(self, semantics => semantics.node_tprop_iter_window(node, view, prop_id, w)) } + #[inline] + fn node_tprop_iter_window_rev<'graph, G: GraphView + 'graph>( + &self, + node: NodeStorageRef<'graph>, + view: G, + prop_id: usize, + w: Range, + ) -> impl Iterator + Send + Sync + 'graph { + for_all_iter!(self, semantics => semantics.node_tprop_iter_window_rev(node, view, prop_id, w)) + } + #[inline] fn node_tprop_last_at<'graph, G: GraphView + 'graph>( &self, diff --git a/raphtory/src/db/api/view/internal/time_semantics/event_semantics.rs b/raphtory/src/db/api/view/internal/time_semantics/event_semantics.rs index 242e395900..9a9fa238db 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/event_semantics.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/event_semantics.rs @@ -129,20 +129,40 @@ impl NodeTimeSemanticsOps for EventSemantics { node: NodeStorageRef<'graph>, _view: G, prop_id: usize, - ) -> impl DoubleEndedIterator + Send + Sync + 'graph { + ) -> impl Iterator + Send + Sync + 'graph { node.tprop(prop_id).iter() } + fn node_tprop_iter_rev<'graph, G: GraphView + 'graph>( + &self, + node: NodeStorageRef<'graph>, + _view: G, + prop_id: usize, + ) -> impl Iterator + Send + Sync + 'graph { + node.tprop(prop_id).iter_rev() + } + fn node_tprop_iter_window<'graph, G: GraphView + 'graph>( &self, node: NodeStorageRef<'graph>, _view: G, prop_id: usize, w: Range, - ) -> impl DoubleEndedIterator + Send + Sync + 'graph { + ) -> impl Iterator + Send + Sync + 'graph { node.tprop(prop_id).iter_window(TimeIndexEntry::range(w)) } + fn node_tprop_iter_window_rev<'graph, G: GraphView + 'graph>( + &self, + node: NodeStorageRef<'graph>, + _view: G, + prop_id: usize, + w: Range, + ) -> impl Iterator + Send + Sync + 'graph { + node.tprop(prop_id) + .iter_window_rev(TimeIndexEntry::range(w)) + } + fn node_tprop_last_at<'graph, G: GraphView + 'graph>( &self, node: NodeStorageRef<'graph>, @@ -654,7 +674,10 @@ impl EdgeTimeSemanticsOps for EventSemantics { prop_id: usize, ) -> impl Iterator + Send + Sync + 'graph { e.filtered_temporal_prop_iter(prop_id, view, layer_ids) - .map(|(layer_id, prop)| prop.iter().rev().map(move |(t, v)| (t, layer_id, v))) + .map(|(layer_id, prop)| { + prop.iter_inner_rev(None) + .map(move |(t, v)| (t, layer_id, v)) + }) .kmerge_by(|(t1, _, _), (t2, _, _)| t1 >= t2) } @@ -684,8 +707,7 @@ impl EdgeTimeSemanticsOps for EventSemantics { ) -> impl Iterator + Send + Sync + 'graph { e.filtered_temporal_prop_iter(prop_id, view, layer_ids) .map(move |(layer_id, prop)| { - prop.iter_window(TimeIndexEntry::range(w.clone())) - .rev() + prop.iter_inner_rev(Some(TimeIndexEntry::range(w.clone()))) .map(move |(t, v)| (t, layer_id, v)) }) .kmerge_by(|(t1, _, _), (t2, _, _)| t1 >= t2) diff --git a/raphtory/src/db/api/view/internal/time_semantics/filtered_edge.rs b/raphtory/src/db/api/view/internal/time_semantics/filtered_edge.rs index 11768d92ee..9c7fa38084 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/filtered_edge.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/filtered_edge.rs @@ -102,37 +102,59 @@ pub struct FilteredEdgeTProp { impl<'graph, G: GraphViewOps<'graph>, P: TPropOps<'graph>> TPropOps<'graph> for FilteredEdgeTProp { - fn iter( + // fn iter( + // self, + // ) -> impl DoubleEndedIterator + Send + Sync + 'graph { + // let view = self.view.clone(); + // let eid = self.eid; + // self.props + // .iter() + // .filter(move |(t, _)| view.filter_edge_history(eid, *t, view.layer_ids())) + // } + + // fn iter_window( + // self, + // r: Range, + // ) -> impl DoubleEndedIterator + Send + Sync + 'graph { + // let view = self.view.clone(); + // let eid = self.eid; + // self.props + // .iter_window(r) + // .filter(move |(t, _)| view.filter_edge_history(eid, *t, view.layer_ids())) + // } + + fn at(&self, ti: &TimeIndexEntry) -> Option { + if self + .view + .filter_edge_history(self.eid, *ti, self.view.layer_ids()) + { + self.props.at(ti) + } else { + None + } + } + + fn iter_inner( self, - ) -> impl DoubleEndedIterator + Send + Sync + 'graph { + range: Option>, + ) -> impl Iterator + Send + Sync + 'graph { let view = self.view.clone(); let eid = self.eid; self.props - .iter() + .iter_inner(range) .filter(move |(t, _)| view.filter_edge_history(eid, *t, view.layer_ids())) } - fn iter_window( + fn iter_inner_rev( self, - r: Range, - ) -> impl DoubleEndedIterator + Send + Sync + 'graph { + range: Option>, + ) -> impl Iterator + Send + Sync + 'graph { let view = self.view.clone(); let eid = self.eid; self.props - .iter_window(r) + .iter_inner_rev(range) .filter(move |(t, _)| view.filter_edge_history(eid, *t, view.layer_ids())) } - - fn at(&self, ti: &TimeIndexEntry) -> Option { - if self - .view - .filter_edge_history(self.eid, *ti, self.view.layer_ids()) - { - self.props.at(ti) - } else { - None - } - } } pub trait FilteredEdgeStorageOps<'a>: EdgeStorageOps<'a> { diff --git a/raphtory/src/db/api/view/internal/time_semantics/mod.rs b/raphtory/src/db/api/view/internal/time_semantics/mod.rs index e3e8883a02..c894982534 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/mod.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/mod.rs @@ -18,6 +18,7 @@ use enum_dispatch::enum_dispatch; use raphtory_api::{ core::{entities::properties::prop::Prop, storage::timeindex::TimeIndexEntry}, inherit::Base, + iter::BoxedLIter, }; use std::ops::Range; @@ -62,7 +63,7 @@ pub trait GraphTimeSemanticsOps { /// A vector of tuples representing the temporal values of the property /// that fall within the specified time window, where the first element of each tuple is the timestamp /// and the second element is the property value. - fn temporal_prop_iter(&self, prop_id: usize) -> BoxedLDIter<(TimeIndexEntry, Prop)>; + fn temporal_prop_iter(&self, prop_id: usize) -> BoxedLIter<(TimeIndexEntry, Prop)>; /// Check if graph has temporal property with the given id in the window /// /// # Arguments @@ -90,7 +91,28 @@ pub trait GraphTimeSemanticsOps { prop_id: usize, start: i64, end: i64, - ) -> BoxedLDIter<(TimeIndexEntry, Prop)>; + ) -> BoxedLIter<(TimeIndexEntry, Prop)>; + + /// Returns all temporal values of the graph property with the given name + /// that fall within the specified time window in reverse order. + /// + /// # Arguments + /// + /// * `name` - The name of the property to retrieve. + /// * `start` - The start time of the window to consider. + /// * `end` - The end time of the window to consider. + /// + /// Returns: + /// + /// Iterator of tuples representing the temporal values of the property in reverse order + /// that fall within the specified time window, where the first element of each tuple is the timestamp + /// and the second element is the property value. + fn temporal_prop_iter_window_rev( + &self, + prop_id: usize, + start: i64, + end: i64, + ) -> BoxedLIter<(TimeIndexEntry, Prop)>; /// Returns the value and update time for the temporal graph property at or before a given timestamp fn temporal_prop_last_at( @@ -168,7 +190,7 @@ impl GraphTimeSemanticsOps for G { } #[inline] - fn temporal_prop_iter(&self, prop_id: usize) -> BoxedLDIter<(TimeIndexEntry, Prop)> { + fn temporal_prop_iter(&self, prop_id: usize) -> BoxedLIter<(TimeIndexEntry, Prop)> { self.graph().temporal_prop_iter(prop_id) } @@ -183,10 +205,21 @@ impl GraphTimeSemanticsOps for G { prop_id: usize, start: i64, end: i64, - ) -> BoxedLDIter<(TimeIndexEntry, Prop)> { + ) -> BoxedLIter<(TimeIndexEntry, Prop)> { self.graph().temporal_prop_iter_window(prop_id, start, end) } + #[inline] + fn temporal_prop_iter_window_rev( + &self, + prop_id: usize, + start: i64, + end: i64, + ) -> BoxedLIter<(TimeIndexEntry, Prop)> { + self.graph() + .temporal_prop_iter_window_rev(prop_id, start, end) + } + #[inline] fn temporal_prop_last_at( &self, diff --git a/raphtory/src/db/api/view/internal/time_semantics/persistent_semantics.rs b/raphtory/src/db/api/view/internal/time_semantics/persistent_semantics.rs index 7acd5879d1..0e9fa28789 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/persistent_semantics.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/persistent_semantics.rs @@ -125,9 +125,8 @@ fn interior_window<'a>( w: Range, deletions: &impl TimeIndexOps<'a, IndexType = TimeIndexEntry>, ) -> Range { - let start = deletions - .range_t(w.start..w.start.saturating_add(1)) - .last() + let last: Option = deletions.range_t(w.start..w.start.saturating_add(1)).last(); + let start = last .map(|t| t.next()) .unwrap_or(TimeIndexEntry::start(w.start)); start..TimeIndexEntry::start(w.end) @@ -318,17 +317,26 @@ impl NodeTimeSemanticsOps for PersistentSemantics { node: NodeStorageRef<'graph>, _view: G, prop_id: usize, - ) -> impl DoubleEndedIterator + Send + Sync + 'graph { + ) -> impl Iterator + Send + Sync + 'graph { node.tprop(prop_id).iter() } + fn node_tprop_iter_rev<'graph, G: GraphView + 'graph>( + &self, + node: NodeStorageRef<'graph>, + _view: G, + prop_id: usize, + ) -> impl Iterator + Send + Sync + 'graph { + node.tprop(prop_id).iter_rev() + } + fn node_tprop_iter_window<'graph, G: GraphViewOps<'graph>>( &self, node: NodeStorageRef<'graph>, _view: G, prop_id: usize, w: Range, - ) -> impl DoubleEndedIterator + Send + Sync + 'graph { + ) -> impl Iterator + Send + Sync + 'graph { let prop = node.tprop(prop_id); let first = if prop.active_t(w.start..w.start.saturating_add(1)) { None @@ -341,6 +349,23 @@ impl NodeTimeSemanticsOps for PersistentSemantics { .chain(prop.iter_window(TimeIndexEntry::range(w))) } + fn node_tprop_iter_window_rev<'graph, G: GraphView + 'graph>( + &self, + node: NodeStorageRef<'graph>, + _view: G, + prop_id: usize, + w: Range, + ) -> impl Iterator + Send + Sync + 'graph { + let prop = node.tprop(prop_id); + let first = if prop.active_t(w.start..w.start.saturating_add(1)) { + None + } else { + prop.last_before(TimeIndexEntry::start(w.start)) + .map(|(t, v)| (t.max(TimeIndexEntry::start(w.start)), v)) + }; + prop.iter_window_rev(TimeIndexEntry::range(w)).chain(first) + } + fn node_tprop_last_at<'graph, G: GraphViewOps<'graph>>( &self, node: NodeStorageRef<'graph>, @@ -862,8 +887,8 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { .last() .unwrap_or(TimeIndexEntry::MIN); e.filtered_temporal_prop_layer(layer_id, prop_id, &view) - .iter_window(search_start..t.next()) - .next_back() + .iter_inner_rev(Some(search_start..t.next())) + .next() .map(|(_, v)| v) } @@ -936,8 +961,8 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { .map(|t| t.next()) .unwrap_or(TimeIndexEntry::MIN); e.filtered_temporal_prop_layer(layer, prop_id, &view) - .iter_window(start..t.next()) - .next_back() + .iter_inner_rev(Some(start..t.next())) + .next() }) .max_by(|(t1, _), (t2, _)| t1.cmp(t2)) .map(|(_, v)| v) @@ -1001,14 +1026,10 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { let deletions = e.filtered_deletions(layer, &view); let first_prop = persisted_prop_value_at(w.start, props.clone(), &deletions) .map(|v| (TimeIndexEntry::start(w.start), layer, v)); - first_prop - .into_iter() - .chain( - props - .iter_window(interior_window(w.clone(), &deletions)) - .map(move |(t, v)| (t, layer, v)), - ) - .rev() + props + .iter_inner_rev(Some(interior_window(w.clone(), &deletions))) + .map(move |(t, v)| (t, layer, v)) + .chain(first_prop.into_iter()) }) .kmerge_by(|(t1, _, _), (t2, _, _)| t1 >= t2) } diff --git a/raphtory/src/db/api/view/internal/time_semantics/time_semantics.rs b/raphtory/src/db/api/view/internal/time_semantics/time_semantics.rs index b36adae1db..1c9c9752ba 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/time_semantics.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/time_semantics.rs @@ -135,7 +135,16 @@ impl NodeTimeSemanticsOps for TimeSemantics { node: NodeStorageRef<'graph>, view: G, prop_id: usize, - ) -> impl DoubleEndedIterator + Send + Sync + 'graph { + ) -> impl Iterator + Send + Sync + 'graph { + for_all_iter!(self, semantics => semantics.node_tprop_iter(node, view, prop_id)) + } + + fn node_tprop_iter_rev<'graph, G: GraphView + 'graph>( + &self, + node: NodeStorageRef<'graph>, + view: G, + prop_id: usize, + ) -> impl Iterator + Send + Sync + 'graph { for_all_iter!(self, semantics => semantics.node_tprop_iter(node, view, prop_id)) } @@ -145,10 +154,20 @@ impl NodeTimeSemanticsOps for TimeSemantics { view: G, prop_id: usize, w: Range, - ) -> impl DoubleEndedIterator + Send + Sync + 'graph { + ) -> impl Iterator + Send + Sync + 'graph { for_all_iter!(self, semantics => semantics.node_tprop_iter_window(node, view, prop_id, w)) } + fn node_tprop_iter_window_rev<'graph, G: GraphView + 'graph>( + &self, + node: NodeStorageRef<'graph>, + view: G, + prop_id: usize, + w: Range, + ) -> impl Iterator + Send + Sync + 'graph { + for_all_iter!(self, semantics => semantics.node_tprop_iter_window_rev(node, view, prop_id, w)) + } + fn node_tprop_last_at<'graph, G: GraphView + 'graph>( &self, node: NodeStorageRef<'graph>, diff --git a/raphtory/src/db/api/view/internal/time_semantics/time_semantics_ops.rs b/raphtory/src/db/api/view/internal/time_semantics/time_semantics_ops.rs index 04dff3fb1e..ba369fc4c5 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/time_semantics_ops.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/time_semantics_ops.rs @@ -78,7 +78,14 @@ pub trait NodeTimeSemanticsOps { node: NodeStorageRef<'graph>, view: G, prop_id: usize, - ) -> impl DoubleEndedIterator + Send + Sync + 'graph; + ) -> impl Iterator + Send + Sync + 'graph; + + fn node_tprop_iter_rev<'graph, G: GraphView + 'graph>( + &self, + node: NodeStorageRef<'graph>, + view: G, + prop_id: usize, + ) -> impl Iterator + Send + Sync + 'graph; fn node_tprop_iter_window<'graph, G: GraphView + 'graph>( &self, @@ -86,7 +93,15 @@ pub trait NodeTimeSemanticsOps { view: G, prop_id: usize, w: Range, - ) -> impl DoubleEndedIterator + Send + Sync + 'graph; + ) -> impl Iterator + Send + Sync + 'graph; + + fn node_tprop_iter_window_rev<'graph, G: GraphView + 'graph>( + &self, + node: NodeStorageRef<'graph>, + view: G, + prop_id: usize, + w: Range, + ) -> impl Iterator + Send + Sync + 'graph; fn node_tprop_last_at<'graph, G: GraphView + 'graph>( &self, diff --git a/raphtory/src/db/api/view/internal/time_semantics/window_time_semantics.rs b/raphtory/src/db/api/view/internal/time_semantics/window_time_semantics.rs index fb8e84809a..e9222c3990 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/window_time_semantics.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/window_time_semantics.rs @@ -135,11 +135,22 @@ impl NodeTimeSemanticsOps for WindowTimeSemantics { node: NodeStorageRef<'graph>, view: G, prop_id: usize, - ) -> impl DoubleEndedIterator + Send + Sync + 'graph { + ) -> impl Iterator + Send + Sync + 'graph { self.semantics .node_tprop_iter_window(node, view, prop_id, self.window.clone()) } + #[inline] + fn node_tprop_iter_rev<'graph, G: GraphView + 'graph>( + &self, + node: NodeStorageRef<'graph>, + view: G, + prop_id: usize, + ) -> impl Iterator + Send + Sync + 'graph { + self.semantics + .node_tprop_iter_window_rev(node, view, prop_id, self.window.clone()) + } + #[inline] fn node_tprop_iter_window<'graph, G: GraphView + 'graph>( &self, @@ -147,11 +158,23 @@ impl NodeTimeSemanticsOps for WindowTimeSemantics { view: G, prop_id: usize, w: Range, - ) -> impl DoubleEndedIterator + Send + Sync + 'graph { + ) -> impl Iterator + Send + Sync + 'graph { self.semantics .node_tprop_iter_window(node, view, prop_id, w) } + #[inline] + fn node_tprop_iter_window_rev<'graph, G: GraphView + 'graph>( + &self, + node: NodeStorageRef<'graph>, + view: G, + prop_id: usize, + w: Range, + ) -> impl Iterator + Send + Sync + 'graph { + self.semantics + .node_tprop_iter_window_rev(node, view, prop_id, w) + } + #[inline] fn node_tprop_last_at<'graph, G: GraphView + 'graph>( &self, diff --git a/raphtory/src/db/graph/node.rs b/raphtory/src/db/graph/node.rs index f2e8bf9842..ba6f70dce0 100644 --- a/raphtory/src/db/graph/node.rs +++ b/raphtory/src/db/graph/node.rs @@ -260,8 +260,8 @@ impl<'graph, G, GH: GraphViewOps<'graph>> TemporalPropertyViewOps for NodeView<' let semantics = self.graph.node_time_semantics(); let node = self.graph.core_node(self.node); let res = semantics - .node_tprop_iter(node.as_ref(), &self.graph, id) - .next_back() + .node_tprop_iter_rev(node.as_ref(), &self.graph, id) + .next() .map(|(_, v)| v); res } @@ -282,8 +282,7 @@ impl<'graph, G, GH: GraphViewOps<'graph>> TemporalPropertyViewOps for NodeView<' let node = self.graph.core_node(self.node); GenLockedIter::from(node, |node| { semantics - .node_tprop_iter(node.as_ref(), &self.graph, id) - .rev() + .node_tprop_iter_rev(node.as_ref(), &self.graph, id) .into_dyn_boxed() }) .into_dyn_boxed() diff --git a/raphtory/src/db/graph/views/deletion_graph.rs b/raphtory/src/db/graph/views/deletion_graph.rs index 4e39790000..ae582f30be 100644 --- a/raphtory/src/db/graph/views/deletion_graph.rs +++ b/raphtory/src/db/graph/views/deletion_graph.rs @@ -2,7 +2,6 @@ use crate::{ core::{ entities::LayerIds, storage::timeindex::{AsTime, TimeIndex, TimeIndexEntry, TimeIndexOps}, - utils::iter::GenLockedDIter, }, db::{ api::{ @@ -16,9 +15,10 @@ use crate::{ use raphtory_api::{ core::entities::{properties::tprop::TPropOps, EID, VID}, inherit::Base, - iter::{BoxedLDIter, IntoDynDBoxed}, + iter::{BoxedLDIter, BoxedLIter, IntoDynBoxed}, GraphType, }; +use raphtory_core::utils::iter::GenLockedIter; use raphtory_storage::{ graph::{ edges::edge_storage_ops::EdgeStorageOps, graph::GraphStorage, @@ -197,7 +197,7 @@ impl GraphTimeSemanticsOps for PersistentGraph { self.0.has_temporal_prop(prop_id) } - fn temporal_prop_iter(&self, prop_id: usize) -> BoxedLDIter<(TimeIndexEntry, Prop)> { + fn temporal_prop_iter(&self, prop_id: usize) -> BoxedLIter<(TimeIndexEntry, Prop)> { self.0.temporal_prop_iter(prop_id) } @@ -213,20 +213,41 @@ impl GraphTimeSemanticsOps for PersistentGraph { prop_id: usize, start: i64, end: i64, - ) -> BoxedLDIter<(TimeIndexEntry, Prop)> { + ) -> BoxedLIter<(TimeIndexEntry, Prop)> { if let Some(prop) = self.graph_meta().get_temporal_prop(prop_id) { let first = persisted_prop_value_at(start, &*prop, &TimeIndex::Empty) .map(|v| (TimeIndexEntry::start(start), v)); first .into_iter() - .chain(GenLockedDIter::from(prop, |prop| { + .chain(GenLockedIter::from(prop, |prop| { prop.deref() .iter_window(TimeIndexEntry::range(start..end)) - .into_dyn_dboxed() + .into_dyn_boxed() })) - .into_dyn_dboxed() + .into_dyn_boxed() } else { - iter::empty().into_dyn_dboxed() + iter::empty().into_dyn_boxed() + } + } + + fn temporal_prop_iter_window_rev( + &self, + prop_id: usize, + start: i64, + end: i64, + ) -> BoxedLIter<(TimeIndexEntry, Prop)> { + if let Some(prop) = self.graph_meta().get_temporal_prop(prop_id) { + let first = persisted_prop_value_at(start, &*prop, &TimeIndex::Empty) + .map(|v| (TimeIndexEntry::start(start), v)); + let iter = GenLockedIter::from(prop, |prop| { + prop.deref() + .iter_window_rev(TimeIndexEntry::range(start..end)) + .into_dyn_boxed() + }); + + iter.into_iter().chain(first).into_dyn_boxed() + } else { + iter::empty().into_dyn_boxed() } } diff --git a/raphtory/src/db/graph/views/window_graph.rs b/raphtory/src/db/graph/views/window_graph.rs index f9be19431d..712f8f8b07 100644 --- a/raphtory/src/db/graph/views/window_graph.rs +++ b/raphtory/src/db/graph/views/window_graph.rs @@ -352,8 +352,7 @@ impl<'graph, G: GraphViewOps<'graph>> TemporalPropertyViewOps for WindowedGraph< fn temporal_iter_rev(&self, id: usize) -> BoxedLIter<(TimeIndexEntry, Prop)> { self.graph - .temporal_prop_iter_window(id, self.start_bound(), self.end_bound()) - .rev() + .temporal_prop_iter_window_rev(id, self.start_bound(), self.end_bound()) .into_dyn_boxed() } @@ -444,7 +443,7 @@ impl<'graph, G: GraphViewOps<'graph>> GraphTimeSemanticsOps for WindowedGraph .has_temporal_prop_window(prop_id, self.start_bound()..self.end_bound()) } - fn temporal_prop_iter(&self, prop_id: usize) -> BoxedLDIter<(TimeIndexEntry, Prop)> { + fn temporal_prop_iter(&self, prop_id: usize) -> BoxedLIter<(TimeIndexEntry, Prop)> { if self.window_is_empty() { return iter::empty().into_dyn_dboxed(); } @@ -461,10 +460,20 @@ impl<'graph, G: GraphViewOps<'graph>> GraphTimeSemanticsOps for WindowedGraph prop_id: usize, start: i64, end: i64, - ) -> BoxedLDIter<(TimeIndexEntry, Prop)> { + ) -> BoxedLIter<(TimeIndexEntry, Prop)> { self.graph.temporal_prop_iter_window(prop_id, start, end) } + fn temporal_prop_iter_window_rev( + &self, + prop_id: usize, + start: i64, + end: i64, + ) -> BoxedLIter<(TimeIndexEntry, Prop)> { + self.graph + .temporal_prop_iter_window_rev(prop_id, start, end) + } + fn temporal_prop_last_at( &self, prop_id: usize, diff --git a/raphtory/src/errors.rs b/raphtory/src/errors.rs index 9c79d900b9..2d01367df9 100644 --- a/raphtory/src/errors.rs +++ b/raphtory/src/errors.rs @@ -98,6 +98,8 @@ pub enum LoadError { NodeIdTypeError { existing: GidType, new: GidType }, #[error("Fatal load error, graph may be in a dirty state.")] FatalError, + #[error("Invalid data type {0:?}")] + ArrowDataType(arrow_schema::DataType), } #[cfg(feature = "proto")] diff --git a/raphtory/src/graphgen/mod.rs b/raphtory/src/graphgen/mod.rs index c7021c5e2b..d462570689 100644 --- a/raphtory/src/graphgen/mod.rs +++ b/raphtory/src/graphgen/mod.rs @@ -15,7 +15,7 @@ pub(crate) fn next_id<'graph, G: GraphViewOps<'graph>>(g: &G, max_gid: Option { let mut rng = rand::thread_rng(); loop { - let new_id = GID::Str(rng.gen::().to_string()); + let new_id = GID::Str(rng.r#gen::().to_string()); if g.node(&new_id).is_none() { break new_id; } diff --git a/raphtory/src/io/arrow/mod.rs b/raphtory/src/io/arrow/mod.rs index 6220d2180f..bac6b5daeb 100644 --- a/raphtory/src/io/arrow/mod.rs +++ b/raphtory/src/io/arrow/mod.rs @@ -1,8 +1,8 @@ pub mod dataframe; pub mod df_loaders; mod layer_col; -mod node_col; -mod prop_handler; +pub mod node_col; +pub mod prop_handler; #[cfg(test)] mod test { diff --git a/raphtory/src/io/arrow/node_col.rs b/raphtory/src/io/arrow/node_col.rs index 4e01a888b3..512a568de1 100644 --- a/raphtory/src/io/arrow/node_col.rs +++ b/raphtory/src/io/arrow/node_col.rs @@ -1,4 +1,5 @@ use crate::{errors::LoadError, io::arrow::dataframe::DFChunk, prelude::AdditionOps}; +use arrow_array::{Array as ArrowArray, Int32Array, Int64Array}; use polars_arrow::{ array::{Array, PrimitiveArray, StaticArray, Utf8Array}, datatypes::ArrowDataType, @@ -7,13 +8,17 @@ use polars_arrow::{ use raphtory_api::core::entities::{GidRef, GidType}; use rayon::prelude::{IndexedParallelIterator, *}; -trait NodeColOps: Array + Send + Sync { +trait NodeColOps: Send + Sync { fn has_missing_values(&self) -> bool { self.null_count() != 0 } fn get(&self, i: usize) -> Option; fn dtype(&self) -> GidType; + + fn null_count(&self) -> usize; + + fn len(&self) -> usize; } impl NodeColOps for PrimitiveArray { @@ -24,6 +29,13 @@ impl NodeColOps for PrimitiveArray { fn dtype(&self) -> GidType { GidType::U64 } + + fn null_count(&self) -> usize { + Array::null_count(self) + } + fn len(&self) -> usize { + Array::len(self) + } } impl NodeColOps for PrimitiveArray { @@ -34,6 +46,13 @@ impl NodeColOps for PrimitiveArray { fn dtype(&self) -> GidType { GidType::U64 } + + fn null_count(&self) -> usize { + Array::null_count(self) + } + fn len(&self) -> usize { + Array::len(self) + } } impl NodeColOps for PrimitiveArray { @@ -44,6 +63,12 @@ impl NodeColOps for PrimitiveArray { fn dtype(&self) -> GidType { GidType::U64 } + fn null_count(&self) -> usize { + Array::null_count(self) + } + fn len(&self) -> usize { + Array::len(self) + } } impl NodeColOps for PrimitiveArray { @@ -54,6 +79,12 @@ impl NodeColOps for PrimitiveArray { fn dtype(&self) -> GidType { GidType::U64 } + fn null_count(&self) -> usize { + Array::null_count(self) + } + fn len(&self) -> usize { + Array::len(self) + } } impl NodeColOps for Utf8Array { @@ -75,6 +106,151 @@ impl NodeColOps for Utf8Array { fn dtype(&self) -> GidType { GidType::Str } + + fn null_count(&self) -> usize { + Array::null_count(self) + } + + fn len(&self) -> usize { + Array::len(self) + } +} + +impl NodeColOps for Int32Array { + fn get(&self, i: usize) -> Option { + self.values().get(i).map(|v| GidRef::U64(*v as u64)) + } + + fn dtype(&self) -> GidType { + GidType::U64 + } + fn null_count(&self) -> usize { + ArrowArray::null_count(self) + } + fn len(&self) -> usize { + ArrowArray::len(self) + } +} + +impl NodeColOps for Int64Array { + fn get(&self, i: usize) -> Option { + self.values().get(i).map(|v| GidRef::U64(*v as u64)) + } + + fn dtype(&self) -> GidType { + GidType::U64 + } + fn null_count(&self) -> usize { + ArrowArray::null_count(self) + } + fn len(&self) -> usize { + ArrowArray::len(self) + } +} + +impl NodeColOps for arrow_array::StringArray { + fn get(&self, i: usize) -> Option { + if i >= ArrowArray::len(self) { + None + } else { + // safety: bounds checked above + unsafe { + let value = self.value_unchecked(i); + Some(GidRef::Str(value)) + } + } + } + + fn dtype(&self) -> GidType { + GidType::Str + } + fn null_count(&self) -> usize { + ArrowArray::null_count(self) + } + fn len(&self) -> usize { + ArrowArray::len(self) + } +} + +impl NodeColOps for arrow_array::LargeStringArray { + fn get(&self, i: usize) -> Option { + if i >= ArrowArray::len(self) { + None + } else { + // safety: bounds checked above + unsafe { + let value = self.value_unchecked(i); + Some(GidRef::Str(value)) + } + } + } + + fn dtype(&self) -> GidType { + GidType::Str + } + fn null_count(&self) -> usize { + ArrowArray::null_count(self) + } + + fn len(&self) -> usize { + ArrowArray::len(self) + } +} + +impl NodeColOps for arrow_array::StringViewArray { + fn get(&self, i: usize) -> Option { + if i >= ArrowArray::len(self) { + None + } else { + // safety: bounds checked above + unsafe { + let value = self.value_unchecked(i); + Some(GidRef::Str(value)) + } + } + } + + fn dtype(&self) -> GidType { + GidType::Str + } + fn null_count(&self) -> usize { + ArrowArray::null_count(self) + } + fn len(&self) -> usize { + ArrowArray::len(self) + } +} + +impl NodeColOps for arrow_array::UInt32Array { + fn get(&self, i: usize) -> Option { + self.values().get(i).map(|v| GidRef::U64(*v as u64)) + } + + fn dtype(&self) -> GidType { + GidType::U64 + } + fn null_count(&self) -> usize { + ArrowArray::null_count(self) + } + fn len(&self) -> usize { + ArrowArray::len(self) + } +} + +impl NodeColOps for arrow_array::UInt64Array { + fn get(&self, i: usize) -> Option { + self.values().get(i).map(|v| GidRef::U64(*v)) + } + + fn dtype(&self) -> GidType { + GidType::U64 + } + fn null_count(&self) -> usize { + ArrowArray::null_count(self) + } + fn len(&self) -> usize { + ArrowArray::len(self) + } } pub struct NodeCol(Box); @@ -137,6 +313,72 @@ impl<'a> TryFrom<&'a dyn Array> for NodeCol { } } +impl<'a> TryFrom<&'a dyn arrow_array::Array> for NodeCol { + type Error = LoadError; + + fn try_from(value: &'a dyn arrow_array::Array) -> Result { + match value.data_type() { + arrow_schema::DataType::Int32 => { + let col = value + .as_any() + .downcast_ref::() + .unwrap() + .clone(); + Ok(NodeCol(Box::new(col))) + } + arrow_schema::DataType::Int64 => { + let col = value + .as_any() + .downcast_ref::() + .unwrap() + .clone(); + Ok(NodeCol(Box::new(col))) + } + arrow_schema::DataType::UInt32 => { + let col = value + .as_any() + .downcast_ref::() + .unwrap() + .clone(); + Ok(NodeCol(Box::new(col))) + } + arrow_schema::DataType::UInt64 => { + let col = value + .as_any() + .downcast_ref::() + .unwrap() + .clone(); + Ok(NodeCol(Box::new(col))) + } + arrow_schema::DataType::Utf8 => { + let col = value + .as_any() + .downcast_ref::() + .unwrap() + .clone(); + Ok(NodeCol(Box::new(col))) + } + arrow_schema::DataType::LargeUtf8 => { + let col = value + .as_any() + .downcast_ref::() + .unwrap() + .clone(); + Ok(NodeCol(Box::new(col))) + } + arrow_schema::DataType::Utf8View => { + let col = value + .as_any() + .downcast_ref::() + .unwrap() + .clone(); + Ok(NodeCol(Box::new(col))) + } + dtype => Err(LoadError::ArrowDataType(dtype.clone())), + } + } +} + impl NodeCol { pub fn par_iter(&self) -> impl IndexedParallelIterator>> + '_ { (0..self.0.len()).into_par_iter().map(|i| self.0.get(i)) diff --git a/raphtory/src/io/arrow/prop_handler.rs b/raphtory/src/io/arrow/prop_handler.rs index eafd938441..eea9277ecb 100644 --- a/raphtory/src/io/arrow/prop_handler.rs +++ b/raphtory/src/io/arrow/prop_handler.rs @@ -3,6 +3,7 @@ use crate::{ io::arrow::dataframe::DFChunk, prelude::Prop, }; +use arrow_array::{Array as ArrowArray, ArrowPrimitiveType, GenericStringArray, OffsetSizeTrait}; use bigdecimal::BigDecimal; use chrono::{DateTime, Utc}; use polars_arrow::{ @@ -13,9 +14,10 @@ use polars_arrow::{ bitmap::Bitmap, datatypes::{ArrowDataType as DataType, TimeUnit}, offset::Offset, + types::NativeType, }; use raphtory_api::core::{ - entities::properties::prop::{IntoPropList, PropType}, + entities::properties::prop::{prop_type_from_arrow_dtype, IntoPropList, PropType}, storage::{arc_str::ArcStr, dict_mapper::MaybeNew}, }; use rayon::prelude::*; @@ -73,6 +75,33 @@ pub(crate) fn combine_properties( }) } +pub fn combine_properties_arrow( + props: &[impl AsRef], + indices: &[usize], + df: &[A], + prop_id_resolver: impl Fn(&str, PropType) -> Result, E>, +) -> Result { + let dtypes = indices + .iter() + .map(|idx| prop_type_from_arrow_dtype(df[*idx].data_type())) + .collect::>(); + let cols = indices + .iter() + .map(|idx| lift_property_col_arrow_rs(&df[*idx])) + .collect::>(); + let prop_ids = props + .iter() + .zip(dtypes.into_iter()) + .map(|(name, dtype)| Ok(prop_id_resolver(name.as_ref(), dtype)?.inner())) + .collect::, E>>()?; + + Ok(PropCols { + prop_ids, + cols, + len: df.len(), + }) +} + fn arr_as_prop(arr: Box) -> Prop { match arr.data_type() { DataType::Boolean => { @@ -236,17 +265,31 @@ trait PropCol: Send + Sync { fn get(&self, i: usize) -> Option; } -impl PropCol for A -where - A: StaticArray, - for<'a> A::ValueT<'a>: Into, -{ +impl> PropCol for PrimitiveArray { #[inline] fn get(&self, i: usize) -> Option { StaticArray::get(self, i).map(|v| v.into()) } } +impl PropCol for BooleanArray { + fn get(&self, i: usize) -> Option { + StaticArray::get(self, i).map(Prop::Bool) + } +} + +impl PropCol for Utf8Array { + fn get(&self, i: usize) -> Option { + Utf8Array::get(self, i).map(Prop::str) + } +} + +impl PropCol for Utf8ViewArray { + fn get(&self, i: usize) -> Option { + StaticArray::get(self, i).map(Prop::str) + } +} + struct Wrap(A); impl PropCol for Wrap> { @@ -301,6 +344,49 @@ impl PropCol for DecimalPropCol { } } +impl PropCol for arrow_array::BooleanArray { + fn get(&self, i: usize) -> Option { + if self.is_null(i) || self.len() <= i { + None + } else { + Some(Prop::Bool(self.value(i))) + } + } +} + +impl PropCol for arrow_array::PrimitiveArray +where + T::Native: Into, +{ + fn get(&self, i: usize) -> Option { + if self.is_null(i) || self.len() <= i { + None + } else { + Some(self.value(i).into()) + } + } +} + +impl PropCol for GenericStringArray { + fn get(&self, i: usize) -> Option { + if self.is_null(i) || self.len() <= i { + None + } else { + Some(Prop::str(self.value(i))) + } + } +} + +impl PropCol for arrow_array::StringViewArray { + fn get(&self, i: usize) -> Option { + if self.is_null(i) || self.len() <= i { + None + } else { + Some(Prop::str(self.value(i))) + } + } +} + struct EmptyCol; impl PropCol for EmptyCol { @@ -496,3 +582,87 @@ fn lift_property_col(arr: &dyn Array) -> Box { unsupported => panic!("Data type not supported: {:?}", unsupported), } } + +fn lift_property_col_arrow_rs(arr: &dyn arrow_array::Array) -> Box { + match arr.data_type() { + arrow_schema::DataType::Boolean => { + let arr = arr + .as_any() + .downcast_ref::() + .unwrap(); + Box::new(arr.clone()) + } + arrow_schema::DataType::Int32 => { + let arr = arr + .as_any() + .downcast_ref::() + .unwrap(); + Box::new(arr.clone()) + } + arrow_schema::DataType::Int64 => { + let arr = arr + .as_any() + .downcast_ref::() + .unwrap(); + Box::new(arr.clone()) + } + arrow_schema::DataType::UInt8 => { + let arr = arr + .as_any() + .downcast_ref::() + .unwrap(); + Box::new(arr.clone()) + } + arrow_schema::DataType::UInt16 => { + let arr = arr + .as_any() + .downcast_ref::() + .unwrap(); + Box::new(arr.clone()) + } + arrow_schema::DataType::UInt32 => { + let arr = arr + .as_any() + .downcast_ref::() + .unwrap(); + Box::new(arr.clone()) + } + arrow_schema::DataType::UInt64 => { + let arr = arr + .as_any() + .downcast_ref::() + .unwrap(); + Box::new(arr.clone()) + } + arrow_schema::DataType::Float32 => { + let arr = arr + .as_any() + .downcast_ref::() + .unwrap(); + Box::new(arr.clone()) + } + arrow_schema::DataType::Float64 => { + let arr = arr + .as_any() + .downcast_ref::() + .unwrap(); + Box::new(arr.clone()) + } + arrow_schema::DataType::Utf8 => { + let arr = arr + .as_any() + .downcast_ref::() + .unwrap(); + Box::new(arr.clone()) + } + arrow_schema::DataType::LargeUtf8 => { + let arr = arr + .as_any() + .downcast_ref::() + .unwrap(); + Box::new(arr.clone()) + } + + unsupported => panic!("Data type not supported: {:?}", unsupported), + } +} diff --git a/raphtory/src/io/mod.rs b/raphtory/src/io/mod.rs index 327e9b42c4..1fd56c86e8 100644 --- a/raphtory/src/io/mod.rs +++ b/raphtory/src/io/mod.rs @@ -1,5 +1,5 @@ #[cfg(feature = "arrow")] -pub(crate) mod arrow; +pub mod arrow; pub mod csv_loader; pub mod json_loader; pub mod neo4j_loader; From 846a08725a624ed93802d68e47731409db337f13 Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Mon, 9 Jun 2025 16:02:09 +0100 Subject: [PATCH 002/321] call rev in the right spot for node time semantics --- .../src/db/api/view/internal/time_semantics/time_semantics.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/raphtory/src/db/api/view/internal/time_semantics/time_semantics.rs b/raphtory/src/db/api/view/internal/time_semantics/time_semantics.rs index 1c9c9752ba..559daedff8 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/time_semantics.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/time_semantics.rs @@ -145,7 +145,7 @@ impl NodeTimeSemanticsOps for TimeSemantics { view: G, prop_id: usize, ) -> impl Iterator + Send + Sync + 'graph { - for_all_iter!(self, semantics => semantics.node_tprop_iter(node, view, prop_id)) + for_all_iter!(self, semantics => semantics.node_tprop_iter_rev(node, view, prop_id)) } fn node_tprop_iter_window<'graph, G: GraphView + 'graph>( From 19f8b6889f7da6e7c8b7fd4a723aaa0e06590499 Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Tue, 10 Jun 2025 12:56:30 +0100 Subject: [PATCH 003/321] add db4-graph --- Cargo.toml | 9 +- db4-graph/Cargo.toml | 17 ++ db4-graph/src/lib.rs | 10 + raphtory-storage/src/mutation/addition_ops.rs | 260 ++++-------------- raphtory/src/db/api/mutation/addition_ops.rs | 42 +-- raphtory/src/db/api/mutation/deletion_ops.rs | 21 +- raphtory/src/db/api/mutation/import_ops.rs | 32 ++- raphtory/src/db/api/mutation/mod.rs | 19 +- .../db/api/mutation/property_addition_ops.rs | 18 +- .../graph/storage_ops/time_semantics.rs | 4 +- raphtory/src/db/api/storage/storage.rs | 148 ++++++---- .../api/view/exploded_edge_property_filter.rs | 9 +- raphtory/src/db/api/view/graph.rs | 9 +- .../api/view/internal/time_semantics/mod.rs | 2 +- raphtory/src/db/graph/edge.rs | 25 +- raphtory/src/db/graph/graph.rs | 11 +- raphtory/src/db/graph/node.rs | 23 +- raphtory/src/db/graph/views/deletion_graph.rs | 16 +- .../views/filter/node_type_filtered_graph.rs | 14 +- raphtory/src/db/graph/views/node_subgraph.rs | 8 +- raphtory/src/db/graph/views/valid_graph.rs | 13 +- raphtory/src/db/graph/views/window_graph.rs | 2 +- raphtory/src/io/arrow/df_loaders.rs | 39 +-- raphtory/src/io/arrow/layer_col.rs | 9 +- raphtory/src/lib.rs | 11 +- 25 files changed, 408 insertions(+), 363 deletions(-) create mode 100644 db4-graph/Cargo.toml create mode 100644 db4-graph/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index a467fc8a14..692b0bd588 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,9 +12,10 @@ members = [ "raphtory-core", "raphtory-storage", "db4-common", - "db4-storage", + "db4-storage", + "db4-graph", ] -default-members = ["raphtory", "db4-common", "db4-storage"] +default-members = ["raphtory", "db4-common", "db4-storage", "db4-graph"] resolver = "2" [workspace.package] @@ -189,3 +190,7 @@ arrow-schema = { git = "https://github.com/apache/arrow-rs.git", tag = "53.4.1" arrow-data = { git = "https://github.com/apache/arrow-rs.git", tag = "53.4.1" } arrow-array = { git = "https://github.com/apache/arrow-rs.git", tag = "53.4.1" } arrow-csv = { git = "https://github.com/apache/arrow-rs.git", tag = "53.4.1" } + +[workspace.dependencies.storage] +package = "db4-storage" +path = "db4-storage" \ No newline at end of file diff --git a/db4-graph/Cargo.toml b/db4-graph/Cargo.toml new file mode 100644 index 0000000000..faf15a7079 --- /dev/null +++ b/db4-graph/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "db4-graph" +version.workspace = true +documentation.workspace = true +repository.workspace = true +license.workspace = true +readme.workspace = true +homepage.workspace = true +keywords.workspace = true +authors.workspace = true +rust-version.workspace = true +edition.workspace = true + +[dependencies] +boxcar.workspace = true +storage.workspace = true +raphtory-api.workspace = true diff --git a/db4-graph/src/lib.rs b/db4-graph/src/lib.rs new file mode 100644 index 0000000000..c0d3cb461d --- /dev/null +++ b/db4-graph/src/lib.rs @@ -0,0 +1,10 @@ +use std::sync::Arc; + +use raphtory_api::core::entities::properties::meta::Meta; +use storage::Layer; + +pub struct TemporalGraph { + layers: boxcar::Vec>, + edge_meta: Arc, + node_meta: Arc, +} diff --git a/raphtory-storage/src/mutation/addition_ops.rs b/raphtory-storage/src/mutation/addition_ops.rs index 24df503418..48cc8fa543 100644 --- a/raphtory-storage/src/mutation/addition_ops.rs +++ b/raphtory-storage/src/mutation/addition_ops.rs @@ -20,9 +20,18 @@ use std::sync::atomic::Ordering; pub trait InternalAdditionOps { type Error: From; + type WS<'a>: SessionAdditionOps + where + Self: 'a; fn write_lock(&self) -> Result; fn write_lock_nodes(&self) -> Result; fn write_lock_edges(&self) -> Result; + + fn write_session(&self) -> Result, Self::Error>; +} + +pub trait SessionAdditionOps: Send + Sync { + type Error: From; /// get the sequence id for the next event fn next_event_id(&self) -> Result; fn reserve_event_ids(&self, num_ids: usize) -> Result; @@ -84,33 +93,27 @@ pub trait InternalAdditionOps { ) -> Result<(), Self::Error>; } -impl InternalAdditionOps for TemporalGraph { - type Error = MutationError; - - fn write_lock(&self) -> Result { - Ok(WriteLockedGraph::new(self)) - } - - fn write_lock_nodes(&self) -> Result { - Ok(self.storage.nodes.write_lock()) - } +#[derive(Clone, Copy)] +pub struct TGWriteSession<'a> { + tg: &'a TemporalGraph, +} - fn write_lock_edges(&self) -> Result { - Ok(self.storage.edges.write_lock()) - } +impl<'a> SessionAdditionOps for TGWriteSession<'a> { + type Error = MutationError; /// get the sequence id for the next event fn next_event_id(&self) -> Result { - Ok(self.event_counter.fetch_add(1, Ordering::Relaxed)) + Ok(self.tg.event_counter.fetch_add(1, Ordering::Relaxed)) } fn reserve_event_ids(&self, num_ids: usize) -> Result { - Ok(self.event_counter.fetch_add(num_ids, Ordering::Relaxed)) + Ok(self.tg.event_counter.fetch_add(num_ids, Ordering::Relaxed)) } /// map layer name to id and allocate a new layer if needed fn resolve_layer(&self, layer: Option<&str>) -> Result, Self::Error> { let id = self + .tg .resolve_layer_inner(layer) .map_err(MutationError::from)?; Ok(id) @@ -118,11 +121,11 @@ impl InternalAdditionOps for TemporalGraph { /// map external node id to internal id, allocating a new empty node if needed fn resolve_node(&self, id: NodeRef) -> Result, Self::Error> { - Ok(self.resolve_node_inner(id)?) + Ok(self.tg.resolve_node_inner(id)?) } fn set_node(&self, gid: GidRef, vid: VID) -> Result<(), Self::Error> { - Ok(self.logical_to_physical.set(gid, vid)?) + Ok(self.tg.logical_to_physical.set(gid, vid)?) } /// resolve a node and corresponding type, outer MaybeNew tracks whether the type assignment is new for the node even if both node and type already existed. @@ -132,15 +135,16 @@ impl InternalAdditionOps for TemporalGraph { node_type: &str, ) -> Result, MaybeNew)>, Self::Error> { let vid = self.resolve_node(id)?; - let mut entry = self.storage.get_node_mut(vid.inner()); + let mut entry = self.tg.storage.get_node_mut(vid.inner()); let mut entry_ref = entry.to_mut(); let node_store = entry_ref.node_store_mut(); if node_store.node_type == 0 { - let node_type_id = self.node_meta.get_or_create_node_type_id(node_type); + let node_type_id = self.tg.node_meta.get_or_create_node_type_id(node_type); node_store.update_node_type(node_type_id.inner()); Ok(MaybeNew::New((vid, node_type_id))) } else { let node_type_id = self + .tg .node_meta .get_node_type_id(node_type) .filter(|&node_type| node_type == node_store.node_type) @@ -156,7 +160,10 @@ impl InternalAdditionOps for TemporalGraph { dtype: PropType, is_static: bool, ) -> Result, Self::Error> { - Ok(self.graph_meta.resolve_property(prop, dtype, is_static)?) + Ok(self + .tg + .graph_meta + .resolve_property(prop, dtype, is_static)?) } /// map property key to internal id, allocating new property if needed and checking property type. @@ -167,7 +174,7 @@ impl InternalAdditionOps for TemporalGraph { dtype: PropType, is_static: bool, ) -> Result, Self::Error> { - Ok(self.node_meta.resolve_prop_id(prop, dtype, is_static)?) + Ok(self.tg.node_meta.resolve_prop_id(prop, dtype, is_static)?) } fn resolve_edge_property( @@ -176,7 +183,7 @@ impl InternalAdditionOps for TemporalGraph { dtype: PropType, is_static: bool, ) -> Result, Self::Error> { - Ok(self.edge_meta.resolve_prop_id(prop, dtype, is_static)?) + Ok(self.tg.edge_meta.resolve_prop_id(prop, dtype, is_static)?) } /// add node update @@ -186,13 +193,13 @@ impl InternalAdditionOps for TemporalGraph { v: VID, props: &[(usize, Prop)], ) -> Result<(), Self::Error> { - self.update_time(t); - let mut entry = self.storage.get_node_mut(v); + self.tg.update_time(t); + let mut entry = self.tg.storage.get_node_mut(v); let mut node = entry.to_mut(); let prop_i = node .t_props_log_mut() .push(props.iter().map(|(prop_id, prop)| { - let prop = self.process_prop_value(prop); + let prop = self.tg.process_prop_value(prop); (*prop_id, prop) })) .map_err(MutationError::from)?; @@ -209,7 +216,7 @@ impl InternalAdditionOps for TemporalGraph { props: &[(usize, Prop)], layer: usize, ) -> Result, Self::Error> { - let edge = self.link_nodes(src, dst, t, layer, false); + let edge = self.tg.link_nodes(src, dst, t, layer, false); edge.try_map(|mut edge| { let eid = edge.eid(); let mut edge = edge.as_mut(); @@ -217,7 +224,7 @@ impl InternalAdditionOps for TemporalGraph { if !props.is_empty() { let edge_layer = edge.layer_mut(layer); for (prop_id, prop) in props { - let prop = self.process_prop_value(prop); + let prop = self.tg.process_prop_value(prop); edge_layer .add_prop(t, *prop_id, prop) .map_err(MutationError::from)?; @@ -235,13 +242,13 @@ impl InternalAdditionOps for TemporalGraph { props: &[(usize, Prop)], layer: usize, ) -> Result<(), Self::Error> { - let mut edge = self.link_edge(edge, t, layer, false); + let mut edge = self.tg.link_edge(edge, t, layer, false); let mut edge = edge.as_mut(); edge.additions_mut(layer).insert(t); if !props.is_empty() { let edge_layer = edge.layer_mut(layer); for (prop_id, prop) in props { - let prop = self.process_prop_value(prop); + let prop = self.tg.process_prop_value(prop); edge_layer .add_prop(t, *prop_id, prop) .map_err(MutationError::from)? @@ -253,6 +260,7 @@ impl InternalAdditionOps for TemporalGraph { impl InternalAdditionOps for GraphStorage { type Error = MutationError; + type WS<'b> = TGWriteSession<'b>; fn write_lock(&self) -> Result { self.mutable()?.write_lock() @@ -266,93 +274,32 @@ impl InternalAdditionOps for GraphStorage { self.mutable()?.write_lock_edges() } - fn next_event_id(&self) -> Result { - self.mutable()?.next_event_id() - } - - fn reserve_event_ids(&self, num_ids: usize) -> Result { - self.mutable()?.reserve_event_ids(num_ids) - } - - fn resolve_layer(&self, layer: Option<&str>) -> Result, Self::Error> { - self.mutable()?.resolve_layer(layer) - } - - fn resolve_node(&self, id: NodeRef) -> Result, Self::Error> { - self.mutable()?.resolve_node(id) - } - - fn set_node(&self, gid: GidRef, vid: VID) -> Result<(), Self::Error> { - self.mutable()?.set_node(gid, vid) - } - - fn resolve_node_and_type( - &self, - id: NodeRef, - node_type: &str, - ) -> Result, MaybeNew)>, Self::Error> { - self.mutable()?.resolve_node_and_type(id, node_type) + fn write_session(&self) -> Result, Self::Error> { + Ok(TGWriteSession { + tg: self.mutable()?, + }) } +} - fn resolve_graph_property( - &self, - prop: &str, - dtype: PropType, - is_static: bool, - ) -> Result, Self::Error> { - self.mutable()? - .resolve_graph_property(prop, dtype, is_static) - } +impl InternalAdditionOps for TemporalGraph { + type Error = MutationError; - fn resolve_node_property( - &self, - prop: &str, - dtype: PropType, - is_static: bool, - ) -> Result, Self::Error> { - self.mutable()? - .resolve_node_property(prop, dtype, is_static) - } + type WS<'b> = TGWriteSession<'b>; - fn resolve_edge_property( - &self, - prop: &str, - dtype: PropType, - is_static: bool, - ) -> Result, Self::Error> { - self.mutable()? - .resolve_edge_property(prop, dtype, is_static) + fn write_lock(&self) -> Result { + Ok(WriteLockedGraph::new(self)) } - fn internal_add_node( - &self, - t: TimeIndexEntry, - v: VID, - props: &[(usize, Prop)], - ) -> Result<(), Self::Error> { - self.mutable()?.internal_add_node(t, v, props) + fn write_lock_nodes(&self) -> Result { + Ok(self.storage.nodes.write_lock()) } - fn internal_add_edge( - &self, - t: TimeIndexEntry, - src: VID, - dst: VID, - props: &[(usize, Prop)], - layer: usize, - ) -> Result, Self::Error> { - self.mutable()?.internal_add_edge(t, src, dst, props, layer) + fn write_lock_edges(&self) -> Result { + Ok(self.storage.edges.write_lock()) } - fn internal_add_edge_update( - &self, - t: TimeIndexEntry, - edge: EID, - props: &[(usize, Prop)], - layer: usize, - ) -> Result<(), Self::Error> { - self.mutable()? - .internal_add_edge_update(t, edge, props, layer) + fn write_session(&self) -> Result, Self::Error> { + Ok(TGWriteSession { tg: self }) } } @@ -363,6 +310,11 @@ where G::Base: InternalAdditionOps, { type Error = ::Error; + type WS<'a> + = ::WS<'a> + where + ::Base: 'a, + G: 'a; #[inline] fn write_lock(&self) -> Result { @@ -380,99 +332,7 @@ where } #[inline] - fn next_event_id(&self) -> Result { - self.base().next_event_id() - } - - #[inline] - fn reserve_event_ids(&self, num_ids: usize) -> Result { - self.base().reserve_event_ids(num_ids) - } - - #[inline] - fn resolve_layer(&self, layer: Option<&str>) -> Result, Self::Error> { - self.base().resolve_layer(layer) - } - - #[inline] - fn resolve_node(&self, id: NodeRef) -> Result, Self::Error> { - self.base().resolve_node(id) - } - - #[inline] - fn set_node(&self, gid: GidRef, vid: VID) -> Result<(), Self::Error> { - self.base().set_node(gid, vid) - } - - #[inline] - fn resolve_node_and_type( - &self, - id: NodeRef, - node_type: &str, - ) -> Result, MaybeNew)>, Self::Error> { - self.base().resolve_node_and_type(id, node_type) - } - - #[inline] - fn resolve_graph_property( - &self, - prop: &str, - dtype: PropType, - is_static: bool, - ) -> Result, Self::Error> { - self.base().resolve_graph_property(prop, dtype, is_static) - } - - #[inline] - fn resolve_node_property( - &self, - prop: &str, - dtype: PropType, - is_static: bool, - ) -> Result, Self::Error> { - self.base().resolve_node_property(prop, dtype, is_static) - } - - #[inline] - fn resolve_edge_property( - &self, - prop: &str, - dtype: PropType, - is_static: bool, - ) -> Result, Self::Error> { - self.base().resolve_edge_property(prop, dtype, is_static) - } - - #[inline] - fn internal_add_node( - &self, - t: TimeIndexEntry, - v: VID, - props: &[(usize, Prop)], - ) -> Result<(), Self::Error> { - self.base().internal_add_node(t, v, props) - } - - #[inline] - fn internal_add_edge( - &self, - t: TimeIndexEntry, - src: VID, - dst: VID, - props: &[(usize, Prop)], - layer: usize, - ) -> Result, Self::Error> { - self.base().internal_add_edge(t, src, dst, props, layer) - } - - #[inline] - fn internal_add_edge_update( - &self, - t: TimeIndexEntry, - edge: EID, - props: &[(usize, Prop)], - layer: usize, - ) -> Result<(), Self::Error> { - self.base().internal_add_edge_update(t, edge, props, layer) + fn write_session(&self) -> Result, Self::Error> { + self.base().write_session() } } diff --git a/raphtory/src/db/api/mutation/addition_ops.rs b/raphtory/src/db/api/mutation/addition_ops.rs index 92cc0e0d5a..aae1b48035 100644 --- a/raphtory/src/db/api/mutation/addition_ops.rs +++ b/raphtory/src/db/api/mutation/addition_ops.rs @@ -6,7 +6,7 @@ use crate::{ }, db::{ api::{ - mutation::{CollectProperties, TryIntoInputTime}, + mutation::{time_from_input_session, CollectProperties, TryIntoInputTime}, view::StaticGraphViewOps, }, graph::{edge::EdgeView, node::NodeView}, @@ -18,7 +18,7 @@ use raphtory_api::core::{ entities::properties::prop::Prop, storage::dict_mapper::MaybeNew::{Existing, New}, }; -use raphtory_storage::mutation::addition_ops::InternalAdditionOps; +use raphtory_storage::mutation::addition_ops::{InternalAdditionOps, SessionAdditionOps}; pub trait AdditionOps: StaticGraphViewOps + InternalAdditionOps> { // TODO: Probably add vector reference here like add @@ -122,27 +122,29 @@ impl> + StaticGraphViewOps> Addit props: PI, node_type: Option<&str>, ) -> Result, GraphError> { + let session = self.write_session().map_err(|err| err.into())?; let ti = time_from_input(self, t)?; let properties = props.collect_properties(|name, dtype| { - Ok(self + Ok(session .resolve_node_property(name, dtype, false) .map_err(into_graph_err)? .inner()) })?; let v_id = match node_type { - None => self + None => session .resolve_node(v.as_node_ref()) .map_err(into_graph_err)? .inner(), Some(node_type) => { - let (v_id, _) = self + let (v_id, _) = session .resolve_node_and_type(v.as_node_ref(), node_type) .map_err(into_graph_err)? .inner(); v_id.inner() } }; - self.internal_add_node(ti, v_id, &properties) + session + .internal_add_node(ti, v_id, &properties) .map_err(into_graph_err)?; Ok(NodeView::new_internal(self.clone(), v_id)) } @@ -155,10 +157,13 @@ impl> + StaticGraphViewOps> Addit node_type: Option<&str>, ) -> Result, GraphError> { let ti = time_from_input(self, t)?; + let session = self.write_session().map_err(|err| err.into())?; let v_id = match node_type { - None => self.resolve_node(v.as_node_ref()).map_err(into_graph_err)?, + None => session + .resolve_node(v.as_node_ref()) + .map_err(into_graph_err)?, Some(node_type) => { - let (v_id, _) = self + let (v_id, _) = session .resolve_node_and_type(v.as_node_ref(), node_type) .map_err(into_graph_err)? .inner(); @@ -168,12 +173,13 @@ impl> + StaticGraphViewOps> Addit match v_id { New(id) => { let properties = props.collect_properties(|name, dtype| { - Ok(self + Ok(session .resolve_node_property(name, dtype, false) .map_err(into_graph_err)? .inner()) })?; - self.internal_add_node(ti, id, &properties) + session + .internal_add_node(ti, id, &properties) .map_err(into_graph_err)?; Ok(NodeView::new_internal(self.clone(), id)) } @@ -192,24 +198,28 @@ impl> + StaticGraphViewOps> Addit props: PI, layer: Option<&str>, ) -> Result, GraphError> { - let ti = time_from_input(self, t)?; - let src_id = self + let session = self.write_session().map_err(|err| err.into())?; + let ti = time_from_input_session(&session, t)?; + let src_id = session .resolve_node(src.as_node_ref()) .map_err(into_graph_err)? .inner(); - let dst_id = self + let dst_id = session .resolve_node(dst.as_node_ref()) .map_err(into_graph_err)? .inner(); - let layer_id = self.resolve_layer(layer).map_err(into_graph_err)?.inner(); + let layer_id = session + .resolve_layer(layer) + .map_err(into_graph_err)? + .inner(); let properties: Vec<(usize, Prop)> = props.collect_properties(|name, dtype| { - Ok(self + Ok(session .resolve_edge_property(name, dtype, false) .map_err(into_graph_err)? .inner()) })?; - let eid = self + let eid = session .internal_add_edge(ti, src_id, dst_id, &properties, layer_id) .map_err(into_graph_err)? .inner(); diff --git a/raphtory/src/db/api/mutation/deletion_ops.rs b/raphtory/src/db/api/mutation/deletion_ops.rs index d6e8c79a1a..17dad3db98 100644 --- a/raphtory/src/db/api/mutation/deletion_ops.rs +++ b/raphtory/src/db/api/mutation/deletion_ops.rs @@ -1,15 +1,18 @@ -use super::time_from_input; use crate::{ core::{entities::nodes::node_ref::AsNodeRef, utils::time::IntoTimeWithFormat}, db::{ - api::{mutation::TryIntoInputTime, view::StaticGraphViewOps}, + api::{ + mutation::{time_from_input_session, TryIntoInputTime}, + view::StaticGraphViewOps, + }, graph::edge::EdgeView, }, errors::{into_graph_err, GraphError}, }; use raphtory_api::core::entities::edges::edge_ref::EdgeRef; use raphtory_storage::mutation::{ - addition_ops::InternalAdditionOps, deletion_ops::InternalDeletionOps, + addition_ops::{InternalAdditionOps, SessionAdditionOps}, + deletion_ops::InternalDeletionOps, }; pub trait DeletionOps: @@ -25,16 +28,20 @@ pub trait DeletionOps: dst: V, layer: Option<&str>, ) -> Result, GraphError> { - let ti = time_from_input(self, t).map_err(into_graph_err)?; - let src_id = self + let session = self.write_session().map_err(|err| err.into())?; + let ti = time_from_input_session(&session, t).map_err(into_graph_err)?; + let src_id = session .resolve_node(src.as_node_ref()) .map_err(into_graph_err)? .inner(); - let dst_id = self + let dst_id = session .resolve_node(dst.as_node_ref()) .map_err(into_graph_err)? .inner(); - let layer = self.resolve_layer(layer).map_err(into_graph_err)?.inner(); + let layer = session + .resolve_layer(layer) + .map_err(into_graph_err)? + .inner(); let eid = self .internal_delete_edge(ti, src_id, dst_id, layer) .map_err(into_graph_err)? diff --git a/raphtory/src/db/api/mutation/import_ops.rs b/raphtory/src/db/api/mutation/import_ops.rs index c4e4fa477f..149273d3a9 100644 --- a/raphtory/src/db/api/mutation/import_ops.rs +++ b/raphtory/src/db/api/mutation/import_ops.rs @@ -1,8 +1,8 @@ -use super::time_from_input; use crate::{ core::entities::nodes::node_ref::AsNodeRef, db::{ api::{ + mutation::time_from_input_session, properties::internal::TemporalPropertiesOps, view::{internal::InternalMaterialize, StaticGraphViewOps}, }, @@ -18,7 +18,8 @@ use raphtory_api::core::{ storage::{arc_str::OptionAsStr, timeindex::AsTime}, }; use raphtory_storage::mutation::{ - addition_ops::InternalAdditionOps, deletion_ops::InternalDeletionOps, + addition_ops::{InternalAdditionOps, SessionAdditionOps}, + deletion_ops::InternalDeletionOps, property_addition_ops::InternalPropertyAdditionOps, }; use std::{borrow::Borrow, fmt::Debug}; @@ -331,10 +332,12 @@ fn import_node_internal< } } + let session = graph.write_session().map_err(|err| err.into())?; + let node_internal = match node.node_type().as_str() { - None => graph.resolve_node(id).map_err(into_graph_err)?.inner(), + None => session.resolve_node(id).map_err(into_graph_err)?.inner(), Some(node_type) => { - let (node_internal, _) = graph + let (node_internal, _) = session .resolve_node_and_type(id, node_type) .map_err(into_graph_err)? .inner(); @@ -344,18 +347,18 @@ fn import_node_internal< let keys = node.temporal_prop_keys().collect::>(); for (t, row) in node.rows() { - let t = time_from_input(graph, t)?; + let t = time_from_input_session(&session, t)?; let props = row .into_iter() .zip(&keys) .map(|((_, prop), key)| { - let prop_id = graph.resolve_node_property(key, prop.dtype(), false); + let prop_id = session.resolve_node_property(key, prop.dtype(), false); prop_id.map(|prop_id| (prop_id.inner(), prop)) }) .collect::, _>>() .map_err(into_graph_err)?; - graph + session .internal_add_node(t, node_internal, &props) .map_err(into_graph_err)?; } @@ -393,6 +396,7 @@ fn import_edge_internal< } // Add edges first to ensure associated nodes are present + let session = graph.write_session().map_err(|err| err.into())?; for ee in edge.explode_layers() { let layer_name = ee.layer_name().expect("exploded layers"); @@ -407,10 +411,16 @@ fn import_edge_internal< } for (t, _) in edge.deletions_hist() { - let ti = time_from_input(graph, t.t())?; - let src_node = graph.resolve_node(src_id).map_err(into_graph_err)?.inner(); - let dst_node = graph.resolve_node(dst_id).map_err(into_graph_err)?.inner(); - let layer = graph + let ti = time_from_input_session(&session, t.t())?; + let src_node = session + .resolve_node(src_id) + .map_err(into_graph_err)? + .inner(); + let dst_node = session + .resolve_node(dst_id) + .map_err(into_graph_err)? + .inner(); + let layer = session .resolve_layer(Some(&layer_name)) .map_err(into_graph_err)? .inner(); diff --git a/raphtory/src/db/api/mutation/mod.rs b/raphtory/src/db/api/mutation/mod.rs index 9d5c01db67..ad8046770d 100644 --- a/raphtory/src/db/api/mutation/mod.rs +++ b/raphtory/src/db/api/mutation/mod.rs @@ -18,11 +18,28 @@ use raphtory_api::core::{ entities::properties::prop::PropType, storage::timeindex::TimeIndexEntry, }; pub(crate) use raphtory_core::utils::time::{InputTime, TryIntoInputTime}; -use raphtory_storage::mutation::addition_ops::InternalAdditionOps; +use raphtory_storage::mutation::addition_ops::{InternalAdditionOps, SessionAdditionOps}; pub fn time_from_input>, T: TryIntoInputTime>( g: &G, t: T, +) -> Result { + let t = t.try_into_input_time()?; + let session = g.write_session().map_err(|err| err.into())?; + Ok(match t { + InputTime::Simple(t) => { + TimeIndexEntry::new(t, session.next_event_id().map_err(into_graph_err)?) + } + InputTime::Indexed(t, s) => TimeIndexEntry::new(t, s), + }) +} + +pub fn time_from_input_session< + G: SessionAdditionOps>, + T: TryIntoInputTime, +>( + g: &G, + t: T, ) -> Result { let t = t.try_into_input_time()?; Ok(match t { diff --git a/raphtory/src/db/api/mutation/property_addition_ops.rs b/raphtory/src/db/api/mutation/property_addition_ops.rs index 74eff2a634..74ce79e0ba 100644 --- a/raphtory/src/db/api/mutation/property_addition_ops.rs +++ b/raphtory/src/db/api/mutation/property_addition_ops.rs @@ -1,10 +1,11 @@ -use super::{time_from_input, CollectProperties}; +use super::CollectProperties; use crate::{ - db::api::mutation::TryIntoInputTime, + db::api::mutation::{time_from_input_session, TryIntoInputTime}, errors::{into_graph_err, GraphError}, }; use raphtory_storage::mutation::{ - addition_ops::InternalAdditionOps, property_addition_ops::InternalPropertyAdditionOps, + addition_ops::{InternalAdditionOps, SessionAdditionOps}, + property_addition_ops::InternalPropertyAdditionOps, }; pub trait PropertyAdditionOps: @@ -33,9 +34,10 @@ impl< t: T, props: PI, ) -> Result<(), GraphError> { - let ti = time_from_input(self, t)?; + let session = self.write_session().map_err(|err| err.into())?; + let ti = time_from_input_session(&session, t)?; let properties: Vec<_> = props.collect_properties(|name, dtype| { - Ok(self + Ok(session .resolve_graph_property(name, dtype, false) .map_err(into_graph_err)? .inner()) @@ -46,8 +48,9 @@ impl< } fn add_constant_properties(&self, props: PI) -> Result<(), GraphError> { + let session = self.write_session().map_err(|err| err.into())?; let properties: Vec<_> = props.collect_properties(|name, dtype| { - Ok(self + Ok(session .resolve_graph_property(name, dtype, true) .map_err(into_graph_err)? .inner()) @@ -61,8 +64,9 @@ impl< &self, props: PI, ) -> Result<(), GraphError> { + let session = self.write_session().map_err(|err| err.into())?; let properties: Vec<_> = props.collect_properties(|name, dtype| { - Ok(self + Ok(session .resolve_graph_property(name, dtype, true) .map_err(into_graph_err)? .inner()) diff --git a/raphtory/src/db/api/storage/graph/storage_ops/time_semantics.rs b/raphtory/src/db/api/storage/graph/storage_ops/time_semantics.rs index 75f566a26e..f572be830c 100644 --- a/raphtory/src/db/api/storage/graph/storage_ops/time_semantics.rs +++ b/raphtory/src/db/api/storage/graph/storage_ops/time_semantics.rs @@ -1,6 +1,6 @@ use super::GraphStorage; use crate::{ - core::{entities::LayerIds, storage::timeindex::TimeIndexOps, utils::iter::GenLockedDIter}, + core::{entities::LayerIds, storage::timeindex::TimeIndexOps}, db::api::view::internal::{ EdgeHistoryFilter, GraphTimeSemanticsOps, NodeHistoryFilter, TimeSemantics, }, @@ -11,7 +11,7 @@ use raphtory_api::{ entities::{properties::tprop::TPropOps, EID, VID}, storage::timeindex::{AsTime, TimeIndexEntry}, }, - iter::{BoxedLDIter, BoxedLIter, IntoDynBoxed, IntoDynDBoxed}, + iter::{BoxedLIter, IntoDynBoxed}, }; use raphtory_core::utils::iter::GenLockedIter; use raphtory_storage::{ diff --git a/raphtory/src/db/api/storage/storage.rs b/raphtory/src/db/api/storage/storage.rs index 2c24b1acaf..1027f68530 100644 --- a/raphtory/src/db/api/storage/storage.rs +++ b/raphtory/src/db/api/storage/storage.rs @@ -11,7 +11,10 @@ use raphtory_api::core::{ entities::{EID, VID}, storage::{dict_mapper::MaybeNew, timeindex::TimeIndexEntry}, }; -use raphtory_storage::graph::graph::GraphStorage; +use raphtory_storage::{ + graph::graph::GraphStorage, + mutation::addition_ops::{SessionAdditionOps, TGWriteSession}, +}; use serde::{Deserialize, Serialize}; use std::{ fmt::{Display, Formatter}, @@ -198,46 +201,41 @@ impl InheritEdgeHistoryFilter for Storage {} impl InheritViewOps for Storage {} -impl InternalAdditionOps for Storage { - type Error = GraphError; - - fn write_lock(&self) -> Result { - Ok(self.graph.write_lock()?) - } - - fn write_lock_nodes(&self) -> Result { - Ok(self.graph.write_lock_nodes()?) - } +#[derive(Clone, Copy)] +pub struct StorageWriteSession<'a> { + session: TGWriteSession<'a>, + storage: &'a Storage, +} - fn write_lock_edges(&self) -> Result { - Ok(self.graph.write_lock_edges()?) - } +impl<'a> SessionAdditionOps for StorageWriteSession<'a> { + type Error = GraphError; fn next_event_id(&self) -> Result { - Ok(self.graph.next_event_id()?) + Ok(self.session.next_event_id()?) } fn reserve_event_ids(&self, num_ids: usize) -> Result { - Ok(self.graph.reserve_event_ids(num_ids)?) + Ok(self.session.reserve_event_ids(num_ids)?) } - fn resolve_layer(&self, layer: Option<&str>) -> Result, GraphError> { - let id = self.graph.resolve_layer(layer)?; + fn resolve_layer(&self, layer: Option<&str>) -> Result, Self::Error> { + let id = self.session.resolve_layer(layer)?; #[cfg(feature = "proto")] - self.if_cache(|cache| cache.resolve_layer(layer, id)); + self.storage + .if_cache(|cache| cache.resolve_layer(layer, id)); Ok(id) } - fn resolve_node(&self, id: NodeRef) -> Result, GraphError> { + fn resolve_node(&self, id: NodeRef) -> Result, Self::Error> { match id { NodeRef::Internal(id) => Ok(MaybeNew::Existing(id)), NodeRef::External(gid) => { - let id = self.graph.resolve_node(id)?; + let id = self.session.resolve_node(id)?; #[cfg(feature = "proto")] - self.if_cache(|cache| cache.resolve_node(id, gid)); + self.storage.if_cache(|cache| cache.resolve_node(id, gid)); Ok(id) } @@ -245,20 +243,22 @@ impl InternalAdditionOps for Storage { } fn set_node(&self, gid: GidRef, vid: VID) -> Result<(), Self::Error> { - Ok(self.graph.set_node(gid, vid)?) + Ok(self.session.set_node(gid, vid)?) } fn resolve_node_and_type( &self, id: NodeRef, node_type: &str, - ) -> Result, MaybeNew)>, GraphError> { - let node_and_type = self.graph.resolve_node_and_type(id, node_type)?; + ) -> Result, MaybeNew)>, Self::Error> { + let node_and_type = self.session.resolve_node_and_type(id, node_type)?; #[cfg(feature = "proto")] - self.if_cache(|cache| { + self.storage.if_cache(|cache| { + use raphtory_storage::core_ops::CoreGraphOps; + let (vid, _) = node_and_type.inner(); - let node_entry = self.graph.core_node(vid.inner()); + let node_entry = self.storage.core_node(vid.inner()); cache.resolve_node_and_type(node_and_type, node_type, node_entry.id()) }); @@ -270,14 +270,15 @@ impl InternalAdditionOps for Storage { prop: &str, dtype: PropType, is_static: bool, - ) -> Result, GraphError> { + ) -> Result, Self::Error> { let id = self - .graph + .session .resolve_graph_property(prop, dtype.clone(), is_static)?; #[cfg(feature = "proto")] - self.if_cache(|cache| cache.resolve_graph_property(prop, id, dtype, is_static)); - + self.storage.if_cache(|cache| { + cache.resolve_graph_property(prop, id, dtype, is_static); + }); Ok(id) } @@ -286,13 +287,15 @@ impl InternalAdditionOps for Storage { prop: &str, dtype: PropType, is_static: bool, - ) -> Result, GraphError> { + ) -> Result, Self::Error> { let id = self - .graph + .session .resolve_node_property(prop, dtype.clone(), is_static)?; #[cfg(feature = "proto")] - self.if_cache(|cache| cache.resolve_node_property(prop, id, &dtype, is_static)); + self.storage.if_cache(|cache| { + cache.resolve_node_property(prop, id, &dtype, is_static); + }); Ok(id) } @@ -302,13 +305,15 @@ impl InternalAdditionOps for Storage { prop: &str, dtype: PropType, is_static: bool, - ) -> Result, GraphError> { + ) -> Result, Self::Error> { let id = self - .graph + .session .resolve_edge_property(prop, dtype.clone(), is_static)?; #[cfg(feature = "proto")] - self.if_cache(|cache| cache.resolve_edge_property(prop, id, &dtype, is_static)); + self.storage.if_cache(|cache| { + cache.resolve_edge_property(prop, id, &dtype, is_static); + }); Ok(id) } @@ -318,14 +323,17 @@ impl InternalAdditionOps for Storage { t: TimeIndexEntry, v: VID, props: &[(usize, Prop)], - ) -> Result<(), GraphError> { - self.graph.internal_add_node(t, v, props)?; + ) -> Result<(), Self::Error> { + self.session.internal_add_node(t, v, props)?; #[cfg(feature = "proto")] - self.if_cache(|cache| cache.add_node_update(t, v, props)); + self.storage + .if_cache(|cache| cache.add_node_update(t, v, props)); #[cfg(feature = "search")] - self.if_index(|index| index.add_node_update(&self.graph, t, MaybeNew::New(v), props))?; + self.storage.if_index(|index| { + index.add_node_update(&self.storage.graph, t, MaybeNew::New(v), props) + })?; Ok(()) } @@ -337,18 +345,16 @@ impl InternalAdditionOps for Storage { dst: VID, props: &[(usize, Prop)], layer: usize, - ) -> Result, GraphError> { - let id = self.graph.internal_add_edge(t, src, dst, props, layer)?; - + ) -> Result, Self::Error> { + let id = self.session.internal_add_edge(t, src, dst, props, layer)?; #[cfg(feature = "proto")] - self.if_cache(|cache| { + self.storage.if_cache(|cache| { cache.resolve_edge(id, src, dst); cache.add_edge_update(t, id.inner(), props, layer); }); - #[cfg(feature = "search")] - self.if_index(|index| index.add_edge_update(&self.graph, id, t, layer, props))?; - + self.storage + .if_index(|index| index.add_edge_update(&self.storage.graph, id, t, layer, props))?; Ok(id) } @@ -358,21 +364,53 @@ impl InternalAdditionOps for Storage { edge: EID, props: &[(usize, Prop)], layer: usize, - ) -> Result<(), GraphError> { - self.graph.internal_add_edge_update(t, edge, props, layer)?; + ) -> Result<(), Self::Error> { + self.session + .internal_add_edge_update(t, edge, props, layer)?; #[cfg(feature = "proto")] - self.if_cache(|cache| cache.add_edge_update(t, edge, props, layer)); - + self.storage + .if_cache(|cache| cache.add_edge_update(t, edge, props, layer)); #[cfg(feature = "search")] - self.if_index(|index| { - index.add_edge_update(&self.graph, MaybeNew::Existing(edge), t, layer, props) + self.storage.if_index(|index| { + index.add_edge_update( + &self.storage.graph, + MaybeNew::Existing(edge), + t, + layer, + props, + ) })?; - Ok(()) } } +impl InternalAdditionOps for Storage { + type Error = GraphError; + + type WS<'a> = StorageWriteSession<'a>; + + fn write_lock(&self) -> Result { + Ok(self.graph.write_lock()?) + } + + fn write_lock_nodes(&self) -> Result { + Ok(self.graph.write_lock_nodes()?) + } + + fn write_lock_edges(&self) -> Result { + Ok(self.graph.write_lock_edges()?) + } + + fn write_session(&self) -> Result, Self::Error> { + let session = self.graph.write_session()?; + Ok(StorageWriteSession { + session, + storage: self, + }) + } +} + impl InternalPropertyAdditionOps for Storage { type Error = GraphError; fn internal_add_properties( diff --git a/raphtory/src/db/api/view/exploded_edge_property_filter.rs b/raphtory/src/db/api/view/exploded_edge_property_filter.rs index 5e5cdffac6..129a85ad59 100644 --- a/raphtory/src/db/api/view/exploded_edge_property_filter.rs +++ b/raphtory/src/db/api/view/exploded_edge_property_filter.rs @@ -44,7 +44,7 @@ mod test { }, }; use proptest::{arbitrary::any, proptest}; - use raphtory_storage::mutation::addition_ops::InternalAdditionOps; + use raphtory_storage::mutation::addition_ops::{InternalAdditionOps, SessionAdditionOps}; use std::collections::HashMap; fn build_filtered_graph( @@ -68,7 +68,7 @@ mod test { } } if !edges.is_empty() { - g.resolve_layer(None).unwrap(); + g.write_session().unwrap().resolve_layer(None).unwrap(); } g } @@ -79,8 +79,9 @@ mod test { ) -> (PersistentGraph, PersistentGraph) { let g = PersistentGraph::new(); let g_filtered = PersistentGraph::new(); + let session = g_filtered.write_session().unwrap(); if !edges.iter().all(|(_, v)| v.is_empty()) { - g_filtered.resolve_layer(None).unwrap(); + session.resolve_layer(None).unwrap(); } for ((src, dst), mut updates) in edges { let mut keep = false; @@ -164,7 +165,7 @@ mod test { )) .unwrap(); let gf = Graph::new(); - gf.resolve_layer(None).unwrap(); + gf.write_session().unwrap().resolve_layer(None).unwrap(); assert_eq!(filtered.count_nodes(), 0); assert_eq!(filtered.count_edges(), 0); assert_graph_equal(&filtered, &gf); diff --git a/raphtory/src/db/api/view/graph.rs b/raphtory/src/db/api/view/graph.rs index 09feaed6b8..e182a05cb7 100644 --- a/raphtory/src/db/api/view/graph.rs +++ b/raphtory/src/db/api/view/graph.rs @@ -45,7 +45,10 @@ use raphtory_storage::{ edges::edge_storage_ops::EdgeStorageOps, graph::GraphStorage, nodes::node_storage_ops::NodeStorageOps, }, - mutation::{addition_ops::InternalAdditionOps, MutationError}, + mutation::{ + addition_ops::{InternalAdditionOps, SessionAdditionOps}, + MutationError, + }, }; use rayon::prelude::*; use rustc_hash::FxHashSet; @@ -302,6 +305,8 @@ impl<'graph, G: GraphView + 'graph> GraphViewOps<'graph> for G { let g = GraphStorage::from(g); + let g_session = g.write_session()?; + { // scope for the write lock let mut new_storage = g.write_lock()?; @@ -325,7 +330,7 @@ impl<'graph, G: GraphView + 'graph> GraphViewOps<'graph> for G { .inner(); new_node.node_store_mut().node_type = new_type_id; } - g.set_node(gid.as_ref(), new_id)?; + g_session.set_node(gid.as_ref(), new_id)?; for (t, rows) in node.rows() { let prop_offset = new_node.t_props_log_mut().push(rows)?; diff --git a/raphtory/src/db/api/view/internal/time_semantics/mod.rs b/raphtory/src/db/api/view/internal/time_semantics/mod.rs index c894982534..eb45a8ccbb 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/mod.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/mod.rs @@ -13,7 +13,7 @@ pub use history_filter::*; pub use time_semantics::TimeSemantics; pub use time_semantics_ops::*; -use crate::db::api::view::{BoxedLDIter, MaterializedGraph}; +use crate::db::api::view::MaterializedGraph; use enum_dispatch::enum_dispatch; use raphtory_api::{ core::{entities::properties::prop::Prop, storage::timeindex::TimeIndexEntry}, diff --git a/raphtory/src/db/graph/edge.rs b/raphtory/src/db/graph/edge.rs index e72744ef25..76343a3f8e 100644 --- a/raphtory/src/db/graph/edge.rs +++ b/raphtory/src/db/graph/edge.rs @@ -11,7 +11,9 @@ use crate::{ }, db::{ api::{ - mutation::{time_from_input, CollectProperties, TryIntoInputTime}, + mutation::{ + time_from_input, time_from_input_session, CollectProperties, TryIntoInputTime, + }, properties::{ internal::{ConstantPropertiesOps, TemporalPropertiesOps, TemporalPropertyViewOps}, Properties, @@ -36,7 +38,8 @@ use raphtory_core::entities::graph::tgraph::InvalidLayer; use raphtory_storage::{ graph::edges::edge_storage_ops::EdgeStorageOps, mutation::{ - addition_ops::InternalAdditionOps, deletion_ops::InternalDeletionOps, + addition_ops::{InternalAdditionOps, SessionAdditionOps}, + deletion_ops::InternalDeletionOps, property_addition_ops::InternalPropertyAdditionOps, }, }; @@ -299,7 +302,8 @@ impl EdgeView { None => { if create { self.graph - .resolve_layer(layer) + .write_session() + .and_then(|s| s.resolve_layer(layer)) .map_err(into_graph_err)? .inner() } else { @@ -356,7 +360,8 @@ impl EdgeView { let properties: Vec<(usize, Prop)> = properties.collect_properties(|name, dtype| { Ok(self .graph - .resolve_edge_property(name, dtype, true) + .write_session() + .and_then(|s| s.resolve_edge_property(name, dtype, true)) .map_err(into_graph_err)? .inner()) })?; @@ -376,7 +381,8 @@ impl EdgeView { let properties: Vec<(usize, Prop)> = props.collect_properties(|name, dtype| { Ok(self .graph - .resolve_edge_property(name, dtype, true) + .write_session() + .and_then(|s| s.resolve_edge_property(name, dtype, true)) .map_err(into_graph_err)? .inner()) })?; @@ -393,17 +399,18 @@ impl EdgeView { props: C, layer: Option<&str>, ) -> Result<(), GraphError> { - let t = time_from_input(&self.graph, time)?; + let session = self.graph.write_session().map_err(into_graph_err)?; + + let t = time_from_input_session(&session, time)?; let layer_id = self.resolve_layer(layer, true)?; let properties: Vec<(usize, Prop)> = props.collect_properties(|name, dtype| { - Ok(self - .graph + Ok(session .resolve_edge_property(name, dtype, false) .map_err(into_graph_err)? .inner()) })?; - self.graph + session .internal_add_edge_update(t, self.edge.pid(), &properties, layer_id) .map_err(into_graph_err)?; Ok(()) diff --git a/raphtory/src/db/graph/graph.rs b/raphtory/src/db/graph/graph.rs index f1fc81863e..92284ec702 100644 --- a/raphtory/src/db/graph/graph.rs +++ b/raphtory/src/db/graph/graph.rs @@ -604,7 +604,10 @@ mod db_tests { utils::logging::global_info_logger, }; use raphtory_core::utils::time::{ParseTimeError, TryIntoTime}; - use raphtory_storage::{core_ops::CoreGraphOps, mutation::addition_ops::InternalAdditionOps}; + use raphtory_storage::{ + core_ops::CoreGraphOps, + mutation::addition_ops::{InternalAdditionOps, SessionAdditionOps}, + }; use rayon::join; use std::{ collections::{HashMap, HashSet}, @@ -4035,7 +4038,11 @@ mod db_tests { let expected = Graph::new(); expected.add_edge(0, 0, 0, NO_PROPS, None).unwrap(); - expected.resolve_layer(Some("a")).unwrap(); + expected + .write_session() + .unwrap() + .resolve_layer(Some("a")) + .unwrap(); assert_graph_equal(&gw, &expected); } diff --git a/raphtory/src/db/graph/node.rs b/raphtory/src/db/graph/node.rs index ba6f70dce0..3f35da4282 100644 --- a/raphtory/src/db/graph/node.rs +++ b/raphtory/src/db/graph/node.rs @@ -4,7 +4,7 @@ use crate::{ core::entities::{edges::edge_ref::EdgeRef, nodes::node_ref::NodeRef, VID}, db::{ api::{ - mutation::{time_from_input, CollectProperties, TryIntoInputTime}, + mutation::{time_from_input_session, CollectProperties, TryIntoInputTime}, properties::internal::{ ConstantPropertiesOps, TemporalPropertiesOps, TemporalPropertyViewOps, }, @@ -36,7 +36,9 @@ use raphtory_api::core::{ entities::properties::prop::PropType, storage::{arc_str::ArcStr, timeindex::TimeIndexEntry}, }; -use raphtory_storage::{core_ops::CoreGraphOps, graph::graph::GraphStorage}; +use raphtory_storage::{ + core_ops::CoreGraphOps, graph::graph::GraphStorage, mutation::addition_ops::SessionAdditionOps, +}; use std::{ fmt, hash::{Hash, Hasher}, @@ -407,7 +409,8 @@ impl NodeView<'static let properties: Vec<(usize, Prop)> = properties.collect_properties(|name, dtype| { Ok(self .graph - .resolve_node_property(name, dtype, true) + .write_session() + .and_then(|s| s.resolve_node_property(name, dtype, true)) .map_err(into_graph_err)? .inner()) })?; @@ -418,7 +421,8 @@ impl NodeView<'static pub fn set_node_type(&self, new_type: &str) -> Result<(), GraphError> { self.graph - .resolve_node_and_type(NodeRef::Internal(self.node), new_type) + .write_session() + .and_then(|s| s.resolve_node_and_type(NodeRef::Internal(self.node), new_type)) .map_err(into_graph_err)?; Ok(()) } @@ -430,7 +434,8 @@ impl NodeView<'static let properties: Vec<(usize, Prop)> = props.collect_properties(|name, dtype| { Ok(self .graph - .resolve_node_property(name, dtype, true) + .write_session() + .and_then(|s| s.resolve_node_property(name, dtype, true)) .map_err(into_graph_err)? .inner()) })?; @@ -444,15 +449,15 @@ impl NodeView<'static time: T, props: C, ) -> Result<(), GraphError> { - let t = time_from_input(&self.graph, time)?; + let session = self.graph.write_session().map_err(|err| err.into())?; + let t = time_from_input_session(&session, time)?; let properties: Vec<(usize, Prop)> = props.collect_properties(|name, dtype| { - Ok(self - .graph + Ok(session .resolve_node_property(name, dtype, false) .map_err(into_graph_err)? .inner()) })?; - self.graph + session .internal_add_node(t, self.node, &properties) .map_err(into_graph_err) } diff --git a/raphtory/src/db/graph/views/deletion_graph.rs b/raphtory/src/db/graph/views/deletion_graph.rs index ae582f30be..f112d7844c 100644 --- a/raphtory/src/db/graph/views/deletion_graph.rs +++ b/raphtory/src/db/graph/views/deletion_graph.rs @@ -15,7 +15,7 @@ use crate::{ use raphtory_api::{ core::entities::{properties::tprop::TPropOps, EID, VID}, inherit::Base, - iter::{BoxedLDIter, BoxedLIter, IntoDynBoxed}, + iter::{BoxedLIter, IntoDynBoxed}, GraphType, }; use raphtory_core::utils::iter::GenLockedIter; @@ -428,7 +428,7 @@ mod test_deletions { use itertools::Itertools; use proptest::{arbitrary::any, proptest, sample::subsequence}; use raphtory_api::core::entities::GID; - use raphtory_storage::mutation::addition_ops::InternalAdditionOps; + use raphtory_storage::mutation::addition_ops::{InternalAdditionOps, SessionAdditionOps}; use std::ops::Range; #[test] @@ -611,7 +611,11 @@ mod test_deletions { let expected = PersistentGraph::new(); expected.add_edge(1, 0, 0, NO_PROPS, None).unwrap(); - expected.resolve_layer(Some("a")).unwrap(); // empty layer exists + expected + .write_session() + .unwrap() + .resolve_layer(Some("a")) + .unwrap(); // empty layer exists println!("expected: {:?}", expected); assert_persistent_materialize_graph_equal(&gw, &expected); @@ -666,7 +670,11 @@ mod test_deletions { g.delete_edge(0, 0, 0, None).unwrap(); let gw = g.window(0, 0).valid_layers("a"); let expected_gw = PersistentGraph::new(); - expected_gw.resolve_layer(Some("a")).unwrap(); + expected_gw + .write_session() + .unwrap() + .resolve_layer(Some("a")) + .unwrap(); assert_graph_equal(&gw, &expected_gw); let gwm = gw.materialize().unwrap(); assert_graph_equal(&gw, &gwm); diff --git a/raphtory/src/db/graph/views/filter/node_type_filtered_graph.rs b/raphtory/src/db/graph/views/filter/node_type_filtered_graph.rs index a5e8c9c200..0f6e7bf1ab 100644 --- a/raphtory/src/db/graph/views/filter/node_type_filtered_graph.rs +++ b/raphtory/src/db/graph/views/filter/node_type_filtered_graph.rs @@ -123,7 +123,7 @@ mod tests_node_type_filtered_subgraph { test_utils::{build_graph, build_graph_strat, make_node_types}, }; use proptest::{arbitrary::any, proptest}; - use raphtory_storage::mutation::addition_ops::InternalAdditionOps; + use raphtory_storage::mutation::addition_ops::{InternalAdditionOps, SessionAdditionOps}; use std::ops::Range; #[test] @@ -209,7 +209,11 @@ mod tests_node_type_filtered_subgraph { g.add_edge(0, 0, 1, NO_PROPS, None).unwrap(); g.node(1).unwrap().set_node_type("test").unwrap(); let expected = Graph::new(); - expected.resolve_layer(None).unwrap(); + expected + .write_session() + .unwrap() + .resolve_layer(None) + .unwrap(); assert_graph_equal(&g.subgraph_node_types(["test"]), &expected); } @@ -220,7 +224,11 @@ mod tests_node_type_filtered_subgraph { g.node(0).unwrap().set_node_type("two").unwrap(); let gw = g.window(0, 1); let expected = Graph::new(); - expected.resolve_layer(None).unwrap(); + expected + .write_session() + .unwrap() + .resolve_layer(None) + .unwrap(); let sg = gw.subgraph_node_types(["_default"]); assert!(!sg.has_node(0)); assert!(!sg.has_node(1)); diff --git a/raphtory/src/db/graph/views/node_subgraph.rs b/raphtory/src/db/graph/views/node_subgraph.rs index e7acc4b0f7..b7713a2a48 100644 --- a/raphtory/src/db/graph/views/node_subgraph.rs +++ b/raphtory/src/db/graph/views/node_subgraph.rs @@ -153,7 +153,7 @@ mod subgraph_tests { use ahash::HashSet; use itertools::Itertools; use proptest::{proptest, sample::subsequence}; - use raphtory_storage::mutation::addition_ops::InternalAdditionOps; + use raphtory_storage::mutation::addition_ops::{InternalAdditionOps, SessionAdditionOps}; use std::collections::BTreeSet; #[test] @@ -652,7 +652,11 @@ mod subgraph_tests { let g = Graph::new(); g.add_edge(0, 0, 1, NO_PROPS, None).unwrap(); let expected = Graph::new(); - expected.resolve_layer(None).unwrap(); + expected + .write_session() + .unwrap() + .resolve_layer(None) + .unwrap(); let subgraph = g.subgraph([0]); assert_graph_equal(&subgraph, &expected); } diff --git a/raphtory/src/db/graph/views/valid_graph.rs b/raphtory/src/db/graph/views/valid_graph.rs index 74cb3709ad..7d2e61adb6 100644 --- a/raphtory/src/db/graph/views/valid_graph.rs +++ b/raphtory/src/db/graph/views/valid_graph.rs @@ -100,7 +100,7 @@ mod tests { }; use itertools::Itertools; use proptest::{arbitrary::any, proptest}; - use raphtory_storage::mutation::addition_ops::InternalAdditionOps; + use raphtory_storage::mutation::addition_ops::{InternalAdditionOps, SessionAdditionOps}; use std::ops::Range; #[test] @@ -156,7 +156,11 @@ mod tests { g.delete_edge(1, 0, 1, None).unwrap(); let gv = g.valid().unwrap(); let expected = PersistentGraph::new(); - expected.resolve_layer(None).unwrap(); + expected + .write_session() + .unwrap() + .resolve_layer(None) + .unwrap(); assert_graph_equal(&gv, &expected); let gm = gv.materialize().unwrap(); assert_graph_equal(&gv, &gm); @@ -307,8 +311,9 @@ mod tests { let gvw = g.valid().unwrap().window(0, 5); assert_eq!(gvw.count_nodes(), 0); let expected = PersistentGraph::new(); - expected.resolve_layer(None).unwrap(); - expected.resolve_layer(Some("a")).unwrap(); + let session = expected.write_session().unwrap(); + session.resolve_layer(None).unwrap(); + session.resolve_layer(Some("a")).unwrap(); assert_graph_equal(&gvw, &expected); let gvwm = gvw.materialize().unwrap(); assert_graph_equal(&gvw, &gvwm); diff --git a/raphtory/src/db/graph/views/window_graph.rs b/raphtory/src/db/graph/views/window_graph.rs index 712f8f8b07..1c3ad4aa88 100644 --- a/raphtory/src/db/graph/views/window_graph.rs +++ b/raphtory/src/db/graph/views/window_graph.rs @@ -68,7 +68,7 @@ use raphtory_api::{ storage::{arc_str::ArcStr, timeindex::TimeIndexEntry}, }, inherit::Base, - iter::{BoxedLDIter, IntoDynDBoxed}, + iter::IntoDynDBoxed, }; use raphtory_storage::{ core_ops::{CoreGraphOps, InheritCoreGraphOps}, diff --git a/raphtory/src/io/arrow/df_loaders.rs b/raphtory/src/io/arrow/df_loaders.rs index 2b8462c238..8fd25ee156 100644 --- a/raphtory/src/io/arrow/df_loaders.rs +++ b/raphtory/src/io/arrow/df_loaders.rs @@ -21,6 +21,7 @@ use raphtory_api::{ Direction, }, }; +use raphtory_storage::mutation::addition_ops::SessionAdditionOps; use rayon::prelude::*; use std::{collections::HashMap, sync::atomic::Ordering}; @@ -77,9 +78,10 @@ pub(crate) fn load_nodes_from_df< let node_id_index = df_view.get_index(node_id)?; let time_index = df_view.get_index(time)?; + let session = graph.write_session().map_err(into_graph_err)?; let shared_constant_properties = process_shared_properties(shared_constant_properties, |key, dtype| { - graph + session .resolve_node_property(key, dtype, true) .map_err(into_graph_err) })?; @@ -98,13 +100,13 @@ pub(crate) fn load_nodes_from_df< .collect::>() }); - let mut start_id = graph + let mut start_id = session .reserve_event_ids(df_view.num_rows) .map_err(into_graph_err)?; for chunk in df_view.chunks { let df = chunk?; let prop_cols = combine_properties(properties, &properties_indices, &df, |key, dtype| { - graph + session .resolve_node_property(key, dtype, false) .map_err(into_graph_err) })?; @@ -113,7 +115,7 @@ pub(crate) fn load_nodes_from_df< &constant_properties_indices, &df, |key, dtype| { - graph + session .resolve_node_property(key, dtype, true) .map_err(into_graph_err) }, @@ -246,9 +248,10 @@ pub(crate) fn load_edges_from_df< } else { None }; + let session = graph.write_session().map_err(into_graph_err)?; let shared_constant_properties = process_shared_properties(shared_constant_properties, |key, dtype| { - graph + session .resolve_edge_property(key, dtype, true) .map_err(into_graph_err) })?; @@ -257,7 +260,7 @@ pub(crate) fn load_edges_from_df< let mut pb = build_progress_bar("Loading edges".to_string(), df_view.num_rows)?; #[cfg(feature = "python")] let _ = pb.update(0); - let mut start_idx = graph + let mut start_idx = session .reserve_event_ids(df_view.num_rows) .map_err(into_graph_err)?; @@ -276,7 +279,7 @@ pub(crate) fn load_edges_from_df< for chunk in df_view.chunks { let df = chunk?; let prop_cols = combine_properties(properties, &properties_indices, &df, |key, dtype| { - graph + session .resolve_edge_property(key, dtype, false) .map_err(into_graph_err) })?; @@ -285,7 +288,7 @@ pub(crate) fn load_edges_from_df< &constant_properties_indices, &df, |key, dtype| { - graph + session .resolve_edge_property(key, dtype, true) .map_err(into_graph_err) }, @@ -494,7 +497,8 @@ pub(crate) fn load_edge_deletions_from_df< let layer_index = layer_index.transpose()?; #[cfg(feature = "python")] let mut pb = build_progress_bar("Loading edge deletions".to_string(), df_view.num_rows)?; - let mut start_idx = graph + let session = graph.write_session().map_err(into_graph_err)?; + let mut start_idx = session .reserve_event_ids(df_view.num_rows) .map_err(into_graph_err)?; @@ -547,10 +551,11 @@ pub(crate) fn load_node_props_from_df< let node_type_index = node_type_index.transpose()?; let node_id_index = df_view.get_index(node_id)?; + let session = graph.write_session().map_err(into_graph_err)?; let shared_constant_properties = process_shared_properties(shared_constant_properties, |key, dtype| { - graph + session .resolve_node_property(key, dtype, true) .map_err(into_graph_err) })?; @@ -576,7 +581,7 @@ pub(crate) fn load_node_props_from_df< &constant_properties_indices, &df, |key, dtype| { - graph + session .resolve_node_property(key, dtype, true) .map_err(into_graph_err) }, @@ -674,9 +679,10 @@ pub(crate) fn load_edges_props_from_df< } else { None }; + let session = graph.write_session().map_err(into_graph_err)?; let shared_constant_properties = process_shared_properties(shared_const_properties, |key, dtype| { - graph + session .resolve_edge_property(key, dtype, true) .map_err(into_graph_err) })?; @@ -707,7 +713,7 @@ pub(crate) fn load_edges_props_from_df< &constant_properties_indices, &df, |key, dtype| { - graph + session .resolve_edge_property(key, dtype, true) .map_err(into_graph_err) }, @@ -844,15 +850,16 @@ pub(crate) fn load_graph_props_from_df< #[cfg(feature = "python")] let mut pb = build_progress_bar("Loading graph properties".to_string(), df_view.num_rows)?; + let session = graph.write_session().map_err(into_graph_err)?; - let mut start_id = graph + let mut start_id = session .reserve_event_ids(df_view.num_rows) .map_err(into_graph_err)?; for chunk in df_view.chunks { let df = chunk?; let prop_cols = combine_properties(properties, &properties_indices, &df, |key, dtype| { - graph + session .resolve_graph_property(key, dtype, false) .map_err(into_graph_err) })?; @@ -861,7 +868,7 @@ pub(crate) fn load_graph_props_from_df< &constant_properties_indices, &df, |key, dtype| { - graph + session .resolve_graph_property(key, dtype, true) .map_err(into_graph_err) }, diff --git a/raphtory/src/io/arrow/layer_col.rs b/raphtory/src/io/arrow/layer_col.rs index 82dc7a4f83..c670b5935c 100644 --- a/raphtory/src/io/arrow/layer_col.rs +++ b/raphtory/src/io/arrow/layer_col.rs @@ -4,6 +4,7 @@ use crate::{ prelude::AdditionOps, }; use polars_arrow::array::{StaticArray, Utf8Array, Utf8ViewArray}; +use raphtory_storage::mutation::addition_ops::SessionAdditionOps; use rayon::{ iter::{ plumbing::{Consumer, ProducerCallback, UnindexedConsumer}, @@ -104,9 +105,10 @@ impl<'a> LayerCol<'a> { self, graph: &(impl AdditionOps + Send + Sync), ) -> Result, GraphError> { + let session = graph.write_session().map_err(|err| err.into())?; match self { LayerCol::Name { name, len } => { - let layer = graph.resolve_layer(name).map_err(into_graph_err)?.inner(); + let layer = session.resolve_layer(name).map_err(into_graph_err)?.inner(); Ok(vec![layer; len]) } col => { @@ -114,7 +116,10 @@ impl<'a> LayerCol<'a> { let mut res = vec![0usize; iter.len()]; iter.zip(res.par_iter_mut()) .try_for_each(|(layer, entry)| { - let layer = graph.resolve_layer(layer).map_err(into_graph_err)?.inner(); + let layer = session + .resolve_layer(layer) + .map_err(into_graph_err)? + .inner(); *entry = layer; Ok::<(), GraphError>(()) })?; diff --git a/raphtory/src/lib.rs b/raphtory/src/lib.rs index 45870737ee..016b49725b 100644 --- a/raphtory/src/lib.rs +++ b/raphtory/src/lib.rs @@ -167,7 +167,7 @@ mod test_utils { use proptest::{arbitrary::any, prelude::*}; use proptest_derive::Arbitrary; use raphtory_api::core::entities::properties::prop::{PropType, DECIMAL_MAX}; - use raphtory_storage::mutation::addition_ops::InternalAdditionOps; + use raphtory_storage::mutation::addition_ops::{InternalAdditionOps, SessionAdditionOps}; use std::{collections::HashMap, sync::Arc}; #[cfg(feature = "storage")] use tempfile::TempDir; @@ -665,15 +665,20 @@ mod test_utils { let g = Arc::new(Storage::default()); let layers = layers.into(); + let session = g.write_session().unwrap(); for ((src, dst, layer), updates) in graph_fix.edges() { // properties always exist in the graph for (_, props) in updates.props.t_props.iter() { for (key, value) in props { - g.resolve_edge_property(key, value.dtype(), false).unwrap(); + session + .resolve_edge_property(key, value.dtype(), false) + .unwrap(); } } for (key, value) in updates.props.c_props.iter() { - g.resolve_edge_property(key, value.dtype(), true).unwrap(); + session + .resolve_edge_property(key, value.dtype(), true) + .unwrap(); } if layers.contains(layer.unwrap_or("_default")) { From 22e36bad0de58e89d453966337c42059389c3fcd Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Tue, 10 Jun 2025 17:40:41 +0100 Subject: [PATCH 004/321] refactor InternalAdditionOps to be used with db4 --- db4-common/Cargo.toml | 1 + db4-common/src/lib.rs | 3 + db4-graph/Cargo.toml | 3 + db4-graph/src/lib.rs | 152 ++++++++++- db4-storage/src/lib.rs | 9 +- db4-storage/src/loaders/mod.rs | 7 +- db4-storage/src/pages/edge_page/writer.rs | 6 +- db4-storage/src/pages/session.rs | 11 +- .../src/properties/props_meta_writer.rs | 2 +- db4-storage/src/segments/edge.rs | 4 +- db4-storage/src/segments/node.rs | 4 +- .../src/entities/graph/logical_to_physical.rs | 24 ++ raphtory-storage/src/mutation/addition_ops.rs | 248 ++++++++++++++---- raphtory/src/db/api/mutation/addition_ops.rs | 19 +- raphtory/src/db/api/mutation/deletion_ops.rs | 8 +- raphtory/src/db/api/mutation/import_ops.rs | 13 +- raphtory/src/db/api/storage/storage.rs | 113 +++++--- .../api/view/exploded_edge_property_filter.rs | 9 +- raphtory/src/db/graph/edge.rs | 4 +- raphtory/src/db/graph/graph.rs | 4 +- raphtory/src/db/graph/node.rs | 3 +- raphtory/src/db/graph/views/deletion_graph.rs | 6 +- .../views/filter/node_type_filtered_graph.rs | 6 +- raphtory/src/db/graph/views/node_subgraph.rs | 2 - raphtory/src/db/graph/views/valid_graph.rs | 9 +- raphtory/src/io/arrow/layer_col.rs | 5 +- 26 files changed, 503 insertions(+), 172 deletions(-) diff --git a/db4-common/Cargo.toml b/db4-common/Cargo.toml index b342de445a..17feeafe45 100644 --- a/db4-common/Cargo.toml +++ b/db4-common/Cargo.toml @@ -12,6 +12,7 @@ edition = "2024" [dependencies] raphtory.workspace = true +raphtory-storage.workspace = true thiserror.workspace = true serde_json.workspace = true arrow-schema.workspace = true diff --git a/db4-common/src/lib.rs b/db4-common/src/lib.rs index f73b972e98..8afdcbcb69 100644 --- a/db4-common/src/lib.rs +++ b/db4-common/src/lib.rs @@ -9,6 +9,7 @@ pub mod error { api::core::entities::properties::prop::PropError, core::utils::time::ParseTimeError, errors::LoadError, }; + use raphtory_storage::mutation::MutationError; #[derive(thiserror::Error, Debug)] pub enum DBV4Error { @@ -34,6 +35,8 @@ pub mod error { #[from] source: ParseTimeError, }, + #[error("Failed to mutate: {0}")] + MutationError(#[from] MutationError), #[error("Unnamed Failure: {0}")] GenericFailure(String), } diff --git a/db4-graph/Cargo.toml b/db4-graph/Cargo.toml index faf15a7079..4b06af31f0 100644 --- a/db4-graph/Cargo.toml +++ b/db4-graph/Cargo.toml @@ -15,3 +15,6 @@ edition.workspace = true boxcar.workspace = true storage.workspace = true raphtory-api.workspace = true +raphtory-storage.workspace = true +db4-common.workspace = true +parking_lot.workspace = true diff --git a/db4-graph/src/lib.rs b/db4-graph/src/lib.rs index c0d3cb461d..532567f767 100644 --- a/db4-graph/src/lib.rs +++ b/db4-graph/src/lib.rs @@ -1,10 +1,156 @@ use std::sync::Arc; +use db4_common::error::DBV4Error; +use parking_lot::RwLockWriteGuard; use raphtory_api::core::entities::properties::meta::Meta; -use storage::Layer; +use raphtory_storage::mutation::addition_ops::{InternalAdditionOps, SessionAdditionOps}; +use storage::{pages::session::WriteSession, segments::node::MemNodeSegment, Layer, ES, NS}; -pub struct TemporalGraph { - layers: boxcar::Vec>, +pub struct TemporalGraph { + layers: boxcar::Vec>, edge_meta: Arc, node_meta: Arc, } + +pub type WriteS<'a, MNS, MES, EXT> = WriteSession<'a, MNS, MES, NS, ES, EXT>; + +pub struct UnlockedSession<'a, EXT> { + graph: &'a TemporalGraph, +} + +impl <'a, EXT: Send + Sync> SessionAdditionOps for UnlockedSession<'a, EXT> { + type Error = DBV4Error; + + fn next_event_id(&self) -> Result { + todo!() + } + + fn reserve_event_ids(&self, num_ids: usize) -> Result { + todo!() + } + + fn set_node(&self, gid: raphtory_api::core::entities::GidRef, vid: raphtory_api::core::entities::VID) -> Result<(), Self::Error> { + todo!() + } + + fn resolve_graph_property( + &self, + prop: &str, + dtype: raphtory_api::core::entities::properties::prop::PropType, + is_static: bool, + ) -> Result, Self::Error> { + todo!() + } + + fn resolve_node_property( + &self, + prop: &str, + dtype: raphtory_api::core::entities::properties::prop::PropType, + is_static: bool, + ) -> Result, Self::Error> { + todo!() + } + + fn resolve_edge_property( + &self, + prop: &str, + dtype: raphtory_api::core::entities::properties::prop::PropType, + is_static: bool, + ) -> Result, Self::Error> { + todo!() + } + + fn internal_add_node( + &self, + t: raphtory_api::core::storage::timeindex::TimeIndexEntry, + v: raphtory_api::core::entities::VID, + props: &[(usize, raphtory_api::core::entities::properties::prop::Prop)], + ) -> Result<(), Self::Error> { + todo!() + } + + fn internal_add_edge( + &self, + t: raphtory_api::core::storage::timeindex::TimeIndexEntry, + src: raphtory_api::core::entities::VID, + dst: raphtory_api::core::entities::VID, + props: &[(usize, raphtory_api::core::entities::properties::prop::Prop)], + layer: usize, + ) -> Result, Self::Error> { + todo!() + } + + fn internal_add_edge_update( + &self, + t: raphtory_api::core::storage::timeindex::TimeIndexEntry, + edge: raphtory_api::core::entities::EID, + props: &[(usize, raphtory_api::core::entities::properties::prop::Prop)], + layer: usize, + ) -> Result<(), Self::Error> { + todo!() + } + +} + +impl InternalAdditionOps for TemporalGraph { + type Error = DBV4Error; + + type WS<'a> = UnlockedSession<'a, EXT> where EXT: 'a; + + type AtomicAddEdge<'a> = WriteS, RwLockWriteGuard, EXT>; + + fn write_lock(&self) -> Result { + todo!() + } + + fn write_lock_nodes(&self) -> Result { + todo!() + } + + fn write_lock_edges(&self) -> Result { + todo!() + } + + fn resolve_layer(&self, layer: Option<&str>) -> Result, Self::Error> { + todo!() + } + + fn resolve_node(&self, id: NodeRef) -> Result, Self::Error> { + todo!() + } + + fn resolve_node_and_type( + &self, + id: NodeRef, + node_type: &str, + ) -> Result, raphtory_api::core::storage::dict_mapper::MaybeNew)>, Self::Error> { + todo!() + } + + fn validate_gids<'a>( + &self, + gids: impl IntoIterator>, + ) -> Result<(), Self::Error> { + todo!() + } + + fn write_session(&self) -> Result, Self::Error> { + todo!() + } + + fn atomic_add_edge( + &self, + src: raphtory_api::core::entities::VID, + dst: raphtory_api::core::entities::VID, + e_id: Option, + ) -> Result, Self::Error> { + todo!() + } + + fn validate_prop>( + &self, + prop: impl ExactSizeIterator, + ) -> Result, Self::Error> { + todo!() + } +} \ No newline at end of file diff --git a/db4-storage/src/lib.rs b/db4-storage/src/lib.rs index 6a3f80b347..23f2307b6a 100644 --- a/db4-storage/src/lib.rs +++ b/db4-storage/src/lib.rs @@ -10,12 +10,8 @@ use crate::{ }; use db4_common::{LocalPOS, error::DBV4Error}; use parking_lot::{RwLockReadGuard, RwLockWriteGuard}; -use raphtory::{ - core::entities::{EID, VID}, - prelude::Prop, -}; use raphtory_api::core::{ - entities::properties::{meta::Meta, tprop::TPropOps}, + entities::{ properties::{meta::Meta, tprop::TPropOps, prop::Prop}, VID, EID}, storage::timeindex::{TimeIndexEntry, TimeIndexOps}, }; use segments::{edge::MemEdgeSegment, node::MemNodeSegment}; @@ -25,6 +21,9 @@ pub mod pages; pub mod properties; pub mod segments; +pub type NS

= NodeSegmentView

; +pub type ES

= EdgeSegmentView

; + pub type Layer = GraphStore; pub trait EdgeSegmentOps: Send + Sync { diff --git a/db4-storage/src/loaders/mod.rs b/db4-storage/src/loaders/mod.rs index e2f928dead..0f3a55545f 100644 --- a/db4-storage/src/loaders/mod.rs +++ b/db4-storage/src/loaders/mod.rs @@ -384,7 +384,7 @@ impl<'a> Loader<'a> { let time_col = rb.time(); - edge_writers.iter_mut().try_for_each(|edge_writer| { + edge_writers.iter_mut().for_each(|edge_writer| { for (row_idx, ((((&src, &dst), &eid), edge_exists), time)) in src_col_resolved .iter() .zip(&dst_col_resolved) @@ -408,11 +408,10 @@ impl<'a> Loader<'a> { props.iter_row(row_idx), 0, Some(edge_exists), - )?; + ); } } - Ok::<_, DBV4Error>(()) - })?; + }); src_col_resolved.clear(); dst_col_resolved.clear(); diff --git a/db4-storage/src/pages/edge_page/writer.rs b/db4-storage/src/pages/edge_page/writer.rs index 106aefe975..3fff89663b 100644 --- a/db4-storage/src/pages/edge_page/writer.rs +++ b/db4-storage/src/pages/edge_page/writer.rs @@ -35,7 +35,7 @@ impl<'a, MP: DerefMut, ES: EdgeSegmentOps> EdgeWriter<' props: impl IntoIterator, lsn: u64, exists_hint: Option, // used when edge_pos is Some but the is not counted, this is used in the bulk loader - ) -> Result { + ) -> LocalPOS { self.writer.as_mut().set_lsn(lsn); if exists_hint == Some(false) && edge_pos.is_some() { @@ -45,9 +45,7 @@ impl<'a, MP: DerefMut, ES: EdgeSegmentOps> EdgeWriter<' let edge_pos = edge_pos.unwrap_or_else(|| self.new_local_pos()); self.writer .insert_edge_internal(t, edge_pos, src, dst, props); - // self.est_size = self.page.increment_size(size_of::<(VID, VID)>()) - // + self.writer.as_ref().t_prop_est_size(); - Ok(edge_pos) + edge_pos } pub fn add_static_edge( diff --git a/db4-storage/src/pages/session.rs b/db4-storage/src/pages/session.rs index ebbb3768c5..4934915114 100644 --- a/db4-storage/src/pages/session.rs +++ b/db4-storage/src/pages/session.rs @@ -59,7 +59,7 @@ impl< edge: MaybeNew, lsn: u64, props: impl IntoIterator, - ) -> Result<(), DBV4Error> { + ) { let src = src.into(); let dst = dst.into(); let e_id = edge.inner(); @@ -82,7 +82,7 @@ impl< let (_, edge_pos) = resolve_pos(e_id.edge, edge_max_page_len); let exists = Some(!edge.is_new()); - edge_writer.add_edge(t, Some(edge_pos), src, dst, props, lsn, exists)?; + edge_writer.add_edge(t, Some(edge_pos), src, dst, props, lsn, exists); edge.if_new(|edge_id| { self.node_writers @@ -101,7 +101,6 @@ impl< .get_mut_dst() .update_timestamp(t, dst_pos, e_id, lsn); - Ok(()) } pub fn add_static_edge( @@ -172,7 +171,7 @@ impl< if let Some(e_id) = self.node_writers.get_mut_src().get_out_edge(src_pos, dst) { let mut edge_writer = self.graph.edge_writer(e_id); let (_, edge_pos) = self.graph.edges().resolve_pos(e_id); - edge_writer.add_edge(t, Some(edge_pos), src, dst, props, lsn, None)?; + edge_writer.add_edge(t, Some(edge_pos), src, dst, props, lsn, None); let e_id = e_id.with_layer(layer); self.node_writers @@ -189,7 +188,7 @@ impl< let (_, edge_pos) = self.graph.edges().resolve_pos(e_id); let e_id = e_id.with_layer(layer); - edge_writer.add_edge(t, Some(edge_pos), src, dst, props, lsn, None)?; + edge_writer.add_edge(t, Some(edge_pos), src, dst, props, lsn, None); self.node_writers .get_mut_src() .update_timestamp(t, src_pos, e_id, lsn); @@ -200,7 +199,7 @@ impl< Ok(MaybeNew::Existing(e_id)) } else { let mut edge_writer = self.graph.get_free_writer(); - let edge_id = edge_writer.add_edge(t, None, src, dst, props, lsn, None)?; + let edge_id = edge_writer.add_edge(t, None, src, dst, props, lsn, None); let edge_id = edge_id.as_eid(edge_writer.segment_id(), self.graph.edges().max_page_len()); let edge_id = edge_id.with_layer(layer); diff --git a/db4-storage/src/properties/props_meta_writer.rs b/db4-storage/src/properties/props_meta_writer.rs index 7899ec4135..7c0b0de961 100644 --- a/db4-storage/src/properties/props_meta_writer.rs +++ b/db4-storage/src/properties/props_meta_writer.rs @@ -1,7 +1,7 @@ use db4_common::error::DBV4Error; use either::Either; -use raphtory::prelude::Prop; use raphtory_api::core::entities::properties::{ + prop::Prop, meta::{LockedPropMapper, Meta, PropMapper}, prop::unify_types, }; diff --git a/db4-storage/src/segments/edge.rs b/db4-storage/src/segments/edge.rs index 44f54b8d22..b3507dcaa7 100644 --- a/db4-storage/src/segments/edge.rs +++ b/db4-storage/src/segments/edge.rs @@ -145,10 +145,11 @@ impl MemEdgeSegment { } } -pub struct EdgeSegmentView { +pub struct EdgeSegmentView { segment: parking_lot::RwLock, segment_id: usize, num_edges: AtomicUsize, + _ext: EXT, } impl EdgeSegmentOps for EdgeSegmentView { @@ -192,6 +193,7 @@ impl EdgeSegmentOps for EdgeSegmentView { segment: parking_lot::RwLock::new(MemEdgeSegment::new(page_id, max_page_len, meta)), segment_id: page_id, num_edges: AtomicUsize::new(0), + _ext: (), } } diff --git a/db4-storage/src/segments/node.rs b/db4-storage/src/segments/node.rs index bf1801f2ba..460d719445 100644 --- a/db4-storage/src/segments/node.rs +++ b/db4-storage/src/segments/node.rs @@ -209,10 +209,11 @@ impl MemNodeSegment { } } -pub struct NodeSegmentView { +pub struct NodeSegmentView { inner: parking_lot::RwLock, segment_id: usize, num_nodes: AtomicUsize, + _ext: EXT, } impl NodeSegmentOps for NodeSegmentView { @@ -256,6 +257,7 @@ impl NodeSegmentOps for NodeSegmentView { inner: parking_lot::RwLock::new(MemNodeSegment::new(page_id, max_page_len, meta)), segment_id: page_id, num_nodes: AtomicUsize::new(0), + _ext: (), } } diff --git a/raphtory-core/src/entities/graph/logical_to_physical.rs b/raphtory-core/src/entities/graph/logical_to_physical.rs index 6c0754ecb2..4f5b797edf 100644 --- a/raphtory-core/src/entities/graph/logical_to_physical.rs +++ b/raphtory-core/src/entities/graph/logical_to_physical.rs @@ -159,6 +159,30 @@ impl Mapping { } } + pub fn validate_gids<'a>( + &self, + gids: impl IntoIterator>, + ) -> Result<(), InvalidNodeId> { + for gid in gids { + let map = self.map.get_or_init(|| match &gid { + GidRef::U64(_) => Map::U64(FxDashMap::default()), + GidRef::Str(_) => Map::Str(FxDashMap::default()), + }); + match gid { + GidRef::U64(id) => { + map.as_u64() + .ok_or_else(|| InvalidNodeId::InvalidNodeIdU64(id))?; + } + GidRef::Str(id) => { + map.as_str() + .ok_or_else(|| InvalidNodeId::InvalidNodeIdStr(id.into()))?; + } + } + } + + Ok(()) + } + #[inline] pub fn get_str(&self, gid: &str) -> Option { let map = self.map.get()?; diff --git a/raphtory-storage/src/mutation/addition_ops.rs b/raphtory-storage/src/mutation/addition_ops.rs index 48cc8fa543..31d4c501c6 100644 --- a/raphtory-storage/src/mutation/addition_ops.rs +++ b/raphtory-storage/src/mutation/addition_ops.rs @@ -23,29 +23,52 @@ pub trait InternalAdditionOps { type WS<'a>: SessionAdditionOps where Self: 'a; + + type AtomicAddEdge<'a>: Send + Sync + where + Self: 'a; + fn write_lock(&self) -> Result; fn write_lock_nodes(&self) -> Result; fn write_lock_edges(&self) -> Result; - - fn write_session(&self) -> Result, Self::Error>; -} - -pub trait SessionAdditionOps: Send + Sync { - type Error: From; - /// get the sequence id for the next event - fn next_event_id(&self) -> Result; - fn reserve_event_ids(&self, num_ids: usize) -> Result; /// map layer name to id and allocate a new layer if needed fn resolve_layer(&self, layer: Option<&str>) -> Result, Self::Error>; /// map external node id to internal id, allocating a new empty node if needed fn resolve_node(&self, id: NodeRef) -> Result, Self::Error>; - fn set_node(&self, gid: GidRef, vid: VID) -> Result<(), Self::Error>; /// resolve a node and corresponding type, outer MaybeNew tracks whether the type assignment is new for the node even if both node and type already existed. fn resolve_node_and_type( &self, id: NodeRef, node_type: &str, ) -> Result, MaybeNew)>, Self::Error>; + + /// validate the GidRef is the correct type + fn validate_gids<'a>( + &self, + gids: impl IntoIterator>, + ) -> Result<(), Self::Error>; + + fn write_session(&self) -> Result, Self::Error>; + + fn atomic_add_edge( + &self, + src: VID, + dst: VID, + e_id: Option, + ) -> Result, Self::Error>; + + fn validate_prop>( + &self, + prop: impl ExactSizeIterator, + ) -> Result, Self::Error>; +} + +pub trait SessionAdditionOps: Send + Sync { + type Error: From; + /// get the sequence id for the next event + fn next_event_id(&self) -> Result; + fn reserve_event_ids(&self, num_ids: usize) -> Result; + fn set_node(&self, gid: GidRef, vid: VID) -> Result<(), Self::Error>; /// map property key to internal id, allocating new property if needed fn resolve_graph_property( &self, @@ -110,49 +133,10 @@ impl<'a> SessionAdditionOps for TGWriteSession<'a> { Ok(self.tg.event_counter.fetch_add(num_ids, Ordering::Relaxed)) } - /// map layer name to id and allocate a new layer if needed - fn resolve_layer(&self, layer: Option<&str>) -> Result, Self::Error> { - let id = self - .tg - .resolve_layer_inner(layer) - .map_err(MutationError::from)?; - Ok(id) - } - - /// map external node id to internal id, allocating a new empty node if needed - fn resolve_node(&self, id: NodeRef) -> Result, Self::Error> { - Ok(self.tg.resolve_node_inner(id)?) - } - fn set_node(&self, gid: GidRef, vid: VID) -> Result<(), Self::Error> { Ok(self.tg.logical_to_physical.set(gid, vid)?) } - /// resolve a node and corresponding type, outer MaybeNew tracks whether the type assignment is new for the node even if both node and type already existed. - fn resolve_node_and_type( - &self, - id: NodeRef, - node_type: &str, - ) -> Result, MaybeNew)>, Self::Error> { - let vid = self.resolve_node(id)?; - let mut entry = self.tg.storage.get_node_mut(vid.inner()); - let mut entry_ref = entry.to_mut(); - let node_store = entry_ref.node_store_mut(); - if node_store.node_type == 0 { - let node_type_id = self.tg.node_meta.get_or_create_node_type_id(node_type); - node_store.update_node_type(node_type_id.inner()); - Ok(MaybeNew::New((vid, node_type_id))) - } else { - let node_type_id = self - .tg - .node_meta - .get_node_type_id(node_type) - .filter(|&node_type| node_type == node_store.node_type) - .ok_or(MutationError::NodeTypeError)?; - Ok(MaybeNew::Existing((vid, MaybeNew::Existing(node_type_id)))) - } - } - /// map property key to internal id, allocating new property if needed fn resolve_graph_property( &self, @@ -261,6 +245,7 @@ impl<'a> SessionAdditionOps for TGWriteSession<'a> { impl InternalAdditionOps for GraphStorage { type Error = MutationError; type WS<'b> = TGWriteSession<'b>; + type AtomicAddEdge<'a> = TGWriteSession<'a>; fn write_lock(&self) -> Result { self.mutable()?.write_lock() @@ -274,17 +259,60 @@ impl InternalAdditionOps for GraphStorage { self.mutable()?.write_lock_edges() } + fn resolve_layer(&self, layer: Option<&str>) -> Result, Self::Error> { + let id = self.mutable()?.resolve_layer_inner(layer)?; + Ok(id) + } + + fn resolve_node(&self, id: NodeRef) -> Result, Self::Error> { + Ok(self.mutable()?.resolve_node(id)?) + } + + fn resolve_node_and_type( + &self, + id: NodeRef, + node_type: &str, + ) -> Result, MaybeNew)>, Self::Error> { + Ok(self.mutable()?.resolve_node_and_type(id, node_type)?) + } + fn write_session(&self) -> Result, Self::Error> { Ok(TGWriteSession { tg: self.mutable()?, }) } + + fn atomic_add_edge( + &self, + _src: VID, + _dst: VID, + _e_id: Option, + ) -> Result, Self::Error> { + self.write_session() + } + + fn validate_prop>( + &self, + prop: impl ExactSizeIterator, + ) -> Result, Self::Error> { + self.mutable()? + .validate_prop(prop) + .map_err(MutationError::from) + } + + fn validate_gids<'a>( + &self, + gids: impl IntoIterator>, + ) -> Result<(), Self::Error> { + Ok(self.mutable()?.validate_gids(gids)?) + } } impl InternalAdditionOps for TemporalGraph { type Error = MutationError; type WS<'b> = TGWriteSession<'b>; + type AtomicAddEdge<'a> = TGWriteSession<'a>; fn write_lock(&self) -> Result { Ok(WriteLockedGraph::new(self)) @@ -298,9 +326,80 @@ impl InternalAdditionOps for TemporalGraph { Ok(self.storage.edges.write_lock()) } + /// map layer name to id and allocate a new layer if needed + fn resolve_layer(&self, layer: Option<&str>) -> Result, Self::Error> { + let id = self + .resolve_layer_inner(layer) + .map_err(MutationError::from)?; + Ok(id) + } + + /// map external node id to internal id, allocating a new empty node if needed + fn resolve_node(&self, id: NodeRef) -> Result, Self::Error> { + Ok(self.resolve_node_inner(id)?) + } + + /// resolve a node and corresponding type, outer MaybeNew tracks whether the type assignment is new for the node even if both node and type already existed. + fn resolve_node_and_type( + &self, + id: NodeRef, + node_type: &str, + ) -> Result, MaybeNew)>, Self::Error> { + let vid = self.resolve_node(id)?; + let mut entry = self.storage.get_node_mut(vid.inner()); + let mut entry_ref = entry.to_mut(); + let node_store = entry_ref.node_store_mut(); + if node_store.node_type == 0 { + let node_type_id = self.node_meta.get_or_create_node_type_id(node_type); + node_store.update_node_type(node_type_id.inner()); + Ok(MaybeNew::New((vid, node_type_id))) + } else { + let node_type_id = self + .node_meta + .get_node_type_id(node_type) + .filter(|&node_type| node_type == node_store.node_type) + .ok_or(MutationError::NodeTypeError)?; + Ok(MaybeNew::Existing((vid, MaybeNew::Existing(node_type_id)))) + } + } + fn write_session(&self) -> Result, Self::Error> { Ok(TGWriteSession { tg: self }) } + + fn atomic_add_edge( + &self, + _src: VID, + _dst: VID, + _e_id: Option, + ) -> Result, Self::Error> { + Ok(TGWriteSession { tg: self }) + } + + fn validate_gids<'a>( + &self, + gids: impl IntoIterator>, + ) -> Result<(), Self::Error> { + self.logical_to_physical + .validate_gids(gids) + .map_err(MutationError::from) + } + + fn validate_prop>( + &self, + prop: impl ExactSizeIterator, + ) -> Result, Self::Error> { + let session = self.write_session()?; + let properties = prop + .map(|(name, prop)| { + let dtype = prop.dtype(); + session + .resolve_node_property(name.as_ref(), dtype, false) + .map(|id| (id.inner(), prop)) + }) + .collect::, _>>()?; + Ok(properties) + } } pub trait InheritAdditionOps: Base {} @@ -316,6 +415,12 @@ where ::Base: 'a, G: 'a; + type AtomicAddEdge<'a> + = ::AtomicAddEdge<'a> + where + ::Base: 'a, + G: 'a; + #[inline] fn write_lock(&self) -> Result { self.base().write_lock() @@ -331,8 +436,53 @@ where self.base().write_lock_edges() } + #[inline] + fn resolve_layer(&self, layer: Option<&str>) -> Result, Self::Error> { + self.base().resolve_layer(layer) + } + + #[inline] + fn resolve_node(&self, id: NodeRef) -> Result, Self::Error> { + self.base().resolve_node(id) + } + + #[inline] + fn resolve_node_and_type( + &self, + id: NodeRef, + node_type: &str, + ) -> Result, MaybeNew)>, Self::Error> { + self.base().resolve_node_and_type(id, node_type) + } + #[inline] fn write_session(&self) -> Result, Self::Error> { self.base().write_session() } + + #[inline] + fn atomic_add_edge( + &self, + src: VID, + dst: VID, + e_id: Option, + ) -> Result, Self::Error> { + self.base().atomic_add_edge(src, dst, e_id) + } + + #[inline] + fn validate_prop>( + &self, + prop: impl ExactSizeIterator, + ) -> Result, Self::Error> { + self.base().validate_prop(prop) + } + + #[inline] + fn validate_gids<'a>( + &self, + gids: impl IntoIterator>, + ) -> Result<(), Self::Error> { + self.base().validate_gids(gids) + } } diff --git a/raphtory/src/db/api/mutation/addition_ops.rs b/raphtory/src/db/api/mutation/addition_ops.rs index aae1b48035..a1b1dc3c6a 100644 --- a/raphtory/src/db/api/mutation/addition_ops.rs +++ b/raphtory/src/db/api/mutation/addition_ops.rs @@ -1,4 +1,3 @@ -use super::time_from_input; use crate::{ core::{ entities::{edges::edge_ref::EdgeRef, nodes::node_ref::AsNodeRef}, @@ -123,7 +122,7 @@ impl> + StaticGraphViewOps> Addit node_type: Option<&str>, ) -> Result, GraphError> { let session = self.write_session().map_err(|err| err.into())?; - let ti = time_from_input(self, t)?; + let ti = time_from_input_session(&session, t)?; let properties = props.collect_properties(|name, dtype| { Ok(session .resolve_node_property(name, dtype, false) @@ -131,12 +130,12 @@ impl> + StaticGraphViewOps> Addit .inner()) })?; let v_id = match node_type { - None => session + None => self .resolve_node(v.as_node_ref()) .map_err(into_graph_err)? .inner(), Some(node_type) => { - let (v_id, _) = session + let (v_id, _) = self .resolve_node_and_type(v.as_node_ref(), node_type) .map_err(into_graph_err)? .inner(); @@ -156,14 +155,14 @@ impl> + StaticGraphViewOps> Addit props: PI, node_type: Option<&str>, ) -> Result, GraphError> { - let ti = time_from_input(self, t)?; let session = self.write_session().map_err(|err| err.into())?; + let ti = time_from_input_session(&session, t)?; let v_id = match node_type { - None => session + None => self .resolve_node(v.as_node_ref()) .map_err(into_graph_err)?, Some(node_type) => { - let (v_id, _) = session + let (v_id, _) = self .resolve_node_and_type(v.as_node_ref(), node_type) .map_err(into_graph_err)? .inner(); @@ -200,15 +199,15 @@ impl> + StaticGraphViewOps> Addit ) -> Result, GraphError> { let session = self.write_session().map_err(|err| err.into())?; let ti = time_from_input_session(&session, t)?; - let src_id = session + let src_id = self .resolve_node(src.as_node_ref()) .map_err(into_graph_err)? .inner(); - let dst_id = session + let dst_id = self .resolve_node(dst.as_node_ref()) .map_err(into_graph_err)? .inner(); - let layer_id = session + let layer_id = self .resolve_layer(layer) .map_err(into_graph_err)? .inner(); diff --git a/raphtory/src/db/api/mutation/deletion_ops.rs b/raphtory/src/db/api/mutation/deletion_ops.rs index 17dad3db98..9269368c6f 100644 --- a/raphtory/src/db/api/mutation/deletion_ops.rs +++ b/raphtory/src/db/api/mutation/deletion_ops.rs @@ -11,7 +11,7 @@ use crate::{ }; use raphtory_api::core::entities::edges::edge_ref::EdgeRef; use raphtory_storage::mutation::{ - addition_ops::{InternalAdditionOps, SessionAdditionOps}, + addition_ops::InternalAdditionOps, deletion_ops::InternalDeletionOps, }; @@ -30,15 +30,15 @@ pub trait DeletionOps: ) -> Result, GraphError> { let session = self.write_session().map_err(|err| err.into())?; let ti = time_from_input_session(&session, t).map_err(into_graph_err)?; - let src_id = session + let src_id = self .resolve_node(src.as_node_ref()) .map_err(into_graph_err)? .inner(); - let dst_id = session + let dst_id = self .resolve_node(dst.as_node_ref()) .map_err(into_graph_err)? .inner(); - let layer = session + let layer = self .resolve_layer(layer) .map_err(into_graph_err)? .inner(); diff --git a/raphtory/src/db/api/mutation/import_ops.rs b/raphtory/src/db/api/mutation/import_ops.rs index 149273d3a9..cc71d0d6c0 100644 --- a/raphtory/src/db/api/mutation/import_ops.rs +++ b/raphtory/src/db/api/mutation/import_ops.rs @@ -332,18 +332,17 @@ fn import_node_internal< } } - let session = graph.write_session().map_err(|err| err.into())?; - let node_internal = match node.node_type().as_str() { - None => session.resolve_node(id).map_err(into_graph_err)?.inner(), + None => graph.resolve_node(id).map_err(into_graph_err)?.inner(), Some(node_type) => { - let (node_internal, _) = session + let (node_internal, _) = graph .resolve_node_and_type(id, node_type) .map_err(into_graph_err)? .inner(); node_internal.inner() } }; + let session = graph.write_session().map_err(|err| err.into())?; let keys = node.temporal_prop_keys().collect::>(); for (t, row) in node.rows() { @@ -412,15 +411,15 @@ fn import_edge_internal< for (t, _) in edge.deletions_hist() { let ti = time_from_input_session(&session, t.t())?; - let src_node = session + let src_node = graph .resolve_node(src_id) .map_err(into_graph_err)? .inner(); - let dst_node = session + let dst_node = graph .resolve_node(dst_id) .map_err(into_graph_err)? .inner(); - let layer = session + let layer = graph .resolve_layer(Some(&layer_name)) .map_err(into_graph_err)? .inner(); diff --git a/raphtory/src/db/api/storage/storage.rs b/raphtory/src/db/api/storage/storage.rs index 1027f68530..5741649547 100644 --- a/raphtory/src/db/api/storage/storage.rs +++ b/raphtory/src/db/api/storage/storage.rs @@ -218,53 +218,10 @@ impl<'a> SessionAdditionOps for StorageWriteSession<'a> { Ok(self.session.reserve_event_ids(num_ids)?) } - fn resolve_layer(&self, layer: Option<&str>) -> Result, Self::Error> { - let id = self.session.resolve_layer(layer)?; - - #[cfg(feature = "proto")] - self.storage - .if_cache(|cache| cache.resolve_layer(layer, id)); - - Ok(id) - } - - fn resolve_node(&self, id: NodeRef) -> Result, Self::Error> { - match id { - NodeRef::Internal(id) => Ok(MaybeNew::Existing(id)), - NodeRef::External(gid) => { - let id = self.session.resolve_node(id)?; - - #[cfg(feature = "proto")] - self.storage.if_cache(|cache| cache.resolve_node(id, gid)); - - Ok(id) - } - } - } - fn set_node(&self, gid: GidRef, vid: VID) -> Result<(), Self::Error> { Ok(self.session.set_node(gid, vid)?) } - fn resolve_node_and_type( - &self, - id: NodeRef, - node_type: &str, - ) -> Result, MaybeNew)>, Self::Error> { - let node_and_type = self.session.resolve_node_and_type(id, node_type)?; - - #[cfg(feature = "proto")] - self.storage.if_cache(|cache| { - use raphtory_storage::core_ops::CoreGraphOps; - - let (vid, _) = node_and_type.inner(); - let node_entry = self.storage.core_node(vid.inner()); - cache.resolve_node_and_type(node_and_type, node_type, node_entry.id()) - }); - - Ok(node_and_type) - } - fn resolve_graph_property( &self, prop: &str, @@ -389,6 +346,7 @@ impl InternalAdditionOps for Storage { type Error = GraphError; type WS<'a> = StorageWriteSession<'a>; + type AtomicAddEdge<'a> = StorageWriteSession<'a>; fn write_lock(&self) -> Result { Ok(self.graph.write_lock()?) @@ -402,6 +360,48 @@ impl InternalAdditionOps for Storage { Ok(self.graph.write_lock_edges()?) } + fn resolve_layer(&self, layer: Option<&str>) -> Result, Self::Error> { + let id = self.graph.resolve_layer(layer)?; + + #[cfg(feature = "proto")] + self.if_cache(|cache| cache.resolve_layer(layer, id)); + + Ok(id) + } + + fn resolve_node(&self, id: NodeRef) -> Result, Self::Error> { + match id { + NodeRef::Internal(id) => Ok(MaybeNew::Existing(id)), + NodeRef::External(gid) => { + let id = self.resolve_node(id)?; + + #[cfg(feature = "proto")] + self.if_cache(|cache| cache.resolve_node(id, gid)); + + Ok(id) + } + } + } + + fn resolve_node_and_type( + &self, + id: NodeRef, + node_type: &str, + ) -> Result, MaybeNew)>, Self::Error> { + let node_and_type = self.graph.resolve_node_and_type(id, node_type)?; + + #[cfg(feature = "proto")] + self.if_cache(|cache| { + use raphtory_storage::core_ops::CoreGraphOps; + + let (vid, _) = node_and_type.inner(); + let node_entry = self.core_node(vid.inner()); + cache.resolve_node_and_type(node_and_type, node_type, node_entry.id()) + }); + + Ok(node_and_type) + } + fn write_session(&self) -> Result, Self::Error> { let session = self.graph.write_session()?; Ok(StorageWriteSession { @@ -409,6 +409,33 @@ impl InternalAdditionOps for Storage { storage: self, }) } + + fn atomic_add_edge( + &self, + src: VID, + dst: VID, + e_id: Option, + ) -> Result, Self::Error> { + let session = self.graph.atomic_add_edge(src, dst, e_id)?; + Ok(StorageWriteSession { + session, + storage: self, + }) + } + + fn validate_prop>( + &self, + prop: impl ExactSizeIterator, + ) -> Result, Self::Error> { + Ok(self.graph.validate_prop(prop)?) + } + + fn validate_gids<'a>( + &self, + gids: impl IntoIterator>, + ) -> Result<(), Self::Error> { + Ok(self.graph.validate_gids(gids)?) + } } impl InternalPropertyAdditionOps for Storage { diff --git a/raphtory/src/db/api/view/exploded_edge_property_filter.rs b/raphtory/src/db/api/view/exploded_edge_property_filter.rs index 129a85ad59..5e5cdffac6 100644 --- a/raphtory/src/db/api/view/exploded_edge_property_filter.rs +++ b/raphtory/src/db/api/view/exploded_edge_property_filter.rs @@ -44,7 +44,7 @@ mod test { }, }; use proptest::{arbitrary::any, proptest}; - use raphtory_storage::mutation::addition_ops::{InternalAdditionOps, SessionAdditionOps}; + use raphtory_storage::mutation::addition_ops::InternalAdditionOps; use std::collections::HashMap; fn build_filtered_graph( @@ -68,7 +68,7 @@ mod test { } } if !edges.is_empty() { - g.write_session().unwrap().resolve_layer(None).unwrap(); + g.resolve_layer(None).unwrap(); } g } @@ -79,9 +79,8 @@ mod test { ) -> (PersistentGraph, PersistentGraph) { let g = PersistentGraph::new(); let g_filtered = PersistentGraph::new(); - let session = g_filtered.write_session().unwrap(); if !edges.iter().all(|(_, v)| v.is_empty()) { - session.resolve_layer(None).unwrap(); + g_filtered.resolve_layer(None).unwrap(); } for ((src, dst), mut updates) in edges { let mut keep = false; @@ -165,7 +164,7 @@ mod test { )) .unwrap(); let gf = Graph::new(); - gf.write_session().unwrap().resolve_layer(None).unwrap(); + gf.resolve_layer(None).unwrap(); assert_eq!(filtered.count_nodes(), 0); assert_eq!(filtered.count_edges(), 0); assert_graph_equal(&filtered, &gf); diff --git a/raphtory/src/db/graph/edge.rs b/raphtory/src/db/graph/edge.rs index 76343a3f8e..06cc92f64f 100644 --- a/raphtory/src/db/graph/edge.rs +++ b/raphtory/src/db/graph/edge.rs @@ -301,9 +301,7 @@ impl EdgeView { })?, None => { if create { - self.graph - .write_session() - .and_then(|s| s.resolve_layer(layer)) + self.graph.resolve_layer(layer) .map_err(into_graph_err)? .inner() } else { diff --git a/raphtory/src/db/graph/graph.rs b/raphtory/src/db/graph/graph.rs index 92284ec702..e755ce00f3 100644 --- a/raphtory/src/db/graph/graph.rs +++ b/raphtory/src/db/graph/graph.rs @@ -606,7 +606,7 @@ mod db_tests { use raphtory_core::utils::time::{ParseTimeError, TryIntoTime}; use raphtory_storage::{ core_ops::CoreGraphOps, - mutation::addition_ops::{InternalAdditionOps, SessionAdditionOps}, + mutation::addition_ops::InternalAdditionOps, }; use rayon::join; use std::{ @@ -4039,8 +4039,6 @@ mod db_tests { let expected = Graph::new(); expected.add_edge(0, 0, 0, NO_PROPS, None).unwrap(); expected - .write_session() - .unwrap() .resolve_layer(Some("a")) .unwrap(); assert_graph_equal(&gw, &expected); diff --git a/raphtory/src/db/graph/node.rs b/raphtory/src/db/graph/node.rs index 3f35da4282..bf0cab4eb1 100644 --- a/raphtory/src/db/graph/node.rs +++ b/raphtory/src/db/graph/node.rs @@ -421,8 +421,7 @@ impl NodeView<'static pub fn set_node_type(&self, new_type: &str) -> Result<(), GraphError> { self.graph - .write_session() - .and_then(|s| s.resolve_node_and_type(NodeRef::Internal(self.node), new_type)) + .resolve_node_and_type(NodeRef::Internal(self.node), new_type) .map_err(into_graph_err)?; Ok(()) } diff --git a/raphtory/src/db/graph/views/deletion_graph.rs b/raphtory/src/db/graph/views/deletion_graph.rs index f112d7844c..a25681f78e 100644 --- a/raphtory/src/db/graph/views/deletion_graph.rs +++ b/raphtory/src/db/graph/views/deletion_graph.rs @@ -428,7 +428,7 @@ mod test_deletions { use itertools::Itertools; use proptest::{arbitrary::any, proptest, sample::subsequence}; use raphtory_api::core::entities::GID; - use raphtory_storage::mutation::addition_ops::{InternalAdditionOps, SessionAdditionOps}; + use raphtory_storage::mutation::addition_ops::InternalAdditionOps; use std::ops::Range; #[test] @@ -612,8 +612,6 @@ mod test_deletions { let expected = PersistentGraph::new(); expected.add_edge(1, 0, 0, NO_PROPS, None).unwrap(); expected - .write_session() - .unwrap() .resolve_layer(Some("a")) .unwrap(); // empty layer exists @@ -671,8 +669,6 @@ mod test_deletions { let gw = g.window(0, 0).valid_layers("a"); let expected_gw = PersistentGraph::new(); expected_gw - .write_session() - .unwrap() .resolve_layer(Some("a")) .unwrap(); assert_graph_equal(&gw, &expected_gw); diff --git a/raphtory/src/db/graph/views/filter/node_type_filtered_graph.rs b/raphtory/src/db/graph/views/filter/node_type_filtered_graph.rs index 0f6e7bf1ab..ea1b675b38 100644 --- a/raphtory/src/db/graph/views/filter/node_type_filtered_graph.rs +++ b/raphtory/src/db/graph/views/filter/node_type_filtered_graph.rs @@ -123,7 +123,7 @@ mod tests_node_type_filtered_subgraph { test_utils::{build_graph, build_graph_strat, make_node_types}, }; use proptest::{arbitrary::any, proptest}; - use raphtory_storage::mutation::addition_ops::{InternalAdditionOps, SessionAdditionOps}; + use raphtory_storage::mutation::addition_ops::InternalAdditionOps; use std::ops::Range; #[test] @@ -210,8 +210,6 @@ mod tests_node_type_filtered_subgraph { g.node(1).unwrap().set_node_type("test").unwrap(); let expected = Graph::new(); expected - .write_session() - .unwrap() .resolve_layer(None) .unwrap(); assert_graph_equal(&g.subgraph_node_types(["test"]), &expected); @@ -225,8 +223,6 @@ mod tests_node_type_filtered_subgraph { let gw = g.window(0, 1); let expected = Graph::new(); expected - .write_session() - .unwrap() .resolve_layer(None) .unwrap(); let sg = gw.subgraph_node_types(["_default"]); diff --git a/raphtory/src/db/graph/views/node_subgraph.rs b/raphtory/src/db/graph/views/node_subgraph.rs index b7713a2a48..49906ffc9b 100644 --- a/raphtory/src/db/graph/views/node_subgraph.rs +++ b/raphtory/src/db/graph/views/node_subgraph.rs @@ -653,8 +653,6 @@ mod subgraph_tests { g.add_edge(0, 0, 1, NO_PROPS, None).unwrap(); let expected = Graph::new(); expected - .write_session() - .unwrap() .resolve_layer(None) .unwrap(); let subgraph = g.subgraph([0]); diff --git a/raphtory/src/db/graph/views/valid_graph.rs b/raphtory/src/db/graph/views/valid_graph.rs index 7d2e61adb6..aa49bc4783 100644 --- a/raphtory/src/db/graph/views/valid_graph.rs +++ b/raphtory/src/db/graph/views/valid_graph.rs @@ -100,7 +100,7 @@ mod tests { }; use itertools::Itertools; use proptest::{arbitrary::any, proptest}; - use raphtory_storage::mutation::addition_ops::{InternalAdditionOps, SessionAdditionOps}; + use raphtory_storage::mutation::addition_ops::InternalAdditionOps; use std::ops::Range; #[test] @@ -157,8 +157,6 @@ mod tests { let gv = g.valid().unwrap(); let expected = PersistentGraph::new(); expected - .write_session() - .unwrap() .resolve_layer(None) .unwrap(); assert_graph_equal(&gv, &expected); @@ -311,9 +309,8 @@ mod tests { let gvw = g.valid().unwrap().window(0, 5); assert_eq!(gvw.count_nodes(), 0); let expected = PersistentGraph::new(); - let session = expected.write_session().unwrap(); - session.resolve_layer(None).unwrap(); - session.resolve_layer(Some("a")).unwrap(); + expected.resolve_layer(None).unwrap(); + expected.resolve_layer(Some("a")).unwrap(); assert_graph_equal(&gvw, &expected); let gvwm = gvw.materialize().unwrap(); assert_graph_equal(&gvw, &gvwm); diff --git a/raphtory/src/io/arrow/layer_col.rs b/raphtory/src/io/arrow/layer_col.rs index c670b5935c..e55afbe8d8 100644 --- a/raphtory/src/io/arrow/layer_col.rs +++ b/raphtory/src/io/arrow/layer_col.rs @@ -105,10 +105,9 @@ impl<'a> LayerCol<'a> { self, graph: &(impl AdditionOps + Send + Sync), ) -> Result, GraphError> { - let session = graph.write_session().map_err(|err| err.into())?; match self { LayerCol::Name { name, len } => { - let layer = session.resolve_layer(name).map_err(into_graph_err)?.inner(); + let layer = graph.resolve_layer(name).map_err(into_graph_err)?.inner(); Ok(vec![layer; len]) } col => { @@ -116,7 +115,7 @@ impl<'a> LayerCol<'a> { let mut res = vec![0usize; iter.len()]; iter.zip(res.par_iter_mut()) .try_for_each(|(layer, entry)| { - let layer = session + let layer = graph .resolve_layer(layer) .map_err(into_graph_err)? .inner(); From 2fc0797f9a5a11b0eaacc5306daf6e450711fe8f Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Wed, 11 Jun 2025 12:45:15 +0100 Subject: [PATCH 005/321] add_edge techically exists in the new TemporalGraph --- db4-graph/Cargo.toml | 1 + db4-graph/src/lib.rs | 232 ++++++++++++++---- db4-graph/src/mutation/addition_ops.rs | 1 + db4-graph/src/mutation/mod.rs | 1 + db4-storage/src/lib.rs | 6 +- db4-storage/src/pages/mod.rs | 15 +- db4-storage/src/pages/node_store.rs | 3 +- db4-storage/src/pages/session.rs | 9 +- db4-storage/src/persist/mod.rs | 1 + db4-storage/src/persist/strategy.rs | 45 ++++ .../src/properties/props_meta_writer.rs | 3 +- .../src/entities/graph/logical_to_physical.rs | 48 ++++ .../src/model/graph/mutable_graph.rs | 2 +- raphtory-storage/src/mutation/addition_ops.rs | 67 +++-- raphtory/src/db/api/mutation/addition_ops.rs | 71 ++++-- raphtory/src/db/api/mutation/deletion_ops.rs | 8 +- raphtory/src/db/api/mutation/import_ops.rs | 10 +- raphtory/src/db/api/storage/storage.rs | 35 ++- raphtory/src/db/graph/edge.rs | 3 +- raphtory/src/db/graph/graph.rs | 9 +- raphtory/src/db/graph/views/deletion_graph.rs | 8 +- .../views/filter/node_type_filtered_graph.rs | 8 +- raphtory/src/db/graph/views/node_subgraph.rs | 4 +- raphtory/src/db/graph/views/valid_graph.rs | 4 +- raphtory/src/io/arrow/layer_col.rs | 5 +- 25 files changed, 434 insertions(+), 165 deletions(-) create mode 100644 db4-graph/src/mutation/addition_ops.rs create mode 100644 db4-graph/src/mutation/mod.rs create mode 100644 db4-storage/src/persist/mod.rs create mode 100644 db4-storage/src/persist/strategy.rs diff --git a/db4-graph/Cargo.toml b/db4-graph/Cargo.toml index 4b06af31f0..a1fc9560d3 100644 --- a/db4-graph/Cargo.toml +++ b/db4-graph/Cargo.toml @@ -16,5 +16,6 @@ boxcar.workspace = true storage.workspace = true raphtory-api.workspace = true raphtory-storage.workspace = true +raphtory-core.workspace = true db4-common.workspace = true parking_lot.workspace = true diff --git a/db4-graph/src/lib.rs b/db4-graph/src/lib.rs index 532567f767..2c64f7d374 100644 --- a/db4-graph/src/lib.rs +++ b/db4-graph/src/lib.rs @@ -1,103 +1,171 @@ -use std::sync::Arc; +use std::{ + ops::DerefMut, + path::PathBuf, + sync::{atomic::AtomicUsize, Arc}, +}; use db4_common::error::DBV4Error; use parking_lot::RwLockWriteGuard; -use raphtory_api::core::entities::properties::meta::Meta; -use raphtory_storage::mutation::addition_ops::{InternalAdditionOps, SessionAdditionOps}; -use storage::{pages::session::WriteSession, segments::node::MemNodeSegment, Layer, ES, NS}; +use raphtory_api::core::{ + entities::{ + properties::{ + meta::Meta, + prop::{Prop, PropType}, + }, + GidRef, EID, VID, + }, + storage::{dict_mapper::MaybeNew, timeindex::TimeIndexEntry}, +}; +use raphtory_core::{ + entities::{graph::logical_to_physical::Mapping, nodes::node_ref::NodeRef, ELID}, + storage::{raw_edges::WriteLockedEdges, WriteLockedNodes}, +}; +use raphtory_storage::mutation::{ + addition_ops::{AtomicAdditionOps, InternalAdditionOps, SessionAdditionOps}, + MutationError, +}; +use storage::{ + pages::session::WriteSession, + persist::strategy::PersistentStrategy, + properties::props_meta_writer::PropsMetaWriter, + segments::{edge::MemEdgeSegment, node::MemNodeSegment}, + Layer, ES, NS, +}; + +pub mod mutation; pub struct TemporalGraph { + graph_dir: PathBuf, + // mapping between logical and physical ids + pub logical_to_physical: Mapping, + pub node_count: AtomicUsize, + + max_page_len_nodes: usize, + max_page_len_edges: usize, + layers: boxcar::Vec>, + edge_meta: Arc, node_meta: Arc, } -pub type WriteS<'a, MNS, MES, EXT> = WriteSession<'a, MNS, MES, NS, ES, EXT>; +#[repr(transparent)] +pub struct WriteS< + 'a, + MNS: DerefMut, + MES: DerefMut, + EXT: PersistentStrategy, ES = ES>, +>(WriteSession<'a, MNS, MES, NS, ES, EXT>); pub struct UnlockedSession<'a, EXT> { graph: &'a TemporalGraph, } -impl <'a, EXT: Send + Sync> SessionAdditionOps for UnlockedSession<'a, EXT> { +impl< + 'a, + MNS: DerefMut + Send + Sync, + MES: DerefMut + Send + Sync, + EXT: PersistentStrategy, ES = ES>, + > AtomicAdditionOps for WriteS<'a, MNS, MES, EXT> +{ + fn internal_add_edge( + &mut self, + t: TimeIndexEntry, + src: impl Into, + dst: impl Into, + lsn: u64, + layer: usize, + props: impl IntoIterator, + ) -> MaybeNew { + self.0.internal_add_edge(t, src, dst, lsn, layer, props) + } +} + +impl<'a, EXT: Send + Sync> SessionAdditionOps for UnlockedSession<'a, EXT> { type Error = DBV4Error; - + fn next_event_id(&self) -> Result { todo!() } - + fn reserve_event_ids(&self, num_ids: usize) -> Result { todo!() } - - fn set_node(&self, gid: raphtory_api::core::entities::GidRef, vid: raphtory_api::core::entities::VID) -> Result<(), Self::Error> { + + fn set_node(&self, gid: GidRef, vid: VID) -> Result<(), Self::Error> { todo!() } - + fn resolve_graph_property( &self, prop: &str, - dtype: raphtory_api::core::entities::properties::prop::PropType, + dtype: PropType, is_static: bool, - ) -> Result, Self::Error> { + ) -> Result, Self::Error> { todo!() } - + fn resolve_node_property( &self, prop: &str, - dtype: raphtory_api::core::entities::properties::prop::PropType, + dtype: PropType, is_static: bool, - ) -> Result, Self::Error> { + ) -> Result, Self::Error> { todo!() } - + fn resolve_edge_property( &self, prop: &str, - dtype: raphtory_api::core::entities::properties::prop::PropType, + dtype: PropType, is_static: bool, - ) -> Result, Self::Error> { + ) -> Result, Self::Error> { todo!() } - + fn internal_add_node( &self, - t: raphtory_api::core::storage::timeindex::TimeIndexEntry, - v: raphtory_api::core::entities::VID, - props: &[(usize, raphtory_api::core::entities::properties::prop::Prop)], + t: TimeIndexEntry, + v: VID, + props: &[(usize, Prop)], ) -> Result<(), Self::Error> { todo!() } - + fn internal_add_edge( &self, - t: raphtory_api::core::storage::timeindex::TimeIndexEntry, - src: raphtory_api::core::entities::VID, - dst: raphtory_api::core::entities::VID, - props: &[(usize, raphtory_api::core::entities::properties::prop::Prop)], + t: TimeIndexEntry, + src: VID, + dst: VID, + props: &[(usize, Prop)], layer: usize, - ) -> Result, Self::Error> { + ) -> Result, Self::Error> { todo!() } - + fn internal_add_edge_update( &self, - t: raphtory_api::core::storage::timeindex::TimeIndexEntry, - edge: raphtory_api::core::entities::EID, - props: &[(usize, raphtory_api::core::entities::properties::prop::Prop)], + t: TimeIndexEntry, + edge: EID, + props: &[(usize, Prop)], layer: usize, ) -> Result<(), Self::Error> { todo!() } - } -impl InternalAdditionOps for TemporalGraph { +impl, ES = ES>> InternalAdditionOps + for TemporalGraph +{ type Error = DBV4Error; - type WS<'a> = UnlockedSession<'a, EXT> where EXT: 'a; + type WS<'a> + = UnlockedSession<'a, EXT> + where + EXT: 'a; - type AtomicAddEdge<'a> = WriteS, RwLockWriteGuard, EXT>; + type AtomicAddEdge<'a> = + WriteS<'a, RwLockWriteGuard<'a, MemNodeSegment>, RwLockWriteGuard<'a, MemEdgeSegment>, EXT>; fn write_lock(&self) -> Result { todo!() @@ -111,27 +179,76 @@ impl InternalAdditionOps for TemporalGraph { todo!() } - fn resolve_layer(&self, layer: Option<&str>) -> Result, Self::Error> { - todo!() + fn resolve_layer(&self, layer: Option<&str>) -> Result, Self::Error> { + let id = self.edge_meta.get_or_create_layer_id(layer); + + let layer_id = id.inner(); + if self.layers.get(layer_id).is_some() { + return Ok(id); + } + let count = self.layers.count(); + if count >= layer_id + 1 { + // something has allocated the layer, wait for it to be added + while self.layers.get(layer_id).is_none() { + // wait for the layer to be created + std::thread::yield_now(); + } + return Ok(id); + } else { + self.layers.reserve(2); + let layer_name = layer.unwrap_or("_default"); + loop { + let new_layer_id = self.layers.push_with(|_| { + Layer::new( + self.graph_dir.join(format!("l_{}", layer_name)), + self.max_page_len_nodes, + self.max_page_len_edges, + ) + }); + if new_layer_id >= layer_id { + while self.layers.get(new_layer_id).is_none() { + // wait for the layer to be created + std::thread::yield_now(); + } + return Ok(id); + } + } + } } - fn resolve_node(&self, id: NodeRef) -> Result, Self::Error> { - todo!() + fn resolve_node(&self, id: NodeRef) -> Result, Self::Error> { + match id { + NodeRef::External(id) => { + let id = self + .logical_to_physical + .get_or_init_vid(id, || { + self.node_count + .fetch_add(1, std::sync::atomic::Ordering::Relaxed) + .into() + }) + .map_err(MutationError::InvalidNodeId)?; + Ok(id) + } + NodeRef::Internal(id) => Ok(MaybeNew::Existing(id)), + } } fn resolve_node_and_type( &self, id: NodeRef, node_type: &str, - ) -> Result, raphtory_api::core::storage::dict_mapper::MaybeNew)>, Self::Error> { + ) -> Result, MaybeNew)>, Self::Error> { todo!() } fn validate_gids<'a>( &self, - gids: impl IntoIterator>, + gids: impl IntoIterator>, ) -> Result<(), Self::Error> { - todo!() + Ok(self + .logical_to_physical + .validate_gids(gids) + .map_err(MutationError::InvalidNodeId)?) } fn write_session(&self) -> Result, Self::Error> { @@ -140,17 +257,24 @@ impl InternalAdditionOps for TemporalGraph { fn atomic_add_edge( &self, - src: raphtory_api::core::entities::VID, - dst: raphtory_api::core::entities::VID, - e_id: Option, - ) -> Result, Self::Error> { - todo!() + src: VID, + dst: VID, + e_id: Option, + layer_id: usize, + ) -> Self::AtomicAddEdge<'_> { + let layer = &self.layers[layer_id]; + WriteS(layer.write_session(src, dst, e_id)) } - fn validate_prop>( + fn validate_edge_props>( &self, - prop: impl ExactSizeIterator, - ) -> Result, Self::Error> { - todo!() + is_static: bool, + props: impl ExactSizeIterator, + ) -> Result, Self::Error> { + if is_static { + PropsMetaWriter::constant(&self.edge_meta, props)?.into_props_const() + } else { + PropsMetaWriter::temporal(&self.edge_meta, props)?.into_props_temporal() + } } -} \ No newline at end of file +} diff --git a/db4-graph/src/mutation/addition_ops.rs b/db4-graph/src/mutation/addition_ops.rs new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/db4-graph/src/mutation/addition_ops.rs @@ -0,0 +1 @@ + diff --git a/db4-graph/src/mutation/mod.rs b/db4-graph/src/mutation/mod.rs new file mode 100644 index 0000000000..74231cd875 --- /dev/null +++ b/db4-graph/src/mutation/mod.rs @@ -0,0 +1 @@ +pub mod addition_ops; diff --git a/db4-storage/src/lib.rs b/db4-storage/src/lib.rs index 23f2307b6a..3fe78b9cd6 100644 --- a/db4-storage/src/lib.rs +++ b/db4-storage/src/lib.rs @@ -11,13 +11,17 @@ use crate::{ use db4_common::{LocalPOS, error::DBV4Error}; use parking_lot::{RwLockReadGuard, RwLockWriteGuard}; use raphtory_api::core::{ - entities::{ properties::{meta::Meta, tprop::TPropOps, prop::Prop}, VID, EID}, + entities::{ + EID, VID, + properties::{meta::Meta, prop::Prop, tprop::TPropOps}, + }, storage::timeindex::{TimeIndexEntry, TimeIndexOps}, }; use segments::{edge::MemEdgeSegment, node::MemNodeSegment}; pub mod loaders; pub mod pages; +pub mod persist; pub mod properties; pub mod segments; diff --git a/db4-storage/src/pages/mod.rs b/db4-storage/src/pages/mod.rs index d82951a74f..72baea74fc 100644 --- a/db4-storage/src/pages/mod.rs +++ b/db4-storage/src/pages/mod.rs @@ -17,6 +17,7 @@ use edge_page::writer::EdgeWriter; use edge_store::EdgeStorageInner; use node_page::writer::{NodeWriter, WriterPair}; use node_store::NodeStorageInner; +use parking_lot::RwLockWriteGuard; use raphtory::{ core::{ entities::{EID, ELID, VID}, @@ -227,7 +228,8 @@ impl, ES: EdgeSegmentOps, E let src = src.into(); let dst = dst.into(); let mut session = self.write_session(src, dst, None); - session.internal_add_edge(t, src, dst, lsn, 0, props) + let elid = session.internal_add_edge(t, src, dst, lsn, 0, props); + Ok(elid) } /// Adds an edge if it doesn't exist yet, does nothing if the edge is there @@ -408,8 +410,8 @@ impl, ES: EdgeSegmentOps, E e_id: Option, ) -> WriteSession< '_, - impl DerefMut, - impl DerefMut, + RwLockWriteGuard, + RwLockWriteGuard, NS, ES, EXT, @@ -446,14 +448,11 @@ impl, ES: EdgeSegmentOps, E WriteSession::new(node_writers, edge_writer, self) } - fn node_writer( - &self, - node_segment: usize, - ) -> NodeWriter, NS> { + fn node_writer(&self, node_segment: usize) -> NodeWriter, NS> { self.nodes().writer(node_segment) } - pub fn edge_writer(&self, eid: EID) -> EdgeWriter, ES> { + pub fn edge_writer(&self, eid: EID) -> EdgeWriter, ES> { self.edges().get_writer(eid) } diff --git a/db4-storage/src/pages/node_store.rs b/db4-storage/src/pages/node_store.rs index bc3657195e..c055ea0c38 100644 --- a/db4-storage/src/pages/node_store.rs +++ b/db4-storage/src/pages/node_store.rs @@ -5,6 +5,7 @@ use crate::{ segments::node::MemNodeSegment, }; use db4_common::{LocalPOS, error::DBV4Error}; +use parking_lot::RwLockWriteGuard; use raphtory::core::entities::{EID, VID}; use raphtory_api::core::entities::properties::meta::Meta; use std::{ @@ -88,7 +89,7 @@ impl, EXT: Clone> NodeStorageInner pub fn writer<'a>( &'a self, segment_id: usize, - ) -> NodeWriter<'a, impl DerefMut + 'a, NS> { + ) -> NodeWriter<'a, RwLockWriteGuard<'a, MemNodeSegment>, NS> { let segment = self.get_or_create_segment(segment_id); let head = segment.head_mut(); NodeWriter::new(segment, &self.num_nodes, head) diff --git a/db4-storage/src/pages/session.rs b/db4-storage/src/pages/session.rs index 4934915114..6f3dff86f9 100644 --- a/db4-storage/src/pages/session.rs +++ b/db4-storage/src/pages/session.rs @@ -100,7 +100,6 @@ impl< self.node_writers .get_mut_dst() .update_timestamp(t, dst_pos, e_id, lsn); - } pub fn add_static_edge( @@ -161,7 +160,7 @@ impl< lsn: u64, layer: usize, props: impl IntoIterator, - ) -> Result, DBV4Error> { + ) -> MaybeNew { let src = src.into(); let dst = dst.into(); @@ -181,7 +180,7 @@ impl< .get_mut_dst() .update_timestamp(t, dst_pos, e_id, lsn); - Ok(MaybeNew::Existing(e_id)) + MaybeNew::Existing(e_id) } else { if let Some(e_id) = self.node_writers.get_mut_src().get_out_edge(src_pos, dst) { let mut edge_writer = self.graph.edge_writer(e_id); @@ -196,7 +195,7 @@ impl< .get_mut_dst() .update_timestamp(t, dst_pos, e_id, lsn); - Ok(MaybeNew::Existing(e_id)) + MaybeNew::Existing(e_id) } else { let mut edge_writer = self.graph.get_free_writer(); let edge_id = edge_writer.add_edge(t, None, src, dst, props, lsn, None); @@ -211,7 +210,7 @@ impl< .get_mut_dst() .add_inbound_edge(t, dst_pos, src, edge_id, lsn); - Ok(MaybeNew::New(edge_id)) + MaybeNew::New(edge_id) } } } diff --git a/db4-storage/src/persist/mod.rs b/db4-storage/src/persist/mod.rs new file mode 100644 index 0000000000..54eb972285 --- /dev/null +++ b/db4-storage/src/persist/mod.rs @@ -0,0 +1 @@ +pub mod strategy; diff --git a/db4-storage/src/persist/strategy.rs b/db4-storage/src/persist/strategy.rs new file mode 100644 index 0000000000..a2418559d8 --- /dev/null +++ b/db4-storage/src/persist/strategy.rs @@ -0,0 +1,45 @@ + +use std::ops::DerefMut; + +use crate::segments::{ + edge::{EdgeSegmentView, MemEdgeSegment}, + node::MemNodeSegment, +}; + +pub trait PersistentStrategy: Default + Clone + std::fmt::Debug + Send + Sync + 'static { + type NS; + type ES; + fn persist_node_page>( + &self, + node_page: &Self::NS, + writer: MP, + ) where + Self: Sized; + fn persist_edge_page>( + &self, + edge_page: &Self::ES, + writer: MP, + ) where + Self: Sized; +} + +impl PersistentStrategy for () { + type ES = EdgeSegmentView; + type NS = MemNodeSegment; + + fn persist_node_page>( + &self, + _node_page: &Self::NS, + _writer: MP, + ) { + // No operation + } + + fn persist_edge_page>( + &self, + _edge_page: &Self::ES, + _writer: MP, + ) { + // No operation + } +} diff --git a/db4-storage/src/properties/props_meta_writer.rs b/db4-storage/src/properties/props_meta_writer.rs index 7c0b0de961..9f4c8395b3 100644 --- a/db4-storage/src/properties/props_meta_writer.rs +++ b/db4-storage/src/properties/props_meta_writer.rs @@ -1,9 +1,8 @@ use db4_common::error::DBV4Error; use either::Either; use raphtory_api::core::entities::properties::{ - prop::Prop, meta::{LockedPropMapper, Meta, PropMapper}, - prop::unify_types, + prop::{Prop, unify_types}, }; pub enum PropsMetaWriter<'a, PN: AsRef> { diff --git a/raphtory-core/src/entities/graph/logical_to_physical.rs b/raphtory-core/src/entities/graph/logical_to_physical.rs index 4f5b797edf..6685e38bd5 100644 --- a/raphtory-core/src/entities/graph/logical_to_physical.rs +++ b/raphtory-core/src/entities/graph/logical_to_physical.rs @@ -159,6 +159,54 @@ impl Mapping { } } + // we assume pre validation of gids + pub fn get_or_init_vid<'a>( + &self, + gid: GidRef, + f_init: impl FnOnce() -> VID, + ) -> Result, InvalidNodeId> { + let map = self.map.get_or_init(|| match &gid { + GidRef::U64(_) => Map::U64(FxDashMap::default()), + GidRef::Str(_) => Map::Str(FxDashMap::default()), + }); + match gid { + GidRef::U64(id) => map + .as_u64() + .map(|m| { + m.get(&id) + .map(|vid| MaybeNew::Existing(*vid)) + .unwrap_or_else(|| match m.entry(id) { + Entry::Occupied(occupied_entry) => { + MaybeNew::Existing(*occupied_entry.get()) + } + Entry::Vacant(vacant_entry) => { + let vid = f_init(); + vacant_entry.insert(vid); + MaybeNew::New(vid) + } + }) + }) + .ok_or(InvalidNodeId::InvalidNodeIdU64(id)), + GidRef::Str(id) => map + .as_str() + .map(|m| { + m.get(id) + .map(|vid| MaybeNew::Existing(*vid)) + .unwrap_or_else(|| match m.entry(id.to_owned()) { + Entry::Occupied(occupied_entry) => { + MaybeNew::Existing(*occupied_entry.get()) + } + Entry::Vacant(vacant_entry) => { + let vid = f_init(); + vacant_entry.insert(vid); + MaybeNew::New(vid) + } + }) + }) + .ok_or_else(|| InvalidNodeId::InvalidNodeIdStr(id.into())), + } + } + pub fn validate_gids<'a>( &self, gids: impl IntoIterator>, diff --git a/raphtory-graphql/src/model/graph/mutable_graph.rs b/raphtory-graphql/src/model/graph/mutable_graph.rs index 94629e7161..dc027df933 100644 --- a/raphtory-graphql/src/model/graph/mutable_graph.rs +++ b/raphtory-graphql/src/model/graph/mutable_graph.rs @@ -60,7 +60,7 @@ impl GqlMutableGraph { fn as_properties( properties: Vec, -) -> Result, GraphError> { +) -> Result, GraphError> { let props: Result, GraphError> = properties .into_iter() .map(|p| { diff --git a/raphtory-storage/src/mutation/addition_ops.rs b/raphtory-storage/src/mutation/addition_ops.rs index 31d4c501c6..800fcc9622 100644 --- a/raphtory-storage/src/mutation/addition_ops.rs +++ b/raphtory-storage/src/mutation/addition_ops.rs @@ -13,7 +13,7 @@ use raphtory_api::{ inherit::Base, }; use raphtory_core::{ - entities::{graph::tgraph::TemporalGraph, nodes::node_ref::NodeRef}, + entities::{graph::tgraph::TemporalGraph, nodes::node_ref::NodeRef, ELID}, storage::{raw_edges::WriteLockedEdges, WriteLockedNodes}, }; use std::sync::atomic::Ordering; @@ -24,7 +24,7 @@ pub trait InternalAdditionOps { where Self: 'a; - type AtomicAddEdge<'a>: Send + Sync + type AtomicAddEdge<'a>: AtomicAdditionOps where Self: 'a; @@ -55,14 +55,28 @@ pub trait InternalAdditionOps { src: VID, dst: VID, e_id: Option, - ) -> Result, Self::Error>; + layer_id: usize, + ) -> Self::AtomicAddEdge<'_>; - fn validate_prop>( + fn validate_edge_props>( &self, + is_static: bool, prop: impl ExactSizeIterator, ) -> Result, Self::Error>; } +pub trait AtomicAdditionOps: Send + Sync { + fn internal_add_edge( + &mut self, + t: TimeIndexEntry, + src: impl Into, + dst: impl Into, + lsn: u64, + layer: usize, + props: impl IntoIterator, + ) -> MaybeNew; +} + pub trait SessionAdditionOps: Send + Sync { type Error: From; /// get the sequence id for the next event @@ -121,6 +135,21 @@ pub struct TGWriteSession<'a> { tg: &'a TemporalGraph, } +impl AtomicAdditionOps for TGWriteSession<'_> { + /// add edge update + fn internal_add_edge( + &mut self, + _t: TimeIndexEntry, + _src: impl Into, + _dst: impl Into, + _lsn: u64, + _layer: usize, + _props: impl IntoIterator, + ) -> MaybeNew { + todo!("Atomic addition operations are not implemented for TGWriteSession"); + } +} + impl<'a> SessionAdditionOps for TGWriteSession<'a> { type Error = MutationError; @@ -287,16 +316,18 @@ impl InternalAdditionOps for GraphStorage { _src: VID, _dst: VID, _e_id: Option, - ) -> Result, Self::Error> { - self.write_session() + _layer_id: usize, + ) -> Self::AtomicAddEdge<'_> { + self.write_session().unwrap() } - fn validate_prop>( + fn validate_edge_props>( &self, + is_static: bool, prop: impl ExactSizeIterator, ) -> Result, Self::Error> { self.mutable()? - .validate_prop(prop) + .validate_edge_props(is_static, prop) .map_err(MutationError::from) } @@ -372,8 +403,9 @@ impl InternalAdditionOps for TemporalGraph { _src: VID, _dst: VID, _e_id: Option, - ) -> Result, Self::Error> { - Ok(TGWriteSession { tg: self }) + _layer_id: usize, + ) -> Self::AtomicAddEdge<'_> { + TGWriteSession { tg: self } } fn validate_gids<'a>( @@ -385,8 +417,9 @@ impl InternalAdditionOps for TemporalGraph { .map_err(MutationError::from) } - fn validate_prop>( + fn validate_edge_props>( &self, + is_static: bool, prop: impl ExactSizeIterator, ) -> Result, Self::Error> { let session = self.write_session()?; @@ -394,7 +427,7 @@ impl InternalAdditionOps for TemporalGraph { .map(|(name, prop)| { let dtype = prop.dtype(); session - .resolve_node_property(name.as_ref(), dtype, false) + .resolve_edge_property(name.as_ref(), dtype, is_static) .map(|id| (id.inner(), prop)) }) .collect::, _>>()?; @@ -466,16 +499,18 @@ where src: VID, dst: VID, e_id: Option, - ) -> Result, Self::Error> { - self.base().atomic_add_edge(src, dst, e_id) + layer_id: usize, + ) -> Self::AtomicAddEdge<'_> { + self.base().atomic_add_edge(src, dst, e_id, layer_id) } #[inline] - fn validate_prop>( + fn validate_edge_props>( &self, + is_static: bool, prop: impl ExactSizeIterator, ) -> Result, Self::Error> { - self.base().validate_prop(prop) + self.base().validate_edge_props(is_static, prop) } #[inline] diff --git a/raphtory/src/db/api/mutation/addition_ops.rs b/raphtory/src/db/api/mutation/addition_ops.rs index a1b1dc3c6a..9857a39b71 100644 --- a/raphtory/src/db/api/mutation/addition_ops.rs +++ b/raphtory/src/db/api/mutation/addition_ops.rs @@ -17,7 +17,9 @@ use raphtory_api::core::{ entities::properties::prop::Prop, storage::dict_mapper::MaybeNew::{Existing, New}, }; -use raphtory_storage::mutation::addition_ops::{InternalAdditionOps, SessionAdditionOps}; +use raphtory_storage::mutation::addition_ops::{ + AtomicAdditionOps, InternalAdditionOps, SessionAdditionOps, +}; pub trait AdditionOps: StaticGraphViewOps + InternalAdditionOps> { // TODO: Probably add vector reference here like add @@ -90,22 +92,35 @@ pub trait AdditionOps: StaticGraphViewOps + InternalAdditionOps( + fn add_edge< + V: AsNodeRef, + T: TryIntoInputTime, + PN: AsRef, + P: Into, + PI: ExactSizeIterator, + PII: IntoIterator, + >( &self, t: T, src: V, dst: V, - props: PI, + props: PII, layer: Option<&str>, ) -> Result, GraphError>; - fn add_edge_with_custom_time_format( + fn add_edge_with_custom_time_format< + V: AsNodeRef, + PN: AsRef, + P: Into, + PI: ExactSizeIterator, + PII: IntoIterator, + >( &self, t: &str, fmt: &str, src: V, dst: V, - props: PI, + props: PII, layer: Option<&str>, ) -> Result, GraphError> { let time: i64 = t.parse_time(fmt)?; @@ -158,9 +173,7 @@ impl> + StaticGraphViewOps> Addit let session = self.write_session().map_err(|err| err.into())?; let ti = time_from_input_session(&session, t)?; let v_id = match node_type { - None => self - .resolve_node(v.as_node_ref()) - .map_err(into_graph_err)?, + None => self.resolve_node(v.as_node_ref()).map_err(into_graph_err)?, Some(node_type) => { let (v_id, _) = self .resolve_node_and_type(v.as_node_ref(), node_type) @@ -189,15 +202,32 @@ impl> + StaticGraphViewOps> Addit } } - fn add_edge( + fn add_edge< + V: AsNodeRef, + T: TryIntoInputTime, + PN: AsRef, + P: Into, + PI: ExactSizeIterator, + PII: IntoIterator, + >( &self, t: T, src: V, dst: V, - props: PI, + props: PII, layer: Option<&str>, ) -> Result, GraphError> { let session = self.write_session().map_err(|err| err.into())?; + self.validate_gids( + [src.as_node_ref(), dst.as_node_ref()] + .iter() + .filter_map(|node_ref| node_ref.as_gid_ref().left()), + ) + .map_err(into_graph_err)?; + let props = self + .validate_edge_props(false, props.into_iter().map(|(k, v)| (k, v.into()))) + .map_err(into_graph_err)?; + let ti = time_from_input_session(&session, t)?; let src_id = self .resolve_node(src.as_node_ref()) @@ -207,24 +237,15 @@ impl> + StaticGraphViewOps> Addit .resolve_node(dst.as_node_ref()) .map_err(into_graph_err)? .inner(); - let layer_id = self - .resolve_layer(layer) - .map_err(into_graph_err)? - .inner(); + let layer_id = self.resolve_layer(layer).map_err(into_graph_err)?.inner(); + + let mut add_edge_op = self.atomic_add_edge(src_id, dst_id, None, layer_id); + + let edge = add_edge_op.internal_add_edge(ti, src_id, dst_id, 0, layer_id, props); - let properties: Vec<(usize, Prop)> = props.collect_properties(|name, dtype| { - Ok(session - .resolve_edge_property(name, dtype, false) - .map_err(into_graph_err)? - .inner()) - })?; - let eid = session - .internal_add_edge(ti, src_id, dst_id, &properties, layer_id) - .map_err(into_graph_err)? - .inner(); Ok(EdgeView::new( self.clone(), - EdgeRef::new_outgoing(eid, src_id, dst_id).at_layer(layer_id), + EdgeRef::new_outgoing(edge.inner().edge, src_id, dst_id).at_layer(layer_id), )) } } diff --git a/raphtory/src/db/api/mutation/deletion_ops.rs b/raphtory/src/db/api/mutation/deletion_ops.rs index 9269368c6f..ef41bcdf56 100644 --- a/raphtory/src/db/api/mutation/deletion_ops.rs +++ b/raphtory/src/db/api/mutation/deletion_ops.rs @@ -11,8 +11,7 @@ use crate::{ }; use raphtory_api::core::entities::edges::edge_ref::EdgeRef; use raphtory_storage::mutation::{ - addition_ops::InternalAdditionOps, - deletion_ops::InternalDeletionOps, + addition_ops::InternalAdditionOps, deletion_ops::InternalDeletionOps, }; pub trait DeletionOps: @@ -38,10 +37,7 @@ pub trait DeletionOps: .resolve_node(dst.as_node_ref()) .map_err(into_graph_err)? .inner(); - let layer = self - .resolve_layer(layer) - .map_err(into_graph_err)? - .inner(); + let layer = self.resolve_layer(layer).map_err(into_graph_err)?.inner(); let eid = self .internal_delete_edge(ti, src_id, dst_id, layer) .map_err(into_graph_err)? diff --git a/raphtory/src/db/api/mutation/import_ops.rs b/raphtory/src/db/api/mutation/import_ops.rs index cc71d0d6c0..411425b85e 100644 --- a/raphtory/src/db/api/mutation/import_ops.rs +++ b/raphtory/src/db/api/mutation/import_ops.rs @@ -411,14 +411,8 @@ fn import_edge_internal< for (t, _) in edge.deletions_hist() { let ti = time_from_input_session(&session, t.t())?; - let src_node = graph - .resolve_node(src_id) - .map_err(into_graph_err)? - .inner(); - let dst_node = graph - .resolve_node(dst_id) - .map_err(into_graph_err)? - .inner(); + let src_node = graph.resolve_node(src_id).map_err(into_graph_err)?.inner(); + let dst_node = graph.resolve_node(dst_id).map_err(into_graph_err)?.inner(); let layer = graph .resolve_layer(Some(&layer_name)) .map_err(into_graph_err)? diff --git a/raphtory/src/db/api/storage/storage.rs b/raphtory/src/db/api/storage/storage.rs index 5741649547..187974de09 100644 --- a/raphtory/src/db/api/storage/storage.rs +++ b/raphtory/src/db/api/storage/storage.rs @@ -13,7 +13,7 @@ use raphtory_api::core::{ }; use raphtory_storage::{ graph::graph::GraphStorage, - mutation::addition_ops::{SessionAdditionOps, TGWriteSession}, + mutation::addition_ops::{AtomicAdditionOps, SessionAdditionOps, TGWriteSession}, }; use serde::{Deserialize, Serialize}; use std::{ @@ -28,7 +28,10 @@ use raphtory_api::core::entities::{ properties::prop::{Prop, PropType}, GidRef, }; -use raphtory_core::storage::{raw_edges::WriteLockedEdges, WriteLockedNodes}; +use raphtory_core::{ + entities::ELID, + storage::{raw_edges::WriteLockedEdges, WriteLockedNodes}, +}; use raphtory_storage::{ core_ops::InheritCoreGraphOps, graph::{locked::WriteLockedGraph, nodes::node_storage_ops::NodeStorageOps}, @@ -207,6 +210,20 @@ pub struct StorageWriteSession<'a> { storage: &'a Storage, } +impl AtomicAdditionOps for StorageWriteSession<'_> { + fn internal_add_edge( + &mut self, + _t: TimeIndexEntry, + _src: impl Into, + _dst: impl Into, + _lsn: u64, + _layer: usize, + _props: impl IntoIterator, + ) -> MaybeNew { + todo!() + } +} + impl<'a> SessionAdditionOps for StorageWriteSession<'a> { type Error = GraphError; @@ -415,19 +432,21 @@ impl InternalAdditionOps for Storage { src: VID, dst: VID, e_id: Option, - ) -> Result, Self::Error> { - let session = self.graph.atomic_add_edge(src, dst, e_id)?; - Ok(StorageWriteSession { + layer_id: usize, + ) -> Self::AtomicAddEdge<'_> { + let session = self.graph.atomic_add_edge(src, dst, e_id, layer_id); + StorageWriteSession { session, storage: self, - }) + } } - fn validate_prop>( + fn validate_edge_props>( &self, + is_static: bool, prop: impl ExactSizeIterator, ) -> Result, Self::Error> { - Ok(self.graph.validate_prop(prop)?) + Ok(self.graph.validate_edge_props(is_static, prop)?) } fn validate_gids<'a>( diff --git a/raphtory/src/db/graph/edge.rs b/raphtory/src/db/graph/edge.rs index 06cc92f64f..2036787dbc 100644 --- a/raphtory/src/db/graph/edge.rs +++ b/raphtory/src/db/graph/edge.rs @@ -301,7 +301,8 @@ impl EdgeView { })?, None => { if create { - self.graph.resolve_layer(layer) + self.graph + .resolve_layer(layer) .map_err(into_graph_err)? .inner() } else { diff --git a/raphtory/src/db/graph/graph.rs b/raphtory/src/db/graph/graph.rs index e755ce00f3..f1fc81863e 100644 --- a/raphtory/src/db/graph/graph.rs +++ b/raphtory/src/db/graph/graph.rs @@ -604,10 +604,7 @@ mod db_tests { utils::logging::global_info_logger, }; use raphtory_core::utils::time::{ParseTimeError, TryIntoTime}; - use raphtory_storage::{ - core_ops::CoreGraphOps, - mutation::addition_ops::InternalAdditionOps, - }; + use raphtory_storage::{core_ops::CoreGraphOps, mutation::addition_ops::InternalAdditionOps}; use rayon::join; use std::{ collections::{HashMap, HashSet}, @@ -4038,9 +4035,7 @@ mod db_tests { let expected = Graph::new(); expected.add_edge(0, 0, 0, NO_PROPS, None).unwrap(); - expected - .resolve_layer(Some("a")) - .unwrap(); + expected.resolve_layer(Some("a")).unwrap(); assert_graph_equal(&gw, &expected); } diff --git a/raphtory/src/db/graph/views/deletion_graph.rs b/raphtory/src/db/graph/views/deletion_graph.rs index a25681f78e..41754756cc 100644 --- a/raphtory/src/db/graph/views/deletion_graph.rs +++ b/raphtory/src/db/graph/views/deletion_graph.rs @@ -611,9 +611,7 @@ mod test_deletions { let expected = PersistentGraph::new(); expected.add_edge(1, 0, 0, NO_PROPS, None).unwrap(); - expected - .resolve_layer(Some("a")) - .unwrap(); // empty layer exists + expected.resolve_layer(Some("a")).unwrap(); // empty layer exists println!("expected: {:?}", expected); assert_persistent_materialize_graph_equal(&gw, &expected); @@ -668,9 +666,7 @@ mod test_deletions { g.delete_edge(0, 0, 0, None).unwrap(); let gw = g.window(0, 0).valid_layers("a"); let expected_gw = PersistentGraph::new(); - expected_gw - .resolve_layer(Some("a")) - .unwrap(); + expected_gw.resolve_layer(Some("a")).unwrap(); assert_graph_equal(&gw, &expected_gw); let gwm = gw.materialize().unwrap(); assert_graph_equal(&gw, &gwm); diff --git a/raphtory/src/db/graph/views/filter/node_type_filtered_graph.rs b/raphtory/src/db/graph/views/filter/node_type_filtered_graph.rs index ea1b675b38..a5e8c9c200 100644 --- a/raphtory/src/db/graph/views/filter/node_type_filtered_graph.rs +++ b/raphtory/src/db/graph/views/filter/node_type_filtered_graph.rs @@ -209,9 +209,7 @@ mod tests_node_type_filtered_subgraph { g.add_edge(0, 0, 1, NO_PROPS, None).unwrap(); g.node(1).unwrap().set_node_type("test").unwrap(); let expected = Graph::new(); - expected - .resolve_layer(None) - .unwrap(); + expected.resolve_layer(None).unwrap(); assert_graph_equal(&g.subgraph_node_types(["test"]), &expected); } @@ -222,9 +220,7 @@ mod tests_node_type_filtered_subgraph { g.node(0).unwrap().set_node_type("two").unwrap(); let gw = g.window(0, 1); let expected = Graph::new(); - expected - .resolve_layer(None) - .unwrap(); + expected.resolve_layer(None).unwrap(); let sg = gw.subgraph_node_types(["_default"]); assert!(!sg.has_node(0)); assert!(!sg.has_node(1)); diff --git a/raphtory/src/db/graph/views/node_subgraph.rs b/raphtory/src/db/graph/views/node_subgraph.rs index 49906ffc9b..e7b70d1b1d 100644 --- a/raphtory/src/db/graph/views/node_subgraph.rs +++ b/raphtory/src/db/graph/views/node_subgraph.rs @@ -652,9 +652,7 @@ mod subgraph_tests { let g = Graph::new(); g.add_edge(0, 0, 1, NO_PROPS, None).unwrap(); let expected = Graph::new(); - expected - .resolve_layer(None) - .unwrap(); + expected.resolve_layer(None).unwrap(); let subgraph = g.subgraph([0]); assert_graph_equal(&subgraph, &expected); } diff --git a/raphtory/src/db/graph/views/valid_graph.rs b/raphtory/src/db/graph/views/valid_graph.rs index aa49bc4783..74cb3709ad 100644 --- a/raphtory/src/db/graph/views/valid_graph.rs +++ b/raphtory/src/db/graph/views/valid_graph.rs @@ -156,9 +156,7 @@ mod tests { g.delete_edge(1, 0, 1, None).unwrap(); let gv = g.valid().unwrap(); let expected = PersistentGraph::new(); - expected - .resolve_layer(None) - .unwrap(); + expected.resolve_layer(None).unwrap(); assert_graph_equal(&gv, &expected); let gm = gv.materialize().unwrap(); assert_graph_equal(&gv, &gm); diff --git a/raphtory/src/io/arrow/layer_col.rs b/raphtory/src/io/arrow/layer_col.rs index e55afbe8d8..dc8973bc5c 100644 --- a/raphtory/src/io/arrow/layer_col.rs +++ b/raphtory/src/io/arrow/layer_col.rs @@ -115,10 +115,7 @@ impl<'a> LayerCol<'a> { let mut res = vec![0usize; iter.len()]; iter.zip(res.par_iter_mut()) .try_for_each(|(layer, entry)| { - let layer = graph - .resolve_layer(layer) - .map_err(into_graph_err)? - .inner(); + let layer = graph.resolve_layer(layer).map_err(into_graph_err)?.inner(); *entry = layer; Ok::<(), GraphError>(()) })?; From 1484503071f74ebff2303722718e6f87644c3685 Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Wed, 11 Jun 2025 16:37:29 +0100 Subject: [PATCH 006/321] move things around so we can implement AdditionOps in the right place and fix cyclical dependencies --- Cargo.toml | 8 +- db4-common/Cargo.toml | 19 - db4-common/src/lib.rs | 81 ---- db4-graph/Cargo.toml | 2 - db4-graph/src/lib.rs | 242 +----------- db4-storage/Cargo.toml | 5 +- db4-storage/src/lib.rs | 79 +++- db4-storage/src/loaders/mod.rs | 13 +- db4-storage/src/pages/edge_page/writer.rs | 7 +- db4-storage/src/pages/edge_store.rs | 6 +- db4-storage/src/pages/locked/edges.rs | 5 +- db4-storage/src/pages/locked/nodes.rs | 5 +- db4-storage/src/pages/mod.rs | 26 +- db4-storage/src/pages/node_page/writer.rs | 10 +- db4-storage/src/pages/node_store.rs | 7 +- db4-storage/src/pages/session.rs | 13 +- db4-storage/src/pages/test_utils.rs | 370 +++++++++--------- db4-storage/src/persist/strategy.rs | 1 - db4-storage/src/properties/mod.rs | 20 +- .../src/properties/props_meta_writer.rs | 3 +- db4-storage/src/segments/additions.rs | 2 +- db4-storage/src/segments/edge.rs | 15 +- db4-storage/src/segments/edge_entry.rs | 8 +- db4-storage/src/segments/mod.rs | 18 +- db4-storage/src/segments/node.rs | 19 +- db4-storage/src/segments/node_entry.rs | 7 +- raphtory-storage/Cargo.toml | 3 + .../src/mutation/addition_ops_ext.rs | 263 +++++++++++++ raphtory-storage/src/mutation/mod.rs | 4 + 29 files changed, 635 insertions(+), 626 deletions(-) delete mode 100644 db4-common/Cargo.toml delete mode 100644 db4-common/src/lib.rs create mode 100644 raphtory-storage/src/mutation/addition_ops_ext.rs diff --git a/Cargo.toml b/Cargo.toml index 692b0bd588..b425877978 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,11 +11,15 @@ members = [ "raphtory-api", "raphtory-core", "raphtory-storage", - "db4-common", + # "db4-common", "db4-storage", "db4-graph", ] -default-members = ["raphtory", "db4-common", "db4-storage", "db4-graph"] +default-members = [ + "raphtory", + # "db4-common", + "db4-storage", + "db4-graph"] resolver = "2" [workspace.package] diff --git a/db4-common/Cargo.toml b/db4-common/Cargo.toml deleted file mode 100644 index 17feeafe45..0000000000 --- a/db4-common/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "db4-common" -version.workspace = true -documentation.workspace = true -repository.workspace = true -readme.workspace = true -homepage.workspace = true -keywords.workspace = true -authors.workspace = true -rust-version.workspace = true -edition = "2024" - -[dependencies] -raphtory.workspace = true -raphtory-storage.workspace = true -thiserror.workspace = true -serde_json.workspace = true -arrow-schema.workspace = true -parquet.workspace = true diff --git a/db4-common/src/lib.rs b/db4-common/src/lib.rs deleted file mode 100644 index 8afdcbcb69..0000000000 --- a/db4-common/src/lib.rs +++ /dev/null @@ -1,81 +0,0 @@ -use std::path::Path; - -use raphtory::core::entities::{EID, VID}; - -pub mod error { - use std::{path::PathBuf, sync::Arc}; - - use raphtory::{ - api::core::entities::properties::prop::PropError, core::utils::time::ParseTimeError, - errors::LoadError, - }; - use raphtory_storage::mutation::MutationError; - - #[derive(thiserror::Error, Debug)] - pub enum DBV4Error { - #[error("External Storage Error {0}")] - External(#[from] Arc), - #[error("IO error: {0}")] - IO(#[from] std::io::Error), - #[error("Serde error: {0}")] - Serde(#[from] serde_json::Error), - #[error("Load error: {0}")] - LoadError(#[from] LoadError), - #[error("Arrow-rs error: {0}")] - ArrowRS(#[from] arrow_schema::ArrowError), - #[error("Parquet error: {0}")] - Parquet(#[from] parquet::errors::ParquetError), - - #[error("Property error: {0}")] - PropError(#[from] PropError), - #[error("Empty Graph: {0}")] - EmptyGraphDir(PathBuf), - #[error("Failed to parse time string")] - ParseTime { - #[from] - source: ParseTimeError, - }, - #[error("Failed to mutate: {0}")] - MutationError(#[from] MutationError), - #[error("Unnamed Failure: {0}")] - GenericFailure(String), - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] -#[repr(transparent)] -pub struct LocalPOS(pub usize); - -impl LocalPOS { - pub fn as_vid(self, page_id: usize, max_page_len: usize) -> VID { - VID(page_id * max_page_len + self.0) - } - - pub fn as_eid(self, page_id: usize, max_page_len: usize) -> EID { - EID(page_id * max_page_len + self.0) - } -} - -impl From for LocalPOS { - fn from(pos: usize) -> Self { - Self(pos) - } -} - -pub fn calculate_size_recursive(path: &Path) -> Result { - let mut size = 0; - if path.is_dir() { - for entry in std::fs::read_dir(path)? { - let entry = entry?; - let path = entry.path(); - if path.is_dir() { - size += calculate_size_recursive(&path)?; - } else { - size += path.metadata()?.len() as usize; - } - } - } else { - size += path.metadata()?.len() as usize; - } - Ok(size) -} diff --git a/db4-graph/Cargo.toml b/db4-graph/Cargo.toml index a1fc9560d3..c8fcb61858 100644 --- a/db4-graph/Cargo.toml +++ b/db4-graph/Cargo.toml @@ -15,7 +15,5 @@ edition.workspace = true boxcar.workspace = true storage.workspace = true raphtory-api.workspace = true -raphtory-storage.workspace = true raphtory-core.workspace = true -db4-common.workspace = true parking_lot.workspace = true diff --git a/db4-graph/src/lib.rs b/db4-graph/src/lib.rs index 2c64f7d374..b8b7fc4fe8 100644 --- a/db4-graph/src/lib.rs +++ b/db4-graph/src/lib.rs @@ -1,10 +1,9 @@ use std::{ ops::DerefMut, - path::PathBuf, + path::{Path, PathBuf}, sync::{atomic::AtomicUsize, Arc}, }; -use db4_common::error::DBV4Error; use parking_lot::RwLockWriteGuard; use raphtory_api::core::{ entities::{ @@ -20,11 +19,12 @@ use raphtory_core::{ entities::{graph::logical_to_physical::Mapping, nodes::node_ref::NodeRef, ELID}, storage::{raw_edges::WriteLockedEdges, WriteLockedNodes}, }; -use raphtory_storage::mutation::{ - addition_ops::{AtomicAdditionOps, InternalAdditionOps, SessionAdditionOps}, - MutationError, -}; +// use raphtory_storage::mutation::{ +// addition_ops::{AtomicAdditionOps, InternalAdditionOps, SessionAdditionOps}, +// MutationError, +// }; use storage::{ + error::DBV4Error, pages::session::WriteSession, persist::strategy::PersistentStrategy, properties::props_meta_writer::PropsMetaWriter, @@ -49,232 +49,28 @@ pub struct TemporalGraph { node_meta: Arc, } -#[repr(transparent)] -pub struct WriteS< - 'a, - MNS: DerefMut, - MES: DerefMut, - EXT: PersistentStrategy, ES = ES>, ->(WriteSession<'a, MNS, MES, NS, ES, EXT>); - -pub struct UnlockedSession<'a, EXT> { - graph: &'a TemporalGraph, -} - -impl< - 'a, - MNS: DerefMut + Send + Sync, - MES: DerefMut + Send + Sync, - EXT: PersistentStrategy, ES = ES>, - > AtomicAdditionOps for WriteS<'a, MNS, MES, EXT> -{ - fn internal_add_edge( - &mut self, - t: TimeIndexEntry, - src: impl Into, - dst: impl Into, - lsn: u64, - layer: usize, - props: impl IntoIterator, - ) -> MaybeNew { - self.0.internal_add_edge(t, src, dst, lsn, layer, props) - } -} - -impl<'a, EXT: Send + Sync> SessionAdditionOps for UnlockedSession<'a, EXT> { - type Error = DBV4Error; - - fn next_event_id(&self) -> Result { - todo!() - } - - fn reserve_event_ids(&self, num_ids: usize) -> Result { - todo!() - } - - fn set_node(&self, gid: GidRef, vid: VID) -> Result<(), Self::Error> { - todo!() - } - - fn resolve_graph_property( - &self, - prop: &str, - dtype: PropType, - is_static: bool, - ) -> Result, Self::Error> { - todo!() - } - - fn resolve_node_property( - &self, - prop: &str, - dtype: PropType, - is_static: bool, - ) -> Result, Self::Error> { - todo!() - } - - fn resolve_edge_property( - &self, - prop: &str, - dtype: PropType, - is_static: bool, - ) -> Result, Self::Error> { - todo!() - } - - fn internal_add_node( - &self, - t: TimeIndexEntry, - v: VID, - props: &[(usize, Prop)], - ) -> Result<(), Self::Error> { - todo!() - } - - fn internal_add_edge( - &self, - t: TimeIndexEntry, - src: VID, - dst: VID, - props: &[(usize, Prop)], - layer: usize, - ) -> Result, Self::Error> { - todo!() - } - - fn internal_add_edge_update( - &self, - t: TimeIndexEntry, - edge: EID, - props: &[(usize, Prop)], - layer: usize, - ) -> Result<(), Self::Error> { - todo!() - } -} - -impl, ES = ES>> InternalAdditionOps - for TemporalGraph -{ - type Error = DBV4Error; - - type WS<'a> - = UnlockedSession<'a, EXT> - where - EXT: 'a; - - type AtomicAddEdge<'a> = - WriteS<'a, RwLockWriteGuard<'a, MemNodeSegment>, RwLockWriteGuard<'a, MemEdgeSegment>, EXT>; - - fn write_lock(&self) -> Result { - todo!() - } - - fn write_lock_nodes(&self) -> Result { - todo!() - } - - fn write_lock_edges(&self) -> Result { - todo!() - } - - fn resolve_layer(&self, layer: Option<&str>) -> Result, Self::Error> { - let id = self.edge_meta.get_or_create_layer_id(layer); - - let layer_id = id.inner(); - if self.layers.get(layer_id).is_some() { - return Ok(id); - } - let count = self.layers.count(); - if count >= layer_id + 1 { - // something has allocated the layer, wait for it to be added - while self.layers.get(layer_id).is_none() { - // wait for the layer to be created - std::thread::yield_now(); - } - return Ok(id); - } else { - self.layers.reserve(2); - let layer_name = layer.unwrap_or("_default"); - loop { - let new_layer_id = self.layers.push_with(|_| { - Layer::new( - self.graph_dir.join(format!("l_{}", layer_name)), - self.max_page_len_nodes, - self.max_page_len_edges, - ) - }); - if new_layer_id >= layer_id { - while self.layers.get(new_layer_id).is_none() { - // wait for the layer to be created - std::thread::yield_now(); - } - return Ok(id); - } - } - } - } - - fn resolve_node(&self, id: NodeRef) -> Result, Self::Error> { - match id { - NodeRef::External(id) => { - let id = self - .logical_to_physical - .get_or_init_vid(id, || { - self.node_count - .fetch_add(1, std::sync::atomic::Ordering::Relaxed) - .into() - }) - .map_err(MutationError::InvalidNodeId)?; - Ok(id) - } - NodeRef::Internal(id) => Ok(MaybeNew::Existing(id)), - } +impl, ES = ES>> TemporalGraph { + pub fn layers(&self) -> &boxcar::Vec> { + &self.layers } - fn resolve_node_and_type( - &self, - id: NodeRef, - node_type: &str, - ) -> Result, MaybeNew)>, Self::Error> { - todo!() + pub fn edge_meta(&self) -> &Arc { + &self.edge_meta } - fn validate_gids<'a>( - &self, - gids: impl IntoIterator>, - ) -> Result<(), Self::Error> { - Ok(self - .logical_to_physical - .validate_gids(gids) - .map_err(MutationError::InvalidNodeId)?) + pub fn node_meta(&self) -> &Arc { + &self.node_meta } - fn write_session(&self) -> Result, Self::Error> { - todo!() + pub fn graph_dir(&self) -> &Path { + &self.graph_dir } - fn atomic_add_edge( - &self, - src: VID, - dst: VID, - e_id: Option, - layer_id: usize, - ) -> Self::AtomicAddEdge<'_> { - let layer = &self.layers[layer_id]; - WriteS(layer.write_session(src, dst, e_id)) + pub fn max_page_len_nodes(&self) -> usize { + self.max_page_len_nodes } - fn validate_edge_props>( - &self, - is_static: bool, - props: impl ExactSizeIterator, - ) -> Result, Self::Error> { - if is_static { - PropsMetaWriter::constant(&self.edge_meta, props)?.into_props_const() - } else { - PropsMetaWriter::temporal(&self.edge_meta, props)?.into_props_temporal() - } + pub fn max_page_len_edges(&self) -> usize { + self.max_page_len_edges } } diff --git a/db4-storage/Cargo.toml b/db4-storage/Cargo.toml index 846b569ab8..3b382524cc 100644 --- a/db4-storage/Cargo.toml +++ b/db4-storage/Cargo.toml @@ -12,8 +12,8 @@ edition = "2024" [dependencies] raphtory-api.workspace = true -raphtory = {workspace = true, features = ["arrow", "io"]} -db4-common = {path = "../db4-common"} +raphtory-core = {workspace = true} +# db4-common = {path = "../db4-common"} bitvec.workspace = true bigdecimal.workspace = true @@ -32,6 +32,7 @@ parquet.workspace = true bytemuck.workspace = true rayon.workspace = true iter-enum = {workspace = true, features = ["rayon"]} +thiserror.workspace = true proptest = {workspace = true, optional = true} tempfile = {workspace = true, optional = true} diff --git a/db4-storage/src/lib.rs b/db4-storage/src/lib.rs index 3fe78b9cd6..0137ee35a1 100644 --- a/db4-storage/src/lib.rs +++ b/db4-storage/src/lib.rs @@ -5,10 +5,10 @@ use std::{ }; use crate::{ + error::DBV4Error, pages::GraphStore, segments::{edge::EdgeSegmentView, node::NodeSegmentView}, }; -use db4_common::{LocalPOS, error::DBV4Error}; use parking_lot::{RwLockReadGuard, RwLockWriteGuard}; use raphtory_api::core::{ entities::{ @@ -19,7 +19,7 @@ use raphtory_api::core::{ }; use segments::{edge::MemEdgeSegment, node::MemNodeSegment}; -pub mod loaders; +// pub mod loaders; pub mod pages; pub mod persist; pub mod properties; @@ -237,3 +237,78 @@ pub trait NodeRefOps<'a>: Copy + Clone + Send + Sync { fn t_prop(self, prop_id: usize) -> Self::TProps; } + +pub mod error { + use std::{path::PathBuf, sync::Arc}; + + use raphtory_api::core::entities::properties::prop::PropError; + use raphtory_core::utils::time::ParseTimeError; + + #[derive(thiserror::Error, Debug)] + pub enum DBV4Error { + #[error("External Storage Error {0}")] + External(#[from] Arc), + #[error("IO error: {0}")] + IO(#[from] std::io::Error), + #[error("Serde error: {0}")] + Serde(#[from] serde_json::Error), + // #[error("Load error: {0}")] + // LoadError(#[from] LoadError), + #[error("Arrow-rs error: {0}")] + ArrowRS(#[from] arrow_schema::ArrowError), + #[error("Parquet error: {0}")] + Parquet(#[from] parquet::errors::ParquetError), + + #[error("Property error: {0}")] + PropError(#[from] PropError), + #[error("Empty Graph: {0}")] + EmptyGraphDir(PathBuf), + #[error("Failed to parse time string")] + ParseTime { + #[from] + source: ParseTimeError, + }, + // #[error("Failed to mutate: {0}")] + // MutationError(#[from] MutationError), + #[error("Unnamed Failure: {0}")] + GenericFailure(String), + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[repr(transparent)] +pub struct LocalPOS(pub usize); + +impl LocalPOS { + pub fn as_vid(self, page_id: usize, max_page_len: usize) -> VID { + VID(page_id * max_page_len + self.0) + } + + pub fn as_eid(self, page_id: usize, max_page_len: usize) -> EID { + EID(page_id * max_page_len + self.0) + } +} + +impl From for LocalPOS { + fn from(pos: usize) -> Self { + Self(pos) + } +} + +pub fn calculate_size_recursive(path: &Path) -> Result { + let mut size = 0; + if path.is_dir() { + for entry in std::fs::read_dir(path)? { + let entry = entry?; + let path = entry.path(); + if path.is_dir() { + size += calculate_size_recursive(&path)?; + } else { + size += path.metadata()?.len() as usize; + } + } + } else { + size += path.metadata()?.len() as usize; + } + Ok(size) +} diff --git a/db4-storage/src/loaders/mod.rs b/db4-storage/src/loaders/mod.rs index 0f3a55545f..9d84409234 100644 --- a/db4-storage/src/loaders/mod.rs +++ b/db4-storage/src/loaders/mod.rs @@ -1,4 +1,4 @@ -use crate::{EdgeSegmentOps, NodeSegmentOps, pages::GraphStore}; +use crate::{error::DBV4Error, pages::GraphStore, EdgeSegmentOps, NodeSegmentOps}; use arrow::buffer::ScalarBuffer; use arrow_array::{ Array, PrimitiveArray, RecordBatch, TimestampMicrosecondArray, TimestampMillisecondArray, @@ -7,22 +7,13 @@ use arrow_array::{ use arrow_csv::reader::Format; use arrow_schema::{ArrowError, DataType, Schema, TimeUnit}; use bytemuck::checked::cast_slice_mut; -use db4_common::error::DBV4Error; use either::Either; use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder; -use raphtory::{ - atomic_extra::atomic_usize_from_mut_slice, - core::entities::{EID, VID, graph::logical_to_physical::Mapping}, - errors::LoadError, - io::arrow::{ - node_col::NodeCol, - prop_handler::{PropCols, combine_properties_arrow}, - }, -}; use raphtory_api::core::{ entities::properties::prop::PropType, storage::{dict_mapper::MaybeNew, timeindex::TimeIndexEntry}, }; +use raphtory_core::entities::{graph::logical_to_physical::Mapping, EID, VID}; use rayon::prelude::*; use std::{ fs::File, diff --git a/db4-storage/src/pages/edge_page/writer.rs b/db4-storage/src/pages/edge_page/writer.rs index 3fff89663b..3004bf33df 100644 --- a/db4-storage/src/pages/edge_page/writer.rs +++ b/db4-storage/src/pages/edge_page/writer.rs @@ -1,9 +1,8 @@ use std::{ops::DerefMut, sync::atomic::AtomicUsize}; -use crate::{EdgeSegmentOps, segments::edge::MemEdgeSegment}; -use db4_common::{LocalPOS, error::DBV4Error}; -use raphtory::{core::storage::timeindex::AsTime, prelude::Prop}; -use raphtory_api::core::entities::VID; +use crate::{EdgeSegmentOps, LocalPOS, error::DBV4Error, segments::edge::MemEdgeSegment}; +use raphtory_api::core::entities::{VID, properties::prop::Prop}; +use raphtory_core::storage::timeindex::AsTime; pub struct EdgeWriter<'a, MP: DerefMut, ES: EdgeSegmentOps> { pub page: &'a ES, diff --git a/db4-storage/src/pages/edge_store.rs b/db4-storage/src/pages/edge_store.rs index a0aec33c27..b28e9bc75d 100644 --- a/db4-storage/src/pages/edge_store.rs +++ b/db4-storage/src/pages/edge_store.rs @@ -9,14 +9,14 @@ use std::{ use super::{edge_page::writer::EdgeWriter, resolve_pos}; use crate::{ - EdgeSegmentOps, + EdgeSegmentOps, LocalPOS, + error::DBV4Error, pages::locked::edges::{LockedEdgePage, WriteLockedEdgePages}, segments::edge::MemEdgeSegment, }; -use db4_common::{LocalPOS, error::DBV4Error}; use parking_lot::{RwLock, RwLockWriteGuard}; -use raphtory::core::storage::timeindex::TimeIndexEntry; use raphtory_api::core::entities::{EID, VID, properties::meta::Meta}; +use raphtory_core::storage::timeindex::TimeIndexEntry; const N: usize = 32; diff --git a/db4-storage/src/pages/locked/edges.rs b/db4-storage/src/pages/locked/edges.rs index 6723e8a7c3..ab59943bb7 100644 --- a/db4-storage/src/pages/locked/edges.rs +++ b/db4-storage/src/pages/locked/edges.rs @@ -1,13 +1,12 @@ use std::{ops::DerefMut, sync::atomic::AtomicUsize}; use crate::{ - EdgeSegmentOps, + EdgeSegmentOps, LocalPOS, pages::{edge_page::writer::EdgeWriter, resolve_pos}, segments::edge::MemEdgeSegment, }; -use db4_common::LocalPOS; use parking_lot::RwLockWriteGuard; -use raphtory::core::entities::EID; +use raphtory_core::entities::EID; use rayon::prelude::*; pub struct LockedEdgePage<'a, ES> { diff --git a/db4-storage/src/pages/locked/nodes.rs b/db4-storage/src/pages/locked/nodes.rs index 49904c51a7..8dcf0a6f45 100644 --- a/db4-storage/src/pages/locked/nodes.rs +++ b/db4-storage/src/pages/locked/nodes.rs @@ -1,11 +1,10 @@ use crate::{ - NodeSegmentOps, + LocalPOS, NodeSegmentOps, pages::{node_page::writer::NodeWriter, resolve_pos}, segments::node::MemNodeSegment, }; -use db4_common::LocalPOS; use parking_lot::RwLockWriteGuard; -use raphtory::core::entities::VID; +use raphtory_core::entities::VID; use rayon::prelude::*; use std::{ops::DerefMut, sync::atomic::AtomicUsize}; diff --git a/db4-storage/src/pages/mod.rs b/db4-storage/src/pages/mod.rs index 72baea74fc..d1eb096e48 100644 --- a/db4-storage/src/pages/mod.rs +++ b/db4-storage/src/pages/mod.rs @@ -8,25 +8,25 @@ use std::{ }; use crate::{ - EdgeSegmentOps, NodeSegmentOps, + EdgeSegmentOps, LocalPOS, NodeSegmentOps, + error::DBV4Error, properties::props_meta_writer::PropsMetaWriter, segments::{edge::MemEdgeSegment, node::MemNodeSegment}, }; -use db4_common::{LocalPOS, error::DBV4Error}; use edge_page::writer::EdgeWriter; use edge_store::EdgeStorageInner; use node_page::writer::{NodeWriter, WriterPair}; use node_store::NodeStorageInner; use parking_lot::RwLockWriteGuard; -use raphtory::{ - core::{ - entities::{EID, ELID, VID}, - storage::timeindex::{AsTime, TimeIndexEntry}, - utils::time::{InputTime, TryIntoInputTime}, - }, - prelude::Prop, +use raphtory_api::core::{ + entities::properties::{meta::Meta, prop::Prop}, + storage::dict_mapper::MaybeNew, +}; +use raphtory_core::{ + entities::{EID, ELID, VID}, + storage::timeindex::{AsTime, TimeIndexEntry}, + utils::time::{InputTime, TryIntoInputTime}, }; -use raphtory_api::core::{entities::properties::meta::Meta, storage::dict_mapper::MaybeNew}; use serde::{Deserialize, Serialize}; use session::WriteSession; @@ -501,10 +501,8 @@ mod test { use chrono::{DateTime, NaiveDateTime, Utc}; use core::panic; use proptest::prelude::*; - use raphtory::{ - core::{entities::VID, storage::timeindex::TimeIndexOps}, - prelude::Prop, - }; + use raphtory_api::core::entities::properties::prop::Prop; + use raphtory_core::{entities::VID, storage::timeindex::TimeIndexOps}; fn check_edges( edges: Vec<(impl Into, impl Into)>, diff --git a/db4-storage/src/pages/node_page/writer.rs b/db4-storage/src/pages/node_page/writer.rs index 272276ea99..074cdfaeba 100644 --- a/db4-storage/src/pages/node_page/writer.rs +++ b/db4-storage/src/pages/node_page/writer.rs @@ -1,10 +1,6 @@ -use crate::{NodeSegmentOps, segments::node::MemNodeSegment}; -use db4_common::LocalPOS; -use raphtory::{ - core::{entities::ELID, storage::timeindex::AsTime}, - prelude::Prop, -}; -use raphtory_api::core::entities::{EID, VID}; +use crate::{LocalPOS, NodeSegmentOps, segments::node::MemNodeSegment}; +use raphtory_api::core::entities::{EID, VID, properties::prop::Prop}; +use raphtory_core::{entities::ELID, storage::timeindex::AsTime}; use std::{ops::DerefMut, sync::atomic::AtomicUsize}; #[derive(Debug)] diff --git a/db4-storage/src/pages/node_store.rs b/db4-storage/src/pages/node_store.rs index c055ea0c38..189ba4e41a 100644 --- a/db4-storage/src/pages/node_store.rs +++ b/db4-storage/src/pages/node_store.rs @@ -1,16 +1,15 @@ use super::{node_page::writer::NodeWriter, resolve_pos}; use crate::{ - NodeSegmentOps, + LocalPOS, NodeSegmentOps, + error::DBV4Error, pages::locked::nodes::{LockedNodePage, WriteLockedNodePages}, segments::node::MemNodeSegment, }; -use db4_common::{LocalPOS, error::DBV4Error}; use parking_lot::RwLockWriteGuard; -use raphtory::core::entities::{EID, VID}; use raphtory_api::core::entities::properties::meta::Meta; +use raphtory_core::entities::{EID, VID}; use std::{ collections::HashMap, - ops::DerefMut, path::{Path, PathBuf}, sync::{ Arc, diff --git a/db4-storage/src/pages/session.rs b/db4-storage/src/pages/session.rs index 6f3dff86f9..b964df20fb 100644 --- a/db4-storage/src/pages/session.rs +++ b/db4-storage/src/pages/session.rs @@ -5,17 +5,14 @@ use super::{ }; use crate::{ EdgeSegmentOps, NodeSegmentOps, + error::DBV4Error, segments::{edge::MemEdgeSegment, node::MemNodeSegment}, }; -use db4_common::error::DBV4Error; -use raphtory::{ - core::{ - entities::{EID, ELID, VID}, - storage::timeindex::AsTime, - }, - prelude::Prop, +use raphtory_api::core::{entities::properties::prop::Prop, storage::dict_mapper::MaybeNew}; +use raphtory_core::{ + entities::{EID, ELID, VID}, + storage::timeindex::AsTime, }; -use raphtory_api::core::storage::dict_mapper::MaybeNew; pub struct WriteSession< 'a, diff --git a/db4-storage/src/pages/test_utils.rs b/db4-storage/src/pages/test_utils.rs index 9f0e6b082c..e0dc2a1fa2 100644 --- a/db4-storage/src/pages/test_utils.rs +++ b/db4-storage/src/pages/test_utils.rs @@ -7,27 +7,19 @@ use std::{ use bigdecimal::BigDecimal; use chrono::{DateTime, NaiveDateTime, Utc}; -use db4_common::error::DBV4Error; use either::Either::Left; use itertools::Itertools; use proptest::{collection, prelude::*}; -use raphtory::{ - core::{ - entities::{VID, graph::logical_to_physical::Mapping}, - storage::timeindex::TimeIndexOps, - }, - prelude::Prop, -}; use raphtory_api::core::entities::properties::{ - prop::{DECIMAL_MAX, PropType}, + prop::{DECIMAL_MAX, Prop, PropType}, tprop::TPropOps, }; +use raphtory_core::{entities::VID, storage::timeindex::TimeIndexOps}; use rayon::prelude::*; use crate::{ EdgeEntryOps, EdgeRefOps, EdgeSegmentOps, NodeEntryOps, NodeRefOps, NodeSegmentOps, - loaders::{FileFormat, Loader}, - pages::GraphStore, + error::DBV4Error, pages::GraphStore, }; pub fn check_edges_support< @@ -444,184 +436,184 @@ pub fn check_graph_with_props_support< } } -pub fn check_load_support< - EXT: Clone + Default + Send + Sync, - NS: NodeSegmentOps, - ES: EdgeSegmentOps, ->( - edges: &[(i64, u64, u64)], - check_load: bool, - make_graph: impl FnOnce(&Path) -> GraphStore, -) { - // Create temporary directory and CSV file - let temp_dir = tempfile::tempdir().unwrap(); - let csv_path = temp_dir.path().join("edges.csv"); - - // Write edges to CSV file - let mut file = File::create(&csv_path).unwrap(); - writeln!(file, "src,time,dst,test").unwrap(); - for (time, src, dst) in edges { - writeln!(file, "{},{},{},a", src, time, dst).unwrap(); - } - file.flush().unwrap(); - - // Create graph store - let graph_dir = temp_dir.path().join("graph"); - std::fs::create_dir_all(&graph_dir).unwrap(); - let graph = make_graph(&graph_dir); - - // Create loader and load data - let loader = Loader::new( - &csv_path, - Left("src"), - Left("dst"), - Left("time"), - FileFormat::CSV { - delimiter: b',', - has_header: true, - sample_records: 10, - }, - ) - .unwrap(); - - let resolver = loader.load_into(&graph, 1024).unwrap(); - - fn check_graph< - NS: NodeSegmentOps, - ES: EdgeSegmentOps, - EXT: Clone + Default + Send + Sync, - >( - edges: &[(i64, u64, u64)], - graph: &GraphStore, - resolver: &Mapping, - label: &str, - ) { - // Create expected adjacency data - let mut expected_out_edges: HashMap> = HashMap::new(); - let mut expected_in_edges: HashMap> = HashMap::new(); - let mut reverse_resolver = vec![0; resolver.len()]; - - for &(_, src, dst) in edges { - expected_out_edges.entry(src).or_default().push(dst); - expected_in_edges.entry(dst).or_default().push(src); - let id = resolver - .get_u64(src) - .unwrap_or_else(|| panic!("Missing src node {}", src)); - reverse_resolver[id.0] = src; - let id = resolver - .get_u64(dst) - .unwrap_or_else(|| panic!("Missing dst node {}", dst)); - reverse_resolver[id.0] = dst; - } - - // Deduplicate expected edges - for values in expected_out_edges.values_mut() { - values.sort_unstable(); - values.dedup(); - } - - for values in expected_in_edges.values_mut() { - values.sort_unstable(); - values.dedup(); - } - - let expected_num_edges = expected_out_edges.values().map(Vec::len).sum::(); - // let expected_num_nodes = expected_out_edges.keys().chain(expected_in_edges.keys()).collect::>().len(); - - // Verify graph structure - let nodes = graph.nodes(); - let edges_store = graph.edges(); - - // assert_eq!(nodes.num_nodes(), expected_num_nodes); - assert_eq!( - edges_store.num_edges(), - expected_num_edges, - "Bad number of edges {label}" - ); - - for (exp_src, expected_outs) in expected_out_edges { - for &exp_dst in &expected_outs { - let src_vid = resolver.get_u64(exp_src).unwrap(); - let dst_vid = resolver.get_u64(exp_dst).unwrap(); - - let edge_id = graph - .nodes() - .get_edge(src_vid, dst_vid) - .expect("Edge not found"); - let edge = edges_store.edge(edge_id); - let (src, dst) = edge.as_ref().edge().unwrap(); - let (src_act, dst_act) = (reverse_resolver[src.0], reverse_resolver[dst.0]); - - assert_eq!( - (src_act, dst_act), - (exp_src, exp_dst), - "{label} Bad Edge {} -> {}", - exp_src, - exp_dst, - ); - } - - let adj = graph.nodes().node(resolver.get_u64(exp_src).unwrap()); - let adj = adj.as_ref(); - - let mut out_neighbours: Vec<_> = adj - .out_nbrs_sorted() - .map(|VID(id)| reverse_resolver[id]) - .collect(); - out_neighbours.sort_unstable(); - let mut expected_outs: Vec<_> = expected_outs.iter().copied().collect(); - expected_outs.sort_unstable(); - - assert_eq!( - out_neighbours, expected_outs, - "{label} Outbound edges don't match for node {}", - exp_src - ); - - // Check edge lookup works and edge_id points to the right (src, dst) - for (exp_dst, edge_id) in adj.out_edges() { - let (VID(src), dst) = edges_store.get_edge(edge_id).unwrap(); - assert_eq!(reverse_resolver[src], exp_src); - assert_eq!(dst, exp_dst); - } - } - - for (exp_dst, expected_ins) in expected_in_edges { - let adj = nodes.node(resolver.get_u64(exp_dst).unwrap()); - let adj = adj.as_ref(); - - let mut in_neighbours: Vec<_> = adj - .inb_nbrs_sorted() - .map(|VID(id)| reverse_resolver[id]) - .collect(); - in_neighbours.sort_unstable(); - let mut expected_ins: Vec<_> = expected_ins.iter().copied().collect(); - expected_ins.sort_unstable(); - - assert_eq!( - in_neighbours, expected_ins, - "Inbound edges don't match for node {}", - exp_dst - ); - - // Check edge lookup works - for (exp_src, edge_id) in adj.inb_edges() { - let (src, VID(dst)) = edges_store.get_edge(edge_id).unwrap(); - assert_eq!(reverse_resolver[dst], exp_dst); - assert_eq!(src, exp_src); - } - } - } - - check_graph(edges, &graph, &resolver, "pre-drop"); - if check_load { - drop(graph); - - // Reload graph and check again - let graph = GraphStore::::load(&graph_dir).unwrap(); - check_graph(edges, &graph, &resolver, "post-drop"); - } -} +// pub fn check_load_support< +// EXT: Clone + Default + Send + Sync, +// NS: NodeSegmentOps, +// ES: EdgeSegmentOps, +// >( +// edges: &[(i64, u64, u64)], +// check_load: bool, +// make_graph: impl FnOnce(&Path) -> GraphStore, +// ) { +// // Create temporary directory and CSV file +// let temp_dir = tempfile::tempdir().unwrap(); +// let csv_path = temp_dir.path().join("edges.csv"); + +// // Write edges to CSV file +// let mut file = File::create(&csv_path).unwrap(); +// writeln!(file, "src,time,dst,test").unwrap(); +// for (time, src, dst) in edges { +// writeln!(file, "{},{},{},a", src, time, dst).unwrap(); +// } +// file.flush().unwrap(); + +// // Create graph store +// let graph_dir = temp_dir.path().join("graph"); +// std::fs::create_dir_all(&graph_dir).unwrap(); +// let graph = make_graph(&graph_dir); + +// // Create loader and load data +// let loader = Loader::new( +// &csv_path, +// Left("src"), +// Left("dst"), +// Left("time"), +// FileFormat::CSV { +// delimiter: b',', +// has_header: true, +// sample_records: 10, +// }, +// ) +// .unwrap(); + +// let resolver = loader.load_into(&graph, 1024).unwrap(); + +// fn check_graph< +// NS: NodeSegmentOps, +// ES: EdgeSegmentOps, +// EXT: Clone + Default + Send + Sync, +// >( +// edges: &[(i64, u64, u64)], +// graph: &GraphStore, +// resolver: &Mapping, +// label: &str, +// ) { +// // Create expected adjacency data +// let mut expected_out_edges: HashMap> = HashMap::new(); +// let mut expected_in_edges: HashMap> = HashMap::new(); +// let mut reverse_resolver = vec![0; resolver.len()]; + +// for &(_, src, dst) in edges { +// expected_out_edges.entry(src).or_default().push(dst); +// expected_in_edges.entry(dst).or_default().push(src); +// let id = resolver +// .get_u64(src) +// .unwrap_or_else(|| panic!("Missing src node {}", src)); +// reverse_resolver[id.0] = src; +// let id = resolver +// .get_u64(dst) +// .unwrap_or_else(|| panic!("Missing dst node {}", dst)); +// reverse_resolver[id.0] = dst; +// } + +// // Deduplicate expected edges +// for values in expected_out_edges.values_mut() { +// values.sort_unstable(); +// values.dedup(); +// } + +// for values in expected_in_edges.values_mut() { +// values.sort_unstable(); +// values.dedup(); +// } + +// let expected_num_edges = expected_out_edges.values().map(Vec::len).sum::(); +// // let expected_num_nodes = expected_out_edges.keys().chain(expected_in_edges.keys()).collect::>().len(); + +// // Verify graph structure +// let nodes = graph.nodes(); +// let edges_store = graph.edges(); + +// // assert_eq!(nodes.num_nodes(), expected_num_nodes); +// assert_eq!( +// edges_store.num_edges(), +// expected_num_edges, +// "Bad number of edges {label}" +// ); + +// for (exp_src, expected_outs) in expected_out_edges { +// for &exp_dst in &expected_outs { +// let src_vid = resolver.get_u64(exp_src).unwrap(); +// let dst_vid = resolver.get_u64(exp_dst).unwrap(); + +// let edge_id = graph +// .nodes() +// .get_edge(src_vid, dst_vid) +// .expect("Edge not found"); +// let edge = edges_store.edge(edge_id); +// let (src, dst) = edge.as_ref().edge().unwrap(); +// let (src_act, dst_act) = (reverse_resolver[src.0], reverse_resolver[dst.0]); + +// assert_eq!( +// (src_act, dst_act), +// (exp_src, exp_dst), +// "{label} Bad Edge {} -> {}", +// exp_src, +// exp_dst, +// ); +// } + +// let adj = graph.nodes().node(resolver.get_u64(exp_src).unwrap()); +// let adj = adj.as_ref(); + +// let mut out_neighbours: Vec<_> = adj +// .out_nbrs_sorted() +// .map(|VID(id)| reverse_resolver[id]) +// .collect(); +// out_neighbours.sort_unstable(); +// let mut expected_outs: Vec<_> = expected_outs.iter().copied().collect(); +// expected_outs.sort_unstable(); + +// assert_eq!( +// out_neighbours, expected_outs, +// "{label} Outbound edges don't match for node {}", +// exp_src +// ); + +// // Check edge lookup works and edge_id points to the right (src, dst) +// for (exp_dst, edge_id) in adj.out_edges() { +// let (VID(src), dst) = edges_store.get_edge(edge_id).unwrap(); +// assert_eq!(reverse_resolver[src], exp_src); +// assert_eq!(dst, exp_dst); +// } +// } + +// for (exp_dst, expected_ins) in expected_in_edges { +// let adj = nodes.node(resolver.get_u64(exp_dst).unwrap()); +// let adj = adj.as_ref(); + +// let mut in_neighbours: Vec<_> = adj +// .inb_nbrs_sorted() +// .map(|VID(id)| reverse_resolver[id]) +// .collect(); +// in_neighbours.sort_unstable(); +// let mut expected_ins: Vec<_> = expected_ins.iter().copied().collect(); +// expected_ins.sort_unstable(); + +// assert_eq!( +// in_neighbours, expected_ins, +// "Inbound edges don't match for node {}", +// exp_dst +// ); + +// // Check edge lookup works +// for (exp_src, edge_id) in adj.inb_edges() { +// let (src, VID(dst)) = edges_store.get_edge(edge_id).unwrap(); +// assert_eq!(reverse_resolver[dst], exp_dst); +// assert_eq!(src, exp_src); +// } +// } +// } + +// check_graph(edges, &graph, &resolver, "pre-drop"); +// if check_load { +// drop(graph); + +// // Reload graph and check again +// let graph = GraphStore::::load(&graph_dir).unwrap(); +// check_graph(edges, &graph, &resolver, "post-drop"); +// } +// } pub fn edges_strat(size: usize) -> impl Strategy> { (1..=size).prop_flat_map(|num_nodes| { diff --git a/db4-storage/src/persist/strategy.rs b/db4-storage/src/persist/strategy.rs index a2418559d8..5449e3344f 100644 --- a/db4-storage/src/persist/strategy.rs +++ b/db4-storage/src/persist/strategy.rs @@ -1,4 +1,3 @@ - use std::ops::DerefMut; use crate::segments::{ diff --git a/db4-storage/src/properties/mod.rs b/db4-storage/src/properties/mod.rs index 900e8cd565..97588856d3 100644 --- a/db4-storage/src/properties/mod.rs +++ b/db4-storage/src/properties/mod.rs @@ -1,17 +1,17 @@ use bigdecimal::ToPrimitive; use polars_arrow::array::{Array, BooleanArray, PrimitiveArray, Utf8ViewArray}; -use raphtory::{ - core::{ - entities::{ - ELID, - nodes::node_store::PropTimestamps, - properties::{tcell::TCell, tprop::TPropCell}, - }, - storage::{PropColumn, TColumns, timeindex::TimeIndexEntry}, +use raphtory_api::core::entities::properties::{ + meta::PropMapper, + prop::{Prop, PropType}, +}; +use raphtory_core::{ + entities::{ + ELID, + nodes::node_store::PropTimestamps, + properties::{tcell::TCell, tprop::TPropCell}, }, - prelude::Prop, + storage::{PropColumn, TColumns, timeindex::TimeIndexEntry}, }; -use raphtory_api::core::entities::properties::{meta::PropMapper, prop::PropType}; pub mod props_meta_writer; diff --git a/db4-storage/src/properties/props_meta_writer.rs b/db4-storage/src/properties/props_meta_writer.rs index 9f4c8395b3..b23d47bb96 100644 --- a/db4-storage/src/properties/props_meta_writer.rs +++ b/db4-storage/src/properties/props_meta_writer.rs @@ -1,10 +1,11 @@ -use db4_common::error::DBV4Error; use either::Either; use raphtory_api::core::entities::properties::{ meta::{LockedPropMapper, Meta, PropMapper}, prop::{Prop, unify_types}, }; +use crate::error::DBV4Error; + pub enum PropsMetaWriter<'a, PN: AsRef> { Change { props: Vec>, diff --git a/db4-storage/src/segments/additions.rs b/db4-storage/src/segments/additions.rs index 0370071cc7..9699f3672b 100644 --- a/db4-storage/src/segments/additions.rs +++ b/db4-storage/src/segments/additions.rs @@ -1,7 +1,7 @@ use std::ops::Range; use iter_enum::{DoubleEndedIterator, ExactSizeIterator, FusedIterator, Iterator}; -use raphtory::core::{ +use raphtory_core::{ entities::nodes::node_store::PropTimestamps, storage::timeindex::{TimeIndexEntry, TimeIndexOps, TimeIndexWindow}, }; diff --git a/db4-storage/src/segments/edge.rs b/db4-storage/src/segments/edge.rs index b3507dcaa7..6865e8f071 100644 --- a/db4-storage/src/segments/edge.rs +++ b/db4-storage/src/segments/edge.rs @@ -6,14 +6,13 @@ use std::{ }, }; -use db4_common::LocalPOS; -use raphtory::{ - core::storage::timeindex::{AsTime, TimeIndexEntry}, - prelude::Prop, +use raphtory_api::core::entities::{ + VID, + properties::{meta::Meta, prop::Prop}, }; -use raphtory_api::core::entities::{VID, properties::meta::Meta}; +use raphtory_core::storage::timeindex::{AsTime, TimeIndexEntry}; -use crate::{EdgeSegmentOps, properties::PropMutEntry}; +use crate::{EdgeSegmentOps, LocalPOS, error::DBV4Error, properties::PropMutEntry}; use super::{HasRow, SegmentContainer, edge_entry::MemEdgeEntry}; @@ -175,7 +174,7 @@ impl EdgeSegmentOps for EdgeSegmentView { _meta: Arc, _path: impl AsRef, _ext: Self::Extension, - ) -> Result + ) -> Result where Self: Sized, { @@ -220,7 +219,7 @@ impl EdgeSegmentOps for EdgeSegmentView { fn notify_write( &self, _head_lock: impl DerefMut, - ) -> Result<(), db4_common::error::DBV4Error> { + ) -> Result<(), DBV4Error> { Ok(()) } diff --git a/db4-storage/src/segments/edge_entry.rs b/db4-storage/src/segments/edge_entry.rs index 47cbcbe9dc..95f724a3ba 100644 --- a/db4-storage/src/segments/edge_entry.rs +++ b/db4-storage/src/segments/edge_entry.rs @@ -1,7 +1,7 @@ -use db4_common::LocalPOS; -use raphtory::core::entities::{VID, properties::tprop::TPropCell}; +use raphtory_api::core::entities::properties::prop::Prop; +use raphtory_core::entities::{VID, properties::tprop::TPropCell}; -use crate::{EdgeEntryOps, EdgeRefOps}; +use crate::{EdgeEntryOps, EdgeRefOps, LocalPOS}; use super::{additions::MemAdditions, edge::MemEdgeSegment}; @@ -67,7 +67,7 @@ impl<'a> EdgeRefOps<'a> for MemEdgeRef<'a> { MemAdditions::Props(self.es.as_ref().additions(self.pos)) } - fn c_prop(self, prop_id: usize) -> Option { + fn c_prop(self, prop_id: usize) -> Option { self.es.as_ref().c_prop(self.pos, prop_id) } diff --git a/db4-storage/src/segments/mod.rs b/db4-storage/src/segments/mod.rs index c37af06a69..b6d83a2468 100644 --- a/db4-storage/src/segments/mod.rs +++ b/db4-storage/src/segments/mod.rs @@ -1,21 +1,19 @@ use std::{collections::hash_map::Entry, fmt::Debug, sync::Arc}; use bitvec::{order::Msb0, vec::BitVec}; -use db4_common::LocalPOS; use either::Either; -use raphtory::{ - core::{ - entities::{ - nodes::node_store::PropTimestamps, - properties::{tcell::TCell, tprop::TPropCell}, - }, - storage::timeindex::TimeIndexEntry, +use raphtory_api::core::entities::properties::{meta::Meta, prop::Prop}; +use raphtory_core::{ + entities::{ + nodes::node_store::PropTimestamps, + properties::{tcell::TCell, tprop::TPropCell}, }, - prelude::Prop, + storage::timeindex::TimeIndexEntry, }; -use raphtory_api::core::entities::properties::meta::Meta; use rustc_hash::FxHashMap; +use crate::LocalPOS; + use super::properties::{PropEntry, Properties}; pub mod edge; diff --git a/db4-storage/src/segments/node.rs b/db4-storage/src/segments/node.rs index 460d719445..71f1090b75 100644 --- a/db4-storage/src/segments/node.rs +++ b/db4-storage/src/segments/node.rs @@ -1,15 +1,14 @@ -use db4_common::{LocalPOS, error::DBV4Error}; use either::Either; -use raphtory::{ - core::{ - entities::{ELID, nodes::structure::adj::Adj}, - storage::timeindex::{AsTime, TimeIndexEntry}, - }, - prelude::Prop, -}; use raphtory_api::core::{ Direction, - entities::{EID, VID, properties::meta::Meta}, + entities::{ + EID, VID, + properties::{meta::Meta, prop::Prop}, + }, +}; +use raphtory_core::{ + entities::{ELID, nodes::structure::adj::Adj}, + storage::timeindex::{AsTime, TimeIndexEntry}, }; use std::{ ops::{Deref, DerefMut}, @@ -17,7 +16,7 @@ use std::{ }; use super::{HasRow, SegmentContainer}; -use crate::{NodeSegmentOps, segments::node_entry::MemNodeEntry}; +use crate::{LocalPOS, NodeSegmentOps, error::DBV4Error, segments::node_entry::MemNodeEntry}; #[derive(Debug)] pub struct MemNodeSegment { diff --git a/db4-storage/src/segments/node_entry.rs b/db4-storage/src/segments/node_entry.rs index 616a689788..ad0ddf0452 100644 --- a/db4-storage/src/segments/node_entry.rs +++ b/db4-storage/src/segments/node_entry.rs @@ -1,7 +1,6 @@ -use crate::{NodeEntryOps, NodeRefOps, segments::node::MemNodeSegment}; -use db4_common::LocalPOS; -use raphtory::{core::entities::properties::tprop::TPropCell, prelude::Prop}; -use raphtory_api::core::entities::{EID, VID}; +use crate::{LocalPOS, NodeEntryOps, NodeRefOps, segments::node::MemNodeSegment}; +use raphtory_api::core::entities::{EID, VID, properties::prop::Prop}; +use raphtory_core::entities::properties::tprop::TPropCell; use std::ops::Deref; use super::additions::MemAdditions; diff --git a/raphtory-storage/Cargo.toml b/raphtory-storage/Cargo.toml index 39f1b0deed..e1da9f430d 100644 --- a/raphtory-storage/Cargo.toml +++ b/raphtory-storage/Cargo.toml @@ -14,6 +14,9 @@ edition.workspace = true [dependencies] raphtory-api = { path = "../raphtory-api", version = "0.15.1" } raphtory-core = { path = "../raphtory-core", version = "0.15.1" } +storage.workspace = true +db4-graph.workspace = true +parking_lot.workspace = true rayon = { workspace = true } either = { workspace = true } iter-enum = { workspace = true } diff --git a/raphtory-storage/src/mutation/addition_ops_ext.rs b/raphtory-storage/src/mutation/addition_ops_ext.rs new file mode 100644 index 0000000000..f9bcfbc552 --- /dev/null +++ b/raphtory-storage/src/mutation/addition_ops_ext.rs @@ -0,0 +1,263 @@ +use std::ops::DerefMut; + +use db4_graph::TemporalGraph; +use parking_lot::RwLockWriteGuard; +use raphtory_api::core::{ + entities::properties::prop::{Prop, PropType}, + storage::dict_mapper::MaybeNew, +}; +use raphtory_core::{ + entities::{nodes::node_ref::NodeRef, GidRef, EID, ELID, VID}, + storage::{raw_edges::WriteLockedEdges, timeindex::TimeIndexEntry, WriteLockedNodes}, +}; +use storage::{ + pages::session::WriteSession, + persist::strategy::PersistentStrategy, + properties::props_meta_writer::PropsMetaWriter, + segments::{edge::MemEdgeSegment, node::MemNodeSegment}, + Layer, ES, NS, +}; + +use crate::{ + graph::locked::WriteLockedGraph, + mutation::{ + addition_ops::{AtomicAdditionOps, InternalAdditionOps, SessionAdditionOps}, + MutationError, + }, +}; + +#[repr(transparent)] +pub struct WriteS< + 'a, + MNS: DerefMut, + MES: DerefMut, + EXT: PersistentStrategy, ES = ES>, +>(WriteSession<'a, MNS, MES, NS, ES, EXT>); + +pub struct UnlockedSession<'a, EXT> { + graph: &'a TemporalGraph, +} + +impl< + 'a, + MNS: DerefMut + Send + Sync, + MES: DerefMut + Send + Sync, + EXT: PersistentStrategy, ES = ES>, + > AtomicAdditionOps for WriteS<'a, MNS, MES, EXT> +{ + fn internal_add_edge( + &mut self, + t: TimeIndexEntry, + src: impl Into, + dst: impl Into, + lsn: u64, + layer: usize, + props: impl IntoIterator, + ) -> MaybeNew { + self.0.internal_add_edge(t, src, dst, lsn, layer, props) + } +} + +impl<'a, EXT: Send + Sync> SessionAdditionOps for UnlockedSession<'a, EXT> { + type Error = MutationError; + + fn next_event_id(&self) -> Result { + todo!() + } + + fn reserve_event_ids(&self, num_ids: usize) -> Result { + todo!() + } + + fn set_node(&self, gid: GidRef, vid: VID) -> Result<(), Self::Error> { + todo!() + } + + fn resolve_graph_property( + &self, + prop: &str, + dtype: PropType, + is_static: bool, + ) -> Result, Self::Error> { + todo!() + } + + fn resolve_node_property( + &self, + prop: &str, + dtype: PropType, + is_static: bool, + ) -> Result, Self::Error> { + todo!() + } + + fn resolve_edge_property( + &self, + prop: &str, + dtype: PropType, + is_static: bool, + ) -> Result, Self::Error> { + todo!() + } + + fn internal_add_node( + &self, + t: TimeIndexEntry, + v: VID, + props: &[(usize, Prop)], + ) -> Result<(), Self::Error> { + todo!() + } + + fn internal_add_edge( + &self, + t: TimeIndexEntry, + src: VID, + dst: VID, + props: &[(usize, Prop)], + layer: usize, + ) -> Result, Self::Error> { + todo!() + } + + fn internal_add_edge_update( + &self, + t: TimeIndexEntry, + edge: EID, + props: &[(usize, Prop)], + layer: usize, + ) -> Result<(), Self::Error> { + todo!() + } +} + +impl, ES = ES>> InternalAdditionOps + for TemporalGraph +{ + type Error = MutationError; + + type WS<'a> + = UnlockedSession<'a, EXT> + where + EXT: 'a; + + type AtomicAddEdge<'a> = + WriteS<'a, RwLockWriteGuard<'a, MemNodeSegment>, RwLockWriteGuard<'a, MemEdgeSegment>, EXT>; + + fn write_lock(&self) -> Result { + todo!() + } + + fn write_lock_nodes(&self) -> Result { + todo!() + } + + fn write_lock_edges(&self) -> Result { + todo!() + } + + fn resolve_layer(&self, layer: Option<&str>) -> Result, Self::Error> { + let id = self.edge_meta().get_or_create_layer_id(layer); + + let layer_id = id.inner(); + if self.layers().get(layer_id).is_some() { + return Ok(id); + } + let count = self.layers().count(); + if count >= layer_id + 1 { + // something has allocated the layer, wait for it to be added + while self.layers().get(layer_id).is_none() { + // wait for the layer to be created + std::thread::yield_now(); + } + return Ok(id); + } else { + self.layers().reserve(2); + let layer_name = layer.unwrap_or("_default"); + loop { + let new_layer_id = self.layers().push_with(|_| { + Layer::new( + self.graph_dir().join(format!("l_{}", layer_name)), + self.max_page_len_nodes(), + self.max_page_len_edges(), + ) + }); + if new_layer_id >= layer_id { + while self.layers().get(new_layer_id).is_none() { + // wait for the layer to be created + std::thread::yield_now(); + } + return Ok(id); + } + } + } + } + + fn resolve_node(&self, id: NodeRef) -> Result, Self::Error> { + match id { + NodeRef::External(id) => { + let id = self + .logical_to_physical + .get_or_init_vid(id, || { + self.node_count + .fetch_add(1, std::sync::atomic::Ordering::Relaxed) + .into() + }) + .map_err(MutationError::InvalidNodeId)?; + Ok(id) + } + NodeRef::Internal(id) => Ok(MaybeNew::Existing(id)), + } + } + + fn resolve_node_and_type( + &self, + id: NodeRef, + node_type: &str, + ) -> Result, MaybeNew)>, Self::Error> { + todo!() + } + + fn validate_gids<'a>( + &self, + gids: impl IntoIterator>, + ) -> Result<(), Self::Error> { + Ok(self + .logical_to_physical + .validate_gids(gids) + .map_err(MutationError::InvalidNodeId)?) + } + + fn write_session(&self) -> Result, Self::Error> { + todo!() + } + + fn atomic_add_edge( + &self, + src: VID, + dst: VID, + e_id: Option, + layer_id: usize, + ) -> Self::AtomicAddEdge<'_> { + let layer = &self.layers()[layer_id]; + WriteS(layer.write_session(src, dst, e_id)) + } + + fn validate_edge_props>( + &self, + is_static: bool, + props: impl ExactSizeIterator, + ) -> Result, Self::Error> { + if is_static { + let prop_ids = PropsMetaWriter::constant(self.edge_meta(), props) + .and_then(|pmw| pmw.into_props_const()) + .map_err(MutationError::DBV4Error)?; + Ok(prop_ids) + } else { + let prop_ids = PropsMetaWriter::temporal(self.edge_meta(), props) + .and_then(|pmw| pmw.into_props_temporal()) + .map_err(MutationError::DBV4Error)?; + Ok(prop_ids) + } + } +} diff --git a/raphtory-storage/src/mutation/mod.rs b/raphtory-storage/src/mutation/mod.rs index 33c16944bd..a9b77a1c7d 100644 --- a/raphtory-storage/src/mutation/mod.rs +++ b/raphtory-storage/src/mutation/mod.rs @@ -18,9 +18,11 @@ use raphtory_core::entities::{ }, }; use std::sync::Arc; +use storage::error::DBV4Error; use thiserror::Error; pub mod addition_ops; +pub mod addition_ops_ext; pub mod deletion_ops; pub mod property_addition_ops; @@ -50,6 +52,8 @@ pub enum MutationError { src: String, dst: String, }, + #[error("Storage error: {0}")] + DBV4Error(DBV4Error), } pub trait InheritMutationOps: Base {} From ae43e2ec19f440ea4958f3c30b68e3d5998dd4cb Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Wed, 11 Jun 2025 17:41:09 +0100 Subject: [PATCH 007/321] add support for full graph locking --- db4-graph/src/lib.rs | 51 +++++++++++------------- db4-storage/src/lib.rs | 42 ++++++++++++++++++-- db4-storage/src/pages/edge_store.rs | 55 ++++++++++++++++---------- db4-storage/src/pages/mod.rs | 25 ++++++++++-- db4-storage/src/pages/node_store.rs | 59 ++++++++++++++++++---------- db4-storage/src/segments/edge.rs | 9 ++++- db4-storage/src/segments/node.rs | 9 ++++- raphtory-storage/src/graph/graph.rs | 15 +++---- raphtory-storage/src/graph/locked.rs | 8 ++-- 9 files changed, 180 insertions(+), 93 deletions(-) diff --git a/db4-graph/src/lib.rs b/db4-graph/src/lib.rs index b8b7fc4fe8..99939c5ccd 100644 --- a/db4-graph/src/lib.rs +++ b/db4-graph/src/lib.rs @@ -1,40 +1,16 @@ use std::{ - ops::DerefMut, path::{Path, PathBuf}, sync::{atomic::AtomicUsize, Arc}, }; -use parking_lot::RwLockWriteGuard; -use raphtory_api::core::{ - entities::{ - properties::{ - meta::Meta, - prop::{Prop, PropType}, - }, - GidRef, EID, VID, - }, - storage::{dict_mapper::MaybeNew, timeindex::TimeIndexEntry}, -}; -use raphtory_core::{ - entities::{graph::logical_to_physical::Mapping, nodes::node_ref::NodeRef, ELID}, - storage::{raw_edges::WriteLockedEdges, WriteLockedNodes}, -}; -// use raphtory_storage::mutation::{ -// addition_ops::{AtomicAdditionOps, InternalAdditionOps, SessionAdditionOps}, -// MutationError, -// }; -use storage::{ - error::DBV4Error, - pages::session::WriteSession, - persist::strategy::PersistentStrategy, - properties::props_meta_writer::PropsMetaWriter, - segments::{edge::MemEdgeSegment, node::MemNodeSegment}, - Layer, ES, NS, -}; +use raphtory_api::core::entities::properties::meta::Meta; +use raphtory_core::entities::graph::logical_to_physical::Mapping; +use storage::{persist::strategy::PersistentStrategy, Extension, Layer, ES, NS}; pub mod mutation; -pub struct TemporalGraph { +#[derive(Debug)] +pub struct TemporalGraph { graph_dir: PathBuf, // mapping between logical and physical ids pub logical_to_physical: Mapping, @@ -49,7 +25,24 @@ pub struct TemporalGraph { node_meta: Arc, } +pub struct ReadLockedTemporalGraph { + graph: Arc>, + locked_layers: Box<[storage::ReadLockedLayer<'static, EXT>]>, +} + impl, ES = ES>> TemporalGraph { + pub fn read_locked(self: &Arc) -> ReadLockedTemporalGraph { + let locked_layers = self + .layers + .iter() + .map(|layer| layer.locked()) + .collect::>(); + ReadLockedTemporalGraph { + graph: self.clone(), + locked_layers, + } + } + pub fn layers(&self) -> &boxcar::Vec> { &self.layers } diff --git a/db4-storage/src/lib.rs b/db4-storage/src/lib.rs index 0137ee35a1..e22f3741eb 100644 --- a/db4-storage/src/lib.rs +++ b/db4-storage/src/lib.rs @@ -6,10 +6,10 @@ use std::{ use crate::{ error::DBV4Error, - pages::GraphStore, + pages::{GraphStore, ReadLockedGraphStore}, segments::{edge::EdgeSegmentView, node::NodeSegmentView}, }; -use parking_lot::{RwLockReadGuard, RwLockWriteGuard}; +use parking_lot::{RwLockReadGuard, RwLockWriteGuard, lock_api::ArcRwLockReadGuard}; use raphtory_api::core::{ entities::{ EID, VID, @@ -28,7 +28,8 @@ pub mod segments; pub type NS

= NodeSegmentView

; pub type ES

= EdgeSegmentView

; -pub type Layer = GraphStore; +pub type Layer = GraphStore, EdgeSegmentView, EXT>; +pub type ReadLockedLayer<'a, EXT> = ReadLockedGraphStore<'a, NodeSegmentView, EdgeSegmentView, EXT>; pub trait EdgeSegmentOps: Send + Sync { type Extension; @@ -66,6 +67,8 @@ pub trait EdgeSegmentOps: Send + Sync { fn head(&self) -> RwLockReadGuard; + fn head_arc(&self) -> ArcRwLockReadGuard; + fn head_mut(&self) -> RwLockWriteGuard; fn try_head_mut(&self) -> Option>; @@ -90,6 +93,22 @@ pub trait EdgeSegmentOps: Send + Sync { ) -> Option<(VID, VID)>; fn entry<'a, LP: Into>(&'a self, edge_pos: LP) -> Self::Entry<'a>; + + fn locked(self: &Arc) -> ReadLockedES + where + Self: Sized, + { + ReadLockedES { + es: self.clone(), + head: self.head_arc(), + } + } +} + +#[derive(Debug)] +pub struct ReadLockedES { + es: Arc, + head: ArcRwLockReadGuard, } pub trait EdgeEntryOps<'a> { @@ -147,6 +166,7 @@ pub trait NodeSegmentOps: Send + Sync { fn segment_id(&self) -> usize; + fn head_arc(&self) -> ArcRwLockReadGuard; fn head(&self) -> RwLockReadGuard; fn head_mut(&self) -> RwLockWriteGuard; @@ -177,6 +197,22 @@ pub trait NodeSegmentOps: Send + Sync { ) -> Option; fn entry<'a>(&'a self, pos: impl Into) -> Self::Entry<'a>; + + fn locked(self: &Arc) -> ReadLockedNS + where + Self: Sized, + { + ReadLockedNS { + ns: self.clone(), + head: self.head_arc(), + } + } +} + +#[derive(Debug)] +pub struct ReadLockedNS { + ns: Arc, + head: ArcRwLockReadGuard, } pub trait NodeEntryOps<'a> { diff --git a/db4-storage/src/pages/edge_store.rs b/db4-storage/src/pages/edge_store.rs index b28e9bc75d..93ff900841 100644 --- a/db4-storage/src/pages/edge_store.rs +++ b/db4-storage/src/pages/edge_store.rs @@ -9,10 +9,7 @@ use std::{ use super::{edge_page::writer::EdgeWriter, resolve_pos}; use crate::{ - EdgeSegmentOps, LocalPOS, - error::DBV4Error, - pages::locked::edges::{LockedEdgePage, WriteLockedEdgePages}, - segments::edge::MemEdgeSegment, + EdgeSegmentOps, LocalPOS, ReadLockedES, error::DBV4Error, segments::edge::MemEdgeSegment, }; use parking_lot::{RwLock, RwLockWriteGuard}; use raphtory_api::core::entities::{EID, VID, properties::meta::Meta}; @@ -31,7 +28,25 @@ pub struct EdgeStorageInner { ext: EXT, } +#[derive(Debug)] +pub struct ReadLockedEdgeStorage { + storage: Arc>, + locked_pages: Box<[ReadLockedES]>, +} + impl, EXT: Clone> EdgeStorageInner { + pub fn locked(self: &Arc) -> ReadLockedEdgeStorage { + let locked_pages = self + .pages + .iter() + .map(|(_, segment)| segment.locked()) + .collect::>(); + ReadLockedEdgeStorage { + storage: self.clone(), + locked_pages, + } + } + pub fn layer( edges_path: impl AsRef, max_page_len: usize, @@ -253,22 +268,22 @@ impl, EXT: Clone> EdgeStorageInner self.max_page_len } - pub fn locked<'a>(&'a self) -> WriteLockedEdgePages<'a, ES> { - WriteLockedEdgePages::new( - self.pages - .iter() - .map(|(page_id, page)| { - LockedEdgePage::new( - page_id, - self.max_page_len, - page.as_ref(), - &self.num_edges, - page.head_mut(), - ) - }) - .collect(), - ) - } + // pub fn locked<'a>(&'a self) -> WriteLockedEdgePages<'a, ES> { + // WriteLockedEdgePages::new( + // self.pages + // .iter() + // .map(|(page_id, page)| { + // LockedEdgePage::new( + // page_id, + // self.max_page_len, + // page.as_ref(), + // &self.num_edges, + // page.head_mut(), + // ) + // }) + // .collect(), + // ) + // } pub fn get_edge(&self, e_id: EID) -> Option<(VID, VID)> { let (chunk, local_edge) = resolve_pos(e_id, self.max_page_len); diff --git a/db4-storage/src/pages/mod.rs b/db4-storage/src/pages/mod.rs index d1eb096e48..e9839760d3 100644 --- a/db4-storage/src/pages/mod.rs +++ b/db4-storage/src/pages/mod.rs @@ -8,10 +8,7 @@ use std::{ }; use crate::{ - EdgeSegmentOps, LocalPOS, NodeSegmentOps, - error::DBV4Error, - properties::props_meta_writer::PropsMetaWriter, - segments::{edge::MemEdgeSegment, node::MemNodeSegment}, + error::DBV4Error, pages::{edge_store::ReadLockedEdgeStorage, node_store::ReadLockedNodeStorage}, properties::props_meta_writer::PropsMetaWriter, segments::{edge::MemEdgeSegment, node::MemNodeSegment}, EdgeSegmentOps, LocalPOS, NodeSegmentOps }; use edge_page::writer::EdgeWriter; use edge_store::EdgeStorageInner; @@ -51,9 +48,29 @@ pub struct GraphStore { _ext: EXT, } +#[derive(Debug)] +pub struct ReadLockedGraphStore { + nodes: ReadLockedNodeStorage, + edges: ReadLockedEdgeStorage, + graph: Arc>, +} + impl, ES: EdgeSegmentOps, EXT: Clone + Default> GraphStore { + + pub fn read_locked( + self: &Arc, + ) -> ReadLockedGraphStore { + let nodes = self.nodes.locked(); + let edges = self.edges.locked(); + ReadLockedGraphStore { + nodes, + edges, + graph: self.clone(), + } + } + pub fn nodes(&self) -> &NodeStorageInner { &self.nodes } diff --git a/db4-storage/src/pages/node_store.rs b/db4-storage/src/pages/node_store.rs index 189ba4e41a..a0cc25800e 100644 --- a/db4-storage/src/pages/node_store.rs +++ b/db4-storage/src/pages/node_store.rs @@ -1,11 +1,8 @@ use super::{node_page::writer::NodeWriter, resolve_pos}; use crate::{ - LocalPOS, NodeSegmentOps, - error::DBV4Error, - pages::locked::nodes::{LockedNodePage, WriteLockedNodePages}, - segments::node::MemNodeSegment, + error::DBV4Error, pages::locked::nodes::{LockedNodePage, WriteLockedNodePages}, segments::node::MemNodeSegment, LocalPOS, ReadLockedNS, NodeSegmentOps }; -use parking_lot::RwLockWriteGuard; +use parking_lot::{RwLockReadGuard, RwLockWriteGuard}; use raphtory_api::core::entities::properties::meta::Meta; use raphtory_core::entities::{EID, VID}; use std::{ @@ -27,7 +24,27 @@ pub struct NodeStorageInner { ext: EXT, } +#[derive(Debug)] +pub struct ReadLockedNodeStorage{ + storage: Arc>, + locked_pages: Box<[ReadLockedNS]>, +} + + impl, EXT: Clone> NodeStorageInner { + + pub fn locked( + self: &Arc, + ) -> ReadLockedNodeStorage { + let locked_pages = self.pages.iter().map(|(_, segment)| { + segment.locked() + }).collect::>(); + ReadLockedNodeStorage { + storage: self.clone(), + locked_pages, + } + } + pub fn layer( nodes_path: impl AsRef, max_page_len: usize, @@ -54,22 +71,22 @@ impl, EXT: Clone> NodeStorageInner } } - pub fn locked<'a>(&'a self) -> WriteLockedNodePages<'a, NS> { - WriteLockedNodePages::new( - self.pages - .iter() - .map(|(page_id, page)| { - LockedNodePage::new( - page_id, - &self.num_nodes, - self.max_page_len, - page.as_ref(), - page.head_mut(), - ) - }) - .collect(), - ) - } + // pub fn locked<'a>(&'a self) -> WriteLockedNodePages<'a, NS> { + // WriteLockedNodePages::new( + // self.pages + // .iter() + // .map(|(page_id, page)| { + // LockedNodePage::new( + // page_id, + // &self.num_nodes, + // self.max_page_len, + // page.as_ref(), + // page.head_mut(), + // ) + // }) + // .collect(), + // ) + // } pub fn node<'a>(&'a self, node: impl Into) -> NS::Entry<'a> { let (page_id, pos) = self.resolve_pos(node); diff --git a/db4-storage/src/segments/edge.rs b/db4-storage/src/segments/edge.rs index 6865e8f071..ec066122c4 100644 --- a/db4-storage/src/segments/edge.rs +++ b/db4-storage/src/segments/edge.rs @@ -6,6 +6,7 @@ use std::{ }, }; +use parking_lot::lock_api::ArcRwLockReadGuard; use raphtory_api::core::entities::{ VID, properties::{meta::Meta, prop::Prop}, @@ -145,7 +146,7 @@ impl MemEdgeSegment { } pub struct EdgeSegmentView { - segment: parking_lot::RwLock, + segment: Arc>, segment_id: usize, num_edges: AtomicUsize, _ext: EXT, @@ -189,7 +190,7 @@ impl EdgeSegmentOps for EdgeSegmentView { _ext: Self::Extension, ) -> Self { Self { - segment: parking_lot::RwLock::new(MemEdgeSegment::new(page_id, max_page_len, meta)), + segment: parking_lot::RwLock::new(MemEdgeSegment::new(page_id, max_page_len, meta)).into(), segment_id: page_id, num_edges: AtomicUsize::new(0), _ext: (), @@ -208,6 +209,10 @@ impl EdgeSegmentOps for EdgeSegmentView { self.segment.read_recursive() } + fn head_arc(&self) -> ArcRwLockReadGuard { + self.segment.read_arc_recursive() + } + fn head_mut(&self) -> parking_lot::RwLockWriteGuard { self.segment.write() } diff --git a/db4-storage/src/segments/node.rs b/db4-storage/src/segments/node.rs index 71f1090b75..654628c35c 100644 --- a/db4-storage/src/segments/node.rs +++ b/db4-storage/src/segments/node.rs @@ -1,4 +1,5 @@ use either::Either; +use parking_lot::lock_api::ArcRwLockReadGuard; use raphtory_api::core::{ Direction, entities::{ @@ -209,7 +210,7 @@ impl MemNodeSegment { } pub struct NodeSegmentView { - inner: parking_lot::RwLock, + inner: Arc>, segment_id: usize, num_nodes: AtomicUsize, _ext: EXT, @@ -253,7 +254,7 @@ impl NodeSegmentOps for NodeSegmentView { _ext: Self::Extension, ) -> Self { Self { - inner: parking_lot::RwLock::new(MemNodeSegment::new(page_id, max_page_len, meta)), + inner: parking_lot::RwLock::new(MemNodeSegment::new(page_id, max_page_len, meta)).into(), segment_id: page_id, num_nodes: AtomicUsize::new(0), _ext: (), @@ -268,6 +269,10 @@ impl NodeSegmentOps for NodeSegmentView { self.inner.read() } + fn head_arc(&self) -> ArcRwLockReadGuard { + self.inner.read_arc_recursive() + } + fn head_mut(&self) -> parking_lot::RwLockWriteGuard { self.inner.write() } diff --git a/raphtory-storage/src/graph/graph.rs b/raphtory-storage/src/graph/graph.rs index 5af606a01c..af21ee5d40 100644 --- a/raphtory-storage/src/graph/graph.rs +++ b/raphtory-storage/src/graph/graph.rs @@ -7,12 +7,12 @@ use crate::graph::{ locked::LockedGraph, nodes::{nodes::NodesStorage, nodes_ref::NodesStorageEntry}, }; +use db4_graph::TemporalGraph; use raphtory_api::core::entities::{properties::meta::Meta, LayerIds, LayerVariants, EID, VID}; -use raphtory_core::entities::{ - graph::tgraph::TemporalGraph, nodes::node_ref::NodeRef, properties::graph_meta::GraphMeta, -}; +use raphtory_core::entities::{nodes::node_ref::NodeRef, properties::graph_meta::GraphMeta}; use serde::{Deserialize, Serialize}; use std::{fmt::Debug, iter, sync::Arc}; +use storage::{Extension, ReadLockedLayer}; use thiserror::Error; #[cfg(feature = "storage")] @@ -25,9 +25,9 @@ use crate::disk::{ }; use crate::mutation::MutationError; -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug)] pub enum GraphStorage { - Mem(LockedGraph), + Mem(Arc>), Unlocked(Arc), #[cfg(feature = "storage")] Disk(Arc), @@ -110,10 +110,7 @@ impl GraphStorage { #[inline(always)] pub fn lock(&self) -> Self { match self { - GraphStorage::Unlocked(storage) => { - let locked = LockedGraph::new(storage.clone()); - GraphStorage::Mem(locked) - } + GraphStorage::Unlocked(storage) => GraphStorage::Mem(storage.read_locked().into()), _ => self.clone(), } } diff --git a/raphtory-storage/src/graph/locked.rs b/raphtory-storage/src/graph/locked.rs index 816c955217..c2d1b12630 100644 --- a/raphtory-storage/src/graph/locked.rs +++ b/raphtory-storage/src/graph/locked.rs @@ -9,13 +9,15 @@ use raphtory_core::{ ReadLockedStorage, WriteLockedNodes, }, }; +use storage::ReadLockedLayer; use std::sync::Arc; #[derive(Debug)] pub struct LockedGraph { - pub(crate) nodes: Arc, - pub(crate) edges: Arc, - pub graph: Arc, + // pub(crate) nodes: Arc, + // pub(crate) edges: Arc, + // pub graph: Arc, + graph: Arc } impl<'de> serde::Deserialize<'de> for LockedGraph { From 0372704e37b06cbd4a622da75b65c907da1d2d24 Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Thu, 12 Jun 2025 16:29:35 +0100 Subject: [PATCH 008/321] issues integrating with dbv4 --- db4-graph/src/entries/mod.rs | 1 + db4-graph/src/entries/node.rs | 49 ++++++++++ db4-graph/src/lib.rs | 90 +++++++++++++++++-- db4-storage/src/lib.rs | 2 +- db4-storage/src/pages/edge_page/writer.rs | 6 +- db4-storage/src/pages/session.rs | 15 ++-- raphtory-storage/src/core_ops.rs | 29 +++--- raphtory-storage/src/graph/graph.rs | 70 +++++++-------- raphtory-storage/src/graph/locked.rs | 7 +- .../src/graph/nodes/node_entry.rs | 33 ++++--- raphtory-storage/src/graph/nodes/node_ref.rs | 4 +- raphtory-storage/src/graph/nodes/nodes.rs | 8 +- raphtory-storage/src/graph/nodes/nodes_ref.rs | 15 ++-- .../src/mutation/addition_ops_ext.rs | 25 ++++-- 14 files changed, 247 insertions(+), 107 deletions(-) create mode 100644 db4-graph/src/entries/mod.rs create mode 100644 db4-graph/src/entries/node.rs diff --git a/db4-graph/src/entries/mod.rs b/db4-graph/src/entries/mod.rs new file mode 100644 index 0000000000..12e2c60478 --- /dev/null +++ b/db4-graph/src/entries/mod.rs @@ -0,0 +1 @@ +pub mod node; \ No newline at end of file diff --git a/db4-graph/src/entries/node.rs b/db4-graph/src/entries/node.rs new file mode 100644 index 0000000000..000d67025e --- /dev/null +++ b/db4-graph/src/entries/node.rs @@ -0,0 +1,49 @@ +use raphtory_core::entities::VID; + +use crate::{ReadLockedTemporalGraph, TemporalGraph}; + +#[derive(Debug, Copy, Clone)] +pub struct LockedNodeEntry<'a, EXT> { + vid: VID, + node_support: &'a ReadLockedTemporalGraph, +} + +impl<'a, EXT> LockedNodeEntry<'a, EXT> { + pub fn new(vid: VID, node_support: &'a ReadLockedTemporalGraph) -> Self { + Self { + vid, + node_support, + } + } + + pub fn vid(&self) -> VID { + self.vid + } + + pub fn node_support(&self) -> &ReadLockedTemporalGraph { + &self.node_support + } +} + +#[derive(Debug, Copy, Clone)] +pub struct UnlockedNodeEntry<'a, EXT> { + vid: VID, + node_support: &'a TemporalGraph, +} + +impl<'a, EXT> UnlockedNodeEntry<'a, EXT> { + pub fn new(vid: VID, node_support: &'a TemporalGraph) -> Self { + Self { + vid, + node_support, + } + } + + pub fn vid(&self) -> VID { + self.vid + } + + pub fn node_support(&self) -> &TemporalGraph { + &self.node_support + } +} \ No newline at end of file diff --git a/db4-graph/src/lib.rs b/db4-graph/src/lib.rs index 99939c5ccd..d1397bd3ac 100644 --- a/db4-graph/src/lib.rs +++ b/db4-graph/src/lib.rs @@ -1,12 +1,22 @@ use std::{ + ops::Deref, path::{Path, PathBuf}, - sync::{atomic::AtomicUsize, Arc}, + sync::{ + atomic::{self, AtomicUsize}, + Arc, + }, }; -use raphtory_api::core::entities::properties::meta::Meta; -use raphtory_core::entities::graph::logical_to_physical::Mapping; +use raphtory_api::core::{entities::properties::meta::Meta, input::input_node::InputNode}; +use raphtory_core::entities::{ + graph::logical_to_physical::Mapping, nodes::node_ref::NodeRef, + properties::graph_meta::GraphMeta, GidRef, VID, +}; use storage::{persist::strategy::PersistentStrategy, Extension, Layer, ES, NS}; +use crate::entries::node::{LockedNodeEntry, UnlockedNodeEntry}; + +pub mod entries; pub mod mutation; #[derive(Debug)] @@ -19,31 +29,95 @@ pub struct TemporalGraph { max_page_len_nodes: usize, max_page_len_edges: usize, - layers: boxcar::Vec>, + static_graph: Arc>, + layers: boxcar::Vec>>, edge_meta: Arc, node_meta: Arc, + + event_counter: AtomicUsize, + graph_meta: Arc, } +#[derive(Debug)] pub struct ReadLockedTemporalGraph { - graph: Arc>, - locked_layers: Box<[storage::ReadLockedLayer<'static, EXT>]>, + pub graph: Arc>, + static_graph: storage::ReadLockedLayer, + locked_layers: Box<[storage::ReadLockedLayer]>, } +impl ReadLockedTemporalGraph { + pub fn graph(&self) -> &Arc> { + &self.graph + } + + pub fn node(&self, vid: VID) -> LockedNodeEntry { + LockedNodeEntry::new(vid, self) + } + + pub fn inner(&self) -> &TemporalGraph { + &self.graph + } +} + + impl, ES = ES>> TemporalGraph { + pub fn node(&self, vid: VID) -> UnlockedNodeEntry { + UnlockedNodeEntry::new(vid, self) + } + + pub fn read_event_counter(&self) -> usize { + self.event_counter.load(atomic::Ordering::Relaxed) + } + + pub fn static_graph(&self) -> &Arc> { + &self.static_graph + } + + pub fn graph_meta(&self) -> &Arc { + &self.graph_meta + } + + pub fn num_layers(&self) -> usize { + self.layers.count() + } + + #[inline] + pub fn resolve_node_ref(&self, v: NodeRef) -> Option { + match v { + NodeRef::Internal(vid) => Some(vid), + NodeRef::External(GidRef::U64(gid)) => self.logical_to_physical.get_u64(gid), + NodeRef::External(GidRef::Str(string)) => self + .logical_to_physical + .get_str(string) + .or_else(|| self.logical_to_physical.get_u64(string.id())), + } + } + + #[inline] + pub fn internal_num_nodes(&self) -> usize { + self.static_graph.nodes().num_nodes() + } + + #[inline] + pub fn internal_num_edges(&self) -> usize { + self.static_graph.edges().num_edges() + } + pub fn read_locked(self: &Arc) -> ReadLockedTemporalGraph { let locked_layers = self .layers .iter() - .map(|layer| layer.locked()) + .map(|(_, layer)| layer.read_locked()) .collect::>(); ReadLockedTemporalGraph { graph: self.clone(), + static_graph: self.static_graph.read_locked(), locked_layers, } } - pub fn layers(&self) -> &boxcar::Vec> { + pub fn layers(&self) -> &boxcar::Vec>> { &self.layers } diff --git a/db4-storage/src/lib.rs b/db4-storage/src/lib.rs index e22f3741eb..aa2ef88483 100644 --- a/db4-storage/src/lib.rs +++ b/db4-storage/src/lib.rs @@ -29,7 +29,7 @@ pub type NS

= NodeSegmentView

; pub type ES

= EdgeSegmentView

; pub type Layer = GraphStore, EdgeSegmentView, EXT>; -pub type ReadLockedLayer<'a, EXT> = ReadLockedGraphStore<'a, NodeSegmentView, EdgeSegmentView, EXT>; +pub type ReadLockedLayer = ReadLockedGraphStore; pub trait EdgeSegmentOps: Send + Sync { type Extension; diff --git a/db4-storage/src/pages/edge_page/writer.rs b/db4-storage/src/pages/edge_page/writer.rs index 3004bf33df..067f155304 100644 --- a/db4-storage/src/pages/edge_page/writer.rs +++ b/db4-storage/src/pages/edge_page/writer.rs @@ -1,6 +1,6 @@ use std::{ops::DerefMut, sync::atomic::AtomicUsize}; -use crate::{EdgeSegmentOps, LocalPOS, error::DBV4Error, segments::edge::MemEdgeSegment}; +use crate::{EdgeSegmentOps, LocalPOS, segments::edge::MemEdgeSegment}; use raphtory_api::core::entities::{VID, properties::prop::Prop}; use raphtory_core::storage::timeindex::AsTime; @@ -54,7 +54,7 @@ impl<'a, MP: DerefMut, ES: EdgeSegmentOps> EdgeWriter<' dst: impl Into, lsn: u64, exists_hint: Option, // used when edge_pos is Some but the is not counted, this is used in the bulk loader - ) -> Result { + ) -> LocalPOS { self.writer.as_mut().set_lsn(lsn); if exists_hint == Some(false) && edge_pos.is_some() { @@ -65,7 +65,7 @@ impl<'a, MP: DerefMut, ES: EdgeSegmentOps> EdgeWriter<' self.writer.insert_static_edge_internal(edge_pos, src, dst); // self.est_size = self.page.increment_size(size_of::<(VID, VID)>()) // + self.writer.as_ref().t_prop_est_size(); - Ok(edge_pos) + edge_pos } pub fn segment_id(&self) -> usize { diff --git a/db4-storage/src/pages/session.rs b/db4-storage/src/pages/session.rs index b964df20fb..3bae4c727f 100644 --- a/db4-storage/src/pages/session.rs +++ b/db4-storage/src/pages/session.rs @@ -5,7 +5,6 @@ use super::{ }; use crate::{ EdgeSegmentOps, NodeSegmentOps, - error::DBV4Error, segments::{edge::MemEdgeSegment, node::MemNodeSegment}, }; use raphtory_api::core::{entities::properties::prop::Prop, storage::dict_mapper::MaybeNew}; @@ -104,7 +103,7 @@ impl< src: impl Into, dst: impl Into, lsn: u64, - ) -> Result, DBV4Error> { + ) -> MaybeNew { let src = src.into(); let dst = dst.into(); @@ -114,20 +113,20 @@ impl< if let Some(e_id) = self.node_writers.get_mut_src().get_out_edge(src_pos, dst) { let mut edge_writer = self.graph.edge_writer(e_id); let (_, edge_pos) = self.graph.edges().resolve_pos(e_id); - edge_writer.add_static_edge(Some(edge_pos), src, dst, lsn, None)?; + edge_writer.add_static_edge(Some(edge_pos), src, dst, lsn, None); - Ok(MaybeNew::Existing(e_id)) + MaybeNew::Existing(e_id) } else { if let Some(e_id) = self.node_writers.get_mut_src().get_out_edge(src_pos, dst) { let mut edge_writer = self.graph.edge_writer(e_id); let (_, edge_pos) = self.graph.edges().resolve_pos(e_id); - edge_writer.add_static_edge(Some(edge_pos), src, dst, lsn, None)?; + edge_writer.add_static_edge(Some(edge_pos), src, dst, lsn, None); - Ok(MaybeNew::Existing(e_id)) + MaybeNew::Existing(e_id) } else { let mut edge_writer = self.graph.get_free_writer(); - let edge_id = edge_writer.add_static_edge(None, src, dst, lsn, None)?; + let edge_id = edge_writer.add_static_edge(None, src, dst, lsn, None); let edge_id = edge_id.as_eid(edge_writer.segment_id(), self.graph.edges().max_page_len()); @@ -144,7 +143,7 @@ impl< lsn, ); - Ok(MaybeNew::New(edge_id)) + MaybeNew::New(edge_id) } } } diff --git a/raphtory-storage/src/core_ops.rs b/raphtory-storage/src/core_ops.rs index d94a30acac..192c0e0651 100644 --- a/raphtory-storage/src/core_ops.rs +++ b/raphtory-storage/src/core_ops.rs @@ -4,6 +4,7 @@ use crate::graph::{ locked::LockedGraph, nodes::{node_entry::NodeStorageEntry, node_storage_ops::NodeStorageOps, nodes::NodesStorage}, }; +use db4_graph::ReadLockedTemporalGraph; use raphtory_api::{ core::{ entities::{ @@ -36,30 +37,28 @@ pub fn is_view_compatible(g1: &impl CoreGraphOps, g2: &impl CoreGraphOps) -> boo pub trait CoreGraphOps: Send + Sync { fn id_type(&self) -> Option { match self.core_graph() { - GraphStorage::Mem(LockedGraph { graph, .. }) | GraphStorage::Unlocked(graph) => { - graph.logical_to_physical.dtype() - } + GraphStorage::Mem(graph) => graph.inner().logical_to_physical.dtype(), + GraphStorage::Unlocked(graph) => graph.logical_to_physical.dtype(), #[cfg(feature = "storage")] GraphStorage::Disk(storage) => Some(storage.inner().id_type()), } } - fn num_shards(&self) -> usize { - match self.core_graph() { - GraphStorage::Mem(LockedGraph { graph, .. }) | GraphStorage::Unlocked(graph) => { - graph.storage.num_shards() - } - #[cfg(feature = "storage")] - GraphStorage::Disk(_) => 1, - } - } + // fn num_shards(&self) -> usize { + // match self.core_graph() { + // GraphStorage::Mem(LockedGraph { graph, .. }) | GraphStorage::Unlocked(graph) => { + // graph.storage.num_shards() + // } + // #[cfg(feature = "storage")] + // GraphStorage::Disk(_) => 1, + // } + // } /// get the current sequence id without incrementing the counter fn read_event_id(&self) -> usize { match self.core_graph() { - GraphStorage::Unlocked(graph) | GraphStorage::Mem(LockedGraph { graph, .. }) => { - graph.event_counter.load(Ordering::Relaxed) - } + GraphStorage::Unlocked(graph) => graph.read_event_counter(), + GraphStorage::Mem(graph) => graph.inner().read_event_counter(), #[cfg(feature = "storage")] GraphStorage::Disk(storage) => storage.inner.count_temporal_edges(), } diff --git a/raphtory-storage/src/graph/graph.rs b/raphtory-storage/src/graph/graph.rs index af21ee5d40..8545a51163 100644 --- a/raphtory-storage/src/graph/graph.rs +++ b/raphtory-storage/src/graph/graph.rs @@ -7,12 +7,10 @@ use crate::graph::{ locked::LockedGraph, nodes::{nodes::NodesStorage, nodes_ref::NodesStorageEntry}, }; -use db4_graph::TemporalGraph; +use db4_graph::{ReadLockedTemporalGraph, TemporalGraph}; use raphtory_api::core::entities::{properties::meta::Meta, LayerIds, LayerVariants, EID, VID}; use raphtory_core::entities::{nodes::node_ref::NodeRef, properties::graph_meta::GraphMeta}; -use serde::{Deserialize, Serialize}; use std::{fmt::Debug, iter, sync::Arc}; -use storage::{Extension, ReadLockedLayer}; use thiserror::Error; #[cfg(feature = "storage")] @@ -27,7 +25,7 @@ use crate::mutation::MutationError; #[derive(Clone, Debug)] pub enum GraphStorage { - Mem(Arc>), + Mem(Arc), Unlocked(Arc), #[cfg(feature = "storage")] Disk(Arc), @@ -50,7 +48,8 @@ impl From for GraphStorage { impl Default for GraphStorage { fn default() -> Self { - GraphStorage::Unlocked(Arc::new(TemporalGraph::default())) + // GraphStorage::Unlocked(Arc::new(TemporalGraph::default())) + todo!("does this even make sense? GraphStorage::default() is not a valid graph, it should be created with a valid graph directory"); } } @@ -69,16 +68,21 @@ impl GraphStorage { /// Check if two storage instances point at the same underlying storage pub fn ptr_eq(&self, other: &Self) -> bool { match self { - GraphStorage::Mem(LockedGraph { - graph: this_graph, .. - }) - | GraphStorage::Unlocked(this_graph) => match other { - GraphStorage::Mem(LockedGraph { - graph: other_graph, .. - }) - | GraphStorage::Unlocked(other_graph) => Arc::ptr_eq(this_graph, other_graph), + GraphStorage::Mem(locked_graph) => match other { + GraphStorage::Mem(other_graph) => { + Arc::ptr_eq(locked_graph.graph(), other_graph.graph()) + } #[cfg(feature = "storage")] - _ => false, + GraphStorage::Disk(_) => false, + GraphStorage::Unlocked(other_graph) => { + Arc::ptr_eq(locked_graph.graph(), other_graph) + } + }, + GraphStorage::Unlocked(this_graph) => match other { + GraphStorage::Mem(other_graph) => Arc::ptr_eq(this_graph, other_graph.graph()), + GraphStorage::Unlocked(other_graph) => Arc::ptr_eq(this_graph, other_graph), + #[cfg(feature = "storage")] + GraphStorage::Disk(_) => false, }, #[cfg(feature = "storage")] GraphStorage::Disk(this_graph) => match other { @@ -118,10 +122,8 @@ impl GraphStorage { #[inline(always)] pub fn nodes(&self) -> NodesStorageEntry { match self { - GraphStorage::Mem(storage) => NodesStorageEntry::Mem(&storage.nodes), - GraphStorage::Unlocked(storage) => { - NodesStorageEntry::Unlocked(storage.storage.nodes.read_lock()) - } + GraphStorage::Mem(storage) => NodesStorageEntry::Mem(storage.as_ref()), + GraphStorage::Unlocked(storage) => NodesStorageEntry::Unlocked(storage.read_locked()), #[cfg(feature = "storage")] GraphStorage::Disk(storage) => { NodesStorageEntry::Disk(DiskNodesRef::new(&storage.inner)) @@ -134,7 +136,7 @@ impl GraphStorage { match v { NodeRef::Internal(vid) => Some(vid), node_ref => match self { - GraphStorage::Mem(locked) => locked.graph.resolve_node_ref(node_ref), + GraphStorage::Mem(locked) => locked.graph().resolve_node_ref(node_ref), GraphStorage::Unlocked(unlocked) => unlocked.resolve_node_ref(node_ref), #[cfg(feature = "storage")] GraphStorage::Disk(storage) => match v { @@ -148,7 +150,7 @@ impl GraphStorage { #[inline(always)] pub fn unfiltered_num_nodes(&self) -> usize { match self { - GraphStorage::Mem(storage) => storage.nodes.len(), + GraphStorage::Mem(storage) => storage.graph().internal_num_nodes(), GraphStorage::Unlocked(storage) => storage.internal_num_nodes(), #[cfg(feature = "storage")] GraphStorage::Disk(storage) => storage.inner.num_nodes(), @@ -158,8 +160,8 @@ impl GraphStorage { #[inline(always)] pub fn unfiltered_num_edges(&self) -> usize { match self { - GraphStorage::Mem(storage) => storage.edges.len(), - GraphStorage::Unlocked(storage) => storage.storage.edges_len(), + GraphStorage::Mem(storage) => storage.graph().internal_num_edges(), + GraphStorage::Unlocked(storage) => storage.internal_num_edges(), #[cfg(feature = "storage")] GraphStorage::Disk(storage) => storage.inner.count_edges(), } @@ -168,7 +170,7 @@ impl GraphStorage { #[inline(always)] pub fn unfiltered_num_layers(&self) -> usize { match self { - GraphStorage::Mem(storage) => storage.graph.num_layers(), + GraphStorage::Mem(storage) => storage.graph().num_layers(), GraphStorage::Unlocked(storage) => storage.num_layers(), #[cfg(feature = "storage")] GraphStorage::Disk(storage) => storage.inner.layers().len(), @@ -178,10 +180,8 @@ impl GraphStorage { #[inline(always)] pub fn core_nodes(&self) -> NodesStorage { match self { - GraphStorage::Mem(storage) => NodesStorage::Mem(storage.nodes.clone()), - GraphStorage::Unlocked(storage) => { - NodesStorage::Mem(LockedGraph::new(storage.clone()).nodes.clone()) - } + GraphStorage::Mem(storage) => NodesStorage::Mem(storage.clone()), + GraphStorage::Unlocked(storage) => NodesStorage::Mem(storage.read_locked().into()), #[cfg(feature = "storage")] GraphStorage::Disk(storage) => { NodesStorage::Disk(DiskNodesOwned::new(storage.inner.clone())) @@ -192,9 +192,9 @@ impl GraphStorage { #[inline(always)] pub fn core_node<'a>(&'a self, vid: VID) -> NodeStorageEntry<'a> { match self { - GraphStorage::Mem(storage) => NodeStorageEntry::Mem(storage.nodes.get_entry(vid)), + GraphStorage::Mem(storage) => NodeStorageEntry::Mem(storage.node(vid)), GraphStorage::Unlocked(storage) => { - NodeStorageEntry::Unlocked(storage.storage.get_node(vid)) + NodeStorageEntry::Unlocked(storage.node(vid)) } #[cfg(feature = "storage")] GraphStorage::Disk(storage) => { @@ -629,8 +629,8 @@ impl GraphStorage { pub fn node_meta(&self) -> &Meta { match self { - GraphStorage::Mem(storage) => &storage.graph.node_meta, - GraphStorage::Unlocked(storage) => &storage.node_meta, + GraphStorage::Mem(storage) => storage.graph().node_meta(), + GraphStorage::Unlocked(storage) => storage.node_meta(), #[cfg(feature = "storage")] GraphStorage::Disk(storage) => storage.node_meta(), } @@ -638,8 +638,8 @@ impl GraphStorage { pub fn edge_meta(&self) -> &Meta { match self { - GraphStorage::Mem(storage) => &storage.graph.edge_meta, - GraphStorage::Unlocked(storage) => &storage.edge_meta, + GraphStorage::Mem(storage) => storage.graph().edge_meta(), + GraphStorage::Unlocked(storage) => storage.edge_meta(), #[cfg(feature = "storage")] GraphStorage::Disk(storage) => storage.edge_meta(), } @@ -647,8 +647,8 @@ impl GraphStorage { pub fn graph_meta(&self) -> &GraphMeta { match self { - GraphStorage::Mem(storage) => &storage.graph.graph_meta, - GraphStorage::Unlocked(storage) => &storage.graph_meta, + GraphStorage::Mem(storage) => storage.graph().graph_meta(), + GraphStorage::Unlocked(storage) => storage.graph_meta(), #[cfg(feature = "storage")] GraphStorage::Disk(storage) => storage.graph_meta(), } diff --git a/raphtory-storage/src/graph/locked.rs b/raphtory-storage/src/graph/locked.rs index c2d1b12630..cee11ff9d0 100644 --- a/raphtory-storage/src/graph/locked.rs +++ b/raphtory-storage/src/graph/locked.rs @@ -14,10 +14,9 @@ use std::sync::Arc; #[derive(Debug)] pub struct LockedGraph { - // pub(crate) nodes: Arc, - // pub(crate) edges: Arc, - // pub graph: Arc, - graph: Arc + pub(crate) nodes: Arc, + pub(crate) edges: Arc, + pub graph: Arc, } impl<'de> serde::Deserialize<'de> for LockedGraph { diff --git a/raphtory-storage/src/graph/nodes/node_entry.rs b/raphtory-storage/src/graph/nodes/node_entry.rs index d680d39bac..35ec9f73ed 100644 --- a/raphtory-storage/src/graph/nodes/node_entry.rs +++ b/raphtory-storage/src/graph/nodes/node_entry.rs @@ -2,6 +2,7 @@ use crate::graph::{ nodes::{node_ref::NodeStorageRef, node_storage_ops::NodeStorageOps}, variants::storage_variants3::StorageVariants3, }; +use db4_graph::{entries::node::{LockedNodeEntry, UnlockedNodeEntry}, ReadLockedTemporalGraph, TemporalGraph}; use raphtory_api::{ core::{ entities::{ @@ -13,34 +14,32 @@ use raphtory_api::{ }, iter::BoxedLIter, }; -use raphtory_core::{ - storage::{node_entry::NodePtr, NodeEntry}, - utils::iter::GenLockedIter, -}; +use raphtory_core::utils::iter::GenLockedIter; use std::borrow::Cow; +use storage::Extension; #[cfg(feature = "storage")] use crate::disk::storage_interface::node::DiskNode; use crate::graph::nodes::node_additions::NodeAdditions; pub enum NodeStorageEntry<'a> { - Mem(NodePtr<'a>), - Unlocked(NodeEntry<'a>), + Mem(LockedNodeEntry<'a, Extension>), + Unlocked(UnlockedNodeEntry<'a, Extension>), #[cfg(feature = "storage")] Disk(DiskNode<'a>), } -impl<'a> From> for NodeStorageEntry<'a> { - fn from(value: NodePtr<'a>) -> Self { - NodeStorageEntry::Mem(value) - } -} - -impl<'a> From> for NodeStorageEntry<'a> { - fn from(value: NodeEntry<'a>) -> Self { - NodeStorageEntry::Unlocked(value) - } -} +// impl<'a> From> for NodeStorageEntry<'a> { +// fn from(value: NodePtr<'a>) -> Self { +// NodeStorageEntry::Mem(value) +// } +// } + +// impl<'a> From> for NodeStorageEntry<'a> { +// fn from(value: NodeEntry<'a>) -> Self { +// NodeStorageEntry::Unlocked(value) +// } +// } #[cfg(feature = "storage")] impl<'a> From> for NodeStorageEntry<'a> { diff --git a/raphtory-storage/src/graph/nodes/node_ref.rs b/raphtory-storage/src/graph/nodes/node_ref.rs index e0b3186554..ad95760cbb 100644 --- a/raphtory-storage/src/graph/nodes/node_ref.rs +++ b/raphtory-storage/src/graph/nodes/node_ref.rs @@ -3,6 +3,7 @@ use crate::graph::{ nodes::{node_additions::NodeAdditions, node_storage_ops::NodeStorageOps}, variants::storage_variants2::StorageVariants2, }; +use db4_graph::{entries::node::LockedNodeEntry, ReadLockedTemporalGraph}; use raphtory_api::{ core::{ entities::{ @@ -16,6 +17,7 @@ use raphtory_api::{ iter::IntoDynBoxed, }; use raphtory_core::storage::node_entry::NodePtr; +use storage::Extension; use std::{borrow::Cow, ops::Range}; #[cfg(feature = "storage")] @@ -23,7 +25,7 @@ use crate::disk::storage_interface::node::DiskNode; #[derive(Copy, Clone, Debug)] pub enum NodeStorageRef<'a> { - Mem(NodePtr<'a>), + Mem(LockedNodeEntry<'a, Extension>), #[cfg(feature = "storage")] Disk(DiskNode<'a>), } diff --git a/raphtory-storage/src/graph/nodes/nodes.rs b/raphtory-storage/src/graph/nodes/nodes.rs index 31a48f4bee..f028a0030f 100644 --- a/raphtory-storage/src/graph/nodes/nodes.rs +++ b/raphtory-storage/src/graph/nodes/nodes.rs @@ -1,14 +1,14 @@ use super::node_ref::NodeStorageRef; use crate::graph::nodes::nodes_ref::NodesStorageEntry; +use db4_graph::ReadLockedTemporalGraph; use raphtory_api::core::entities::VID; -use raphtory_core::storage::ReadLockedStorage; use std::sync::Arc; #[cfg(feature = "storage")] use crate::disk::storage_interface::nodes::DiskNodesOwned; pub enum NodesStorage { - Mem(Arc), + Mem(Arc), #[cfg(feature = "storage")] Disk(DiskNodesOwned), } @@ -17,7 +17,7 @@ impl NodesStorage { #[inline] pub fn as_ref(&self) -> NodesStorageEntry { match self { - NodesStorage::Mem(storage) => NodesStorageEntry::Mem(storage), + NodesStorage::Mem(storage) => NodesStorageEntry::Mem(storage.as_ref()), #[cfg(feature = "storage")] NodesStorage::Disk(storage) => NodesStorageEntry::Disk(storage.as_ref()), } @@ -26,7 +26,7 @@ impl NodesStorage { #[inline] pub fn node_entry(&self, vid: VID) -> NodeStorageRef { match self { - NodesStorage::Mem(storage) => NodeStorageRef::Mem(storage.get_entry(vid)), + NodesStorage::Mem(storage) => NodeStorageRef::Mem(storage.node(vid)), #[cfg(feature = "storage")] NodesStorage::Disk(storage) => NodeStorageRef::Disk(storage.node(vid)), } diff --git a/raphtory-storage/src/graph/nodes/nodes_ref.rs b/raphtory-storage/src/graph/nodes/nodes_ref.rs index dfe02fbde2..9132440786 100644 --- a/raphtory-storage/src/graph/nodes/nodes_ref.rs +++ b/raphtory-storage/src/graph/nodes/nodes_ref.rs @@ -1,16 +1,19 @@ +use std::sync::Arc; + use super::node_ref::NodeStorageRef; use crate::graph::variants::storage_variants3::StorageVariants3; +use db4_graph::{ReadLockedTemporalGraph, TemporalGraph}; use raphtory_api::core::entities::VID; -use raphtory_core::storage::ReadLockedStorage; use rayon::iter::ParallelIterator; +use storage::Extension; #[cfg(feature = "storage")] use crate::disk::storage_interface::nodes_ref::DiskNodesRef; #[derive(Debug)] -pub enum NodesStorageEntry<'a> { - Mem(&'a ReadLockedStorage), - Unlocked(ReadLockedStorage), +pub enum NodesStorageEntry<'a, EXT = Extension> { + Mem(&'a ReadLockedTemporalGraph), + Unlocked(ReadLockedTemporalGraph), #[cfg(feature = "storage")] Disk(DiskNodesRef<'a>), } @@ -29,8 +32,8 @@ macro_rules! for_all_variants { impl<'a> NodesStorageEntry<'a> { pub fn node(&self, vid: VID) -> NodeStorageRef<'_> { match self { - NodesStorageEntry::Mem(store) => NodeStorageRef::Mem(store.get_entry(vid)), - NodesStorageEntry::Unlocked(store) => NodeStorageRef::Mem(store.get_entry(vid)), + NodesStorageEntry::Mem(store) => NodeStorageRef::Mem(store.node(vid)), + NodesStorageEntry::Unlocked(store) => NodeStorageRef::Mem(store.node(vid)), #[cfg(feature = "storage")] NodesStorageEntry::Disk(store) => NodeStorageRef::Disk(store.node(vid)), } diff --git a/raphtory-storage/src/mutation/addition_ops_ext.rs b/raphtory-storage/src/mutation/addition_ops_ext.rs index f9bcfbc552..9f8be019d7 100644 --- a/raphtory-storage/src/mutation/addition_ops_ext.rs +++ b/raphtory-storage/src/mutation/addition_ops_ext.rs @@ -26,13 +26,15 @@ use crate::{ }, }; -#[repr(transparent)] pub struct WriteS< 'a, MNS: DerefMut, MES: DerefMut, EXT: PersistentStrategy, ES = ES>, ->(WriteSession<'a, MNS, MES, NS, ES, EXT>); +>{ + static_session: WriteSession<'a, MNS, MES, NS, ES, EXT>, + layer: Option, ES, EXT>> +} pub struct UnlockedSession<'a, EXT> { graph: &'a TemporalGraph, @@ -54,7 +56,15 @@ impl< layer: usize, props: impl IntoIterator, ) -> MaybeNew { - self.0.internal_add_edge(t, src, dst, lsn, layer, props) + let src = src.into(); + let dst = dst.into(); + let eid = self.static_session.add_static_edge(src, dst, lsn).map(|eid| eid.with_layer(layer)); + self.layer + .as_mut() + .map(|layer| { + layer.add_edge_into_layer(t, src, dst, eid, lsn, props); + }); + eid } } @@ -180,7 +190,7 @@ impl, ES = ES>> InternalAdditionOps self.graph_dir().join(format!("l_{}", layer_name)), self.max_page_len_nodes(), self.max_page_len_edges(), - ) + ).into() }); if new_layer_id >= layer_id { while self.layers().get(new_layer_id).is_none() { @@ -239,8 +249,13 @@ impl, ES = ES>> InternalAdditionOps e_id: Option, layer_id: usize, ) -> Self::AtomicAddEdge<'_> { + let static_session = self.static_graph().write_session(src, dst, e_id); let layer = &self.layers()[layer_id]; - WriteS(layer.write_session(src, dst, e_id)) + let layer = layer.write_session(src, dst, e_id); + WriteS { + static_session, + layer: Some(layer), + } } fn validate_edge_props>( From 92dc97cfaec94e2790d186c18a6c48d05af845ab Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Fri, 13 Jun 2025 14:14:26 +0100 Subject: [PATCH 009/321] add support for layers for nodes --- db4-storage/src/lib.rs | 34 +++--- db4-storage/src/pages/mod.rs | 19 +-- db4-storage/src/pages/node_page/writer.rs | 35 +++--- db4-storage/src/pages/node_store.rs | 24 ++-- db4-storage/src/pages/session.rs | 11 +- db4-storage/src/pages/test_utils.rs | 27 ++--- db4-storage/src/segments/edge_entry.rs | 2 +- db4-storage/src/segments/mod.rs | 1 - db4-storage/src/segments/node.rs | 114 +++++++++++------- db4-storage/src/segments/node_entry.rs | 31 +++-- .../src/mutation/addition_ops_ext.rs | 2 +- 11 files changed, 163 insertions(+), 137 deletions(-) diff --git a/db4-storage/src/lib.rs b/db4-storage/src/lib.rs index aa2ef88483..1690439474 100644 --- a/db4-storage/src/lib.rs +++ b/db4-storage/src/lib.rs @@ -180,12 +180,13 @@ pub trait NodeSegmentOps: Send + Sync { head_lock: impl DerefMut, ) -> Result<(), DBV4Error>; - fn check_node(&self, pos: LocalPOS) -> bool; + fn check_node(&self, pos: LocalPOS, layer_id: usize) -> bool; fn get_out_edge( &self, pos: LocalPOS, dst: impl Into, + layer_id: usize, locked_head: impl Deref, ) -> Option; @@ -193,6 +194,7 @@ pub trait NodeSegmentOps: Send + Sync { &self, pos: LocalPOS, src: impl Into, + layer_id: usize, locked_head: impl Deref, ) -> Option; @@ -231,47 +233,47 @@ pub trait NodeRefOps<'a>: Copy + Clone + Send + Sync { type TProps: TPropOps<'a>; - fn out_edges(self) -> impl Iterator + 'a; + fn out_edges(self, layer_id: usize) -> impl Iterator + 'a; - fn inb_edges(self) -> impl Iterator + 'a; + fn inb_edges(self, layer_id: usize) -> impl Iterator + 'a; - fn out_edges_sorted(self) -> impl Iterator + 'a; + fn out_edges_sorted(self, layer_id: usize) -> impl Iterator + 'a; - fn inb_edges_sorted(self) -> impl Iterator + 'a; + fn inb_edges_sorted(self, layer_id: usize) -> impl Iterator + 'a; - fn out_nbrs(self) -> impl Iterator + 'a + fn out_nbrs(self, layer_id: usize) -> impl Iterator + 'a where Self: Sized, { - self.out_edges().map(|(v, _)| v) + self.out_edges(layer_id).map(|(v, _)| v) } - fn inb_nbrs(self) -> impl Iterator + 'a + fn inb_nbrs(self, layer_id: usize) -> impl Iterator + 'a where Self: Sized, { - self.inb_edges().map(|(v, _)| v) + self.inb_edges(layer_id).map(|(v, _)| v) } - fn out_nbrs_sorted(self) -> impl Iterator + 'a + fn out_nbrs_sorted(self, layer_id: usize) -> impl Iterator + 'a where Self: Sized, { - self.out_edges_sorted().map(|(v, _)| v) + self.out_edges_sorted(layer_id).map(|(v, _)| v) } - fn inb_nbrs_sorted(self) -> impl Iterator + 'a + fn inb_nbrs_sorted(self, layer_id: usize) -> impl Iterator + 'a where Self: Sized, { - self.inb_edges_sorted().map(|(v, _)| v) + self.inb_edges_sorted(layer_id).map(|(v, _)| v) } - fn additions(self) -> Self::Additions; + fn additions(self, layer_id: usize) -> Self::Additions; - fn c_prop(self, prop_id: usize) -> Option; + fn c_prop(self, layer_id: usize, prop_id: usize) -> Option; - fn t_prop(self, prop_id: usize) -> Self::TProps; + fn t_prop(self, layer_id: usize, prop_id: usize) -> Self::TProps; } pub mod error { diff --git a/db4-storage/src/pages/mod.rs b/db4-storage/src/pages/mod.rs index e9839760d3..c5e6c3cbc0 100644 --- a/db4-storage/src/pages/mod.rs +++ b/db4-storage/src/pages/mod.rs @@ -8,7 +8,11 @@ use std::{ }; use crate::{ - error::DBV4Error, pages::{edge_store::ReadLockedEdgeStorage, node_store::ReadLockedNodeStorage}, properties::props_meta_writer::PropsMetaWriter, segments::{edge::MemEdgeSegment, node::MemNodeSegment}, EdgeSegmentOps, LocalPOS, NodeSegmentOps + EdgeSegmentOps, LocalPOS, NodeSegmentOps, + error::DBV4Error, + pages::{edge_store::ReadLockedEdgeStorage, node_store::ReadLockedNodeStorage}, + properties::props_meta_writer::PropsMetaWriter, + segments::{edge::MemEdgeSegment, node::MemNodeSegment}, }; use edge_page::writer::EdgeWriter; use edge_store::EdgeStorageInner; @@ -58,10 +62,7 @@ pub struct ReadLockedGraphStore { impl, ES: EdgeSegmentOps, EXT: Clone + Default> GraphStore { - - pub fn read_locked( - self: &Arc, - ) -> ReadLockedGraphStore { + pub fn read_locked(self: &Arc) -> ReadLockedGraphStore { let nodes = self.nodes.locked(); let edges = self.edges.locked(); ReadLockedGraphStore { @@ -393,13 +394,14 @@ impl, ES: EdgeSegmentOps, E pub fn update_node_const_props>( &self, node: impl Into, + layer_id: usize, props: Vec<(PN, Prop)>, ) -> Result<(), DBV4Error> { let node = node.into(); let (segment, node_pos) = self.nodes.resolve_pos(node); let mut node_writer = self.nodes.writer(segment); let prop_writer = PropsMetaWriter::constant(&self.node_meta, props.into_iter())?; - node_writer.update_c_props(node_pos, prop_writer.into_props_const()?, 0); // TODO: LSN + node_writer.update_c_props(node_pos, layer_id, prop_writer.into_props_const()?, 0); // TODO: LSN Ok(()) } @@ -407,6 +409,7 @@ impl, ES: EdgeSegmentOps, E &self, t: impl TryIntoInputTime, node: impl Into, + layer_id: usize, props: Vec<(PN, Prop)>, ) -> Result<(), DBV4Error> { let node = node.into(); @@ -416,7 +419,7 @@ impl, ES: EdgeSegmentOps, E let mut node_writer = self.nodes.writer(segment); let prop_writer = PropsMetaWriter::temporal(&self.node_meta, props.into_iter())?; - node_writer.add_props(t, node_pos, prop_writer.into_props_temporal()?, 0); // TODO: LSN + node_writer.add_props(t, node_pos, layer_id, prop_writer.into_props_temporal()?, 0); // TODO: LSN Ok(()) } @@ -600,7 +603,7 @@ mod test { let node = g.nodes().node(3); let node_entry = node.as_ref(); - let actual: Vec<_> = node_entry.additions().iter_t().collect(); + let actual: Vec<_> = node_entry.additions(0).iter_t().collect(); assert_eq!(actual, vec![4]); }; diff --git a/db4-storage/src/pages/node_page/writer.rs b/db4-storage/src/pages/node_page/writer.rs index 074cdfaeba..3a5df5539a 100644 --- a/db4-storage/src/pages/node_page/writer.rs +++ b/db4-storage/src/pages/node_page/writer.rs @@ -49,12 +49,13 @@ impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> NodeWri lsn: u64, ) { let src_pos = src_pos.into(); - self.writer.as_mut().set_lsn(lsn); let e_id = e_id.into(); + let layer_id = e_id.layer(); + self.writer.as_mut()[layer_id].set_lsn(lsn); let is_new_node = self.writer.add_outbound_edge(t, src_pos, dst, e_id); - if is_new_node && !self.page.check_node(src_pos) { + if is_new_node && !self.page.check_node(src_pos, layer_id) { self.page.increment_num_nodes(); self.global_num_nodes .fetch_add(1, std::sync::atomic::Ordering::Relaxed); @@ -90,12 +91,13 @@ impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> NodeWri e_id: impl Into, lsn: u64, ) { - self.writer.as_mut().set_lsn(lsn); let e_id = e_id.into(); + let layer = e_id.layer(); + self.writer.as_mut()[layer].set_lsn(lsn); let dst_pos = dst_pos.into(); let is_new_node = self.writer.add_inbound_edge(t, dst_pos, src, e_id); - if is_new_node && !self.page.check_node(dst_pos) { + if is_new_node && !self.page.check_node(dst_pos, layer) { self.page.increment_num_nodes(); self.global_num_nodes .fetch_add(1, std::sync::atomic::Ordering::Relaxed); @@ -106,37 +108,40 @@ impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> NodeWri &mut self, t: T, pos: LocalPOS, + layer_id: usize, props: impl IntoIterator, lsn: u64, ) { - self.writer.as_mut().set_lsn(lsn); - self.writer.add_props(t, pos, props); - // self.est_size = self.page.increment_size(size_of::<(i64, i64)>()); + self.writer.as_mut()[layer_id].set_lsn(lsn); + self.writer.add_props(t, pos, layer_id, props); } pub fn update_c_props( &mut self, pos: LocalPOS, + layer_id: usize, props: impl IntoIterator, lsn: u64, ) { - self.writer.as_mut().set_lsn(lsn); - self.writer.update_c_props(pos, props); - // self.est_size = self.page.increment_size(size_of::<(i64, i64)>()); + self.writer.as_mut()[layer_id].set_lsn(lsn); + self.writer.update_c_props(pos, layer_id, props); } pub fn update_timestamp(&mut self, t: T, pos: LocalPOS, e_id: ELID, lsn: u64) { - self.writer.as_mut().set_lsn(lsn); + let layer_id = e_id.layer(); + self.writer.as_mut()[layer_id].set_lsn(lsn); self.writer.update_timestamp(t, pos, e_id); // self.est_size = self.page.increment_size(size_of::<(i64, i64)>()); } - pub fn get_out_edge(&self, pos: LocalPOS, dst: VID) -> Option { - self.page.get_out_edge(pos, dst, self.writer.deref()) + pub fn get_out_edge(&self, pos: LocalPOS, dst: VID, layer_id: usize) -> Option { + self.page + .get_out_edge(pos, dst, layer_id, self.writer.deref()) } - pub fn get_inb_edge(&self, pos: LocalPOS, src: VID) -> Option { - self.page.get_inb_edge(pos, src, self.writer.deref()) + pub fn get_inb_edge(&self, pos: LocalPOS, src: VID, layer_id: usize) -> Option { + self.page + .get_inb_edge(pos, src, layer_id, self.writer.deref()) } } diff --git a/db4-storage/src/pages/node_store.rs b/db4-storage/src/pages/node_store.rs index a0cc25800e..d0cbbd9219 100644 --- a/db4-storage/src/pages/node_store.rs +++ b/db4-storage/src/pages/node_store.rs @@ -1,8 +1,8 @@ use super::{node_page::writer::NodeWriter, resolve_pos}; use crate::{ - error::DBV4Error, pages::locked::nodes::{LockedNodePage, WriteLockedNodePages}, segments::node::MemNodeSegment, LocalPOS, ReadLockedNS, NodeSegmentOps + LocalPOS, NodeSegmentOps, ReadLockedNS, error::DBV4Error, segments::node::MemNodeSegment, }; -use parking_lot::{RwLockReadGuard, RwLockWriteGuard}; +use parking_lot::RwLockWriteGuard; use raphtory_api::core::entities::properties::meta::Meta; use raphtory_core::entities::{EID, VID}; use std::{ @@ -25,20 +25,18 @@ pub struct NodeStorageInner { } #[derive(Debug)] -pub struct ReadLockedNodeStorage{ +pub struct ReadLockedNodeStorage { storage: Arc>, locked_pages: Box<[ReadLockedNS]>, } - impl, EXT: Clone> NodeStorageInner { - - pub fn locked( - self: &Arc, - ) -> ReadLockedNodeStorage { - let locked_pages = self.pages.iter().map(|(_, segment)| { - segment.locked() - }).collect::>(); + pub fn locked(self: &Arc) -> ReadLockedNodeStorage { + let locked_pages = self + .pages + .iter() + .map(|(_, segment)| segment.locked()) + .collect::>(); ReadLockedNodeStorage { storage: self.clone(), locked_pages, @@ -202,13 +200,13 @@ impl, EXT: Clone> NodeStorageInner resolve_pos(i.into(), self.max_page_len) } - pub fn get_edge(&self, src: VID, dst: VID) -> Option { + pub fn get_edge(&self, src: VID, dst: VID, layer_id: usize) -> Option { let (src_chunk, src_pos) = self.resolve_pos(src); if src_chunk >= self.pages.count() { return None; } let src_page = &self.pages[src_chunk]; - src_page.get_out_edge(src_pos, dst, src_page.head()) + src_page.get_out_edge(src_pos, dst, layer_id, src_page.head()) } pub fn grow(&self, new_len: usize) { diff --git a/db4-storage/src/pages/session.rs b/db4-storage/src/pages/session.rs index 3bae4c727f..f0d36647e9 100644 --- a/db4-storage/src/pages/session.rs +++ b/db4-storage/src/pages/session.rs @@ -59,6 +59,7 @@ impl< let src = src.into(); let dst = dst.into(); let e_id = edge.inner(); + let layer = e_id.layer(); let edge_writer = self .edge_writer @@ -69,7 +70,7 @@ impl< .node_writers .get_mut_src() .writer - .as_ref() + .as_ref()[layer] .max_page_len(); let edge_max_page_len = edge_writer.writer.as_ref().max_page_len(); @@ -110,14 +111,14 @@ impl< let (_, src_pos) = self.graph.nodes().resolve_pos(src); let (_, dst_pos) = self.graph.nodes().resolve_pos(dst); - if let Some(e_id) = self.node_writers.get_mut_src().get_out_edge(src_pos, dst) { + if let Some(e_id) = self.node_writers.get_mut_src().get_out_edge(src_pos, dst, 0) { let mut edge_writer = self.graph.edge_writer(e_id); let (_, edge_pos) = self.graph.edges().resolve_pos(e_id); edge_writer.add_static_edge(Some(edge_pos), src, dst, lsn, None); MaybeNew::Existing(e_id) } else { - if let Some(e_id) = self.node_writers.get_mut_src().get_out_edge(src_pos, dst) { + if let Some(e_id) = self.node_writers.get_mut_src().get_out_edge(src_pos, dst, 0) { let mut edge_writer = self.graph.edge_writer(e_id); let (_, edge_pos) = self.graph.edges().resolve_pos(e_id); @@ -163,7 +164,7 @@ impl< let (_, src_pos) = self.graph.nodes().resolve_pos(src); let (_, dst_pos) = self.graph.nodes().resolve_pos(dst); - if let Some(e_id) = self.node_writers.get_mut_src().get_out_edge(src_pos, dst) { + if let Some(e_id) = self.node_writers.get_mut_src().get_out_edge(src_pos, dst, layer) { let mut edge_writer = self.graph.edge_writer(e_id); let (_, edge_pos) = self.graph.edges().resolve_pos(e_id); edge_writer.add_edge(t, Some(edge_pos), src, dst, props, lsn, None); @@ -178,7 +179,7 @@ impl< MaybeNew::Existing(e_id) } else { - if let Some(e_id) = self.node_writers.get_mut_src().get_out_edge(src_pos, dst) { + if let Some(e_id) = self.node_writers.get_mut_src().get_out_edge(src_pos, dst, layer) { let mut edge_writer = self.graph.edge_writer(e_id); let (_, edge_pos) = self.graph.edges().resolve_pos(e_id); let e_id = e_id.with_layer(layer); diff --git a/db4-storage/src/pages/test_utils.rs b/db4-storage/src/pages/test_utils.rs index e0dc2a1fa2..5cbdf41656 100644 --- a/db4-storage/src/pages/test_utils.rs +++ b/db4-storage/src/pages/test_utils.rs @@ -1,13 +1,10 @@ use std::{ collections::{HashMap, HashSet}, - fs::File, - io::Write, path::Path, }; use bigdecimal::BigDecimal; use chrono::{DateTime, NaiveDateTime, Utc}; -use either::Either::Left; use itertools::Itertools; use proptest::{collection, prelude::*}; use raphtory_api::core::entities::properties::{ @@ -117,19 +114,19 @@ pub fn check_edges_support< let entry = nodes.node(n); let adj = entry.as_ref(); - let out_nbrs: Vec<_> = adj.out_nbrs_sorted().collect(); + let out_nbrs: Vec<_> = adj.out_nbrs_sorted(0).collect(); assert_eq!(out_nbrs, exp_out, "{stage} node: {:?}", n); - let in_nbrs: Vec<_> = adj.inb_nbrs_sorted().collect(); + let in_nbrs: Vec<_> = adj.inb_nbrs_sorted(0).collect(); assert_eq!(in_nbrs, exp_inb, "{stage} node: {:?}", n); - for (exp_dst, eid) in adj.out_edges() { + for (exp_dst, eid) in adj.out_edges(0) { let (src, dst) = edges.get_edge(eid).unwrap(); assert_eq!(src, n, "{stage}"); assert_eq!(dst, exp_dst, "{stage}"); } - for (exp_src, eid) in adj.inb_edges() { + for (exp_src, eid) in adj.inb_edges(0) { let (src, dst) = edges.get_edge(eid).unwrap(); assert_eq!(src, exp_src, "{stage}"); assert_eq!(dst, n, "{stage}"); @@ -175,13 +172,13 @@ pub fn check_graph_with_nodes_support< let graph = make_graph(graph_dir.path()); for (node, t, t_props) in temp_props { - let err = graph.add_node_props(*t, *node, t_props.clone()); + let err = graph.add_node_props(*t, *node, 0, t_props.clone()); assert!(err.is_ok(), "Failed to add node: {:?}", err); } for (node, const_props) in const_props { - let err = graph.update_node_const_props(*node, const_props.clone()); + let err = graph.update_node_const_props(*node, 0, const_props.clone()); assert!(err.is_ok(), "Failed to add node: {:?}", err); } @@ -200,7 +197,7 @@ pub fn check_graph_with_nodes_support< for (node, ts_expected) in ts_for_nodes { let ne = graph.nodes().node(node); let node_entry = ne.as_ref(); - let actual: Vec<_> = node_entry.additions().iter_t().collect(); + let actual: Vec<_> = node_entry.additions(0).iter_t().collect(); assert_eq!( actual, ts_expected, "Expected node additions for node ({node:?})", @@ -227,7 +224,7 @@ pub fn check_graph_with_nodes_support< .const_prop_meta() .get_id(&name) .unwrap_or_else(|| panic!("Failed to get prop id for {}", name)); - let actual_props = node_entry.c_prop(prop_id); + let actual_props = node_entry.c_prop(0, prop_id); if !const_props.is_empty() { let actual_prop = actual_props @@ -267,7 +264,7 @@ pub fn check_graph_with_nodes_support< let ne = graph.nodes().node(node); let node_entry = ne.as_ref(); - let actual_props = node_entry.t_prop(prop_id).iter_t().collect::>(); + let actual_props = node_entry.t_prop(0, prop_id).iter_t().collect::>(); assert_eq!( actual_props, props, @@ -310,7 +307,7 @@ pub fn check_graph_with_props_support< for ((src, dst), const_props) in const_props { let eid = graph .nodes() - .get_edge(*src, *dst) + .get_edge(*src, *dst, 0) .unwrap_or_else(|| panic!("Failed to get edge ({:?}, {:?}) from graph", src, dst)); let res = graph.update_edge_const_props(eid, const_props.clone()); @@ -377,7 +374,7 @@ pub fn check_graph_with_props_support< let edge = graph .nodes() - .get_edge(src, dst) + .get_edge(src, dst, 0) .unwrap_or_else(|| panic!("Failed to get edge ({:?}, {:?}) from graph", src, dst)); let edge = graph.edges().edge(edge); let e = edge.as_ref(); @@ -415,7 +412,7 @@ pub fn check_graph_with_props_support< for (node_id, ts) in node_groups { let node = graph.nodes().node(node_id); let node_entry = node.as_ref(); - let actual_additions_ts = node_entry.additions().iter_t().collect::>(); + let actual_additions_ts = node_entry.additions(0).iter_t().collect::>(); assert_eq!( actual_additions_ts, ts, diff --git a/db4-storage/src/segments/edge_entry.rs b/db4-storage/src/segments/edge_entry.rs index 95f724a3ba..92e8db7bdd 100644 --- a/db4-storage/src/segments/edge_entry.rs +++ b/db4-storage/src/segments/edge_entry.rs @@ -74,7 +74,7 @@ impl<'a> EdgeRefOps<'a> for MemEdgeRef<'a> { fn t_prop(self, prop_id: usize) -> Self::TProps { self.es .as_ref() - .t_prop(self.pos, prop_id, 0) + .t_prop(self.pos, prop_id) .unwrap_or_default() } } diff --git a/db4-storage/src/segments/mod.rs b/db4-storage/src/segments/mod.rs index b6d83a2468..59edc03aca 100644 --- a/db4-storage/src/segments/mod.rs +++ b/db4-storage/src/segments/mod.rs @@ -208,7 +208,6 @@ impl SegmentContainer { &self, item_id: impl Into, prop_id: usize, - _layer_id: usize, ) -> Option> { let item_id = item_id.into(); self.data.get(&item_id).and_then(|entry| { diff --git a/db4-storage/src/segments/node.rs b/db4-storage/src/segments/node.rs index 654628c35c..57a64e71bb 100644 --- a/db4-storage/src/segments/node.rs +++ b/db4-storage/src/segments/node.rs @@ -21,7 +21,15 @@ use crate::{LocalPOS, NodeSegmentOps, error::DBV4Error, segments::node_entry::Me #[derive(Debug)] pub struct MemNodeSegment { - inner: SegmentContainer, + inner: Vec>, +} + +impl >> From for MemNodeSegment { + fn from(inner: I) -> Self { + Self { + inner: inner.into_iter().collect(), + } + } } #[derive(Debug, Default)] @@ -54,59 +62,57 @@ impl HasRow for AdjEntry { } } -impl AsRef> for MemNodeSegment { - fn as_ref(&self) -> &SegmentContainer { +impl AsRef<[SegmentContainer]> for MemNodeSegment { + fn as_ref(&self) -> &[SegmentContainer] { &self.inner } } -impl AsMut> for MemNodeSegment { - fn as_mut(&mut self) -> &mut SegmentContainer { +impl AsMut<[SegmentContainer]> for MemNodeSegment { + fn as_mut(&mut self) -> &mut [SegmentContainer] { &mut self.inner } } impl MemNodeSegment { - #[inline(always)] - fn get_adj(&self, n: LocalPOS) -> Option<&Adj> { - self.inner.get(&n).map(|AdjEntry { adj, .. }| adj) + pub fn est_size(&self) -> usize { + self.inner.iter().map(|seg| seg.est_size()).sum::() } - pub fn has_node(&self, n: LocalPOS) -> bool { - self.inner.get(&n).is_some() + #[inline(always)] + fn get_adj(&self, n: LocalPOS, layer_id: usize) -> Option<&Adj> { + self.inner[layer_id].get(&n).map(|AdjEntry { adj, .. }| adj) } - // pub(crate) fn contains_out(&self, n: LocalPOS, dst: VID) -> bool { - // self.get_out_edge(n, dst).is_some() - // } - - // pub(crate) fn contains_in(&self, n: LocalPOS, src: VID) -> bool { - // self.get_in_edge(n, src).is_some() - // } + pub fn has_node(&self, n: LocalPOS, layer_id: usize) -> bool { + self.inner[layer_id].items().get(n.0).map_or(false, |v| *v) + } - pub fn get_out_edge(&self, n: LocalPOS, dst: VID) -> Option { - self.get_adj(n) + pub fn get_out_edge(&self, n: LocalPOS, dst: VID, layer_id: usize) -> Option { + self.get_adj(n, layer_id) .and_then(|adj| adj.get_edge(dst, Direction::OUT)) } - pub fn get_inb_edge(&self, n: LocalPOS, src: VID) -> Option { - self.get_adj(n) + pub fn get_inb_edge(&self, n: LocalPOS, src: VID, layer_id: usize) -> Option { + self.get_adj(n, layer_id) .and_then(|adj| adj.get_edge(src, Direction::IN)) } - pub fn out_edges(&self, n: LocalPOS) -> impl Iterator + '_ { - self.get_adj(n).into_iter().flat_map(|adj| adj.out_iter()) + pub fn out_edges(&self, n: LocalPOS, layer_id: usize) -> impl Iterator + '_ { + self.get_adj(n, layer_id) + .into_iter() + .flat_map(|adj| adj.out_iter()) } - pub fn inb_edges(&self, n: LocalPOS) -> impl Iterator + '_ { - self.get_adj(n).into_iter().flat_map(|adj| adj.inb_iter()) + pub fn inb_edges(&self, n: LocalPOS, layer_id: usize) -> impl Iterator + '_ { + self.get_adj(n, layer_id) + .into_iter() + .flat_map(|adj| adj.inb_iter()) } -} -impl MemNodeSegment { pub fn new(segment_id: usize, max_page_len: usize, meta: Arc) -> Self { Self { - inner: SegmentContainer::new(segment_id, max_page_len, meta), + inner: vec![SegmentContainer::new(segment_id, max_page_len, meta)], } } @@ -119,7 +125,8 @@ impl MemNodeSegment { ) -> bool { let dst = dst.into(); let e_id = e_id.into(); - let add_out = self.inner.reserve_local_row(src_pos).map_either( + let layer_id = e_id.layer(); + let add_out = self.inner[layer_id].reserve_local_row(src_pos).map_either( |row| { row.adj.add_edge_out(dst, e_id.edge); row.row() @@ -146,9 +153,10 @@ impl MemNodeSegment { ) -> bool { let src = src.into(); let e_id = e_id.into(); + let layer_id = e_id.layer(); let dst_pos = dst_pos.into(); - let add_in = self.inner.reserve_local_row(dst_pos).map_either( + let add_in = self.inner[layer_id].reserve_local_row(dst_pos).map_either( |row| { row.adj.add_edge_into(src, e_id.edge); row.row() @@ -166,15 +174,14 @@ impl MemNodeSegment { } fn update_timestamp_inner(&mut self, t: T, row: usize, e_id: ELID) { - let mut prop_mut_entry = self.inner.properties_mut().get_mut_entry(row); + let mut prop_mut_entry = self.inner[e_id.layer()].properties_mut().get_mut_entry(row); let ts = TimeIndexEntry::new(t.t(), t.i()); prop_mut_entry.append_edge_ts(ts, e_id); } pub fn update_timestamp(&mut self, t: T, node_pos: LocalPOS, e_id: ELID) { - let row = self - .inner + let row = self.inner[e_id.layer()] .reserve_local_row(node_pos) .either(|a| a.row, |a| a.row); self.update_timestamp_inner(t, row, e_id); @@ -184,13 +191,13 @@ impl MemNodeSegment { &mut self, t: T, node_pos: LocalPOS, + layer_id: usize, props: impl IntoIterator, ) { - let row = self - .inner + let row = self.inner[layer_id] .reserve_local_row(node_pos) .either(|a| a.row, |a| a.row); - let mut prop_mut_entry = self.inner.properties_mut().get_mut_entry(row); + let mut prop_mut_entry = self.inner[layer_id].properties_mut().get_mut_entry(row); let ts = TimeIndexEntry::new(t.t(), t.i()); prop_mut_entry.append_t_props(ts, props); } @@ -198,15 +205,27 @@ impl MemNodeSegment { pub fn update_c_props( &mut self, node_pos: LocalPOS, + layer_id: usize, props: impl IntoIterator, ) { - let row = self - .inner + let row = self.inner[layer_id] .reserve_local_row(node_pos) .either(|a| a.row, |a| a.row); - let mut prop_mut_entry = self.inner.properties_mut().get_mut_entry(row); + let mut prop_mut_entry = self.inner[layer_id].properties_mut().get_mut_entry(row); prop_mut_entry.append_const_props(props); } + + pub fn latest(&self) -> Option { + Iterator::max(self.inner.iter().filter_map(|seg| seg.latest())) + } + + pub fn earliest(&self) -> Option { + Iterator::min(self.inner.iter().filter_map(|seg| seg.earliest())) + } + + pub fn t_len(&self) -> usize { + self.inner.iter().map(|seg| seg.t_len()).sum() + } } pub struct NodeSegmentView { @@ -222,15 +241,15 @@ impl NodeSegmentOps for NodeSegmentView { type Entry<'a> = MemNodeEntry<'a, parking_lot::RwLockReadGuard<'a, MemNodeSegment>>; fn latest(&self) -> Option { - self.head().as_ref().latest() + self.head().latest() } fn earliest(&self) -> Option { - self.head().as_ref().earliest() + self.head().latest() } fn t_len(&self) -> usize { - self.head().as_ref().t_len() + self.head().t_len() } fn load( @@ -254,7 +273,8 @@ impl NodeSegmentOps for NodeSegmentView { _ext: Self::Extension, ) -> Self { Self { - inner: parking_lot::RwLock::new(MemNodeSegment::new(page_id, max_page_len, meta)).into(), + inner: parking_lot::RwLock::new(MemNodeSegment::new(page_id, max_page_len, meta)) + .into(), segment_id: page_id, num_nodes: AtomicUsize::new(0), _ext: (), @@ -292,7 +312,7 @@ impl NodeSegmentOps for NodeSegmentView { Ok(()) } - fn check_node(&self, _pos: LocalPOS) -> bool { + fn check_node(&self, _pos: LocalPOS, _layer_id: usize) -> bool { false } @@ -300,18 +320,20 @@ impl NodeSegmentOps for NodeSegmentView { &self, pos: LocalPOS, dst: impl Into, + layer_id: usize, locked_head: impl Deref, ) -> Option { - locked_head.get_out_edge(pos, dst.into()) + locked_head.get_out_edge(pos, dst.into(), layer_id) } fn get_inb_edge( &self, pos: LocalPOS, src: impl Into, + layer_id: usize, locked_head: impl Deref, ) -> Option { - locked_head.get_inb_edge(pos, src.into()) + locked_head.get_inb_edge(pos, src.into(), layer_id) } fn entry<'a>(&'a self, pos: impl Into) -> Self::Entry<'a> { diff --git a/db4-storage/src/segments/node_entry.rs b/db4-storage/src/segments/node_entry.rs index ad0ddf0452..cdf4f6cdba 100644 --- a/db4-storage/src/segments/node_entry.rs +++ b/db4-storage/src/segments/node_entry.rs @@ -54,34 +54,33 @@ impl<'a> NodeRefOps<'a> for MemNodeRef<'a> { type Additions = MemAdditions<'a>; type TProps = TPropCell<'a>; - fn out_edges(self) -> impl Iterator + 'a { - self.ns.out_edges(self.pos) + fn out_edges(self, layer_id: usize) -> impl Iterator + 'a { + self.ns.out_edges(self.pos, layer_id) } - fn inb_edges(self) -> impl Iterator + 'a { - self.ns.inb_edges(self.pos) + fn inb_edges(self, layer_id: usize) -> impl Iterator + 'a { + self.ns.inb_edges(self.pos, layer_id) } - fn out_edges_sorted(self) -> impl Iterator + 'a { - self.ns.out_edges(self.pos) + fn out_edges_sorted(self, layer_id: usize) -> impl Iterator + 'a { + self.ns.out_edges(self.pos, layer_id) } - fn inb_edges_sorted(self) -> impl Iterator + 'a { - self.ns.inb_edges(self.pos) + fn inb_edges_sorted(self, layer_id: usize) -> impl Iterator + 'a { + self.ns.inb_edges(self.pos, layer_id) } - fn additions(self) -> Self::Additions { - MemAdditions::Props(self.ns.as_ref().additions(self.pos)) + fn additions(self, layer_id: usize) -> Self::Additions { + MemAdditions::Props(self.ns.as_ref()[layer_id].additions(self.pos)) } - fn c_prop(self, prop_id: usize) -> Option { - self.ns.as_ref().c_prop(self.pos, prop_id) + fn c_prop(self, layer_id: usize, prop_id: usize) -> Option { + self.ns.as_ref()[layer_id].c_prop(self.pos, prop_id) } - fn t_prop(self, prop_id: usize) -> Self::TProps { - self.ns - .as_ref() - .t_prop(self.pos, prop_id, 0) + fn t_prop(self, layer_id: usize, prop_id: usize) -> Self::TProps { + self.ns.as_ref()[layer_id] + .t_prop(self.pos, prop_id) .unwrap_or_default() } } diff --git a/raphtory-storage/src/mutation/addition_ops_ext.rs b/raphtory-storage/src/mutation/addition_ops_ext.rs index 9f8be019d7..60ac135f71 100644 --- a/raphtory-storage/src/mutation/addition_ops_ext.rs +++ b/raphtory-storage/src/mutation/addition_ops_ext.rs @@ -58,7 +58,7 @@ impl< ) -> MaybeNew { let src = src.into(); let dst = dst.into(); - let eid = self.static_session.add_static_edge(src, dst, lsn).map(|eid| eid.with_layer(layer)); + let eid = self.static_session.add_static_edge(src, dst, lsn).map(|eid| eid.with_layer(0)); self.layer .as_mut() .map(|layer| { From 2fadc5b441b36fc318244c546b13d9a44cc6d220 Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Fri, 13 Jun 2025 17:54:52 +0100 Subject: [PATCH 010/321] add support for layers in edges --- db4-storage/src/lib.rs | 12 ++- db4-storage/src/pages/edge_page/writer.rs | 26 +++-- db4-storage/src/pages/edge_store.rs | 8 +- db4-storage/src/pages/mod.rs | 7 +- db4-storage/src/segments/edge.rs | 117 ++++++++++++++++------ db4-storage/src/segments/edge_entry.rs | 16 +-- 6 files changed, 125 insertions(+), 61 deletions(-) diff --git a/db4-storage/src/lib.rs b/db4-storage/src/lib.rs index 1690439474..e23ebc5a56 100644 --- a/db4-storage/src/lib.rs +++ b/db4-storage/src/lib.rs @@ -25,9 +25,9 @@ pub mod persist; pub mod properties; pub mod segments; +pub type Extension = (); pub type NS

= NodeSegmentView

; pub type ES

= EdgeSegmentView

; - pub type Layer = GraphStore, EdgeSegmentView, EXT>; pub type ReadLockedLayer = ReadLockedGraphStore; @@ -83,12 +83,14 @@ pub trait EdgeSegmentOps: Send + Sync { fn contains_edge( &self, edge_pos: LocalPOS, + layer_id: usize, locked_head: impl Deref, ) -> bool; fn get_edge( &self, edge_pos: LocalPOS, + layer_id: usize, locked_head: impl Deref, ) -> Option<(VID, VID)>; @@ -126,13 +128,13 @@ pub trait EdgeRefOps<'a>: Copy + Clone + Send + Sync { type Additions: TimeIndexOps<'a>; type TProps: TPropOps<'a>; - fn edge(self) -> Option<(VID, VID)>; + fn edge(self, layer_id: usize) -> Option<(VID, VID)>; - fn additions(self) -> Self::Additions; + fn additions(self, layer_id: usize) -> Self::Additions; - fn c_prop(self, prop_id: usize) -> Option; + fn c_prop(self, layer_id: usize, prop_id: usize) -> Option; - fn t_prop(self, prop_id: usize) -> Self::TProps; + fn t_prop(self, layer_id: usize, prop_id: usize) -> Self::TProps; } pub trait NodeSegmentOps: Send + Sync { diff --git a/db4-storage/src/pages/edge_page/writer.rs b/db4-storage/src/pages/edge_page/writer.rs index 067f155304..3628d5d9f8 100644 --- a/db4-storage/src/pages/edge_page/writer.rs +++ b/db4-storage/src/pages/edge_page/writer.rs @@ -33,9 +33,10 @@ impl<'a, MP: DerefMut, ES: EdgeSegmentOps> EdgeWriter<' dst: impl Into, props: impl IntoIterator, lsn: u64, + layer_id: usize, exists_hint: Option, // used when edge_pos is Some but the is not counted, this is used in the bulk loader ) -> LocalPOS { - self.writer.as_mut().set_lsn(lsn); + self.writer.as_mut()[layer_id].set_lsn(lsn); if exists_hint == Some(false) && edge_pos.is_some() { self.new_local_pos(); // increment the counts, this is triggered from the bulk loader @@ -43,7 +44,7 @@ impl<'a, MP: DerefMut, ES: EdgeSegmentOps> EdgeWriter<' let edge_pos = edge_pos.unwrap_or_else(|| self.new_local_pos()); self.writer - .insert_edge_internal(t, edge_pos, src, dst, props); + .insert_edge_internal(t, edge_pos, src, dst, layer_id, props); edge_pos } @@ -52,17 +53,18 @@ impl<'a, MP: DerefMut, ES: EdgeSegmentOps> EdgeWriter<' edge_pos: Option, src: impl Into, dst: impl Into, + layer_id: usize, lsn: u64, exists_hint: Option, // used when edge_pos is Some but the is not counted, this is used in the bulk loader ) -> LocalPOS { - self.writer.as_mut().set_lsn(lsn); + self.writer.as_mut()[layer_id].set_lsn(lsn); if exists_hint == Some(false) && edge_pos.is_some() { self.new_local_pos(); // increment the counts, this is triggered from the bulk loader } let edge_pos = edge_pos.unwrap_or_else(|| self.new_local_pos()); - self.writer.insert_static_edge_internal(edge_pos, src, dst); + self.writer.insert_static_edge_internal(edge_pos, src, dst, layer_id); // self.est_size = self.page.increment_size(size_of::<(VID, VID)>()) // + self.writer.as_ref().t_prop_est_size(); edge_pos @@ -77,16 +79,12 @@ impl<'a, MP: DerefMut, ES: EdgeSegmentOps> EdgeWriter<' .fetch_add(1, std::sync::atomic::Ordering::Relaxed); } - pub fn contains_edge(&self, pos: LocalPOS) -> bool { - // self.writer.contains_edge(pos) || self.page.disk_contains_edge(pos) - self.page.contains_edge(pos, self.writer.deref()) + pub fn contains_edge(&self, pos: LocalPOS, layer_id: usize) -> bool { + self.page.contains_edge(pos, layer_id, self.writer.deref()) } - pub fn get_edge(&self, edge_pos: LocalPOS) -> Option<(VID, VID)> { - // self.writer - // .get_edge(edge_pos) - // .or_else(|| self.page.get_disk_edge(edge_pos)) - self.page.get_edge(edge_pos, self.writer.deref()) + pub fn get_edge(&self, layer_id: usize, edge_pos: LocalPOS) -> Option<(VID, VID)> { + self.page.get_edge(edge_pos, layer_id, self.writer.deref()) } pub fn update_c_props( @@ -94,11 +92,11 @@ impl<'a, MP: DerefMut, ES: EdgeSegmentOps> EdgeWriter<' edge_pos: LocalPOS, src: impl Into, dst: impl Into, + layer_id: usize, props: impl IntoIterator, ) { - // self.page.increment_size(size_of::<(VID, VID)>()); self.writer - .update_const_properties(edge_pos, src, dst, props); + .update_const_properties(edge_pos, src, dst, layer_id, props); } } diff --git a/db4-storage/src/pages/edge_store.rs b/db4-storage/src/pages/edge_store.rs index 93ff900841..77b78f8240 100644 --- a/db4-storage/src/pages/edge_store.rs +++ b/db4-storage/src/pages/edge_store.rs @@ -13,7 +13,7 @@ use crate::{ }; use parking_lot::{RwLock, RwLockWriteGuard}; use raphtory_api::core::entities::{EID, VID, properties::meta::Meta}; -use raphtory_core::storage::timeindex::TimeIndexEntry; +use raphtory_core::{entities::ELID, storage::timeindex::TimeIndexEntry}; const N: usize = 32; @@ -285,10 +285,12 @@ impl, EXT: Clone> EdgeStorageInner // ) // } - pub fn get_edge(&self, e_id: EID) -> Option<(VID, VID)> { + pub fn get_edge(&self, e_id: ELID) -> Option<(VID, VID)> { + let layer = e_id.layer(); + let e_id = e_id.edge; let (chunk, local_edge) = resolve_pos(e_id, self.max_page_len); let page = self.pages.get(chunk)?; - page.get_edge(local_edge, page.head()) + page.get_edge(local_edge, layer, page.head()) } pub fn edge(&self, e_id: impl Into) -> ES::Entry<'_> { diff --git a/db4-storage/src/pages/mod.rs b/db4-storage/src/pages/mod.rs index c5e6c3cbc0..804a7b6d4d 100644 --- a/db4-storage/src/pages/mod.rs +++ b/db4-storage/src/pages/mod.rs @@ -377,12 +377,13 @@ impl, ES: EdgeSegmentOps, E pub fn update_edge_const_props>( &self, - eid: impl Into, + eid: impl Into, props: Vec<(PN, Prop)>, ) -> Result<(), DBV4Error> { let eid = eid.into(); - let (_, edge_pos) = self.edges.resolve_pos(eid); - let mut edge_writer = self.edges.try_get_writer(eid)?; + let layer = eid.layer(); + let (_, edge_pos) = self.edges.resolve_pos(eid.edge); + let mut edge_writer = self.edges.try_get_writer(eid.edge)?; let (src, dst) = edge_writer .get_edge(edge_pos) .expect("Internal Error, EID should be checked at this point!"); diff --git a/db4-storage/src/segments/edge.rs b/db4-storage/src/segments/edge.rs index ec066122c4..e95bfea17a 100644 --- a/db4-storage/src/segments/edge.rs +++ b/db4-storage/src/segments/edge.rs @@ -8,7 +8,7 @@ use std::{ use parking_lot::lock_api::ArcRwLockReadGuard; use raphtory_api::core::entities::{ - VID, + VID, properties::{meta::Meta, prop::Prop}, }; use raphtory_core::storage::timeindex::{AsTime, TimeIndexEntry}; @@ -36,17 +36,25 @@ impl HasRow for MemPageEntry { #[derive(Debug)] pub struct MemEdgeSegment { - inner: SegmentContainer, + inner: Vec>, } -impl AsRef> for MemEdgeSegment { - fn as_ref(&self) -> &SegmentContainer { +impl>> From for MemEdgeSegment { + fn from(inner: I) -> Self { + Self { + inner: inner.into_iter().collect(), + } + } +} + +impl AsRef<[SegmentContainer]> for MemEdgeSegment { + fn as_ref(&self) -> &[SegmentContainer] { &self.inner } } -impl AsMut> for MemEdgeSegment { - fn as_mut(&mut self) -> &mut SegmentContainer { +impl AsMut<[SegmentContainer]> for MemEdgeSegment { + fn as_mut(&mut self) -> &mut [SegmentContainer] { &mut self.inner } } @@ -54,15 +62,17 @@ impl AsMut> for MemEdgeSegment { impl MemEdgeSegment { pub fn new(segment_id: usize, max_page_len: usize, meta: Arc) -> Self { Self { - inner: SegmentContainer::new(segment_id, max_page_len, meta), + inner: vec![SegmentContainer::new(segment_id, max_page_len, meta)], } } - pub fn get_edge(&self, edge_pos: impl Into) -> Option<(VID, VID)> { + pub fn est_size(&self) -> usize { + self.inner.iter().map(|seg| seg.est_size()).sum::() + } + + pub fn get_edge(&self, edge_pos: impl Into, layer_id: usize) -> Option<(VID, VID)> { let edge_pos = edge_pos.into(); - self.inner - .get(&edge_pos) - .map(|entry| (entry.src, entry.dst)) + self.inner.get(layer_id)?.get(&edge_pos).map(|entry| (entry.src, entry.dst)) } pub fn insert_edge_internal( @@ -71,14 +81,19 @@ impl MemEdgeSegment { edge_pos: impl Into, src: impl Into, dst: impl Into, + layer_id: usize, props: impl IntoIterator, ) { let edge_pos = edge_pos.into(); let src = src.into(); let dst = dst.into(); - let local_row = self.reserve_local_row(edge_pos, src, dst); + + // Ensure we have enough layers + self.ensure_layer(layer_id); + + let local_row = self.reserve_local_row(edge_pos, src, dst, layer_id); - let mut prop_entry: PropMutEntry<'_> = self.inner.properties_mut().get_mut_entry(local_row); + let mut prop_entry: PropMutEntry<'_> = self.inner[layer_id].properties_mut().get_mut_entry(local_row); let ts = TimeIndexEntry::new(t.t(), t.i()); prop_entry.append_t_props(ts, props) } @@ -88,10 +103,31 @@ impl MemEdgeSegment { edge_pos: LocalPOS, src: impl Into, dst: impl Into, + layer_id: usize, ) { let src = src.into(); let dst = dst.into(); - self.reserve_local_row(edge_pos, src, dst); + + // Ensure we have enough layers + self.ensure_layer(layer_id); + + self.reserve_local_row(edge_pos, src, dst, layer_id); + } + + fn ensure_layer(&mut self, layer_id: usize) { + if layer_id >= self.inner.len() { + // Get details from first layer to create consistent new layers + if let Some(first_layer) = self.inner.first() { + let segment_id = first_layer.segment_id(); + let max_page_len = first_layer.max_page_len(); + let meta = first_layer.meta().clone(); + + // Extend with new layers + while self.inner.len() <= layer_id { + self.inner.push(SegmentContainer::new(segment_id, max_page_len, meta.clone())); + } + } + } } fn reserve_local_row( @@ -99,10 +135,15 @@ impl MemEdgeSegment { edge_pos: LocalPOS, src: impl Into, dst: impl Into, + layer_id: usize, ) -> usize { let src = src.into(); let dst = dst.into(); - let row = self.inner.reserve_local_row(edge_pos).map_either( + + // Ensure we have enough layers + self.ensure_layer(layer_id); + + let row = self.inner[layer_id].reserve_local_row(edge_pos).map_either( |row| { row.src = src; row.dst = dst; @@ -122,29 +163,47 @@ impl MemEdgeSegment { edge_pos: impl Into, src: impl Into, dst: impl Into, + layer_id: usize, props: impl IntoIterator, ) { let edge_pos = edge_pos.into(); let src = src.into(); let dst = dst.into(); - let local_row = self.reserve_local_row(edge_pos, src, dst); - let mut prop_entry: PropMutEntry<'_> = self.inner.properties_mut().get_mut_entry(local_row); + + // Ensure we have enough layers + self.ensure_layer(layer_id); + + let local_row = self.reserve_local_row(edge_pos, src, dst, layer_id); + let mut prop_entry: PropMutEntry<'_> = self.inner[layer_id].properties_mut().get_mut_entry(local_row); prop_entry.append_const_props(props) } - pub fn insert_edge(&mut self, edge_pos: LocalPOS, src: impl Into, dst: impl Into) { - self.insert_edge_internal(0, edge_pos, src, dst, []); + pub fn insert_edge(&mut self, edge_pos: LocalPOS, src: impl Into, dst: impl Into, layer_id: usize) { + self.insert_edge_internal(0, edge_pos, src, dst, layer_id, []); } - pub fn contains_edge(&self, edge_pos: LocalPOS) -> bool { + pub fn contains_edge(&self, edge_pos: LocalPOS, layer_id: usize) -> bool { self.inner - .items() - .get::(edge_pos.0) + .get(layer_id) + .and_then(|layer| layer.items().get::(edge_pos.0)) .map(|b| *b) .unwrap_or_default() } + + pub fn latest(&self) -> Option { + Iterator::max(self.inner.iter().filter_map(|seg| seg.latest())) + } + + pub fn earliest(&self) -> Option { + Iterator::min(self.inner.iter().filter_map(|seg| seg.earliest())) + } + + pub fn t_len(&self) -> usize { + self.inner.iter().map(|seg| seg.t_len()).sum() + } } +// Update EdgeSegmentView implementation to use multiple layers pub struct EdgeSegmentView { segment: Arc>, segment_id: usize, @@ -158,15 +217,15 @@ impl EdgeSegmentOps for EdgeSegmentView { type Entry<'a> = MemEdgeEntry<'a, parking_lot::RwLockReadGuard<'a, MemEdgeSegment>>; fn latest(&self) -> Option { - self.head().as_ref().latest() + self.head().latest() } fn earliest(&self) -> Option { - self.head().as_ref().earliest() + self.head().earliest() } fn t_len(&self) -> usize { - self.head().as_ref().t_len() + self.head().t_len() } fn load( @@ -235,21 +294,23 @@ impl EdgeSegmentOps for EdgeSegmentView { fn contains_edge( &self, edge_pos: LocalPOS, + layer_id: usize, locked_head: impl Deref, ) -> bool { - locked_head.contains_edge(edge_pos) + locked_head.contains_edge(edge_pos, layer_id) } fn get_edge( &self, edge_pos: LocalPOS, + layer_id: usize, locked_head: impl Deref, ) -> Option<(VID, VID)> { - locked_head.get_edge(edge_pos) + locked_head.get_edge(edge_pos, layer_id) } fn entry<'a, LP: Into>(&'a self, edge_pos: LP) -> Self::Entry<'a> { let edge_pos = edge_pos.into(); MemEdgeEntry::new(edge_pos, self.head()) } -} +} \ No newline at end of file diff --git a/db4-storage/src/segments/edge_entry.rs b/db4-storage/src/segments/edge_entry.rs index 92e8db7bdd..b915257ba4 100644 --- a/db4-storage/src/segments/edge_entry.rs +++ b/db4-storage/src/segments/edge_entry.rs @@ -56,24 +56,24 @@ impl<'a> EdgeRefOps<'a> for MemEdgeRef<'a> { type TProps = TPropCell<'a>; - fn edge(self) -> Option<(VID, VID)> { + fn edge(self, layer_id: usize) -> Option<(VID, VID)> { self.es - .as_ref() + .as_ref()[layer_id] .get(&self.pos) .map(|entry| (entry.src, entry.dst)) } - fn additions(self) -> Self::Additions { - MemAdditions::Props(self.es.as_ref().additions(self.pos)) + fn additions(self, layer_id: usize) -> Self::Additions { + MemAdditions::Props(self.es.as_ref()[layer_id].additions(self.pos)) } - fn c_prop(self, prop_id: usize) -> Option { - self.es.as_ref().c_prop(self.pos, prop_id) + fn c_prop(self, layer_id: usize, prop_id: usize) -> Option { + self.es.as_ref()[layer_id].c_prop(self.pos, prop_id) } - fn t_prop(self, prop_id: usize) -> Self::TProps { + fn t_prop(self, layer_id:usize, prop_id: usize) -> Self::TProps { self.es - .as_ref() + .as_ref()[layer_id] .t_prop(self.pos, prop_id) .unwrap_or_default() } From 8ee5935238839317277c7e19572a368fe465d989 Mon Sep 17 00:00:00 2001 From: Fadhil Abubaker Date: Sat, 14 Jun 2025 07:54:34 -0400 Subject: [PATCH 011/321] Pass layer_id into edge ops wherever needed (#2124) * Pass layer ids into edge ops wherever needed * Make param order consistent for add_edge --- db4-storage/src/pages/edge_page/writer.rs | 2 +- db4-storage/src/pages/mod.rs | 6 ++++-- db4-storage/src/pages/session.rs | 26 ++++++++++++----------- db4-storage/src/pages/test_utils.rs | 26 +++++++++++++++-------- 4 files changed, 36 insertions(+), 24 deletions(-) diff --git a/db4-storage/src/pages/edge_page/writer.rs b/db4-storage/src/pages/edge_page/writer.rs index 3628d5d9f8..379903f23f 100644 --- a/db4-storage/src/pages/edge_page/writer.rs +++ b/db4-storage/src/pages/edge_page/writer.rs @@ -32,8 +32,8 @@ impl<'a, MP: DerefMut, ES: EdgeSegmentOps> EdgeWriter<' src: impl Into, dst: impl Into, props: impl IntoIterator, - lsn: u64, layer_id: usize, + lsn: u64, exists_hint: Option, // used when edge_pos is Some but the is not counted, this is used in the bulk loader ) -> LocalPOS { self.writer.as_mut()[layer_id].set_lsn(lsn); diff --git a/db4-storage/src/pages/mod.rs b/db4-storage/src/pages/mod.rs index 804a7b6d4d..d6bd6c848f 100644 --- a/db4-storage/src/pages/mod.rs +++ b/db4-storage/src/pages/mod.rs @@ -385,10 +385,12 @@ impl, ES: EdgeSegmentOps, E let (_, edge_pos) = self.edges.resolve_pos(eid.edge); let mut edge_writer = self.edges.try_get_writer(eid.edge)?; let (src, dst) = edge_writer - .get_edge(edge_pos) + .get_edge(layer, edge_pos) .expect("Internal Error, EID should be checked at this point!"); let prop_writer = PropsMetaWriter::constant(&self.edge_meta, props.into_iter())?; - edge_writer.update_c_props(edge_pos, src, dst, prop_writer.into_props_const()?); + + edge_writer.update_c_props(edge_pos, src, dst, layer, prop_writer.into_props_const()?); + Ok(()) } diff --git a/db4-storage/src/pages/session.rs b/db4-storage/src/pages/session.rs index f0d36647e9..9a478bc54b 100644 --- a/db4-storage/src/pages/session.rs +++ b/db4-storage/src/pages/session.rs @@ -72,14 +72,14 @@ impl< .writer .as_ref()[layer] .max_page_len(); - let edge_max_page_len = edge_writer.writer.as_ref().max_page_len(); + let edge_max_page_len = edge_writer.writer.as_ref()[layer].max_page_len(); let (_, src_pos) = resolve_pos(src, node_max_page_len); let (_, dst_pos) = resolve_pos(dst, node_max_page_len); let (_, edge_pos) = resolve_pos(e_id.edge, edge_max_page_len); let exists = Some(!edge.is_new()); - edge_writer.add_edge(t, Some(edge_pos), src, dst, props, lsn, exists); + edge_writer.add_edge(t, Some(edge_pos), src, dst, props, layer, lsn, exists); edge.if_new(|edge_id| { self.node_writers @@ -107,40 +107,42 @@ impl< ) -> MaybeNew { let src = src.into(); let dst = dst.into(); + let layer_id = 0; // static graph goes to layer 0 let (_, src_pos) = self.graph.nodes().resolve_pos(src); let (_, dst_pos) = self.graph.nodes().resolve_pos(dst); - if let Some(e_id) = self.node_writers.get_mut_src().get_out_edge(src_pos, dst, 0) { + if let Some(e_id) = self.node_writers.get_mut_src().get_out_edge(src_pos, dst, layer_id) { let mut edge_writer = self.graph.edge_writer(e_id); let (_, edge_pos) = self.graph.edges().resolve_pos(e_id); - edge_writer.add_static_edge(Some(edge_pos), src, dst, lsn, None); + + edge_writer.add_static_edge(Some(edge_pos), src, dst, layer_id, lsn, None); MaybeNew::Existing(e_id) } else { - if let Some(e_id) = self.node_writers.get_mut_src().get_out_edge(src_pos, dst, 0) { + if let Some(e_id) = self.node_writers.get_mut_src().get_out_edge(src_pos, dst, layer_id) { let mut edge_writer = self.graph.edge_writer(e_id); let (_, edge_pos) = self.graph.edges().resolve_pos(e_id); - edge_writer.add_static_edge(Some(edge_pos), src, dst, lsn, None); + edge_writer.add_static_edge(Some(edge_pos), src, dst, layer_id, lsn, None); MaybeNew::Existing(e_id) } else { let mut edge_writer = self.graph.get_free_writer(); - let edge_id = edge_writer.add_static_edge(None, src, dst, lsn, None); + let edge_id = edge_writer.add_static_edge(None, src, dst, layer_id, lsn, None); let edge_id = edge_id.as_eid(edge_writer.segment_id(), self.graph.edges().max_page_len()); self.node_writers.get_mut_src().add_static_outbound_edge( src_pos, dst, - edge_id.with_layer(0), + edge_id.with_layer(layer_id), lsn, ); self.node_writers.get_mut_dst().add_static_inbound_edge( dst_pos, src, - edge_id.with_layer(0), + edge_id.with_layer(layer_id), lsn, ); @@ -167,7 +169,7 @@ impl< if let Some(e_id) = self.node_writers.get_mut_src().get_out_edge(src_pos, dst, layer) { let mut edge_writer = self.graph.edge_writer(e_id); let (_, edge_pos) = self.graph.edges().resolve_pos(e_id); - edge_writer.add_edge(t, Some(edge_pos), src, dst, props, lsn, None); + edge_writer.add_edge(t, Some(edge_pos), src, dst, props, layer, lsn, None); let e_id = e_id.with_layer(layer); self.node_writers @@ -184,7 +186,7 @@ impl< let (_, edge_pos) = self.graph.edges().resolve_pos(e_id); let e_id = e_id.with_layer(layer); - edge_writer.add_edge(t, Some(edge_pos), src, dst, props, lsn, None); + edge_writer.add_edge(t, Some(edge_pos), src, dst, props, layer, lsn, None); self.node_writers .get_mut_src() .update_timestamp(t, src_pos, e_id, lsn); @@ -195,7 +197,7 @@ impl< MaybeNew::Existing(e_id) } else { let mut edge_writer = self.graph.get_free_writer(); - let edge_id = edge_writer.add_edge(t, None, src, dst, props, lsn, None); + let edge_id = edge_writer.add_edge(t, None, src, dst, props, layer, lsn, None); let edge_id = edge_id.as_eid(edge_writer.segment_id(), self.graph.edges().max_page_len()); let edge_id = edge_id.with_layer(layer); diff --git a/db4-storage/src/pages/test_utils.rs b/db4-storage/src/pages/test_utils.rs index 5cbdf41656..e17f91fe14 100644 --- a/db4-storage/src/pages/test_utils.rs +++ b/db4-storage/src/pages/test_utils.rs @@ -11,7 +11,7 @@ use raphtory_api::core::entities::properties::{ prop::{DECIMAL_MAX, Prop, PropType}, tprop::TPropOps, }; -use raphtory_core::{entities::VID, storage::timeindex::TimeIndexOps}; +use raphtory_core::{entities::{ELID, VID}, storage::timeindex::TimeIndexOps}; use rayon::prelude::*; use crate::{ @@ -77,6 +77,7 @@ pub fn check_edges_support< ) { let nodes = graph.nodes(); let edges = graph.edges(); + let layer_id = 0; if !es.is_empty() { assert!(nodes.pages().count() > 0, "{stage}"); @@ -114,20 +115,24 @@ pub fn check_edges_support< let entry = nodes.node(n); let adj = entry.as_ref(); - let out_nbrs: Vec<_> = adj.out_nbrs_sorted(0).collect(); + let out_nbrs: Vec<_> = adj.out_nbrs_sorted(layer_id).collect(); assert_eq!(out_nbrs, exp_out, "{stage} node: {:?}", n); - let in_nbrs: Vec<_> = adj.inb_nbrs_sorted(0).collect(); + let in_nbrs: Vec<_> = adj.inb_nbrs_sorted(layer_id).collect(); assert_eq!(in_nbrs, exp_inb, "{stage} node: {:?}", n); for (exp_dst, eid) in adj.out_edges(0) { - let (src, dst) = edges.get_edge(eid).unwrap(); + let elid = ELID::new(eid, layer_id); + let (src, dst) = edges.get_edge(elid).unwrap(); + assert_eq!(src, n, "{stage}"); assert_eq!(dst, exp_dst, "{stage}"); } for (exp_src, eid) in adj.inb_edges(0) { - let (src, dst) = edges.get_edge(eid).unwrap(); + let elid = ELID::new(eid, layer_id); + let (src, dst) = edges.get_edge(elid).unwrap(); + assert_eq!(src, exp_src, "{stage}"); assert_eq!(dst, n, "{stage}"); } @@ -305,11 +310,13 @@ pub fn check_graph_with_props_support< // Add const props for ((src, dst), const_props) in const_props { + let layer_id = 0; let eid = graph .nodes() - .get_edge(*src, *dst, 0) + .get_edge(*src, *dst, layer_id) .unwrap_or_else(|| panic!("Failed to get edge ({:?}, {:?}) from graph", src, dst)); - let res = graph.update_edge_const_props(eid, const_props.clone()); + let elid = ELID::new(eid, layer_id); + let res = graph.update_edge_const_props(elid, const_props.clone()); assert!( res.is_ok(), @@ -378,7 +385,8 @@ pub fn check_graph_with_props_support< .unwrap_or_else(|| panic!("Failed to get edge ({:?}, {:?}) from graph", src, dst)); let edge = graph.edges().edge(edge); let e = edge.as_ref(); - let actual_props = e.t_prop(prop_id).iter_t().collect::>(); + let layer_id = 0; + let actual_props = e.t_prop(layer_id, prop_id).iter_t().collect::>(); assert_eq!( actual_props, props, @@ -394,7 +402,7 @@ pub fn check_graph_with_props_support< .const_prop_meta() .get_id(name) .unwrap_or_else(|| panic!("Failed to get prop id for {}", name)); - let actual_props = e.c_prop(prop_id); + let actual_props = e.c_prop(layer_id, prop_id); assert_eq!( actual_props.as_ref(), Some(prop), From 88476e16a03c4f7d197c4b1c6a621bff31bcb63f Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Mon, 16 Jun 2025 14:48:28 +0100 Subject: [PATCH 012/321] fixing layers on disk as they make no sense part 2 --- db4-storage/src/segments/edge.rs | 90 +++++++++++++++++---------- db4-storage/src/segments/mod.rs | 10 +-- db4-storage/src/segments/node.rs | 44 +++++++------ raphtory-api/src/core/entities/mod.rs | 6 ++ 4 files changed, 93 insertions(+), 57 deletions(-) diff --git a/db4-storage/src/segments/edge.rs b/db4-storage/src/segments/edge.rs index e95bfea17a..13396cfff7 100644 --- a/db4-storage/src/segments/edge.rs +++ b/db4-storage/src/segments/edge.rs @@ -8,7 +8,7 @@ use std::{ use parking_lot::lock_api::ArcRwLockReadGuard; use raphtory_api::core::entities::{ - VID, + VID, properties::{meta::Meta, prop::Prop}, }; use raphtory_core::storage::timeindex::{AsTime, TimeIndexEntry}; @@ -36,43 +36,54 @@ impl HasRow for MemPageEntry { #[derive(Debug)] pub struct MemEdgeSegment { - inner: Vec>, + layers: Vec>, } impl>> From for MemEdgeSegment { fn from(inner: I) -> Self { Self { - inner: inner.into_iter().collect(), + layers: inner.into_iter().collect(), } } } impl AsRef<[SegmentContainer]> for MemEdgeSegment { fn as_ref(&self) -> &[SegmentContainer] { - &self.inner + &self.layers } } impl AsMut<[SegmentContainer]> for MemEdgeSegment { fn as_mut(&mut self) -> &mut [SegmentContainer] { - &mut self.inner + &mut self.layers } } impl MemEdgeSegment { pub fn new(segment_id: usize, max_page_len: usize, meta: Arc) -> Self { Self { - inner: vec![SegmentContainer::new(segment_id, max_page_len, meta)], + layers: vec![SegmentContainer::new(segment_id, max_page_len, meta)], } } pub fn est_size(&self) -> usize { - self.inner.iter().map(|seg| seg.est_size()).sum::() + self.layers.iter().map(|seg| seg.est_size()).sum::() + } + + pub fn lsn(&self) -> u64 { + self.layers.iter().map(|seg| seg.lsn()).min().unwrap_or(0) + } + + pub fn max_page_len(&self) -> usize { + self.layers[0].max_page_len() } pub fn get_edge(&self, edge_pos: impl Into, layer_id: usize) -> Option<(VID, VID)> { let edge_pos = edge_pos.into(); - self.inner.get(layer_id)?.get(&edge_pos).map(|entry| (entry.src, entry.dst)) + self.layers + .get(layer_id)? + .get(&edge_pos) + .map(|entry| (entry.src, entry.dst)) } pub fn insert_edge_internal( @@ -87,13 +98,15 @@ impl MemEdgeSegment { let edge_pos = edge_pos.into(); let src = src.into(); let dst = dst.into(); - + // Ensure we have enough layers self.ensure_layer(layer_id); - + let local_row = self.reserve_local_row(edge_pos, src, dst, layer_id); - let mut prop_entry: PropMutEntry<'_> = self.inner[layer_id].properties_mut().get_mut_entry(local_row); + let mut prop_entry: PropMutEntry<'_> = self.layers[layer_id] + .properties_mut() + .get_mut_entry(local_row); let ts = TimeIndexEntry::new(t.t(), t.i()); prop_entry.append_t_props(ts, props) } @@ -107,24 +120,28 @@ impl MemEdgeSegment { ) { let src = src.into(); let dst = dst.into(); - + // Ensure we have enough layers self.ensure_layer(layer_id); - + self.reserve_local_row(edge_pos, src, dst, layer_id); } fn ensure_layer(&mut self, layer_id: usize) { - if layer_id >= self.inner.len() { + if layer_id >= self.layers.len() { // Get details from first layer to create consistent new layers - if let Some(first_layer) = self.inner.first() { + if let Some(first_layer) = self.layers.first() { let segment_id = first_layer.segment_id(); let max_page_len = first_layer.max_page_len(); let meta = first_layer.meta().clone(); - + // Extend with new layers - while self.inner.len() <= layer_id { - self.inner.push(SegmentContainer::new(segment_id, max_page_len, meta.clone())); + while self.layers.len() <= layer_id { + self.layers.push(SegmentContainer::new( + segment_id, + max_page_len, + meta.clone(), + )); } } } @@ -139,11 +156,11 @@ impl MemEdgeSegment { ) -> usize { let src = src.into(); let dst = dst.into(); - + // Ensure we have enough layers self.ensure_layer(layer_id); - - let row = self.inner[layer_id].reserve_local_row(edge_pos).map_either( + + let row = self.layers[layer_id].reserve_local_row(edge_pos).map_either( |row| { row.src = src; row.dst = dst; @@ -169,37 +186,45 @@ impl MemEdgeSegment { let edge_pos = edge_pos.into(); let src = src.into(); let dst = dst.into(); - + // Ensure we have enough layers self.ensure_layer(layer_id); - + let local_row = self.reserve_local_row(edge_pos, src, dst, layer_id); - let mut prop_entry: PropMutEntry<'_> = self.inner[layer_id].properties_mut().get_mut_entry(local_row); + let mut prop_entry: PropMutEntry<'_> = self.layers[layer_id] + .properties_mut() + .get_mut_entry(local_row); prop_entry.append_const_props(props) } - pub fn insert_edge(&mut self, edge_pos: LocalPOS, src: impl Into, dst: impl Into, layer_id: usize) { + pub fn insert_edge( + &mut self, + edge_pos: LocalPOS, + src: impl Into, + dst: impl Into, + layer_id: usize, + ) { self.insert_edge_internal(0, edge_pos, src, dst, layer_id, []); } pub fn contains_edge(&self, edge_pos: LocalPOS, layer_id: usize) -> bool { - self.inner + self.layers .get(layer_id) .and_then(|layer| layer.items().get::(edge_pos.0)) .map(|b| *b) .unwrap_or_default() } - + pub fn latest(&self) -> Option { - Iterator::max(self.inner.iter().filter_map(|seg| seg.latest())) + Iterator::max(self.layers.iter().filter_map(|seg| seg.latest())) } pub fn earliest(&self) -> Option { - Iterator::min(self.inner.iter().filter_map(|seg| seg.earliest())) + Iterator::min(self.layers.iter().filter_map(|seg| seg.earliest())) } pub fn t_len(&self) -> usize { - self.inner.iter().map(|seg| seg.t_len()).sum() + self.layers.iter().map(|seg| seg.t_len()).sum() } } @@ -249,7 +274,8 @@ impl EdgeSegmentOps for EdgeSegmentView { _ext: Self::Extension, ) -> Self { Self { - segment: parking_lot::RwLock::new(MemEdgeSegment::new(page_id, max_page_len, meta)).into(), + segment: parking_lot::RwLock::new(MemEdgeSegment::new(page_id, max_page_len, meta)) + .into(), segment_id: page_id, num_edges: AtomicUsize::new(0), _ext: (), @@ -313,4 +339,4 @@ impl EdgeSegmentOps for EdgeSegmentView { let edge_pos = edge_pos.into(); MemEdgeEntry::new(edge_pos, self.head()) } -} \ No newline at end of file +} diff --git a/db4-storage/src/segments/mod.rs b/db4-storage/src/segments/mod.rs index 59edc03aca..2b916a077e 100644 --- a/db4-storage/src/segments/mod.rs +++ b/db4-storage/src/segments/mod.rs @@ -24,7 +24,7 @@ pub mod edge_entry; pub mod node_entry; pub struct SegmentContainer { - page_id: usize, + segment_id: usize, items: BitVec, data: FxHashMap, max_page_len: usize, @@ -44,7 +44,7 @@ impl Debug for SegmentContainer { data.sort_by(|a, b| a.0.cmp(&b.0)); f.debug_struct("SegmentContainer") - .field("page_id", &self.page_id) + .field("page_id", &self.segment_id) .field("items", &items as &dyn Debug) .field("data", &data) .field("max_page_len", &self.max_page_len) @@ -59,10 +59,10 @@ pub trait HasRow: Default { } impl SegmentContainer { - pub fn new(page_id: usize, max_page_len: usize, meta: Arc) -> Self { + pub fn new(segment_id: usize, max_page_len: usize, meta: Arc) -> Self { assert!(max_page_len > 0, "max_page_len must be greater than 0"); Self { - page_id, + segment_id, items: BitVec::repeat(false, max_page_len), data: Default::default(), max_page_len, @@ -142,7 +142,7 @@ impl SegmentContainer { #[inline(always)] pub fn segment_id(&self) -> usize { - self.page_id + self.segment_id } #[inline(always)] diff --git a/db4-storage/src/segments/node.rs b/db4-storage/src/segments/node.rs index 57a64e71bb..3da74691a1 100644 --- a/db4-storage/src/segments/node.rs +++ b/db4-storage/src/segments/node.rs @@ -21,13 +21,13 @@ use crate::{LocalPOS, NodeSegmentOps, error::DBV4Error, segments::node_entry::Me #[derive(Debug)] pub struct MemNodeSegment { - inner: Vec>, + layers: Vec>, } -impl >> From for MemNodeSegment { +impl>> From for MemNodeSegment { fn from(inner: I) -> Self { Self { - inner: inner.into_iter().collect(), + layers: inner.into_iter().collect(), } } } @@ -64,28 +64,32 @@ impl HasRow for AdjEntry { impl AsRef<[SegmentContainer]> for MemNodeSegment { fn as_ref(&self) -> &[SegmentContainer] { - &self.inner + &self.layers } } impl AsMut<[SegmentContainer]> for MemNodeSegment { fn as_mut(&mut self) -> &mut [SegmentContainer] { - &mut self.inner + &mut self.layers } } impl MemNodeSegment { + pub fn lsn(&self) -> u64 { + self.layers.iter().map(|seg| seg.lsn()).min().unwrap_or(0) + } + pub fn est_size(&self) -> usize { - self.inner.iter().map(|seg| seg.est_size()).sum::() + self.layers.iter().map(|seg| seg.est_size()).sum::() } #[inline(always)] fn get_adj(&self, n: LocalPOS, layer_id: usize) -> Option<&Adj> { - self.inner[layer_id].get(&n).map(|AdjEntry { adj, .. }| adj) + self.layers[layer_id].get(&n).map(|AdjEntry { adj, .. }| adj) } pub fn has_node(&self, n: LocalPOS, layer_id: usize) -> bool { - self.inner[layer_id].items().get(n.0).map_or(false, |v| *v) + self.layers[layer_id].items().get(n.0).map_or(false, |v| *v) } pub fn get_out_edge(&self, n: LocalPOS, dst: VID, layer_id: usize) -> Option { @@ -112,7 +116,7 @@ impl MemNodeSegment { pub fn new(segment_id: usize, max_page_len: usize, meta: Arc) -> Self { Self { - inner: vec![SegmentContainer::new(segment_id, max_page_len, meta)], + layers: vec![SegmentContainer::new(segment_id, max_page_len, meta)], } } @@ -126,7 +130,7 @@ impl MemNodeSegment { let dst = dst.into(); let e_id = e_id.into(); let layer_id = e_id.layer(); - let add_out = self.inner[layer_id].reserve_local_row(src_pos).map_either( + let add_out = self.layers[layer_id].reserve_local_row(src_pos).map_either( |row| { row.adj.add_edge_out(dst, e_id.edge); row.row() @@ -156,7 +160,7 @@ impl MemNodeSegment { let layer_id = e_id.layer(); let dst_pos = dst_pos.into(); - let add_in = self.inner[layer_id].reserve_local_row(dst_pos).map_either( + let add_in = self.layers[layer_id].reserve_local_row(dst_pos).map_either( |row| { row.adj.add_edge_into(src, e_id.edge); row.row() @@ -174,14 +178,14 @@ impl MemNodeSegment { } fn update_timestamp_inner(&mut self, t: T, row: usize, e_id: ELID) { - let mut prop_mut_entry = self.inner[e_id.layer()].properties_mut().get_mut_entry(row); + let mut prop_mut_entry = self.layers[e_id.layer()].properties_mut().get_mut_entry(row); let ts = TimeIndexEntry::new(t.t(), t.i()); prop_mut_entry.append_edge_ts(ts, e_id); } pub fn update_timestamp(&mut self, t: T, node_pos: LocalPOS, e_id: ELID) { - let row = self.inner[e_id.layer()] + let row = self.layers[e_id.layer()] .reserve_local_row(node_pos) .either(|a| a.row, |a| a.row); self.update_timestamp_inner(t, row, e_id); @@ -194,10 +198,10 @@ impl MemNodeSegment { layer_id: usize, props: impl IntoIterator, ) { - let row = self.inner[layer_id] + let row = self.layers[layer_id] .reserve_local_row(node_pos) .either(|a| a.row, |a| a.row); - let mut prop_mut_entry = self.inner[layer_id].properties_mut().get_mut_entry(row); + let mut prop_mut_entry = self.layers[layer_id].properties_mut().get_mut_entry(row); let ts = TimeIndexEntry::new(t.t(), t.i()); prop_mut_entry.append_t_props(ts, props); } @@ -208,23 +212,23 @@ impl MemNodeSegment { layer_id: usize, props: impl IntoIterator, ) { - let row = self.inner[layer_id] + let row = self.layers[layer_id] .reserve_local_row(node_pos) .either(|a| a.row, |a| a.row); - let mut prop_mut_entry = self.inner[layer_id].properties_mut().get_mut_entry(row); + let mut prop_mut_entry = self.layers[layer_id].properties_mut().get_mut_entry(row); prop_mut_entry.append_const_props(props); } pub fn latest(&self) -> Option { - Iterator::max(self.inner.iter().filter_map(|seg| seg.latest())) + Iterator::max(self.layers.iter().filter_map(|seg| seg.latest())) } pub fn earliest(&self) -> Option { - Iterator::min(self.inner.iter().filter_map(|seg| seg.earliest())) + Iterator::min(self.layers.iter().filter_map(|seg| seg.earliest())) } pub fn t_len(&self) -> usize { - self.inner.iter().map(|seg| seg.t_len()).sum() + self.layers.iter().map(|seg| seg.t_len()).sum() } } diff --git a/raphtory-api/src/core/entities/mod.rs b/raphtory-api/src/core/entities/mod.rs index 8529734f26..16b0d64aa3 100644 --- a/raphtory-api/src/core/entities/mod.rs +++ b/raphtory-api/src/core/entities/mod.rs @@ -95,6 +95,12 @@ impl EID { } } +impl From for EID { + fn from(elid: ELID) -> Self { + elid.edge + } +} + #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)] pub struct ELID { pub edge: EID, From 946ccd08ef0870f7c3060af2c6415fa84aac288d Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Tue, 17 Jun 2025 14:27:35 +0100 Subject: [PATCH 013/321] minor changes to MemNodeSegment --- db4-storage/src/segments/node.rs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/db4-storage/src/segments/node.rs b/db4-storage/src/segments/node.rs index 3da74691a1..9516460fe9 100644 --- a/db4-storage/src/segments/node.rs +++ b/db4-storage/src/segments/node.rs @@ -17,7 +17,11 @@ use std::{ }; use super::{HasRow, SegmentContainer}; -use crate::{LocalPOS, NodeSegmentOps, error::DBV4Error, segments::node_entry::MemNodeEntry}; +use crate::{ + LocalPOS, NodeSegmentOps, + error::DBV4Error, + segments::node_entry::{MemNodeEntry, MemNodeRef}, +}; #[derive(Debug)] pub struct MemNodeSegment { @@ -85,7 +89,9 @@ impl MemNodeSegment { #[inline(always)] fn get_adj(&self, n: LocalPOS, layer_id: usize) -> Option<&Adj> { - self.layers[layer_id].get(&n).map(|AdjEntry { adj, .. }| adj) + self.layers[layer_id] + .get(&n) + .map(|AdjEntry { adj, .. }| adj) } pub fn has_node(&self, n: LocalPOS, layer_id: usize) -> bool { @@ -178,7 +184,9 @@ impl MemNodeSegment { } fn update_timestamp_inner(&mut self, t: T, row: usize, e_id: ELID) { - let mut prop_mut_entry = self.layers[e_id.layer()].properties_mut().get_mut_entry(row); + let mut prop_mut_entry = self.layers[e_id.layer()] + .properties_mut() + .get_mut_entry(row); let ts = TimeIndexEntry::new(t.t(), t.i()); prop_mut_entry.append_edge_ts(ts, e_id); @@ -230,6 +238,10 @@ impl MemNodeSegment { pub fn t_len(&self) -> usize { self.layers.iter().map(|seg| seg.t_len()).sum() } + + pub fn node_ref(&self, pos: LocalPOS) -> MemNodeRef { + MemNodeRef::new(pos, self) + } } pub struct NodeSegmentView { From cc1a32ffce7933fe166bd555ee5fdfd8f857d4ce Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Tue, 17 Jun 2025 17:45:55 +0100 Subject: [PATCH 014/321] formatting and bug fixes --- db4-graph/src/entries/mod.rs | 2 +- db4-graph/src/entries/node.rs | 12 ++----- db4-graph/src/lib.rs | 1 - db4-storage/src/lib.rs | 2 +- db4-storage/src/pages/edge_page/writer.rs | 3 +- db4-storage/src/pages/session.rs | 32 +++++++++++++------ db4-storage/src/pages/test_utils.rs | 5 ++- db4-storage/src/segments/edge.rs | 27 +++++++++------- db4-storage/src/segments/edge_entry.rs | 8 ++--- db4-storage/src/segments/mod.rs | 6 +--- raphtory-storage/src/graph/graph.rs | 4 +-- raphtory-storage/src/graph/locked.rs | 2 +- .../src/graph/nodes/node_entry.rs | 5 ++- raphtory-storage/src/graph/nodes/node_ref.rs | 2 +- .../src/mutation/addition_ops_ext.rs | 20 ++++++------ 15 files changed, 70 insertions(+), 61 deletions(-) diff --git a/db4-graph/src/entries/mod.rs b/db4-graph/src/entries/mod.rs index 12e2c60478..492bc84b46 100644 --- a/db4-graph/src/entries/mod.rs +++ b/db4-graph/src/entries/mod.rs @@ -1 +1 @@ -pub mod node; \ No newline at end of file +pub mod node; diff --git a/db4-graph/src/entries/node.rs b/db4-graph/src/entries/node.rs index 000d67025e..467a0e57c7 100644 --- a/db4-graph/src/entries/node.rs +++ b/db4-graph/src/entries/node.rs @@ -10,10 +10,7 @@ pub struct LockedNodeEntry<'a, EXT> { impl<'a, EXT> LockedNodeEntry<'a, EXT> { pub fn new(vid: VID, node_support: &'a ReadLockedTemporalGraph) -> Self { - Self { - vid, - node_support, - } + Self { vid, node_support } } pub fn vid(&self) -> VID { @@ -33,10 +30,7 @@ pub struct UnlockedNodeEntry<'a, EXT> { impl<'a, EXT> UnlockedNodeEntry<'a, EXT> { pub fn new(vid: VID, node_support: &'a TemporalGraph) -> Self { - Self { - vid, - node_support, - } + Self { vid, node_support } } pub fn vid(&self) -> VID { @@ -46,4 +40,4 @@ impl<'a, EXT> UnlockedNodeEntry<'a, EXT> { pub fn node_support(&self) -> &TemporalGraph { &self.node_support } -} \ No newline at end of file +} diff --git a/db4-graph/src/lib.rs b/db4-graph/src/lib.rs index d1397bd3ac..b0d645bcd6 100644 --- a/db4-graph/src/lib.rs +++ b/db4-graph/src/lib.rs @@ -60,7 +60,6 @@ impl ReadLockedTemporalGraph { } } - impl, ES = ES>> TemporalGraph { pub fn node(&self, vid: VID) -> UnlockedNodeEntry { UnlockedNodeEntry::new(vid, self) diff --git a/db4-storage/src/lib.rs b/db4-storage/src/lib.rs index e23ebc5a56..470af4e52b 100644 --- a/db4-storage/src/lib.rs +++ b/db4-storage/src/lib.rs @@ -31,7 +31,7 @@ pub type ES

= EdgeSegmentView

; pub type Layer = GraphStore, EdgeSegmentView, EXT>; pub type ReadLockedLayer = ReadLockedGraphStore; -pub trait EdgeSegmentOps: Send + Sync { +pub trait EdgeSegmentOps: Send + Sync + std::fmt::Debug { type Extension; type Entry<'a>: EdgeEntryOps<'a> diff --git a/db4-storage/src/pages/edge_page/writer.rs b/db4-storage/src/pages/edge_page/writer.rs index 379903f23f..88590896cc 100644 --- a/db4-storage/src/pages/edge_page/writer.rs +++ b/db4-storage/src/pages/edge_page/writer.rs @@ -64,7 +64,8 @@ impl<'a, MP: DerefMut, ES: EdgeSegmentOps> EdgeWriter<' } let edge_pos = edge_pos.unwrap_or_else(|| self.new_local_pos()); - self.writer.insert_static_edge_internal(edge_pos, src, dst, layer_id); + self.writer + .insert_static_edge_internal(edge_pos, src, dst, layer_id); // self.est_size = self.page.increment_size(size_of::<(VID, VID)>()) // + self.writer.as_ref().t_prop_est_size(); edge_pos diff --git a/db4-storage/src/pages/session.rs b/db4-storage/src/pages/session.rs index 9a478bc54b..9b14a294bf 100644 --- a/db4-storage/src/pages/session.rs +++ b/db4-storage/src/pages/session.rs @@ -66,12 +66,8 @@ impl< .as_mut() .expect("Internal Error: Edge writer is not set"); - let node_max_page_len = self - .node_writers - .get_mut_src() - .writer - .as_ref()[layer] - .max_page_len(); + let node_max_page_len = + self.node_writers.get_mut_src().writer.as_ref()[layer].max_page_len(); let edge_max_page_len = edge_writer.writer.as_ref()[layer].max_page_len(); let (_, src_pos) = resolve_pos(src, node_max_page_len); @@ -112,7 +108,11 @@ impl< let (_, src_pos) = self.graph.nodes().resolve_pos(src); let (_, dst_pos) = self.graph.nodes().resolve_pos(dst); - if let Some(e_id) = self.node_writers.get_mut_src().get_out_edge(src_pos, dst, layer_id) { + if let Some(e_id) = self + .node_writers + .get_mut_src() + .get_out_edge(src_pos, dst, layer_id) + { let mut edge_writer = self.graph.edge_writer(e_id); let (_, edge_pos) = self.graph.edges().resolve_pos(e_id); @@ -120,7 +120,11 @@ impl< MaybeNew::Existing(e_id) } else { - if let Some(e_id) = self.node_writers.get_mut_src().get_out_edge(src_pos, dst, layer_id) { + if let Some(e_id) = self + .node_writers + .get_mut_src() + .get_out_edge(src_pos, dst, layer_id) + { let mut edge_writer = self.graph.edge_writer(e_id); let (_, edge_pos) = self.graph.edges().resolve_pos(e_id); @@ -166,7 +170,11 @@ impl< let (_, src_pos) = self.graph.nodes().resolve_pos(src); let (_, dst_pos) = self.graph.nodes().resolve_pos(dst); - if let Some(e_id) = self.node_writers.get_mut_src().get_out_edge(src_pos, dst, layer) { + if let Some(e_id) = self + .node_writers + .get_mut_src() + .get_out_edge(src_pos, dst, layer) + { let mut edge_writer = self.graph.edge_writer(e_id); let (_, edge_pos) = self.graph.edges().resolve_pos(e_id); edge_writer.add_edge(t, Some(edge_pos), src, dst, props, layer, lsn, None); @@ -181,7 +189,11 @@ impl< MaybeNew::Existing(e_id) } else { - if let Some(e_id) = self.node_writers.get_mut_src().get_out_edge(src_pos, dst, layer) { + if let Some(e_id) = self + .node_writers + .get_mut_src() + .get_out_edge(src_pos, dst, layer) + { let mut edge_writer = self.graph.edge_writer(e_id); let (_, edge_pos) = self.graph.edges().resolve_pos(e_id); let e_id = e_id.with_layer(layer); diff --git a/db4-storage/src/pages/test_utils.rs b/db4-storage/src/pages/test_utils.rs index e17f91fe14..54e315a1f8 100644 --- a/db4-storage/src/pages/test_utils.rs +++ b/db4-storage/src/pages/test_utils.rs @@ -11,7 +11,10 @@ use raphtory_api::core::entities::properties::{ prop::{DECIMAL_MAX, Prop, PropType}, tprop::TPropOps, }; -use raphtory_core::{entities::{ELID, VID}, storage::timeindex::TimeIndexOps}; +use raphtory_core::{ + entities::{ELID, VID}, + storage::timeindex::TimeIndexOps, +}; use rayon::prelude::*; use crate::{ diff --git a/db4-storage/src/segments/edge.rs b/db4-storage/src/segments/edge.rs index 13396cfff7..c403fb9acd 100644 --- a/db4-storage/src/segments/edge.rs +++ b/db4-storage/src/segments/edge.rs @@ -160,18 +160,20 @@ impl MemEdgeSegment { // Ensure we have enough layers self.ensure_layer(layer_id); - let row = self.layers[layer_id].reserve_local_row(edge_pos).map_either( - |row| { - row.src = src; - row.dst = dst; - row.row() - }, - |row| { - row.src = src; - row.dst = dst; - row.row() - }, - ); + let row = self.layers[layer_id] + .reserve_local_row(edge_pos) + .map_either( + |row| { + row.src = src; + row.dst = dst; + row.row() + }, + |row| { + row.src = src; + row.dst = dst; + row.row() + }, + ); row.either(|a| a, |a| a) } @@ -229,6 +231,7 @@ impl MemEdgeSegment { } // Update EdgeSegmentView implementation to use multiple layers +#[derive(Debug)] pub struct EdgeSegmentView { segment: Arc>, segment_id: usize, diff --git a/db4-storage/src/segments/edge_entry.rs b/db4-storage/src/segments/edge_entry.rs index b915257ba4..da0b331a04 100644 --- a/db4-storage/src/segments/edge_entry.rs +++ b/db4-storage/src/segments/edge_entry.rs @@ -57,8 +57,7 @@ impl<'a> EdgeRefOps<'a> for MemEdgeRef<'a> { type TProps = TPropCell<'a>; fn edge(self, layer_id: usize) -> Option<(VID, VID)> { - self.es - .as_ref()[layer_id] + self.es.as_ref()[layer_id] .get(&self.pos) .map(|entry| (entry.src, entry.dst)) } @@ -71,9 +70,8 @@ impl<'a> EdgeRefOps<'a> for MemEdgeRef<'a> { self.es.as_ref()[layer_id].c_prop(self.pos, prop_id) } - fn t_prop(self, layer_id:usize, prop_id: usize) -> Self::TProps { - self.es - .as_ref()[layer_id] + fn t_prop(self, layer_id: usize, prop_id: usize) -> Self::TProps { + self.es.as_ref()[layer_id] .t_prop(self.pos, prop_id) .unwrap_or_default() } diff --git a/db4-storage/src/segments/mod.rs b/db4-storage/src/segments/mod.rs index 2b916a077e..26a3894227 100644 --- a/db4-storage/src/segments/mod.rs +++ b/db4-storage/src/segments/mod.rs @@ -204,11 +204,7 @@ impl SegmentContainer { .collect::>() } - pub fn t_prop( - &self, - item_id: impl Into, - prop_id: usize, - ) -> Option> { + pub fn t_prop(&self, item_id: impl Into, prop_id: usize) -> Option> { let item_id = item_id.into(); self.data.get(&item_id).and_then(|entry| { let prop_entry = self.properties.get_entry(entry.row()); diff --git a/raphtory-storage/src/graph/graph.rs b/raphtory-storage/src/graph/graph.rs index 8545a51163..3ffe9d6f4f 100644 --- a/raphtory-storage/src/graph/graph.rs +++ b/raphtory-storage/src/graph/graph.rs @@ -193,9 +193,7 @@ impl GraphStorage { pub fn core_node<'a>(&'a self, vid: VID) -> NodeStorageEntry<'a> { match self { GraphStorage::Mem(storage) => NodeStorageEntry::Mem(storage.node(vid)), - GraphStorage::Unlocked(storage) => { - NodeStorageEntry::Unlocked(storage.node(vid)) - } + GraphStorage::Unlocked(storage) => NodeStorageEntry::Unlocked(storage.node(vid)), #[cfg(feature = "storage")] GraphStorage::Disk(storage) => { NodeStorageEntry::Disk(DiskNode::new(&storage.inner, vid)) diff --git a/raphtory-storage/src/graph/locked.rs b/raphtory-storage/src/graph/locked.rs index cee11ff9d0..86409e97b5 100644 --- a/raphtory-storage/src/graph/locked.rs +++ b/raphtory-storage/src/graph/locked.rs @@ -9,8 +9,8 @@ use raphtory_core::{ ReadLockedStorage, WriteLockedNodes, }, }; -use storage::ReadLockedLayer; use std::sync::Arc; +use storage::ReadLockedLayer; #[derive(Debug)] pub struct LockedGraph { diff --git a/raphtory-storage/src/graph/nodes/node_entry.rs b/raphtory-storage/src/graph/nodes/node_entry.rs index 35ec9f73ed..f4ce934888 100644 --- a/raphtory-storage/src/graph/nodes/node_entry.rs +++ b/raphtory-storage/src/graph/nodes/node_entry.rs @@ -2,7 +2,10 @@ use crate::graph::{ nodes::{node_ref::NodeStorageRef, node_storage_ops::NodeStorageOps}, variants::storage_variants3::StorageVariants3, }; -use db4_graph::{entries::node::{LockedNodeEntry, UnlockedNodeEntry}, ReadLockedTemporalGraph, TemporalGraph}; +use db4_graph::{ + entries::node::{LockedNodeEntry, UnlockedNodeEntry}, + ReadLockedTemporalGraph, TemporalGraph, +}; use raphtory_api::{ core::{ entities::{ diff --git a/raphtory-storage/src/graph/nodes/node_ref.rs b/raphtory-storage/src/graph/nodes/node_ref.rs index ad95760cbb..2e1b7b134f 100644 --- a/raphtory-storage/src/graph/nodes/node_ref.rs +++ b/raphtory-storage/src/graph/nodes/node_ref.rs @@ -17,8 +17,8 @@ use raphtory_api::{ iter::IntoDynBoxed, }; use raphtory_core::storage::node_entry::NodePtr; -use storage::Extension; use std::{borrow::Cow, ops::Range}; +use storage::Extension; #[cfg(feature = "storage")] use crate::disk::storage_interface::node::DiskNode; diff --git a/raphtory-storage/src/mutation/addition_ops_ext.rs b/raphtory-storage/src/mutation/addition_ops_ext.rs index 60ac135f71..ed4d356e86 100644 --- a/raphtory-storage/src/mutation/addition_ops_ext.rs +++ b/raphtory-storage/src/mutation/addition_ops_ext.rs @@ -31,9 +31,9 @@ pub struct WriteS< MNS: DerefMut, MES: DerefMut, EXT: PersistentStrategy, ES = ES>, ->{ +> { static_session: WriteSession<'a, MNS, MES, NS, ES, EXT>, - layer: Option, ES, EXT>> + layer: Option, ES, EXT>>, } pub struct UnlockedSession<'a, EXT> { @@ -58,12 +58,13 @@ impl< ) -> MaybeNew { let src = src.into(); let dst = dst.into(); - let eid = self.static_session.add_static_edge(src, dst, lsn).map(|eid| eid.with_layer(0)); - self.layer - .as_mut() - .map(|layer| { - layer.add_edge_into_layer(t, src, dst, eid, lsn, props); - }); + let eid = self + .static_session + .add_static_edge(src, dst, lsn) + .map(|eid| eid.with_layer(0)); + self.layer.as_mut().map(|layer| { + layer.add_edge_into_layer(t, src, dst, eid, lsn, props); + }); eid } } @@ -190,7 +191,8 @@ impl, ES = ES>> InternalAdditionOps self.graph_dir().join(format!("l_{}", layer_name)), self.max_page_len_nodes(), self.max_page_len_edges(), - ).into() + ) + .into() }); if new_layer_id >= layer_id { while self.layers().get(new_layer_id).is_none() { From d3043726d547dc3982ac0322f866c0f3952f3216 Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Tue, 17 Jun 2025 18:12:26 +0100 Subject: [PATCH 015/321] hide some add_edges functions --- db4-storage/src/pages/mod.rs | 4 ++-- db4-storage/src/pages/test_utils.rs | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/db4-storage/src/pages/mod.rs b/db4-storage/src/pages/mod.rs index d6bd6c848f..998921d5e5 100644 --- a/db4-storage/src/pages/mod.rs +++ b/db4-storage/src/pages/mod.rs @@ -222,7 +222,7 @@ impl, ES: EdgeSegmentOps, E self.internal_add_edge(t, src, dst, 0, []) } - pub fn add_edge_props, T: TryIntoInputTime>( + pub(crate) fn add_edge_props, T: TryIntoInputTime>( &self, t: T, src: impl Into, @@ -235,7 +235,7 @@ impl, ES: EdgeSegmentOps, E self.internal_add_edge(t, src, dst, 0, prop_writer.into_props_temporal()?) } - pub fn internal_add_edge( + fn internal_add_edge( &self, t: TimeIndexEntry, src: impl Into, diff --git a/db4-storage/src/pages/test_utils.rs b/db4-storage/src/pages/test_utils.rs index 54e315a1f8..f537a97d9d 100644 --- a/db4-storage/src/pages/test_utils.rs +++ b/db4-storage/src/pages/test_utils.rs @@ -46,6 +46,9 @@ pub fn check_edges_support< nodes.insert(*dst); } + // let eid = session.add_static_edge(src, dst, lsn); + // session.add_edge_into_layer(t, src, dst, eid.inner().with_layer(layer), lsn, props); + if par_load { edges .par_iter() From 74c4d2fbec5a9332cdd3fb6a2c7327fad37764c9 Mon Sep 17 00:00:00 2001 From: Fadhil Abubaker Date: Wed, 18 Jun 2025 07:55:59 -0400 Subject: [PATCH 016/321] Add tests for inserting edges to layers (#2130) * Split test_utils into its own module * Add layer_id as an arg to check_edges_support * Implement test_storage_with_layers * Add todo * add layer counters --------- Co-authored-by: Fabian Murariu --- db4-storage/src/lib.rs | 10 +- db4-storage/src/pages/edge_page/writer.rs | 2 - db4-storage/src/pages/layer_counter.rs | 73 ++ db4-storage/src/pages/locked/nodes.rs | 12 +- db4-storage/src/pages/mod.rs | 76 +- db4-storage/src/pages/node_page/writer.rs | 26 +- db4-storage/src/pages/node_store.rs | 54 +- db4-storage/src/pages/session.rs | 110 ++- db4-storage/src/pages/test_utils.rs | 872 ------------------- db4-storage/src/pages/test_utils/checkers.rs | 495 +++++++++++ db4-storage/src/pages/test_utils/fixtures.rs | 136 +++ db4-storage/src/pages/test_utils/mod.rs | 7 + db4-storage/src/pages/test_utils/props.rs | 139 +++ db4-storage/src/segments/edge.rs | 12 + db4-storage/src/segments/node.rs | 52 +- 15 files changed, 1035 insertions(+), 1041 deletions(-) create mode 100644 db4-storage/src/pages/layer_counter.rs delete mode 100644 db4-storage/src/pages/test_utils.rs create mode 100644 db4-storage/src/pages/test_utils/checkers.rs create mode 100644 db4-storage/src/pages/test_utils/fixtures.rs create mode 100644 db4-storage/src/pages/test_utils/mod.rs create mode 100644 db4-storage/src/pages/test_utils/props.rs diff --git a/db4-storage/src/lib.rs b/db4-storage/src/lib.rs index 470af4e52b..8c824abb64 100644 --- a/db4-storage/src/lib.rs +++ b/db4-storage/src/lib.rs @@ -137,7 +137,7 @@ pub trait EdgeRefOps<'a>: Copy + Clone + Send + Sync { fn t_prop(self, layer_id: usize, prop_id: usize) -> Self::TProps; } -pub trait NodeSegmentOps: Send + Sync { +pub trait NodeSegmentOps: Send + Sync + std::fmt::Debug { type Extension; type Entry<'a>: NodeEntryOps<'a> @@ -173,9 +173,13 @@ pub trait NodeSegmentOps: Send + Sync { fn head_mut(&self) -> RwLockWriteGuard; - fn num_nodes(&self) -> usize; + fn num_nodes(&self) -> usize { + self.layer_num_nodes(0) + } + + fn num_layers(&self) -> usize; - fn increment_num_nodes(&self) -> usize; + fn layer_num_nodes(&self, layer_id: usize) -> usize; fn notify_write( &self, diff --git a/db4-storage/src/pages/edge_page/writer.rs b/db4-storage/src/pages/edge_page/writer.rs index 88590896cc..2747f9ff32 100644 --- a/db4-storage/src/pages/edge_page/writer.rs +++ b/db4-storage/src/pages/edge_page/writer.rs @@ -66,8 +66,6 @@ impl<'a, MP: DerefMut, ES: EdgeSegmentOps> EdgeWriter<' let edge_pos = edge_pos.unwrap_or_else(|| self.new_local_pos()); self.writer .insert_static_edge_internal(edge_pos, src, dst, layer_id); - // self.est_size = self.page.increment_size(size_of::<(VID, VID)>()) - // + self.writer.as_ref().t_prop_est_size(); edge_pos } diff --git a/db4-storage/src/pages/layer_counter.rs b/db4-storage/src/pages/layer_counter.rs new file mode 100644 index 0000000000..50439b8866 --- /dev/null +++ b/db4-storage/src/pages/layer_counter.rs @@ -0,0 +1,73 @@ +use std::sync::atomic::AtomicUsize; + +#[derive(Debug)] +pub struct LayerCounter { + layers: boxcar::Vec, +} + +impl > From for LayerCounter { + fn from(iter: I) -> Self { + let counts = iter.into_iter().map(|c| AtomicUsize::new(c)).collect(); + let layers = boxcar::Vec::from(counts); + Self { layers } + } +} + +impl LayerCounter { + pub fn new() -> Self { + let layers = boxcar::Vec::new(); + for _ in 0..16 { + let id = layers.push_with(|_| AtomicUsize::new(0)); + while layers.get(id).is_none() { + // wait for the layer to be created + std::thread::yield_now(); + } + } + Self { layers } + } + + pub fn increment(&self, layer_id: usize) -> usize { + let counter = self.get_or_create_layer(layer_id); + counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + } + + pub fn get(&self, layer_id: usize) -> usize { + let counter = self.get_or_create_layer(layer_id); + counter.load(std::sync::atomic::Ordering::Relaxed) + } + + fn get_or_create_layer(&self, layer_id: usize) -> &AtomicUsize { + if let Some(counter) = self.layers.get(layer_id) { + return counter; + } + + if self.layers.count() >= layer_id + 1 { + // something has allocated the layer, wait for it to be added + loop { + if let Some(counter) = self.layers.get(layer_id) { + return counter; + } else { + // wait for the layer to be created + std::thread::yield_now(); + } + } + } else { + // we need to create the layer + self.layers.reserve(layer_id + 1 - self.layers.count()); + + loop { + let new_layer_id = self.layers.push_with(|_| AtomicUsize::new(0)); + if new_layer_id >= layer_id { + loop { + if let Some(counter) = self.layers.get(layer_id) { + return counter; + } else { + // wait for the layer to be created + std::thread::yield_now(); + } + } + } + } + } + } +} diff --git a/db4-storage/src/pages/locked/nodes.rs b/db4-storage/src/pages/locked/nodes.rs index 8dcf0a6f45..05bd9c16a7 100644 --- a/db4-storage/src/pages/locked/nodes.rs +++ b/db4-storage/src/pages/locked/nodes.rs @@ -1,17 +1,17 @@ use crate::{ LocalPOS, NodeSegmentOps, - pages::{node_page::writer::NodeWriter, resolve_pos}, + pages::{layer_counter::LayerCounter, node_page::writer::NodeWriter, resolve_pos}, segments::node::MemNodeSegment, }; use parking_lot::RwLockWriteGuard; use raphtory_core::entities::VID; use rayon::prelude::*; -use std::{ops::DerefMut, sync::atomic::AtomicUsize}; +use std::ops::DerefMut; pub struct LockedNodePage<'a, NS> { page_id: usize, max_page_len: usize, - num_nodes: &'a AtomicUsize, + layer_counter: &'a LayerCounter, page: &'a NS, lock: RwLockWriteGuard<'a, MemNodeSegment>, } @@ -19,14 +19,14 @@ pub struct LockedNodePage<'a, NS> { impl<'a, EXT, NS: NodeSegmentOps> LockedNodePage<'a, NS> { pub fn new( page_id: usize, - num_nodes: &'a AtomicUsize, + layer_counter: &'a LayerCounter, max_page_len: usize, page: &'a NS, lock: RwLockWriteGuard<'a, MemNodeSegment>, ) -> Self { Self { page_id, - num_nodes, + layer_counter, max_page_len, page, lock, @@ -35,7 +35,7 @@ impl<'a, EXT, NS: NodeSegmentOps> LockedNodePage<'a, NS> { #[inline(always)] pub fn writer(&mut self) -> NodeWriter<'_, &mut MemNodeSegment, NS> { - NodeWriter::new(self.page, self.num_nodes, self.lock.deref_mut()) + NodeWriter::new(self.page, self.layer_counter, self.lock.deref_mut()) } #[inline(always)] diff --git a/db4-storage/src/pages/mod.rs b/db4-storage/src/pages/mod.rs index 998921d5e5..a14f785c3d 100644 --- a/db4-storage/src/pages/mod.rs +++ b/db4-storage/src/pages/mod.rs @@ -33,6 +33,7 @@ use session::WriteSession; pub mod edge_page; pub mod edge_store; +pub mod layer_counter; pub mod locked; pub mod node_page; pub mod node_store; @@ -136,42 +137,6 @@ impl, ES: EdgeSegmentOps, E }) } - pub fn layer( - graph_dir: impl AsRef, - max_page_len_nodes: usize, - max_page_len_edges: usize, - node_meta: Arc, - edge_meta: Arc, - ) -> Self { - let nodes_path = graph_dir.as_ref().join("nodes"); - let edges_path = graph_dir.as_ref().join("edges"); - let ext = EXT::default(); - - let nodes = Arc::new(NodeStorageInner::layer( - nodes_path, - max_page_len_nodes, - &node_meta, - ext.clone(), - )); - let edges = Arc::new(EdgeStorageInner::layer( - edges_path, - max_page_len_edges, - &edge_meta, - ext.clone(), - )); - - Self { - nodes: nodes.clone(), - edges: edges.clone(), - edge_meta, - node_meta, - earliest: AtomicI64::new(0), - latest: AtomicI64::new(0), - event_id: AtomicUsize::new(0), - _ext: ext, - } - } - pub fn new( graph_dir: impl AsRef, max_page_len_nodes: usize, @@ -518,7 +483,8 @@ mod test { Layer, NodeEntryOps, NodeRefOps, pages::test_utils::{ AddEdge, Fixture, NodeFixture, check_edges_support, check_graph_with_nodes_support, - check_graph_with_props_support, edges_strat, make_edges, make_nodes, + check_graph_with_props_support, edges_strat, edges_strat_with_layers, make_edges, + make_nodes, }, }; use chrono::{DateTime, NaiveDateTime, Utc}; @@ -531,6 +497,23 @@ mod test { edges: Vec<(impl Into, impl Into)>, chunk_size: usize, par_load: bool, + ) { + // Set optional layer_id to None + let layer_id = None; + let edges = edges + .into_iter() + .map(|(src, dst)| (src, dst, layer_id)) + .collect(); + + check_edges_support(edges, par_load, false, |graph_dir| { + Layer::new(graph_dir, chunk_size, chunk_size) + }) + } + + fn check_edges_with_layers( + edges: Vec<(impl Into, impl Into, Option)>, // src, dst, layer_id + chunk_size: usize, + par_load: bool, ) { check_edges_support(edges, par_load, false, |graph_dir| { Layer::new(graph_dir, chunk_size, chunk_size) @@ -587,6 +570,21 @@ mod test { check_edges(edges, 2, false); } + #[test] + fn test_storage_with_layers() { + let edges_strat = edges_strat_with_layers(10); + + proptest!(|(edges in edges_strat, chunk_size in 1usize .. 100)|{ + check_edges_with_layers(edges, chunk_size, false); + }); + } + + #[test] + fn test_storage_with_layers_1() { + let edges = vec![(VID(4), VID(0), Some(1)), (VID(0), VID(0), Some(6))]; + check_edges_with_layers(edges, 4, false); + } + #[test] fn test_add_one_edge_get_num_nodes() { let graph_dir = tempfile::tempdir().unwrap(); @@ -611,10 +609,6 @@ mod test { }; check(&g); - // drop(g); - // - // let g = GraphStore::::load(graph_dir.path()).unwrap(); - // check(&g); } #[test] diff --git a/db4-storage/src/pages/node_page/writer.rs b/db4-storage/src/pages/node_page/writer.rs index 3a5df5539a..6bf30766fe 100644 --- a/db4-storage/src/pages/node_page/writer.rs +++ b/db4-storage/src/pages/node_page/writer.rs @@ -1,21 +1,23 @@ -use crate::{LocalPOS, NodeSegmentOps, segments::node::MemNodeSegment}; +use crate::{ + LocalPOS, NodeSegmentOps, pages::layer_counter::LayerCounter, segments::node::MemNodeSegment, +}; use raphtory_api::core::entities::{EID, VID, properties::prop::Prop}; use raphtory_core::{entities::ELID, storage::timeindex::AsTime}; -use std::{ops::DerefMut, sync::atomic::AtomicUsize}; +use std::ops::DerefMut; #[derive(Debug)] pub struct NodeWriter<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> { pub page: &'a NS, pub writer: MP, // TODO: rename to m_segment - pub global_num_nodes: &'a AtomicUsize, + pub l_counter: &'a LayerCounter, } impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> NodeWriter<'a, MP, NS> { - pub fn new(page: &'a NS, global_num_nodes: &'a AtomicUsize, writer: MP) -> Self { + pub fn new(page: &'a NS, global_num_nodes: &'a LayerCounter, writer: MP) -> Self { Self { page, writer, - global_num_nodes, + l_counter: global_num_nodes, } } @@ -52,13 +54,10 @@ impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> NodeWri let e_id = e_id.into(); let layer_id = e_id.layer(); - self.writer.as_mut()[layer_id].set_lsn(lsn); - let is_new_node = self.writer.add_outbound_edge(t, src_pos, dst, e_id); + let is_new_node = self.writer.add_outbound_edge(t, src_pos, dst, e_id, lsn); if is_new_node && !self.page.check_node(src_pos, layer_id) { - self.page.increment_num_nodes(); - self.global_num_nodes - .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + self.l_counter.increment(layer_id); } } @@ -93,14 +92,11 @@ impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> NodeWri ) { let e_id = e_id.into(); let layer = e_id.layer(); - self.writer.as_mut()[layer].set_lsn(lsn); let dst_pos = dst_pos.into(); - let is_new_node = self.writer.add_inbound_edge(t, dst_pos, src, e_id); + let is_new_node = self.writer.add_inbound_edge(t, dst_pos, src, e_id, lsn); if is_new_node && !self.page.check_node(dst_pos, layer) { - self.page.increment_num_nodes(); - self.global_num_nodes - .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + self.l_counter.increment(layer); } } diff --git a/db4-storage/src/pages/node_store.rs b/db4-storage/src/pages/node_store.rs index d0cbbd9219..394a6a59a3 100644 --- a/db4-storage/src/pages/node_store.rs +++ b/db4-storage/src/pages/node_store.rs @@ -1,6 +1,6 @@ use super::{node_page::writer::NodeWriter, resolve_pos}; use crate::{ - LocalPOS, NodeSegmentOps, ReadLockedNS, error::DBV4Error, segments::node::MemNodeSegment, + error::DBV4Error, pages::layer_counter::LayerCounter, segments::node::MemNodeSegment, LocalPOS, NodeSegmentOps, ReadLockedNS }; use parking_lot::RwLockWriteGuard; use raphtory_api::core::entities::properties::meta::Meta; @@ -14,10 +14,12 @@ use std::{ }, }; +// graph // (nodes|edges) // graph segments // layers // chunks + #[derive(Debug)] pub struct NodeStorageInner { pages: boxcar::Vec>, - num_nodes: AtomicUsize, + layer_counter: LayerCounter, nodes_path: PathBuf, max_page_len: usize, prop_meta: Arc, @@ -43,25 +45,10 @@ impl, EXT: Clone> NodeStorageInner } } - pub fn layer( - nodes_path: impl AsRef, - max_page_len: usize, - meta: &Arc, - ext: EXT, - ) -> Self { - Self { - pages: boxcar::Vec::new(), - num_nodes: AtomicUsize::new(0), - nodes_path: nodes_path.as_ref().to_path_buf(), - max_page_len, - prop_meta: meta.clone(), - ext, - } - } pub fn new(nodes_path: impl AsRef, max_page_len: usize, ext: EXT) -> Self { Self { pages: boxcar::Vec::new(), - num_nodes: AtomicUsize::new(0), + layer_counter: LayerCounter::new(), nodes_path: nodes_path.as_ref().to_path_buf(), max_page_len, prop_meta: Arc::new(Meta::new()), @@ -106,11 +93,15 @@ impl, EXT: Clone> NodeStorageInner ) -> NodeWriter<'a, RwLockWriteGuard<'a, MemNodeSegment>, NS> { let segment = self.get_or_create_segment(segment_id); let head = segment.head_mut(); - NodeWriter::new(segment, &self.num_nodes, head) + NodeWriter::new(segment, &self.layer_counter, head) } pub fn num_nodes(&self) -> usize { - self.num_nodes.load(atomic::Ordering::Relaxed) + self.layer_counter.get(0) + } + + pub fn layer_num_nodes(&self, layer_id: usize) -> usize { + self.layer_counter.get(layer_id) } pub fn pages(&self) -> &boxcar::Vec> { @@ -121,12 +112,6 @@ impl, EXT: Clone> NodeStorageInner &self.nodes_path } - // pub fn iter<'a>(&'a self) -> impl Iterator> + 'a { - // self.pages() - // .iter() - // .flat_map(|(_, node_segment)| node_segment.iter(self.max_page_len)) - // } - pub fn load( nodes_path: impl AsRef, max_page_len: usize, @@ -180,16 +165,23 @@ impl, EXT: Clone> NodeStorageInner ))); } - let num_nodes = pages - .iter() - .map(|(_, page)| page.num_nodes()) - .sum::(); + let mut layer_counts = vec![]; + + for (_, page) in pages.iter() { + for layer_id in 0..page.num_layers() { + let count = page.layer_num_nodes(layer_id); + if layer_counts.len() <= layer_id { + layer_counts.resize(layer_id + 1, 0); + } + layer_counts[layer_id] += count; + } + } Ok(Self { pages, nodes_path: nodes_path.to_path_buf(), max_page_len, - num_nodes: AtomicUsize::new(num_nodes), + layer_counter: LayerCounter::from(layer_counts), prop_meta: meta, ext, }) diff --git a/db4-storage/src/pages/session.rs b/db4-storage/src/pages/session.rs index 9b14a294bf..76190ba6bf 100644 --- a/db4-storage/src/pages/session.rs +++ b/db4-storage/src/pages/session.rs @@ -60,39 +60,48 @@ impl< let dst = dst.into(); let e_id = edge.inner(); let layer = e_id.layer(); + assert!(layer > 0, "Edge must be in a layer greater than 0"); - let edge_writer = self - .edge_writer - .as_mut() - .expect("Internal Error: Edge writer is not set"); - - let node_max_page_len = - self.node_writers.get_mut_src().writer.as_ref()[layer].max_page_len(); - let edge_max_page_len = edge_writer.writer.as_ref()[layer].max_page_len(); + let (_, src_pos) = self.graph.nodes().resolve_pos(src); + let (_, dst_pos) = self.graph.nodes().resolve_pos(dst); - let (_, src_pos) = resolve_pos(src, node_max_page_len); - let (_, dst_pos) = resolve_pos(dst, node_max_page_len); - let (_, edge_pos) = resolve_pos(e_id.edge, edge_max_page_len); + if let Some(writer) = self.edge_writer.as_mut() { + let edge_max_page_len = writer.writer.get_or_create_layer(layer).max_page_len(); + let (_, edge_pos) = resolve_pos(e_id.edge, edge_max_page_len); + let exists = Some(!edge.is_new()); + writer.add_edge(t, Some(edge_pos), src, dst, props, layer, lsn, exists); + } else { + let mut writer = self.graph.edge_writer(e_id.edge); + let edge_max_page_len = writer.writer.get_or_create_layer(layer).max_page_len(); + let (_, edge_pos) = resolve_pos(e_id.edge, edge_max_page_len); + let exists = Some(!edge.is_new()); + writer.add_edge(t, Some(edge_pos), src, dst, props, layer, lsn, exists); + } - let exists = Some(!edge.is_new()); - edge_writer.add_edge(t, Some(edge_pos), src, dst, props, layer, lsn, exists); + let edge_id = edge.inner(); + if edge_id.layer() > 0 { + if edge.is_new() + || self + .node_writers + .get_mut_src() + .get_out_edge(src_pos, dst, edge_id.layer()) + .is_none() + { + self.node_writers + .get_mut_src() + .add_outbound_edge(t, src_pos, dst, edge_id, lsn); + self.node_writers + .get_mut_dst() + .add_inbound_edge(t, dst_pos, src, edge_id, lsn); + } - edge.if_new(|edge_id| { self.node_writers .get_mut_src() - .add_outbound_edge(t, src_pos, dst, edge_id, lsn); + .update_timestamp(t, src_pos, e_id, lsn); self.node_writers .get_mut_dst() - .add_inbound_edge(t, dst_pos, src, edge_id, lsn); - edge_id - }); - - self.node_writers - .get_mut_src() - .update_timestamp(t, src_pos, e_id, lsn); - self.node_writers - .get_mut_dst() - .update_timestamp(t, dst_pos, e_id, lsn); + .update_timestamp(t, dst_pos, e_id, lsn); + } } pub fn add_static_edge( @@ -120,38 +129,25 @@ impl< MaybeNew::Existing(e_id) } else { - if let Some(e_id) = self - .node_writers - .get_mut_src() - .get_out_edge(src_pos, dst, layer_id) - { - let mut edge_writer = self.graph.edge_writer(e_id); - let (_, edge_pos) = self.graph.edges().resolve_pos(e_id); - - edge_writer.add_static_edge(Some(edge_pos), src, dst, layer_id, lsn, None); - - MaybeNew::Existing(e_id) - } else { - let mut edge_writer = self.graph.get_free_writer(); - let edge_id = edge_writer.add_static_edge(None, src, dst, layer_id, lsn, None); - let edge_id = - edge_id.as_eid(edge_writer.segment_id(), self.graph.edges().max_page_len()); - - self.node_writers.get_mut_src().add_static_outbound_edge( - src_pos, - dst, - edge_id.with_layer(layer_id), - lsn, - ); - self.node_writers.get_mut_dst().add_static_inbound_edge( - dst_pos, - src, - edge_id.with_layer(layer_id), - lsn, - ); - - MaybeNew::New(edge_id) - } + let mut edge_writer = self.graph.get_free_writer(); + let edge_id = edge_writer.add_static_edge(None, src, dst, layer_id, lsn, None); + let edge_id = + edge_id.as_eid(edge_writer.segment_id(), self.graph.edges().max_page_len()); + + self.node_writers.get_mut_src().add_static_outbound_edge( + src_pos, + dst, + edge_id.with_layer(layer_id), + lsn, + ); + self.node_writers.get_mut_dst().add_static_inbound_edge( + dst_pos, + src, + edge_id.with_layer(layer_id), + lsn, + ); + + MaybeNew::New(edge_id) } } diff --git a/db4-storage/src/pages/test_utils.rs b/db4-storage/src/pages/test_utils.rs deleted file mode 100644 index f537a97d9d..0000000000 --- a/db4-storage/src/pages/test_utils.rs +++ /dev/null @@ -1,872 +0,0 @@ -use std::{ - collections::{HashMap, HashSet}, - path::Path, -}; - -use bigdecimal::BigDecimal; -use chrono::{DateTime, NaiveDateTime, Utc}; -use itertools::Itertools; -use proptest::{collection, prelude::*}; -use raphtory_api::core::entities::properties::{ - prop::{DECIMAL_MAX, Prop, PropType}, - tprop::TPropOps, -}; -use raphtory_core::{ - entities::{ELID, VID}, - storage::timeindex::TimeIndexOps, -}; -use rayon::prelude::*; - -use crate::{ - EdgeEntryOps, EdgeRefOps, EdgeSegmentOps, NodeEntryOps, NodeRefOps, NodeSegmentOps, - error::DBV4Error, pages::GraphStore, -}; - -pub fn check_edges_support< - NS: NodeSegmentOps, - ES: EdgeSegmentOps, - EXT: Clone + Default + Send + Sync, ->( - edges: Vec<(impl Into, impl Into)>, - par_load: bool, - check_load: bool, - make_graph: impl FnOnce(&Path) -> GraphStore, -) { - let mut edges = edges - .into_iter() - .map(|(src, dst)| (src.into(), dst.into())) - .collect::>(); - - let graph_dir = tempfile::tempdir().unwrap(); - let graph = make_graph(graph_dir.path()); - let mut nodes = HashSet::new(); - - for (src, dst) in &edges { - nodes.insert(*src); - nodes.insert(*dst); - } - - // let eid = session.add_static_edge(src, dst, lsn); - // session.add_edge_into_layer(t, src, dst, eid.inner().with_layer(layer), lsn, props); - - if par_load { - edges - .par_iter() - .try_for_each(|(src, dst)| { - let _ = graph.add_edge(0, *src, *dst)?; - Ok::<_, DBV4Error>(()) - }) - .expect("Failed to add edge"); - } else { - edges - .iter() - .try_for_each(|(src, dst)| { - let _ = graph.add_edge(0, *src, *dst)?; - Ok::<_, DBV4Error>(()) - }) - .expect("Failed to add edge"); - } - - let actual_num_nodes = graph.nodes().num_nodes(); - assert_eq!(actual_num_nodes, nodes.len()); - - edges.sort_unstable(); - - fn check< - NS: NodeSegmentOps, - ES: EdgeSegmentOps, - EXT: Clone + Default, - >( - stage: &str, - es: &[(VID, VID)], - graph: &GraphStore, - ) { - let nodes = graph.nodes(); - let edges = graph.edges(); - let layer_id = 0; - - if !es.is_empty() { - assert!(nodes.pages().count() > 0, "{stage}"); - } - - let mut expected_graph: HashMap, Vec)> = es - .iter() - .chunk_by(|(src, _)| *src) - .into_iter() - .map(|(src, edges)| { - let mut out: Vec<_> = edges.map(|(_, dst)| *dst).collect(); - out.sort_unstable(); - out.dedup(); - (src, (out, vec![])) - }) - .collect::>(); - - let mut edges_sorted_by_dest = es.to_vec(); - edges_sorted_by_dest.sort_unstable_by_key(|(_, dst)| *dst); - - // now inbounds - edges_sorted_by_dest - .iter() - .chunk_by(|(_, dst)| *dst) - .into_iter() - .for_each(|(dst, edges)| { - let mut edges: Vec<_> = edges.map(|(src, _)| *src).collect(); - edges.sort_unstable(); - edges.dedup(); - let (_, inb) = expected_graph.entry(dst).or_default(); - *inb = edges; - }); - - for (n, (exp_out, exp_inb)) in expected_graph { - let entry = nodes.node(n); - - let adj = entry.as_ref(); - let out_nbrs: Vec<_> = adj.out_nbrs_sorted(layer_id).collect(); - assert_eq!(out_nbrs, exp_out, "{stage} node: {:?}", n); - - let in_nbrs: Vec<_> = adj.inb_nbrs_sorted(layer_id).collect(); - assert_eq!(in_nbrs, exp_inb, "{stage} node: {:?}", n); - - for (exp_dst, eid) in adj.out_edges(0) { - let elid = ELID::new(eid, layer_id); - let (src, dst) = edges.get_edge(elid).unwrap(); - - assert_eq!(src, n, "{stage}"); - assert_eq!(dst, exp_dst, "{stage}"); - } - - for (exp_src, eid) in adj.inb_edges(0) { - let elid = ELID::new(eid, layer_id); - let (src, dst) = edges.get_edge(elid).unwrap(); - - assert_eq!(src, exp_src, "{stage}"); - assert_eq!(dst, n, "{stage}"); - } - } - } - - check("pre-drop", &edges, &graph); - if check_load { - drop(graph); - - let maybe_ns = GraphStore::::load(graph_dir.path()); - if edges.is_empty() { - assert!(maybe_ns.is_err()); - } else { - match maybe_ns { - Ok(graph) => { - check("post-drop", &edges, &graph); - } - Err(e) => { - panic!("Failed to load graph: {:?}", e); - } - } - } - } -} - -pub fn check_graph_with_nodes_support< - EXT: Clone + Default + Send + Sync, - NS: NodeSegmentOps, - ES: EdgeSegmentOps, ->( - fixture: &NodeFixture, - check_load: bool, - make_graph: impl FnOnce(&Path) -> GraphStore, -) { - let NodeFixture { - temp_props, - const_props, - } = fixture; - - let graph_dir = tempfile::tempdir().unwrap(); - let graph = make_graph(graph_dir.path()); - - for (node, t, t_props) in temp_props { - let err = graph.add_node_props(*t, *node, 0, t_props.clone()); - - assert!(err.is_ok(), "Failed to add node: {:?}", err); - } - - for (node, const_props) in const_props { - let err = graph.update_node_const_props(*node, 0, const_props.clone()); - - assert!(err.is_ok(), "Failed to add node: {:?}", err); - } - - let check_fn = |temp_props: &[(VID, i64, Vec<(String, Prop)>)], - const_props: &[(VID, Vec<(String, Prop)>)], - graph: &GraphStore| { - let mut ts_for_nodes = HashMap::new(); - for (node, t, _) in temp_props { - ts_for_nodes.entry(*node).or_insert_with(|| vec![]).push(*t); - } - ts_for_nodes.iter_mut().for_each(|(_, ts)| { - ts.sort_unstable(); - }); - - for (node, ts_expected) in ts_for_nodes { - let ne = graph.nodes().node(node); - let node_entry = ne.as_ref(); - let actual: Vec<_> = node_entry.additions(0).iter_t().collect(); - assert_eq!( - actual, ts_expected, - "Expected node additions for node ({node:?})", - ); - } - - let mut const_props_values = HashMap::new(); - for (node, const_props) in const_props { - let node = *node; - for (name, prop) in const_props { - const_props_values - .entry((node, name)) - .or_insert_with(|| HashSet::new()) - .insert(prop.clone()); - } - } - - for ((node, name), const_props) in const_props_values { - let ne = graph.nodes().node(node); - let node_entry = ne.as_ref(); - - let prop_id = graph - .node_meta() - .const_prop_meta() - .get_id(&name) - .unwrap_or_else(|| panic!("Failed to get prop id for {}", name)); - let actual_props = node_entry.c_prop(0, prop_id); - - if !const_props.is_empty() { - let actual_prop = actual_props - .unwrap_or_else(|| panic!("Failed to get prop {name} for {node:?}")); - assert!( - const_props.contains(&actual_prop), - "failed to get const prop {name} for {node:?}, expected {:?}, got {:?}", - const_props, - actual_prop - ); - } - } - - let mut nod_t_prop_groups = HashMap::new(); - for (node, t, t_props) in temp_props { - let node = *node; - let t = *t; - - for (prop_name, prop) in t_props { - let prop_values = nod_t_prop_groups - .entry((node, prop_name)) - .or_insert_with(|| vec![]); - prop_values.push((t, prop.clone())); - } - } - - nod_t_prop_groups.iter_mut().for_each(|(_, props)| { - props.sort_unstable_by_key(|(t, _)| *t); - }); - - for ((node, prop_name), props) in nod_t_prop_groups { - let prop_id = graph - .node_meta() - .temporal_prop_meta() - .get_id(&prop_name) - .unwrap_or_else(|| panic!("Failed to get prop id for {}", prop_name)); - - let ne = graph.nodes().node(node); - let node_entry = ne.as_ref(); - let actual_props = node_entry.t_prop(0, prop_id).iter_t().collect::>(); - - assert_eq!( - actual_props, props, - "Expected properties for node ({:?}) to be {:?}, but got {:?}", - node, props, actual_props - ); - } - }; - - check_fn(temp_props, const_props, &graph); - - if check_load { - drop(graph); - let graph = GraphStore::::load(graph_dir.path()).unwrap(); - check_fn(temp_props, const_props, &graph); - } -} - -pub fn check_graph_with_props_support< - EXT: Clone + Default + Send + Sync, - NS: NodeSegmentOps, - ES: EdgeSegmentOps, ->( - fixture: &Fixture, - check_load: bool, - make_graph: impl FnOnce(&Path) -> GraphStore, -) { - let Fixture { edges, const_props } = fixture; - let graph_dir = tempfile::tempdir().unwrap(); - let graph = make_graph(graph_dir.path()); - - // Add edges - for (src, dst, t, t_props, _, _) in edges { - let err = graph.add_edge_props(*t, *src, *dst, t_props.clone(), 0); - - assert!(err.is_ok(), "Failed to add edge: {:?}", err); - } - - // Add const props - for ((src, dst), const_props) in const_props { - let layer_id = 0; - let eid = graph - .nodes() - .get_edge(*src, *dst, layer_id) - .unwrap_or_else(|| panic!("Failed to get edge ({:?}, {:?}) from graph", src, dst)); - let elid = ELID::new(eid, layer_id); - let res = graph.update_edge_const_props(elid, const_props.clone()); - - assert!( - res.is_ok(), - "Failed to update edge const props: {:?} {src:?} -> {dst:?}", - res - ); - } - - assert!(graph.edges().num_edges() > 0); - - let check_fn = |edges: &[AddEdge], graph: &GraphStore| { - let mut edge_groups = HashMap::new(); - let mut node_groups: HashMap> = HashMap::new(); - - // Group temporal edge props and their timestamps - for (src, dst, t, t_props, _, _) in edges { - let src = *src; - let dst = *dst; - let t = *t; - - for (prop_name, prop) in t_props { - let prop_values = edge_groups - .entry((src, dst, prop_name)) - .or_insert_with(|| vec![]); - prop_values.push((t, prop.clone())); - } - } - - edge_groups.iter_mut().for_each(|(_, props)| { - props.sort_unstable_by_key(|(t, _)| *t); - }); - - // Group node additions and their timestamps - for (src, dst, t, _, _, _) in edges { - let src = *src; - let dst = *dst; - let t = *t; - - // Include src additions - node_groups.entry(src).or_insert_with(|| vec![]).push(t); - - // Self-edges don't have dst additions, so skip - if src == dst { - continue; - } - - // Include dst additions - node_groups.entry(dst).or_insert_with(|| vec![]).push(t); - } - - node_groups.iter_mut().for_each(|(_, ts)| { - ts.sort_unstable(); - }); - - for ((src, dst, prop_name), props) in edge_groups { - // Check temporal props - let prop_id = graph - .edge_meta() - .temporal_prop_meta() - .get_id(&prop_name) - .unwrap_or_else(|| panic!("Failed to get prop id for {}", prop_name)); - - let edge = graph - .nodes() - .get_edge(src, dst, 0) - .unwrap_or_else(|| panic!("Failed to get edge ({:?}, {:?}) from graph", src, dst)); - let edge = graph.edges().edge(edge); - let e = edge.as_ref(); - let layer_id = 0; - let actual_props = e.t_prop(layer_id, prop_id).iter_t().collect::>(); - - assert_eq!( - actual_props, props, - "Expected properties for edge ({:?}, {:?}) to be {:?}, but got {:?}", - src, dst, props, actual_props - ); - - // Check const props - if let Some(exp_const_props) = const_props.get(&(src, dst)) { - for (name, prop) in exp_const_props { - let prop_id = graph - .edge_meta() - .const_prop_meta() - .get_id(name) - .unwrap_or_else(|| panic!("Failed to get prop id for {}", name)); - let actual_props = e.c_prop(layer_id, prop_id); - assert_eq!( - actual_props.as_ref(), - Some(prop), - "Expected const properties for edge ({:?}, {:?}) to be {:?}, but got {:?}", - src, - dst, - prop, - actual_props - ); - } - } - } - - // Check node additions and their timestamps - for (node_id, ts) in node_groups { - let node = graph.nodes().node(node_id); - let node_entry = node.as_ref(); - let actual_additions_ts = node_entry.additions(0).iter_t().collect::>(); - - assert_eq!( - actual_additions_ts, ts, - "Expected node additions for node ({:?}) to be {:?}, but got {:?}", - node_id, ts, actual_additions_ts - ); - } - }; - - check_fn(edges, &graph); - - if check_load { - // Load the graph from disk and check again - drop(graph); - - let graph = GraphStore::::load(graph_dir.path()).unwrap(); - check_fn(edges, &graph); - } -} - -// pub fn check_load_support< -// EXT: Clone + Default + Send + Sync, -// NS: NodeSegmentOps, -// ES: EdgeSegmentOps, -// >( -// edges: &[(i64, u64, u64)], -// check_load: bool, -// make_graph: impl FnOnce(&Path) -> GraphStore, -// ) { -// // Create temporary directory and CSV file -// let temp_dir = tempfile::tempdir().unwrap(); -// let csv_path = temp_dir.path().join("edges.csv"); - -// // Write edges to CSV file -// let mut file = File::create(&csv_path).unwrap(); -// writeln!(file, "src,time,dst,test").unwrap(); -// for (time, src, dst) in edges { -// writeln!(file, "{},{},{},a", src, time, dst).unwrap(); -// } -// file.flush().unwrap(); - -// // Create graph store -// let graph_dir = temp_dir.path().join("graph"); -// std::fs::create_dir_all(&graph_dir).unwrap(); -// let graph = make_graph(&graph_dir); - -// // Create loader and load data -// let loader = Loader::new( -// &csv_path, -// Left("src"), -// Left("dst"), -// Left("time"), -// FileFormat::CSV { -// delimiter: b',', -// has_header: true, -// sample_records: 10, -// }, -// ) -// .unwrap(); - -// let resolver = loader.load_into(&graph, 1024).unwrap(); - -// fn check_graph< -// NS: NodeSegmentOps, -// ES: EdgeSegmentOps, -// EXT: Clone + Default + Send + Sync, -// >( -// edges: &[(i64, u64, u64)], -// graph: &GraphStore, -// resolver: &Mapping, -// label: &str, -// ) { -// // Create expected adjacency data -// let mut expected_out_edges: HashMap> = HashMap::new(); -// let mut expected_in_edges: HashMap> = HashMap::new(); -// let mut reverse_resolver = vec![0; resolver.len()]; - -// for &(_, src, dst) in edges { -// expected_out_edges.entry(src).or_default().push(dst); -// expected_in_edges.entry(dst).or_default().push(src); -// let id = resolver -// .get_u64(src) -// .unwrap_or_else(|| panic!("Missing src node {}", src)); -// reverse_resolver[id.0] = src; -// let id = resolver -// .get_u64(dst) -// .unwrap_or_else(|| panic!("Missing dst node {}", dst)); -// reverse_resolver[id.0] = dst; -// } - -// // Deduplicate expected edges -// for values in expected_out_edges.values_mut() { -// values.sort_unstable(); -// values.dedup(); -// } - -// for values in expected_in_edges.values_mut() { -// values.sort_unstable(); -// values.dedup(); -// } - -// let expected_num_edges = expected_out_edges.values().map(Vec::len).sum::(); -// // let expected_num_nodes = expected_out_edges.keys().chain(expected_in_edges.keys()).collect::>().len(); - -// // Verify graph structure -// let nodes = graph.nodes(); -// let edges_store = graph.edges(); - -// // assert_eq!(nodes.num_nodes(), expected_num_nodes); -// assert_eq!( -// edges_store.num_edges(), -// expected_num_edges, -// "Bad number of edges {label}" -// ); - -// for (exp_src, expected_outs) in expected_out_edges { -// for &exp_dst in &expected_outs { -// let src_vid = resolver.get_u64(exp_src).unwrap(); -// let dst_vid = resolver.get_u64(exp_dst).unwrap(); - -// let edge_id = graph -// .nodes() -// .get_edge(src_vid, dst_vid) -// .expect("Edge not found"); -// let edge = edges_store.edge(edge_id); -// let (src, dst) = edge.as_ref().edge().unwrap(); -// let (src_act, dst_act) = (reverse_resolver[src.0], reverse_resolver[dst.0]); - -// assert_eq!( -// (src_act, dst_act), -// (exp_src, exp_dst), -// "{label} Bad Edge {} -> {}", -// exp_src, -// exp_dst, -// ); -// } - -// let adj = graph.nodes().node(resolver.get_u64(exp_src).unwrap()); -// let adj = adj.as_ref(); - -// let mut out_neighbours: Vec<_> = adj -// .out_nbrs_sorted() -// .map(|VID(id)| reverse_resolver[id]) -// .collect(); -// out_neighbours.sort_unstable(); -// let mut expected_outs: Vec<_> = expected_outs.iter().copied().collect(); -// expected_outs.sort_unstable(); - -// assert_eq!( -// out_neighbours, expected_outs, -// "{label} Outbound edges don't match for node {}", -// exp_src -// ); - -// // Check edge lookup works and edge_id points to the right (src, dst) -// for (exp_dst, edge_id) in adj.out_edges() { -// let (VID(src), dst) = edges_store.get_edge(edge_id).unwrap(); -// assert_eq!(reverse_resolver[src], exp_src); -// assert_eq!(dst, exp_dst); -// } -// } - -// for (exp_dst, expected_ins) in expected_in_edges { -// let adj = nodes.node(resolver.get_u64(exp_dst).unwrap()); -// let adj = adj.as_ref(); - -// let mut in_neighbours: Vec<_> = adj -// .inb_nbrs_sorted() -// .map(|VID(id)| reverse_resolver[id]) -// .collect(); -// in_neighbours.sort_unstable(); -// let mut expected_ins: Vec<_> = expected_ins.iter().copied().collect(); -// expected_ins.sort_unstable(); - -// assert_eq!( -// in_neighbours, expected_ins, -// "Inbound edges don't match for node {}", -// exp_dst -// ); - -// // Check edge lookup works -// for (exp_src, edge_id) in adj.inb_edges() { -// let (src, VID(dst)) = edges_store.get_edge(edge_id).unwrap(); -// assert_eq!(reverse_resolver[dst], exp_dst); -// assert_eq!(src, exp_src); -// } -// } -// } - -// check_graph(edges, &graph, &resolver, "pre-drop"); -// if check_load { -// drop(graph); - -// // Reload graph and check again -// let graph = GraphStore::::load(&graph_dir).unwrap(); -// check_graph(edges, &graph, &resolver, "post-drop"); -// } -// } - -pub fn edges_strat(size: usize) -> impl Strategy> { - (1..=size).prop_flat_map(|num_nodes| { - let num_edges = 0..(num_nodes * num_nodes); - let srcs = (0usize..num_nodes).prop_map(VID); - let dsts = (0usize..num_nodes).prop_map(VID); - num_edges.prop_flat_map(move |num_edges| { - collection::vec((srcs.clone(), dsts.clone()), num_edges as usize) - }) - }) -} - -pub type AddEdge = ( - VID, - VID, - i64, - Vec<(String, Prop)>, - Vec<(String, Prop)>, - Option<&'static str>, -); - -#[derive(Debug)] -pub struct NodeFixture { - pub temp_props: Vec<(VID, i64, Vec<(String, Prop)>)>, - pub const_props: Vec<(VID, Vec<(String, Prop)>)>, -} - -#[derive(Debug)] -pub struct Fixture { - pub edges: Vec, - pub const_props: HashMap<(VID, VID), Vec<(String, Prop)>>, -} - -impl From> for Fixture { - fn from(edges: Vec) -> Self { - let mut const_props = HashMap::new(); - for (src, dst, _, _, c_props, _) in &edges { - for (k, v) in c_props { - const_props - .entry((*src, *dst)) - .or_insert_with(|| vec![]) - .push((k.clone(), v.clone())); - } - } - const_props.iter_mut().for_each(|(_, v)| { - v.sort_by(|a, b| a.0.cmp(&b.0)); - v.dedup_by(|a, b| a.0 == b.0); - }); - Self { edges, const_props } - } -} - -pub fn make_edges(num_edges: usize, num_nodes: usize) -> impl Strategy { - assert!(num_edges > 0); - assert!(num_nodes > 0); - (1..=num_edges, 1..=num_nodes) - .prop_flat_map(|(len, num_nodes)| build_raw_edges(len, num_nodes)) - .prop_map(|edges| edges.into()) -} - -pub fn make_nodes(num_nodes: usize) -> impl Strategy { - assert!(num_nodes > 0); - let schema = proptest::collection::hash_map( - (0i32..1000).prop_map(|i| i.to_string()), - prop_type(), - 0..30, - ); - - schema.prop_flat_map(move |schema| { - let (t_props, c_props) = make_props(&schema); - let temp_props = proptest::collection::vec( - ((0..num_nodes).prop_map(VID), 0i64..1000, t_props), - 1..=num_nodes, - ); - - let const_props = - proptest::collection::vec(((0..num_nodes).prop_map(VID), c_props), 1..=num_nodes); - - (temp_props, const_props).prop_map(|(temp_props, const_props)| NodeFixture { - temp_props, - const_props, - }) - }) -} - -pub fn build_raw_edges( - len: usize, - num_nodes: usize, -) -> impl Strategy< - Value = Vec<( - VID, - VID, - i64, - Vec<(String, Prop)>, - Vec<(String, Prop)>, - Option<&'static str>, - )>, -> { - proptest::collection::hash_map((0i32..1000).prop_map(|i| i.to_string()), prop_type(), 0..20) - .prop_flat_map(move |schema| { - let (t_props, c_props) = make_props(&schema); - - proptest::collection::vec( - ( - (0..num_nodes).prop_map(VID), - (0..num_nodes).prop_map(VID), - 0i64..(num_nodes as i64 * 5), - t_props, - c_props, - proptest::sample::select(vec![Some("a"), Some("b"), None]), - ), - 1..=len, - ) - }) -} - -pub fn prop_type() -> impl Strategy { - let leaf = proptest::sample::select(&[ - PropType::Str, - PropType::I64, - PropType::F64, - PropType::F32, - PropType::I32, - PropType::U8, - PropType::Bool, - PropType::DTime, - PropType::NDTime, - PropType::Decimal { scale: 7 }, // decimal breaks the tests because of polars-parquet - ]); - - // leaf.prop_recursive(3, 10, 10, |inner| { - // let dict = proptest::collection::hash_map(r"\w{1,10}", inner.clone(), 1..10) - // .prop_map(|map| PropType::map(map)); - // let list = inner - // .clone() - // .prop_map(|p_type| PropType::List(Box::new(p_type))); - // prop_oneof![inner, list, dict] - // }) - leaf -} - -pub fn make_props( - schema: &HashMap, -) -> ( - BoxedStrategy>, - BoxedStrategy>, -) { - let mut iter = schema.iter(); - - // split in half, one temporal one constant - let t_prop_s = (&mut iter) - .take(schema.len() / 2) - .map(|(k, v)| (k.clone(), v.clone())) - .collect::>(); - let c_prop_s = iter - .map(|(k, v)| (k.clone(), v.clone())) - .collect::>(); - - let num_tprops = t_prop_s.len(); - let num_cprops = c_prop_s.len(); - - let t_props = proptest::sample::subsequence(t_prop_s, 0..=num_tprops).prop_flat_map(|schema| { - schema - .into_iter() - .map(|(k, v)| prop(&v).prop_map(move |prop| (k.clone(), prop))) - .collect::>() - }); - let c_props = proptest::sample::subsequence(c_prop_s, 0..=num_cprops).prop_flat_map(|schema| { - schema - .into_iter() - .map(|(k, v)| prop(&v).prop_map(move |prop| (k.clone(), prop))) - .collect::>() - }); - (t_props.boxed(), c_props.boxed()) -} - -pub(crate) fn prop(p_type: &PropType) -> impl Strategy + use<> { - match p_type { - PropType::Str => (0i32..1000).prop_map(|s| Prop::str(s.to_string())).boxed(), - PropType::I64 => any::().prop_map(Prop::I64).boxed(), - PropType::I32 => any::().prop_map(Prop::I32).boxed(), - PropType::F64 => any::().prop_map(Prop::F64).boxed(), - PropType::F32 => any::().prop_map(Prop::F32).boxed(), - PropType::U8 => any::().prop_map(Prop::U8).boxed(), - PropType::Bool => any::().prop_map(Prop::Bool).boxed(), - PropType::DTime => (1900..2024, 1..=12, 1..28, 0..24, 0..60, 0..60) - .prop_map(|(year, month, day, h, m, s)| { - Prop::DTime( - format!( - "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z", - year, month, day, h, m, s - ) - .parse::>() - .unwrap(), - ) - }) - .boxed(), - PropType::NDTime => (1970..2024, 1..=12, 1..28, 0..24, 0..60, 0..60) - .prop_map(|(year, month, day, h, m, s)| { - // 2015-09-18T23:56:04 - Prop::NDTime( - format!( - "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}", - year, month, day, h, m, s - ) - .parse::() - .unwrap(), - ) - }) - .boxed(), - PropType::List(p_type) => proptest::collection::vec(prop(p_type), 0..10) - .prop_map(|props| Prop::List(props.into())) - .boxed(), - PropType::Map(p_types) => { - let prop_types: Vec> = p_types - .iter() - .map(|(a, b)| (a.clone(), b.clone())) - .collect::>() - .into_iter() - .map(|(name, p_type)| { - let pt_strat = prop(&p_type) - .prop_map(move |prop| (name.clone(), prop.clone())) - .boxed(); - pt_strat - }) - .collect_vec(); - - let props = proptest::sample::select(prop_types).prop_flat_map(|prop| prop); - - proptest::collection::vec(props, 1..10) - .prop_map(|props| Prop::map(props)) - .boxed() - } - PropType::Decimal { scale } => { - let scale = *scale; - let dec_max = DECIMAL_MAX; - ((scale as i128)..dec_max) - .prop_map(move |int| Prop::Decimal(BigDecimal::new(int.into(), scale))) - .boxed() - } - pt => { - panic!("Unsupported prop type: {:?}", pt); - } - } -} diff --git a/db4-storage/src/pages/test_utils/checkers.rs b/db4-storage/src/pages/test_utils/checkers.rs new file mode 100644 index 0000000000..9930f7fa5a --- /dev/null +++ b/db4-storage/src/pages/test_utils/checkers.rs @@ -0,0 +1,495 @@ +use std::{ + collections::{HashMap, HashSet}, + path::Path, +}; + +use itertools::Itertools; +use raphtory_api::core::{ + entities::properties::{prop::Prop, tprop::TPropOps}, + storage::dict_mapper::MaybeNew, +}; +use raphtory_core::{ + entities::{ELID, VID}, + storage::timeindex::TimeIndexOps, +}; +use rayon::prelude::*; + +use crate::{ + EdgeEntryOps, EdgeRefOps, EdgeSegmentOps, NodeEntryOps, NodeRefOps, NodeSegmentOps, + error::DBV4Error, pages::GraphStore, +}; + +use super::fixtures::{AddEdge, Fixture, NodeFixture}; + +pub fn check_edges_support< + NS: NodeSegmentOps, + ES: EdgeSegmentOps, + EXT: Clone + Default + Send + Sync + std::fmt::Debug, +>( + edges: Vec<(impl Into, impl Into, Option)>, // src, dst, optional layer_id + par_load: bool, + check_load: bool, + make_graph: impl FnOnce(&Path) -> GraphStore, +) { + let mut edges = edges + .into_iter() + .map(|(src, dst, layer_id)| (src.into(), dst.into(), layer_id)) + .collect::>(); + + let graph_dir = tempfile::tempdir().unwrap(); + let graph = make_graph(graph_dir.path()); + let mut nodes = HashSet::new(); + + for (src, dst, _) in &edges { + nodes.insert(*src); + nodes.insert(*dst); + } + + if par_load { + edges + .par_iter() + .try_for_each(|(src, dst, layer_id)| { + let lsn = 0; + let timestamp = 0; + + if let Some(layer_id) = layer_id { + let mut session = graph.write_session(*src, *dst, None); + let eid = session.add_static_edge(*src, *dst, lsn); + let elid = eid.map(|eid| eid.with_layer(*layer_id)); + + session.add_edge_into_layer(timestamp, *src, *dst, elid, lsn, []); + } else { + let _ = graph.add_edge(timestamp, *src, *dst)?; + } + + Ok::<_, DBV4Error>(()) + }) + .expect("Failed to add edge"); + } else { + edges + .iter() + .try_for_each(|(src, dst, layer_id)| { + let lsn = 0; + let timestamp = 0; + + if let Some(layer_id) = layer_id { + let mut session = graph.write_session(*src, *dst, None); + let eid = session.add_static_edge(*src, *dst, lsn).inner(); + let elid = eid.with_layer(*layer_id); + + session.add_edge_into_layer( + timestamp, + *src, + *dst, + MaybeNew::Existing(elid), + lsn, + [], + ); + } else { + let _ = graph.add_edge(timestamp, *src, *dst)?; + } + + Ok::<_, DBV4Error>(()) + }) + .expect("Failed to add edge"); + } + + let actual_num_nodes = graph.nodes().num_nodes(); + assert_eq!(actual_num_nodes, nodes.len()); + + edges.sort_unstable(); + + fn check< + NS: NodeSegmentOps, + ES: EdgeSegmentOps, + EXT: Clone + Default + std::fmt::Debug, + >( + stage: &str, + expected_edges: &[(VID, VID, Option)], // (src, dst, layer_id) + graph: &GraphStore, + ) { + let nodes = graph.nodes(); + let edges = graph.edges(); + + if !expected_edges.is_empty() { + assert!(nodes.pages().count() > 0, "{stage}"); + } + + // Group edges by layer_id first + let mut edges_by_layer: HashMap> = HashMap::new(); + for (src, dst, layer_id) in expected_edges { + edges_by_layer + .entry(layer_id.unwrap_or(0)) // Default layer_id to 0 + .or_default() + .push((*src, *dst)); + } + + // For each layer, build the expected graph structure + for (layer_id, layer_edges) in edges_by_layer { + let mut expected_graph: HashMap, Vec)> = layer_edges + .iter() + .chunk_by(|(src, _)| *src) + .into_iter() + .map(|(src, edges)| { + let mut out: Vec<_> = edges.map(|(_, dst)| *dst).collect(); + out.sort_unstable(); + out.dedup(); + (src, (out, vec![])) + }) + .collect::>(); + + let mut edges_sorted_by_dest = layer_edges.clone(); + edges_sorted_by_dest.sort_unstable_by_key(|(_, dst)| *dst); + + // now inbounds + edges_sorted_by_dest + .iter() + .chunk_by(|(_, dst)| *dst) + .into_iter() + .for_each(|(dst, edges)| { + let mut edges: Vec<_> = edges.map(|(src, _)| *src).collect(); + edges.sort_unstable(); + edges.dedup(); + let (_, inb) = expected_graph.entry(dst).or_default(); + *inb = edges; + }); + + for (n, (exp_out, exp_inb)) in expected_graph { + let entry = nodes.node(n); + + let adj = entry.as_ref(); + let out_nbrs: Vec<_> = adj.out_nbrs_sorted(layer_id).collect(); + assert_eq!( + out_nbrs, exp_out, + "{stage} node: {:?} layer: {}", + n, layer_id + ); + + let in_nbrs: Vec<_> = adj.inb_nbrs_sorted(layer_id).collect(); + assert_eq!( + in_nbrs, exp_inb, + "{stage} node: {:?} layer: {}", + n, layer_id + ); + + for (exp_dst, eid) in adj.out_edges(layer_id) { + let elid = ELID::new(eid, layer_id); + let (src, dst) = edges.get_edge(elid).unwrap(); + + assert_eq!(src, n, "{stage} layer: {}", layer_id); + assert_eq!(dst, exp_dst, "{stage} layer: {}", layer_id); + } + + for (exp_src, eid) in adj.inb_edges(layer_id) { + let elid = ELID::new(eid, layer_id); + let (src, dst) = edges.get_edge(elid).unwrap(); + + assert_eq!(src, exp_src, "{stage} layer: {}", layer_id); + assert_eq!(dst, n, "{stage} layer: {}", layer_id); + } + } + } + } + + check("pre-drop", &edges, &graph); + + if check_load { + drop(graph); + + let maybe_ns = GraphStore::::load(graph_dir.path()); + if edges.is_empty() { + assert!(maybe_ns.is_err()); + } else { + match maybe_ns { + Ok(graph) => { + check("post-drop", &edges, &graph); + } + Err(e) => { + panic!("Failed to load graph: {:?}", e); + } + } + } + } +} + +pub fn check_graph_with_nodes_support< + EXT: Clone + Default + Send + Sync, + NS: NodeSegmentOps, + ES: EdgeSegmentOps, +>( + fixture: &NodeFixture, + check_load: bool, + make_graph: impl FnOnce(&Path) -> GraphStore, +) { + let NodeFixture { + temp_props, + const_props, + } = fixture; + + let graph_dir = tempfile::tempdir().unwrap(); + let graph = make_graph(graph_dir.path()); + + for (node, t, t_props) in temp_props { + let err = graph.add_node_props(*t, *node, 0, t_props.clone()); + + assert!(err.is_ok(), "Failed to add node: {:?}", err); + } + + for (node, const_props) in const_props { + let err = graph.update_node_const_props(*node, 0, const_props.clone()); + + assert!(err.is_ok(), "Failed to add node: {:?}", err); + } + + let check_fn = |temp_props: &[(VID, i64, Vec<(String, Prop)>)], + const_props: &[(VID, Vec<(String, Prop)>)], + graph: &GraphStore| { + let mut ts_for_nodes = HashMap::new(); + for (node, t, _) in temp_props { + ts_for_nodes.entry(*node).or_insert_with(|| vec![]).push(*t); + } + ts_for_nodes.iter_mut().for_each(|(_, ts)| { + ts.sort_unstable(); + }); + + for (node, ts_expected) in ts_for_nodes { + let ne = graph.nodes().node(node); + let node_entry = ne.as_ref(); + let actual: Vec<_> = node_entry.additions(0).iter_t().collect(); + assert_eq!( + actual, ts_expected, + "Expected node additions for node ({node:?})", + ); + } + + let mut const_props_values = HashMap::new(); + for (node, const_props) in const_props { + let node = *node; + for (name, prop) in const_props { + const_props_values + .entry((node, name)) + .or_insert_with(|| HashSet::new()) + .insert(prop.clone()); + } + } + + for ((node, name), const_props) in const_props_values { + let ne = graph.nodes().node(node); + let node_entry = ne.as_ref(); + + let prop_id = graph + .node_meta() + .const_prop_meta() + .get_id(&name) + .unwrap_or_else(|| panic!("Failed to get prop id for {}", name)); + let actual_props = node_entry.c_prop(0, prop_id); + + if !const_props.is_empty() { + let actual_prop = actual_props + .unwrap_or_else(|| panic!("Failed to get prop {name} for {node:?}")); + assert!( + const_props.contains(&actual_prop), + "failed to get const prop {name} for {node:?}, expected {:?}, got {:?}", + const_props, + actual_prop + ); + } + } + + let mut nod_t_prop_groups = HashMap::new(); + for (node, t, t_props) in temp_props { + let node = *node; + let t = *t; + + for (prop_name, prop) in t_props { + let prop_values = nod_t_prop_groups + .entry((node, prop_name)) + .or_insert_with(|| vec![]); + prop_values.push((t, prop.clone())); + } + } + + nod_t_prop_groups.iter_mut().for_each(|(_, props)| { + props.sort_unstable_by_key(|(t, _)| *t); + }); + + for ((node, prop_name), props) in nod_t_prop_groups { + let prop_id = graph + .node_meta() + .temporal_prop_meta() + .get_id(&prop_name) + .unwrap_or_else(|| panic!("Failed to get prop id for {}", prop_name)); + + let ne = graph.nodes().node(node); + let node_entry = ne.as_ref(); + let actual_props = node_entry.t_prop(0, prop_id).iter_t().collect::>(); + + assert_eq!( + actual_props, props, + "Expected properties for node ({:?}) to be {:?}, but got {:?}", + node, props, actual_props + ); + } + }; + + check_fn(temp_props, const_props, &graph); + + if check_load { + drop(graph); + let graph = GraphStore::::load(graph_dir.path()).unwrap(); + check_fn(temp_props, const_props, &graph); + } +} + +pub fn check_graph_with_props_support< + EXT: Clone + Default + Send + Sync, + NS: NodeSegmentOps, + ES: EdgeSegmentOps, +>( + fixture: &Fixture, + check_load: bool, + make_graph: impl FnOnce(&Path) -> GraphStore, +) { + let Fixture { edges, const_props } = fixture; + let graph_dir = tempfile::tempdir().unwrap(); + let graph = make_graph(graph_dir.path()); + + // Add edges + for (src, dst, t, t_props, _, _) in edges { + let err = graph.add_edge_props(*t, *src, *dst, t_props.clone(), 0); + + assert!(err.is_ok(), "Failed to add edge: {:?}", err); + } + + // Add const props + for ((src, dst), const_props) in const_props { + let layer_id = 0; + let eid = graph + .nodes() + .get_edge(*src, *dst, layer_id) + .unwrap_or_else(|| panic!("Failed to get edge ({:?}, {:?}) from graph", src, dst)); + let elid = ELID::new(eid, layer_id); + let res = graph.update_edge_const_props(elid, const_props.clone()); + + assert!( + res.is_ok(), + "Failed to update edge const props: {:?} {src:?} -> {dst:?}", + res + ); + } + + assert!(graph.edges().num_edges() > 0); + + let check_fn = |edges: &[AddEdge], graph: &GraphStore| { + let mut edge_groups = HashMap::new(); + let mut node_groups: HashMap> = HashMap::new(); + + // Group temporal edge props and their timestamps + for (src, dst, t, t_props, _, _) in edges { + let src = *src; + let dst = *dst; + let t = *t; + + for (prop_name, prop) in t_props { + let prop_values = edge_groups + .entry((src, dst, prop_name)) + .or_insert_with(|| vec![]); + prop_values.push((t, prop.clone())); + } + } + + edge_groups.iter_mut().for_each(|(_, props)| { + props.sort_unstable_by_key(|(t, _)| *t); + }); + + // Group node additions and their timestamps + for (src, dst, t, _, _, _) in edges { + let src = *src; + let dst = *dst; + let t = *t; + + // Include src additions + node_groups.entry(src).or_insert_with(|| vec![]).push(t); + + // Self-edges don't have dst additions, so skip + if src == dst { + continue; + } + + // Include dst additions + node_groups.entry(dst).or_insert_with(|| vec![]).push(t); + } + + node_groups.iter_mut().for_each(|(_, ts)| { + ts.sort_unstable(); + }); + + for ((src, dst, prop_name), props) in edge_groups { + // Check temporal props + let prop_id = graph + .edge_meta() + .temporal_prop_meta() + .get_id(&prop_name) + .unwrap_or_else(|| panic!("Failed to get prop id for {}", prop_name)); + + let edge = graph + .nodes() + .get_edge(src, dst, 0) + .unwrap_or_else(|| panic!("Failed to get edge ({:?}, {:?}) from graph", src, dst)); + let edge = graph.edges().edge(edge); + let e = edge.as_ref(); + let layer_id = 0; + let actual_props = e.t_prop(layer_id, prop_id).iter_t().collect::>(); + + assert_eq!( + actual_props, props, + "Expected properties for edge ({:?}, {:?}) to be {:?}, but got {:?}", + src, dst, props, actual_props + ); + + // Check const props + if let Some(exp_const_props) = const_props.get(&(src, dst)) { + for (name, prop) in exp_const_props { + let prop_id = graph + .edge_meta() + .const_prop_meta() + .get_id(name) + .unwrap_or_else(|| panic!("Failed to get prop id for {}", name)); + let actual_props = e.c_prop(layer_id, prop_id); + assert_eq!( + actual_props.as_ref(), + Some(prop), + "Expected const properties for edge ({:?}, {:?}) to be {:?}, but got {:?}", + src, + dst, + prop, + actual_props + ); + } + } + } + + // Check node additions and their timestamps + for (node_id, ts) in node_groups { + let node = graph.nodes().node(node_id); + let node_entry = node.as_ref(); + let actual_additions_ts = node_entry.additions(0).iter_t().collect::>(); + + assert_eq!( + actual_additions_ts, ts, + "Expected node additions for node ({:?}) to be {:?}, but got {:?}", + node_id, ts, actual_additions_ts + ); + } + }; + + check_fn(edges, &graph); + + if check_load { + // Load the graph from disk and check again + drop(graph); + + let graph = GraphStore::::load(graph_dir.path()).unwrap(); + check_fn(edges, &graph); + } +} diff --git a/db4-storage/src/pages/test_utils/fixtures.rs b/db4-storage/src/pages/test_utils/fixtures.rs new file mode 100644 index 0000000000..257dcda713 --- /dev/null +++ b/db4-storage/src/pages/test_utils/fixtures.rs @@ -0,0 +1,136 @@ +use std::collections::HashMap; +use proptest::{collection, prelude::*}; +use raphtory_core::entities::VID; +use raphtory_api::core::entities::properties::prop::Prop; + +use super::props::{make_props, prop_type}; + +pub type AddEdge = ( + VID, + VID, + i64, + Vec<(String, Prop)>, + Vec<(String, Prop)>, + Option<&'static str>, +); + +#[derive(Debug)] +pub struct NodeFixture { + pub temp_props: Vec<(VID, i64, Vec<(String, Prop)>)>, + pub const_props: Vec<(VID, Vec<(String, Prop)>)>, +} + +#[derive(Debug)] +pub struct Fixture { + pub edges: Vec, + pub const_props: HashMap<(VID, VID), Vec<(String, Prop)>>, +} + +impl From> for Fixture { + fn from(edges: Vec) -> Self { + let mut const_props = HashMap::new(); + for (src, dst, _, _, c_props, _) in &edges { + for (k, v) in c_props { + const_props + .entry((*src, *dst)) + .or_insert_with(|| vec![]) + .push((k.clone(), v.clone())); + } + } + const_props.iter_mut().for_each(|(_, v)| { + v.sort_by(|a, b| a.0.cmp(&b.0)); + v.dedup_by(|a, b| a.0 == b.0); + }); + Self { edges, const_props } + } +} + +pub fn make_edges(num_edges: usize, num_nodes: usize) -> impl Strategy { + assert!(num_edges > 0); + assert!(num_nodes > 0); + (1..=num_edges, 1..=num_nodes) + .prop_flat_map(|(len, num_nodes)| build_raw_edges(len, num_nodes)) + .prop_map(|edges| edges.into()) +} + +pub fn make_nodes(num_nodes: usize) -> impl Strategy { + assert!(num_nodes > 0); + let schema = proptest::collection::hash_map( + (0i32..1000).prop_map(|i| i.to_string()), + prop_type(), + 0..30, + ); + + schema.prop_flat_map(move |schema| { + let (t_props, c_props) = make_props(&schema); + let temp_props = proptest::collection::vec( + ((0..num_nodes).prop_map(VID), 0i64..1000, t_props), + 1..=num_nodes, + ); + + let const_props = + proptest::collection::vec(((0..num_nodes).prop_map(VID), c_props), 1..=num_nodes); + + (temp_props, const_props).prop_map(|(temp_props, const_props)| NodeFixture { + temp_props, + const_props, + }) + }) +} + +pub fn edges_strat(size: usize) -> impl Strategy> { + (1..=size).prop_flat_map(|num_nodes| { + let num_edges = 0..(num_nodes * num_nodes); + let srcs = (0usize..num_nodes).prop_map(VID); + let dsts = (0usize..num_nodes).prop_map(VID); + num_edges.prop_flat_map(move |num_edges| { + collection::vec((srcs.clone(), dsts.clone()), num_edges as usize) + }) + }) +} + +pub fn edges_strat_with_layers(size: usize) -> impl Strategy)>> { + const MAX_LAYERS: usize = 16; + + (1..=size).prop_flat_map(|num_nodes| { + let num_edges = 0..(num_nodes * num_nodes); + let srcs = (0usize..num_nodes).prop_map(VID); + let dsts = (0usize..num_nodes).prop_map(VID); + let layer_ids = (1usize..MAX_LAYERS).prop_map(|i| Some(i as usize)); + + num_edges.prop_flat_map(move |num_edges| { + collection::vec((srcs.clone(), dsts.clone(), layer_ids.clone()), num_edges as usize) + }) + }) +} + +pub fn build_raw_edges( + len: usize, + num_nodes: usize, +) -> impl Strategy< + Value = Vec<( + VID, + VID, + i64, + Vec<(String, Prop)>, + Vec<(String, Prop)>, + Option<&'static str>, + )>, +> { + proptest::collection::hash_map((0i32..1000).prop_map(|i| i.to_string()), prop_type(), 0..20) + .prop_flat_map(move |schema| { + let (t_props, c_props) = make_props(&schema); + + proptest::collection::vec( + ( + (0..num_nodes).prop_map(VID), + (0..num_nodes).prop_map(VID), + 0i64..(num_nodes as i64 * 5), + t_props, + c_props, + proptest::sample::select(vec![Some("a"), Some("b"), None]), + ), + 1..=len, + ) + }) +} diff --git a/db4-storage/src/pages/test_utils/mod.rs b/db4-storage/src/pages/test_utils/mod.rs new file mode 100644 index 0000000000..7690f415af --- /dev/null +++ b/db4-storage/src/pages/test_utils/mod.rs @@ -0,0 +1,7 @@ +mod checkers; +mod fixtures; +mod props; + +pub use checkers::*; +pub use fixtures::*; +pub use props::*; diff --git a/db4-storage/src/pages/test_utils/props.rs b/db4-storage/src/pages/test_utils/props.rs new file mode 100644 index 0000000000..665510d00f --- /dev/null +++ b/db4-storage/src/pages/test_utils/props.rs @@ -0,0 +1,139 @@ +use std::collections::HashMap; +use proptest::{collection, prelude::*}; +use raphtory_api::core::entities::properties::{ + prop::{DECIMAL_MAX, Prop, PropType}, + tprop::TPropOps, +}; +use chrono::{DateTime, NaiveDateTime, Utc}; +use bigdecimal::BigDecimal; +use itertools::Itertools; + +pub fn prop_type() -> impl Strategy { + let leaf = proptest::sample::select(&[ + PropType::Str, + PropType::I64, + PropType::F64, + PropType::F32, + PropType::I32, + PropType::U8, + PropType::Bool, + PropType::DTime, + PropType::NDTime, + PropType::Decimal { scale: 7 }, // decimal breaks the tests because of polars-parquet + ]); + + // leaf.prop_recursive(3, 10, 10, |inner| { + // let dict = proptest::collection::hash_map(r"\w{1,10}", inner.clone(), 1..10) + // .prop_map(|map| PropType::map(map)); + // let list = inner + // .clone() + // .prop_map(|p_type| PropType::List(Box::new(p_type))); + // prop_oneof![inner, list, dict] + // }) + leaf +} + +pub fn make_props( + schema: &HashMap, +) -> ( + BoxedStrategy>, + BoxedStrategy>, +) { + let mut iter = schema.iter(); + + // split in half, one temporal one constant + let t_prop_s = (&mut iter) + .take(schema.len() / 2) + .map(|(k, v)| (k.clone(), v.clone())) + .collect::>(); + let c_prop_s = iter + .map(|(k, v)| (k.clone(), v.clone())) + .collect::>(); + + let num_tprops = t_prop_s.len(); + let num_cprops = c_prop_s.len(); + + let t_props = proptest::sample::subsequence(t_prop_s, 0..=num_tprops).prop_flat_map(|schema| { + schema + .into_iter() + .map(|(k, v)| prop(&v).prop_map(move |prop| (k.clone(), prop))) + .collect::>() + }); + let c_props = proptest::sample::subsequence(c_prop_s, 0..=num_cprops).prop_flat_map(|schema| { + schema + .into_iter() + .map(|(k, v)| prop(&v).prop_map(move |prop| (k.clone(), prop))) + .collect::>() + }); + (t_props.boxed(), c_props.boxed()) +} + +pub(crate) fn prop(p_type: &PropType) -> impl Strategy + use<> { + match p_type { + PropType::Str => (0i32..1000).prop_map(|s| Prop::str(s.to_string())).boxed(), + PropType::I64 => any::().prop_map(Prop::I64).boxed(), + PropType::I32 => any::().prop_map(Prop::I32).boxed(), + PropType::F64 => any::().prop_map(Prop::F64).boxed(), + PropType::F32 => any::().prop_map(Prop::F32).boxed(), + PropType::U8 => any::().prop_map(Prop::U8).boxed(), + PropType::Bool => any::().prop_map(Prop::Bool).boxed(), + PropType::DTime => (1900..2024, 1..=12, 1..28, 0..24, 0..60, 0..60) + .prop_map(|(year, month, day, h, m, s)| { + Prop::DTime( + format!( + "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z", + year, month, day, h, m, s + ) + .parse::>() + .unwrap(), + ) + }) + .boxed(), + PropType::NDTime => (1970..2024, 1..=12, 1..28, 0..24, 0..60, 0..60) + .prop_map(|(year, month, day, h, m, s)| { + // 2015-09-18T23:56:04 + Prop::NDTime( + format!( + "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}", + year, month, day, h, m, s + ) + .parse::() + .unwrap(), + ) + }) + .boxed(), + PropType::List(p_type) => proptest::collection::vec(prop(p_type), 0..10) + .prop_map(|props| Prop::List(props.into())) + .boxed(), + PropType::Map(p_types) => { + let prop_types: Vec> = p_types + .iter() + .map(|(a, b)| (a.clone(), b.clone())) + .collect::>() + .into_iter() + .map(|(name, p_type)| { + let pt_strat = prop(&p_type) + .prop_map(move |prop| (name.clone(), prop.clone())) + .boxed(); + pt_strat + }) + .collect_vec(); + + let props = proptest::sample::select(prop_types).prop_flat_map(|prop| prop); + + proptest::collection::vec(props, 1..10) + .prop_map(|props| Prop::map(props)) + .boxed() + } + PropType::Decimal { scale } => { + let scale = *scale; + let dec_max = DECIMAL_MAX; + ((scale as i128)..dec_max) + .prop_map(move |int| Prop::Decimal(BigDecimal::new(int.into(), scale))) + .boxed() + } + pt => { + panic!("Unsupported prop type: {:?}", pt); + } + } +} diff --git a/db4-storage/src/segments/edge.rs b/db4-storage/src/segments/edge.rs index c403fb9acd..73ed13e824 100644 --- a/db4-storage/src/segments/edge.rs +++ b/db4-storage/src/segments/edge.rs @@ -66,6 +66,18 @@ impl MemEdgeSegment { } } + pub fn get_or_create_layer(&mut self, layer_id: usize) -> &mut SegmentContainer { + if layer_id >= self.layers.len() { + let max_page_len = self.layers[0].max_page_len(); + let segment_id = self.layers[0].segment_id(); + let meta = self.layers[0].meta().clone(); + self.layers.resize_with(layer_id + 1, || { + SegmentContainer::new(segment_id, max_page_len, meta.clone()) + }); + } + &mut self.layers[layer_id] + } + pub fn est_size(&self) -> usize { self.layers.iter().map(|seg| seg.est_size()).sum::() } diff --git a/db4-storage/src/segments/node.rs b/db4-storage/src/segments/node.rs index 9516460fe9..d49977af58 100644 --- a/db4-storage/src/segments/node.rs +++ b/db4-storage/src/segments/node.rs @@ -13,7 +13,7 @@ use raphtory_core::{ }; use std::{ ops::{Deref, DerefMut}, - sync::{Arc, atomic, atomic::AtomicUsize}, + sync::Arc, }; use super::{HasRow, SegmentContainer}; @@ -79,6 +79,22 @@ impl AsMut<[SegmentContainer]> for MemNodeSegment { } impl MemNodeSegment { + pub fn get_or_create_layer(&mut self, layer_id: usize) -> &mut SegmentContainer { + if layer_id >= self.layers.len() { + let max_page_len = self.layers[0].max_page_len(); + let segment_id = self.layers[0].segment_id(); + let meta = self.layers[0].meta().clone(); + self.layers.resize_with(layer_id + 1, || { + SegmentContainer::new(segment_id, max_page_len, meta.clone()) + }); + } + &mut self.layers[layer_id] + } + + pub fn get_layer(&self, layer_id: usize) -> Option<&SegmentContainer> { + self.layers.get(layer_id) + } + pub fn lsn(&self) -> u64 { self.layers.iter().map(|seg| seg.lsn()).min().unwrap_or(0) } @@ -89,7 +105,7 @@ impl MemNodeSegment { #[inline(always)] fn get_adj(&self, n: LocalPOS, layer_id: usize) -> Option<&Adj> { - self.layers[layer_id] + self.layers.get(layer_id)? .get(&n) .map(|AdjEntry { adj, .. }| adj) } @@ -132,11 +148,15 @@ impl MemNodeSegment { src_pos: LocalPOS, dst: impl Into, e_id: impl Into, + lsn: u64, ) -> bool { let dst = dst.into(); let e_id = e_id.into(); let layer_id = e_id.layer(); - let add_out = self.layers[layer_id].reserve_local_row(src_pos).map_either( + let layer = self.get_or_create_layer(layer_id); + layer.set_lsn(lsn); + + let add_out = layer.reserve_local_row(src_pos).map_either( |row| { row.adj.add_edge_out(dst, e_id.edge); row.row() @@ -160,13 +180,16 @@ impl MemNodeSegment { dst_pos: impl Into, src: impl Into, e_id: impl Into, + lsn: u64, ) -> bool { let src = src.into(); let e_id = e_id.into(); let layer_id = e_id.layer(); let dst_pos = dst_pos.into(); - let add_in = self.layers[layer_id].reserve_local_row(dst_pos).map_either( + let layer = self.get_or_create_layer(layer_id); + layer.set_lsn(lsn); + let add_in = layer.reserve_local_row(dst_pos).map_either( |row| { row.adj.add_edge_into(src, e_id.edge); row.row() @@ -244,10 +267,10 @@ impl MemNodeSegment { } } +#[derive(Debug)] pub struct NodeSegmentView { inner: Arc>, segment_id: usize, - num_nodes: AtomicUsize, _ext: EXT, } @@ -292,7 +315,6 @@ impl NodeSegmentOps for NodeSegmentView { inner: parking_lot::RwLock::new(MemNodeSegment::new(page_id, max_page_len, meta)) .into(), segment_id: page_id, - num_nodes: AtomicUsize::new(0), _ext: (), } } @@ -313,14 +335,6 @@ impl NodeSegmentOps for NodeSegmentView { self.inner.write() } - fn num_nodes(&self) -> usize { - self.num_nodes.load(atomic::Ordering::Relaxed) - } - - fn increment_num_nodes(&self) -> usize { - self.num_nodes.fetch_add(1, atomic::Ordering::Relaxed) - } - fn notify_write( &self, _head_lock: impl DerefMut, @@ -356,4 +370,14 @@ impl NodeSegmentOps for NodeSegmentView { let pos = pos.into(); MemNodeEntry::new(pos, self.head()) } + + fn num_layers(&self) -> usize { + self.head().layers.len() + } + + fn layer_num_nodes(&self, layer_id: usize) -> usize { + self.head() + .get_layer(layer_id) + .map_or(0, |layer| layer.len()) + } } From 22e58b474eaf9222c97387a3a746bfbe58d3de69 Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Wed, 18 Jun 2025 13:12:23 +0100 Subject: [PATCH 017/321] fix issues with accessing layers --- db4-storage/src/pages/node_store.rs | 8 +++----- db4-storage/src/segments/node.rs | 10 +++++++--- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/db4-storage/src/pages/node_store.rs b/db4-storage/src/pages/node_store.rs index 394a6a59a3..fd881a5585 100644 --- a/db4-storage/src/pages/node_store.rs +++ b/db4-storage/src/pages/node_store.rs @@ -1,6 +1,7 @@ use super::{node_page::writer::NodeWriter, resolve_pos}; use crate::{ - error::DBV4Error, pages::layer_counter::LayerCounter, segments::node::MemNodeSegment, LocalPOS, NodeSegmentOps, ReadLockedNS + LocalPOS, NodeSegmentOps, ReadLockedNS, error::DBV4Error, pages::layer_counter::LayerCounter, + segments::node::MemNodeSegment, }; use parking_lot::RwLockWriteGuard; use raphtory_api::core::entities::properties::meta::Meta; @@ -8,10 +9,7 @@ use raphtory_core::entities::{EID, VID}; use std::{ collections::HashMap, path::{Path, PathBuf}, - sync::{ - Arc, - atomic::{self, AtomicUsize}, - }, + sync::Arc, }; // graph // (nodes|edges) // graph segments // layers // chunks diff --git a/db4-storage/src/segments/node.rs b/db4-storage/src/segments/node.rs index d49977af58..b09544ab5f 100644 --- a/db4-storage/src/segments/node.rs +++ b/db4-storage/src/segments/node.rs @@ -105,13 +105,16 @@ impl MemNodeSegment { #[inline(always)] fn get_adj(&self, n: LocalPOS, layer_id: usize) -> Option<&Adj> { - self.layers.get(layer_id)? + self.layers + .get(layer_id)? .get(&n) .map(|AdjEntry { adj, .. }| adj) } pub fn has_node(&self, n: LocalPOS, layer_id: usize) -> bool { - self.layers[layer_id].items().get(n.0).map_or(false, |v| *v) + self.layers + .get(layer_id) + .is_some_and(|layer| layer.items().get(n.0).map_or(false, |v| *v)) } pub fn get_out_edge(&self, n: LocalPOS, dst: VID, layer_id: usize) -> Option { @@ -229,7 +232,8 @@ impl MemNodeSegment { layer_id: usize, props: impl IntoIterator, ) { - let row = self.layers[layer_id] + let layer = self.get_or_create_layer(layer_id); + let row = layer .reserve_local_row(node_pos) .either(|a| a.row, |a| a.row); let mut prop_mut_entry = self.layers[layer_id].properties_mut().get_mut_entry(row); From 1973b87b90cfda117d05b9d011c514e67742df93 Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Wed, 18 Jun 2025 17:45:48 +0100 Subject: [PATCH 018/321] changes to integrate with raphtory-storage --- db4-graph/src/entries/mod.rs | 2 +- db4-graph/src/entries/node.rs | 2 +- db4-graph/src/lib.rs | 63 ++++--------------- db4-storage/src/lib.rs | 24 ++++++- db4-storage/src/pages/layer_counter.rs | 4 ++ db4-storage/src/pages/mod.rs | 4 +- db4-storage/src/pages/node_store.rs | 14 +++++ db4-storage/src/segments/edge_entry.rs | 3 +- db4-storage/src/segments/node_entry.rs | 2 +- .../src/graph/edges/edge_entry.rs | 8 +-- raphtory-storage/src/graph/edges/edge_ref.rs | 3 +- raphtory-storage/src/graph/edges/edges.rs | 9 +-- raphtory-storage/src/graph/edges/unlocked.rs | 3 +- raphtory-storage/src/graph/graph.rs | 56 ++++++++--------- raphtory-storage/src/graph/locked.rs | 51 +++++++-------- .../src/graph/nodes/node_entry.rs | 11 ++-- raphtory-storage/src/graph/nodes/nodes_ref.rs | 9 +-- 17 files changed, 134 insertions(+), 134 deletions(-) diff --git a/db4-graph/src/entries/mod.rs b/db4-graph/src/entries/mod.rs index 492bc84b46..1b1a4501d6 100644 --- a/db4-graph/src/entries/mod.rs +++ b/db4-graph/src/entries/mod.rs @@ -1 +1 @@ -pub mod node; +// pub mod node; diff --git a/db4-graph/src/entries/node.rs b/db4-graph/src/entries/node.rs index 467a0e57c7..1a2e199a2c 100644 --- a/db4-graph/src/entries/node.rs +++ b/db4-graph/src/entries/node.rs @@ -1,6 +1,6 @@ use raphtory_core::entities::VID; -use crate::{ReadLockedTemporalGraph, TemporalGraph}; +use crate::TemporalGraph; #[derive(Debug, Copy, Clone)] pub struct LockedNodeEntry<'a, EXT> { diff --git a/db4-graph/src/lib.rs b/db4-graph/src/lib.rs index b0d645bcd6..345398f9d6 100644 --- a/db4-graph/src/lib.rs +++ b/db4-graph/src/lib.rs @@ -1,5 +1,4 @@ use std::{ - ops::Deref, path::{Path, PathBuf}, sync::{ atomic::{self, AtomicUsize}, @@ -7,14 +6,13 @@ use std::{ }, }; +// use crate::entries::node::UnlockedNodeEntry; use raphtory_api::core::{entities::properties::meta::Meta, input::input_node::InputNode}; use raphtory_core::entities::{ graph::logical_to_physical::Mapping, nodes::node_ref::NodeRef, properties::graph_meta::GraphMeta, GidRef, VID, }; -use storage::{persist::strategy::PersistentStrategy, Extension, Layer, ES, NS}; - -use crate::entries::node::{LockedNodeEntry, UnlockedNodeEntry}; +use storage::{persist::strategy::PersistentStrategy, Extension, Layer, ReadLockedLayer, ES, NS}; pub mod entries; pub mod mutation; @@ -29,8 +27,7 @@ pub struct TemporalGraph { max_page_len_nodes: usize, max_page_len_edges: usize, - static_graph: Arc>, - layers: boxcar::Vec>>, + storage: Arc>, edge_meta: Arc, node_meta: Arc, @@ -39,38 +36,17 @@ pub struct TemporalGraph { graph_meta: Arc, } -#[derive(Debug)] -pub struct ReadLockedTemporalGraph { - pub graph: Arc>, - static_graph: storage::ReadLockedLayer, - locked_layers: Box<[storage::ReadLockedLayer]>, -} - -impl ReadLockedTemporalGraph { - pub fn graph(&self) -> &Arc> { - &self.graph - } - - pub fn node(&self, vid: VID) -> LockedNodeEntry { - LockedNodeEntry::new(vid, self) - } - - pub fn inner(&self) -> &TemporalGraph { - &self.graph - } -} - impl, ES = ES>> TemporalGraph { - pub fn node(&self, vid: VID) -> UnlockedNodeEntry { - UnlockedNodeEntry::new(vid, self) - } + // pub fn node(&self, vid: VID) -> UnlockedNodeEntry { + // UnlockedNodeEntry::new(vid, self) + // } pub fn read_event_counter(&self) -> usize { self.event_counter.load(atomic::Ordering::Relaxed) } - pub fn static_graph(&self) -> &Arc> { - &self.static_graph + pub fn storage(&self) -> &Arc> { + &self.storage } pub fn graph_meta(&self) -> &Arc { @@ -78,7 +54,7 @@ impl, ES = ES>> TemporalGraph { } pub fn num_layers(&self) -> usize { - self.layers.count() + self.storage.nodes().num_layers() } #[inline] @@ -95,29 +71,16 @@ impl, ES = ES>> TemporalGraph { #[inline] pub fn internal_num_nodes(&self) -> usize { - self.static_graph.nodes().num_nodes() + self.storage.nodes().num_nodes() } #[inline] pub fn internal_num_edges(&self) -> usize { - self.static_graph.edges().num_edges() - } - - pub fn read_locked(self: &Arc) -> ReadLockedTemporalGraph { - let locked_layers = self - .layers - .iter() - .map(|(_, layer)| layer.read_locked()) - .collect::>(); - ReadLockedTemporalGraph { - graph: self.clone(), - static_graph: self.static_graph.read_locked(), - locked_layers, - } + self.storage.edges().num_edges() } - pub fn layers(&self) -> &boxcar::Vec>> { - &self.layers + pub fn read_locked(self: &Arc) -> ReadLockedLayer { + self.storage.read_locked() } pub fn edge_meta(&self) -> &Arc { diff --git a/db4-storage/src/lib.rs b/db4-storage/src/lib.rs index 8c824abb64..891b08ec75 100644 --- a/db4-storage/src/lib.rs +++ b/db4-storage/src/lib.rs @@ -7,7 +7,7 @@ use std::{ use crate::{ error::DBV4Error, pages::{GraphStore, ReadLockedGraphStore}, - segments::{edge::EdgeSegmentView, node::NodeSegmentView}, + segments::{edge::EdgeSegmentView, edge_entry::{MemEdgeEntry, MemEdgeRef}, node::NodeSegmentView, node_entry::{MemNodeEntry, MemNodeRef}}, }; use parking_lot::{RwLockReadGuard, RwLockWriteGuard, lock_api::ArcRwLockReadGuard}; use raphtory_api::core::{ @@ -18,6 +18,8 @@ use raphtory_api::core::{ storage::timeindex::{TimeIndexEntry, TimeIndexOps}, }; use segments::{edge::MemEdgeSegment, node::MemNodeSegment}; +use crate::pages::edge_store::ReadLockedEdgeStorage; +use crate::pages::node_store::ReadLockedNodeStorage; // pub mod loaders; pub mod pages; @@ -31,6 +33,14 @@ pub type ES

= EdgeSegmentView

; pub type Layer = GraphStore, EdgeSegmentView, EXT>; pub type ReadLockedLayer = ReadLockedGraphStore; +pub type ReadLockedNodes

= ReadLockedNodeStorage; +pub type ReadLockedEdges

= ReadLockedEdgeStorage; + +pub type NodeEntry<'a> = MemNodeEntry<'a, parking_lot::RwLockReadGuard<'a, MemNodeSegment>>; +pub type EdgeEntry<'a> = MemEdgeEntry<'a, parking_lot::RwLockReadGuard<'a, MemEdgeSegment>>; +pub type NodeEntryRef<'a> = MemNodeRef<'a>; +pub type EdgeEntryRef<'a> = MemEdgeRef<'a>; + pub trait EdgeSegmentOps: Send + Sync + std::fmt::Debug { type Extension; @@ -144,6 +154,10 @@ pub trait NodeSegmentOps: Send + Sync + std::fmt::Debug { where Self: 'a; + type EntryRef<'a>: NodeRefOps<'a> + where + Self: 'a; + fn latest(&self) -> Option; fn earliest(&self) -> Option; @@ -223,6 +237,14 @@ pub struct ReadLockedNS { head: ArcRwLockReadGuard, } +impl > ReadLockedNS { + + pub fn entry_ref<'a>(&'a self, pos: impl Into) -> NS::EntryRef<'a> { + self.ns.entry(pos) + } + +} + pub trait NodeEntryOps<'a> { type Ref<'b>: NodeRefOps<'b> where diff --git a/db4-storage/src/pages/layer_counter.rs b/db4-storage/src/pages/layer_counter.rs index 50439b8866..48b1826048 100644 --- a/db4-storage/src/pages/layer_counter.rs +++ b/db4-storage/src/pages/layer_counter.rs @@ -25,6 +25,10 @@ impl LayerCounter { } Self { layers } } + + pub fn len(&self) -> usize { + self.layers.count() + } pub fn increment(&self, layer_id: usize) -> usize { let counter = self.get_or_create_layer(layer_id); diff --git a/db4-storage/src/pages/mod.rs b/db4-storage/src/pages/mod.rs index a14f785c3d..bd6c1f0071 100644 --- a/db4-storage/src/pages/mod.rs +++ b/db4-storage/src/pages/mod.rs @@ -73,11 +73,11 @@ impl, ES: EdgeSegmentOps, E } } - pub fn nodes(&self) -> &NodeStorageInner { + pub fn nodes(&self) -> &Arc> { &self.nodes } - pub fn edges(&self) -> &EdgeStorageInner { + pub fn edges(&self) -> &Arc> { &self.edges } diff --git a/db4-storage/src/pages/node_store.rs b/db4-storage/src/pages/node_store.rs index fd881a5585..9cdd9b5de0 100644 --- a/db4-storage/src/pages/node_store.rs +++ b/db4-storage/src/pages/node_store.rs @@ -30,6 +30,16 @@ pub struct ReadLockedNodeStorage { locked_pages: Box<[ReadLockedNS]>, } +impl , EXT: Clone> ReadLockedNodeStorage { + + pub fn node(&self, node: impl Into) -> NS::Entry<'_> { + let (page_id, pos) = self.storage.resolve_pos(node); + let locked_page = self.locked_pages[page_id]; + locked_page.entry_ref(pos) + } + +} + impl, EXT: Clone> NodeStorageInner { pub fn locked(self: &Arc) -> ReadLockedNodeStorage { let locked_pages = self @@ -70,6 +80,10 @@ impl, EXT: Clone> NodeStorageInner // .collect(), // ) // } + + pub fn num_layers(&self) -> usize { + self.layer_counter.len() + } pub fn node<'a>(&'a self, node: impl Into) -> NS::Entry<'a> { let (page_id, pos) = self.resolve_pos(node); diff --git a/db4-storage/src/segments/edge_entry.rs b/db4-storage/src/segments/edge_entry.rs index da0b331a04..e3851651ba 100644 --- a/db4-storage/src/segments/edge_entry.rs +++ b/db4-storage/src/segments/edge_entry.rs @@ -5,6 +5,7 @@ use crate::{EdgeEntryOps, EdgeRefOps, LocalPOS}; use super::{additions::MemAdditions, edge::MemEdgeSegment}; +#[derive(Debug)] pub struct MemEdgeEntry<'a, MES> { pos: LocalPOS, es: MES, @@ -39,7 +40,7 @@ impl<'a, MES: std::ops::Deref> EdgeEntryOps<'a> for Mem } } -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Debug)] pub struct MemEdgeRef<'a> { pos: LocalPOS, es: &'a MemEdgeSegment, diff --git a/db4-storage/src/segments/node_entry.rs b/db4-storage/src/segments/node_entry.rs index cdf4f6cdba..b6a135b976 100644 --- a/db4-storage/src/segments/node_entry.rs +++ b/db4-storage/src/segments/node_entry.rs @@ -38,7 +38,7 @@ impl<'a, MNS: Deref> NodeEntryOps<'a> for MemNodeEntry< } } } -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Debug)] pub struct MemNodeRef<'a> { pos: LocalPOS, ns: &'a MemNodeSegment, diff --git a/raphtory-storage/src/graph/edges/edge_entry.rs b/raphtory-storage/src/graph/edges/edge_entry.rs index 3ae2d9fb78..79842291f7 100644 --- a/raphtory-storage/src/graph/edges/edge_entry.rs +++ b/raphtory-storage/src/graph/edges/edge_entry.rs @@ -6,7 +6,7 @@ use raphtory_api::core::entities::{ properties::{prop::Prop, tprop::TPropOps}, LayerIds, EID, VID, }; -use raphtory_core::{entities::edges::edge_store::MemEdge, storage::raw_edges::EdgeRGuard}; +use storage::{EdgeEntry, EdgeEntryRef, EdgeEntryOps}; use std::ops::Range; #[cfg(feature = "storage")] @@ -14,8 +14,8 @@ use crate::disk::graph_impl::DiskEdge; #[derive(Debug)] pub enum EdgeStorageEntry<'a> { - Mem(MemEdge<'a>), - Unlocked(EdgeRGuard<'a>), + Mem(EdgeEntryRef<'a>), + Unlocked(EdgeEntry<'a>), #[cfg(feature = "storage")] Disk(DiskEdge<'a>), } @@ -25,7 +25,7 @@ impl<'a> EdgeStorageEntry<'a> { pub fn as_ref(&self) -> EdgeStorageRef { match self { EdgeStorageEntry::Mem(edge) => EdgeStorageRef::Mem(*edge), - EdgeStorageEntry::Unlocked(edge) => EdgeStorageRef::Mem(edge.as_mem_edge()), + EdgeStorageEntry::Unlocked(edge) => EdgeStorageRef::Mem(edge.as_ref()), #[cfg(feature = "storage")] EdgeStorageEntry::Disk(edge) => EdgeStorageRef::Disk(*edge), } diff --git a/raphtory-storage/src/graph/edges/edge_ref.rs b/raphtory-storage/src/graph/edges/edge_ref.rs index dfbc9c3e03..57d9ae1402 100644 --- a/raphtory-storage/src/graph/edges/edge_ref.rs +++ b/raphtory-storage/src/graph/edges/edge_ref.rs @@ -4,6 +4,7 @@ use raphtory_api::core::entities::{ LayerIds, EID, VID, }; use raphtory_core::entities::edges::edge_store::MemEdge; +use storage::{EdgeEntry, EdgeEntryRef}; use std::ops::Range; #[cfg(feature = "storage")] @@ -40,7 +41,7 @@ macro_rules! for_all_iter { #[derive(Copy, Clone, Debug)] pub enum EdgeStorageRef<'a> { - Mem(MemEdge<'a>), + Mem(EdgeEntryRef<'a>), #[cfg(feature = "storage")] Disk(DiskEdge<'a>), } diff --git a/raphtory-storage/src/graph/edges/edges.rs b/raphtory-storage/src/graph/edges/edges.rs index d923e459f3..dfa6d2646a 100644 --- a/raphtory-storage/src/graph/edges/edges.rs +++ b/raphtory-storage/src/graph/edges/edges.rs @@ -6,6 +6,7 @@ use crate::graph::{ use raphtory_api::core::entities::{LayerIds, EID}; use raphtory_core::storage::raw_edges::LockedEdges; use rayon::iter::ParallelIterator; +use storage::{Extension, ReadLockedEdges}; use std::sync::Arc; #[cfg(feature = "storage")] @@ -13,7 +14,7 @@ use crate::disk::storage_interface::{edges::DiskEdges, edges_ref::DiskEdgesRef}; use crate::graph::variants::storage_variants2::StorageVariants2; pub enum EdgesStorage { - Mem(Arc), + Mem(Arc>), #[cfg(feature = "storage")] Disk(DiskEdges), } @@ -74,9 +75,9 @@ impl EdgesStorage { } #[derive(Debug, Copy, Clone)] -pub enum EdgesStorageRef<'a> { - Mem(&'a LockedEdges), - Unlocked(UnlockedEdges<'a>), +pub enum EdgesStorageRef<'a, EXT = Extension> { + Mem(&'a ReadLockedEdges), + Unlocked(UnlockedEdges<'a, EXT>), #[cfg(feature = "storage")] Disk(DiskEdgesRef<'a>), } diff --git a/raphtory-storage/src/graph/edges/unlocked.rs b/raphtory-storage/src/graph/edges/unlocked.rs index addf3d6358..5543c5a0c4 100644 --- a/raphtory-storage/src/graph/edges/unlocked.rs +++ b/raphtory-storage/src/graph/edges/unlocked.rs @@ -3,9 +3,10 @@ use raphtory_core::{ entities::graph::tgraph_storage::GraphStorage, storage::raw_edges::EdgeRGuard, }; use rayon::prelude::*; +use storage::{Extension, Layer}; #[derive(Copy, Clone, Debug)] -pub struct UnlockedEdges<'a>(pub(crate) &'a GraphStorage); +pub struct UnlockedEdges<'a, EXT = Extension>(pub(crate) &'a Layer); impl<'a> UnlockedEdges<'a> { pub fn iter(self) -> impl Iterator> + 'a { diff --git a/raphtory-storage/src/graph/graph.rs b/raphtory-storage/src/graph/graph.rs index 3ffe9d6f4f..93f377d6dc 100644 --- a/raphtory-storage/src/graph/graph.rs +++ b/raphtory-storage/src/graph/graph.rs @@ -7,7 +7,7 @@ use crate::graph::{ locked::LockedGraph, nodes::{nodes::NodesStorage, nodes_ref::NodesStorageEntry}, }; -use db4_graph::{ReadLockedTemporalGraph, TemporalGraph}; +use db4_graph::TemporalGraph; use raphtory_api::core::entities::{properties::meta::Meta, LayerIds, LayerVariants, EID, VID}; use raphtory_core::entities::{nodes::node_ref::NodeRef, properties::graph_meta::GraphMeta}; use std::{fmt::Debug, iter, sync::Arc}; @@ -25,7 +25,7 @@ use crate::mutation::MutationError; #[derive(Clone, Debug)] pub enum GraphStorage { - Mem(Arc), + Mem(LockedGraph), Unlocked(Arc), #[cfg(feature = "storage")] Disk(Arc), @@ -68,21 +68,16 @@ impl GraphStorage { /// Check if two storage instances point at the same underlying storage pub fn ptr_eq(&self, other: &Self) -> bool { match self { - GraphStorage::Mem(locked_graph) => match other { - GraphStorage::Mem(other_graph) => { - Arc::ptr_eq(locked_graph.graph(), other_graph.graph()) - } + GraphStorage::Mem(LockedGraph { + graph: this_graph, .. + }) + | GraphStorage::Unlocked(this_graph) => match other { + GraphStorage::Mem(LockedGraph { + graph: other_graph, .. + }) + | GraphStorage::Unlocked(other_graph) => Arc::ptr_eq(this_graph, other_graph), #[cfg(feature = "storage")] - GraphStorage::Disk(_) => false, - GraphStorage::Unlocked(other_graph) => { - Arc::ptr_eq(locked_graph.graph(), other_graph) - } - }, - GraphStorage::Unlocked(this_graph) => match other { - GraphStorage::Mem(other_graph) => Arc::ptr_eq(this_graph, other_graph.graph()), - GraphStorage::Unlocked(other_graph) => Arc::ptr_eq(this_graph, other_graph), - #[cfg(feature = "storage")] - GraphStorage::Disk(_) => false, + _ => false, }, #[cfg(feature = "storage")] GraphStorage::Disk(this_graph) => match other { @@ -114,7 +109,10 @@ impl GraphStorage { #[inline(always)] pub fn lock(&self) -> Self { match self { - GraphStorage::Unlocked(storage) => GraphStorage::Mem(storage.read_locked().into()), + GraphStorage::Unlocked(storage) => { + let locked = LockedGraph::new(storage.clone()); + GraphStorage::Mem(locked) + } _ => self.clone(), } } @@ -122,8 +120,10 @@ impl GraphStorage { #[inline(always)] pub fn nodes(&self) -> NodesStorageEntry { match self { - GraphStorage::Mem(storage) => NodesStorageEntry::Mem(storage.as_ref()), - GraphStorage::Unlocked(storage) => NodesStorageEntry::Unlocked(storage.read_locked()), + GraphStorage::Mem(storage) => NodesStorageEntry::Mem(&storage.nodes), + GraphStorage::Unlocked(storage) => { + NodesStorageEntry::Unlocked(storage.storage().nodes().locked()) + } #[cfg(feature = "storage")] GraphStorage::Disk(storage) => { NodesStorageEntry::Disk(DiskNodesRef::new(&storage.inner)) @@ -136,7 +136,7 @@ impl GraphStorage { match v { NodeRef::Internal(vid) => Some(vid), node_ref => match self { - GraphStorage::Mem(locked) => locked.graph().resolve_node_ref(node_ref), + GraphStorage::Mem(locked) => locked.graph.resolve_node_ref(node_ref), GraphStorage::Unlocked(unlocked) => unlocked.resolve_node_ref(node_ref), #[cfg(feature = "storage")] GraphStorage::Disk(storage) => match v { @@ -150,7 +150,7 @@ impl GraphStorage { #[inline(always)] pub fn unfiltered_num_nodes(&self) -> usize { match self { - GraphStorage::Mem(storage) => storage.graph().internal_num_nodes(), + GraphStorage::Mem(storage) => storage.graph.internal_num_nodes(), GraphStorage::Unlocked(storage) => storage.internal_num_nodes(), #[cfg(feature = "storage")] GraphStorage::Disk(storage) => storage.inner.num_nodes(), @@ -160,7 +160,7 @@ impl GraphStorage { #[inline(always)] pub fn unfiltered_num_edges(&self) -> usize { match self { - GraphStorage::Mem(storage) => storage.graph().internal_num_edges(), + GraphStorage::Mem(storage) => storage.graph.internal_num_edges(), GraphStorage::Unlocked(storage) => storage.internal_num_edges(), #[cfg(feature = "storage")] GraphStorage::Disk(storage) => storage.inner.count_edges(), @@ -170,7 +170,7 @@ impl GraphStorage { #[inline(always)] pub fn unfiltered_num_layers(&self) -> usize { match self { - GraphStorage::Mem(storage) => storage.graph().num_layers(), + GraphStorage::Mem(storage) => storage.graph.num_layers(), GraphStorage::Unlocked(storage) => storage.num_layers(), #[cfg(feature = "storage")] GraphStorage::Disk(storage) => storage.inner.layers().len(), @@ -192,7 +192,7 @@ impl GraphStorage { #[inline(always)] pub fn core_node<'a>(&'a self, vid: VID) -> NodeStorageEntry<'a> { match self { - GraphStorage::Mem(storage) => NodeStorageEntry::Mem(storage.node(vid)), + GraphStorage::Mem(storage) => NodeStorageEntry::Mem(storage.nodes.node(vid)), GraphStorage::Unlocked(storage) => NodeStorageEntry::Unlocked(storage.node(vid)), #[cfg(feature = "storage")] GraphStorage::Disk(storage) => { @@ -206,7 +206,7 @@ impl GraphStorage { match self { GraphStorage::Mem(storage) => EdgesStorageRef::Mem(&storage.edges), GraphStorage::Unlocked(storage) => { - EdgesStorageRef::Unlocked(UnlockedEdges(&storage.storage)) + EdgesStorageRef::Unlocked(UnlockedEdges(storage.storage())) } #[cfg(feature = "storage")] GraphStorage::Disk(storage) => EdgesStorageRef::Disk(DiskEdgesRef::new(&storage.inner)), @@ -218,7 +218,7 @@ impl GraphStorage { match self { GraphStorage::Mem(storage) => EdgesStorage::Mem(storage.edges.clone()), GraphStorage::Unlocked(storage) => { - GraphStorage::Mem(LockedGraph::new(storage.clone())).owned_edges() + EdgesStorage::Mem(storage.storage().edges().locked().into()) } #[cfg(feature = "storage")] GraphStorage::Disk(storage) => EdgesStorage::Disk(DiskEdges::new(storage)), @@ -229,9 +229,7 @@ impl GraphStorage { pub fn edge_entry(&self, eid: EID) -> EdgeStorageEntry { match self { GraphStorage::Mem(storage) => EdgeStorageEntry::Mem(storage.edges.get_mem(eid)), - GraphStorage::Unlocked(storage) => { - EdgeStorageEntry::Unlocked(storage.storage.edge_entry(eid)) - } + GraphStorage::Unlocked(storage) => EdgeStorageEntry::Unlocked(storage.edge_entry(eid)), #[cfg(feature = "storage")] GraphStorage::Disk(storage) => EdgeStorageEntry::Disk(storage.inner.edge(eid)), } diff --git a/raphtory-storage/src/graph/locked.rs b/raphtory-storage/src/graph/locked.rs index 86409e97b5..d4ea940c06 100644 --- a/raphtory-storage/src/graph/locked.rs +++ b/raphtory-storage/src/graph/locked.rs @@ -3,44 +3,45 @@ use raphtory_api::core::{ storage::dict_mapper::MaybeNew, }; use raphtory_core::{ - entities::graph::{logical_to_physical::InvalidNodeId, tgraph::TemporalGraph}, + entities::graph::logical_to_physical::InvalidNodeId, storage::{ - raw_edges::{LockedEdges, WriteLockedEdges}, - ReadLockedStorage, WriteLockedNodes, + raw_edges::WriteLockedEdges, + WriteLockedNodes, }, }; use std::sync::Arc; -use storage::ReadLockedLayer; +use db4_graph::TemporalGraph; +use storage::{Extension, ReadLockedEdges, ReadLockedNodes}; #[derive(Debug)] pub struct LockedGraph { - pub(crate) nodes: Arc, - pub(crate) edges: Arc, + pub(crate) nodes: Arc>, + pub(crate) edges: Arc>, pub graph: Arc, } -impl<'de> serde::Deserialize<'de> for LockedGraph { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - TemporalGraph::deserialize(deserializer).map(|graph| LockedGraph::new(Arc::new(graph))) - } -} - -impl serde::Serialize for LockedGraph { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - self.graph.serialize(serializer) - } -} +// impl<'de> serde::Deserialize<'de> for LockedGraph { +// fn deserialize(deserializer: D) -> Result +// where +// D: serde::Deserializer<'de>, +// { +// TemporalGraph::deserialize(deserializer).map(|graph| LockedGraph::new(Arc::new(graph))) +// } +// } +// +// impl serde::Serialize for LockedGraph { +// fn serialize(&self, serializer: S) -> Result +// where +// S: serde::Serializer, +// { +// self.graph.serialize(serializer) +// } +// } impl LockedGraph { pub fn new(graph: Arc) -> Self { - let nodes = Arc::new(graph.storage.nodes_read_lock()); - let edges = Arc::new(graph.storage.edges_read_lock()); + let nodes = Arc::new(graph.storage().nodes().locked()); + let edges = Arc::new(graph.storage().edges().locked()); Self { nodes, edges, diff --git a/raphtory-storage/src/graph/nodes/node_entry.rs b/raphtory-storage/src/graph/nodes/node_entry.rs index f4ce934888..b2e13c5ac0 100644 --- a/raphtory-storage/src/graph/nodes/node_entry.rs +++ b/raphtory-storage/src/graph/nodes/node_entry.rs @@ -2,10 +2,7 @@ use crate::graph::{ nodes::{node_ref::NodeStorageRef, node_storage_ops::NodeStorageOps}, variants::storage_variants3::StorageVariants3, }; -use db4_graph::{ - entries::node::{LockedNodeEntry, UnlockedNodeEntry}, - ReadLockedTemporalGraph, TemporalGraph, -}; +use db4_graph::TemporalGraph; use raphtory_api::{ core::{ entities::{ @@ -19,15 +16,15 @@ use raphtory_api::{ }; use raphtory_core::utils::iter::GenLockedIter; use std::borrow::Cow; -use storage::Extension; +use storage::{NodeEntry, NodeEntryRef}; #[cfg(feature = "storage")] use crate::disk::storage_interface::node::DiskNode; use crate::graph::nodes::node_additions::NodeAdditions; pub enum NodeStorageEntry<'a> { - Mem(LockedNodeEntry<'a, Extension>), - Unlocked(UnlockedNodeEntry<'a, Extension>), + Mem(NodeEntryRef<'a>), + Unlocked(NodeEntry<'a>), #[cfg(feature = "storage")] Disk(DiskNode<'a>), } diff --git a/raphtory-storage/src/graph/nodes/nodes_ref.rs b/raphtory-storage/src/graph/nodes/nodes_ref.rs index 9132440786..c0e536c679 100644 --- a/raphtory-storage/src/graph/nodes/nodes_ref.rs +++ b/raphtory-storage/src/graph/nodes/nodes_ref.rs @@ -1,19 +1,16 @@ -use std::sync::Arc; - use super::node_ref::NodeStorageRef; use crate::graph::variants::storage_variants3::StorageVariants3; -use db4_graph::{ReadLockedTemporalGraph, TemporalGraph}; use raphtory_api::core::entities::VID; use rayon::iter::ParallelIterator; -use storage::Extension; +use storage::{Extension, ReadLockedNodes}; #[cfg(feature = "storage")] use crate::disk::storage_interface::nodes_ref::DiskNodesRef; #[derive(Debug)] pub enum NodesStorageEntry<'a, EXT = Extension> { - Mem(&'a ReadLockedTemporalGraph), - Unlocked(ReadLockedTemporalGraph), + Mem(&'a ReadLockedNodes), + Unlocked(ReadLockedNodes), #[cfg(feature = "storage")] Disk(DiskNodesRef<'a>), } From 3bd91dc29c69810fcdd6978ef4e8967e75686afd Mon Sep 17 00:00:00 2001 From: Fadhil Abubaker Date: Thu, 19 Jun 2025 07:15:10 -0400 Subject: [PATCH 019/321] Store node id as a const prop (#2134) * Add store_node_id to AtomicAdditionOps * Call store_node_id in AdditionOps --- db4-storage/src/pages/session.rs | 22 ++++++++++++++++--- raphtory-storage/src/mutation/addition_ops.rs | 8 +++++++ .../src/mutation/addition_ops_ext.rs | 11 ++++++++++ raphtory/src/db/api/mutation/addition_ops.rs | 5 ++++- raphtory/src/db/api/storage/storage.rs | 4 ++++ 5 files changed, 46 insertions(+), 4 deletions(-) diff --git a/db4-storage/src/pages/session.rs b/db4-storage/src/pages/session.rs index 76190ba6bf..b27f6a943b 100644 --- a/db4-storage/src/pages/session.rs +++ b/db4-storage/src/pages/session.rs @@ -4,15 +4,16 @@ use super::{ GraphStore, edge_page::writer::EdgeWriter, node_page::writer::WriterPair, resolve_pos, }; use crate::{ - EdgeSegmentOps, NodeSegmentOps, - segments::{edge::MemEdgeSegment, node::MemNodeSegment}, + error::DBV4Error, segments::{edge::MemEdgeSegment, node::MemNodeSegment}, EdgeSegmentOps, NodeSegmentOps }; use raphtory_api::core::{entities::properties::prop::Prop, storage::dict_mapper::MaybeNew}; use raphtory_core::{ - entities::{EID, ELID, VID}, + entities::{GidRef, EID, ELID, VID}, storage::timeindex::AsTime, }; +const NODE_ID_CONST_PROP_NAME: &str = "_raphtory_node_id"; + pub struct WriteSession< 'a, MNS: DerefMut + 'a, @@ -221,4 +222,19 @@ impl< } } } + + pub fn store_node_id(&mut self, id: GidRef, vid: impl Into) -> Result<(), DBV4Error> { + // node ids go to const props in layer 0 + let layer = 0; + let prop_name = NODE_ID_CONST_PROP_NAME; + let prop_val = match id { + GidRef::U64(id) => Prop::U64(id), + GidRef::Str(id) => Prop::Str(id.into()), + }; + let props = vec![(prop_name, prop_val)]; + + self.graph.update_node_const_props(vid, layer, props)?; + + Ok(()) + } } diff --git a/raphtory-storage/src/mutation/addition_ops.rs b/raphtory-storage/src/mutation/addition_ops.rs index 800fcc9622..4024c97c09 100644 --- a/raphtory-storage/src/mutation/addition_ops.rs +++ b/raphtory-storage/src/mutation/addition_ops.rs @@ -66,6 +66,7 @@ pub trait InternalAdditionOps { } pub trait AtomicAdditionOps: Send + Sync { + /// add edge update fn internal_add_edge( &mut self, t: TimeIndexEntry, @@ -75,6 +76,9 @@ pub trait AtomicAdditionOps: Send + Sync { layer: usize, props: impl IntoIterator, ) -> MaybeNew; + + /// Sets id as a const prop within the node + fn store_node_id(&self, id: NodeRef, vid: impl Into); } pub trait SessionAdditionOps: Send + Sync { @@ -148,6 +152,10 @@ impl AtomicAdditionOps for TGWriteSession<'_> { ) -> MaybeNew { todo!("Atomic addition operations are not implemented for TGWriteSession"); } + + fn store_node_id(&self, id: NodeRef, vid: impl Into) { + todo!("set_node_id is not implemented for TGWriteSession"); + } } impl<'a> SessionAdditionOps for TGWriteSession<'a> { diff --git a/raphtory-storage/src/mutation/addition_ops_ext.rs b/raphtory-storage/src/mutation/addition_ops_ext.rs index ed4d356e86..3674d792a5 100644 --- a/raphtory-storage/src/mutation/addition_ops_ext.rs +++ b/raphtory-storage/src/mutation/addition_ops_ext.rs @@ -65,8 +65,19 @@ impl< self.layer.as_mut().map(|layer| { layer.add_edge_into_layer(t, src, dst, eid, lsn, props); }); + eid } + + fn store_node_id(&self, id: NodeRef, vid: impl Into) { + match id { + NodeRef::External(id) => { + let vid = vid.into(); + self.static_session.store_node_id(id, vid) + } + NodeRef::Internal(id) => Ok(()), + } + } } impl<'a, EXT: Send + Sync> SessionAdditionOps for UnlockedSession<'a, EXT> { diff --git a/raphtory/src/db/api/mutation/addition_ops.rs b/raphtory/src/db/api/mutation/addition_ops.rs index 9857a39b71..16b682484c 100644 --- a/raphtory/src/db/api/mutation/addition_ops.rs +++ b/raphtory/src/db/api/mutation/addition_ops.rs @@ -91,7 +91,7 @@ pub trait AdditionOps: StaticGraphViewOps + InternalAdditionOps> + StaticGraphViewOps> Addit let edge = add_edge_op.internal_add_edge(ti, src_id, dst_id, 0, layer_id, props); + add_edge_op.store_node_id(src.as_node_ref(), src_id); + add_edge_op.store_node_id(dst.as_node_ref(), dst_id); + Ok(EdgeView::new( self.clone(), EdgeRef::new_outgoing(edge.inner().edge, src_id, dst_id).at_layer(layer_id), diff --git a/raphtory/src/db/api/storage/storage.rs b/raphtory/src/db/api/storage/storage.rs index 187974de09..87310fbf5e 100644 --- a/raphtory/src/db/api/storage/storage.rs +++ b/raphtory/src/db/api/storage/storage.rs @@ -222,6 +222,10 @@ impl AtomicAdditionOps for StorageWriteSession<'_> { ) -> MaybeNew { todo!() } + + fn store_node_id(&self, id: NodeRef, vid: impl Into) { + todo!("set_node_id is not implemented for StorageWriteSession"); + } } impl<'a> SessionAdditionOps for StorageWriteSession<'a> { From 6833d55028805f4fb594ef5fbd267e149e4378c1 Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Thu, 19 Jun 2025 12:13:25 +0100 Subject: [PATCH 020/321] support for interop with GraphStorage # Conflicts: # db4-storage/src/pages/session.rs --- db4-storage/src/api/edges.rs | 124 ++++++++ db4-storage/src/api/mod.rs | 2 + db4-storage/src/api/nodes.rs | 161 ++++++++++ db4-storage/src/lib.rs | 280 +----------------- db4-storage/src/pages/edge_page/writer.rs | 2 +- db4-storage/src/pages/edge_store.rs | 20 +- db4-storage/src/pages/layer_counter.rs | 4 +- db4-storage/src/pages/locked/edges.rs | 3 +- db4-storage/src/pages/locked/nodes.rs | 3 +- db4-storage/src/pages/mod.rs | 12 +- db4-storage/src/pages/node_page/writer.rs | 3 +- db4-storage/src/pages/node_store.rs | 26 +- db4-storage/src/pages/session.rs | 4 +- db4-storage/src/pages/test_utils/checkers.rs | 8 +- db4-storage/src/pages/test_utils/fixtures.rs | 13 +- db4-storage/src/pages/test_utils/props.rs | 8 +- db4-storage/src/segments/edge.rs | 33 ++- db4-storage/src/segments/edge_entry.rs | 5 +- db4-storage/src/segments/node.rs | 25 +- db4-storage/src/segments/node_entry.rs | 6 +- .../src/graph/edges/edge_entry.rs | 2 +- raphtory-storage/src/graph/edges/edge_ref.rs | 2 +- raphtory-storage/src/graph/edges/edges.rs | 2 +- raphtory-storage/src/graph/graph.rs | 18 +- raphtory-storage/src/graph/locked.rs | 7 +- 25 files changed, 452 insertions(+), 321 deletions(-) create mode 100644 db4-storage/src/api/edges.rs create mode 100644 db4-storage/src/api/mod.rs create mode 100644 db4-storage/src/api/nodes.rs diff --git a/db4-storage/src/api/edges.rs b/db4-storage/src/api/edges.rs new file mode 100644 index 0000000000..b857853a0a --- /dev/null +++ b/db4-storage/src/api/edges.rs @@ -0,0 +1,124 @@ +use std::{ + ops::{Deref, DerefMut}, + path::Path, + sync::Arc, +}; + +use parking_lot::{RwLockReadGuard, RwLockWriteGuard, lock_api::ArcRwLockReadGuard}; +use raphtory_api::core::entities::properties::{meta::Meta, prop::Prop, tprop::TPropOps}; +use raphtory_core::{ + entities::VID, + storage::timeindex::{TimeIndexEntry, TimeIndexOps}, +}; + +use crate::{LocalPOS, error::DBV4Error, segments::edge::MemEdgeSegment}; + +pub trait EdgeSegmentOps: Send + Sync + std::fmt::Debug { + type Extension; + + type Entry<'a>: EdgeEntryOps<'a> + where + Self: 'a; + + type ArcLockedSegment: LockedESegment; + + fn latest(&self) -> Option; + fn earliest(&self) -> Option; + + fn t_len(&self) -> usize; + + fn load( + page_id: usize, + max_page_len: usize, + meta: Arc, + path: impl AsRef, + ext: Self::Extension, + ) -> Result + where + Self: Sized; + + fn new( + page_id: usize, + max_page_len: usize, + meta: Arc, + path: impl AsRef, + ext: Self::Extension, + ) -> Self; + + fn segment_id(&self) -> usize; + + fn num_edges(&self) -> usize; + + fn head(&self) -> RwLockReadGuard; + + fn head_arc(&self) -> ArcRwLockReadGuard; + + fn head_mut(&self) -> RwLockWriteGuard; + + fn try_head_mut(&self) -> Option>; + + fn notify_write( + &self, + head_lock: impl DerefMut, + ) -> Result<(), DBV4Error>; + + fn increment_num_edges(&self) -> usize; + + fn contains_edge( + &self, + edge_pos: LocalPOS, + layer_id: usize, + locked_head: impl Deref, + ) -> bool; + + fn get_edge( + &self, + edge_pos: LocalPOS, + layer_id: usize, + locked_head: impl Deref, + ) -> Option<(VID, VID)>; + + fn entry<'a, LP: Into>(&'a self, edge_pos: LP) -> Self::Entry<'a>; + + fn locked(self: &Arc) -> Self::ArcLockedSegment; +} + +pub trait LockedESegment: Send + Sync + std::fmt::Debug { + type EntryRef<'a>: EdgeRefOps<'a> + where + Self: 'a; + + fn entry_ref<'a>(&'a self, edge_pos: impl Into) -> Self::EntryRef<'a> + where + Self: 'a; +} + +#[derive(Debug)] +pub struct ReadLockedES { + es: Arc, + head: ES::ArcLockedSegment, +} + +pub trait EdgeEntryOps<'a> { + type Ref<'b>: EdgeRefOps<'b> + where + 'a: 'b, + Self: 'b; + + fn as_ref<'b>(&'b self) -> Self::Ref<'b> + where + 'a: 'b; +} + +pub trait EdgeRefOps<'a>: Copy + Clone + Send + Sync { + type Additions: TimeIndexOps<'a>; + type TProps: TPropOps<'a>; + + fn edge(self, layer_id: usize) -> Option<(VID, VID)>; + + fn additions(self, layer_id: usize) -> Self::Additions; + + fn c_prop(self, layer_id: usize, prop_id: usize) -> Option; + + fn t_prop(self, layer_id: usize, prop_id: usize) -> Self::TProps; +} diff --git a/db4-storage/src/api/mod.rs b/db4-storage/src/api/mod.rs new file mode 100644 index 0000000000..bd58cef13c --- /dev/null +++ b/db4-storage/src/api/mod.rs @@ -0,0 +1,2 @@ +pub mod edges; +pub mod nodes; diff --git a/db4-storage/src/api/nodes.rs b/db4-storage/src/api/nodes.rs new file mode 100644 index 0000000000..7077c075dc --- /dev/null +++ b/db4-storage/src/api/nodes.rs @@ -0,0 +1,161 @@ +use std::{ + ops::{Deref, DerefMut}, + path::Path, + sync::Arc, +}; + +use parking_lot::{RwLockReadGuard, RwLockWriteGuard, lock_api::ArcRwLockReadGuard}; +use raphtory_api::core::entities::properties::{meta::Meta, prop::Prop, tprop::TPropOps}; +use raphtory_core::{ + entities::{EID, VID}, + storage::timeindex::{TimeIndexEntry, TimeIndexOps}, +}; + +use crate::{LocalPOS, error::DBV4Error, segments::node::MemNodeSegment}; + +pub trait NodeSegmentOps: Send + Sync + std::fmt::Debug { + type Extension; + + type Entry<'a>: NodeEntryOps<'a> + where + Self: 'a; + + type ArcLockedSegment: LockedNSSegment; + + fn latest(&self) -> Option; + fn earliest(&self) -> Option; + + fn t_len(&self) -> usize; + + fn load( + page_id: usize, + max_page_len: usize, + meta: Arc, + path: impl AsRef, + ext: Self::Extension, + ) -> Result + where + Self: Sized; + fn new( + page_id: usize, + max_page_len: usize, + meta: Arc, + path: impl AsRef, + ext: Self::Extension, + ) -> Self; + + fn segment_id(&self) -> usize; + + fn head_arc(&self) -> ArcRwLockReadGuard; + fn head(&self) -> RwLockReadGuard; + + fn head_mut(&self) -> RwLockWriteGuard; + + fn num_nodes(&self) -> usize { + self.layer_num_nodes(0) + } + + fn num_layers(&self) -> usize; + + fn layer_num_nodes(&self, layer_id: usize) -> usize; + + fn notify_write( + &self, + head_lock: impl DerefMut, + ) -> Result<(), DBV4Error>; + + fn check_node(&self, pos: LocalPOS, layer_id: usize) -> bool; + + fn get_out_edge( + &self, + pos: LocalPOS, + dst: impl Into, + layer_id: usize, + locked_head: impl Deref, + ) -> Option; + + fn get_inb_edge( + &self, + pos: LocalPOS, + src: impl Into, + layer_id: usize, + locked_head: impl Deref, + ) -> Option; + + fn entry<'a>(&'a self, pos: impl Into) -> Self::Entry<'a>; + + fn locked(self: &Arc) -> Self::ArcLockedSegment; +} + +pub trait LockedNSSegment: std::fmt::Debug + Send + Sync { + type EntryRef<'a>: NodeRefOps<'a> + where + Self: 'a; + + fn entry_ref<'a>(&'a self, pos: impl Into) -> Self::EntryRef<'a>; +} + +#[derive(Debug)] +pub struct ReadLockedNS { + ns: Arc, + head: NS::ArcLockedSegment, +} + +pub trait NodeEntryOps<'a> { + type Ref<'b>: NodeRefOps<'b> + where + 'a: 'b, + Self: 'b; + + fn as_ref<'b>(&'b self) -> Self::Ref<'b> + where + 'a: 'b; +} + +pub trait NodeRefOps<'a>: Copy + Clone + Send + Sync { + type Additions: TimeIndexOps<'a>; + + type TProps: TPropOps<'a>; + + fn out_edges(self, layer_id: usize) -> impl Iterator + 'a; + + fn inb_edges(self, layer_id: usize) -> impl Iterator + 'a; + + fn out_edges_sorted(self, layer_id: usize) -> impl Iterator + 'a; + + fn inb_edges_sorted(self, layer_id: usize) -> impl Iterator + 'a; + + fn out_nbrs(self, layer_id: usize) -> impl Iterator + 'a + where + Self: Sized, + { + self.out_edges(layer_id).map(|(v, _)| v) + } + + fn inb_nbrs(self, layer_id: usize) -> impl Iterator + 'a + where + Self: Sized, + { + self.inb_edges(layer_id).map(|(v, _)| v) + } + + fn out_nbrs_sorted(self, layer_id: usize) -> impl Iterator + 'a + where + Self: Sized, + { + self.out_edges_sorted(layer_id).map(|(v, _)| v) + } + + fn inb_nbrs_sorted(self, layer_id: usize) -> impl Iterator + 'a + where + Self: Sized, + { + self.inb_edges_sorted(layer_id).map(|(v, _)| v) + } + + fn additions(self, layer_id: usize) -> Self::Additions; + + fn c_prop(self, layer_id: usize, prop_id: usize) -> Option; + + fn t_prop(self, layer_id: usize, prop_id: usize) -> Self::TProps; +} diff --git a/db4-storage/src/lib.rs b/db4-storage/src/lib.rs index 891b08ec75..3af394bd3a 100644 --- a/db4-storage/src/lib.rs +++ b/db4-storage/src/lib.rs @@ -6,8 +6,16 @@ use std::{ use crate::{ error::DBV4Error, - pages::{GraphStore, ReadLockedGraphStore}, - segments::{edge::EdgeSegmentView, edge_entry::{MemEdgeEntry, MemEdgeRef}, node::NodeSegmentView, node_entry::{MemNodeEntry, MemNodeRef}}, + pages::{ + GraphStore, ReadLockedGraphStore, edge_store::ReadLockedEdgeStorage, + node_store::ReadLockedNodeStorage, + }, + segments::{ + edge::EdgeSegmentView, + edge_entry::{MemEdgeEntry, MemEdgeRef}, + node::NodeSegmentView, + node_entry::{MemNodeEntry, MemNodeRef}, + }, }; use parking_lot::{RwLockReadGuard, RwLockWriteGuard, lock_api::ArcRwLockReadGuard}; use raphtory_api::core::{ @@ -18,15 +26,14 @@ use raphtory_api::core::{ storage::timeindex::{TimeIndexEntry, TimeIndexOps}, }; use segments::{edge::MemEdgeSegment, node::MemNodeSegment}; -use crate::pages::edge_store::ReadLockedEdgeStorage; -use crate::pages::node_store::ReadLockedNodeStorage; -// pub mod loaders; pub mod pages; pub mod persist; pub mod properties; pub mod segments; +pub mod api; + pub type Extension = (); pub type NS

= NodeSegmentView

; pub type ES

= EdgeSegmentView

; @@ -41,269 +48,6 @@ pub type EdgeEntry<'a> = MemEdgeEntry<'a, parking_lot::RwLockReadGuard<'a, MemEd pub type NodeEntryRef<'a> = MemNodeRef<'a>; pub type EdgeEntryRef<'a> = MemEdgeRef<'a>; -pub trait EdgeSegmentOps: Send + Sync + std::fmt::Debug { - type Extension; - - type Entry<'a>: EdgeEntryOps<'a> - where - Self: 'a; - - fn latest(&self) -> Option; - fn earliest(&self) -> Option; - - fn t_len(&self) -> usize; - - fn load( - page_id: usize, - max_page_len: usize, - meta: Arc, - path: impl AsRef, - ext: Self::Extension, - ) -> Result - where - Self: Sized; - - fn new( - page_id: usize, - max_page_len: usize, - meta: Arc, - path: impl AsRef, - ext: Self::Extension, - ) -> Self; - - fn segment_id(&self) -> usize; - - fn num_edges(&self) -> usize; - - fn head(&self) -> RwLockReadGuard; - - fn head_arc(&self) -> ArcRwLockReadGuard; - - fn head_mut(&self) -> RwLockWriteGuard; - - fn try_head_mut(&self) -> Option>; - - fn notify_write( - &self, - head_lock: impl DerefMut, - ) -> Result<(), DBV4Error>; - - fn increment_num_edges(&self) -> usize; - - fn contains_edge( - &self, - edge_pos: LocalPOS, - layer_id: usize, - locked_head: impl Deref, - ) -> bool; - - fn get_edge( - &self, - edge_pos: LocalPOS, - layer_id: usize, - locked_head: impl Deref, - ) -> Option<(VID, VID)>; - - fn entry<'a, LP: Into>(&'a self, edge_pos: LP) -> Self::Entry<'a>; - - fn locked(self: &Arc) -> ReadLockedES - where - Self: Sized, - { - ReadLockedES { - es: self.clone(), - head: self.head_arc(), - } - } -} - -#[derive(Debug)] -pub struct ReadLockedES { - es: Arc, - head: ArcRwLockReadGuard, -} - -pub trait EdgeEntryOps<'a> { - type Ref<'b>: EdgeRefOps<'b> - where - 'a: 'b, - Self: 'b; - - fn as_ref<'b>(&'b self) -> Self::Ref<'b> - where - 'a: 'b; -} - -pub trait EdgeRefOps<'a>: Copy + Clone + Send + Sync { - type Additions: TimeIndexOps<'a>; - type TProps: TPropOps<'a>; - - fn edge(self, layer_id: usize) -> Option<(VID, VID)>; - - fn additions(self, layer_id: usize) -> Self::Additions; - - fn c_prop(self, layer_id: usize, prop_id: usize) -> Option; - - fn t_prop(self, layer_id: usize, prop_id: usize) -> Self::TProps; -} - -pub trait NodeSegmentOps: Send + Sync + std::fmt::Debug { - type Extension; - - type Entry<'a>: NodeEntryOps<'a> - where - Self: 'a; - - type EntryRef<'a>: NodeRefOps<'a> - where - Self: 'a; - - fn latest(&self) -> Option; - fn earliest(&self) -> Option; - - fn t_len(&self) -> usize; - - fn load( - page_id: usize, - max_page_len: usize, - meta: Arc, - path: impl AsRef, - ext: Self::Extension, - ) -> Result - where - Self: Sized; - fn new( - page_id: usize, - max_page_len: usize, - meta: Arc, - path: impl AsRef, - ext: Self::Extension, - ) -> Self; - - fn segment_id(&self) -> usize; - - fn head_arc(&self) -> ArcRwLockReadGuard; - fn head(&self) -> RwLockReadGuard; - - fn head_mut(&self) -> RwLockWriteGuard; - - fn num_nodes(&self) -> usize { - self.layer_num_nodes(0) - } - - fn num_layers(&self) -> usize; - - fn layer_num_nodes(&self, layer_id: usize) -> usize; - - fn notify_write( - &self, - head_lock: impl DerefMut, - ) -> Result<(), DBV4Error>; - - fn check_node(&self, pos: LocalPOS, layer_id: usize) -> bool; - - fn get_out_edge( - &self, - pos: LocalPOS, - dst: impl Into, - layer_id: usize, - locked_head: impl Deref, - ) -> Option; - - fn get_inb_edge( - &self, - pos: LocalPOS, - src: impl Into, - layer_id: usize, - locked_head: impl Deref, - ) -> Option; - - fn entry<'a>(&'a self, pos: impl Into) -> Self::Entry<'a>; - - fn locked(self: &Arc) -> ReadLockedNS - where - Self: Sized, - { - ReadLockedNS { - ns: self.clone(), - head: self.head_arc(), - } - } -} - -#[derive(Debug)] -pub struct ReadLockedNS { - ns: Arc, - head: ArcRwLockReadGuard, -} - -impl > ReadLockedNS { - - pub fn entry_ref<'a>(&'a self, pos: impl Into) -> NS::EntryRef<'a> { - self.ns.entry(pos) - } - -} - -pub trait NodeEntryOps<'a> { - type Ref<'b>: NodeRefOps<'b> - where - 'a: 'b, - Self: 'b; - - fn as_ref<'b>(&'b self) -> Self::Ref<'b> - where - 'a: 'b; -} - -pub trait NodeRefOps<'a>: Copy + Clone + Send + Sync { - type Additions: TimeIndexOps<'a>; - - type TProps: TPropOps<'a>; - - fn out_edges(self, layer_id: usize) -> impl Iterator + 'a; - - fn inb_edges(self, layer_id: usize) -> impl Iterator + 'a; - - fn out_edges_sorted(self, layer_id: usize) -> impl Iterator + 'a; - - fn inb_edges_sorted(self, layer_id: usize) -> impl Iterator + 'a; - - fn out_nbrs(self, layer_id: usize) -> impl Iterator + 'a - where - Self: Sized, - { - self.out_edges(layer_id).map(|(v, _)| v) - } - - fn inb_nbrs(self, layer_id: usize) -> impl Iterator + 'a - where - Self: Sized, - { - self.inb_edges(layer_id).map(|(v, _)| v) - } - - fn out_nbrs_sorted(self, layer_id: usize) -> impl Iterator + 'a - where - Self: Sized, - { - self.out_edges_sorted(layer_id).map(|(v, _)| v) - } - - fn inb_nbrs_sorted(self, layer_id: usize) -> impl Iterator + 'a - where - Self: Sized, - { - self.inb_edges_sorted(layer_id).map(|(v, _)| v) - } - - fn additions(self, layer_id: usize) -> Self::Additions; - - fn c_prop(self, layer_id: usize, prop_id: usize) -> Option; - - fn t_prop(self, layer_id: usize, prop_id: usize) -> Self::TProps; -} - pub mod error { use std::{path::PathBuf, sync::Arc}; diff --git a/db4-storage/src/pages/edge_page/writer.rs b/db4-storage/src/pages/edge_page/writer.rs index 2747f9ff32..516e2525ea 100644 --- a/db4-storage/src/pages/edge_page/writer.rs +++ b/db4-storage/src/pages/edge_page/writer.rs @@ -1,6 +1,6 @@ use std::{ops::DerefMut, sync::atomic::AtomicUsize}; -use crate::{EdgeSegmentOps, LocalPOS, segments::edge::MemEdgeSegment}; +use crate::{LocalPOS, api::edges::EdgeSegmentOps, segments::edge::MemEdgeSegment}; use raphtory_api::core::entities::{VID, properties::prop::Prop}; use raphtory_core::storage::timeindex::AsTime; diff --git a/db4-storage/src/pages/edge_store.rs b/db4-storage/src/pages/edge_store.rs index 77b78f8240..9e2f584617 100644 --- a/db4-storage/src/pages/edge_store.rs +++ b/db4-storage/src/pages/edge_store.rs @@ -9,7 +9,10 @@ use std::{ use super::{edge_page::writer::EdgeWriter, resolve_pos}; use crate::{ - EdgeSegmentOps, LocalPOS, ReadLockedES, error::DBV4Error, segments::edge::MemEdgeSegment, + LocalPOS, + api::edges::{EdgeSegmentOps, LockedESegment}, + error::DBV4Error, + segments::edge::MemEdgeSegment, }; use parking_lot::{RwLock, RwLockWriteGuard}; use raphtory_api::core::entities::{EID, VID, properties::meta::Meta}; @@ -29,9 +32,20 @@ pub struct EdgeStorageInner { } #[derive(Debug)] -pub struct ReadLockedEdgeStorage { +pub struct ReadLockedEdgeStorage, EXT> { storage: Arc>, - locked_pages: Box<[ReadLockedES]>, + locked_pages: Box<[ES::ArcLockedSegment]>, +} + +impl, EXT: Clone> ReadLockedEdgeStorage { + pub fn edge_ref( + &self, + e_id: impl Into, + ) -> <::ArcLockedSegment as LockedESegment>::EntryRef<'_> { + let (page_id, pos) = self.storage.resolve_pos(e_id.into()); + let locked_page = &self.locked_pages[page_id]; + locked_page.entry_ref(pos) + } } impl, EXT: Clone> EdgeStorageInner { diff --git a/db4-storage/src/pages/layer_counter.rs b/db4-storage/src/pages/layer_counter.rs index 48b1826048..23aa68a5c0 100644 --- a/db4-storage/src/pages/layer_counter.rs +++ b/db4-storage/src/pages/layer_counter.rs @@ -5,7 +5,7 @@ pub struct LayerCounter { layers: boxcar::Vec, } -impl > From for LayerCounter { +impl> From for LayerCounter { fn from(iter: I) -> Self { let counts = iter.into_iter().map(|c| AtomicUsize::new(c)).collect(); let layers = boxcar::Vec::from(counts); @@ -25,7 +25,7 @@ impl LayerCounter { } Self { layers } } - + pub fn len(&self) -> usize { self.layers.count() } diff --git a/db4-storage/src/pages/locked/edges.rs b/db4-storage/src/pages/locked/edges.rs index ab59943bb7..487813797b 100644 --- a/db4-storage/src/pages/locked/edges.rs +++ b/db4-storage/src/pages/locked/edges.rs @@ -1,7 +1,8 @@ use std::{ops::DerefMut, sync::atomic::AtomicUsize}; use crate::{ - EdgeSegmentOps, LocalPOS, + LocalPOS, + api::edges::EdgeSegmentOps, pages::{edge_page::writer::EdgeWriter, resolve_pos}, segments::edge::MemEdgeSegment, }; diff --git a/db4-storage/src/pages/locked/nodes.rs b/db4-storage/src/pages/locked/nodes.rs index 05bd9c16a7..259a769852 100644 --- a/db4-storage/src/pages/locked/nodes.rs +++ b/db4-storage/src/pages/locked/nodes.rs @@ -1,5 +1,6 @@ use crate::{ - LocalPOS, NodeSegmentOps, + LocalPOS, + api::nodes::NodeSegmentOps, pages::{layer_counter::LayerCounter, node_page::writer::NodeWriter, resolve_pos}, segments::node::MemNodeSegment, }; diff --git a/db4-storage/src/pages/mod.rs b/db4-storage/src/pages/mod.rs index bd6c1f0071..a914954b09 100644 --- a/db4-storage/src/pages/mod.rs +++ b/db4-storage/src/pages/mod.rs @@ -8,7 +8,8 @@ use std::{ }; use crate::{ - EdgeSegmentOps, LocalPOS, NodeSegmentOps, + LocalPOS, + api::{edges::EdgeSegmentOps, nodes::NodeSegmentOps}, error::DBV4Error, pages::{edge_store::ReadLockedEdgeStorage, node_store::ReadLockedNodeStorage}, properties::props_meta_writer::PropsMetaWriter, @@ -54,7 +55,11 @@ pub struct GraphStore { } #[derive(Debug)] -pub struct ReadLockedGraphStore { +pub struct ReadLockedGraphStore< + NS: NodeSegmentOps, + ES: EdgeSegmentOps, + EXT, +> { nodes: ReadLockedNodeStorage, edges: ReadLockedEdgeStorage, graph: Arc>, @@ -480,7 +485,8 @@ pub fn resolve_pos>(i: I, max_page_len: usize) -> (usize, mod test { use super::GraphStore; use crate::{ - Layer, NodeEntryOps, NodeRefOps, + Layer, + api::nodes::{NodeEntryOps, NodeRefOps}, pages::test_utils::{ AddEdge, Fixture, NodeFixture, check_edges_support, check_graph_with_nodes_support, check_graph_with_props_support, edges_strat, edges_strat_with_layers, make_edges, diff --git a/db4-storage/src/pages/node_page/writer.rs b/db4-storage/src/pages/node_page/writer.rs index 6bf30766fe..6335b28f2a 100644 --- a/db4-storage/src/pages/node_page/writer.rs +++ b/db4-storage/src/pages/node_page/writer.rs @@ -1,5 +1,6 @@ use crate::{ - LocalPOS, NodeSegmentOps, pages::layer_counter::LayerCounter, segments::node::MemNodeSegment, + LocalPOS, api::nodes::NodeSegmentOps, pages::layer_counter::LayerCounter, + segments::node::MemNodeSegment, }; use raphtory_api::core::entities::{EID, VID, properties::prop::Prop}; use raphtory_core::{entities::ELID, storage::timeindex::AsTime}; diff --git a/db4-storage/src/pages/node_store.rs b/db4-storage/src/pages/node_store.rs index 9cdd9b5de0..bb3b79c9a7 100644 --- a/db4-storage/src/pages/node_store.rs +++ b/db4-storage/src/pages/node_store.rs @@ -1,6 +1,9 @@ use super::{node_page::writer::NodeWriter, resolve_pos}; use crate::{ - LocalPOS, NodeSegmentOps, ReadLockedNS, error::DBV4Error, pages::layer_counter::LayerCounter, + LocalPOS, + api::nodes::{LockedNSSegment, NodeSegmentOps}, + error::DBV4Error, + pages::layer_counter::LayerCounter, segments::node::MemNodeSegment, }; use parking_lot::RwLockWriteGuard; @@ -25,31 +28,32 @@ pub struct NodeStorageInner { } #[derive(Debug)] -pub struct ReadLockedNodeStorage { +pub struct ReadLockedNodeStorage, EXT> { storage: Arc>, - locked_pages: Box<[ReadLockedNS]>, + locked_segments: Box<[NS::ArcLockedSegment]>, } -impl , EXT: Clone> ReadLockedNodeStorage { - - pub fn node(&self, node: impl Into) -> NS::Entry<'_> { +impl, EXT: Send + Sync + Clone> ReadLockedNodeStorage { + pub fn node_ref( + &self, + node: impl Into, + ) -> <::ArcLockedSegment as LockedNSSegment>::EntryRef<'_> { let (page_id, pos) = self.storage.resolve_pos(node); - let locked_page = self.locked_pages[page_id]; + let locked_page = &self.locked_segments[page_id]; locked_page.entry_ref(pos) } - } impl, EXT: Clone> NodeStorageInner { pub fn locked(self: &Arc) -> ReadLockedNodeStorage { - let locked_pages = self + let locked_segments = self .pages .iter() .map(|(_, segment)| segment.locked()) .collect::>(); ReadLockedNodeStorage { storage: self.clone(), - locked_pages, + locked_segments, } } @@ -80,7 +84,7 @@ impl, EXT: Clone> NodeStorageInner // .collect(), // ) // } - + pub fn num_layers(&self) -> usize { self.layer_counter.len() } diff --git a/db4-storage/src/pages/session.rs b/db4-storage/src/pages/session.rs index b27f6a943b..3b3733a4d6 100644 --- a/db4-storage/src/pages/session.rs +++ b/db4-storage/src/pages/session.rs @@ -4,7 +4,9 @@ use super::{ GraphStore, edge_page::writer::EdgeWriter, node_page::writer::WriterPair, resolve_pos, }; use crate::{ - error::DBV4Error, segments::{edge::MemEdgeSegment, node::MemNodeSegment}, EdgeSegmentOps, NodeSegmentOps + api::{edges::EdgeSegmentOps, nodes::NodeSegmentOps}, + segments::{edge::MemEdgeSegment, node::MemNodeSegment}, + error::DBV4Error, }; use raphtory_api::core::{entities::properties::prop::Prop, storage::dict_mapper::MaybeNew}; use raphtory_core::{ diff --git a/db4-storage/src/pages/test_utils/checkers.rs b/db4-storage/src/pages/test_utils/checkers.rs index 9930f7fa5a..00b6429bde 100644 --- a/db4-storage/src/pages/test_utils/checkers.rs +++ b/db4-storage/src/pages/test_utils/checkers.rs @@ -15,8 +15,12 @@ use raphtory_core::{ use rayon::prelude::*; use crate::{ - EdgeEntryOps, EdgeRefOps, EdgeSegmentOps, NodeEntryOps, NodeRefOps, NodeSegmentOps, - error::DBV4Error, pages::GraphStore, + api::{ + edges::{EdgeEntryOps, EdgeRefOps, EdgeSegmentOps}, + nodes::{NodeEntryOps, NodeRefOps, NodeSegmentOps}, + }, + error::DBV4Error, + pages::GraphStore, }; use super::fixtures::{AddEdge, Fixture, NodeFixture}; diff --git a/db4-storage/src/pages/test_utils/fixtures.rs b/db4-storage/src/pages/test_utils/fixtures.rs index 257dcda713..ed4f313116 100644 --- a/db4-storage/src/pages/test_utils/fixtures.rs +++ b/db4-storage/src/pages/test_utils/fixtures.rs @@ -1,7 +1,7 @@ -use std::collections::HashMap; use proptest::{collection, prelude::*}; -use raphtory_core::entities::VID; use raphtory_api::core::entities::properties::prop::Prop; +use raphtory_core::entities::VID; +use std::collections::HashMap; use super::props::{make_props, prop_type}; @@ -89,7 +89,9 @@ pub fn edges_strat(size: usize) -> impl Strategy> { }) } -pub fn edges_strat_with_layers(size: usize) -> impl Strategy)>> { +pub fn edges_strat_with_layers( + size: usize, +) -> impl Strategy)>> { const MAX_LAYERS: usize = 16; (1..=size).prop_flat_map(|num_nodes| { @@ -99,7 +101,10 @@ pub fn edges_strat_with_layers(size: usize) -> impl Strategy impl Strategy { let leaf = proptest::sample::select(&[ diff --git a/db4-storage/src/segments/edge.rs b/db4-storage/src/segments/edge.rs index 73ed13e824..46ed456a1f 100644 --- a/db4-storage/src/segments/edge.rs +++ b/db4-storage/src/segments/edge.rs @@ -13,7 +13,13 @@ use raphtory_api::core::entities::{ }; use raphtory_core::storage::timeindex::{AsTime, TimeIndexEntry}; -use crate::{EdgeSegmentOps, LocalPOS, error::DBV4Error, properties::PropMutEntry}; +use crate::{ + LocalPOS, + api::edges::{EdgeSegmentOps, LockedESegment}, + error::DBV4Error, + properties::PropMutEntry, + segments::edge_entry::MemEdgeRef, +}; use super::{HasRow, SegmentContainer, edge_entry::MemEdgeEntry}; @@ -251,11 +257,30 @@ pub struct EdgeSegmentView { _ext: EXT, } +#[derive(Debug)] +pub struct ArcLockedSegmentView { + inner: ArcRwLockReadGuard, +} + +impl LockedESegment for ArcLockedSegmentView { + type EntryRef<'a> = MemEdgeRef<'a>; + + fn entry_ref<'a>(&'a self, edge_pos: impl Into) -> Self::EntryRef<'a> + where + Self: 'a, + { + let edge_pos = edge_pos.into(); + MemEdgeRef::new(edge_pos, &self.inner) + } +} + impl EdgeSegmentOps for EdgeSegmentView { type Extension = (); type Entry<'a> = MemEdgeEntry<'a, parking_lot::RwLockReadGuard<'a, MemEdgeSegment>>; + type ArcLockedSegment = ArcLockedSegmentView; + fn latest(&self) -> Option { self.head().latest() } @@ -354,4 +379,10 @@ impl EdgeSegmentOps for EdgeSegmentView { let edge_pos = edge_pos.into(); MemEdgeEntry::new(edge_pos, self.head()) } + + fn locked(self: &Arc) -> Self::ArcLockedSegment { + ArcLockedSegmentView { + inner: self.head_arc(), + } + } } diff --git a/db4-storage/src/segments/edge_entry.rs b/db4-storage/src/segments/edge_entry.rs index e3851651ba..797a04bff4 100644 --- a/db4-storage/src/segments/edge_entry.rs +++ b/db4-storage/src/segments/edge_entry.rs @@ -1,7 +1,10 @@ use raphtory_api::core::entities::properties::prop::Prop; use raphtory_core::entities::{VID, properties::tprop::TPropCell}; -use crate::{EdgeEntryOps, EdgeRefOps, LocalPOS}; +use crate::{ + LocalPOS, + api::edges::{EdgeEntryOps, EdgeRefOps}, +}; use super::{additions::MemAdditions, edge::MemEdgeSegment}; diff --git a/db4-storage/src/segments/node.rs b/db4-storage/src/segments/node.rs index b09544ab5f..5fa8b6938f 100644 --- a/db4-storage/src/segments/node.rs +++ b/db4-storage/src/segments/node.rs @@ -18,7 +18,8 @@ use std::{ use super::{HasRow, SegmentContainer}; use crate::{ - LocalPOS, NodeSegmentOps, + LocalPOS, + api::nodes::{LockedNSSegment, NodeSegmentOps}, error::DBV4Error, segments::node_entry::{MemNodeEntry, MemNodeRef}, }; @@ -278,11 +279,27 @@ pub struct NodeSegmentView { _ext: EXT, } +#[derive(Debug)] +pub struct ArcLockedSegmentView { + inner: ArcRwLockReadGuard, +} + +impl LockedNSSegment for ArcLockedSegmentView { + type EntryRef<'a> = MemNodeRef<'a>; + + fn entry_ref<'a>(&'a self, pos: impl Into) -> Self::EntryRef<'a> { + let pos = pos.into(); + MemNodeRef::new(pos, &self.inner) + } +} + impl NodeSegmentOps for NodeSegmentView { type Extension = (); type Entry<'a> = MemNodeEntry<'a, parking_lot::RwLockReadGuard<'a, MemNodeSegment>>; + type ArcLockedSegment = ArcLockedSegmentView; + fn latest(&self) -> Option { self.head().latest() } @@ -375,6 +392,12 @@ impl NodeSegmentOps for NodeSegmentView { MemNodeEntry::new(pos, self.head()) } + fn locked(self: &Arc) -> Self::ArcLockedSegment { + ArcLockedSegmentView { + inner: self.inner.read_arc(), + } + } + fn num_layers(&self) -> usize { self.head().layers.len() } diff --git a/db4-storage/src/segments/node_entry.rs b/db4-storage/src/segments/node_entry.rs index b6a135b976..de1097851a 100644 --- a/db4-storage/src/segments/node_entry.rs +++ b/db4-storage/src/segments/node_entry.rs @@ -1,4 +1,8 @@ -use crate::{LocalPOS, NodeEntryOps, NodeRefOps, segments::node::MemNodeSegment}; +use crate::{ + LocalPOS, + api::nodes::{NodeEntryOps, NodeRefOps}, + segments::node::MemNodeSegment, +}; use raphtory_api::core::entities::{EID, VID, properties::prop::Prop}; use raphtory_core::entities::properties::tprop::TPropCell; use std::ops::Deref; diff --git a/raphtory-storage/src/graph/edges/edge_entry.rs b/raphtory-storage/src/graph/edges/edge_entry.rs index 79842291f7..98eb92d31d 100644 --- a/raphtory-storage/src/graph/edges/edge_entry.rs +++ b/raphtory-storage/src/graph/edges/edge_entry.rs @@ -6,8 +6,8 @@ use raphtory_api::core::entities::{ properties::{prop::Prop, tprop::TPropOps}, LayerIds, EID, VID, }; -use storage::{EdgeEntry, EdgeEntryRef, EdgeEntryOps}; use std::ops::Range; +use storage::{EdgeEntry, EdgeEntryOps, EdgeEntryRef}; #[cfg(feature = "storage")] use crate::disk::graph_impl::DiskEdge; diff --git a/raphtory-storage/src/graph/edges/edge_ref.rs b/raphtory-storage/src/graph/edges/edge_ref.rs index 57d9ae1402..1ee424acba 100644 --- a/raphtory-storage/src/graph/edges/edge_ref.rs +++ b/raphtory-storage/src/graph/edges/edge_ref.rs @@ -4,8 +4,8 @@ use raphtory_api::core::entities::{ LayerIds, EID, VID, }; use raphtory_core::entities::edges::edge_store::MemEdge; -use storage::{EdgeEntry, EdgeEntryRef}; use std::ops::Range; +use storage::{EdgeEntry, EdgeEntryRef}; #[cfg(feature = "storage")] use crate::{disk::graph_impl::DiskEdge, graph::variants::storage_variants2::StorageVariants2}; diff --git a/raphtory-storage/src/graph/edges/edges.rs b/raphtory-storage/src/graph/edges/edges.rs index dfa6d2646a..be37ed47cf 100644 --- a/raphtory-storage/src/graph/edges/edges.rs +++ b/raphtory-storage/src/graph/edges/edges.rs @@ -6,8 +6,8 @@ use crate::graph::{ use raphtory_api::core::entities::{LayerIds, EID}; use raphtory_core::storage::raw_edges::LockedEdges; use rayon::iter::ParallelIterator; -use storage::{Extension, ReadLockedEdges}; use std::sync::Arc; +use storage::{Extension, ReadLockedEdges}; #[cfg(feature = "storage")] use crate::disk::storage_interface::{edges::DiskEdges, edges_ref::DiskEdgesRef}; diff --git a/raphtory-storage/src/graph/graph.rs b/raphtory-storage/src/graph/graph.rs index 93f377d6dc..a2e55ce6dd 100644 --- a/raphtory-storage/src/graph/graph.rs +++ b/raphtory-storage/src/graph/graph.rs @@ -192,8 +192,10 @@ impl GraphStorage { #[inline(always)] pub fn core_node<'a>(&'a self, vid: VID) -> NodeStorageEntry<'a> { match self { - GraphStorage::Mem(storage) => NodeStorageEntry::Mem(storage.nodes.node(vid)), - GraphStorage::Unlocked(storage) => NodeStorageEntry::Unlocked(storage.node(vid)), + GraphStorage::Mem(storage) => NodeStorageEntry::Mem(storage.nodes.node_ref(vid)), + GraphStorage::Unlocked(storage) => { + NodeStorageEntry::Unlocked(storage.storage().nodes().node(vid)) + } #[cfg(feature = "storage")] GraphStorage::Disk(storage) => { NodeStorageEntry::Disk(DiskNode::new(&storage.inner, vid)) @@ -228,8 +230,10 @@ impl GraphStorage { #[inline(always)] pub fn edge_entry(&self, eid: EID) -> EdgeStorageEntry { match self { - GraphStorage::Mem(storage) => EdgeStorageEntry::Mem(storage.edges.get_mem(eid)), - GraphStorage::Unlocked(storage) => EdgeStorageEntry::Unlocked(storage.edge_entry(eid)), + GraphStorage::Mem(storage) => EdgeStorageEntry::Mem(storage.edges.edge_ref(eid)), + GraphStorage::Unlocked(storage) => { + EdgeStorageEntry::Unlocked(storage.storage().edges().edge(eid)) + } #[cfg(feature = "storage")] GraphStorage::Disk(storage) => EdgeStorageEntry::Disk(storage.inner.edge(eid)), } @@ -625,7 +629,7 @@ impl GraphStorage { pub fn node_meta(&self) -> &Meta { match self { - GraphStorage::Mem(storage) => storage.graph().node_meta(), + GraphStorage::Mem(storage) => storage.graph.node_meta(), GraphStorage::Unlocked(storage) => storage.node_meta(), #[cfg(feature = "storage")] GraphStorage::Disk(storage) => storage.node_meta(), @@ -634,7 +638,7 @@ impl GraphStorage { pub fn edge_meta(&self) -> &Meta { match self { - GraphStorage::Mem(storage) => storage.graph().edge_meta(), + GraphStorage::Mem(storage) => storage.graph.edge_meta(), GraphStorage::Unlocked(storage) => storage.edge_meta(), #[cfg(feature = "storage")] GraphStorage::Disk(storage) => storage.edge_meta(), @@ -643,7 +647,7 @@ impl GraphStorage { pub fn graph_meta(&self) -> &GraphMeta { match self { - GraphStorage::Mem(storage) => storage.graph().graph_meta(), + GraphStorage::Mem(storage) => storage.graph.graph_meta(), GraphStorage::Unlocked(storage) => storage.graph_meta(), #[cfg(feature = "storage")] GraphStorage::Disk(storage) => storage.graph_meta(), diff --git a/raphtory-storage/src/graph/locked.rs b/raphtory-storage/src/graph/locked.rs index d4ea940c06..5c8752f18d 100644 --- a/raphtory-storage/src/graph/locked.rs +++ b/raphtory-storage/src/graph/locked.rs @@ -1,16 +1,13 @@ +use db4_graph::TemporalGraph; use raphtory_api::core::{ entities::{GidRef, VID}, storage::dict_mapper::MaybeNew, }; use raphtory_core::{ entities::graph::logical_to_physical::InvalidNodeId, - storage::{ - raw_edges::WriteLockedEdges, - WriteLockedNodes, - }, + storage::{raw_edges::WriteLockedEdges, WriteLockedNodes}, }; use std::sync::Arc; -use db4_graph::TemporalGraph; use storage::{Extension, ReadLockedEdges, ReadLockedNodes}; #[derive(Debug)] From 614a010354ae52a2d22e3277c1252ae84a897ceb Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Thu, 19 Jun 2025 13:00:56 +0100 Subject: [PATCH 021/321] make nodes_ref compile in graph storage --- db4-storage/src/pages/mod.rs | 10 +++---- db4-storage/src/pages/node_store.rs | 28 +++++++++++++++++++ raphtory-storage/src/graph/graph.rs | 4 +-- .../src/graph/nodes/node_entry.rs | 25 ++++++++--------- raphtory-storage/src/graph/nodes/node_ref.rs | 11 ++++++-- raphtory-storage/src/graph/nodes/nodes.rs | 11 ++++---- raphtory-storage/src/graph/nodes/nodes_ref.rs | 10 +++---- 7 files changed, 66 insertions(+), 33 deletions(-) diff --git a/db4-storage/src/pages/mod.rs b/db4-storage/src/pages/mod.rs index a914954b09..45a3ad7ef6 100644 --- a/db4-storage/src/pages/mod.rs +++ b/db4-storage/src/pages/mod.rs @@ -60,17 +60,17 @@ pub struct ReadLockedGraphStore< ES: EdgeSegmentOps, EXT, > { - nodes: ReadLockedNodeStorage, - edges: ReadLockedEdgeStorage, - graph: Arc>, + pub nodes: Arc>, + pub edges: Arc>, + pub graph: Arc>, } impl, ES: EdgeSegmentOps, EXT: Clone + Default> GraphStore { pub fn read_locked(self: &Arc) -> ReadLockedGraphStore { - let nodes = self.nodes.locked(); - let edges = self.edges.locked(); + let nodes = self.nodes.locked().into(); + let edges = self.edges.locked().into(); ReadLockedGraphStore { nodes, edges, diff --git a/db4-storage/src/pages/node_store.rs b/db4-storage/src/pages/node_store.rs index bb3b79c9a7..c6b34b0ba8 100644 --- a/db4-storage/src/pages/node_store.rs +++ b/db4-storage/src/pages/node_store.rs @@ -9,6 +9,7 @@ use crate::{ use parking_lot::RwLockWriteGuard; use raphtory_api::core::entities::properties::meta::Meta; use raphtory_core::entities::{EID, VID}; +use rayon::prelude::*; use std::{ collections::HashMap, path::{Path, PathBuf}, @@ -42,6 +43,33 @@ impl, EXT: Send + Sync + Clone> ReadLockedNo let locked_page = &self.locked_segments[page_id]; locked_page.entry_ref(pos) } + + pub fn len(&self) -> usize { + self.storage.num_nodes() + } + + pub fn iter( + &self, + ) -> impl Iterator< + Item = <::ArcLockedSegment as LockedNSSegment>::EntryRef<'_>, + > + '_ { + (0..self.len()).map(move |i| { + let vid = VID(i); + self.node_ref(vid) + }) + } + + pub fn par_iter( + &self, + ) -> impl rayon::iter::ParallelIterator< + Item = <::ArcLockedSegment as LockedNSSegment>::EntryRef<'_>, + > + '_ { + + (0..self.len()).into_par_iter().map(move |i| { + let vid = VID(i); + self.node_ref(vid) + }) + } } impl, EXT: Clone> NodeStorageInner { diff --git a/raphtory-storage/src/graph/graph.rs b/raphtory-storage/src/graph/graph.rs index a2e55ce6dd..aaeb16289b 100644 --- a/raphtory-storage/src/graph/graph.rs +++ b/raphtory-storage/src/graph/graph.rs @@ -180,8 +180,8 @@ impl GraphStorage { #[inline(always)] pub fn core_nodes(&self) -> NodesStorage { match self { - GraphStorage::Mem(storage) => NodesStorage::Mem(storage.clone()), - GraphStorage::Unlocked(storage) => NodesStorage::Mem(storage.read_locked().into()), + GraphStorage::Mem(storage) => NodesStorage::Mem(storage.nodes.clone()), + GraphStorage::Unlocked(storage) => NodesStorage::Mem(storage.read_locked().nodes.clone()), #[cfg(feature = "storage")] GraphStorage::Disk(storage) => { NodesStorage::Disk(DiskNodesOwned::new(storage.inner.clone())) diff --git a/raphtory-storage/src/graph/nodes/node_entry.rs b/raphtory-storage/src/graph/nodes/node_entry.rs index b2e13c5ac0..4fdb62dad8 100644 --- a/raphtory-storage/src/graph/nodes/node_entry.rs +++ b/raphtory-storage/src/graph/nodes/node_entry.rs @@ -2,7 +2,6 @@ use crate::graph::{ nodes::{node_ref::NodeStorageRef, node_storage_ops::NodeStorageOps}, variants::storage_variants3::StorageVariants3, }; -use db4_graph::TemporalGraph; use raphtory_api::{ core::{ entities::{ @@ -16,7 +15,7 @@ use raphtory_api::{ }; use raphtory_core::utils::iter::GenLockedIter; use std::borrow::Cow; -use storage::{NodeEntry, NodeEntryRef}; +use storage::{api::nodes::NodeEntryOps, NodeEntry, NodeEntryRef}; #[cfg(feature = "storage")] use crate::disk::storage_interface::node::DiskNode; @@ -29,17 +28,17 @@ pub enum NodeStorageEntry<'a> { Disk(DiskNode<'a>), } -// impl<'a> From> for NodeStorageEntry<'a> { -// fn from(value: NodePtr<'a>) -> Self { -// NodeStorageEntry::Mem(value) -// } -// } - -// impl<'a> From> for NodeStorageEntry<'a> { -// fn from(value: NodeEntry<'a>) -> Self { -// NodeStorageEntry::Unlocked(value) -// } -// } +impl<'a> From> for NodeStorageEntry<'a> { + fn from(value: NodeEntryRef<'a>) -> Self { + NodeStorageEntry::Mem(value) + } +} + +impl<'a> From> for NodeStorageEntry<'a> { + fn from(value: NodeEntry<'a>) -> Self { + NodeStorageEntry::Unlocked(value) + } +} #[cfg(feature = "storage")] impl<'a> From> for NodeStorageEntry<'a> { diff --git a/raphtory-storage/src/graph/nodes/node_ref.rs b/raphtory-storage/src/graph/nodes/node_ref.rs index 2e1b7b134f..5f5749d2bf 100644 --- a/raphtory-storage/src/graph/nodes/node_ref.rs +++ b/raphtory-storage/src/graph/nodes/node_ref.rs @@ -3,7 +3,6 @@ use crate::graph::{ nodes::{node_additions::NodeAdditions, node_storage_ops::NodeStorageOps}, variants::storage_variants2::StorageVariants2, }; -use db4_graph::{entries::node::LockedNodeEntry, ReadLockedTemporalGraph}; use raphtory_api::{ core::{ entities::{ @@ -18,18 +17,24 @@ use raphtory_api::{ }; use raphtory_core::storage::node_entry::NodePtr; use std::{borrow::Cow, ops::Range}; -use storage::Extension; +use storage::NodeEntryRef; #[cfg(feature = "storage")] use crate::disk::storage_interface::node::DiskNode; #[derive(Copy, Clone, Debug)] pub enum NodeStorageRef<'a> { - Mem(LockedNodeEntry<'a, Extension>), + Mem(NodeEntryRef<'a>), #[cfg(feature = "storage")] Disk(DiskNode<'a>), } +impl<'a> From> for NodeStorageRef<'a> { + fn from(value: NodeEntryRef<'a>) -> Self { + NodeStorageRef::Mem(value) + } +} + impl<'a> NodeStorageRef<'a> { pub fn temp_prop_rows(self) -> impl Iterator)> + 'a { match self { diff --git a/raphtory-storage/src/graph/nodes/nodes.rs b/raphtory-storage/src/graph/nodes/nodes.rs index f028a0030f..89bf5b3ee0 100644 --- a/raphtory-storage/src/graph/nodes/nodes.rs +++ b/raphtory-storage/src/graph/nodes/nodes.rs @@ -1,14 +1,15 @@ +use std::sync::Arc; + use super::node_ref::NodeStorageRef; use crate::graph::nodes::nodes_ref::NodesStorageEntry; -use db4_graph::ReadLockedTemporalGraph; use raphtory_api::core::entities::VID; -use std::sync::Arc; +use storage::{Extension, ReadLockedNodes}; #[cfg(feature = "storage")] use crate::disk::storage_interface::nodes::DiskNodesOwned; pub enum NodesStorage { - Mem(Arc), + Mem(Arc>), #[cfg(feature = "storage")] Disk(DiskNodesOwned), } @@ -17,7 +18,7 @@ impl NodesStorage { #[inline] pub fn as_ref(&self) -> NodesStorageEntry { match self { - NodesStorage::Mem(storage) => NodesStorageEntry::Mem(storage.as_ref()), + NodesStorage::Mem(storage) => NodesStorageEntry::Mem(&storage), #[cfg(feature = "storage")] NodesStorage::Disk(storage) => NodesStorageEntry::Disk(storage.as_ref()), } @@ -26,7 +27,7 @@ impl NodesStorage { #[inline] pub fn node_entry(&self, vid: VID) -> NodeStorageRef { match self { - NodesStorage::Mem(storage) => NodeStorageRef::Mem(storage.node(vid)), + NodesStorage::Mem(storage) => NodeStorageRef::Mem(storage.node_ref(vid)), #[cfg(feature = "storage")] NodesStorage::Disk(storage) => NodeStorageRef::Disk(storage.node(vid)), } diff --git a/raphtory-storage/src/graph/nodes/nodes_ref.rs b/raphtory-storage/src/graph/nodes/nodes_ref.rs index c0e536c679..969649e718 100644 --- a/raphtory-storage/src/graph/nodes/nodes_ref.rs +++ b/raphtory-storage/src/graph/nodes/nodes_ref.rs @@ -8,9 +8,9 @@ use storage::{Extension, ReadLockedNodes}; use crate::disk::storage_interface::nodes_ref::DiskNodesRef; #[derive(Debug)] -pub enum NodesStorageEntry<'a, EXT = Extension> { - Mem(&'a ReadLockedNodes), - Unlocked(ReadLockedNodes), +pub enum NodesStorageEntry<'a> { + Mem(&'a ReadLockedNodes), + Unlocked(ReadLockedNodes), #[cfg(feature = "storage")] Disk(DiskNodesRef<'a>), } @@ -29,8 +29,8 @@ macro_rules! for_all_variants { impl<'a> NodesStorageEntry<'a> { pub fn node(&self, vid: VID) -> NodeStorageRef<'_> { match self { - NodesStorageEntry::Mem(store) => NodeStorageRef::Mem(store.node(vid)), - NodesStorageEntry::Unlocked(store) => NodeStorageRef::Mem(store.node(vid)), + NodesStorageEntry::Mem(store) => NodeStorageRef::Mem(store.node_ref(vid)), + NodesStorageEntry::Unlocked(store) => NodeStorageRef::Mem(store.node_ref(vid)), #[cfg(feature = "storage")] NodesStorageEntry::Disk(store) => NodeStorageRef::Disk(store.node(vid)), } From bbfa29212d66fca7144b6980b4c00eb659e2210c Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Thu, 19 Jun 2025 14:11:35 +0100 Subject: [PATCH 022/321] node_entry in graph storage compiles --- db4-storage/src/api/nodes.rs | 103 ++++++++++++++++-- db4-storage/src/lib.rs | 20 +--- db4-storage/src/pages/node_store.rs | 1 - db4-storage/src/pages/session.rs | 4 +- db4-storage/src/segments/additions.rs | 9 +- db4-storage/src/segments/node.rs | 19 +++- db4-storage/src/segments/node_entry.rs | 8 +- db4-storage/src/utils.rs | 52 +++++++++ raphtory-storage/src/graph/graph.rs | 4 +- .../src/graph/nodes/node_entry.rs | 53 ++++----- 10 files changed, 211 insertions(+), 62 deletions(-) create mode 100644 db4-storage/src/utils.rs diff --git a/db4-storage/src/api/nodes.rs b/db4-storage/src/api/nodes.rs index 7077c075dc..a384a93751 100644 --- a/db4-storage/src/api/nodes.rs +++ b/db4-storage/src/api/nodes.rs @@ -4,14 +4,27 @@ use std::{ sync::Arc, }; +use itertools::Itertools; use parking_lot::{RwLockReadGuard, RwLockWriteGuard, lock_api::ArcRwLockReadGuard}; -use raphtory_api::core::entities::properties::{meta::Meta, prop::Prop, tprop::TPropOps}; +use raphtory_api::{ + core::{ + Direction, + entities::properties::{meta::Meta, prop::Prop, tprop::TPropOps}, + }, + iter::IntoDynBoxed, +}; use raphtory_core::{ - entities::{EID, VID}, + entities::{EID, LayerIds, Multiple, VID, edges::edge_ref::EdgeRef}, storage::timeindex::{TimeIndexEntry, TimeIndexOps}, + utils::iter::GenLockedIter, }; -use crate::{LocalPOS, error::DBV4Error, segments::node::MemNodeSegment}; +use crate::{ + LocalPOS, + error::DBV4Error, + segments::node::MemNodeSegment, + utils::{Iter3, Iter4}, +}; pub trait NodeSegmentOps: Send + Sync + std::fmt::Debug { type Extension; @@ -101,7 +114,7 @@ pub struct ReadLockedNS { head: NS::ArcLockedSegment, } -pub trait NodeEntryOps<'a> { +pub trait NodeEntryOps<'a>: Send + Sync + 'a { type Ref<'b>: NodeRefOps<'b> where 'a: 'b, @@ -110,6 +123,19 @@ pub trait NodeEntryOps<'a> { fn as_ref<'b>(&'b self) -> Self::Ref<'b> where 'a: 'b; + + fn into_edges<'b: 'a>( + self, + layers: &'b LayerIds, + dir: Direction, + ) -> impl Iterator + Send + Sync + 'a + where + Self: Sized, + { + GenLockedIter::from((self, layers), |(e, layers)| { + e.as_ref().edges_iter(layers, dir).into_dyn_boxed() + }) + } } pub trait NodeRefOps<'a>: Copy + Clone + Send + Sync { @@ -117,13 +143,74 @@ pub trait NodeRefOps<'a>: Copy + Clone + Send + Sync { type TProps: TPropOps<'a>; - fn out_edges(self, layer_id: usize) -> impl Iterator + 'a; + fn out_edges(self, layer_id: usize) -> impl Iterator + Send + Sync + 'a; + + fn inb_edges(self, layer_id: usize) -> impl Iterator + Send + Sync + 'a; - fn inb_edges(self, layer_id: usize) -> impl Iterator + 'a; + fn out_edges_sorted( + self, + layer_id: usize, + ) -> impl Iterator + Send + Sync + 'a; + + fn inb_edges_sorted( + self, + layer_id: usize, + ) -> impl Iterator + Send + Sync + 'a; - fn out_edges_sorted(self, layer_id: usize) -> impl Iterator + 'a; + fn vid(&self) -> VID; - fn inb_edges_sorted(self, layer_id: usize) -> impl Iterator + 'a; + fn edges_dir( + self, + layer_id: usize, + dir: Direction, + ) -> impl Iterator + Send + Sync + 'a + where + Self: Sized, + { + let src_pid = self.vid(); + match dir { + Direction::OUT => Iter3::I( + self.out_edges(layer_id) + .map(move |(v, e)| EdgeRef::new_outgoing(e, src_pid, v)), + ), + Direction::IN => Iter3::J( + self.inb_edges(layer_id) + .map(move |(v, e)| EdgeRef::new_incoming(e, v, src_pid)), + ), + Direction::BOTH => Iter3::K( + self.out_edges(layer_id) + .map(move |(v, e)| EdgeRef::new_outgoing(e, src_pid, v)) + .merge_by( + self.inb_edges(layer_id) + .map(move |(v, e)| EdgeRef::new_incoming(e, v, src_pid)), + |e1, e2| e1.remote() < e2.remote(), + ) + .dedup(), + ), + } + } + + fn edges_iter<'b>( + self, + layers_ids: &'b LayerIds, + dir: Direction, + ) -> impl Iterator + Send + Sync + 'a + where + Self: Sized, + { + match layers_ids { + LayerIds::One(layer_id) => Iter4::I(self.edges_dir(*layer_id, dir)), + LayerIds::All => Iter4::J(self.edges_dir(0, dir)), + LayerIds::Multiple(layers) => Iter4::K( + layers + .into_iter() + .map(|layer_id| self.edges_dir(layer_id, dir)) + .kmerge_by(|e1, e2| e1.remote() < e2.remote()) + .dedup(), + ), + LayerIds::None => Iter4::L(std::iter::empty()), + } + } fn out_nbrs(self, layer_id: usize) -> impl Iterator + 'a where diff --git a/db4-storage/src/lib.rs b/db4-storage/src/lib.rs index 3af394bd3a..1ce6592194 100644 --- a/db4-storage/src/lib.rs +++ b/db4-storage/src/lib.rs @@ -1,11 +1,6 @@ -use std::{ - ops::{Deref, DerefMut}, - path::Path, - sync::Arc, -}; +use std::path::Path; use crate::{ - error::DBV4Error, pages::{ GraphStore, ReadLockedGraphStore, edge_store::ReadLockedEdgeStorage, node_store::ReadLockedNodeStorage, @@ -17,22 +12,15 @@ use crate::{ node_entry::{MemNodeEntry, MemNodeRef}, }, }; -use parking_lot::{RwLockReadGuard, RwLockWriteGuard, lock_api::ArcRwLockReadGuard}; -use raphtory_api::core::{ - entities::{ - EID, VID, - properties::{meta::Meta, prop::Prop, tprop::TPropOps}, - }, - storage::timeindex::{TimeIndexEntry, TimeIndexOps}, -}; +use raphtory_api::core::entities::{EID, VID}; use segments::{edge::MemEdgeSegment, node::MemNodeSegment}; +pub mod api; pub mod pages; pub mod persist; pub mod properties; pub mod segments; - -pub mod api; +pub mod utils; pub type Extension = (); pub type NS

= NodeSegmentView

; diff --git a/db4-storage/src/pages/node_store.rs b/db4-storage/src/pages/node_store.rs index c6b34b0ba8..c38f211918 100644 --- a/db4-storage/src/pages/node_store.rs +++ b/db4-storage/src/pages/node_store.rs @@ -64,7 +64,6 @@ impl, EXT: Send + Sync + Clone> ReadLockedNo ) -> impl rayon::iter::ParallelIterator< Item = <::ArcLockedSegment as LockedNSSegment>::EntryRef<'_>, > + '_ { - (0..self.len()).into_par_iter().map(move |i| { let vid = VID(i); self.node_ref(vid) diff --git a/db4-storage/src/pages/session.rs b/db4-storage/src/pages/session.rs index 3b3733a4d6..39c1d348df 100644 --- a/db4-storage/src/pages/session.rs +++ b/db4-storage/src/pages/session.rs @@ -5,12 +5,12 @@ use super::{ }; use crate::{ api::{edges::EdgeSegmentOps, nodes::NodeSegmentOps}, - segments::{edge::MemEdgeSegment, node::MemNodeSegment}, error::DBV4Error, + segments::{edge::MemEdgeSegment, node::MemNodeSegment}, }; use raphtory_api::core::{entities::properties::prop::Prop, storage::dict_mapper::MaybeNew}; use raphtory_core::{ - entities::{GidRef, EID, ELID, VID}, + entities::{EID, ELID, GidRef, VID}, storage::timeindex::AsTime, }; diff --git a/db4-storage/src/segments/additions.rs b/db4-storage/src/segments/additions.rs index 9699f3672b..2b2041349e 100644 --- a/db4-storage/src/segments/additions.rs +++ b/db4-storage/src/segments/additions.rs @@ -1,11 +1,12 @@ use std::ops::Range; -use iter_enum::{DoubleEndedIterator, ExactSizeIterator, FusedIterator, Iterator}; use raphtory_core::{ entities::nodes::node_store::PropTimestamps, storage::timeindex::{TimeIndexEntry, TimeIndexOps, TimeIndexWindow}, }; +use crate::utils::Iter2; + #[derive(Clone, Debug)] pub enum MemAdditions<'a> { Props(&'a PropTimestamps), @@ -52,9 +53,3 @@ impl<'a> TimeIndexOps<'a> for MemAdditions<'a> { } } } - -#[derive(Clone, Debug, Iterator, DoubleEndedIterator, ExactSizeIterator, FusedIterator)] -pub enum Iter2 { - I1(I1), - I2(I2), -} diff --git a/db4-storage/src/segments/node.rs b/db4-storage/src/segments/node.rs index 5fa8b6938f..13feeb0f14 100644 --- a/db4-storage/src/segments/node.rs +++ b/db4-storage/src/segments/node.rs @@ -26,13 +26,24 @@ use crate::{ #[derive(Debug)] pub struct MemNodeSegment { + segment_id: usize, + max_page_len: usize, layers: Vec>, } impl>> From for MemNodeSegment { fn from(inner: I) -> Self { + let layers = inner.into_iter().collect::>(); + assert!( + !layers.is_empty(), + "MemNodeSegment must have at least one layer" + ); + let segment_id = layers[0].segment_id(); + let max_page_len = layers[0].max_page_len(); Self { - layers: inner.into_iter().collect(), + segment_id, + max_page_len, + layers, } } } @@ -104,6 +115,10 @@ impl MemNodeSegment { self.layers.iter().map(|seg| seg.est_size()).sum::() } + pub fn to_vid(&self, pos: LocalPOS) -> VID { + pos.as_vid(self.segment_id, self.max_page_len) + } + #[inline(always)] fn get_adj(&self, n: LocalPOS, layer_id: usize) -> Option<&Adj> { self.layers @@ -142,6 +157,8 @@ impl MemNodeSegment { pub fn new(segment_id: usize, max_page_len: usize, meta: Arc) -> Self { Self { + segment_id, + max_page_len, layers: vec![SegmentContainer::new(segment_id, max_page_len, meta)], } } diff --git a/db4-storage/src/segments/node_entry.rs b/db4-storage/src/segments/node_entry.rs index de1097851a..c1721ab245 100644 --- a/db4-storage/src/segments/node_entry.rs +++ b/db4-storage/src/segments/node_entry.rs @@ -25,7 +25,9 @@ impl<'a, MNS: Deref> MemNodeEntry<'a, MNS> { } } -impl<'a, MNS: Deref> NodeEntryOps<'a> for MemNodeEntry<'a, MNS> { +impl<'a, MNS: Deref + Send + Sync + 'a> NodeEntryOps<'a> + for MemNodeEntry<'a, MNS> +{ type Ref<'b> = MemNodeRef<'b> where @@ -58,6 +60,10 @@ impl<'a> NodeRefOps<'a> for MemNodeRef<'a> { type Additions = MemAdditions<'a>; type TProps = TPropCell<'a>; + fn vid(&self) -> VID { + self.ns.to_vid(self.pos) + } + fn out_edges(self, layer_id: usize) -> impl Iterator + 'a { self.ns.out_edges(self.pos, layer_id) } diff --git a/db4-storage/src/utils.rs b/db4-storage/src/utils.rs new file mode 100644 index 0000000000..9a28e2d86e --- /dev/null +++ b/db4-storage/src/utils.rs @@ -0,0 +1,52 @@ +use iter_enum::{ + DoubleEndedIterator, ExactSizeIterator, FusedIterator, IndexedParallelIterator, Iterator, + ParallelIterator, +}; + +#[derive( + Clone, + Debug, + Iterator, + DoubleEndedIterator, + ExactSizeIterator, + ParallelIterator, + IndexedParallelIterator, + FusedIterator, +)] +pub enum Iter2 { + I1(I1), + I2(I2), +} + +#[derive( + Copy, + Clone, + Iterator, + ExactSizeIterator, + DoubleEndedIterator, + ParallelIterator, + IndexedParallelIterator, + FusedIterator, +)] +pub enum Iter3 { + I(I), + J(J), + K(K), +} + +#[derive( + Copy, + Clone, + Iterator, + ExactSizeIterator, + DoubleEndedIterator, + ParallelIterator, + IndexedParallelIterator, + FusedIterator, +)] +pub enum Iter4 { + I(I), + J(J), + K(K), + L(L), +} diff --git a/raphtory-storage/src/graph/graph.rs b/raphtory-storage/src/graph/graph.rs index aaeb16289b..52254234c1 100644 --- a/raphtory-storage/src/graph/graph.rs +++ b/raphtory-storage/src/graph/graph.rs @@ -181,7 +181,9 @@ impl GraphStorage { pub fn core_nodes(&self) -> NodesStorage { match self { GraphStorage::Mem(storage) => NodesStorage::Mem(storage.nodes.clone()), - GraphStorage::Unlocked(storage) => NodesStorage::Mem(storage.read_locked().nodes.clone()), + GraphStorage::Unlocked(storage) => { + NodesStorage::Mem(storage.read_locked().nodes.clone()) + } #[cfg(feature = "storage")] GraphStorage::Disk(storage) => { NodesStorage::Disk(DiskNodesOwned::new(storage.inner.clone())) diff --git a/raphtory-storage/src/graph/nodes/node_entry.rs b/raphtory-storage/src/graph/nodes/node_entry.rs index 4fdb62dad8..e68845d9b2 100644 --- a/raphtory-storage/src/graph/nodes/node_entry.rs +++ b/raphtory-storage/src/graph/nodes/node_entry.rs @@ -15,7 +15,10 @@ use raphtory_api::{ }; use raphtory_core::utils::iter::GenLockedIter; use std::borrow::Cow; -use storage::{api::nodes::NodeEntryOps, NodeEntry, NodeEntryRef}; +use storage::{ + api::nodes::{NodeEntryOps, NodeRefOps}, + NodeEntry, NodeEntryRef, +}; #[cfg(feature = "storage")] use crate::disk::storage_interface::node::DiskNode; @@ -66,11 +69,11 @@ impl<'a, 'b: 'a> From<&'a NodeStorageEntry<'b>> for NodeStorageRef<'a> { } impl<'b> NodeStorageEntry<'b> { - pub fn into_edges_iter( + pub fn into_edges_iter<'a: 'b>( self, - layers: &LayerIds, + layers: &'a LayerIds, dir: Direction, - ) -> impl Iterator + use<'b, '_> { + ) -> impl Iterator + Send + Sync + 'b { match self { NodeStorageEntry::Mem(entry) => StorageVariants3::Mem(entry.edges_iter(layers, dir)), NodeStorageEntry::Unlocked(entry) => { @@ -81,27 +84,27 @@ impl<'b> NodeStorageEntry<'b> { } } - pub fn prop_ids(self) -> BoxedLIter<'b, usize> { - match self { - NodeStorageEntry::Mem(entry) => Box::new(entry.node().const_prop_ids()), - NodeStorageEntry::Unlocked(entry) => Box::new(GenLockedIter::from(entry, |e| { - Box::new(e.as_ref().node().const_prop_ids()) - })), - #[cfg(feature = "storage")] - NodeStorageEntry::Disk(node) => Box::new(node.constant_node_prop_ids()), - } - } - - pub fn temporal_prop_ids(self) -> Box + 'b> { - match self { - NodeStorageEntry::Mem(entry) => Box::new(entry.temporal_prop_ids()), - NodeStorageEntry::Unlocked(entry) => Box::new(GenLockedIter::from(entry, |e| { - Box::new(e.as_ref().temporal_prop_ids()) - })), - #[cfg(feature = "storage")] - NodeStorageEntry::Disk(node) => Box::new(node.temporal_node_prop_ids()), - } - } + // pub fn prop_ids(self) -> BoxedLIter<'b, usize> { + // match self { + // NodeStorageEntry::Mem(entry) => Box::new(entry.node().const_prop_ids()), + // NodeStorageEntry::Unlocked(entry) => Box::new(GenLockedIter::from(entry, |e| { + // Box::new(e.as_ref().node().const_prop_ids()) + // })), + // #[cfg(feature = "storage")] + // NodeStorageEntry::Disk(node) => Box::new(node.constant_node_prop_ids()), + // } + // } + + // pub fn temporal_prop_ids(self) -> Box + 'b> { + // match self { + // NodeStorageEntry::Mem(entry) => Box::new(entry.temporal_prop_ids()), + // NodeStorageEntry::Unlocked(entry) => Box::new(GenLockedIter::from(entry, |e| { + // Box::new(e.as_ref().temporal_prop_ids()) + // })), + // #[cfg(feature = "storage")] + // NodeStorageEntry::Disk(node) => Box::new(node.temporal_node_prop_ids()), + // } + // } } impl<'a, 'b: 'a> NodeStorageOps<'a> for &'a NodeStorageEntry<'b> { From 5e48870134d5b3a714491016ec035ba8cba46df9 Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Thu, 19 Jun 2025 16:08:21 +0100 Subject: [PATCH 023/321] start working on prop rows --- db4-storage/src/api/nodes.rs | 4 ++++ db4-storage/src/properties/mod.rs | 12 +++++++----- db4-storage/src/segments/mod.rs | 11 +++++++++++ db4-storage/src/segments/node_entry.rs | 15 +++++++++++++-- raphtory-storage/src/graph/nodes/node_ref.rs | 6 ------ 5 files changed, 35 insertions(+), 13 deletions(-) diff --git a/db4-storage/src/api/nodes.rs b/db4-storage/src/api/nodes.rs index a384a93751..b1c5c62fa2 100644 --- a/db4-storage/src/api/nodes.rs +++ b/db4-storage/src/api/nodes.rs @@ -212,6 +212,10 @@ pub trait NodeRefOps<'a>: Copy + Clone + Send + Sync { } } + + fn temp_prop_rows(self) -> impl Iterator)>)> + 'a; + + fn out_nbrs(self, layer_id: usize) -> impl Iterator + 'a where Self: Sized, diff --git a/db4-storage/src/properties/mod.rs b/db4-storage/src/properties/mod.rs index 97588856d3..6a3e161d3b 100644 --- a/db4-storage/src/properties/mod.rs +++ b/db4-storage/src/properties/mod.rs @@ -315,12 +315,14 @@ impl<'a> PropEntry<'a> { } pub(crate) fn prop(self, prop_id: usize) -> Option> { - let t_cell = self - .properties + let t_cell = self.t_cell(); + Some(TPropCell::new(t_cell, self.properties.t_column(prop_id))) + } + + pub fn t_cell(self) -> &'a TCell> { + self.properties .t_index .get(self.row) - .map_or(&TCell::Empty, |ts| &ts.props_ts); - - Some(TPropCell::new(t_cell, self.properties.t_column(prop_id))) + .map_or(&TCell::Empty, |ts| &ts.props_ts) } } diff --git a/db4-storage/src/segments/mod.rs b/db4-storage/src/segments/mod.rs index 26a3894227..109c8dc615 100644 --- a/db4-storage/src/segments/mod.rs +++ b/db4-storage/src/segments/mod.rs @@ -212,6 +212,17 @@ impl SegmentContainer { }) } + pub fn t_prop_rows(&self, item_id: impl Into) -> &TCell> { + let item_id = item_id.into(); + self.data + .get(&item_id) + .map(|entry| { + let prop_entry = self.properties.get_entry(entry.row()); + prop_entry.t_cell() + }) + .unwrap_or(&TCell::Empty) + } + pub fn c_prop(&self, item_id: impl Into, prop_id: usize) -> Option { let item_id = item_id.into(); self.data.get(&item_id).and_then(|entry| { diff --git a/db4-storage/src/segments/node_entry.rs b/db4-storage/src/segments/node_entry.rs index c1721ab245..f5731623e8 100644 --- a/db4-storage/src/segments/node_entry.rs +++ b/db4-storage/src/segments/node_entry.rs @@ -4,8 +4,8 @@ use crate::{ segments::node::MemNodeSegment, }; use raphtory_api::core::entities::{EID, VID, properties::prop::Prop}; -use raphtory_core::entities::properties::tprop::TPropCell; -use std::ops::Deref; +use raphtory_core::{entities::properties::tprop::TPropCell, storage::timeindex::TimeIndexEntry}; +use std::{iter::Empty, ops::Deref}; use super::additions::MemAdditions; @@ -93,4 +93,15 @@ impl<'a> NodeRefOps<'a> for MemNodeRef<'a> { .t_prop(self.pos, prop_id) .unwrap_or_default() } + + fn temp_prop_rows(self) -> impl Iterator)>)> + 'a { + // self.ns.as_ref().iter().enumerate().flat_map(|(layer_id, layer)| { + // let rows = layer.t_prop_rows(self.pos); + // }).flat_map(|t_prop| { + // t_prop. + // }) + + //TODO + std::iter::empty::<(_, _, Empty<_>)>() + } } diff --git a/raphtory-storage/src/graph/nodes/node_ref.rs b/raphtory-storage/src/graph/nodes/node_ref.rs index 5f5749d2bf..3b8d0bf2cd 100644 --- a/raphtory-storage/src/graph/nodes/node_ref.rs +++ b/raphtory-storage/src/graph/nodes/node_ref.rs @@ -70,12 +70,6 @@ impl<'a> NodeStorageRef<'a> { } } -impl<'a> From> for NodeStorageRef<'a> { - fn from(value: NodePtr<'a>) -> Self { - NodeStorageRef::Mem(value) - } -} - #[cfg(feature = "storage")] impl<'a> From> for NodeStorageRef<'a> { fn from(value: DiskNode<'a>) -> Self { From 380a1061e9c4aa26a9a6f84e50954c45dc99aa88 Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Thu, 19 Jun 2025 17:44:20 +0100 Subject: [PATCH 024/321] added degree into NodeStorageRef --- db4-storage/src/api/nodes.rs | 2 + db4-storage/src/segments/node.rs | 5 ++ db4-storage/src/segments/node_entry.rs | 15 +++++- raphtory-storage/src/graph/nodes/node_ref.rs | 48 +++++++++++--------- 4 files changed, 46 insertions(+), 24 deletions(-) diff --git a/db4-storage/src/api/nodes.rs b/db4-storage/src/api/nodes.rs index b1c5c62fa2..3710e769e1 100644 --- a/db4-storage/src/api/nodes.rs +++ b/db4-storage/src/api/nodes.rs @@ -249,4 +249,6 @@ pub trait NodeRefOps<'a>: Copy + Clone + Send + Sync { fn c_prop(self, layer_id: usize, prop_id: usize) -> Option; fn t_prop(self, layer_id: usize, prop_id: usize) -> Self::TProps; + + fn degree(self, layers: &LayerIds, dir: Direction) -> usize; } diff --git a/db4-storage/src/segments/node.rs b/db4-storage/src/segments/node.rs index 13feeb0f14..bcc25b7c6b 100644 --- a/db4-storage/src/segments/node.rs +++ b/db4-storage/src/segments/node.rs @@ -107,6 +107,11 @@ impl MemNodeSegment { self.layers.get(layer_id) } + pub fn degree(&self, n: LocalPOS, layer_id: usize, dir: Direction) -> usize { + self.get_adj(n, layer_id) + .map_or(0, |adj| adj.degree(dir)) + } + pub fn lsn(&self) -> u64 { self.layers.iter().map(|seg| seg.lsn()).min().unwrap_or(0) } diff --git a/db4-storage/src/segments/node_entry.rs b/db4-storage/src/segments/node_entry.rs index f5731623e8..277e7a4713 100644 --- a/db4-storage/src/segments/node_entry.rs +++ b/db4-storage/src/segments/node_entry.rs @@ -3,8 +3,8 @@ use crate::{ api::nodes::{NodeEntryOps, NodeRefOps}, segments::node::MemNodeSegment, }; -use raphtory_api::core::entities::{EID, VID, properties::prop::Prop}; -use raphtory_core::{entities::properties::tprop::TPropCell, storage::timeindex::TimeIndexEntry}; +use raphtory_api::core::{entities::{properties::prop::Prop, EID, VID}, Direction}; +use raphtory_core::{entities::{properties::tprop::TPropCell, LayerIds}, storage::timeindex::TimeIndexEntry}; use std::{iter::Empty, ops::Deref}; use super::additions::MemAdditions; @@ -104,4 +104,15 @@ impl<'a> NodeRefOps<'a> for MemNodeRef<'a> { //TODO std::iter::empty::<(_, _, Empty<_>)>() } + + fn degree(self, layers: &LayerIds, dir: Direction) -> usize { + match layers { + LayerIds::One(layer_id) => self.ns.degree(self.pos, *layer_id, dir), + LayerIds::All => self.ns.degree(self.pos, 0, dir), + LayerIds::None => 0, + layers => { + self.edges_iter(layers, dir).count() + } + } + } } diff --git a/raphtory-storage/src/graph/nodes/node_ref.rs b/raphtory-storage/src/graph/nodes/node_ref.rs index 3b8d0bf2cd..431d7ba26e 100644 --- a/raphtory-storage/src/graph/nodes/node_ref.rs +++ b/raphtory-storage/src/graph/nodes/node_ref.rs @@ -17,7 +17,7 @@ use raphtory_api::{ }; use raphtory_core::storage::node_entry::NodePtr; use std::{borrow::Cow, ops::Range}; -use storage::NodeEntryRef; +use storage::{NodeEntryRef, api::nodes::NodeRefOps}; #[cfg(feature = "storage")] use crate::disk::storage_interface::node::DiskNode; @@ -37,36 +37,40 @@ impl<'a> From> for NodeStorageRef<'a> { impl<'a> NodeStorageRef<'a> { pub fn temp_prop_rows(self) -> impl Iterator)> + 'a { - match self { - NodeStorageRef::Mem(node_entry) => node_entry - .into_rows() - .map(|(t, row)| (t, Row::Mem(row))) - .into_dyn_boxed(), - #[cfg(feature = "storage")] - NodeStorageRef::Disk(disk_node) => disk_node.into_rows().into_dyn_boxed(), - } + // match self { + // NodeStorageRef::Mem(node_entry) => node_entry + // .into_rows() + // .map(|(t, row)| (t, Row::Mem(row))) + // .into_dyn_boxed(), + // #[cfg(feature = "storage")] + // NodeStorageRef::Disk(disk_node) => disk_node.into_rows().into_dyn_boxed(), + // } + //TODO: + std::iter::empty() } pub fn temp_prop_rows_window( self, window: Range, ) -> impl Iterator)> + 'a { - match self { - NodeStorageRef::Mem(node_entry) => node_entry - .into_rows_window(window) - .map(|(t, row)| (t, Row::Mem(row))) - .into_dyn_boxed(), - #[cfg(feature = "storage")] - NodeStorageRef::Disk(disk_node) => disk_node.into_rows_window(window).into_dyn_boxed(), - } + // match self { + // NodeStorageRef::Mem(node_entry) => node_entry + // .into_rows_window(window) + // .map(|(t, row)| (t, Row::Mem(row))) + // .into_dyn_boxed(), + // #[cfg(feature = "storage")] + // NodeStorageRef::Disk(disk_node) => disk_node.into_rows_window(window).into_dyn_boxed(), + // } + std::iter::empty() } pub fn last_before_row(self, t: TimeIndexEntry) -> Vec<(usize, Prop)> { - match self { - NodeStorageRef::Mem(node_entry) => node_entry.last_before_row(t), - #[cfg(feature = "storage")] - NodeStorageRef::Disk(disk_node) => disk_node.last_before_row(t), - } + // match self { + // NodeStorageRef::Mem(node_entry) => node_entry.last_before_row(t), + // #[cfg(feature = "storage")] + // NodeStorageRef::Disk(disk_node) => disk_node.last_before_row(t), + // } + todo!() } } From 1e5f9086b270df65d1595587713ce5ab8ab4d1a7 Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Fri, 20 Jun 2025 12:12:32 +0100 Subject: [PATCH 025/321] make the additions take LayerIds and be generic --- db4-storage/src/api/edges.rs | 4 +- db4-storage/src/api/nodes.rs | 14 +- db4-storage/src/gen_ts.rs | 129 ++++++++++++++++++ db4-storage/src/lib.rs | 9 ++ db4-storage/src/pages/mod.rs | 7 +- db4-storage/src/pages/test_utils/checkers.rs | 9 +- db4-storage/src/segments/edge_entry.rs | 42 +++++- db4-storage/src/segments/node.rs | 3 +- db4-storage/src/segments/node_entry.rs | 67 +++++++-- raphtory-storage/src/graph/nodes/node_ref.rs | 6 +- .../src/graph/nodes/node_storage_ops.rs | 96 ++++++------- 11 files changed, 304 insertions(+), 82 deletions(-) create mode 100644 db4-storage/src/gen_ts.rs diff --git a/db4-storage/src/api/edges.rs b/db4-storage/src/api/edges.rs index b857853a0a..88ede4de2b 100644 --- a/db4-storage/src/api/edges.rs +++ b/db4-storage/src/api/edges.rs @@ -7,7 +7,7 @@ use std::{ use parking_lot::{RwLockReadGuard, RwLockWriteGuard, lock_api::ArcRwLockReadGuard}; use raphtory_api::core::entities::properties::{meta::Meta, prop::Prop, tprop::TPropOps}; use raphtory_core::{ - entities::VID, + entities::{LayerIds, VID}, storage::timeindex::{TimeIndexEntry, TimeIndexOps}, }; @@ -116,7 +116,7 @@ pub trait EdgeRefOps<'a>: Copy + Clone + Send + Sync { fn edge(self, layer_id: usize) -> Option<(VID, VID)>; - fn additions(self, layer_id: usize) -> Self::Additions; + fn additions(self, layer_ids: &'a LayerIds) -> Self::Additions; fn c_prop(self, layer_id: usize, prop_id: usize) -> Option; diff --git a/db4-storage/src/api/nodes.rs b/db4-storage/src/api/nodes.rs index 3710e769e1..c95ccd352c 100644 --- a/db4-storage/src/api/nodes.rs +++ b/db4-storage/src/api/nodes.rs @@ -212,9 +212,15 @@ pub trait NodeRefOps<'a>: Copy + Clone + Send + Sync { } } - - fn temp_prop_rows(self) -> impl Iterator)>)> + 'a; - + fn temp_prop_rows( + self, + ) -> impl Iterator< + Item = ( + TimeIndexEntry, + usize, + impl Iterator)>, + ), + > + 'a; fn out_nbrs(self, layer_id: usize) -> impl Iterator + 'a where @@ -244,7 +250,7 @@ pub trait NodeRefOps<'a>: Copy + Clone + Send + Sync { self.inb_edges_sorted(layer_id).map(|(v, _)| v) } - fn additions(self, layer_id: usize) -> Self::Additions; + fn additions(self, layers: &'a LayerIds) -> Self::Additions; fn c_prop(self, layer_id: usize, prop_id: usize) -> Option; diff --git a/db4-storage/src/gen_ts.rs b/db4-storage/src/gen_ts.rs new file mode 100644 index 0000000000..7b53dc5912 --- /dev/null +++ b/db4-storage/src/gen_ts.rs @@ -0,0 +1,129 @@ +use std::{borrow::Borrow, ops::Range}; + +use itertools::Itertools; +use raphtory_core::{ + entities::LayerIds, + storage::timeindex::{TimeIndexEntry, TimeIndexOps}, +}; + +use crate::utils::Iter4; + +#[derive(Clone, Copy)] +pub struct GenericTimeOps<'a, Ref> { + range: Option<(TimeIndexEntry, TimeIndexEntry)>, + layer_id: &'a LayerIds, + node: Ref, +} + +impl<'a, Ref> GenericTimeOps<'a, Ref> { + pub fn new(node: Ref, layer_id: &'a LayerIds) -> Self { + Self { + range: None, + layer_id, + node, + } + } +} + +pub trait WithTimeCells<'a>: Copy + Clone + Send + Sync +where + Self: 'a, +{ + type TimeCell: TimeIndexOps<'a, IndexType = TimeIndexEntry>; + + fn layer_time_cells( + self, + layer_id: usize, + range: Option<(TimeIndexEntry, TimeIndexEntry)>, + ) -> impl Iterator + 'a; + fn num_layers(&self) -> usize; + + fn time_cells + 'a>( + self, + layer_ids: B, + range: Option<(TimeIndexEntry, TimeIndexEntry)>, + ) -> impl Iterator + 'a { + match layer_ids.borrow() { + LayerIds::None => Iter4::I(std::iter::empty()), + LayerIds::One(layer_id) => Iter4::J(self.layer_time_cells(*layer_id, range)), + LayerIds::All => Iter4::K( + (0..self.num_layers()) + .flat_map(move |layer_id| self.layer_time_cells(layer_id, range)), + ), + LayerIds::Multiple(layers) => Iter4::L( + layers + .clone() + .into_iter() + .flat_map(move |layer_id| self.layer_time_cells(layer_id, range)), + ), + } + } + + fn into_iter( + self, + layer_ids: &'a LayerIds, + range: Option<(TimeIndexEntry, TimeIndexEntry)>, + ) -> impl Iterator + Send + Sync + 'a { + let iters = self.time_cells(layer_ids, range); + iters.map(|cell| cell.iter()).kmerge() + } + + fn into_iter_rev( + self, + layer_ids: &'a LayerIds, + range: Option<(TimeIndexEntry, TimeIndexEntry)>, + ) -> impl Iterator + Send + Sync + 'a { + let iters = self.time_cells(layer_ids, range); + iters.map(|cell| cell.iter_rev()).kmerge_by(|a, b| a > b) + } +} + +impl<'a, Ref: WithTimeCells<'a> + 'a> TimeIndexOps<'a> for GenericTimeOps<'a, Ref> { + type IndexType = TimeIndexEntry; + + type RangeType = Self; + fn active(&self, w: Range) -> bool { + self.node + .time_cells(self.layer_id, self.range) + .any(|t_cell| t_cell.active(w.clone())) + } + + fn range(&self, w: Range) -> Self::RangeType { + GenericTimeOps { + range: Some((w.start, w.end)), + node: self.node, + layer_id: self.layer_id, + } + } + + fn first(&self) -> Option { + Iterator::min( + self.node + .time_cells(self.layer_id, self.range) + .filter_map(|t_cell| t_cell.first()), + ) + } + + fn last(&self) -> Option { + Iterator::max( + self.node + .time_cells(self.layer_id, self.range) + .filter_map(|t_cell| t_cell.last()), + ) + } + + fn iter(self) -> impl Iterator + Send + Sync + 'a { + self.node.into_iter(self.layer_id, self.range) + } + + fn iter_rev(self) -> impl Iterator + Send + Sync + 'a { + self.node.into_iter_rev(self.layer_id, self.range) + } + + fn len(&self) -> usize { + self.node + .time_cells(self.layer_id, self.range) + .map(|t_cell| t_cell.len()) + .sum() + } +} diff --git a/db4-storage/src/lib.rs b/db4-storage/src/lib.rs index 1ce6592194..e162f6be79 100644 --- a/db4-storage/src/lib.rs +++ b/db4-storage/src/lib.rs @@ -1,11 +1,13 @@ use std::path::Path; use crate::{ + gen_ts::GenericTimeOps, pages::{ GraphStore, ReadLockedGraphStore, edge_store::ReadLockedEdgeStorage, node_store::ReadLockedNodeStorage, }, segments::{ + additions::MemAdditions, edge::EdgeSegmentView, edge_entry::{MemEdgeEntry, MemEdgeRef}, node::NodeSegmentView, @@ -13,9 +15,11 @@ use crate::{ }, }; use raphtory_api::core::entities::{EID, VID}; +use raphtory_core::entities::properties::tprop::TPropCell; use segments::{edge::MemEdgeSegment, node::MemNodeSegment}; pub mod api; +pub mod gen_ts; pub mod pages; pub mod persist; pub mod properties; @@ -36,6 +40,11 @@ pub type EdgeEntry<'a> = MemEdgeEntry<'a, parking_lot::RwLockReadGuard<'a, MemEd pub type NodeEntryRef<'a> = MemNodeRef<'a>; pub type EdgeEntryRef<'a> = MemEdgeRef<'a>; +pub type NodeAdditions<'a> = GenericTimeOps<'a, MemNodeRef<'a>>; +pub type EdgeAdditions<'a> = GenericTimeOps<'a, MemEdgeRef<'a>>; +pub type EdgeTProps<'a> = TPropCell<'a>; +pub type NodeTProps<'a> = TPropCell<'a>; + pub mod error { use std::{path::PathBuf, sync::Arc}; diff --git a/db4-storage/src/pages/mod.rs b/db4-storage/src/pages/mod.rs index 45a3ad7ef6..813b1e8291 100644 --- a/db4-storage/src/pages/mod.rs +++ b/db4-storage/src/pages/mod.rs @@ -497,7 +497,10 @@ mod test { use core::panic; use proptest::prelude::*; use raphtory_api::core::entities::properties::prop::Prop; - use raphtory_core::{entities::VID, storage::timeindex::TimeIndexOps}; + use raphtory_core::{ + entities::{LayerIds, VID}, + storage::timeindex::TimeIndexOps, + }; fn check_edges( edges: Vec<(impl Into, impl Into)>, @@ -610,7 +613,7 @@ mod test { let node = g.nodes().node(3); let node_entry = node.as_ref(); - let actual: Vec<_> = node_entry.additions(0).iter_t().collect(); + let actual: Vec<_> = node_entry.additions(&LayerIds::One(0)).iter_t().collect(); assert_eq!(actual, vec![4]); }; diff --git a/db4-storage/src/pages/test_utils/checkers.rs b/db4-storage/src/pages/test_utils/checkers.rs index 00b6429bde..574a4194f6 100644 --- a/db4-storage/src/pages/test_utils/checkers.rs +++ b/db4-storage/src/pages/test_utils/checkers.rs @@ -9,7 +9,7 @@ use raphtory_api::core::{ storage::dict_mapper::MaybeNew, }; use raphtory_core::{ - entities::{ELID, VID}, + entities::{ELID, LayerIds, VID}, storage::timeindex::TimeIndexOps, }; use rayon::prelude::*; @@ -259,7 +259,7 @@ pub fn check_graph_with_nodes_support< for (node, ts_expected) in ts_for_nodes { let ne = graph.nodes().node(node); let node_entry = ne.as_ref(); - let actual: Vec<_> = node_entry.additions(0).iter_t().collect(); + let actual: Vec<_> = node_entry.additions(&LayerIds::One(0)).iter_t().collect(); assert_eq!( actual, ts_expected, "Expected node additions for node ({node:?})", @@ -477,7 +477,10 @@ pub fn check_graph_with_props_support< for (node_id, ts) in node_groups { let node = graph.nodes().node(node_id); let node_entry = node.as_ref(); - let actual_additions_ts = node_entry.additions(0).iter_t().collect::>(); + let actual_additions_ts = node_entry + .additions(&LayerIds::One(0)) + .iter_t() + .collect::>(); assert_eq!( actual_additions_ts, ts, diff --git a/db4-storage/src/segments/edge_entry.rs b/db4-storage/src/segments/edge_entry.rs index 797a04bff4..d0173ccdbb 100644 --- a/db4-storage/src/segments/edge_entry.rs +++ b/db4-storage/src/segments/edge_entry.rs @@ -1,9 +1,13 @@ use raphtory_api::core::entities::properties::prop::Prop; -use raphtory_core::entities::{VID, properties::tprop::TPropCell}; +use raphtory_core::{ + entities::{LayerIds, VID, properties::tprop::TPropCell}, + storage::timeindex::{TimeIndexEntry, TimeIndexOps}, +}; use crate::{ - LocalPOS, + EdgeAdditions, LocalPOS, api::edges::{EdgeEntryOps, EdgeRefOps}, + gen_ts::WithTimeCells, }; use super::{additions::MemAdditions, edge::MemEdgeSegment}; @@ -55,8 +59,36 @@ impl<'a> MemEdgeRef<'a> { } } +impl<'a> WithTimeCells<'a> for MemEdgeRef<'a> { + type TimeCell = MemAdditions<'a>; + + fn layer_time_cells( + self, + layer_id: usize, + range: Option<(TimeIndexEntry, TimeIndexEntry)>, + ) -> impl Iterator + 'a { + std::iter::once( + range + .map(|(start, end)| { + MemAdditions::Window( + self.es.as_ref()[layer_id] + .additions(self.pos) + .range(start..end), + ) + }) + .unwrap_or_else(|| { + MemAdditions::Props(self.es.as_ref()[layer_id].additions(self.pos)) + }), + ) + } + + fn num_layers(&self) -> usize { + self.es.as_ref().len() + } +} + impl<'a> EdgeRefOps<'a> for MemEdgeRef<'a> { - type Additions = MemAdditions<'a>; + type Additions = EdgeAdditions<'a>; type TProps = TPropCell<'a>; @@ -66,8 +98,8 @@ impl<'a> EdgeRefOps<'a> for MemEdgeRef<'a> { .map(|entry| (entry.src, entry.dst)) } - fn additions(self, layer_id: usize) -> Self::Additions { - MemAdditions::Props(self.es.as_ref()[layer_id].additions(self.pos)) + fn additions(self, layer_id: &'a LayerIds) -> Self::Additions { + EdgeAdditions::new(self, layer_id) } fn c_prop(self, layer_id: usize, prop_id: usize) -> Option { diff --git a/db4-storage/src/segments/node.rs b/db4-storage/src/segments/node.rs index bcc25b7c6b..286ef5e166 100644 --- a/db4-storage/src/segments/node.rs +++ b/db4-storage/src/segments/node.rs @@ -108,8 +108,7 @@ impl MemNodeSegment { } pub fn degree(&self, n: LocalPOS, layer_id: usize, dir: Direction) -> usize { - self.get_adj(n, layer_id) - .map_or(0, |adj| adj.degree(dir)) + self.get_adj(n, layer_id).map_or(0, |adj| adj.degree(dir)) } pub fn lsn(&self) -> u64 { diff --git a/db4-storage/src/segments/node_entry.rs b/db4-storage/src/segments/node_entry.rs index 277e7a4713..927511ce49 100644 --- a/db4-storage/src/segments/node_entry.rs +++ b/db4-storage/src/segments/node_entry.rs @@ -1,10 +1,17 @@ use crate::{ - LocalPOS, + LocalPOS, NodeAdditions, api::nodes::{NodeEntryOps, NodeRefOps}, + gen_ts::{GenericTimeOps, WithTimeCells}, segments::node::MemNodeSegment, }; -use raphtory_api::core::{entities::{properties::prop::Prop, EID, VID}, Direction}; -use raphtory_core::{entities::{properties::tprop::TPropCell, LayerIds}, storage::timeindex::TimeIndexEntry}; +use raphtory_api::core::{ + Direction, + entities::{EID, VID, properties::prop::Prop}, +}; +use raphtory_core::{ + entities::{LayerIds, nodes::node_store::PropTimestamps, properties::tprop::TPropCell}, + storage::timeindex::{TimeIndexEntry, TimeIndexOps}, +}; use std::{iter::Empty, ops::Deref}; use super::additions::MemAdditions; @@ -56,8 +63,36 @@ impl<'a> MemNodeRef<'a> { } } +impl<'a> WithTimeCells<'a> for MemNodeRef<'a> { + type TimeCell = MemAdditions<'a>; + + fn layer_time_cells( + self, + layer_id: usize, + range: Option<(TimeIndexEntry, TimeIndexEntry)>, + ) -> impl Iterator + 'a { + std::iter::once( + range + .map(|(start, end)| { + MemAdditions::Window( + self.ns.as_ref()[layer_id] + .additions(self.pos) + .range(start..end), + ) + }) + .unwrap_or_else(|| { + MemAdditions::Props(self.ns.as_ref()[layer_id].additions(self.pos)) + }), + ) + } + + fn num_layers(&self) -> usize { + self.ns.as_ref().len() + } +} + impl<'a> NodeRefOps<'a> for MemNodeRef<'a> { - type Additions = MemAdditions<'a>; + type Additions = NodeAdditions<'a>; type TProps = TPropCell<'a>; fn vid(&self) -> VID { @@ -80,8 +115,8 @@ impl<'a> NodeRefOps<'a> for MemNodeRef<'a> { self.ns.inb_edges(self.pos, layer_id) } - fn additions(self, layer_id: usize) -> Self::Additions { - MemAdditions::Props(self.ns.as_ref()[layer_id].additions(self.pos)) + fn additions(self, layer_ids: &'a LayerIds) -> Self::Additions { + NodeAdditions::new(self, layer_ids) } fn c_prop(self, layer_id: usize, prop_id: usize) -> Option { @@ -93,8 +128,16 @@ impl<'a> NodeRefOps<'a> for MemNodeRef<'a> { .t_prop(self.pos, prop_id) .unwrap_or_default() } - - fn temp_prop_rows(self) -> impl Iterator)>)> + 'a { + + fn temp_prop_rows( + self, + ) -> impl Iterator< + Item = ( + TimeIndexEntry, + usize, + impl Iterator)>, + ), + > + 'a { // self.ns.as_ref().iter().enumerate().flat_map(|(layer_id, layer)| { // let rows = layer.t_prop_rows(self.pos); // }).flat_map(|t_prop| { @@ -104,15 +147,13 @@ impl<'a> NodeRefOps<'a> for MemNodeRef<'a> { //TODO std::iter::empty::<(_, _, Empty<_>)>() } - + fn degree(self, layers: &LayerIds, dir: Direction) -> usize { match layers { LayerIds::One(layer_id) => self.ns.degree(self.pos, *layer_id, dir), LayerIds::All => self.ns.degree(self.pos, 0, dir), LayerIds::None => 0, - layers => { - self.edges_iter(layers, dir).count() - } - } + layers => self.edges_iter(layers, dir).count(), + } } } diff --git a/raphtory-storage/src/graph/nodes/node_ref.rs b/raphtory-storage/src/graph/nodes/node_ref.rs index 431d7ba26e..8f127f065e 100644 --- a/raphtory-storage/src/graph/nodes/node_ref.rs +++ b/raphtory-storage/src/graph/nodes/node_ref.rs @@ -17,7 +17,7 @@ use raphtory_api::{ }; use raphtory_core::storage::node_entry::NodePtr; use std::{borrow::Cow, ops::Range}; -use storage::{NodeEntryRef, api::nodes::NodeRefOps}; +use storage::{api::nodes::NodeRefOps, NodeEntryRef}; #[cfg(feature = "storage")] use crate::disk::storage_interface::node::DiskNode; @@ -115,8 +115,8 @@ impl<'a> NodeStorageOps<'a> for NodeStorageRef<'a> { for_all!(self, node => node.degree(layers, dir)) } - fn additions(self) -> NodeAdditions<'a> { - for_all!(self, node => node.additions()) + fn additions(self) -> storage::NodeAdditions<'a> { + for_all!(self, node => node.additions(&LayerIds::All)) } fn tprop(self, prop_id: usize) -> impl TPropOps<'a> { diff --git a/raphtory-storage/src/graph/nodes/node_storage_ops.rs b/raphtory-storage/src/graph/nodes/node_storage_ops.rs index f0d9565501..f011cb7354 100644 --- a/raphtory-storage/src/graph/nodes/node_storage_ops.rs +++ b/raphtory-storage/src/graph/nodes/node_storage_ops.rs @@ -13,7 +13,7 @@ use std::borrow::Cow; pub trait NodeStorageOps<'a>: Sized { fn degree(self, layers: &LayerIds, dir: Direction) -> usize; - fn additions(self) -> NodeAdditions<'a>; + fn additions(self) -> storage::NodeAdditions<'a>; fn tprop(self, prop_id: usize) -> impl TPropOps<'a>; @@ -38,50 +38,50 @@ pub trait NodeStorageOps<'a>: Sized { fn find_edge(self, dst: VID, layer_ids: &LayerIds) -> Option; } -impl<'a> NodeStorageOps<'a> for NodePtr<'a> { - fn degree(self, layers: &LayerIds, dir: Direction) -> usize { - self.node.degree(layers, dir) - } - - fn additions(self) -> NodeAdditions<'a> { - NodeAdditions::Mem(self.node.timestamps()) - } - - fn tprop(self, prop_id: usize) -> impl TPropOps<'a> { - self.t_prop(prop_id) - } - - fn tprops(self) -> impl Iterator)> { - self.temporal_prop_ids() - .map(move |tid| (tid, self.tprop(tid))) - } - - fn prop(self, prop_id: usize) -> Option { - self.node.constant_property(prop_id).cloned() - } - - fn edges_iter(self, layers: &LayerIds, dir: Direction) -> impl Iterator + 'a { - self.node.edge_tuples(layers, dir) - } - - fn node_type_id(self) -> usize { - self.node.node_type - } - - fn vid(self) -> VID { - self.node.vid - } - - fn id(self) -> GidRef<'a> { - (&self.node.global_id).into() - } - - fn name(self) -> Option> { - self.node.global_id.as_str().map(Cow::from) - } - - fn find_edge(self, dst: VID, layer_ids: &LayerIds) -> Option { - let eid = NodeStore::find_edge_eid(self.node, dst, layer_ids)?; - Some(EdgeRef::new_outgoing(eid, self.node.vid, dst)) - } -} +// impl<'a> NodeStorageOps<'a> for NodePtr<'a> { +// fn degree(self, layers: &LayerIds, dir: Direction) -> usize { +// self.node.degree(layers, dir) +// } + +// fn additions(self) -> NodeAdditions<'a> { +// NodeAdditions::Mem(self.node.timestamps()) +// } + +// fn tprop(self, prop_id: usize) -> impl TPropOps<'a> { +// self.t_prop(prop_id) +// } + +// fn tprops(self) -> impl Iterator)> { +// self.temporal_prop_ids() +// .map(move |tid| (tid, self.tprop(tid))) +// } + +// fn prop(self, prop_id: usize) -> Option { +// self.node.constant_property(prop_id).cloned() +// } + +// fn edges_iter(self, layers: &LayerIds, dir: Direction) -> impl Iterator + 'a { +// self.node.edge_tuples(layers, dir) +// } + +// fn node_type_id(self) -> usize { +// self.node.node_type +// } + +// fn vid(self) -> VID { +// self.node.vid +// } + +// fn id(self) -> GidRef<'a> { +// (&self.node.global_id).into() +// } + +// fn name(self) -> Option> { +// self.node.global_id.as_str().map(Cow::from) +// } + +// fn find_edge(self, dst: VID, layer_ids: &LayerIds) -> Option { +// let eid = NodeStore::find_edge_eid(self.node, dst, layer_ids)?; +// Some(EdgeRef::new_outgoing(eid, self.node.vid, dst)) +// } +// } From df094d84595cf76c483e6103f7f9030bb7c6a42d Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Fri, 20 Jun 2025 16:56:18 +0100 Subject: [PATCH 026/321] add support for temporal properties on layers and find edge --- db4-storage/src/api/edges.rs | 2 +- db4-storage/src/api/nodes.rs | 6 +- db4-storage/src/gen_t_props.rs | 102 +++++++++++++++++++ db4-storage/src/gen_ts.rs | 11 +- db4-storage/src/lib.rs | 6 +- db4-storage/src/pages/test_utils/checkers.rs | 11 +- db4-storage/src/segments/edge_entry.rs | 31 ++++-- db4-storage/src/segments/node_entry.rs | 49 +++++++-- raphtory-storage/src/graph/nodes/node_ref.rs | 6 +- 9 files changed, 193 insertions(+), 31 deletions(-) create mode 100644 db4-storage/src/gen_t_props.rs diff --git a/db4-storage/src/api/edges.rs b/db4-storage/src/api/edges.rs index 88ede4de2b..c35c46164e 100644 --- a/db4-storage/src/api/edges.rs +++ b/db4-storage/src/api/edges.rs @@ -120,5 +120,5 @@ pub trait EdgeRefOps<'a>: Copy + Clone + Send + Sync { fn c_prop(self, layer_id: usize, prop_id: usize) -> Option; - fn t_prop(self, layer_id: usize, prop_id: usize) -> Self::TProps; + fn t_prop(self, layer_id: &'a LayerIds, prop_id: usize) -> Self::TProps; } diff --git a/db4-storage/src/api/nodes.rs b/db4-storage/src/api/nodes.rs index c95ccd352c..c45aa468cf 100644 --- a/db4-storage/src/api/nodes.rs +++ b/db4-storage/src/api/nodes.rs @@ -14,7 +14,7 @@ use raphtory_api::{ iter::IntoDynBoxed, }; use raphtory_core::{ - entities::{EID, LayerIds, Multiple, VID, edges::edge_ref::EdgeRef}, + entities::{EID, LayerIds, VID, edges::edge_ref::EdgeRef}, storage::timeindex::{TimeIndexEntry, TimeIndexOps}, utils::iter::GenLockedIter, }; @@ -254,7 +254,9 @@ pub trait NodeRefOps<'a>: Copy + Clone + Send + Sync { fn c_prop(self, layer_id: usize, prop_id: usize) -> Option; - fn t_prop(self, layer_id: usize, prop_id: usize) -> Self::TProps; + fn t_prop(self, layer_id: &'a LayerIds, prop_id: usize) -> Self::TProps; fn degree(self, layers: &LayerIds, dir: Direction) -> usize; + + fn find_edge(&self, dst: VID, layers: &LayerIds) -> Option; } diff --git a/db4-storage/src/gen_t_props.rs b/db4-storage/src/gen_t_props.rs new file mode 100644 index 0000000000..119ae36c6e --- /dev/null +++ b/db4-storage/src/gen_t_props.rs @@ -0,0 +1,102 @@ +use std::ops::Range; + +use itertools::Itertools; +use raphtory_api::core::entities::properties::{prop::Prop, tprop::TPropOps}; +use raphtory_core::{entities::LayerIds, storage::timeindex::TimeIndexEntry}; + +use crate::utils::Iter4; + +pub trait WithTProps<'a>: Clone + Copy + Send + Sync +where + Self: 'a, +{ + type TProp: TPropOps<'a>; + + fn num_layers(&self) -> usize; + fn into_t_props( + self, + layer_id: usize, + prop_id: usize, + ) -> impl Iterator + 'a; + + fn into_t_props_layers<'b>( + self, + layers: &'b LayerIds, + prop_id: usize, + ) -> impl Iterator + 'a { + match layers { + LayerIds::None => Iter4::I(std::iter::empty()), + LayerIds::One(layer_id) => Iter4::J(self.into_t_props(*layer_id, prop_id)), + LayerIds::All => Iter4::K( + (0..self.num_layers()) + .flat_map(move |layer_id| self.into_t_props(layer_id, prop_id)), + ), + LayerIds::Multiple(layers) => Iter4::L( + layers + .clone() + .into_iter() + .flat_map(move |layer_id| self.into_t_props(layer_id, prop_id)), + ), + } + } +} + +#[derive(Clone, Copy)] +pub struct GenTProps<'a, Ref> { + node: Ref, + layer_id: &'a LayerIds, + prop_id: usize, +} + +impl<'a, Ref> GenTProps<'a, Ref> { + pub fn new(node: Ref, layer_id: &'a LayerIds, prop_id: usize) -> Self { + Self { + node, + layer_id, + prop_id, + } + } +} + +impl<'a, Ref: WithTProps<'a>> GenTProps<'a, Ref> { + fn tprops(self, prop_id: usize) -> impl Iterator + 'a { + self.node.into_t_props_layers(self.layer_id, prop_id) + } +} + +impl<'a, Ref: WithTProps<'a> + 'a> TPropOps<'a> for GenTProps<'a, Ref> { + fn last_before(&self, t: TimeIndexEntry) -> Option<(TimeIndexEntry, Prop)> { + self.tprops(self.prop_id) + .map(|t_props| t_props.last_before(t)) + .flatten() + .max_by_key(|(t, _)| *t) + } + + fn iter_inner( + self, + w: Option>, + ) -> impl Iterator + Send + Sync + 'a { + let w = w.map(|w| (w.start, w.end)); + let tprops = self.tprops(self.prop_id); + tprops + .map(|t_prop| t_prop.iter_inner(w.map(|(start, end)| start..end))) + .kmerge_by(|(a, _), (b, _)| a < b) + } + + fn iter_inner_rev( + self, + w: Option>, + ) -> impl Iterator + Send + Sync + 'a { + let w = w.map(|w| (w.start, w.end)); + let tprops = self + .tprops(self.prop_id) + .map(move |t_cell| t_cell.iter_inner_rev(w.map(|(start, end)| start..end))); + tprops.kmerge_by(|(a, _), (b, _)| a > b) + } + + fn at(&self, ti: &TimeIndexEntry) -> Option { + self.tprops(self.prop_id) + .flat_map(|t_props| t_props.at(ti)) + .next() //TODO: need to figure out how to handle this + } +} diff --git a/db4-storage/src/gen_ts.rs b/db4-storage/src/gen_ts.rs index 7b53dc5912..09c194af65 100644 --- a/db4-storage/src/gen_ts.rs +++ b/db4-storage/src/gen_ts.rs @@ -8,6 +8,7 @@ use raphtory_core::{ use crate::utils::Iter4; +// TODO: split the Node time operations into edge additions and property additions #[derive(Clone, Copy)] pub struct GenericTimeOps<'a, Ref> { range: Option<(TimeIndexEntry, TimeIndexEntry)>, @@ -38,7 +39,7 @@ where ) -> impl Iterator + 'a; fn num_layers(&self) -> usize; - fn time_cells + 'a>( + fn time_cells>( self, layer_ids: B, range: Option<(TimeIndexEntry, TimeIndexEntry)>, @@ -59,18 +60,18 @@ where } } - fn into_iter( + fn into_iter<'b>( self, - layer_ids: &'a LayerIds, + layer_ids: &'b LayerIds, range: Option<(TimeIndexEntry, TimeIndexEntry)>, ) -> impl Iterator + Send + Sync + 'a { let iters = self.time_cells(layer_ids, range); iters.map(|cell| cell.iter()).kmerge() } - fn into_iter_rev( + fn into_iter_rev<'b>( self, - layer_ids: &'a LayerIds, + layer_ids: &'b LayerIds, range: Option<(TimeIndexEntry, TimeIndexEntry)>, ) -> impl Iterator + Send + Sync + 'a { let iters = self.time_cells(layer_ids, range); diff --git a/db4-storage/src/lib.rs b/db4-storage/src/lib.rs index e162f6be79..24a49ef6c6 100644 --- a/db4-storage/src/lib.rs +++ b/db4-storage/src/lib.rs @@ -1,6 +1,7 @@ use std::path::Path; use crate::{ + gen_t_props::GenTProps, gen_ts::GenericTimeOps, pages::{ GraphStore, ReadLockedGraphStore, edge_store::ReadLockedEdgeStorage, @@ -19,6 +20,7 @@ use raphtory_core::entities::properties::tprop::TPropCell; use segments::{edge::MemEdgeSegment, node::MemNodeSegment}; pub mod api; +pub mod gen_t_props; pub mod gen_ts; pub mod pages; pub mod persist; @@ -42,8 +44,8 @@ pub type EdgeEntryRef<'a> = MemEdgeRef<'a>; pub type NodeAdditions<'a> = GenericTimeOps<'a, MemNodeRef<'a>>; pub type EdgeAdditions<'a> = GenericTimeOps<'a, MemEdgeRef<'a>>; -pub type EdgeTProps<'a> = TPropCell<'a>; -pub type NodeTProps<'a> = TPropCell<'a>; +pub type NodeTProps<'a> = GenTProps<'a, MemNodeRef<'a>>; +pub type EdgeTProps<'a> = GenTProps<'a, MemEdgeRef<'a>>; pub mod error { use std::{path::PathBuf, sync::Arc}; diff --git a/db4-storage/src/pages/test_utils/checkers.rs b/db4-storage/src/pages/test_utils/checkers.rs index 574a4194f6..12f7d3a1ad 100644 --- a/db4-storage/src/pages/test_utils/checkers.rs +++ b/db4-storage/src/pages/test_utils/checkers.rs @@ -326,7 +326,10 @@ pub fn check_graph_with_nodes_support< let ne = graph.nodes().node(node); let node_entry = ne.as_ref(); - let actual_props = node_entry.t_prop(0, prop_id).iter_t().collect::>(); + let actual_props = node_entry + .t_prop(&LayerIds::One(0), prop_id) + .iter_t() + .collect::>(); assert_eq!( actual_props, props, @@ -442,8 +445,8 @@ pub fn check_graph_with_props_support< .unwrap_or_else(|| panic!("Failed to get edge ({:?}, {:?}) from graph", src, dst)); let edge = graph.edges().edge(edge); let e = edge.as_ref(); - let layer_id = 0; - let actual_props = e.t_prop(layer_id, prop_id).iter_t().collect::>(); + let layer_id = LayerIds::One(0); + let actual_props = e.t_prop(&layer_id, prop_id).iter_t().collect::>(); assert_eq!( actual_props, props, @@ -459,7 +462,7 @@ pub fn check_graph_with_props_support< .const_prop_meta() .get_id(name) .unwrap_or_else(|| panic!("Failed to get prop id for {}", name)); - let actual_props = e.c_prop(layer_id, prop_id); + let actual_props = e.c_prop(0, prop_id); assert_eq!( actual_props.as_ref(), Some(prop), diff --git a/db4-storage/src/segments/edge_entry.rs b/db4-storage/src/segments/edge_entry.rs index d0173ccdbb..371ee6c3f6 100644 --- a/db4-storage/src/segments/edge_entry.rs +++ b/db4-storage/src/segments/edge_entry.rs @@ -5,8 +5,9 @@ use raphtory_core::{ }; use crate::{ - EdgeAdditions, LocalPOS, + EdgeAdditions, EdgeTProps, LocalPOS, api::edges::{EdgeEntryOps, EdgeRefOps}, + gen_t_props::WithTProps, gen_ts::WithTimeCells, }; @@ -87,10 +88,30 @@ impl<'a> WithTimeCells<'a> for MemEdgeRef<'a> { } } +impl<'a> WithTProps<'a> for MemEdgeRef<'a> { + type TProp = TPropCell<'a>; + + fn num_layers(&self) -> usize { + self.es.as_ref().len() + } + + fn into_t_props( + self, + layer_id: usize, + prop_id: usize, + ) -> impl Iterator + 'a { + let edge_pos = self.pos; + self.es.as_ref()[layer_id] + .t_prop(edge_pos, prop_id) + .into_iter() + .map(|t_prop| t_prop.into()) + } +} + impl<'a> EdgeRefOps<'a> for MemEdgeRef<'a> { type Additions = EdgeAdditions<'a>; - type TProps = TPropCell<'a>; + type TProps = EdgeTProps<'a>; fn edge(self, layer_id: usize) -> Option<(VID, VID)> { self.es.as_ref()[layer_id] @@ -106,9 +127,7 @@ impl<'a> EdgeRefOps<'a> for MemEdgeRef<'a> { self.es.as_ref()[layer_id].c_prop(self.pos, prop_id) } - fn t_prop(self, layer_id: usize, prop_id: usize) -> Self::TProps { - self.es.as_ref()[layer_id] - .t_prop(self.pos, prop_id) - .unwrap_or_default() + fn t_prop(self, layer_id: &'a LayerIds, prop_id: usize) -> Self::TProps { + EdgeTProps::new(self, layer_id, prop_id) } } diff --git a/db4-storage/src/segments/node_entry.rs b/db4-storage/src/segments/node_entry.rs index 927511ce49..96d8ef1f74 100644 --- a/db4-storage/src/segments/node_entry.rs +++ b/db4-storage/src/segments/node_entry.rs @@ -1,7 +1,8 @@ use crate::{ - LocalPOS, NodeAdditions, + LocalPOS, NodeAdditions, NodeTProps, api::nodes::{NodeEntryOps, NodeRefOps}, - gen_ts::{GenericTimeOps, WithTimeCells}, + gen_t_props::WithTProps, + gen_ts::WithTimeCells, segments::node::MemNodeSegment, }; use raphtory_api::core::{ @@ -9,7 +10,7 @@ use raphtory_api::core::{ entities::{EID, VID, properties::prop::Prop}, }; use raphtory_core::{ - entities::{LayerIds, nodes::node_store::PropTimestamps, properties::tprop::TPropCell}, + entities::{LayerIds, edges::edge_ref::EdgeRef, properties::tprop::TPropCell}, storage::timeindex::{TimeIndexEntry, TimeIndexOps}, }; use std::{iter::Empty, ops::Deref}; @@ -91,9 +92,29 @@ impl<'a> WithTimeCells<'a> for MemNodeRef<'a> { } } +impl<'a> WithTProps<'a> for MemNodeRef<'a> { + type TProp = TPropCell<'a>; + + fn num_layers(&self) -> usize { + self.ns.as_ref().len() + } + + fn into_t_props( + self, + layer_id: usize, + prop_id: usize, + ) -> impl Iterator + 'a { + let node_pos = self.pos; + self.ns.as_ref()[layer_id] + .t_prop(node_pos, prop_id) + .into_iter() + .map(|t_prop| t_prop.into()) + } +} + impl<'a> NodeRefOps<'a> for MemNodeRef<'a> { type Additions = NodeAdditions<'a>; - type TProps = TPropCell<'a>; + type TProps = NodeTProps<'a>; fn vid(&self) -> VID { self.ns.to_vid(self.pos) @@ -123,10 +144,8 @@ impl<'a> NodeRefOps<'a> for MemNodeRef<'a> { self.ns.as_ref()[layer_id].c_prop(self.pos, prop_id) } - fn t_prop(self, layer_id: usize, prop_id: usize) -> Self::TProps { - self.ns.as_ref()[layer_id] - .t_prop(self.pos, prop_id) - .unwrap_or_default() + fn t_prop(self, layer_id: &'a LayerIds, prop_id: usize) -> Self::TProps { + NodeTProps::new(self, layer_id, prop_id) } fn temp_prop_rows( @@ -156,4 +175,18 @@ impl<'a> NodeRefOps<'a> for MemNodeRef<'a> { layers => self.edges_iter(layers, dir).count(), } } + + fn find_edge(&self, dst: VID, layers: &LayerIds) -> Option { + let eid = match layers { + LayerIds::One(layer_id) => self.ns.get_out_edge(self.pos, dst, *layer_id), + LayerIds::All => self.ns.get_out_edge(self.pos, dst, 0), + LayerIds::Multiple(layers) => layers + .iter() + .find_map(|layer_id| self.ns.get_out_edge(self.pos, dst, layer_id)), + LayerIds::None => None, + }; + + let src_id = self.ns.to_vid(self.pos); + eid.map(|eid| EdgeRef::new_outgoing(eid, src_id, dst)) + } } diff --git a/raphtory-storage/src/graph/nodes/node_ref.rs b/raphtory-storage/src/graph/nodes/node_ref.rs index 8f127f065e..1891997374 100644 --- a/raphtory-storage/src/graph/nodes/node_ref.rs +++ b/raphtory-storage/src/graph/nodes/node_ref.rs @@ -120,7 +120,7 @@ impl<'a> NodeStorageOps<'a> for NodeStorageRef<'a> { } fn tprop(self, prop_id: usize) -> impl TPropOps<'a> { - for_all_iter!(self, node => node.tprop(prop_id)) + for_all_iter!(self, node => node.t_prop(&LayerIds::All, prop_id)) } fn edges_iter(self, layers: &LayerIds, dir: Direction) -> impl Iterator + 'a { @@ -144,11 +144,11 @@ impl<'a> NodeStorageOps<'a> for NodeStorageRef<'a> { } fn find_edge(self, dst: VID, layer_ids: &LayerIds) -> Option { - for_all!(self, node => NodeStorageOps::find_edge(node, dst, layer_ids)) + for_all!(self, node => node.find_edge(dst, layer_ids)) } fn prop(self, prop_id: usize) -> Option { - for_all!(self, node => node.prop(prop_id)) + for_all!(self, node => node.c_prop(prop_id)) } fn tprops(self) -> impl Iterator)> { From 14120574499544421cba974ccaca05ef87bdd47e Mon Sep 17 00:00:00 2001 From: Fadhil Abubaker Date: Mon, 23 Jun 2025 05:36:16 -0400 Subject: [PATCH 027/321] Set node_id and node_type as const props (#2148) * Reserve node_type as a const prop on GraphStore init * Reserve node_id as const prop on node init * Store node_id as const prop * Fix compiler errors * Add method to store node_type as const prop * small changes before merge --------- Co-authored-by: Fabian Murariu --- db4-storage/Cargo.toml | 7 +- db4-storage/src/pages/mod.rs | 125 ++---------------- db4-storage/src/pages/session.rs | 73 ++++++++-- raphtory-storage/src/core_ops.rs | 1 - raphtory-storage/src/mutation/addition_ops.rs | 21 +-- .../src/mutation/addition_ops_ext.rs | 83 ++++++------ raphtory/src/db/api/mutation/addition_ops.rs | 12 +- raphtory/src/db/api/storage/storage.rs | 2 +- 8 files changed, 132 insertions(+), 192 deletions(-) diff --git a/db4-storage/Cargo.toml b/db4-storage/Cargo.toml index 3b382524cc..5ae48498db 100644 --- a/db4-storage/Cargo.toml +++ b/db4-storage/Cargo.toml @@ -31,21 +31,20 @@ arrow-schema.workspace = true parquet.workspace = true bytemuck.workspace = true rayon.workspace = true -iter-enum = {workspace = true, features = ["rayon"]} +itertools.workspace = true thiserror.workspace = true proptest = {workspace = true, optional = true} tempfile = {workspace = true, optional = true} -itertools = {workspace = true, optional = true} +iter-enum = {workspace = true, features = ["rayon"]} chrono = {workspace = true, optional = true} [dev-dependencies] proptest.workspace = true tempfile.workspace = true -itertools.workspace = true chrono.workspace = true rayon.workspace = true [features] -test-utils = ["proptest", "tempfile", "itertools", "chrono"] +test-utils = ["proptest", "tempfile", "chrono"] default = ["test-utils"] \ No newline at end of file diff --git a/db4-storage/src/pages/mod.rs b/db4-storage/src/pages/mod.rs index 813b1e8291..7c1ab2881c 100644 --- a/db4-storage/src/pages/mod.rs +++ b/db4-storage/src/pages/mod.rs @@ -21,7 +21,7 @@ use node_page::writer::{NodeWriter, WriterPair}; use node_store::NodeStorageInner; use parking_lot::RwLockWriteGuard; use raphtory_api::core::{ - entities::properties::{meta::Meta, prop::Prop}, + entities::properties::{meta::Meta, prop::{Prop, PropType}}, storage::dict_mapper::MaybeNew, }; use raphtory_core::{ @@ -42,6 +42,10 @@ pub mod session; #[cfg(feature = "test-utils")] pub mod test_utils; +// Internal const props for node id and type +pub const NODE_ID_PROP_KEY: &str = "_raphtory_node_id"; +pub const NODE_TYPE_PROP_KEY: &str = "_raphtory_node_type"; + #[derive(Debug)] pub struct GraphStore { nodes: Arc>, @@ -163,6 +167,12 @@ impl, ES: EdgeSegmentOps, E )); let edge_meta = edges.prop_meta(); let node_meta = nodes.prop_meta(); + + // Reserve node_type as a const prop on init + let _ = node_meta + .const_prop_meta() + .get_or_create_and_validate(NODE_TYPE_PROP_KEY, PropType::Str); + let graph_meta = GraphMeta { max_page_len_nodes, max_page_len_edges, @@ -220,119 +230,6 @@ impl, ES: EdgeSegmentOps, E Ok(elid) } - /// Adds an edge if it doesn't exist yet, does nothing if the edge is there - // pub fn internal_add_edge( - // &self, - // t: T, - // src: impl Into, - // dst: impl Into, - // lsn: u64, - // props: impl IntoIterator, - // ) -> Result, DBV4Error> { - // let src = src.into(); - // let dst = dst.into(); - - // let (src_chunk, src_pos) = self.nodes.resolve_pos(src); - // let (dst_chunk, dst_pos) = self.nodes.resolve_pos(dst); - - // self.nodes.grow(src_chunk.max(dst_chunk) + 1); - - // let src_page = &self.nodes.pages()[src_chunk]; - // // let dst_page = &self.nodes.pages()[dst_chunk]; - - // let acquire_node_writers = || { - // // let writer_pair = if src_chunk < dst_chunk { - // // let src_writer = src_page.writer::(); - // // let dst_writer = dst_page.writer::(); - // // WriterPair::Different { - // // writer_i: src_writer, - // // writer_j: dst_writer, - // // } - // // } else if src_chunk > dst_chunk { - // // let dst_writer = dst_page.writer::(); - // // let src_writer = src_page.writer::(); - // // WriterPair::Different { - // // writer_i: src_writer, - // // writer_j: dst_writer, - // // } - // // } else { - // // let writer = src_page.writer::(); - // // WriterPair::Same { writer } - // // }; - // // writer_pair - - // // let mut loop_count = 0; - // loop { - // if src_chunk == dst_chunk { - // if let Some(writer) = self - // .nodes() - // .try_writer(src_chunk) - // { - // return WriterPair::Same { writer }; - // } - // } else { - // if let Some(writer_i) = self - // .nodes - // .try_writer(src_chunk, self.persistence.strategy()) - // { - // if let Some(writer_j) = self - // .nodes - // .try_writer(dst_chunk, self.persistence.strategy()) - // { - // return WriterPair::Different { writer_i, writer_j }; - // } - // } - // } - // } - // }; - - // if let Some(e_id) = src_page.disk_get_out_edge(src_pos, dst) { - // let mut edge_writer = self.edges.get_writer(e_id); - // let (_, edge_pos) = self.edges.resolve_pos(e_id); - // edge_writer.add_edge(t, Some(edge_pos), src, dst, props, lsn, None)?; - - // let mut node_writers = acquire_node_writers(); - // node_writers - // .get_mut_i() - // .update_timestamp(t, src_pos, e_id, lsn); - // node_writers - // .get_mut_j() - // .update_timestamp(t, dst_pos, e_id, lsn); - - // Ok(MaybeNew::Existing(e_id)) - // } else { - // let mut node_writers = acquire_node_writers(); - - // if let Some(e_id) = node_writers.get_mut_i().get_out_edge(src_pos, dst) { - // let mut edge_writer = self.edges.get_writer(e_id); - // let (_, edge_pos) = self.edges.resolve_pos(e_id); - - // edge_writer.add_edge(t, Some(edge_pos), src, dst, props, lsn, None)?; - // node_writers - // .get_mut_i() - // .update_timestamp(t, src_pos, e_id, lsn); - // node_writers - // .get_mut_j() - // .update_timestamp(t, dst_pos, e_id, lsn); - - // Ok(MaybeNew::Existing(e_id)) - // } else { - // let mut edge_writer = self.get_free_writer(); - // let edge_id = edge_writer.add_edge(t, None, src, dst, props, lsn, None)?; - // let edge_id = edge_id.as_eid(edge_writer.segment_id(), self.edges.max_page_len()); - - // node_writers - // .get_mut_i() - // .add_outbound_edge(t, src_pos, dst, edge_id, lsn); - // node_writers - // .get_mut_j() - // .add_inbound_edge(t, dst_pos, src, edge_id, lsn); - - // Ok(MaybeNew::New(edge_id)) - // } - // } - // } - fn as_time_index_entry(&self, t: T) -> Result { let input_time = t.try_into_input_time()?; let t = match input_time { diff --git a/db4-storage/src/pages/session.rs b/db4-storage/src/pages/session.rs index 39c1d348df..d5faa9a8e7 100644 --- a/db4-storage/src/pages/session.rs +++ b/db4-storage/src/pages/session.rs @@ -6,16 +6,15 @@ use super::{ use crate::{ api::{edges::EdgeSegmentOps, nodes::NodeSegmentOps}, error::DBV4Error, + pages::{NODE_ID_PROP_KEY, NODE_TYPE_PROP_KEY}, segments::{edge::MemEdgeSegment, node::MemNodeSegment}, }; -use raphtory_api::core::{entities::properties::prop::Prop, storage::dict_mapper::MaybeNew}; +use raphtory_api::core::{entities::properties::prop::{Prop, PropType}, storage::dict_mapper::MaybeNew}; use raphtory_core::{ entities::{EID, ELID, GidRef, VID}, storage::timeindex::AsTime, }; -const NODE_ID_CONST_PROP_NAME: &str = "_raphtory_node_id"; - pub struct WriteSession< 'a, MNS: DerefMut + 'a, @@ -63,6 +62,7 @@ impl< let dst = dst.into(); let e_id = edge.inner(); let layer = e_id.layer(); + assert!(layer > 0, "Edge must be in a layer greater than 0"); let (_, src_pos) = self.graph.nodes().resolve_pos(src); @@ -72,16 +72,19 @@ impl< let edge_max_page_len = writer.writer.get_or_create_layer(layer).max_page_len(); let (_, edge_pos) = resolve_pos(e_id.edge, edge_max_page_len); let exists = Some(!edge.is_new()); + writer.add_edge(t, Some(edge_pos), src, dst, props, layer, lsn, exists); } else { let mut writer = self.graph.edge_writer(e_id.edge); let edge_max_page_len = writer.writer.get_or_create_layer(layer).max_page_len(); let (_, edge_pos) = resolve_pos(e_id.edge, edge_max_page_len); let exists = Some(!edge.is_new()); + writer.add_edge(t, Some(edge_pos), src, dst, props, layer, lsn, exists); } let edge_id = edge.inner(); + if edge_id.layer() > 0 { if edge.is_new() || self @@ -225,17 +228,63 @@ impl< } } - pub fn store_node_id(&mut self, id: GidRef, vid: impl Into) -> Result<(), DBV4Error> { - // node ids go to const props in layer 0 - let layer = 0; - let prop_name = NODE_ID_CONST_PROP_NAME; - let prop_val = match id { - GidRef::U64(id) => Prop::U64(id), - GidRef::Str(id) => Prop::Str(id.into()), + pub fn store_node_id_as_prop( + &mut self, + id: GidRef, + vid: impl Into, + ) -> Result<(), DBV4Error> { + let (prop_val, prop_dtype) = match id { + GidRef::U64(id) => (Prop::U64(id), PropType::U64), + GidRef::Str(id) => (Prop::Str(id.into()), PropType::Str), }; - let props = vec![(prop_name, prop_val)]; - self.graph.update_node_const_props(vid, layer, props)?; + self.store_node_const_prop( + NODE_ID_PROP_KEY, + prop_val, + prop_dtype, + vid, + ) + } + + pub fn store_node_type_as_prop( + &mut self, + node_type: &str, + vid: impl Into, + ) -> Result<(), DBV4Error> { + let (prop_val, prop_dtype) = (Prop::Str(node_type.into()), PropType::Str); + + self.store_node_const_prop( + NODE_TYPE_PROP_KEY, + prop_val, + prop_dtype, + vid, + ) + } + + fn store_node_const_prop( + &mut self, + prop_key: &str, + prop_val: Prop, + prop_dtype: PropType, + vid: impl Into, + ) -> Result<(), DBV4Error> { + let layer = 0; + let prop_id = self.graph + .node_meta() + .const_prop_meta() + .get_and_validate(prop_key, prop_dtype)? + .ok_or_else(|| DBV4Error::GenericFailure(format!( + "{} const prop not found", + prop_key + )))?; + + let props = vec![(prop_id, prop_val)]; + let (_, local_pos) = self.graph.nodes().resolve_pos(vid); + let lsn = 0; // TODO: lsn should be passed in + + self.node_writers + .get_mut_src() + .update_c_props(local_pos, layer, props, lsn); Ok(()) } diff --git a/raphtory-storage/src/core_ops.rs b/raphtory-storage/src/core_ops.rs index 192c0e0651..8bce9899e5 100644 --- a/raphtory-storage/src/core_ops.rs +++ b/raphtory-storage/src/core_ops.rs @@ -4,7 +4,6 @@ use crate::graph::{ locked::LockedGraph, nodes::{node_entry::NodeStorageEntry, node_storage_ops::NodeStorageOps, nodes::NodesStorage}, }; -use db4_graph::ReadLockedTemporalGraph; use raphtory_api::{ core::{ entities::{ diff --git a/raphtory-storage/src/mutation/addition_ops.rs b/raphtory-storage/src/mutation/addition_ops.rs index 4024c97c09..5a6de24fe4 100644 --- a/raphtory-storage/src/mutation/addition_ops.rs +++ b/raphtory-storage/src/mutation/addition_ops.rs @@ -77,8 +77,8 @@ pub trait AtomicAdditionOps: Send + Sync { props: impl IntoIterator, ) -> MaybeNew; - /// Sets id as a const prop within the node - fn store_node_id(&self, id: NodeRef, vid: impl Into); + /// Stores id as a const prop within the node + fn store_node_id_as_prop(&mut self, id: NodeRef, vid: impl Into); } pub trait SessionAdditionOps: Send + Sync { @@ -153,7 +153,7 @@ impl AtomicAdditionOps for TGWriteSession<'_> { todo!("Atomic addition operations are not implemented for TGWriteSession"); } - fn store_node_id(&self, id: NodeRef, vid: impl Into) { + fn store_node_id_as_prop(&mut self, id: NodeRef, vid: impl Into) { todo!("set_node_id is not implemented for TGWriteSession"); } } @@ -297,8 +297,9 @@ impl InternalAdditionOps for GraphStorage { } fn resolve_layer(&self, layer: Option<&str>) -> Result, Self::Error> { - let id = self.mutable()?.resolve_layer_inner(layer)?; - Ok(id) + // let id = self.mutable()?.resolve_layer_inner(layer)?; + // Ok(id) + todo!("remove this once we have a mutable graph storage"); } fn resolve_node(&self, id: NodeRef) -> Result, Self::Error> { @@ -314,9 +315,10 @@ impl InternalAdditionOps for GraphStorage { } fn write_session(&self) -> Result, Self::Error> { - Ok(TGWriteSession { - tg: self.mutable()?, - }) + // Ok(TGWriteSession { + // tg: self.mutable()?, + // }) + todo!("remove this once we have a mutable graph storage"); } fn atomic_add_edge( @@ -354,7 +356,8 @@ impl InternalAdditionOps for TemporalGraph { type AtomicAddEdge<'a> = TGWriteSession<'a>; fn write_lock(&self) -> Result { - Ok(WriteLockedGraph::new(self)) + // Ok(WriteLockedGraph::new(self)) + todo!("remove this once we have a mutable graph storage"); } fn write_lock_nodes(&self) -> Result { diff --git a/raphtory-storage/src/mutation/addition_ops_ext.rs b/raphtory-storage/src/mutation/addition_ops_ext.rs index 3674d792a5..ab535a97ea 100644 --- a/raphtory-storage/src/mutation/addition_ops_ext.rs +++ b/raphtory-storage/src/mutation/addition_ops_ext.rs @@ -3,7 +3,10 @@ use std::ops::DerefMut; use db4_graph::TemporalGraph; use parking_lot::RwLockWriteGuard; use raphtory_api::core::{ - entities::properties::prop::{Prop, PropType}, + entities::properties::{ + meta::Meta, + prop::{Prop, PropType}, + }, storage::dict_mapper::MaybeNew, }; use raphtory_core::{ @@ -11,7 +14,7 @@ use raphtory_core::{ storage::{raw_edges::WriteLockedEdges, timeindex::TimeIndexEntry, WriteLockedNodes}, }; use storage::{ - pages::session::WriteSession, + pages::{session::WriteSession, NODE_ID_PROP_KEY}, persist::strategy::PersistentStrategy, properties::props_meta_writer::PropsMetaWriter, segments::{edge::MemEdgeSegment, node::MemNodeSegment}, @@ -62,20 +65,22 @@ impl< .static_session .add_static_edge(src, dst, lsn) .map(|eid| eid.with_layer(0)); - self.layer.as_mut().map(|layer| { - layer.add_edge_into_layer(t, src, dst, eid, lsn, props); - }); + + self.static_session + .add_edge_into_layer(t, src, dst, eid, lsn, props); + + // TODO: consider storing node id as const prop here? eid } - fn store_node_id(&self, id: NodeRef, vid: impl Into) { + fn store_node_id_as_prop(&mut self, id: NodeRef, vid: impl Into) { match id { NodeRef::External(id) => { let vid = vid.into(); - self.static_session.store_node_id(id, vid) + let _ = self.static_session.store_node_id_as_prop(id, vid); } - NodeRef::Internal(id) => Ok(()), + NodeRef::Internal(_) => (), } } } @@ -180,40 +185,7 @@ impl, ES = ES>> InternalAdditionOps fn resolve_layer(&self, layer: Option<&str>) -> Result, Self::Error> { let id = self.edge_meta().get_or_create_layer_id(layer); - - let layer_id = id.inner(); - if self.layers().get(layer_id).is_some() { - return Ok(id); - } - let count = self.layers().count(); - if count >= layer_id + 1 { - // something has allocated the layer, wait for it to be added - while self.layers().get(layer_id).is_none() { - // wait for the layer to be created - std::thread::yield_now(); - } - return Ok(id); - } else { - self.layers().reserve(2); - let layer_name = layer.unwrap_or("_default"); - loop { - let new_layer_id = self.layers().push_with(|_| { - Layer::new( - self.graph_dir().join(format!("l_{}", layer_name)), - self.max_page_len_nodes(), - self.max_page_len_edges(), - ) - .into() - }); - if new_layer_id >= layer_id { - while self.layers().get(new_layer_id).is_none() { - // wait for the layer to be created - std::thread::yield_now(); - } - return Ok(id); - } - } - } + Ok(id) } fn resolve_node(&self, id: NodeRef) -> Result, Self::Error> { @@ -222,11 +194,16 @@ impl, ES = ES>> InternalAdditionOps let id = self .logical_to_physical .get_or_init_vid(id, || { + // When initializing a new node, reserve node_id as a const prop. + // Done here since the id type is not known until node creation. + reserve_node_id_as_prop(self.node_meta(), id); + self.node_count .fetch_add(1, std::sync::atomic::Ordering::Relaxed) .into() }) .map_err(MutationError::InvalidNodeId)?; + Ok(id) } NodeRef::Internal(id) => Ok(MaybeNew::Existing(id)), @@ -262,12 +239,11 @@ impl, ES = ES>> InternalAdditionOps e_id: Option, layer_id: usize, ) -> Self::AtomicAddEdge<'_> { - let static_session = self.static_graph().write_session(src, dst, e_id); - let layer = &self.layers()[layer_id]; - let layer = layer.write_session(src, dst, e_id); + let static_session = self.storage().write_session(src, dst, e_id); + WriteS { static_session, - layer: Some(layer), + layer: None, } } @@ -289,3 +265,18 @@ impl, ES = ES>> InternalAdditionOps } } } + +fn reserve_node_id_as_prop(node_meta: &Meta, id: GidRef) -> usize { + match id { + GidRef::U64(_) => node_meta + .const_prop_meta() + .get_or_create_and_validate(NODE_ID_PROP_KEY, PropType::U64) + .unwrap() + .inner(), + GidRef::Str(_) => node_meta + .const_prop_meta() + .get_or_create_and_validate(NODE_ID_PROP_KEY, PropType::Str) + .unwrap() + .inner(), + } +} diff --git a/raphtory/src/db/api/mutation/addition_ops.rs b/raphtory/src/db/api/mutation/addition_ops.rs index 16b682484c..6fa9ff4d12 100644 --- a/raphtory/src/db/api/mutation/addition_ops.rs +++ b/raphtory/src/db/api/mutation/addition_ops.rs @@ -182,6 +182,7 @@ impl> + StaticGraphViewOps> Addit v_id } }; + match v_id { New(id) => { let properties = props.collect_properties(|name, dtype| { @@ -190,9 +191,11 @@ impl> + StaticGraphViewOps> Addit .map_err(into_graph_err)? .inner()) })?; + session .internal_add_node(ti, id, &properties) .map_err(into_graph_err)?; + Ok(NodeView::new_internal(self.clone(), id)) } Existing(id) => { @@ -240,15 +243,14 @@ impl> + StaticGraphViewOps> Addit let layer_id = self.resolve_layer(layer).map_err(into_graph_err)?.inner(); let mut add_edge_op = self.atomic_add_edge(src_id, dst_id, None, layer_id); + let edge_id = add_edge_op.internal_add_edge(ti, src_id, dst_id, 0, layer_id, props); - let edge = add_edge_op.internal_add_edge(ti, src_id, dst_id, 0, layer_id, props); - - add_edge_op.store_node_id(src.as_node_ref(), src_id); - add_edge_op.store_node_id(dst.as_node_ref(), dst_id); + add_edge_op.store_node_id_as_prop(src.as_node_ref(), src_id); + add_edge_op.store_node_id_as_prop(dst.as_node_ref(), dst_id); Ok(EdgeView::new( self.clone(), - EdgeRef::new_outgoing(edge.inner().edge, src_id, dst_id).at_layer(layer_id), + EdgeRef::new_outgoing(edge_id.inner().edge, src_id, dst_id).at_layer(layer_id), )) } } diff --git a/raphtory/src/db/api/storage/storage.rs b/raphtory/src/db/api/storage/storage.rs index 87310fbf5e..34b1cecf59 100644 --- a/raphtory/src/db/api/storage/storage.rs +++ b/raphtory/src/db/api/storage/storage.rs @@ -223,7 +223,7 @@ impl AtomicAdditionOps for StorageWriteSession<'_> { todo!() } - fn store_node_id(&self, id: NodeRef, vid: impl Into) { + fn store_node_id_as_prop(&self, id: NodeRef, vid: impl Into) { todo!("set_node_id is not implemented for StorageWriteSession"); } } From f1739a32384876c9ada8821416a82866fe72b855 Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Mon, 23 Jun 2025 13:21:16 +0100 Subject: [PATCH 028/321] raphtory-storage nodes compiles --- db4-storage/src/api/nodes.rs | 39 +++- db4-storage/src/pages/edge_store.rs | 4 + db4-storage/src/pages/mod.rs | 33 ++- db4-storage/src/pages/node_store.rs | 4 + db4-storage/src/pages/session.rs | 29 +-- db4-storage/src/segments/edge.rs | 13 +- db4-storage/src/segments/mod.rs | 10 + db4-storage/src/segments/node.rs | 4 + db4-storage/src/segments/node_entry.rs | 15 +- .../src/core/entities/properties/prop/mod.rs | 2 + .../entities/properties/prop/prop_enum.rs | 29 ++- .../entities/properties/prop/prop_ref_enum.rs | 41 ++++ raphtory-core/src/storage/mod.rs | 25 ++- .../src/graph/nodes/node_entry.rs | 87 +++----- raphtory-storage/src/graph/nodes/node_ref.rs | 202 ++++-------------- .../src/graph/nodes/node_storage_ops.rs | 119 ++++++----- raphtory-storage/src/graph/nodes/nodes.rs | 2 +- raphtory-storage/src/graph/nodes/nodes_ref.rs | 4 +- 18 files changed, 336 insertions(+), 326 deletions(-) create mode 100644 raphtory-api/src/core/entities/properties/prop/prop_ref_enum.rs diff --git a/db4-storage/src/api/nodes.rs b/db4-storage/src/api/nodes.rs index c45aa468cf..12e6b6451a 100644 --- a/db4-storage/src/api/nodes.rs +++ b/db4-storage/src/api/nodes.rs @@ -1,4 +1,5 @@ use std::{ + borrow::Cow, ops::{Deref, DerefMut}, path::Path, sync::Arc, @@ -9,12 +10,16 @@ use parking_lot::{RwLockReadGuard, RwLockWriteGuard, lock_api::ArcRwLockReadGuar use raphtory_api::{ core::{ Direction, - entities::properties::{meta::Meta, prop::Prop, tprop::TPropOps}, + entities::properties::{ + meta::Meta, + prop::{Prop, PropUnwrap}, + tprop::TPropOps, + }, }, iter::IntoDynBoxed, }; use raphtory_core::{ - entities::{EID, LayerIds, VID, edges::edge_ref::EdgeRef}, + entities::{EID, GidRef, LayerIds, VID, edges::edge_ref::EdgeRef}, storage::timeindex::{TimeIndexEntry, TimeIndexOps}, utils::iter::GenLockedIter, }; @@ -212,6 +217,8 @@ pub trait NodeRefOps<'a>: Copy + Clone + Send + Sync { } } + fn node_meta(&self) -> &Arc; + fn temp_prop_rows( self, ) -> impl Iterator< @@ -254,9 +261,37 @@ pub trait NodeRefOps<'a>: Copy + Clone + Send + Sync { fn c_prop(self, layer_id: usize, prop_id: usize) -> Option; + fn c_prop_str(self, layer_id: usize, prop_id: usize) -> Option<&'a str>; + fn t_prop(self, layer_id: &'a LayerIds, prop_id: usize) -> Self::TProps; + fn t_props(self, layer_id: &'a LayerIds) -> impl Iterator { + (0..self.node_meta().temporal_prop_meta().len()) + .map(move |prop_id| (prop_id, self.t_prop(layer_id, prop_id))) + } + fn degree(self, layers: &LayerIds, dir: Direction) -> usize; fn find_edge(&self, dst: VID, layers: &LayerIds) -> Option; + + fn name(&self) -> Cow<'a, str> { + self.gid().to_str() + } + + fn gid(&self) -> GidRef<'a> { + self.c_prop_str(0, 1) + .map(GidRef::Str) + .or_else(|| { + self.c_prop(0, 1) + .and_then(|prop| prop.into_u64().map(GidRef::U64)) + }) + .expect("Node GID should be present") + } + + fn node_type_id(&self) -> usize { + self.c_prop(0, 0) + .and_then(|prop| prop.into_u64()) + .map(|id| id as usize) + .expect("Node type should be present") + } } diff --git a/db4-storage/src/pages/edge_store.rs b/db4-storage/src/pages/edge_store.rs index 9e2f584617..e0536d3a61 100644 --- a/db4-storage/src/pages/edge_store.rs +++ b/db4-storage/src/pages/edge_store.rs @@ -61,6 +61,10 @@ impl, EXT: Clone> EdgeStorageInner } } + pub fn edge_meta(&self) -> &Arc { + &self.prop_meta + } + pub fn layer( edges_path: impl AsRef, max_page_len: usize, diff --git a/db4-storage/src/pages/mod.rs b/db4-storage/src/pages/mod.rs index 7c1ab2881c..f694ec1456 100644 --- a/db4-storage/src/pages/mod.rs +++ b/db4-storage/src/pages/mod.rs @@ -21,7 +21,10 @@ use node_page::writer::{NodeWriter, WriterPair}; use node_store::NodeStorageInner; use parking_lot::RwLockWriteGuard; use raphtory_api::core::{ - entities::properties::{meta::Meta, prop::{Prop, PropType}}, + entities::properties::{ + meta::Meta, + prop::{Prop, PropType}, + }, storage::dict_mapper::MaybeNew, }; use raphtory_core::{ @@ -50,8 +53,6 @@ pub const NODE_TYPE_PROP_KEY: &str = "_raphtory_node_type"; pub struct GraphStore { nodes: Arc>, edges: Arc>, - edge_meta: Arc, - node_meta: Arc, earliest: AtomicI64, latest: AtomicI64, event_id: AtomicUsize, @@ -91,11 +92,11 @@ impl, ES: EdgeSegmentOps, E } pub fn edge_meta(&self) -> &Meta { - &self.edge_meta + self.edges.edge_meta() } pub fn node_meta(&self) -> &Meta { - &self.node_meta + self.nodes.node_meta() } pub fn earliest(&self) -> i64 { @@ -127,8 +128,6 @@ impl, ES: EdgeSegmentOps, E max_page_len_edges, ext.clone(), )?); - let edge_meta = edges.prop_meta().clone(); - let node_meta = nodes.prop_meta().clone(); let earliest = AtomicI64::new(edges.earliest().map(|t| t.t()).unwrap_or_default()); let latest = AtomicI64::new(edges.latest().map(|t| t.t()).unwrap_or_default()); @@ -137,8 +136,6 @@ impl, ES: EdgeSegmentOps, E Ok(Self { nodes, edges, - edge_meta, - node_meta, earliest, latest, event_id: AtomicUsize::new(t_len), @@ -165,13 +162,11 @@ impl, ES: EdgeSegmentOps, E max_page_len_edges, ext.clone(), )); - let edge_meta = edges.prop_meta(); - let node_meta = nodes.prop_meta(); - // Reserve node_type as a const prop on init - let _ = node_meta + let _ = nodes + .prop_meta() .const_prop_meta() - .get_or_create_and_validate(NODE_TYPE_PROP_KEY, PropType::Str); + .get_or_create_and_validate(NODE_TYPE_PROP_KEY, PropType::U64); let graph_meta = GraphMeta { max_page_len_nodes, @@ -183,8 +178,6 @@ impl, ES: EdgeSegmentOps, E Self { nodes: nodes.clone(), edges: edges.clone(), - edge_meta: edge_meta.clone(), - node_meta: node_meta.clone(), earliest: AtomicI64::new(0), latest: AtomicI64::new(0), event_id: AtomicUsize::new(0), @@ -211,7 +204,7 @@ impl, ES: EdgeSegmentOps, E _lsn: u64, ) -> Result, DBV4Error> { let t = self.as_time_index_entry(t)?; - let prop_writer = PropsMetaWriter::temporal(&self.edge_meta, props.into_iter())?; + let prop_writer = PropsMetaWriter::temporal(self.edge_meta(), props.into_iter())?; self.internal_add_edge(t, src, dst, 0, prop_writer.into_props_temporal()?) } @@ -254,7 +247,7 @@ impl, ES: EdgeSegmentOps, E let (src, dst) = edge_writer .get_edge(layer, edge_pos) .expect("Internal Error, EID should be checked at this point!"); - let prop_writer = PropsMetaWriter::constant(&self.edge_meta, props.into_iter())?; + let prop_writer = PropsMetaWriter::constant(self.edge_meta(), props.into_iter())?; edge_writer.update_c_props(edge_pos, src, dst, layer, prop_writer.into_props_const()?); @@ -270,7 +263,7 @@ impl, ES: EdgeSegmentOps, E let node = node.into(); let (segment, node_pos) = self.nodes.resolve_pos(node); let mut node_writer = self.nodes.writer(segment); - let prop_writer = PropsMetaWriter::constant(&self.node_meta, props.into_iter())?; + let prop_writer = PropsMetaWriter::constant(self.node_meta(), props.into_iter())?; node_writer.update_c_props(node_pos, layer_id, prop_writer.into_props_const()?, 0); // TODO: LSN Ok(()) } @@ -288,7 +281,7 @@ impl, ES: EdgeSegmentOps, E let t = self.as_time_index_entry(t)?; let mut node_writer = self.nodes.writer(segment); - let prop_writer = PropsMetaWriter::temporal(&self.node_meta, props.into_iter())?; + let prop_writer = PropsMetaWriter::temporal(self.node_meta(), props.into_iter())?; node_writer.add_props(t, node_pos, layer_id, prop_writer.into_props_temporal()?, 0); // TODO: LSN Ok(()) } diff --git a/db4-storage/src/pages/node_store.rs b/db4-storage/src/pages/node_store.rs index c38f211918..bcfc96cd29 100644 --- a/db4-storage/src/pages/node_store.rs +++ b/db4-storage/src/pages/node_store.rs @@ -95,6 +95,10 @@ impl, EXT: Clone> NodeStorageInner } } + pub fn node_meta(&self) -> &Arc { + &self.prop_meta + } + // pub fn locked<'a>(&'a self) -> WriteLockedNodePages<'a, NS> { // WriteLockedNodePages::new( // self.pages diff --git a/db4-storage/src/pages/session.rs b/db4-storage/src/pages/session.rs index d5faa9a8e7..97cba61418 100644 --- a/db4-storage/src/pages/session.rs +++ b/db4-storage/src/pages/session.rs @@ -9,7 +9,10 @@ use crate::{ pages::{NODE_ID_PROP_KEY, NODE_TYPE_PROP_KEY}, segments::{edge::MemEdgeSegment, node::MemNodeSegment}, }; -use raphtory_api::core::{entities::properties::prop::{Prop, PropType}, storage::dict_mapper::MaybeNew}; +use raphtory_api::core::{ + entities::properties::prop::{Prop, PropType}, + storage::dict_mapper::MaybeNew, +}; use raphtory_core::{ entities::{EID, ELID, GidRef, VID}, storage::timeindex::AsTime, @@ -238,12 +241,7 @@ impl< GidRef::Str(id) => (Prop::Str(id.into()), PropType::Str), }; - self.store_node_const_prop( - NODE_ID_PROP_KEY, - prop_val, - prop_dtype, - vid, - ) + self.store_node_const_prop(NODE_ID_PROP_KEY, prop_val, prop_dtype, vid) } pub fn store_node_type_as_prop( @@ -253,12 +251,7 @@ impl< ) -> Result<(), DBV4Error> { let (prop_val, prop_dtype) = (Prop::Str(node_type.into()), PropType::Str); - self.store_node_const_prop( - NODE_TYPE_PROP_KEY, - prop_val, - prop_dtype, - vid, - ) + self.store_node_const_prop(NODE_TYPE_PROP_KEY, prop_val, prop_dtype, vid) } fn store_node_const_prop( @@ -269,14 +262,14 @@ impl< vid: impl Into, ) -> Result<(), DBV4Error> { let layer = 0; - let prop_id = self.graph + let prop_id = self + .graph .node_meta() .const_prop_meta() .get_and_validate(prop_key, prop_dtype)? - .ok_or_else(|| DBV4Error::GenericFailure(format!( - "{} const prop not found", - prop_key - )))?; + .ok_or_else(|| { + DBV4Error::GenericFailure(format!("{} const prop not found", prop_key)) + })?; let props = vec![(prop_id, prop_val)]; let (_, local_pos) = self.graph.nodes().resolve_pos(vid); diff --git a/db4-storage/src/segments/edge.rs b/db4-storage/src/segments/edge.rs index 46ed456a1f..14a99b01cb 100644 --- a/db4-storage/src/segments/edge.rs +++ b/db4-storage/src/segments/edge.rs @@ -47,9 +47,12 @@ pub struct MemEdgeSegment { impl>> From for MemEdgeSegment { fn from(inner: I) -> Self { - Self { - layers: inner.into_iter().collect(), - } + let layers: Vec<_> = inner.into_iter().collect(); + assert!( + !layers.is_empty(), + "MemEdgeSegment must have at least one layer" + ); + Self { layers } } } @@ -72,6 +75,10 @@ impl MemEdgeSegment { } } + pub fn edge_meta(&self) -> &Arc { + self.layers[0].meta() + } + pub fn get_or_create_layer(&mut self, layer_id: usize) -> &mut SegmentContainer { if layer_id >= self.layers.len() { let max_page_len = self.layers[0].max_page_len(); diff --git a/db4-storage/src/segments/mod.rs b/db4-storage/src/segments/mod.rs index 109c8dc615..b3b166eb44 100644 --- a/db4-storage/src/segments/mod.rs +++ b/db4-storage/src/segments/mod.rs @@ -231,6 +231,16 @@ impl SegmentContainer { }) } + pub fn c_prop_str(&self, item_id: impl Into, prop_id: usize) -> Option<&str> { + let item_id = item_id.into(); + self.data.get(&item_id).and_then(|entry| { + let prop_entry = self.properties.c_column(prop_id)?; + prop_entry + .get_ref(entry.row()) + .and_then(|prop| prop.as_str()) + }) + } + pub fn additions(&self, item_pos: LocalPOS) -> &PropTimestamps { self.data .get(&item_pos) diff --git a/db4-storage/src/segments/node.rs b/db4-storage/src/segments/node.rs index 286ef5e166..6baadd478b 100644 --- a/db4-storage/src/segments/node.rs +++ b/db4-storage/src/segments/node.rs @@ -103,6 +103,10 @@ impl MemNodeSegment { &mut self.layers[layer_id] } + pub fn node_meta(&self) -> &Arc { + self.layers[0].meta() + } + pub fn get_layer(&self, layer_id: usize) -> Option<&SegmentContainer> { self.layers.get(layer_id) } diff --git a/db4-storage/src/segments/node_entry.rs b/db4-storage/src/segments/node_entry.rs index 96d8ef1f74..87992b920d 100644 --- a/db4-storage/src/segments/node_entry.rs +++ b/db4-storage/src/segments/node_entry.rs @@ -7,13 +7,16 @@ use crate::{ }; use raphtory_api::core::{ Direction, - entities::{EID, VID, properties::prop::Prop}, + entities::{ + EID, VID, + properties::{meta::Meta, prop::Prop}, + }, }; use raphtory_core::{ entities::{LayerIds, edges::edge_ref::EdgeRef, properties::tprop::TPropCell}, storage::timeindex::{TimeIndexEntry, TimeIndexOps}, }; -use std::{iter::Empty, ops::Deref}; +use std::{iter::Empty, ops::Deref, sync::Arc}; use super::additions::MemAdditions; @@ -116,6 +119,10 @@ impl<'a> NodeRefOps<'a> for MemNodeRef<'a> { type Additions = NodeAdditions<'a>; type TProps = NodeTProps<'a>; + fn node_meta(&self) -> &Arc { + self.ns.node_meta() + } + fn vid(&self) -> VID { self.ns.to_vid(self.pos) } @@ -144,6 +151,10 @@ impl<'a> NodeRefOps<'a> for MemNodeRef<'a> { self.ns.as_ref()[layer_id].c_prop(self.pos, prop_id) } + fn c_prop_str(self, layer_id: usize, prop_id: usize) -> Option<&'a str> { + self.ns.as_ref()[layer_id].c_prop_str(self.pos, prop_id) + } + fn t_prop(self, layer_id: &'a LayerIds, prop_id: usize) -> Self::TProps { NodeTProps::new(self, layer_id, prop_id) } diff --git a/raphtory-api/src/core/entities/properties/prop/mod.rs b/raphtory-api/src/core/entities/properties/prop/mod.rs index 3b449d5059..a7e4ff1952 100644 --- a/raphtory-api/src/core/entities/properties/prop/mod.rs +++ b/raphtory-api/src/core/entities/properties/prop/mod.rs @@ -1,6 +1,7 @@ #[cfg(feature = "arrow")] mod prop_array; mod prop_enum; +mod prop_ref_enum; mod prop_type; mod prop_unwrap; #[cfg(feature = "io")] @@ -12,5 +13,6 @@ mod template; #[cfg(feature = "arrow")] pub use prop_array::*; pub use prop_enum::*; +pub use prop_ref_enum::*; pub use prop_type::*; pub use prop_unwrap::*; diff --git a/raphtory-api/src/core/entities/properties/prop/prop_enum.rs b/raphtory-api/src/core/entities/properties/prop/prop_enum.rs index 4627c81c1e..e2505ecf68 100644 --- a/raphtory-api/src/core/entities/properties/prop/prop_enum.rs +++ b/raphtory-api/src/core/entities/properties/prop/prop_enum.rs @@ -1,4 +1,7 @@ -use crate::core::{entities::properties::prop::PropType, storage::arc_str::ArcStr}; +use crate::core::{ + entities::properties::prop::{prop_ref_enum::PropRef, PropType}, + storage::arc_str::ArcStr, +}; use bigdecimal::{num_bigint::BigInt, BigDecimal}; use chrono::{DateTime, NaiveDateTime, Utc}; use itertools::Itertools; @@ -46,6 +49,30 @@ pub enum Prop { Decimal(BigDecimal), } +impl<'a> From> for Prop { + fn from(prop_ref: PropRef<'a>) -> Self { + match prop_ref { + PropRef::Str(s) => Prop::str(s), + PropRef::U8(u) => Prop::U8(u), + PropRef::U16(u) => Prop::U16(u), + PropRef::I32(i) => Prop::I32(i), + PropRef::I64(i) => Prop::I64(i), + PropRef::U32(u) => Prop::U32(u), + PropRef::U64(u) => Prop::U64(u), + PropRef::F32(f) => Prop::F32(f), + PropRef::F64(f) => Prop::F64(f), + PropRef::Bool(b) => Prop::Bool(b), + PropRef::List(v) => Prop::List(v.clone()), + PropRef::Map(m) => Prop::Map(m.clone()), + PropRef::NDTime(dt) => Prop::NDTime(dt.clone()), + PropRef::DTime(dt) => Prop::DTime(dt.clone()), + #[cfg(feature = "arrow")] + PropRef::Array(arr) => Prop::Array(arr.clone()), + PropRef::Decimal(d) => Prop::Decimal(d.clone()), + } + } +} + impl Hash for Prop { fn hash(&self, state: &mut H) { match self { diff --git a/raphtory-api/src/core/entities/properties/prop/prop_ref_enum.rs b/raphtory-api/src/core/entities/properties/prop/prop_ref_enum.rs new file mode 100644 index 0000000000..9283b20398 --- /dev/null +++ b/raphtory-api/src/core/entities/properties/prop/prop_ref_enum.rs @@ -0,0 +1,41 @@ +use std::sync::Arc; + +use bigdecimal::BigDecimal; +use chrono::{DateTime, NaiveDateTime, Utc}; +use rustc_hash::FxHashMap; + +#[cfg(feature = "arrow")] +use crate::core::entities::properties::prop::PropArray; +use crate::core::{entities::properties::prop::Prop, storage::arc_str::ArcStr}; + +#[derive(Debug, PartialEq, Clone)] +// TODO: this needs more refinement, as it's not generic enough for all the storage types +pub enum PropRef<'a> { + Str(&'a str), + U8(u8), + U16(u16), + I32(i32), + I64(i64), + U32(u32), + U64(u64), + F32(f32), + F64(f64), + Bool(bool), + List(&'a Arc>), + Map(&'a Arc>), + NDTime(&'a NaiveDateTime), + DTime(&'a DateTime), + #[cfg(feature = "arrow")] + Array(&'a PropArray), + Decimal(&'a BigDecimal), +} + +impl<'a> PropRef<'a> { + pub fn as_str(&self) -> Option<&'a str> { + if let PropRef::Str(s) = self { + Some(s) + } else { + None + } + } +} diff --git a/raphtory-core/src/storage/mod.rs b/raphtory-core/src/storage/mod.rs index edf9d3e767..2b806dd167 100644 --- a/raphtory-core/src/storage/mod.rs +++ b/raphtory-core/src/storage/mod.rs @@ -15,7 +15,7 @@ use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard}; use raphtory_api::core::entities::properties::prop::PropArray; use raphtory_api::core::{ entities::{ - properties::prop::{Prop, PropType}, + properties::prop::{Prop, PropRef, PropType}, GidRef, VID, }, storage::arc_str::ArcStr, @@ -390,6 +390,29 @@ impl PropColumn { } } + pub fn get_ref(&self, index: usize) -> Option { + match self { + PropColumn::Bool(col) => col.get_opt(index).map(|prop| PropRef::Bool(*prop)), + PropColumn::I64(col) => col.get_opt(index).map(|prop| PropRef::I64(*prop)), + PropColumn::U32(col) => col.get_opt(index).map(|prop| PropRef::U32(*prop)), + PropColumn::U64(col) => col.get_opt(index).map(|prop| PropRef::U64(*prop)), + PropColumn::F32(col) => col.get_opt(index).map(|prop| PropRef::F32(*prop)), + PropColumn::F64(col) => col.get_opt(index).map(|prop| PropRef::F64(*prop)), + PropColumn::Str(col) => col.get_opt(index).map(|prop| PropRef::Str(prop.as_ref())), + #[cfg(feature = "arrow")] + PropColumn::Array(col) => col.get_opt(index).map(PropRef::Array), + PropColumn::U8(col) => col.get_opt(index).map(|prop| PropRef::U8(*prop)), + PropColumn::U16(col) => col.get_opt(index).map(|prop| PropRef::U16(*prop)), + PropColumn::I32(col) => col.get_opt(index).map(|prop| PropRef::I32(*prop)), + PropColumn::List(col) => col.get_opt(index).map(PropRef::List), + PropColumn::Map(col) => col.get_opt(index).map(PropRef::Map), + PropColumn::NDTime(col) => col.get_opt(index).map(PropRef::NDTime), + PropColumn::DTime(col) => col.get_opt(index).map(PropRef::DTime), + PropColumn::Decimal(col) => col.get_opt(index).map(PropRef::Decimal), + PropColumn::Empty(_) => None, + } + } + pub(crate) fn len(&self) -> usize { match self { PropColumn::Bool(col) => col.len(), diff --git a/raphtory-storage/src/graph/nodes/node_entry.rs b/raphtory-storage/src/graph/nodes/node_entry.rs index e68845d9b2..17694e6e1d 100644 --- a/raphtory-storage/src/graph/nodes/node_entry.rs +++ b/raphtory-storage/src/graph/nodes/node_entry.rs @@ -1,34 +1,17 @@ -use crate::graph::{ - nodes::{node_ref::NodeStorageRef, node_storage_ops::NodeStorageOps}, - variants::storage_variants3::StorageVariants3, +use crate::graph::nodes::{node_ref::NodeStorageRef, node_storage_ops::NodeStorageOps}; +use raphtory_api::core::{ + entities::{edges::edge_ref::EdgeRef, properties::prop::Prop, GidRef, LayerIds, VID}, + Direction, }; -use raphtory_api::{ - core::{ - entities::{ - edges::edge_ref::EdgeRef, - properties::{prop::Prop, tprop::TPropOps}, - GidRef, LayerIds, VID, - }, - Direction, - }, - iter::BoxedLIter, -}; -use raphtory_core::utils::iter::GenLockedIter; -use std::borrow::Cow; use storage::{ - api::nodes::{NodeEntryOps, NodeRefOps}, + api::nodes::{self, NodeEntryOps}, + utils::Iter2, NodeEntry, NodeEntryRef, }; -#[cfg(feature = "storage")] -use crate::disk::storage_interface::node::DiskNode; -use crate::graph::nodes::node_additions::NodeAdditions; - pub enum NodeStorageEntry<'a> { Mem(NodeEntryRef<'a>), Unlocked(NodeEntry<'a>), - #[cfg(feature = "storage")] - Disk(DiskNode<'a>), } impl<'a> From> for NodeStorageEntry<'a> { @@ -43,21 +26,12 @@ impl<'a> From> for NodeStorageEntry<'a> { } } -#[cfg(feature = "storage")] -impl<'a> From> for NodeStorageEntry<'a> { - fn from(value: DiskNode<'a>) -> Self { - NodeStorageEntry::Disk(value) - } -} - impl<'a> NodeStorageEntry<'a> { #[inline] pub fn as_ref(&self) -> NodeStorageRef { match self { - NodeStorageEntry::Mem(entry) => NodeStorageRef::Mem(*entry), - NodeStorageEntry::Unlocked(entry) => NodeStorageRef::Mem(entry.as_ref()), - #[cfg(feature = "storage")] - NodeStorageEntry::Disk(node) => NodeStorageRef::Disk(*node), + NodeStorageEntry::Mem(entry) => *entry, + NodeStorageEntry::Unlocked(entry) => entry.as_ref(), } } } @@ -75,12 +49,10 @@ impl<'b> NodeStorageEntry<'b> { dir: Direction, ) -> impl Iterator + Send + Sync + 'b { match self { - NodeStorageEntry::Mem(entry) => StorageVariants3::Mem(entry.edges_iter(layers, dir)), - NodeStorageEntry::Unlocked(entry) => { - StorageVariants3::Unlocked(entry.into_edges(layers, dir)) + NodeStorageEntry::Mem(entry) => { + Iter2::I1(nodes::NodeRefOps::edges_iter(entry, layers, dir)) } - #[cfg(feature = "storage")] - NodeStorageEntry::Disk(node) => StorageVariants3::Disk(node.edges_iter(layers, dir)), + NodeStorageEntry::Unlocked(entry) => Iter2::I2(entry.into_edges(layers, dir)), } } @@ -112,15 +84,30 @@ impl<'a, 'b: 'a> NodeStorageOps<'a> for &'a NodeStorageEntry<'b> { self.as_ref().degree(layers, dir) } - fn additions(self) -> NodeAdditions<'a> { - self.as_ref().additions() + fn additions(self, layer_ids: &'a LayerIds) -> storage::NodeAdditions<'a> { + self.as_ref().additions(layer_ids) + } + + fn tprop(self, layer_ids: &'a LayerIds, prop_id: usize) -> storage::NodeTProps<'a> { + self.as_ref().tprop(layer_ids, prop_id) + } + + fn tprops( + self, + layer_ids: &'a LayerIds, + ) -> impl Iterator)> { + self.as_ref().tprops(layer_ids) } - fn tprop(self, prop_id: usize) -> impl TPropOps<'a> { - self.as_ref().tprop(prop_id) + fn prop(self, layer_id: usize, prop_id: usize) -> Option { + self.as_ref().prop(layer_id, prop_id) } - fn edges_iter(self, layers: &LayerIds, dir: Direction) -> impl Iterator + 'a { + fn edges_iter( + self, + layers: &LayerIds, + dir: Direction, + ) -> impl Iterator + Send + Sync + 'a { self.as_ref().edges_iter(layers, dir) } @@ -136,19 +123,7 @@ impl<'a, 'b: 'a> NodeStorageOps<'a> for &'a NodeStorageEntry<'b> { self.as_ref().id() } - fn name(self) -> Option> { - self.as_ref().name() - } - fn find_edge(self, dst: VID, layer_ids: &LayerIds) -> Option { self.as_ref().find_edge(dst, layer_ids) } - - fn prop(self, prop_id: usize) -> Option { - self.as_ref().prop(prop_id) - } - - fn tprops(self) -> impl Iterator)> { - self.as_ref().tprops() - } } diff --git a/raphtory-storage/src/graph/nodes/node_ref.rs b/raphtory-storage/src/graph/nodes/node_ref.rs index 1891997374..4c03a8486d 100644 --- a/raphtory-storage/src/graph/nodes/node_ref.rs +++ b/raphtory-storage/src/graph/nodes/node_ref.rs @@ -1,165 +1,45 @@ -use super::row::Row; -use crate::graph::{ - nodes::{node_additions::NodeAdditions, node_storage_ops::NodeStorageOps}, - variants::storage_variants2::StorageVariants2, -}; -use raphtory_api::{ - core::{ - entities::{ - edges::edge_ref::EdgeRef, - properties::{prop::Prop, tprop::TPropOps}, - GidRef, LayerIds, VID, - }, - storage::timeindex::TimeIndexEntry, - Direction, - }, - iter::IntoDynBoxed, -}; -use raphtory_core::storage::node_entry::NodePtr; -use std::{borrow::Cow, ops::Range}; -use storage::{api::nodes::NodeRefOps, NodeEntryRef}; +use storage::NodeEntryRef; #[cfg(feature = "storage")] use crate::disk::storage_interface::node::DiskNode; -#[derive(Copy, Clone, Debug)] -pub enum NodeStorageRef<'a> { - Mem(NodeEntryRef<'a>), - #[cfg(feature = "storage")] - Disk(DiskNode<'a>), -} - -impl<'a> From> for NodeStorageRef<'a> { - fn from(value: NodeEntryRef<'a>) -> Self { - NodeStorageRef::Mem(value) - } -} - -impl<'a> NodeStorageRef<'a> { - pub fn temp_prop_rows(self) -> impl Iterator)> + 'a { - // match self { - // NodeStorageRef::Mem(node_entry) => node_entry - // .into_rows() - // .map(|(t, row)| (t, Row::Mem(row))) - // .into_dyn_boxed(), - // #[cfg(feature = "storage")] - // NodeStorageRef::Disk(disk_node) => disk_node.into_rows().into_dyn_boxed(), - // } - //TODO: - std::iter::empty() - } - - pub fn temp_prop_rows_window( - self, - window: Range, - ) -> impl Iterator)> + 'a { - // match self { - // NodeStorageRef::Mem(node_entry) => node_entry - // .into_rows_window(window) - // .map(|(t, row)| (t, Row::Mem(row))) - // .into_dyn_boxed(), - // #[cfg(feature = "storage")] - // NodeStorageRef::Disk(disk_node) => disk_node.into_rows_window(window).into_dyn_boxed(), - // } - std::iter::empty() - } - - pub fn last_before_row(self, t: TimeIndexEntry) -> Vec<(usize, Prop)> { - // match self { - // NodeStorageRef::Mem(node_entry) => node_entry.last_before_row(t), - // #[cfg(feature = "storage")] - // NodeStorageRef::Disk(disk_node) => disk_node.last_before_row(t), - // } - todo!() - } -} - -#[cfg(feature = "storage")] -impl<'a> From> for NodeStorageRef<'a> { - fn from(value: DiskNode<'a>) -> Self { - NodeStorageRef::Disk(value) - } -} - -macro_rules! for_all { - ($value:expr, $pattern:pat => $result:expr) => { - match $value { - NodeStorageRef::Mem($pattern) => $result, - #[cfg(feature = "storage")] - NodeStorageRef::Disk($pattern) => $result, - } - }; -} - -#[cfg(feature = "storage")] -macro_rules! for_all_iter { - ($value:expr, $pattern:pat => $result:expr) => {{ - match $value { - NodeStorageRef::Mem($pattern) => StorageVariants2::Mem($result), - NodeStorageRef::Disk($pattern) => StorageVariants2::Disk($result), - } - }}; -} - -#[cfg(not(feature = "storage"))] -macro_rules! for_all_iter { - ($value:expr, $pattern:pat => $result:expr) => {{ - match $value { - NodeStorageRef::Mem($pattern) => $result, - } - }}; -} - -impl<'a> NodeStorageOps<'a> for NodeStorageRef<'a> { - fn degree(self, layers: &LayerIds, dir: Direction) -> usize { - for_all!(self, node => node.degree(layers, dir)) - } - - fn additions(self) -> storage::NodeAdditions<'a> { - for_all!(self, node => node.additions(&LayerIds::All)) - } - - fn tprop(self, prop_id: usize) -> impl TPropOps<'a> { - for_all_iter!(self, node => node.t_prop(&LayerIds::All, prop_id)) - } - - fn edges_iter(self, layers: &LayerIds, dir: Direction) -> impl Iterator + 'a { - for_all_iter!(self, node => node.edges_iter(layers, dir)) - } - - fn node_type_id(self) -> usize { - for_all!(self, node => node.node_type_id()) - } - - fn vid(self) -> VID { - for_all!(self, node => node.vid()) - } - - fn id(self) -> GidRef<'a> { - for_all!(self, node => node.id()) - } - - fn name(self) -> Option> { - for_all!(self, node => node.name()) - } - - fn find_edge(self, dst: VID, layer_ids: &LayerIds) -> Option { - for_all!(self, node => node.find_edge(dst, layer_ids)) - } - - fn prop(self, prop_id: usize) -> Option { - for_all!(self, node => node.c_prop(prop_id)) - } - - fn tprops(self) -> impl Iterator)> { - match self { - NodeStorageRef::Mem(node) => { - StorageVariants2::Mem(node.tprops().map(|(k, v)| (k, StorageVariants2::Mem(v)))) - } - #[cfg(feature = "storage")] - NodeStorageRef::Disk(node) => { - StorageVariants2::Disk(node.tprops().map(|(k, v)| (k, StorageVariants2::Disk(v)))) - } - } - } -} +pub type NodeStorageRef<'a> = NodeEntryRef<'a>; + +// impl<'a> NodeStorageRef<'a> { +// pub fn temp_prop_rows(self) -> impl Iterator)> + 'a { +// // match self { +// // NodeStorageRef::Mem(node_entry) => node_entry +// // .into_rows() +// // .map(|(t, row)| (t, Row::Mem(row))) +// // .into_dyn_boxed(), +// // #[cfg(feature = "storage")] +// // NodeStorageRef::Disk(disk_node) => disk_node.into_rows().into_dyn_boxed(), +// // } +// //TODO: +// std::iter::empty() +// } + +// pub fn temp_prop_rows_window( +// self, +// window: Range, +// ) -> impl Iterator)> + 'a { +// // match self { +// // NodeStorageRef::Mem(node_entry) => node_entry +// // .into_rows_window(window) +// // .map(|(t, row)| (t, Row::Mem(row))) +// // .into_dyn_boxed(), +// // #[cfg(feature = "storage")] +// // NodeStorageRef::Disk(disk_node) => disk_node.into_rows_window(window).into_dyn_boxed(), +// // } +// std::iter::empty() +// } + +// pub fn last_before_row(self, t: TimeIndexEntry) -> Vec<(usize, Prop)> { +// // match self { +// // NodeStorageRef::Mem(node_entry) => node_entry.last_before_row(t), +// // #[cfg(feature = "storage")] +// // NodeStorageRef::Disk(disk_node) => disk_node.last_before_row(t), +// // } +// todo!() +// } +// } diff --git a/raphtory-storage/src/graph/nodes/node_storage_ops.rs b/raphtory-storage/src/graph/nodes/node_storage_ops.rs index f011cb7354..acfd8e2aa8 100644 --- a/raphtory-storage/src/graph/nodes/node_storage_ops.rs +++ b/raphtory-storage/src/graph/nodes/node_storage_ops.rs @@ -1,25 +1,23 @@ -use crate::graph::nodes::node_additions::NodeAdditions; use raphtory_api::core::{ - entities::{ - edges::edge_ref::EdgeRef, - properties::{prop::Prop, tprop::TPropOps}, - GidRef, LayerIds, VID, - }, + entities::{edges::edge_ref::EdgeRef, properties::prop::Prop, GidRef, LayerIds, VID}, Direction, }; -use raphtory_core::{entities::nodes::node_store::NodeStore, storage::node_entry::NodePtr}; use std::borrow::Cow; +use storage::{api::nodes::NodeRefOps, NodeEntryRef}; pub trait NodeStorageOps<'a>: Sized { fn degree(self, layers: &LayerIds, dir: Direction) -> usize; - fn additions(self) -> storage::NodeAdditions<'a>; + fn additions(self, layer_ids: &'a LayerIds) -> storage::NodeAdditions<'a>; - fn tprop(self, prop_id: usize) -> impl TPropOps<'a>; + fn tprop(self, layer_ids: &'a LayerIds, prop_id: usize) -> storage::NodeTProps<'a>; - fn tprops(self) -> impl Iterator)>; + fn tprops( + self, + layer_ids: &'a LayerIds, + ) -> impl Iterator)>; - fn prop(self, prop_id: usize) -> Option; + fn prop(self, layer_id: usize, prop_id: usize) -> Option; fn edges_iter( self, @@ -33,55 +31,58 @@ pub trait NodeStorageOps<'a>: Sized { fn id(self) -> GidRef<'a>; - fn name(self) -> Option>; + fn name(self) -> Cow<'a, str> { + self.id().to_str() + } fn find_edge(self, dst: VID, layer_ids: &LayerIds) -> Option; } -// impl<'a> NodeStorageOps<'a> for NodePtr<'a> { -// fn degree(self, layers: &LayerIds, dir: Direction) -> usize { -// self.node.degree(layers, dir) -// } - -// fn additions(self) -> NodeAdditions<'a> { -// NodeAdditions::Mem(self.node.timestamps()) -// } - -// fn tprop(self, prop_id: usize) -> impl TPropOps<'a> { -// self.t_prop(prop_id) -// } - -// fn tprops(self) -> impl Iterator)> { -// self.temporal_prop_ids() -// .map(move |tid| (tid, self.tprop(tid))) -// } - -// fn prop(self, prop_id: usize) -> Option { -// self.node.constant_property(prop_id).cloned() -// } - -// fn edges_iter(self, layers: &LayerIds, dir: Direction) -> impl Iterator + 'a { -// self.node.edge_tuples(layers, dir) -// } - -// fn node_type_id(self) -> usize { -// self.node.node_type -// } - -// fn vid(self) -> VID { -// self.node.vid -// } - -// fn id(self) -> GidRef<'a> { -// (&self.node.global_id).into() -// } - -// fn name(self) -> Option> { -// self.node.global_id.as_str().map(Cow::from) -// } - -// fn find_edge(self, dst: VID, layer_ids: &LayerIds) -> Option { -// let eid = NodeStore::find_edge_eid(self.node, dst, layer_ids)?; -// Some(EdgeRef::new_outgoing(eid, self.node.vid, dst)) -// } -// } +impl<'a> NodeStorageOps<'a> for NodeEntryRef<'a> { + fn degree(self, layers: &LayerIds, dir: Direction) -> usize { + NodeRefOps::degree(self, layers, dir) + } + + fn additions(self, layer_ids: &'a LayerIds) -> storage::NodeAdditions<'a> { + NodeRefOps::additions(self, layer_ids) + } + + fn tprop(self, layer_ids: &'a LayerIds, prop_id: usize) -> storage::NodeTProps<'a> { + NodeRefOps::t_prop(self, layer_ids, prop_id) + } + + fn tprops( + self, + layer_ids: &'a LayerIds, + ) -> impl Iterator)> { + NodeRefOps::t_props(self, layer_ids) + } + + fn prop(self, layer_id: usize, prop_id: usize) -> Option { + NodeRefOps::c_prop(self, layer_id, prop_id) + } + + fn edges_iter( + self, + layers: &LayerIds, + dir: Direction, + ) -> impl Iterator + Send + Sync + 'a { + NodeRefOps::edges_iter(self, layers, dir) + } + + fn node_type_id(self) -> usize { + NodeRefOps::node_type_id(&self) + } + + fn vid(self) -> VID { + NodeRefOps::vid(&self) + } + + fn id(self) -> GidRef<'a> { + NodeRefOps::gid(&self) + } + + fn find_edge(self, dst: VID, layer_ids: &LayerIds) -> Option { + NodeRefOps::find_edge(&self, dst, layer_ids) + } +} diff --git a/raphtory-storage/src/graph/nodes/nodes.rs b/raphtory-storage/src/graph/nodes/nodes.rs index 89bf5b3ee0..572dfc2db8 100644 --- a/raphtory-storage/src/graph/nodes/nodes.rs +++ b/raphtory-storage/src/graph/nodes/nodes.rs @@ -27,7 +27,7 @@ impl NodesStorage { #[inline] pub fn node_entry(&self, vid: VID) -> NodeStorageRef { match self { - NodesStorage::Mem(storage) => NodeStorageRef::Mem(storage.node_ref(vid)), + NodesStorage::Mem(storage) => storage.node_ref(vid), #[cfg(feature = "storage")] NodesStorage::Disk(storage) => NodeStorageRef::Disk(storage.node(vid)), } diff --git a/raphtory-storage/src/graph/nodes/nodes_ref.rs b/raphtory-storage/src/graph/nodes/nodes_ref.rs index 969649e718..c2749e7e88 100644 --- a/raphtory-storage/src/graph/nodes/nodes_ref.rs +++ b/raphtory-storage/src/graph/nodes/nodes_ref.rs @@ -29,8 +29,8 @@ macro_rules! for_all_variants { impl<'a> NodesStorageEntry<'a> { pub fn node(&self, vid: VID) -> NodeStorageRef<'_> { match self { - NodesStorageEntry::Mem(store) => NodeStorageRef::Mem(store.node_ref(vid)), - NodesStorageEntry::Unlocked(store) => NodeStorageRef::Mem(store.node_ref(vid)), + NodesStorageEntry::Mem(store) => store.node_ref(vid), + NodesStorageEntry::Unlocked(store) => store.node_ref(vid), #[cfg(feature = "storage")] NodesStorageEntry::Disk(store) => NodeStorageRef::Disk(store.node(vid)), } From a537d6f11ab27944ea3bd3377d22d810e49b716f Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Tue, 24 Jun 2025 13:33:40 +0100 Subject: [PATCH 029/321] add support for edge iterators --- db4-graph/src/lib.rs | 2 + db4-storage/src/api/edges.rs | 11 ++ db4-storage/src/pages/edge_store.rs | 28 +++- db4-storage/src/segments/edge.rs | 63 +++++++- db4-storage/src/segments/edge_entry.rs | 12 +- .../src/graph/edges/edge_entry.rs | 146 +++++++++--------- raphtory-storage/src/graph/edges/edge_ref.rs | 111 +------------ raphtory-storage/src/graph/edges/edges.rs | 57 ++----- raphtory-storage/src/graph/edges/unlocked.rs | 2 +- raphtory-storage/src/graph/nodes/nodes.rs | 21 +-- 10 files changed, 204 insertions(+), 249 deletions(-) diff --git a/db4-graph/src/lib.rs b/db4-graph/src/lib.rs index 345398f9d6..c2b5adcf5f 100644 --- a/db4-graph/src/lib.rs +++ b/db4-graph/src/lib.rs @@ -29,6 +29,8 @@ pub struct TemporalGraph { storage: Arc>, + // resolver + // node_type_resolver edge_meta: Arc, node_meta: Arc, diff --git a/db4-storage/src/api/edges.rs b/db4-storage/src/api/edges.rs index c35c46164e..6eb535798b 100644 --- a/db4-storage/src/api/edges.rs +++ b/db4-storage/src/api/edges.rs @@ -10,6 +10,7 @@ use raphtory_core::{ entities::{LayerIds, VID}, storage::timeindex::{TimeIndexEntry, TimeIndexOps}, }; +use rayon::iter::ParallelIterator; use crate::{LocalPOS, error::DBV4Error, segments::edge::MemEdgeSegment}; @@ -91,6 +92,16 @@ pub trait LockedESegment: Send + Sync + std::fmt::Debug { fn entry_ref<'a>(&'a self, edge_pos: impl Into) -> Self::EntryRef<'a> where Self: 'a; + + fn edge_iter<'a, 'b: 'a>( + &'a self, + layer_ids: &'b LayerIds, + ) -> impl Iterator> + Send + Sync + 'a; + + fn edge_par_iter<'a, 'b: 'a>( + &'a self, + layer_ids: &'b LayerIds, + ) -> impl ParallelIterator> + Send + Sync + 'a; } #[derive(Debug)] diff --git a/db4-storage/src/pages/edge_store.rs b/db4-storage/src/pages/edge_store.rs index e0536d3a61..0df71d642e 100644 --- a/db4-storage/src/pages/edge_store.rs +++ b/db4-storage/src/pages/edge_store.rs @@ -16,7 +16,11 @@ use crate::{ }; use parking_lot::{RwLock, RwLockWriteGuard}; use raphtory_api::core::entities::{EID, VID, properties::meta::Meta}; -use raphtory_core::{entities::ELID, storage::timeindex::TimeIndexEntry}; +use raphtory_core::{ + entities::{ELID, LayerIds}, + storage::timeindex::TimeIndexEntry, +}; +use rayon::prelude::*; const N: usize = 32; @@ -46,6 +50,28 @@ impl, EXT: Clone> ReadLockedEdgeStorage( + &'a self, + layer_ids: &'b LayerIds, + ) -> impl Iterator< + Item = <::ArcLockedSegment as LockedESegment>::EntryRef<'a>, + > + 'a { + self.locked_pages + .iter() + .flat_map(move |page| page.edge_iter(layer_ids)) + } + + pub fn par_iter<'a, 'b: 'a>( + &'a self, + layer_ids: &'b LayerIds, + ) -> impl ParallelIterator< + Item = <::ArcLockedSegment as LockedESegment>::EntryRef<'a>, + > + 'a { + self.locked_pages + .par_iter() + .flat_map(move |page| page.edge_par_iter(layer_ids)) + } } impl, EXT: Clone> EdgeStorageInner { diff --git a/db4-storage/src/segments/edge.rs b/db4-storage/src/segments/edge.rs index 14a99b01cb..eb0ceafe64 100644 --- a/db4-storage/src/segments/edge.rs +++ b/db4-storage/src/segments/edge.rs @@ -11,7 +11,11 @@ use raphtory_api::core::entities::{ VID, properties::{meta::Meta, prop::Prop}, }; -use raphtory_core::storage::timeindex::{AsTime, TimeIndexEntry}; +use raphtory_core::{ + entities::LayerIds, + storage::timeindex::{AsTime, TimeIndexEntry}, +}; +use rayon::prelude::*; use crate::{ LocalPOS, @@ -19,6 +23,7 @@ use crate::{ error::DBV4Error, properties::PropMutEntry, segments::edge_entry::MemEdgeRef, + utils::Iter4, }; use super::{HasRow, SegmentContainer, edge_entry::MemEdgeEntry}; @@ -269,6 +274,32 @@ pub struct ArcLockedSegmentView { inner: ArcRwLockReadGuard, } +impl ArcLockedSegmentView { + fn edge_iter_layer<'a>( + &'a self, + layer_id: usize, + ) -> impl Iterator> + Send + Sync + 'a { + self.inner + .layers + .get(layer_id) + .into_iter() + .flat_map(|layer| layer.items().iter_ones()) + .map(move |pos| MemEdgeRef::new(LocalPOS(pos), &self.inner)) + } + + fn edge_par_iter_layer<'a>( + &'a self, + layer_id: usize, + ) -> impl ParallelIterator> + Send + Sync + 'a { + self.inner + .layers + .get(layer_id) + .into_par_iter() + .flat_map(|layer| layer.items().iter_ones().par_bridge()) + .map(move |pos| MemEdgeRef::new(LocalPOS(pos), &self.inner)) + } +} + impl LockedESegment for ArcLockedSegmentView { type EntryRef<'a> = MemEdgeRef<'a>; @@ -279,6 +310,36 @@ impl LockedESegment for ArcLockedSegmentView { let edge_pos = edge_pos.into(); MemEdgeRef::new(edge_pos, &self.inner) } + + fn edge_iter<'a, 'b: 'a>( + &'a self, + layer_ids: &'b LayerIds, + ) -> impl Iterator> + Send + Sync + 'a { + match layer_ids { + LayerIds::None => Iter4::I(std::iter::empty()), + LayerIds::All => Iter4::J(self.edge_iter_layer(0)), + LayerIds::One(layer_id) => Iter4::K(self.edge_iter_layer(*layer_id)), + LayerIds::Multiple(multiple) => Iter4::L( + self.edge_iter_layer(0) + .filter(|pos| pos.has_layers(multiple)), + ), + } + } + + fn edge_par_iter<'a, 'b: 'a>( + &'a self, + layer_ids: &'b LayerIds, + ) -> impl ParallelIterator> + Send + Sync + 'a { + match layer_ids { + LayerIds::None => Iter4::I(rayon::iter::empty()), + LayerIds::All => Iter4::J(self.edge_par_iter_layer(0)), + LayerIds::One(layer_id) => Iter4::K(self.edge_par_iter_layer(*layer_id)), + LayerIds::Multiple(multiple) => Iter4::L( + self.edge_par_iter_layer(0) + .filter(|pos| pos.has_layers(multiple)), + ), + } + } } impl EdgeSegmentOps for EdgeSegmentView { diff --git a/db4-storage/src/segments/edge_entry.rs b/db4-storage/src/segments/edge_entry.rs index 371ee6c3f6..56d635eb65 100644 --- a/db4-storage/src/segments/edge_entry.rs +++ b/db4-storage/src/segments/edge_entry.rs @@ -1,6 +1,6 @@ use raphtory_api::core::entities::properties::prop::Prop; use raphtory_core::{ - entities::{LayerIds, VID, properties::tprop::TPropCell}, + entities::{LayerIds, Multiple, VID, properties::tprop::TPropCell}, storage::timeindex::{TimeIndexEntry, TimeIndexOps}, }; @@ -58,6 +58,16 @@ impl<'a> MemEdgeRef<'a> { pub fn new(pos: LocalPOS, es: &'a MemEdgeSegment) -> Self { Self { pos, es } } + + pub fn has_layers(&self, layer_ids: &Multiple) -> bool { + layer_ids.iter().any(|layer_id| { + self.es + .as_ref() + .get(layer_id) + .and_then(|entry| entry.items().get(self.pos.0)) + .is_some_and(|item| *item) + }) + } } impl<'a> WithTimeCells<'a> for MemEdgeRef<'a> { diff --git a/raphtory-storage/src/graph/edges/edge_entry.rs b/raphtory-storage/src/graph/edges/edge_entry.rs index 98eb92d31d..bae8054fc5 100644 --- a/raphtory-storage/src/graph/edges/edge_entry.rs +++ b/raphtory-storage/src/graph/edges/edge_entry.rs @@ -7,7 +7,7 @@ use raphtory_api::core::entities::{ LayerIds, EID, VID, }; use std::ops::Range; -use storage::{EdgeEntry, EdgeEntryOps, EdgeEntryRef}; +use storage::{api::edges::EdgeEntryOps, EdgeEntry, EdgeEntryRef}; #[cfg(feature = "storage")] use crate::disk::graph_impl::DiskEdge; @@ -24,81 +24,81 @@ impl<'a> EdgeStorageEntry<'a> { #[inline] pub fn as_ref(&self) -> EdgeStorageRef { match self { - EdgeStorageEntry::Mem(edge) => EdgeStorageRef::Mem(*edge), - EdgeStorageEntry::Unlocked(edge) => EdgeStorageRef::Mem(edge.as_ref()), + EdgeStorageEntry::Mem(edge) => *edge, + EdgeStorageEntry::Unlocked(edge) => edge.as_ref(), #[cfg(feature = "storage")] EdgeStorageEntry::Disk(edge) => EdgeStorageRef::Disk(*edge), } } } -impl<'a, 'b: 'a> EdgeStorageOps<'a> for &'a EdgeStorageEntry<'b> { - fn added(self, layer_ids: &LayerIds, w: Range) -> bool { - self.as_ref().added(layer_ids, w) - } - - fn has_layer(self, layer_ids: &LayerIds) -> bool { - self.as_ref().has_layer(layer_ids) - } - - fn src(self) -> VID { - self.as_ref().src() - } - - fn dst(self) -> VID { - self.as_ref().dst() - } - - fn eid(self) -> EID { - self.as_ref().eid() - } - - fn layer_ids_iter(self, layer_ids: &'a LayerIds) -> impl Iterator + 'a { - self.as_ref().layer_ids_iter(layer_ids) - } - - fn additions_iter( - self, - layer_ids: &'a LayerIds, - ) -> impl Iterator)> + 'a { - self.as_ref().additions_iter(layer_ids) - } - - fn deletions_iter( - self, - layer_ids: &'a LayerIds, - ) -> impl Iterator)> + 'a { - self.as_ref().deletions_iter(layer_ids) - } - - fn updates_iter( - self, - layer_ids: &'a LayerIds, - ) -> impl Iterator, TimeIndexRef<'a>)> + 'a { - self.as_ref().updates_iter(layer_ids) - } - - fn additions(self, layer_id: usize) -> TimeIndexRef<'a> { - self.as_ref().additions(layer_id) - } - - fn deletions(self, layer_id: usize) -> TimeIndexRef<'a> { - self.as_ref().deletions(layer_id) - } - - fn temporal_prop_layer(self, layer_id: usize, prop_id: usize) -> impl TPropOps<'a> + 'a { - self.as_ref().temporal_prop_layer(layer_id, prop_id) - } - - fn temporal_prop_iter( - self, - layer_ids: &'a LayerIds, - prop_id: usize, - ) -> impl Iterator)> + 'a { - self.as_ref().temporal_prop_iter(layer_ids, prop_id) - } - - fn constant_prop_layer(self, layer_id: usize, prop_id: usize) -> Option { - self.as_ref().constant_prop_layer(layer_id, prop_id) - } -} +// impl<'a, 'b: 'a> EdgeStorageOps<'a> for &'a EdgeStorageEntry<'b> { +// fn added(self, layer_ids: &LayerIds, w: Range) -> bool { +// self.as_ref().added(layer_ids, w) +// } + +// fn has_layer(self, layer_ids: &LayerIds) -> bool { +// self.as_ref().has_layer(layer_ids) +// } + +// fn src(self) -> VID { +// self.as_ref().src() +// } + +// fn dst(self) -> VID { +// self.as_ref().dst() +// } + +// fn eid(self) -> EID { +// self.as_ref().eid() +// } + +// fn layer_ids_iter(self, layer_ids: &'a LayerIds) -> impl Iterator + 'a { +// self.as_ref().layer_ids_iter(layer_ids) +// } + +// fn additions_iter( +// self, +// layer_ids: &'a LayerIds, +// ) -> impl Iterator)> + 'a { +// self.as_ref().additions_iter(layer_ids) +// } + +// fn deletions_iter( +// self, +// layer_ids: &'a LayerIds, +// ) -> impl Iterator)> + 'a { +// self.as_ref().deletions_iter(layer_ids) +// } + +// fn updates_iter( +// self, +// layer_ids: &'a LayerIds, +// ) -> impl Iterator, TimeIndexRef<'a>)> + 'a { +// self.as_ref().updates_iter(layer_ids) +// } + +// fn additions(self, layer_id: usize) -> TimeIndexRef<'a> { +// self.as_ref().additions(layer_id) +// } + +// fn deletions(self, layer_id: usize) -> TimeIndexRef<'a> { +// self.as_ref().deletions(layer_id) +// } + +// fn temporal_prop_layer(self, layer_id: usize, prop_id: usize) -> impl TPropOps<'a> + 'a { +// self.as_ref().temporal_prop_layer(layer_id, prop_id) +// } + +// fn temporal_prop_iter( +// self, +// layer_ids: &'a LayerIds, +// prop_id: usize, +// ) -> impl Iterator)> + 'a { +// self.as_ref().temporal_prop_iter(layer_ids, prop_id) +// } + +// fn constant_prop_layer(self, layer_id: usize, prop_id: usize) -> Option { +// self.as_ref().constant_prop_layer(layer_id, prop_id) +// } +// } diff --git a/raphtory-storage/src/graph/edges/edge_ref.rs b/raphtory-storage/src/graph/edges/edge_ref.rs index 1ee424acba..3569cff334 100644 --- a/raphtory-storage/src/graph/edges/edge_ref.rs +++ b/raphtory-storage/src/graph/edges/edge_ref.rs @@ -1,110 +1,3 @@ -use crate::graph::edges::edge_storage_ops::{EdgeStorageOps, TimeIndexRef}; -use raphtory_api::core::entities::{ - properties::{prop::Prop, tprop::TPropOps}, - LayerIds, EID, VID, -}; -use raphtory_core::entities::edges::edge_store::MemEdge; -use std::ops::Range; -use storage::{EdgeEntry, EdgeEntryRef}; +use storage::EdgeEntryRef; -#[cfg(feature = "storage")] -use crate::{disk::graph_impl::DiskEdge, graph::variants::storage_variants2::StorageVariants2}; - -macro_rules! for_all { - ($value:expr, $pattern:pat => $result:expr) => { - match $value { - EdgeStorageRef::Mem($pattern) => $result, - #[cfg(feature = "storage")] - EdgeStorageRef::Disk($pattern) => $result, - } - }; -} - -#[cfg(feature = "storage")] -macro_rules! for_all_iter { - ($value:expr, $pattern:pat => $result:expr) => { - match $value { - EdgeStorageRef::Mem($pattern) => StorageVariants2::Mem($result), - EdgeStorageRef::Disk($pattern) => StorageVariants2::Disk($result), - } - }; -} - -#[cfg(not(feature = "storage"))] -macro_rules! for_all_iter { - ($value:expr, $pattern:pat => $result:expr) => { - match $value { - EdgeStorageRef::Mem($pattern) => $result, - } - }; -} - -#[derive(Copy, Clone, Debug)] -pub enum EdgeStorageRef<'a> { - Mem(EdgeEntryRef<'a>), - #[cfg(feature = "storage")] - Disk(DiskEdge<'a>), -} - -impl<'a> EdgeStorageOps<'a> for EdgeStorageRef<'a> { - fn added(self, layer_ids: &LayerIds, w: Range) -> bool { - for_all!(self, edge => EdgeStorageOps::added(edge, layer_ids, w)) - } - - fn has_layer(self, layer_ids: &LayerIds) -> bool { - for_all!(self, edge => EdgeStorageOps::has_layer(edge, layer_ids)) - } - - fn src(self) -> VID { - for_all!(self, edge => edge.src()) - } - - fn dst(self) -> VID { - for_all!(self, edge => edge.dst()) - } - - fn eid(self) -> EID { - for_all!(self, edge => edge.eid()) - } - - fn layer_ids_iter(self, layer_ids: &'a LayerIds) -> impl Iterator + 'a { - for_all_iter!(self, edge => EdgeStorageOps::layer_ids_iter(edge, layer_ids)) - } - - fn additions_iter( - self, - layer_ids: &'a LayerIds, - ) -> impl Iterator)> + 'a { - for_all_iter!(self, edge => EdgeStorageOps::additions_iter(edge, layer_ids)) - } - - fn deletions_iter( - self, - layer_ids: &'a LayerIds, - ) -> impl Iterator)> + 'a { - for_all_iter!(self, edge => EdgeStorageOps::deletions_iter(edge, layer_ids)) - } - - fn updates_iter( - self, - layer_ids: &'a LayerIds, - ) -> impl Iterator, TimeIndexRef<'a>)> + 'a { - for_all_iter!(self, edge => EdgeStorageOps::updates_iter(edge, layer_ids)) - } - - fn additions(self, layer_id: usize) -> TimeIndexRef<'a> { - for_all!(self, edge => EdgeStorageOps::additions(edge, layer_id)) - } - - fn deletions(self, layer_id: usize) -> TimeIndexRef<'a> { - for_all!(self, edge => EdgeStorageOps::deletions(edge, layer_id)) - } - - fn temporal_prop_layer(self, layer_id: usize, prop_id: usize) -> impl TPropOps<'a> + 'a { - for_all_iter!(self, edge => edge.temporal_prop_layer(layer_id, prop_id)) - } - - fn constant_prop_layer(self, layer_id: usize, prop_id: usize) -> Option { - for_all!(self, edge => edge.constant_prop_layer(layer_id, prop_id)) - } -} +pub type EdgeStorageRef<'a> = EdgeEntryRef<'a>; diff --git a/raphtory-storage/src/graph/edges/edges.rs b/raphtory-storage/src/graph/edges/edges.rs index be37ed47cf..32a44b4443 100644 --- a/raphtory-storage/src/graph/edges/edges.rs +++ b/raphtory-storage/src/graph/edges/edges.rs @@ -4,82 +4,43 @@ use crate::graph::{ variants::storage_variants3::StorageVariants3, }; use raphtory_api::core::entities::{LayerIds, EID}; -use raphtory_core::storage::raw_edges::LockedEdges; use rayon::iter::ParallelIterator; use std::sync::Arc; use storage::{Extension, ReadLockedEdges}; -#[cfg(feature = "storage")] -use crate::disk::storage_interface::{edges::DiskEdges, edges_ref::DiskEdgesRef}; -use crate::graph::variants::storage_variants2::StorageVariants2; - -pub enum EdgesStorage { - Mem(Arc>), - #[cfg(feature = "storage")] - Disk(DiskEdges), +pub struct EdgesStorage { + storage: Arc>, } impl EdgesStorage { #[inline] pub fn as_ref(&self) -> EdgesStorageRef { - match self { - EdgesStorage::Mem(storage) => EdgesStorageRef::Mem(storage), - #[cfg(feature = "storage")] - EdgesStorage::Disk(storage) => EdgesStorageRef::Disk(storage.as_ref()), - } + EdgesStorageRef::Mem(self.storage.as_ref()) } pub fn edge(&self, eid: EID) -> EdgeStorageRef { - match self { - EdgesStorage::Mem(storage) => EdgeStorageRef::Mem(storage.get_mem(eid)), - #[cfg(feature = "storage")] - EdgesStorage::Disk(storage) => EdgeStorageRef::Disk(storage.get(eid)), - } + self.storage.edge_ref(eid) } pub fn iter<'a>( &'a self, layers: &'a LayerIds, ) -> impl Iterator> + Send + Sync + 'a { - match self { - EdgesStorage::Mem(storage) => { - StorageVariants2::Mem((0..storage.len()).map(EID).filter_map(|e| { - let edge = storage.get_mem(e); - edge.has_layer(layers).then_some(EdgeStorageRef::Mem(edge)) - })) - } - #[cfg(feature = "storage")] - EdgesStorage::Disk(storage) => { - StorageVariants2::Disk(storage.as_ref().iter(layers).map(EdgeStorageRef::Disk)) - } - } + self.storage.iter(layers) } pub fn par_iter<'a>( &'a self, layers: &'a LayerIds, ) -> impl ParallelIterator> + Sync + 'a { - match self { - EdgesStorage::Mem(storage) => StorageVariants2::Mem( - storage - .par_iter() - .filter(|e| e.has_layer(layers)) - .map(EdgeStorageRef::Mem), - ), - #[cfg(feature = "storage")] - EdgesStorage::Disk(storage) => { - StorageVariants2::Disk(storage.as_ref().par_iter(layers).map(EdgeStorageRef::Disk)) - } - } + self.storage.par_iter(layers) } } #[derive(Debug, Copy, Clone)] -pub enum EdgesStorageRef<'a, EXT = Extension> { - Mem(&'a ReadLockedEdges), - Unlocked(UnlockedEdges<'a, EXT>), - #[cfg(feature = "storage")] - Disk(DiskEdgesRef<'a>), +pub enum EdgesStorageRef<'a> { + Mem(&'a ReadLockedEdges), + Unlocked(UnlockedEdges<'a>), } impl<'a> EdgesStorageRef<'a> { diff --git a/raphtory-storage/src/graph/edges/unlocked.rs b/raphtory-storage/src/graph/edges/unlocked.rs index 5543c5a0c4..a4e526ceb4 100644 --- a/raphtory-storage/src/graph/edges/unlocked.rs +++ b/raphtory-storage/src/graph/edges/unlocked.rs @@ -6,7 +6,7 @@ use rayon::prelude::*; use storage::{Extension, Layer}; #[derive(Copy, Clone, Debug)] -pub struct UnlockedEdges<'a, EXT = Extension>(pub(crate) &'a Layer); +pub struct UnlockedEdges<'a>(pub(crate) &'a Layer); impl<'a> UnlockedEdges<'a> { pub fn iter(self) -> impl Iterator> + 'a { diff --git a/raphtory-storage/src/graph/nodes/nodes.rs b/raphtory-storage/src/graph/nodes/nodes.rs index 572dfc2db8..3745b899c0 100644 --- a/raphtory-storage/src/graph/nodes/nodes.rs +++ b/raphtory-storage/src/graph/nodes/nodes.rs @@ -8,32 +8,23 @@ use storage::{Extension, ReadLockedNodes}; #[cfg(feature = "storage")] use crate::disk::storage_interface::nodes::DiskNodesOwned; -pub enum NodesStorage { - Mem(Arc>), - #[cfg(feature = "storage")] - Disk(DiskNodesOwned), +#[repr(transparent)] +pub struct NodesStorage { + storage: Arc>, } impl NodesStorage { #[inline] pub fn as_ref(&self) -> NodesStorageEntry { - match self { - NodesStorage::Mem(storage) => NodesStorageEntry::Mem(&storage), - #[cfg(feature = "storage")] - NodesStorage::Disk(storage) => NodesStorageEntry::Disk(storage.as_ref()), - } + NodesStorageEntry::Mem(self.storage.as_ref()) } #[inline] pub fn node_entry(&self, vid: VID) -> NodeStorageRef { - match self { - NodesStorage::Mem(storage) => storage.node_ref(vid), - #[cfg(feature = "storage")] - NodesStorage::Disk(storage) => NodeStorageRef::Disk(storage.node(vid)), - } + self.storage.node_ref(vid) } pub fn len(&self) -> usize { - self.as_ref().len() + self.storage.len() } } From 4e678dd80bfaf3273094532e5ace690282aaeab0 Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Tue, 24 Jun 2025 17:36:46 +0100 Subject: [PATCH 030/321] unlocked edges now compiles --- db4-graph/src/lib.rs | 2 -- db4-storage/Cargo.toml | 1 + db4-storage/src/api/edges.rs | 15 ++++++++- db4-storage/src/pages/edge_store.rs | 24 +++++++++++++-- db4-storage/src/pages/mod.rs | 7 +++-- db4-storage/src/pages/session.rs | 2 +- db4-storage/src/pages/test_utils/checkers.rs | 2 +- db4-storage/src/segments/edge.rs | 12 ++++++++ db4-storage/src/segments/edge_entry.rs | 4 ++- raphtory-storage/src/graph/edges/edges.rs | 21 +++---------- raphtory-storage/src/graph/edges/unlocked.rs | 32 +++++++++++--------- 11 files changed, 81 insertions(+), 41 deletions(-) diff --git a/db4-graph/src/lib.rs b/db4-graph/src/lib.rs index c2b5adcf5f..345398f9d6 100644 --- a/db4-graph/src/lib.rs +++ b/db4-graph/src/lib.rs @@ -29,8 +29,6 @@ pub struct TemporalGraph { storage: Arc>, - // resolver - // node_type_resolver edge_meta: Arc, node_meta: Arc, diff --git a/db4-storage/Cargo.toml b/db4-storage/Cargo.toml index 5ae48498db..f27fdb77c2 100644 --- a/db4-storage/Cargo.toml +++ b/db4-storage/Cargo.toml @@ -15,6 +15,7 @@ raphtory-api.workspace = true raphtory-core = {workspace = true} # db4-common = {path = "../db4-common"} +ouroboros = {workspace = true} bitvec.workspace = true bigdecimal.workspace = true polars-arrow.workspace = true diff --git a/db4-storage/src/api/edges.rs b/db4-storage/src/api/edges.rs index 6eb535798b..ccbca1a86a 100644 --- a/db4-storage/src/api/edges.rs +++ b/db4-storage/src/api/edges.rs @@ -81,6 +81,19 @@ pub trait EdgeSegmentOps: Send + Sync + std::fmt::Debug { fn entry<'a, LP: Into>(&'a self, edge_pos: LP) -> Self::Entry<'a>; + fn layer_entry<'a, LP: Into>( + &'a self, + edge_pos: LP, + layer_id: usize, + ) -> Option> { + let edge_pos = edge_pos.into(); + if self.head().contains_edge(edge_pos, layer_id) { + Some(self.entry(edge_pos)) + } else { + None + } + } + fn locked(self: &Arc) -> Self::ArcLockedSegment; } @@ -110,7 +123,7 @@ pub struct ReadLockedES { head: ES::ArcLockedSegment, } -pub trait EdgeEntryOps<'a> { +pub trait EdgeEntryOps<'a>: Send + Sync { type Ref<'b>: EdgeRefOps<'b> where 'a: 'b, diff --git a/db4-storage/src/pages/edge_store.rs b/db4-storage/src/pages/edge_store.rs index 0df71d642e..2ca13ec119 100644 --- a/db4-storage/src/pages/edge_store.rs +++ b/db4-storage/src/pages/edge_store.rs @@ -41,7 +41,7 @@ pub struct ReadLockedEdgeStorage, EXT> { locked_pages: Box<[ES::ArcLockedSegment]>, } -impl, EXT: Clone> ReadLockedEdgeStorage { +impl, EXT: Clone + Send + Sync> ReadLockedEdgeStorage { pub fn edge_ref( &self, e_id: impl Into, @@ -74,7 +74,7 @@ impl, EXT: Clone> ReadLockedEdgeStorage, EXT: Clone> EdgeStorageInner { +impl, EXT: Clone + Send + Sync> EdgeStorageInner { pub fn locked(self: &Arc) -> ReadLockedEdgeStorage { let locked_pages = self .pages @@ -409,4 +409,24 @@ impl, EXT: Clone> EdgeStorageInner } } } + + pub fn par_iter(&self, layer: usize) -> impl ParallelIterator> + '_ { + (0..self.pages.count()) + .into_par_iter() + .filter_map(move |page_id| self.pages.get(page_id)) + .flat_map(move |page| { + (0..page.num_edges()) + .into_par_iter() + .filter_map(move |local_edge| page.layer_entry(local_edge, layer)) + }) + } + + pub fn iter(&self, layer: usize) -> impl Iterator> + '_ { + (0..self.pages.count()) + .filter_map(move |page_id| self.pages.get(page_id)) + .flat_map(move |page| { + (0..page.num_edges()) + .filter_map(move |local_edge| page.layer_entry(local_edge, layer)) + }) + } } diff --git a/db4-storage/src/pages/mod.rs b/db4-storage/src/pages/mod.rs index f694ec1456..1c1144eb67 100644 --- a/db4-storage/src/pages/mod.rs +++ b/db4-storage/src/pages/mod.rs @@ -70,8 +70,11 @@ pub struct ReadLockedGraphStore< pub graph: Arc>, } -impl, ES: EdgeSegmentOps, EXT: Clone + Default> - GraphStore +impl< + NS: NodeSegmentOps, + ES: EdgeSegmentOps, + EXT: Clone + Send + Sync + Default, +> GraphStore { pub fn read_locked(self: &Arc) -> ReadLockedGraphStore { let nodes = self.nodes.locked().into(); diff --git a/db4-storage/src/pages/session.rs b/db4-storage/src/pages/session.rs index 97cba61418..7b527ced89 100644 --- a/db4-storage/src/pages/session.rs +++ b/db4-storage/src/pages/session.rs @@ -37,7 +37,7 @@ impl< MES: DerefMut + 'a, NS: NodeSegmentOps, ES: EdgeSegmentOps, - EXT: Clone + Default, + EXT: Clone + Default + Send + Sync, > WriteSession<'a, MNS, MES, NS, ES, EXT> { pub fn new( diff --git a/db4-storage/src/pages/test_utils/checkers.rs b/db4-storage/src/pages/test_utils/checkers.rs index 12f7d3a1ad..114ca327ae 100644 --- a/db4-storage/src/pages/test_utils/checkers.rs +++ b/db4-storage/src/pages/test_utils/checkers.rs @@ -106,7 +106,7 @@ pub fn check_edges_support< fn check< NS: NodeSegmentOps, ES: EdgeSegmentOps, - EXT: Clone + Default + std::fmt::Debug, + EXT: Clone + Send + Sync + Default + std::fmt::Debug, >( stage: &str, expected_edges: &[(VID, VID, Option)], // (src, dst, layer_id) diff --git a/db4-storage/src/segments/edge.rs b/db4-storage/src/segments/edge.rs index eb0ceafe64..dce26edea8 100644 --- a/db4-storage/src/segments/edge.rs +++ b/db4-storage/src/segments/edge.rs @@ -448,6 +448,18 @@ impl EdgeSegmentOps for EdgeSegmentView { MemEdgeEntry::new(edge_pos, self.head()) } + fn layer_entry<'a, LP: Into>( + &'a self, + edge_pos: LP, + layer_id: usize, + ) -> Option> { + let edge_pos = edge_pos.into(); + let locked_head = self.head(); + let layer = locked_head.as_ref().get(layer_id)?; + let has_edge = layer.items().get(edge_pos.0).is_some_and(|item| *item); + has_edge.then(|| MemEdgeEntry::new(edge_pos, locked_head)) + } + fn locked(self: &Arc) -> Self::ArcLockedSegment { ArcLockedSegmentView { inner: self.head_arc(), diff --git a/db4-storage/src/segments/edge_entry.rs b/db4-storage/src/segments/edge_entry.rs index 56d635eb65..28f86bfeb4 100644 --- a/db4-storage/src/segments/edge_entry.rs +++ b/db4-storage/src/segments/edge_entry.rs @@ -30,7 +30,9 @@ impl<'a, MES: std::ops::Deref> MemEdgeEntry<'a, MES> { } } -impl<'a, MES: std::ops::Deref> EdgeEntryOps<'a> for MemEdgeEntry<'a, MES> { +impl<'a, MES: std::ops::Deref + Send + Sync> EdgeEntryOps<'a> + for MemEdgeEntry<'a, MES> +{ type Ref<'b> = MemEdgeRef<'b> where diff --git a/raphtory-storage/src/graph/edges/edges.rs b/raphtory-storage/src/graph/edges/edges.rs index 32a44b4443..714c6ab28c 100644 --- a/raphtory-storage/src/graph/edges/edges.rs +++ b/raphtory-storage/src/graph/edges/edges.rs @@ -4,9 +4,10 @@ use crate::graph::{ variants::storage_variants3::StorageVariants3, }; use raphtory_api::core::entities::{LayerIds, EID}; +use raphtory_core::utils::iter::GenLockedIter; use rayon::iter::ParallelIterator; use std::sync::Arc; -use storage::{Extension, ReadLockedEdges}; +use storage::{utils::Iter2, Extension, ReadLockedEdges}; pub struct EdgesStorage { storage: Arc>, @@ -49,22 +50,10 @@ impl<'a> EdgesStorageRef<'a> { layers: &'a LayerIds, ) -> impl Iterator> + Send + Sync + 'a { match self { - EdgesStorageRef::Mem(storage) => StorageVariants3::Mem( - storage - .iter() - .filter(move |e| e.has_layer(layers)) - .map(EdgeStorageEntry::Mem), - ), - EdgesStorageRef::Unlocked(edges) => StorageVariants3::Unlocked( - edges - .iter() - .filter(move |e| e.as_mem_edge().has_layer(layers)) - .map(EdgeStorageEntry::Unlocked), - ), - #[cfg(feature = "storage")] - EdgesStorageRef::Disk(storage) => { - StorageVariants3::Disk(storage.iter(layers).map(EdgeStorageEntry::Disk)) + EdgesStorageRef::Mem(storage) => { + Iter2::I1(storage.iter(layers).map(EdgeStorageEntry::Mem)) } + EdgesStorageRef::Unlocked(edges) => Iter2::I2(edges.iter(layers)), } } diff --git a/raphtory-storage/src/graph/edges/unlocked.rs b/raphtory-storage/src/graph/edges/unlocked.rs index a4e526ceb4..f5592ae218 100644 --- a/raphtory-storage/src/graph/edges/unlocked.rs +++ b/raphtory-storage/src/graph/edges/unlocked.rs @@ -1,31 +1,33 @@ use raphtory_api::core::entities::EID; -use raphtory_core::{ - entities::graph::tgraph_storage::GraphStorage, storage::raw_edges::EdgeRGuard, -}; +use raphtory_core::storage::raw_edges::EdgeRGuard; use rayon::prelude::*; use storage::{Extension, Layer}; +use crate::graph::edges::edge_entry::EdgeStorageEntry; + #[derive(Copy, Clone, Debug)] pub struct UnlockedEdges<'a>(pub(crate) &'a Layer); impl<'a> UnlockedEdges<'a> { - pub fn iter(self) -> impl Iterator> + 'a { - let storage = self.0; - (0..storage.edges_len()) - .map(EID) - .map(|eid| storage.edge_entry(eid)) + pub fn iter(self, layer_id: usize) -> impl Iterator> + 'a { + self.0 + .edges() + .iter(layer_id) + .map(EdgeStorageEntry::Unlocked) } - pub fn par_iter(self) -> impl ParallelIterator> + 'a { - let storage = self.0; - (0..storage.edges_len()) - .into_par_iter() - .map(EID) - .map(|eid| storage.edge_entry(eid)) + pub fn par_iter( + self, + layer_id: usize, + ) -> impl ParallelIterator> + 'a { + self.0 + .edges() + .par_iter(layer_id) + .map(EdgeStorageEntry::Unlocked) } #[inline] pub fn len(self) -> usize { - self.0.edges_len() + self.0.edges().num_edges() } } From 9938b242dc7fd512475da21fc11961307f103f67 Mon Sep 17 00:00:00 2001 From: Fadhil Abubaker Date: Tue, 24 Jun 2025 15:56:32 -0400 Subject: [PATCH 031/321] Add initial Resolver trait --- db4-storage/src/lib.rs | 1 + db4-storage/src/resolver/mod.rs | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 db4-storage/src/resolver/mod.rs diff --git a/db4-storage/src/lib.rs b/db4-storage/src/lib.rs index 24a49ef6c6..25727d59f8 100644 --- a/db4-storage/src/lib.rs +++ b/db4-storage/src/lib.rs @@ -25,6 +25,7 @@ pub mod gen_ts; pub mod pages; pub mod persist; pub mod properties; +pub mod resolver; pub mod segments; pub mod utils; diff --git a/db4-storage/src/resolver/mod.rs b/db4-storage/src/resolver/mod.rs new file mode 100644 index 0000000000..7bde1cbe4d --- /dev/null +++ b/db4-storage/src/resolver/mod.rs @@ -0,0 +1,30 @@ +use std::path::Path; +use thiserror::Error; + +pub trait ResolverEntryOps: Send + Sync { + fn set(&self, value: impl Into) -> Result<(), ResolverError>; +} + +pub trait ResolverOps: Send + Sync { + type Entry: ResolverEntryOps; + + fn new(path: impl AsRef) -> Self + where + Self: Sized; + + fn generate_id(&self) -> V; + + /// Reserve an entry for a key in the resolver. + fn entry(&self, key: impl Into) -> Result>; + + fn get(&self, key: impl AsRef) -> Option; +} + +#[derive(Debug, Error)] +pub enum ResolverError { + #[error("Key {0} already exists with value {1}")] + KeyExists(K, V), + + #[error("Operation failed: {0}")] + OperationFailed(String), +} From d7fde1d226c21efbb7c8f94abfa1b10ee06d4253 Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Wed, 25 Jun 2025 10:54:00 +0100 Subject: [PATCH 032/321] raphtory-storage edges now compiles --- db4-storage/Cargo.toml | 1 - db4-storage/src/api/edges.rs | 2 + db4-storage/src/api/nodes.rs | 4 +- db4-storage/src/lib.rs | 2 - db4-storage/src/pages/edge_page/writer.rs | 25 ++++---- db4-storage/src/pages/edge_store.rs | 61 ++++++++++---------- db4-storage/src/pages/locked/edges.rs | 9 +-- db4-storage/src/pages/node_store.rs | 2 +- db4-storage/src/segments/edge.rs | 14 +++++ db4-storage/src/segments/node.rs | 2 +- raphtory-storage/src/graph/edges/edges.rs | 61 +++++++------------- raphtory-storage/src/graph/edges/unlocked.rs | 45 ++++++++++++--- raphtory-storage/src/graph/graph.rs | 8 +-- raphtory-storage/src/graph/nodes/nodes.rs | 5 ++ 14 files changed, 131 insertions(+), 110 deletions(-) diff --git a/db4-storage/Cargo.toml b/db4-storage/Cargo.toml index f27fdb77c2..5ae48498db 100644 --- a/db4-storage/Cargo.toml +++ b/db4-storage/Cargo.toml @@ -15,7 +15,6 @@ raphtory-api.workspace = true raphtory-core = {workspace = true} # db4-common = {path = "../db4-common"} -ouroboros = {workspace = true} bitvec.workspace = true bigdecimal.workspace = true polars-arrow.workspace = true diff --git a/db4-storage/src/api/edges.rs b/db4-storage/src/api/edges.rs index ccbca1a86a..a7652db10f 100644 --- a/db4-storage/src/api/edges.rs +++ b/db4-storage/src/api/edges.rs @@ -27,6 +27,8 @@ pub trait EdgeSegmentOps: Send + Sync + std::fmt::Debug { fn earliest(&self) -> Option; fn t_len(&self) -> usize; + fn num_layers(&self) -> usize; + fn layer_count(&self, layer_id: usize) -> usize; fn load( page_id: usize, diff --git a/db4-storage/src/api/nodes.rs b/db4-storage/src/api/nodes.rs index 12e6b6451a..97413edcfa 100644 --- a/db4-storage/src/api/nodes.rs +++ b/db4-storage/src/api/nodes.rs @@ -70,12 +70,12 @@ pub trait NodeSegmentOps: Send + Sync + std::fmt::Debug { fn head_mut(&self) -> RwLockWriteGuard; fn num_nodes(&self) -> usize { - self.layer_num_nodes(0) + self.layer_count(0) } fn num_layers(&self) -> usize; - fn layer_num_nodes(&self, layer_id: usize) -> usize; + fn layer_count(&self, layer_id: usize) -> usize; fn notify_write( &self, diff --git a/db4-storage/src/lib.rs b/db4-storage/src/lib.rs index 24a49ef6c6..fd68a7bc87 100644 --- a/db4-storage/src/lib.rs +++ b/db4-storage/src/lib.rs @@ -8,7 +8,6 @@ use crate::{ node_store::ReadLockedNodeStorage, }, segments::{ - additions::MemAdditions, edge::EdgeSegmentView, edge_entry::{MemEdgeEntry, MemEdgeRef}, node::NodeSegmentView, @@ -16,7 +15,6 @@ use crate::{ }, }; use raphtory_api::core::entities::{EID, VID}; -use raphtory_core::entities::properties::tprop::TPropCell; use segments::{edge::MemEdgeSegment, node::MemNodeSegment}; pub mod api; diff --git a/db4-storage/src/pages/edge_page/writer.rs b/db4-storage/src/pages/edge_page/writer.rs index 516e2525ea..536cbc96cf 100644 --- a/db4-storage/src/pages/edge_page/writer.rs +++ b/db4-storage/src/pages/edge_page/writer.rs @@ -1,17 +1,17 @@ -use std::{ops::DerefMut, sync::atomic::AtomicUsize}; +use std::ops::DerefMut; -use crate::{LocalPOS, api::edges::EdgeSegmentOps, segments::edge::MemEdgeSegment}; +use crate::{api::edges::EdgeSegmentOps, pages::layer_counter::LayerCounter, segments::edge::MemEdgeSegment, LocalPOS}; use raphtory_api::core::entities::{VID, properties::prop::Prop}; use raphtory_core::storage::timeindex::AsTime; pub struct EdgeWriter<'a, MP: DerefMut, ES: EdgeSegmentOps> { pub page: &'a ES, pub writer: MP, - pub global_num_edges: &'a AtomicUsize, + pub global_num_edges: &'a LayerCounter, } impl<'a, MP: DerefMut, ES: EdgeSegmentOps> EdgeWriter<'a, MP, ES> { - pub fn new(global_num_edges: &'a AtomicUsize, page: &'a ES, writer: MP) -> Self { + pub fn new(global_num_edges: &'a LayerCounter, page: &'a ES, writer: MP) -> Self { Self { page, writer, @@ -19,9 +19,9 @@ impl<'a, MP: DerefMut, ES: EdgeSegmentOps> EdgeWriter<' } } - fn new_local_pos(&self) -> LocalPOS { + fn new_local_pos(&self, layer_id: usize) -> LocalPOS { let new_pos = LocalPOS(self.page.increment_num_edges()); - self.increment_global_num_edges(); + self.increment_layer_num_edges(layer_id); new_pos } @@ -39,10 +39,10 @@ impl<'a, MP: DerefMut, ES: EdgeSegmentOps> EdgeWriter<' self.writer.as_mut()[layer_id].set_lsn(lsn); if exists_hint == Some(false) && edge_pos.is_some() { - self.new_local_pos(); // increment the counts, this is triggered from the bulk loader + self.new_local_pos(layer_id); // increment the counts, this is triggered from the bulk loader } - let edge_pos = edge_pos.unwrap_or_else(|| self.new_local_pos()); + let edge_pos = edge_pos.unwrap_or_else(|| self.new_local_pos(layer_id)); self.writer .insert_edge_internal(t, edge_pos, src, dst, layer_id, props); edge_pos @@ -60,10 +60,10 @@ impl<'a, MP: DerefMut, ES: EdgeSegmentOps> EdgeWriter<' self.writer.as_mut()[layer_id].set_lsn(lsn); if exists_hint == Some(false) && edge_pos.is_some() { - self.new_local_pos(); // increment the counts, this is triggered from the bulk loader + self.new_local_pos(layer_id); // increment the counts, this is triggered from the bulk loader } - let edge_pos = edge_pos.unwrap_or_else(|| self.new_local_pos()); + let edge_pos = edge_pos.unwrap_or_else(|| self.new_local_pos(layer_id)); self.writer .insert_static_edge_internal(edge_pos, src, dst, layer_id); edge_pos @@ -73,9 +73,8 @@ impl<'a, MP: DerefMut, ES: EdgeSegmentOps> EdgeWriter<' self.page.segment_id() } - fn increment_global_num_edges(&self) { - self.global_num_edges - .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + fn increment_layer_num_edges(&self, layer_id: usize) { + self.global_num_edges.increment(layer_id); } pub fn contains_edge(&self, pos: LocalPOS, layer_id: usize) -> bool { diff --git a/db4-storage/src/pages/edge_store.rs b/db4-storage/src/pages/edge_store.rs index 2ca13ec119..41af00b54a 100644 --- a/db4-storage/src/pages/edge_store.rs +++ b/db4-storage/src/pages/edge_store.rs @@ -9,10 +9,7 @@ use std::{ use super::{edge_page::writer::EdgeWriter, resolve_pos}; use crate::{ - LocalPOS, - api::edges::{EdgeSegmentOps, LockedESegment}, - error::DBV4Error, - segments::edge::MemEdgeSegment, + api::edges::{EdgeSegmentOps, LockedESegment}, error::DBV4Error, pages::layer_counter::LayerCounter, segments::edge::MemEdgeSegment, LocalPOS }; use parking_lot::{RwLock, RwLockWriteGuard}; use raphtory_api::core::entities::{EID, VID, properties::meta::Meta}; @@ -27,7 +24,7 @@ const N: usize = 32; #[derive(Debug)] pub struct EdgeStorageInner { pages: boxcar::Vec>, - num_edges: AtomicUsize, + layer_counter: LayerCounter, free_pages: Box<[RwLock; N]>, edges_path: PathBuf, max_page_len: usize, @@ -42,6 +39,11 @@ pub struct ReadLockedEdgeStorage, EXT> { } impl, EXT: Clone + Send + Sync> ReadLockedEdgeStorage { + + pub fn storage(&self) -> &EdgeStorageInner { + &self.storage + } + pub fn edge_ref( &self, e_id: impl Into, @@ -91,29 +93,11 @@ impl, EXT: Clone + Send + Sync> EdgeStorageI &self.prop_meta } - pub fn layer( - edges_path: impl AsRef, - max_page_len: usize, - meta: &Arc, - ext: EXT, - ) -> Self { - let free_pages = (0..N).map(RwLock::new).collect::>(); - Self { - pages: boxcar::Vec::new(), - num_edges: AtomicUsize::new(0), - free_pages: free_pages.try_into().unwrap(), - edges_path: edges_path.as_ref().to_path_buf(), - max_page_len, - prop_meta: meta.clone(), - ext, - } - } - pub fn new(edges_path: impl AsRef, max_page_len: usize, ext: EXT) -> Self { let free_pages = (0..N).map(RwLock::new).collect::>(); Self { pages: boxcar::Vec::new(), - num_edges: AtomicUsize::new(0), + layer_counter: LayerCounter::new(), free_pages: free_pages.try_into().unwrap(), edges_path: edges_path.as_ref().to_path_buf(), max_page_len, @@ -230,13 +214,24 @@ impl, EXT: Clone + Send + Sync> EdgeStorageI lock }); - let num_edges = pages.iter().map(|(_, page)| page.num_edges()).sum(); + + let mut layer_counts = vec![]; + + for (_, page) in pages.iter() { + for layer_id in 0..page.num_layers() { + let count = page.layer_count(layer_id); + if layer_counts.len() <= layer_id { + layer_counts.resize(layer_id + 1, 0); + } + layer_counts[layer_id] += count; + } + } Ok(Self { pages, edges_path: edges_path.to_path_buf(), max_page_len, - num_edges: AtomicUsize::new(num_edges), + layer_counter: LayerCounter::from(layer_counts), free_pages: free_pages.try_into().unwrap(), prop_meta: meta, ext, @@ -348,7 +343,11 @@ impl, EXT: Clone + Send + Sync> EdgeStorageI } pub fn num_edges(&self) -> usize { - self.num_edges.load(atomic::Ordering::Relaxed) + self.layer_counter.get(0) + } + + pub fn num_edges_layer(&self, layer_id: usize) -> usize { + self.layer_counter.get(layer_id) } pub fn get_writer<'a>( @@ -357,7 +356,7 @@ impl, EXT: Clone + Send + Sync> EdgeStorageI ) -> EdgeWriter<'a, RwLockWriteGuard<'a, MemEdgeSegment>, ES> { let (chunk, _) = resolve_pos(e_id, self.max_page_len); let page = self.get_or_create_segment(chunk); - EdgeWriter::new(&self.num_edges, page, page.head_mut()) + EdgeWriter::new(&self.layer_counter, page, page.head_mut()) } pub fn try_get_writer<'a>( @@ -367,7 +366,7 @@ impl, EXT: Clone + Send + Sync> EdgeStorageI let (segment_id, _) = resolve_pos(e_id, self.max_page_len); let page = self.get_or_create_segment(segment_id); let writer = page.head_mut(); - Ok(EdgeWriter::new(&self.num_edges, page, writer)) + Ok(EdgeWriter::new(&self.layer_counter, page, writer)) } pub fn get_free_writer<'a>( @@ -393,14 +392,14 @@ impl, EXT: Clone + Send + Sync> EdgeStorageI .next(); if let Some((edge_page, writer)) = maybe_free_page { - EdgeWriter::new(&self.num_edges, edge_page, writer) + EdgeWriter::new(&self.layer_counter, edge_page, writer) } else { // not lucky, go wait on your slot loop { let mut slot = self.free_pages[slot_idx].write(); match self.pages.get(*slot).map(|page| (page, page.head_mut())) { Some((edge_page, writer)) if edge_page.num_edges() < self.max_page_len => { - return EdgeWriter::new(&self.num_edges, edge_page, writer); + return EdgeWriter::new(&self.layer_counter, edge_page, writer); } _ => { *slot = self.push_new_page(); diff --git a/db4-storage/src/pages/locked/edges.rs b/db4-storage/src/pages/locked/edges.rs index 487813797b..412be52624 100644 --- a/db4-storage/src/pages/locked/edges.rs +++ b/db4-storage/src/pages/locked/edges.rs @@ -1,10 +1,7 @@ use std::{ops::DerefMut, sync::atomic::AtomicUsize}; use crate::{ - LocalPOS, - api::edges::EdgeSegmentOps, - pages::{edge_page::writer::EdgeWriter, resolve_pos}, - segments::edge::MemEdgeSegment, + api::edges::EdgeSegmentOps, pages::{edge_page::writer::EdgeWriter, layer_counter::LayerCounter, resolve_pos}, segments::edge::MemEdgeSegment, LocalPOS }; use parking_lot::RwLockWriteGuard; use raphtory_core::entities::EID; @@ -14,7 +11,7 @@ pub struct LockedEdgePage<'a, ES> { page_id: usize, max_page_len: usize, page: &'a ES, - num_edges: &'a AtomicUsize, + num_edges: &'a LayerCounter, lock: RwLockWriteGuard<'a, MemEdgeSegment>, } @@ -23,7 +20,7 @@ impl<'a, EXT, ES: EdgeSegmentOps> LockedEdgePage<'a, ES> { page_id: usize, max_page_len: usize, page: &'a ES, - num_edges: &'a AtomicUsize, + num_edges: &'a LayerCounter, lock: RwLockWriteGuard<'a, MemEdgeSegment>, ) -> Self { Self { diff --git a/db4-storage/src/pages/node_store.rs b/db4-storage/src/pages/node_store.rs index bcfc96cd29..3ba05c7789 100644 --- a/db4-storage/src/pages/node_store.rs +++ b/db4-storage/src/pages/node_store.rs @@ -216,7 +216,7 @@ impl, EXT: Clone> NodeStorageInner for (_, page) in pages.iter() { for layer_id in 0..page.num_layers() { - let count = page.layer_num_nodes(layer_id); + let count = page.layer_count(layer_id); if layer_counts.len() <= layer_id { layer_counts.resize(layer_id + 1, 0); } diff --git a/db4-storage/src/segments/edge.rs b/db4-storage/src/segments/edge.rs index dce26edea8..a5667fe4c6 100644 --- a/db4-storage/src/segments/edge.rs +++ b/db4-storage/src/segments/edge.rs @@ -96,6 +96,10 @@ impl MemEdgeSegment { &mut self.layers[layer_id] } + pub fn get_layer(&self, layer_id: usize) -> Option<&SegmentContainer> { + self.layers.get(layer_id) + } + pub fn est_size(&self) -> usize { self.layers.iter().map(|seg| seg.est_size()).sum::() } @@ -465,4 +469,14 @@ impl EdgeSegmentOps for EdgeSegmentView { inner: self.head_arc(), } } + + fn num_layers(&self) -> usize { + self.head().layers.len() + } + + fn layer_count(&self, layer_id: usize) -> usize { + self.head() + .get_layer(layer_id) + .map_or(0, |layer| layer.len()) + } } diff --git a/db4-storage/src/segments/node.rs b/db4-storage/src/segments/node.rs index 6baadd478b..590f94666b 100644 --- a/db4-storage/src/segments/node.rs +++ b/db4-storage/src/segments/node.rs @@ -427,7 +427,7 @@ impl NodeSegmentOps for NodeSegmentView { self.head().layers.len() } - fn layer_num_nodes(&self, layer_id: usize) -> usize { + fn layer_count(&self, layer_id: usize) -> usize { self.head() .get_layer(layer_id) .map_or(0, |layer| layer.len()) diff --git a/raphtory-storage/src/graph/edges/edges.rs b/raphtory-storage/src/graph/edges/edges.rs index 714c6ab28c..c71bf7b815 100644 --- a/raphtory-storage/src/graph/edges/edges.rs +++ b/raphtory-storage/src/graph/edges/edges.rs @@ -1,10 +1,6 @@ use super::{edge_entry::EdgeStorageEntry, unlocked::UnlockedEdges}; -use crate::graph::{ - edges::{edge_ref::EdgeStorageRef, edge_storage_ops::EdgeStorageOps}, - variants::storage_variants3::StorageVariants3, -}; +use crate::graph::edges::edge_ref::EdgeStorageRef; use raphtory_api::core::entities::{LayerIds, EID}; -use raphtory_core::utils::iter::GenLockedIter; use rayon::iter::ParallelIterator; use std::sync::Arc; use storage::{utils::Iter2, Extension, ReadLockedEdges}; @@ -14,6 +10,10 @@ pub struct EdgesStorage { } impl EdgesStorage { + pub fn new(storage: Arc>) -> Self { + Self { storage } + } + #[inline] pub fn as_ref(&self) -> EdgesStorageRef { EdgesStorageRef::Mem(self.storage.as_ref()) @@ -59,25 +59,13 @@ impl<'a> EdgesStorageRef<'a> { pub fn par_iter( self, - layers: &LayerIds, - ) -> impl ParallelIterator> + use<'a, '_> { + layers: &'a LayerIds, + ) -> impl ParallelIterator> + use<'a> { match self { - EdgesStorageRef::Mem(storage) => StorageVariants3::Mem( - storage - .par_iter() - .filter(move |e| e.has_layer(layers)) - .map(EdgeStorageEntry::Mem), - ), - EdgesStorageRef::Unlocked(edges) => StorageVariants3::Unlocked( - edges - .par_iter() - .filter(move |e| e.as_mem_edge().has_layer(layers)) - .map(EdgeStorageEntry::Unlocked), - ), - #[cfg(feature = "storage")] - EdgesStorageRef::Disk(storage) => { - StorageVariants3::Disk(storage.par_iter(layers).map(EdgeStorageEntry::Disk)) + EdgesStorageRef::Mem(storage) => { + Iter2::I1(storage.par_iter(layers).map(EdgeStorageEntry::Mem)) } + EdgesStorageRef::Unlocked(edges) => Iter2::I2(edges.par_iter(layers)), } } @@ -86,41 +74,32 @@ impl<'a> EdgesStorageRef<'a> { match self { EdgesStorageRef::Mem(storage) => match layers { LayerIds::None => 0, - LayerIds::All => storage.len(), - _ => storage.par_iter().filter(|e| e.has_layer(layers)).count(), + LayerIds::All => storage.storage().num_edges(), + LayerIds::One(layer_id) => storage.storage().num_edges_layer(*layer_id), + _ => self.par_iter(layers).count(), }, EdgesStorageRef::Unlocked(edges) => match layers { LayerIds::None => 0, - LayerIds::All => edges.len(), - _ => edges - .par_iter() - .filter(|e| e.as_mem_edge().has_layer(layers)) - .count(), + LayerIds::One(layer_id) => edges.storage().num_edges_layer(*layer_id), + LayerIds::All => edges.storage().num_edges_layer(0), + _ => self.par_iter(layers).count(), }, - #[cfg(feature = "storage")] - EdgesStorageRef::Disk(storage) => storage.count(layers), } } #[inline] pub fn edge(self, edge: EID) -> EdgeStorageEntry<'a> { match self { - EdgesStorageRef::Mem(storage) => EdgeStorageEntry::Mem(storage.get_mem(edge)), - EdgesStorageRef::Unlocked(storage) => { - EdgeStorageEntry::Unlocked(storage.0.edge_entry(edge)) - } - #[cfg(feature = "storage")] - EdgesStorageRef::Disk(storage) => EdgeStorageEntry::Disk(storage.edge(edge)), + EdgesStorageRef::Mem(storage) => EdgeStorageEntry::Mem(storage.edge_ref(edge)), + EdgesStorageRef::Unlocked(storage) => storage.edge(edge), } } #[inline] pub fn len(&self) -> usize { match self { - EdgesStorageRef::Mem(storage) => storage.len(), - EdgesStorageRef::Unlocked(storage) => storage.len(), - #[cfg(feature = "storage")] - EdgesStorageRef::Disk(storage) => storage.len(), + EdgesStorageRef::Mem(storage) => storage.storage().num_edges(), + EdgesStorageRef::Unlocked(storage) => storage.storage().num_edges(), } } } diff --git a/raphtory-storage/src/graph/edges/unlocked.rs b/raphtory-storage/src/graph/edges/unlocked.rs index f5592ae218..087d6125e0 100644 --- a/raphtory-storage/src/graph/edges/unlocked.rs +++ b/raphtory-storage/src/graph/edges/unlocked.rs @@ -1,7 +1,6 @@ -use raphtory_api::core::entities::EID; -use raphtory_core::storage::raw_edges::EdgeRGuard; +use raphtory_core::entities::{LayerIds, EID}; use rayon::prelude::*; -use storage::{Extension, Layer}; +use storage::{pages::edge_store::EdgeStorageInner, utils::Iter4, Extension, Layer}; use crate::graph::edges::edge_entry::EdgeStorageEntry; @@ -9,14 +8,34 @@ use crate::graph::edges::edge_entry::EdgeStorageEntry; pub struct UnlockedEdges<'a>(pub(crate) &'a Layer); impl<'a> UnlockedEdges<'a> { - pub fn iter(self, layer_id: usize) -> impl Iterator> + 'a { + pub fn storage(&self) -> &'a EdgeStorageInner, Extension> { + self.0.edges() + } + + pub fn edge(&self, e_id: EID) -> EdgeStorageEntry<'a> { + EdgeStorageEntry::Unlocked(self.0.edges().edge(e_id)) + } + + pub fn iter_layer(self, layer_id: usize) -> impl Iterator> + 'a { self.0 .edges() .iter(layer_id) .map(EdgeStorageEntry::Unlocked) } - pub fn par_iter( + pub fn iter(self, layer_ids: &'a LayerIds) -> impl Iterator> + 'a { + match layer_ids { + LayerIds::None => Iter4::I(std::iter::empty()), + LayerIds::All => Iter4::J(self.iter_layer(0)), + LayerIds::One(layer_id) => Iter4::K(self.iter_layer(*layer_id)), + LayerIds::Multiple(multiple) => Iter4::L( + self.iter_layer(0) + .filter(|edge| edge.as_ref().has_layers(multiple)), + ), + } + } + + pub fn par_iter_layer( self, layer_id: usize, ) -> impl ParallelIterator> + 'a { @@ -26,8 +45,18 @@ impl<'a> UnlockedEdges<'a> { .map(EdgeStorageEntry::Unlocked) } - #[inline] - pub fn len(self) -> usize { - self.0.edges().num_edges() + pub fn par_iter( + self, + layer_ids: &'a LayerIds, + ) -> impl ParallelIterator> + 'a { + match layer_ids { + LayerIds::None => Iter4::I(rayon::iter::empty()), + LayerIds::All => Iter4::J(self.par_iter_layer(0)), + LayerIds::One(layer_id) => Iter4::K(self.par_iter_layer(*layer_id)), + LayerIds::Multiple(multiple) => Iter4::L( + self.par_iter_layer(0) + .filter(|edge| edge.as_ref().has_layers(multiple)), + ), + } } } diff --git a/raphtory-storage/src/graph/graph.rs b/raphtory-storage/src/graph/graph.rs index 52254234c1..935cce2b41 100644 --- a/raphtory-storage/src/graph/graph.rs +++ b/raphtory-storage/src/graph/graph.rs @@ -180,9 +180,9 @@ impl GraphStorage { #[inline(always)] pub fn core_nodes(&self) -> NodesStorage { match self { - GraphStorage::Mem(storage) => NodesStorage::Mem(storage.nodes.clone()), + GraphStorage::Mem(storage) => NodesStorage::new(storage.nodes.clone()), GraphStorage::Unlocked(storage) => { - NodesStorage::Mem(storage.read_locked().nodes.clone()) + NodesStorage::new(storage.read_locked().nodes.clone()) } #[cfg(feature = "storage")] GraphStorage::Disk(storage) => { @@ -220,9 +220,9 @@ impl GraphStorage { #[inline(always)] pub fn owned_edges(&self) -> EdgesStorage { match self { - GraphStorage::Mem(storage) => EdgesStorage::Mem(storage.edges.clone()), + GraphStorage::Mem(storage) => EdgesStorage::new(storage.edges.clone()), GraphStorage::Unlocked(storage) => { - EdgesStorage::Mem(storage.storage().edges().locked().into()) + EdgesStorage::new(storage.storage().edges().locked().into()) } #[cfg(feature = "storage")] GraphStorage::Disk(storage) => EdgesStorage::Disk(DiskEdges::new(storage)), diff --git a/raphtory-storage/src/graph/nodes/nodes.rs b/raphtory-storage/src/graph/nodes/nodes.rs index 3745b899c0..b9ed0b973f 100644 --- a/raphtory-storage/src/graph/nodes/nodes.rs +++ b/raphtory-storage/src/graph/nodes/nodes.rs @@ -14,6 +14,11 @@ pub struct NodesStorage { } impl NodesStorage { + + pub fn new(storage: Arc>) -> Self { + Self { storage } + } + #[inline] pub fn as_ref(&self) -> NodesStorageEntry { NodesStorageEntry::Mem(self.storage.as_ref()) From f5fe58c629a706ea4438d3b1eb52fe591c776351 Mon Sep 17 00:00:00 2001 From: Fadhil Abubaker Date: Wed, 25 Jun 2025 11:39:59 -0400 Subject: [PATCH 033/321] Use graph.resolve_node for Storage InternalAdditionOps --- raphtory/src/db/api/storage/storage.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/raphtory/src/db/api/storage/storage.rs b/raphtory/src/db/api/storage/storage.rs index 34b1cecf59..0870d2b78b 100644 --- a/raphtory/src/db/api/storage/storage.rs +++ b/raphtory/src/db/api/storage/storage.rs @@ -394,7 +394,7 @@ impl InternalAdditionOps for Storage { match id { NodeRef::Internal(id) => Ok(MaybeNew::Existing(id)), NodeRef::External(gid) => { - let id = self.resolve_node(id)?; + let id = self.graph.resolve_node(id)?; #[cfg(feature = "proto")] self.if_cache(|cache| cache.resolve_node(id, gid)); From d1d637c14f3a3afc3f3a76b25ab62b72adab33cc Mon Sep 17 00:00:00 2001 From: Fadhil Abubaker Date: Wed, 25 Jun 2025 12:00:14 -0400 Subject: [PATCH 034/321] Call graph methods correctly --- raphtory-storage/src/graph/locked.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/raphtory-storage/src/graph/locked.rs b/raphtory-storage/src/graph/locked.rs index 5c8752f18d..c840bbf19d 100644 --- a/raphtory-storage/src/graph/locked.rs +++ b/raphtory-storage/src/graph/locked.rs @@ -65,8 +65,8 @@ pub struct WriteLockedGraph<'a> { impl<'a> WriteLockedGraph<'a> { pub(crate) fn new(graph: &'a TemporalGraph) -> Self { - let nodes = graph.storage.nodes.write_lock(); - let edges = graph.storage.edges.write_lock(); + let nodes = graph.storage().nodes().write_lock(); + let edges = graph.storage().edges().write_lock(); Self { nodes, edges, @@ -75,17 +75,17 @@ impl<'a> WriteLockedGraph<'a> { } pub fn num_nodes(&self) -> usize { - self.graph.storage.nodes.len() + self.graph.storage().nodes().len() } pub fn resolve_node(&self, gid: GidRef) -> Result, InvalidNodeId> { self.graph .logical_to_physical - .get_or_init(gid, || self.graph.storage.nodes.next_id()) + .get_or_init(gid, || self.graph.storage().nodes().next_id()) } pub fn resolve_node_type(&self, node_type: Option<&str>) -> MaybeNew { node_type - .map(|node_type| self.graph.node_meta.get_or_create_node_type_id(node_type)) + .map(|node_type| self.graph.node_meta().get_or_create_node_type_id(node_type)) .unwrap_or_else(|| MaybeNew::Existing(0)) } From 8f69a72c0dc723efa8b91a5e77d7ec7956a67c17 Mon Sep 17 00:00:00 2001 From: Fadhil Abubaker Date: Wed, 25 Jun 2025 15:08:43 -0400 Subject: [PATCH 035/321] Remove Resolver trait for now --- db4-storage/src/resolver/mod.rs | 30 ------------------------------ 1 file changed, 30 deletions(-) delete mode 100644 db4-storage/src/resolver/mod.rs diff --git a/db4-storage/src/resolver/mod.rs b/db4-storage/src/resolver/mod.rs deleted file mode 100644 index 7bde1cbe4d..0000000000 --- a/db4-storage/src/resolver/mod.rs +++ /dev/null @@ -1,30 +0,0 @@ -use std::path::Path; -use thiserror::Error; - -pub trait ResolverEntryOps: Send + Sync { - fn set(&self, value: impl Into) -> Result<(), ResolverError>; -} - -pub trait ResolverOps: Send + Sync { - type Entry: ResolverEntryOps; - - fn new(path: impl AsRef) -> Self - where - Self: Sized; - - fn generate_id(&self) -> V; - - /// Reserve an entry for a key in the resolver. - fn entry(&self, key: impl Into) -> Result>; - - fn get(&self, key: impl AsRef) -> Option; -} - -#[derive(Debug, Error)] -pub enum ResolverError { - #[error("Key {0} already exists with value {1}")] - KeyExists(K, V), - - #[error("Operation failed: {0}")] - OperationFailed(String), -} From 009762dd4218213229d6b3673fc0a5c020082055 Mon Sep 17 00:00:00 2001 From: Fadhil Abubaker Date: Wed, 25 Jun 2025 15:09:14 -0400 Subject: [PATCH 036/321] Create GIDResolver type alias --- db4-graph/src/lib.rs | 11 +++++++---- db4-storage/src/lib.rs | 6 ++++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/db4-graph/src/lib.rs b/db4-graph/src/lib.rs index 345398f9d6..0014bda466 100644 --- a/db4-graph/src/lib.rs +++ b/db4-graph/src/lib.rs @@ -9,10 +9,12 @@ use std::{ // use crate::entries::node::UnlockedNodeEntry; use raphtory_api::core::{entities::properties::meta::Meta, input::input_node::InputNode}; use raphtory_core::entities::{ - graph::logical_to_physical::Mapping, nodes::node_ref::NodeRef, - properties::graph_meta::GraphMeta, GidRef, VID, + nodes::node_ref::NodeRef, properties::graph_meta::GraphMeta, GidRef, VID, +}; +use storage::{ + persist::strategy::PersistentStrategy, Extension, GIDResolver, + Layer, ReadLockedLayer, ES, NS, }; -use storage::{persist::strategy::PersistentStrategy, Extension, Layer, ReadLockedLayer, ES, NS}; pub mod entries; pub mod mutation; @@ -20,8 +22,9 @@ pub mod mutation; #[derive(Debug)] pub struct TemporalGraph { graph_dir: PathBuf, + // mapping between logical and physical ids - pub logical_to_physical: Mapping, + pub logical_to_physical: GIDResolver, pub node_count: AtomicUsize, max_page_len_nodes: usize, diff --git a/db4-storage/src/lib.rs b/db4-storage/src/lib.rs index 69dd7ac44e..298cb6dced 100644 --- a/db4-storage/src/lib.rs +++ b/db4-storage/src/lib.rs @@ -15,6 +15,7 @@ use crate::{ }, }; use raphtory_api::core::entities::{EID, VID}; +use raphtory_core::entities::graph::logical_to_physical::Mapping; use segments::{edge::MemEdgeSegment, node::MemNodeSegment}; pub mod api; @@ -23,7 +24,6 @@ pub mod gen_ts; pub mod pages; pub mod persist; pub mod properties; -pub mod resolver; pub mod segments; pub mod utils; @@ -31,8 +31,10 @@ pub type Extension = (); pub type NS

= NodeSegmentView

; pub type ES

= EdgeSegmentView

; pub type Layer = GraphStore, EdgeSegmentView, EXT>; -pub type ReadLockedLayer = ReadLockedGraphStore; +pub type GIDResolver = Mapping; + +pub type ReadLockedLayer = ReadLockedGraphStore; pub type ReadLockedNodes

= ReadLockedNodeStorage; pub type ReadLockedEdges

= ReadLockedEdgeStorage; From 43443af40af484796556e1e8dfaad84cf7b7e617 Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Thu, 26 Jun 2025 11:20:58 +0100 Subject: [PATCH 037/321] raphtory-storage now compiles --- db4-graph/src/lib.rs | 103 +++++++++++++++++- raphtory-core/src/storage/node_entry.rs | 8 +- raphtory-storage/src/core_ops.rs | 35 +++--- raphtory-storage/src/graph/locked.rs | 66 +++++------ raphtory-storage/src/mutation/deletion_ops.rs | 23 ++++ raphtory-storage/src/mutation/mod.rs | 2 +- .../src/mutation/property_addition_ops.rs | 61 ++++++++++- 7 files changed, 236 insertions(+), 62 deletions(-) diff --git a/db4-graph/src/lib.rs b/db4-graph/src/lib.rs index 345398f9d6..c197863641 100644 --- a/db4-graph/src/lib.rs +++ b/db4-graph/src/lib.rs @@ -7,12 +7,27 @@ use std::{ }; // use crate::entries::node::UnlockedNodeEntry; -use raphtory_api::core::{entities::properties::meta::Meta, input::input_node::InputNode}; -use raphtory_core::entities::{ - graph::logical_to_physical::Mapping, nodes::node_ref::NodeRef, - properties::graph_meta::GraphMeta, GidRef, VID, +use raphtory_api::core::{ + entities::{ + self, + properties::{meta::Meta, prop::Prop}, + }, + input::input_node::InputNode, + storage::dict_mapper::MaybeNew, +}; +use raphtory_core::{ + entities::{ + graph::{logical_to_physical::Mapping, tgraph::InvalidLayer}, + nodes::node_ref::NodeRef, + properties::graph_meta::GraphMeta, + GidRef, LayerIds, EID, VID, + }, + storage::timeindex::TimeIndexEntry, +}; +use storage::{ + error::DBV4Error, persist::strategy::PersistentStrategy, Extension, Layer, ReadLockedLayer, ES, + NS, }; -use storage::{persist::strategy::PersistentStrategy, Extension, Layer, ReadLockedLayer, ES, NS}; pub mod entries; pub mod mutation; @@ -102,4 +117,82 @@ impl, ES = ES>> TemporalGraph { pub fn max_page_len_edges(&self) -> usize { self.max_page_len_edges } + + pub fn layer_ids(&self, key: entities::Layer) -> Result { + match key { + entities::Layer::None => Ok(LayerIds::None), + entities::Layer::All => Ok(LayerIds::All), + entities::Layer::Default => Ok(LayerIds::One(0)), + entities::Layer::One(id) => match self.edge_meta.get_layer_id(&id) { + Some(id) => Ok(LayerIds::One(id)), + None => Err(InvalidLayer::new( + id, + Self::get_valid_layers(&self.edge_meta), + )), + }, + entities::Layer::Multiple(ids) => { + let mut new_layers = ids + .iter() + .map(|id| { + self.edge_meta.get_layer_id(id).ok_or_else(|| { + InvalidLayer::new(id.clone(), Self::get_valid_layers(&self.edge_meta)) + }) + }) + .collect::, InvalidLayer>>()?; + let num_layers = self.num_layers(); + let num_new_layers = new_layers.len(); + if num_new_layers == 0 { + Ok(LayerIds::None) + } else if num_new_layers == 1 { + Ok(LayerIds::One(new_layers[0])) + } else if num_new_layers == num_layers { + Ok(LayerIds::All) + } else { + new_layers.sort_unstable(); + new_layers.dedup(); + Ok(LayerIds::Multiple(new_layers.into())) + } + } + } + } + + fn get_valid_layers(edge_meta: &Meta) -> Vec { + edge_meta + .layer_meta() + .get_keys() + .iter() + .map(|x| x.to_string()) + .collect::>() + } + + pub fn valid_layer_ids(&self, key: entities::Layer) -> LayerIds { + match key { + entities::Layer::None => LayerIds::None, + entities::Layer::All => LayerIds::All, + entities::Layer::Default => LayerIds::One(0), + entities::Layer::One(id) => match self.edge_meta.get_layer_id(&id) { + Some(id) => LayerIds::One(id), + None => LayerIds::None, + }, + entities::Layer::Multiple(ids) => { + let mut new_layers = ids + .iter() + .flat_map(|id| self.edge_meta.get_layer_id(id)) + .collect::>(); + let num_layers = self.num_layers(); + let num_new_layers = new_layers.len(); + if num_new_layers == 0 { + LayerIds::None + } else if num_new_layers == 1 { + LayerIds::One(new_layers[0]) + } else if num_new_layers == num_layers { + LayerIds::All + } else { + new_layers.sort_unstable(); + new_layers.dedup(); + LayerIds::Multiple(new_layers.into()) + } + } + } + } } diff --git a/raphtory-core/src/storage/node_entry.rs b/raphtory-core/src/storage/node_entry.rs index ce3c1b90fe..0d851bf8ab 100644 --- a/raphtory-core/src/storage/node_entry.rs +++ b/raphtory-core/src/storage/node_entry.rs @@ -3,9 +3,7 @@ use crate::entities::{nodes::node_store::NodeStore, properties::tprop::TPropCell use itertools::Itertools; use raphtory_api::core::{ entities::{ - edges::edge_ref::EdgeRef, - properties::{prop::Prop, tprop::TPropOps}, - LayerIds, + edges::edge_ref::EdgeRef, properties::{prop::Prop, tprop::TPropOps}, GidRef, LayerIds }, storage::timeindex::TimeIndexEntry, Direction, @@ -135,4 +133,8 @@ impl<'a> NodePtr<'a> { .iter_window(w) .map(move |(t, row)| (*t, MemRow::new(self.t_props_log, *row))) } + + pub fn id(&self) -> GidRef { + self.node.global_id().into() + } } diff --git a/raphtory-storage/src/core_ops.rs b/raphtory-storage/src/core_ops.rs index 8bce9899e5..437a69f3f8 100644 --- a/raphtory-storage/src/core_ops.rs +++ b/raphtory-storage/src/core_ops.rs @@ -36,10 +36,9 @@ pub fn is_view_compatible(g1: &impl CoreGraphOps, g2: &impl CoreGraphOps) -> boo pub trait CoreGraphOps: Send + Sync { fn id_type(&self) -> Option { match self.core_graph() { - GraphStorage::Mem(graph) => graph.inner().logical_to_physical.dtype(), - GraphStorage::Unlocked(graph) => graph.logical_to_physical.dtype(), - #[cfg(feature = "storage")] - GraphStorage::Disk(storage) => Some(storage.inner().id_type()), + GraphStorage::Mem(LockedGraph { graph, .. }) | GraphStorage::Unlocked(graph) => { + graph.logical_to_physical.dtype() + } } } @@ -56,10 +55,9 @@ pub trait CoreGraphOps: Send + Sync { /// get the current sequence id without incrementing the counter fn read_event_id(&self) -> usize { match self.core_graph() { - GraphStorage::Unlocked(graph) => graph.read_event_counter(), - GraphStorage::Mem(graph) => graph.inner().read_event_counter(), - #[cfg(feature = "storage")] - GraphStorage::Disk(storage) => storage.inner.count_temporal_edges(), + GraphStorage::Mem(LockedGraph { graph, .. }) | GraphStorage::Unlocked(graph) => { + graph.read_event_counter() + } } } @@ -166,9 +164,7 @@ pub trait CoreGraphOps: Send + Sync { #[inline] fn node_name(&self, v: VID) -> String { let node = self.core_node(v); - node.name() - .map(|name| name.to_string()) - .unwrap_or_else(|| node.id().to_str().to_string()) + node.name().as_ref().to_owned() } /// Returns the type of node @@ -232,7 +228,8 @@ pub trait CoreGraphOps: Send + Sync { /// The property value if it exists. fn constant_node_prop(&self, v: VID, id: usize) -> Option { let core_node_entry = self.core_node(v); - core_node_entry.prop(id) + // TODO: figure out how to expose the layer_id to the calling API + core_node_entry.prop(0, id) } /// Gets the keys of constant properties of a given node @@ -243,9 +240,12 @@ pub trait CoreGraphOps: Send + Sync { /// /// # Returns /// The keys of the constant properties. - fn constant_node_prop_ids(&self, v: VID) -> BoxedLIter { - let core_node_entry = self.core_node(v); - core_node_entry.prop_ids() + fn constant_node_prop_ids(&self, _v: VID) -> BoxedLIter { + // property 0 = node type, property 1 = external node id + // on an empty graph, this will return an empty range + let end = self.node_meta().const_prop_meta().len(); + let start = 2.min(end); + Box::new(start..end) } /// Returns a vector of all ids of temporal properties within the given node @@ -256,9 +256,8 @@ pub trait CoreGraphOps: Send + Sync { /// /// # Returns /// The ids of the temporal properties - fn temporal_node_prop_ids(&self, v: VID) -> Box + '_> { - let core_node_entry = self.core_node(v); - core_node_entry.temporal_prop_ids() + fn temporal_node_prop_ids(&self, _v: VID) -> Box + '_> { + Box::new(0..self.node_meta().temporal_prop_meta().len()) } } diff --git a/raphtory-storage/src/graph/locked.rs b/raphtory-storage/src/graph/locked.rs index 5c8752f18d..af80094d38 100644 --- a/raphtory-storage/src/graph/locked.rs +++ b/raphtory-storage/src/graph/locked.rs @@ -63,41 +63,41 @@ pub struct WriteLockedGraph<'a> { pub graph: &'a TemporalGraph, } -impl<'a> WriteLockedGraph<'a> { - pub(crate) fn new(graph: &'a TemporalGraph) -> Self { - let nodes = graph.storage.nodes.write_lock(); - let edges = graph.storage.edges.write_lock(); - Self { - nodes, - edges, - graph, - } - } +// impl<'a> WriteLockedGraph<'a> { +// pub(crate) fn new(graph: &'a TemporalGraph) -> Self { +// let nodes = graph.storage.nodes.write_lock(); +// let edges = graph.storage.edges.write_lock(); +// Self { +// nodes, +// edges, +// graph, +// } +// } - pub fn num_nodes(&self) -> usize { - self.graph.storage.nodes.len() - } - pub fn resolve_node(&self, gid: GidRef) -> Result, InvalidNodeId> { - self.graph - .logical_to_physical - .get_or_init(gid, || self.graph.storage.nodes.next_id()) - } +// pub fn num_nodes(&self) -> usize { +// self.graph.storage.nodes.len() +// } +// pub fn resolve_node(&self, gid: GidRef) -> Result, InvalidNodeId> { +// self.graph +// .logical_to_physical +// .get_or_init(gid, || self.graph.storage.nodes.next_id()) +// } - pub fn resolve_node_type(&self, node_type: Option<&str>) -> MaybeNew { - node_type - .map(|node_type| self.graph.node_meta.get_or_create_node_type_id(node_type)) - .unwrap_or_else(|| MaybeNew::Existing(0)) - } +// pub fn resolve_node_type(&self, node_type: Option<&str>) -> MaybeNew { +// node_type +// .map(|node_type| self.graph.node_meta.get_or_create_node_type_id(node_type)) +// .unwrap_or_else(|| MaybeNew::Existing(0)) +// } - pub fn num_shards(&self) -> usize { - self.nodes.num_shards().max(self.edges.num_shards()) - } +// pub fn num_shards(&self) -> usize { +// self.nodes.num_shards().max(self.edges.num_shards()) +// } - pub fn edges_mut(&mut self) -> &mut WriteLockedEdges<'a> { - &mut self.edges - } +// pub fn edges_mut(&mut self) -> &mut WriteLockedEdges<'a> { +// &mut self.edges +// } - pub fn graph(&self) -> &TemporalGraph { - self.graph - } -} +// pub fn graph(&self) -> &TemporalGraph { +// self.graph +// } +// } diff --git a/raphtory-storage/src/mutation/deletion_ops.rs b/raphtory-storage/src/mutation/deletion_ops.rs index 7d2f2ddcf6..4d240314af 100644 --- a/raphtory-storage/src/mutation/deletion_ops.rs +++ b/raphtory-storage/src/mutation/deletion_ops.rs @@ -25,6 +25,29 @@ pub trait InternalDeletionOps { ) -> Result<(), Self::Error>; } +impl InternalDeletionOps for db4_graph::TemporalGraph { + type Error = MutationError; + + fn internal_delete_edge( + &self, + t: TimeIndexEntry, + src: VID, + dst: VID, + layer: usize, + ) -> Result, Self::Error> { + todo!() + } + + fn internal_delete_existing_edge( + &self, + t: TimeIndexEntry, + eid: EID, + layer: usize, + ) -> Result<(), Self::Error> { + todo!() + } +} + impl InternalDeletionOps for TemporalGraph { type Error = MutationError; diff --git a/raphtory-storage/src/mutation/mod.rs b/raphtory-storage/src/mutation/mod.rs index a9b77a1c7d..37fa9cc386 100644 --- a/raphtory-storage/src/mutation/mod.rs +++ b/raphtory-storage/src/mutation/mod.rs @@ -53,7 +53,7 @@ pub enum MutationError { dst: String, }, #[error("Storage error: {0}")] - DBV4Error(DBV4Error), + DBV4Error(#[from] DBV4Error), } pub trait InheritMutationOps: Base {} diff --git a/raphtory-storage/src/mutation/property_addition_ops.rs b/raphtory-storage/src/mutation/property_addition_ops.rs index 53db8e8b68..8ce98b629f 100644 --- a/raphtory-storage/src/mutation/property_addition_ops.rs +++ b/raphtory-storage/src/mutation/property_addition_ops.rs @@ -176,6 +176,63 @@ impl InternalPropertyAdditionOps for TemporalGraph { } } +impl InternalPropertyAdditionOps for db4_graph::TemporalGraph { + type Error = MutationError; + + fn internal_add_properties( + &self, + t: TimeIndexEntry, + props: &[(usize, Prop)], + ) -> Result<(), Self::Error> { + todo!() + } + + fn internal_add_constant_properties(&self, props: &[(usize, Prop)]) -> Result<(), Self::Error> { + todo!() + } + + fn internal_update_constant_properties( + &self, + props: &[(usize, Prop)], + ) -> Result<(), Self::Error> { + todo!() + } + + fn internal_add_constant_node_properties( + &self, + vid: VID, + props: &[(usize, Prop)], + ) -> Result<(), Self::Error> { + todo!() + } + + fn internal_update_constant_node_properties( + &self, + vid: VID, + props: &[(usize, Prop)], + ) -> Result<(), Self::Error> { + todo!() + } + + fn internal_add_constant_edge_properties( + &self, + eid: EID, + layer: usize, + props: &[(usize, Prop)], + ) -> Result<(), Self::Error> { + todo!() + } + + fn internal_update_constant_edge_properties( + &self, + eid: EID, + layer: usize, + props: &[(usize, Prop)], + ) -> Result<(), Self::Error> { + todo!() + } +} + impl InternalPropertyAdditionOps for GraphStorage { type Error = MutationError; @@ -184,11 +241,11 @@ impl InternalPropertyAdditionOps for GraphStorage { t: TimeIndexEntry, props: &[(usize, Prop)], ) -> Result<(), Self::Error> { - self.mutable()?.internal_add_properties(t, props) + Ok(self.mutable()?.internal_add_properties(t, props)?) } fn internal_add_constant_properties(&self, props: &[(usize, Prop)]) -> Result<(), Self::Error> { - self.mutable()?.internal_add_constant_properties(props) + Ok(self.mutable()?.internal_add_constant_properties(props)?) } fn internal_update_constant_properties( From 8ce8a8858153013b4e15788a1db441cf5cf570ca Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Thu, 26 Jun 2025 14:22:23 +0100 Subject: [PATCH 038/321] adapt additions and timestamps to one layer only --- db4-storage/src/api/edges.rs | 17 +- db4-storage/src/gen_t_props.rs | 28 +++- db4-storage/src/gen_ts.rs | 67 +++++--- db4-storage/src/segments/edge_entry.rs | 34 +++- .../src/graph/edges/edge_entry.rs | 154 +++++++++--------- .../src/graph/edges/edge_storage_ops.rs | 65 +++----- raphtory/Cargo.toml | 1 + .../internal/time_semantics/filtered_edge.rs | 4 +- 8 files changed, 218 insertions(+), 152 deletions(-) diff --git a/db4-storage/src/api/edges.rs b/db4-storage/src/api/edges.rs index a7652db10f..0d7aa6750f 100644 --- a/db4-storage/src/api/edges.rs +++ b/db4-storage/src/api/edges.rs @@ -7,7 +7,7 @@ use std::{ use parking_lot::{RwLockReadGuard, RwLockWriteGuard, lock_api::ArcRwLockReadGuard}; use raphtory_api::core::entities::properties::{meta::Meta, prop::Prop, tprop::TPropOps}; use raphtory_core::{ - entities::{LayerIds, VID}, + entities::{LayerIds, EID, VID}, storage::timeindex::{TimeIndexEntry, TimeIndexOps}, }; use rayon::iter::ParallelIterator; @@ -142,9 +142,24 @@ pub trait EdgeRefOps<'a>: Copy + Clone + Send + Sync { fn edge(self, layer_id: usize) -> Option<(VID, VID)>; + fn has_layer_inner(self, layer_id: usize) -> bool{ + self.edge(layer_id).is_some() + } + + fn internal_num_layers(self) -> usize; + fn additions(self, layer_ids: &'a LayerIds) -> Self::Additions; + fn layer_additions(self, layer_id: usize) -> Self::Additions; + fn c_prop(self, layer_id: usize, prop_id: usize) -> Option; fn t_prop(self, layer_id: &'a LayerIds, prop_id: usize) -> Self::TProps; + fn layer_t_prop(self, layer_id: usize, prop_id: usize) -> Self::TProps; + + fn src(&self) -> VID; + + fn dst(&self) -> VID; + + fn edge_id(&self) -> EID; } diff --git a/db4-storage/src/gen_t_props.rs b/db4-storage/src/gen_t_props.rs index 119ae36c6e..2ca0f73a3f 100644 --- a/db4-storage/src/gen_t_props.rs +++ b/db4-storage/src/gen_t_props.rs @@ -1,5 +1,6 @@ -use std::ops::Range; +use std::{borrow::Borrow, ops::Range}; +use either::Either; use itertools::Itertools; use raphtory_api::core::entities::properties::{prop::Prop, tprop::TPropOps}; use raphtory_core::{entities::LayerIds, storage::timeindex::TimeIndexEntry}; @@ -19,12 +20,12 @@ where prop_id: usize, ) -> impl Iterator + 'a; - fn into_t_props_layers<'b>( + fn into_t_props_layers( self, - layers: &'b LayerIds, + layers: impl Borrow, prop_id: usize, ) -> impl Iterator + 'a { - match layers { + match layers.borrow() { LayerIds::None => Iter4::I(std::iter::empty()), LayerIds::One(layer_id) => Iter4::J(self.into_t_props(*layer_id, prop_id)), LayerIds::All => Iter4::K( @@ -44,7 +45,7 @@ where #[derive(Clone, Copy)] pub struct GenTProps<'a, Ref> { node: Ref, - layer_id: &'a LayerIds, + layer_id: Either<&'a LayerIds, usize>, prop_id: usize, } @@ -52,7 +53,15 @@ impl<'a, Ref> GenTProps<'a, Ref> { pub fn new(node: Ref, layer_id: &'a LayerIds, prop_id: usize) -> Self { Self { node, - layer_id, + layer_id: Either::Left(layer_id), + prop_id, + } + } + + pub fn new_with_layer(node: Ref, layer_id: usize, prop_id: usize) -> Self { + Self { + node, + layer_id: Either::Right(layer_id), prop_id, } } @@ -60,7 +69,12 @@ impl<'a, Ref> GenTProps<'a, Ref> { impl<'a, Ref: WithTProps<'a>> GenTProps<'a, Ref> { fn tprops(self, prop_id: usize) -> impl Iterator + 'a { - self.node.into_t_props_layers(self.layer_id, prop_id) + match self.layer_id { + Either::Left(layer_ids) => { + Either::Left(self.node.into_t_props_layers(layer_ids, prop_id)) + } + Either::Right(layer_id) => Either::Right(self.node.into_t_props(layer_id, prop_id)), + } } } diff --git a/db4-storage/src/gen_ts.rs b/db4-storage/src/gen_ts.rs index 09c194af65..8864f2f2c4 100644 --- a/db4-storage/src/gen_ts.rs +++ b/db4-storage/src/gen_ts.rs @@ -1,5 +1,6 @@ use std::{borrow::Borrow, ops::Range}; +use either::Either; use itertools::Itertools; use raphtory_core::{ entities::LayerIds, @@ -12,7 +13,7 @@ use crate::utils::Iter4; #[derive(Clone, Copy)] pub struct GenericTimeOps<'a, Ref> { range: Option<(TimeIndexEntry, TimeIndexEntry)>, - layer_id: &'a LayerIds, + layer_id: Either<&'a LayerIds, usize>, node: Ref, } @@ -20,7 +21,15 @@ impl<'a, Ref> GenericTimeOps<'a, Ref> { pub fn new(node: Ref, layer_id: &'a LayerIds) -> Self { Self { range: None, - layer_id, + layer_id: Either::Left(layer_id), + node, + } + } + + pub fn new_with_layer(node: Ref, layer_id: usize) -> Self { + Self { + range: None, + layer_id: Either::Right(layer_id), node, } } @@ -60,18 +69,18 @@ where } } - fn into_iter<'b>( + fn into_iter>( self, - layer_ids: &'b LayerIds, + layer_ids: B, range: Option<(TimeIndexEntry, TimeIndexEntry)>, ) -> impl Iterator + Send + Sync + 'a { let iters = self.time_cells(layer_ids, range); iters.map(|cell| cell.iter()).kmerge() } - fn into_iter_rev<'b>( + fn into_iter_rev>( self, - layer_ids: &'b LayerIds, + layer_ids: B, range: Option<(TimeIndexEntry, TimeIndexEntry)>, ) -> impl Iterator + Send + Sync + 'a { let iters = self.time_cells(layer_ids, range); @@ -79,14 +88,23 @@ where } } +impl<'a, Ref: WithTimeCells<'a> + 'a> GenericTimeOps<'a, Ref> { + pub fn time_cells(self) -> impl Iterator + 'a { + match self.layer_id { + Either::Left(layer_ids) => Either::Left(self.node.time_cells(layer_ids, self.range)), + Either::Right(layer_id) => { + Either::Right(self.node.layer_time_cells(layer_id, self.range)) + } + } + } +} + impl<'a, Ref: WithTimeCells<'a> + 'a> TimeIndexOps<'a> for GenericTimeOps<'a, Ref> { type IndexType = TimeIndexEntry; type RangeType = Self; fn active(&self, w: Range) -> bool { - self.node - .time_cells(self.layer_id, self.range) - .any(|t_cell| t_cell.active(w.clone())) + self.time_cells().any(|t_cell| t_cell.active(w.clone())) } fn range(&self, w: Range) -> Self::RangeType { @@ -98,33 +116,32 @@ impl<'a, Ref: WithTimeCells<'a> + 'a> TimeIndexOps<'a> for GenericTimeOps<'a, Re } fn first(&self) -> Option { - Iterator::min( - self.node - .time_cells(self.layer_id, self.range) - .filter_map(|t_cell| t_cell.first()), - ) + Iterator::min(self.time_cells().filter_map(|t_cell| t_cell.first())) } fn last(&self) -> Option { - Iterator::max( - self.node - .time_cells(self.layer_id, self.range) - .filter_map(|t_cell| t_cell.last()), - ) + Iterator::max(self.time_cells().filter_map(|t_cell| t_cell.last())) } fn iter(self) -> impl Iterator + Send + Sync + 'a { - self.node.into_iter(self.layer_id, self.range) + match self.layer_id { + Either::Left(layer_id) => Either::Left(self.node.into_iter(layer_id, self.range)), + Either::Right(layer_id) => { + Either::Right(self.node.into_iter(LayerIds::One(layer_id), self.range)) + } + } } fn iter_rev(self) -> impl Iterator + Send + Sync + 'a { - self.node.into_iter_rev(self.layer_id, self.range) + match self.layer_id { + Either::Left(layer_id) => Either::Left(self.node.into_iter_rev(layer_id, self.range)), + Either::Right(layer_id) => { + Either::Right(self.node.into_iter_rev(LayerIds::One(layer_id), self.range)) + } + } } fn len(&self) -> usize { - self.node - .time_cells(self.layer_id, self.range) - .map(|t_cell| t_cell.len()) - .sum() + self.time_cells().map(|t_cell| t_cell.len()).sum() } } diff --git a/db4-storage/src/segments/edge_entry.rs b/db4-storage/src/segments/edge_entry.rs index 28f86bfeb4..82a2aec1fd 100644 --- a/db4-storage/src/segments/edge_entry.rs +++ b/db4-storage/src/segments/edge_entry.rs @@ -1,6 +1,6 @@ use raphtory_api::core::entities::properties::prop::Prop; use raphtory_core::{ - entities::{LayerIds, Multiple, VID, properties::tprop::TPropCell}, + entities::{EID, LayerIds, Multiple, VID, properties::tprop::TPropCell}, storage::timeindex::{TimeIndexEntry, TimeIndexOps}, }; @@ -135,6 +135,10 @@ impl<'a> EdgeRefOps<'a> for MemEdgeRef<'a> { EdgeAdditions::new(self, layer_id) } + fn layer_additions(self, layer_id: usize) -> Self::Additions { + EdgeAdditions::new_with_layer(self, layer_id) + } + fn c_prop(self, layer_id: usize, prop_id: usize) -> Option { self.es.as_ref()[layer_id].c_prop(self.pos, prop_id) } @@ -142,4 +146,32 @@ impl<'a> EdgeRefOps<'a> for MemEdgeRef<'a> { fn t_prop(self, layer_id: &'a LayerIds, prop_id: usize) -> Self::TProps { EdgeTProps::new(self, layer_id, prop_id) } + + fn layer_t_prop(self, layer_id: usize, prop_id: usize) -> Self::TProps { + EdgeTProps::new_with_layer(self, layer_id, prop_id) + } + + fn src(&self) -> VID { + self.es.as_ref()[0] + .get(&self.pos) + .map(|entry| entry.src) + .expect("Edge must have a source vertex") + } + + fn dst(&self) -> VID { + self.es.as_ref()[0] + .get(&self.pos) + .map(|entry| entry.dst) + .expect("Edge must have a destination vertex") + } + + fn edge_id(&self) -> EID { + let segment_id = self.es.as_ref()[0].segment_id(); + let max_page_len = self.es.as_ref()[0].max_page_len(); + self.pos.as_eid(segment_id, max_page_len) + } + + fn internal_num_layers(self) -> usize { + self.es.as_ref().len() + } } diff --git a/raphtory-storage/src/graph/edges/edge_entry.rs b/raphtory-storage/src/graph/edges/edge_entry.rs index bae8054fc5..daee4e4314 100644 --- a/raphtory-storage/src/graph/edges/edge_entry.rs +++ b/raphtory-storage/src/graph/edges/edge_entry.rs @@ -1,12 +1,10 @@ -use crate::graph::edges::{ - edge_ref::EdgeStorageRef, - edge_storage_ops::{EdgeStorageOps, TimeIndexRef}, -}; -use raphtory_api::core::entities::{ - properties::{prop::Prop, tprop::TPropOps}, - LayerIds, EID, VID, -}; use std::ops::Range; + +use crate::graph::edges::{ + edge_ref::EdgeStorageRef, edge_storage_ops::EdgeStorageOps} +; +use raphtory_api::core::entities::properties::{prop::Prop, tprop::TPropOps}; +use raphtory_core::{entities::{LayerIds, EID, VID}}; use storage::{api::edges::EdgeEntryOps, EdgeEntry, EdgeEntryRef}; #[cfg(feature = "storage")] @@ -32,73 +30,73 @@ impl<'a> EdgeStorageEntry<'a> { } } -// impl<'a, 'b: 'a> EdgeStorageOps<'a> for &'a EdgeStorageEntry<'b> { -// fn added(self, layer_ids: &LayerIds, w: Range) -> bool { -// self.as_ref().added(layer_ids, w) -// } - -// fn has_layer(self, layer_ids: &LayerIds) -> bool { -// self.as_ref().has_layer(layer_ids) -// } - -// fn src(self) -> VID { -// self.as_ref().src() -// } - -// fn dst(self) -> VID { -// self.as_ref().dst() -// } - -// fn eid(self) -> EID { -// self.as_ref().eid() -// } - -// fn layer_ids_iter(self, layer_ids: &'a LayerIds) -> impl Iterator + 'a { -// self.as_ref().layer_ids_iter(layer_ids) -// } - -// fn additions_iter( -// self, -// layer_ids: &'a LayerIds, -// ) -> impl Iterator)> + 'a { -// self.as_ref().additions_iter(layer_ids) -// } - -// fn deletions_iter( -// self, -// layer_ids: &'a LayerIds, -// ) -> impl Iterator)> + 'a { -// self.as_ref().deletions_iter(layer_ids) -// } - -// fn updates_iter( -// self, -// layer_ids: &'a LayerIds, -// ) -> impl Iterator, TimeIndexRef<'a>)> + 'a { -// self.as_ref().updates_iter(layer_ids) -// } - -// fn additions(self, layer_id: usize) -> TimeIndexRef<'a> { -// self.as_ref().additions(layer_id) -// } - -// fn deletions(self, layer_id: usize) -> TimeIndexRef<'a> { -// self.as_ref().deletions(layer_id) -// } - -// fn temporal_prop_layer(self, layer_id: usize, prop_id: usize) -> impl TPropOps<'a> + 'a { -// self.as_ref().temporal_prop_layer(layer_id, prop_id) -// } - -// fn temporal_prop_iter( -// self, -// layer_ids: &'a LayerIds, -// prop_id: usize, -// ) -> impl Iterator)> + 'a { -// self.as_ref().temporal_prop_iter(layer_ids, prop_id) -// } - -// fn constant_prop_layer(self, layer_id: usize, prop_id: usize) -> Option { -// self.as_ref().constant_prop_layer(layer_id, prop_id) -// } -// } +impl<'a, 'b: 'a> EdgeStorageOps<'a> for &'a EdgeStorageEntry<'b> { + fn added(self, layer_ids: &LayerIds, w: Range) -> bool { + self.as_ref().added(layer_ids, w) + } + + fn has_layer(self, layer_ids: &LayerIds) -> bool { + self.as_ref().has_layer(layer_ids) + } + + fn src(self) -> VID { + self.as_ref().src() + } + + fn dst(self) -> VID { + self.as_ref().dst() + } + + fn eid(self) -> EID { + self.as_ref().eid() + } + + fn layer_ids_iter(self, layer_ids: &'a LayerIds) -> impl Iterator + 'a { + self.as_ref().layer_ids_iter(layer_ids) + } + + fn additions_iter( + self, + layer_ids: &'a LayerIds, + ) -> impl Iterator)> + 'a { + self.as_ref().additions_iter(layer_ids) + } + + fn deletions_iter( + self, + layer_ids: &'a LayerIds, + ) -> impl Iterator)> + 'a { + self.as_ref().deletions_iter(layer_ids) + } + + fn updates_iter( + self, + layer_ids: &'a LayerIds, + ) -> impl Iterator, storage::EdgeAdditions<'a>)> + 'a { + self.as_ref().updates_iter(layer_ids) + } + + fn additions(self, layer_id: usize) -> storage::EdgeAdditions<'a>{ + self.as_ref().additions(layer_id) + } + + fn deletions(self, layer_id: usize) -> storage::EdgeAdditions<'a> { + self.as_ref().deletions(layer_id) + } + + fn temporal_prop_layer(self, layer_id: usize, prop_id: usize) -> impl TPropOps<'a> + 'a { + self.as_ref().temporal_prop_layer(layer_id, prop_id) + } + + fn temporal_prop_iter( + self, + layer_ids: &'a LayerIds, + prop_id: usize, + ) -> impl Iterator)> + 'a { + self.as_ref().temporal_prop_iter(layer_ids, prop_id) + } + + fn constant_prop_layer(self, layer_id: usize, prop_id: usize) -> Option { + self.as_ref().constant_prop_layer(layer_id, prop_id) + } +} diff --git a/raphtory-storage/src/graph/edges/edge_storage_ops.rs b/raphtory-storage/src/graph/edges/edge_storage_ops.rs index 58b9f26be2..00a354acfd 100644 --- a/raphtory-storage/src/graph/edges/edge_storage_ops.rs +++ b/raphtory-storage/src/graph/edges/edge_storage_ops.rs @@ -9,11 +9,9 @@ use raphtory_api::core::{ }, storage::timeindex::{TimeIndexEntry, TimeIndexOps}, }; -use raphtory_core::{ - entities::{edges::edge_store::MemEdge, properties::tprop::TProp}, - storage::timeindex::{TimeIndex, TimeIndexWindow}, -}; +use raphtory_core::storage::timeindex::{TimeIndex, TimeIndexWindow}; use std::ops::Range; +use storage::api::edges::EdgeRefOps; #[derive(Clone)] pub enum TimeIndexRef<'a> { @@ -130,7 +128,7 @@ pub trait EdgeStorageOps<'a>: Copy + Sized + Send + Sync + 'a { fn additions_iter( self, layer_ids: &'a LayerIds, - ) -> impl Iterator)> + Send + Sync + 'a { + ) -> impl Iterator)> + Send + Sync + 'a { self.layer_ids_iter(layer_ids) .map(move |id| (id, self.additions(id))) } @@ -138,7 +136,7 @@ pub trait EdgeStorageOps<'a>: Copy + Sized + Send + Sync + 'a { fn deletions_iter( self, layer_ids: &'a LayerIds, - ) -> impl Iterator)> + 'a { + ) -> impl Iterator)> + 'a { self.layer_ids_iter(layer_ids) .map(move |id| (id, self.deletions(id))) } @@ -146,14 +144,20 @@ pub trait EdgeStorageOps<'a>: Copy + Sized + Send + Sync + 'a { fn updates_iter( self, layer_ids: &'a LayerIds, - ) -> impl Iterator, TimeIndexRef<'a>)> + 'a { + ) -> impl Iterator< + Item = ( + usize, + storage::EdgeAdditions<'a>, + storage::EdgeAdditions<'a>, + ), + > + 'a { self.layer_ids_iter(layer_ids) .map(move |id| (id, self.additions(id), self.deletions(id))) } - fn additions(self, layer_id: usize) -> TimeIndexRef<'a>; + fn additions(self, layer_id: usize) -> storage::EdgeAdditions<'a>; - fn deletions(self, layer_id: usize) -> TimeIndexRef<'a>; + fn deletions(self, layer_id: usize) -> storage::EdgeAdditions<'a>; fn temporal_prop_layer(self, layer_id: usize, prop_id: usize) -> impl TPropOps<'a> + 'a; @@ -178,42 +182,30 @@ pub trait EdgeStorageOps<'a>: Copy + Sized + Send + Sync + 'a { } } -impl<'a> EdgeStorageOps<'a> for MemEdge<'a> { +impl<'a> EdgeStorageOps<'a> for storage::EdgeEntryRef<'a> { fn added(self, layer_ids: &LayerIds, w: Range) -> bool { - match layer_ids { - LayerIds::None => false, - LayerIds::All => self - .additions_iter(&LayerIds::All) - .any(|(_, t_index)| t_index.active_t(w.clone())), - LayerIds::One(l_id) => self - .get_additions(*l_id) - .filter(|a| a.active_t(w)) - .is_some(), - LayerIds::Multiple(layers) => layers - .iter() - .any(|l_id| self.added(&LayerIds::One(l_id), w.clone())), - } + EdgeRefOps::additions(self, layer_ids).active_t(w) } fn has_layer(self, layer_ids: &LayerIds) -> bool { match layer_ids { LayerIds::None => false, - LayerIds::All => true, - LayerIds::One(id) => self.has_layer_inner(*id), - LayerIds::Multiple(ids) => ids.iter().any(|id| self.has_layer_inner(id)), + LayerIds::All => self.edge(0).is_some(), + LayerIds::One(id) => self.edge(*id).is_some(), + LayerIds::Multiple(ids) => self.has_layers(ids), } } fn src(self) -> VID { - self.edge_store().src + EdgeRefOps::src(&self) } fn dst(self) -> VID { - self.edge_store().dst + EdgeRefOps::dst(&self) } fn eid(self) -> EID { - self.eid() + EdgeRefOps::edge_id(&self) } fn layer_ids_iter(self, layer_ids: &'a LayerIds) -> impl Iterator + 'a { @@ -231,23 +223,20 @@ impl<'a> EdgeStorageOps<'a> for MemEdge<'a> { } } - fn additions(self, layer_id: usize) -> TimeIndexRef<'a> { - TimeIndexRef::Ref(self.get_additions(layer_id).unwrap_or(&TimeIndex::Empty)) + fn additions(self, layer_id: usize) -> storage::EdgeAdditions<'a> { + EdgeRefOps::layer_additions(self, layer_id) } - fn deletions(self, layer_id: usize) -> TimeIndexRef<'a> { - TimeIndexRef::Ref(self.get_deletions(layer_id).unwrap_or(&TimeIndex::Empty)) + fn deletions(self, layer_id: usize) -> storage::EdgeAdditions<'a> { + EdgeRefOps::layer_additions(self, layer_id) } #[inline(always)] fn temporal_prop_layer(self, layer_id: usize, prop_id: usize) -> impl TPropOps<'a> + 'a { - self.props(layer_id) - .and_then(|props| props.temporal_prop(prop_id)) - .unwrap_or(&TProp::Empty) + EdgeRefOps::layer_t_prop(self, layer_id, prop_id) } fn constant_prop_layer(self, layer_id: usize, prop_id: usize) -> Option { - self.props(layer_id) - .and_then(|props| props.const_prop(prop_id).cloned()) + EdgeRefOps::c_prop(self, layer_id, prop_id) } } diff --git a/raphtory/Cargo.toml b/raphtory/Cargo.toml index a869b3f981..2be465d61c 100644 --- a/raphtory/Cargo.toml +++ b/raphtory/Cargo.toml @@ -18,6 +18,7 @@ homepage.workspace = true raphtory-api = { path = "../raphtory-api", version = "0.15.1" } raphtory-core = { path = "../raphtory-core", version = "0.15.1" } raphtory-storage = { path = "../raphtory-storage", version = "0.15.1" } +storage.workspace = true iter-enum = { workspace = true, features = ["rayon"] } arrow-array = { workspace = true, features = ["chrono-tz"] } arrow-schema = { workspace = true } diff --git a/raphtory/src/db/api/view/internal/time_semantics/filtered_edge.rs b/raphtory/src/db/api/view/internal/time_semantics/filtered_edge.rs index 9c7fa38084..c9f3ecd162 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/filtered_edge.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/filtered_edge.rs @@ -12,7 +12,7 @@ use raphtory_api::core::{ }; use raphtory_storage::graph::edges::{ edge_ref::EdgeStorageRef, - edge_storage_ops::{EdgeStorageOps, TimeIndexRef}, + edge_storage_ops::EdgeStorageOps, edges::EdgesStorage, }; use rayon::iter::ParallelIterator; @@ -21,7 +21,7 @@ use std::ops::Range; #[derive(Clone)] pub struct FilteredEdgeTimeIndex<'graph, G> { eid: ELID, - time_index: TimeIndexRef<'graph>, + time_index: storage::EdgeAdditions<'graph>, view: G, } From 0418d86e6c78b3ff9f5dfc6708c987b5f00a9119 Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Thu, 26 Jun 2025 17:32:02 +0100 Subject: [PATCH 039/321] add rows for noderefops --- db4-storage/src/api/nodes.rs | 57 +++++++++--- db4-storage/src/segments/node_entry.rs | 20 +++++ .../src/graph/nodes/node_entry.rs | 36 ++++---- .../src/graph/nodes/node_storage_ops.rs | 90 +++++++++++++------ raphtory/src/db/api/storage/storage.rs | 2 +- 5 files changed, 151 insertions(+), 54 deletions(-) diff --git a/db4-storage/src/api/nodes.rs b/db4-storage/src/api/nodes.rs index 97413edcfa..f681da2adf 100644 --- a/db4-storage/src/api/nodes.rs +++ b/db4-storage/src/api/nodes.rs @@ -1,6 +1,6 @@ use std::{ borrow::Cow, - ops::{Deref, DerefMut}, + ops::{Deref, DerefMut, Range}, path::Path, sync::Arc, }; @@ -28,7 +28,7 @@ use crate::{ LocalPOS, error::DBV4Error, segments::node::MemNodeSegment, - utils::{Iter3, Iter4}, + utils::{Iter2, Iter3, Iter4}, }; pub trait NodeSegmentOps: Send + Sync + std::fmt::Debug { @@ -143,7 +143,7 @@ pub trait NodeEntryOps<'a>: Send + Sync + 'a { } } -pub trait NodeRefOps<'a>: Copy + Clone + Send + Sync { +pub trait NodeRefOps<'a>: Copy + Clone + Send + Sync + 'a { type Additions: TimeIndexOps<'a>; type TProps: TPropOps<'a>; @@ -221,13 +221,42 @@ pub trait NodeRefOps<'a>: Copy + Clone + Send + Sync { fn temp_prop_rows( self, - ) -> impl Iterator< - Item = ( - TimeIndexEntry, - usize, - impl Iterator)>, - ), - > + 'a; + w: Option>, + ) -> impl Iterator)> + 'a { + (0..self.internal_num_layers()).flat_map(move |layer_id| { + let w = w.clone(); + let mut time_ordered_iter = (0..self.node_meta().temporal_prop_meta().len()) + .map(move |prop_id| { + self.temporal_prop_layer(layer_id, prop_id) + .iter_inner(w.clone()) + .map(move |(t, prop)| (t, (prop_id, prop))) + }) + .kmerge_by(|(t1, _), (t2, _)| t1 <= t2); + + if let Some((mut current_time, (prop_id, prop))) = time_ordered_iter.next() { + let mut current_row = vec![(prop_id, prop)]; + Iter2::I2(std::iter::from_fn(move || { + while let Some((t, (prop_id, prop))) = time_ordered_iter.next() { + if t == current_time { + current_row.push((prop_id, prop)); + } else { + let row = std::mem::take(&mut current_row); + current_time = t; + return Some((current_time, layer_id, row)); + } + } + if !current_row.is_empty() { + let row = std::mem::take(&mut current_row); + Some((current_time, layer_id, row)) + } else { + None + } + })) + } else { + Iter2::I1(std::iter::empty()) + } + }) + } fn out_nbrs(self, layer_id: usize) -> impl Iterator + 'a where @@ -259,12 +288,16 @@ pub trait NodeRefOps<'a>: Copy + Clone + Send + Sync { fn additions(self, layers: &'a LayerIds) -> Self::Additions; + fn layer_additions(self, layer_id: usize) -> Self::Additions; + fn c_prop(self, layer_id: usize, prop_id: usize) -> Option; fn c_prop_str(self, layer_id: usize, prop_id: usize) -> Option<&'a str>; fn t_prop(self, layer_id: &'a LayerIds, prop_id: usize) -> Self::TProps; + fn temporal_prop_layer(self, layer_id: usize, prop_id: usize) -> Self::TProps; + fn t_props(self, layer_id: &'a LayerIds) -> impl Iterator { (0..self.node_meta().temporal_prop_meta().len()) .map(move |prop_id| (prop_id, self.t_prop(layer_id, prop_id))) @@ -294,4 +327,8 @@ pub trait NodeRefOps<'a>: Copy + Clone + Send + Sync { .map(|id| id as usize) .expect("Node type should be present") } + + fn internal_num_layers(&self) -> usize; + + fn has_layer_inner(self, layer_id: usize) -> bool; } diff --git a/db4-storage/src/segments/node_entry.rs b/db4-storage/src/segments/node_entry.rs index 87992b920d..25ba76861f 100644 --- a/db4-storage/src/segments/node_entry.rs +++ b/db4-storage/src/segments/node_entry.rs @@ -200,4 +200,24 @@ impl<'a> NodeRefOps<'a> for MemNodeRef<'a> { let src_id = self.ns.to_vid(self.pos); eid.map(|eid| EdgeRef::new_outgoing(eid, src_id, dst)) } + + fn layer_additions(self, layer_id: usize) -> Self::Additions { + NodeAdditions::new_with_layer(self, layer_id) + } + + fn temporal_prop_layer(self, layer_id: usize, prop_id: usize) -> Self::TProps { + NodeTProps::new_with_layer(self, layer_id, prop_id) + } + + fn internal_num_layers(&self) -> usize { + self.ns.as_ref().len() + } + + fn has_layer_inner(self, layer_id: usize) -> bool { + self.ns + .as_ref() + .get(layer_id) + .and_then(|seg| seg.items().get(self.pos.0)) + .map_or(false, |x| *x) + } } diff --git a/raphtory-storage/src/graph/nodes/node_entry.rs b/raphtory-storage/src/graph/nodes/node_entry.rs index 17694e6e1d..e0e24ad3fb 100644 --- a/raphtory-storage/src/graph/nodes/node_entry.rs +++ b/raphtory-storage/src/graph/nodes/node_entry.rs @@ -84,25 +84,10 @@ impl<'a, 'b: 'a> NodeStorageOps<'a> for &'a NodeStorageEntry<'b> { self.as_ref().degree(layers, dir) } - fn additions(self, layer_ids: &'a LayerIds) -> storage::NodeAdditions<'a> { + fn additions(self, layer_ids: usize) -> storage::NodeAdditions<'a> { self.as_ref().additions(layer_ids) } - fn tprop(self, layer_ids: &'a LayerIds, prop_id: usize) -> storage::NodeTProps<'a> { - self.as_ref().tprop(layer_ids, prop_id) - } - - fn tprops( - self, - layer_ids: &'a LayerIds, - ) -> impl Iterator)> { - self.as_ref().tprops(layer_ids) - } - - fn prop(self, layer_id: usize, prop_id: usize) -> Option { - self.as_ref().prop(layer_id, prop_id) - } - fn edges_iter( self, layers: &LayerIds, @@ -126,4 +111,23 @@ impl<'a, 'b: 'a> NodeStorageOps<'a> for &'a NodeStorageEntry<'b> { fn find_edge(self, dst: VID, layer_ids: &LayerIds) -> Option { self.as_ref().find_edge(dst, layer_ids) } + + fn layer_ids_iter( + self, + layer_ids: &'a LayerIds, + ) -> impl Iterator + Send + Sync + 'a { + self.as_ref().layer_ids_iter(layer_ids) + } + + fn deletions(self, layer_id: usize) -> storage::NodeAdditions<'a> { + self.as_ref().deletions(layer_id) + } + + fn temporal_prop_layer(self, layer_id: usize, prop_id: usize) -> storage::NodeTProps<'a> { + self.as_ref().temporal_prop_layer(layer_id, prop_id) + } + + fn constant_prop_layer(self, layer_id: usize, prop_id: usize) -> Option { + self.as_ref().constant_prop_layer(layer_id, prop_id) + } } diff --git a/raphtory-storage/src/graph/nodes/node_storage_ops.rs b/raphtory-storage/src/graph/nodes/node_storage_ops.rs index acfd8e2aa8..124e98c7f8 100644 --- a/raphtory-storage/src/graph/nodes/node_storage_ops.rs +++ b/raphtory-storage/src/graph/nodes/node_storage_ops.rs @@ -2,23 +2,13 @@ use raphtory_api::core::{ entities::{edges::edge_ref::EdgeRef, properties::prop::Prop, GidRef, LayerIds, VID}, Direction, }; +use raphtory_core::entities::LayerVariants; use std::borrow::Cow; use storage::{api::nodes::NodeRefOps, NodeEntryRef}; -pub trait NodeStorageOps<'a>: Sized { +pub trait NodeStorageOps<'a>: Copy + Sized + Send + Sync + 'a { fn degree(self, layers: &LayerIds, dir: Direction) -> usize; - fn additions(self, layer_ids: &'a LayerIds) -> storage::NodeAdditions<'a>; - - fn tprop(self, layer_ids: &'a LayerIds, prop_id: usize) -> storage::NodeTProps<'a>; - - fn tprops( - self, - layer_ids: &'a LayerIds, - ) -> impl Iterator)>; - - fn prop(self, layer_id: usize, prop_id: usize) -> Option; - fn edges_iter( self, layers: &LayerIds, @@ -36,30 +26,42 @@ pub trait NodeStorageOps<'a>: Sized { } fn find_edge(self, dst: VID, layer_ids: &LayerIds) -> Option; -} -impl<'a> NodeStorageOps<'a> for NodeEntryRef<'a> { - fn degree(self, layers: &LayerIds, dir: Direction) -> usize { - NodeRefOps::degree(self, layers, dir) - } + fn layer_ids_iter( + self, + layer_ids: &'a LayerIds, + ) -> impl Iterator + Send + Sync + 'a; - fn additions(self, layer_ids: &'a LayerIds) -> storage::NodeAdditions<'a> { - NodeRefOps::additions(self, layer_ids) - } + fn additions(self, layer_id: usize) -> storage::NodeAdditions<'a>; - fn tprop(self, layer_ids: &'a LayerIds, prop_id: usize) -> storage::NodeTProps<'a> { - NodeRefOps::t_prop(self, layer_ids, prop_id) + fn deletions(self, layer_id: usize) -> storage::NodeAdditions<'a>; + + fn temporal_prop_layer(self, layer_id: usize, prop_id: usize) -> storage::NodeTProps<'a>; + + fn temporal_prop_iter( + self, + layer_ids: &'a LayerIds, + prop_id: usize, + ) -> impl Iterator)> + 'a { + self.layer_ids_iter(layer_ids) + .map(move |id| (id, self.temporal_prop_layer(id, prop_id))) } - fn tprops( + fn constant_prop_layer(self, layer_id: usize, prop_id: usize) -> Option; + + fn constant_prop_iter( self, layer_ids: &'a LayerIds, - ) -> impl Iterator)> { - NodeRefOps::t_props(self, layer_ids) + prop_id: usize, + ) -> impl Iterator + 'a { + self.layer_ids_iter(layer_ids) + .filter_map(move |id| Some((id, self.constant_prop_layer(id, prop_id)?))) } +} - fn prop(self, layer_id: usize, prop_id: usize) -> Option { - NodeRefOps::c_prop(self, layer_id, prop_id) +impl<'a> NodeStorageOps<'a> for NodeEntryRef<'a> { + fn degree(self, layers: &LayerIds, dir: Direction) -> usize { + NodeRefOps::degree(self, layers, dir) } fn edges_iter( @@ -85,4 +87,38 @@ impl<'a> NodeStorageOps<'a> for NodeEntryRef<'a> { fn find_edge(self, dst: VID, layer_ids: &LayerIds) -> Option { NodeRefOps::find_edge(&self, dst, layer_ids) } + + fn layer_ids_iter( + self, + layer_ids: &'a LayerIds, + ) -> impl Iterator + Send + Sync + 'a { + match layer_ids { + LayerIds::None => LayerVariants::None(std::iter::empty()), + LayerIds::All => LayerVariants::All( + (0..self.internal_num_layers()).filter(move |&l| self.has_layer_inner(l)), + ), + LayerIds::One(id) => { + LayerVariants::One(self.has_layer_inner(*id).then_some(*id).into_iter()) + } + LayerIds::Multiple(ids) => { + LayerVariants::Multiple(ids.iter().filter(move |&id| self.has_layer_inner(id))) + } + } + } + + fn deletions(self, layer_id: usize) -> storage::NodeAdditions<'a> { + NodeRefOps::layer_additions(self, layer_id) + } + + fn additions(self, layer_ids: usize) -> storage::NodeAdditions<'a> { + NodeRefOps::layer_additions(self, layer_ids) + } + + fn temporal_prop_layer(self, layer_id: usize, prop_id: usize) -> storage::NodeTProps<'a> { + NodeRefOps::temporal_prop_layer(self, layer_id, prop_id) + } + + fn constant_prop_layer(self, layer_id: usize, prop_id: usize) -> Option { + NodeRefOps::c_prop(self, layer_id, prop_id) + } } diff --git a/raphtory/src/db/api/storage/storage.rs b/raphtory/src/db/api/storage/storage.rs index 34b1cecf59..c3385e6617 100644 --- a/raphtory/src/db/api/storage/storage.rs +++ b/raphtory/src/db/api/storage/storage.rs @@ -223,7 +223,7 @@ impl AtomicAdditionOps for StorageWriteSession<'_> { todo!() } - fn store_node_id_as_prop(&self, id: NodeRef, vid: impl Into) { + fn store_node_id_as_prop(&mut self, id: NodeRef, vid: impl Into) { todo!("set_node_id is not implemented for StorageWriteSession"); } } From 8bd9f8a1d38d3bc343e71fd2bb0742697df6da00 Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Fri, 27 Jun 2025 16:09:00 +0100 Subject: [PATCH 040/321] add back WriteLockedGraph --- db4-graph/src/lib.rs | 75 ++++++++++++-- db4-storage/src/lib.rs | 1 + db4-storage/src/pages/edge_page/writer.rs | 19 +++- db4-storage/src/pages/edge_store.rs | 34 +++---- db4-storage/src/pages/locked/nodes.rs | 6 ++ db4-storage/src/pages/mod.rs | 15 +++ db4-storage/src/pages/node_page/writer.rs | 22 ++++- db4-storage/src/pages/node_store.rs | 42 ++++---- db4-storage/src/properties/mod.rs | 38 ++++--- db4-storage/src/segments/edge.rs | 21 ++++ db4-storage/src/segments/node.rs | 2 +- db4-storage/src/segments/node_entry.rs | 21 +--- .../entities/properties/prop/prop_enum.rs | 11 ++- raphtory-storage/src/core_ops.rs | 3 +- raphtory-storage/src/graph/locked.rs | 46 +-------- .../src/graph/nodes/node_entry.rs | 10 ++ .../src/graph/nodes/node_storage_ops.rs | 21 +++- raphtory-storage/src/mutation/addition_ops.rs | 15 +-- .../src/mutation/addition_ops_ext.rs | 37 ++++--- raphtory/Cargo.toml | 1 + .../time_semantics/event_semantics.rs | 2 +- .../time_semantics/persistent_semantics.rs | 2 +- raphtory/src/io/arrow/df_loaders.rs | 99 +++++-------------- raphtory/src/serialise/serialise.rs | 2 +- 24 files changed, 317 insertions(+), 228 deletions(-) diff --git a/db4-graph/src/lib.rs b/db4-graph/src/lib.rs index c197863641..1a29c0ac81 100644 --- a/db4-graph/src/lib.rs +++ b/db4-graph/src/lib.rs @@ -8,25 +8,27 @@ use std::{ // use crate::entries::node::UnlockedNodeEntry; use raphtory_api::core::{ - entities::{ - self, - properties::{meta::Meta, prop::Prop}, - }, + entities::{self, properties::meta::Meta}, input::input_node::InputNode, storage::dict_mapper::MaybeNew, }; use raphtory_core::{ entities::{ - graph::{logical_to_physical::Mapping, tgraph::InvalidLayer}, + graph::{ + logical_to_physical::{InvalidNodeId, Mapping}, + tgraph::InvalidLayer, + timer::{MinCounter, TimeCounterTrait}, + }, nodes::node_ref::NodeRef, properties::graph_meta::GraphMeta, - GidRef, LayerIds, EID, VID, + GidRef, LayerIds, VID, }, - storage::timeindex::TimeIndexEntry, + storage::timeindex::{AsTime, TimeIndexEntry}, }; use storage::{ - error::DBV4Error, persist::strategy::PersistentStrategy, Extension, Layer, ReadLockedLayer, ES, - NS, + pages::locked::{edges::WriteLockedEdgePages, nodes::WriteLockedNodePages}, + persist::strategy::PersistentStrategy, + Extension, Layer, ReadLockedLayer, ES, NS, }; pub mod entries; @@ -49,6 +51,9 @@ pub struct TemporalGraph { event_counter: AtomicUsize, graph_meta: Arc, + + earliest: MinCounter, + latest: MinCounter, } impl, ES = ES>> TemporalGraph { @@ -195,4 +200,56 @@ impl, ES = ES>> TemporalGraph { } } } + + pub fn write_locked_graph<'a>(&'a self) -> WriteLockedGraph<'a, EXT> { + WriteLockedGraph::new(self) + } +} + +pub struct WriteLockedGraph<'a, EXT> { + pub nodes: WriteLockedNodePages<'a, storage::NS>, + pub edges: WriteLockedEdgePages<'a, storage::ES>, + pub graph: &'a TemporalGraph, + pub num_nodes: Arc, +} + +impl<'a, EXT: PersistentStrategy, ES = ES>> WriteLockedGraph<'a, EXT> { + pub fn new(graph: &'a TemporalGraph) -> Self { + WriteLockedGraph { + nodes: graph.storage.nodes().write_locked().into(), + edges: graph.storage.edges().write_locked().into(), + graph, + num_nodes: Arc::new(AtomicUsize::new(graph.internal_num_nodes())), + } + } + + pub fn resolve_node(&self, gid: GidRef) -> Result, InvalidNodeId> { + self.graph.logical_to_physical.get_or_init_vid(gid, || { + VID(self.num_nodes.fetch_add(1, atomic::Ordering::Relaxed)) + }) + } + + pub fn num_nodes(&self) -> usize { + self.num_nodes.load(atomic::Ordering::Relaxed) + } + + pub fn resolve_node_type(&self, node_type: Option<&str>) -> MaybeNew { + node_type + .map(|node_type| self.graph.node_meta.get_or_create_node_type_id(node_type)) + .unwrap_or_else(|| MaybeNew::Existing(0)) + } + + pub fn resize_chunks_to_num_nodes(&mut self) { + let num_nodes = self.num_nodes(); + self.graph.storage().nodes().grow_to_num_nodes(num_nodes); + std::mem::take(&mut self.nodes); + self.nodes = self.graph.storage.nodes().write_locked().into(); + } + + #[inline] + pub fn update_time(&self, time: TimeIndexEntry) { + let t = time.t(); + self.graph.earliest.update(t); + self.graph.latest.update(t); + } } diff --git a/db4-storage/src/lib.rs b/db4-storage/src/lib.rs index fd68a7bc87..72fc69018b 100644 --- a/db4-storage/src/lib.rs +++ b/db4-storage/src/lib.rs @@ -25,6 +25,7 @@ pub mod persist; pub mod properties; pub mod segments; pub mod utils; +// pub mod loaders; pub type Extension = (); pub type NS

= NodeSegmentView

; diff --git a/db4-storage/src/pages/edge_page/writer.rs b/db4-storage/src/pages/edge_page/writer.rs index 536cbc96cf..0e0dcf6d09 100644 --- a/db4-storage/src/pages/edge_page/writer.rs +++ b/db4-storage/src/pages/edge_page/writer.rs @@ -1,6 +1,9 @@ use std::ops::DerefMut; -use crate::{api::edges::EdgeSegmentOps, pages::layer_counter::LayerCounter, segments::edge::MemEdgeSegment, LocalPOS}; +use crate::{ + LocalPOS, api::edges::EdgeSegmentOps, pages::layer_counter::LayerCounter, + segments::edge::MemEdgeSegment, +}; use raphtory_api::core::entities::{VID, properties::prop::Prop}; use raphtory_core::storage::timeindex::AsTime; @@ -48,6 +51,20 @@ impl<'a, MP: DerefMut, ES: EdgeSegmentOps> EdgeWriter<' edge_pos } + pub fn delete_edge( + &mut self, + t: T, + edge_pos: LocalPOS, + src: impl Into, + dst: impl Into, + layer_id: usize, + lsn: u64, + ) { + self.writer.as_mut()[layer_id].set_lsn(lsn); + self.writer + .delete_edge_internal(t, edge_pos, src, dst, layer_id); + } + pub fn add_static_edge( &mut self, edge_pos: Option, diff --git a/db4-storage/src/pages/edge_store.rs b/db4-storage/src/pages/edge_store.rs index 41af00b54a..43a6847815 100644 --- a/db4-storage/src/pages/edge_store.rs +++ b/db4-storage/src/pages/edge_store.rs @@ -9,7 +9,7 @@ use std::{ use super::{edge_page::writer::EdgeWriter, resolve_pos}; use crate::{ - api::edges::{EdgeSegmentOps, LockedESegment}, error::DBV4Error, pages::layer_counter::LayerCounter, segments::edge::MemEdgeSegment, LocalPOS + api::edges::{EdgeSegmentOps, LockedESegment}, error::DBV4Error, pages::{layer_counter::LayerCounter, locked::edges::{LockedEdgePage, WriteLockedEdgePages}}, segments::edge::MemEdgeSegment, LocalPOS }; use parking_lot::{RwLock, RwLockWriteGuard}; use raphtory_api::core::entities::{EID, VID, properties::meta::Meta}; @@ -307,22 +307,22 @@ impl, EXT: Clone + Send + Sync> EdgeStorageI self.max_page_len } - // pub fn locked<'a>(&'a self) -> WriteLockedEdgePages<'a, ES> { - // WriteLockedEdgePages::new( - // self.pages - // .iter() - // .map(|(page_id, page)| { - // LockedEdgePage::new( - // page_id, - // self.max_page_len, - // page.as_ref(), - // &self.num_edges, - // page.head_mut(), - // ) - // }) - // .collect(), - // ) - // } + pub fn write_locked<'a>(&'a self) -> WriteLockedEdgePages<'a, ES> { + WriteLockedEdgePages::new( + self.pages + .iter() + .map(|(page_id, page)| { + LockedEdgePage::new( + page_id, + self.max_page_len, + page.as_ref(), + &self.layer_counter, + page.head_mut(), + ) + }) + .collect(), + ) + } pub fn get_edge(&self, e_id: ELID) -> Option<(VID, VID)> { let layer = e_id.layer(); diff --git a/db4-storage/src/pages/locked/nodes.rs b/db4-storage/src/pages/locked/nodes.rs index 259a769852..432e95dde6 100644 --- a/db4-storage/src/pages/locked/nodes.rs +++ b/db4-storage/src/pages/locked/nodes.rs @@ -58,6 +58,12 @@ pub struct WriteLockedNodePages<'a, NS> { writers: Vec>, } +impl <'a, NS> Default for WriteLockedNodePages<'_, NS> { + fn default() -> Self { + Self { writers: Vec::new() } + } +} + impl<'a, EXT, NS: NodeSegmentOps> WriteLockedNodePages<'a, NS> { pub fn new(writers: Vec>) -> Self { Self { writers } diff --git a/db4-storage/src/pages/mod.rs b/db4-storage/src/pages/mod.rs index 1c1144eb67..6b35b532f4 100644 --- a/db4-storage/src/pages/mod.rs +++ b/db4-storage/src/pages/mod.rs @@ -226,6 +226,21 @@ impl< Ok(elid) } + fn internal_delete_edge( + &self, + t: TimeIndexEntry, + src: impl Into, + dst: impl Into, + lsn: u64, + ) -> Result<(), DBV4Error> { + let src = src.into(); + let dst = dst.into(); + let mut session = self.write_session(src, dst, None); + todo!("Implement internal_delete_edge"); + // session.internal_delete_edge(t, src, dst, lsn, 0)?; + Ok(()) + } + fn as_time_index_entry(&self, t: T) -> Result { let input_time = t.try_into_input_time()?; let t = match input_time { diff --git a/db4-storage/src/pages/node_page/writer.rs b/db4-storage/src/pages/node_page/writer.rs index 6335b28f2a..e48385d2fa 100644 --- a/db4-storage/src/pages/node_page/writer.rs +++ b/db4-storage/src/pages/node_page/writer.rs @@ -3,7 +3,10 @@ use crate::{ segments::node::MemNodeSegment, }; use raphtory_api::core::entities::{EID, VID, properties::prop::Prop}; -use raphtory_core::{entities::ELID, storage::timeindex::AsTime}; +use raphtory_core::{ + entities::{ELID, GidRef}, + storage::timeindex::AsTime, +}; use std::ops::DerefMut; #[derive(Debug)] @@ -128,7 +131,6 @@ impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> NodeWri let layer_id = e_id.layer(); self.writer.as_mut()[layer_id].set_lsn(lsn); self.writer.update_timestamp(t, pos, e_id); - // self.est_size = self.page.increment_size(size_of::<(i64, i64)>()); } pub fn get_out_edge(&self, pos: LocalPOS, dst: VID, layer_id: usize) -> Option { @@ -140,6 +142,22 @@ impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> NodeWri self.page .get_inb_edge(pos, src, layer_id, self.writer.deref()) } + + pub fn store_node_id_and_node_type( + &mut self, + pos: LocalPOS, + layer_id: usize, + gid: GidRef<'_>, + node_type: usize, + lsn: u64, + ) { + self.update_c_props( + pos, + layer_id, + [(0, Prop::U64(node_type as u64)), (1, gid.into())], + lsn, + ); + } } impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> Drop diff --git a/db4-storage/src/pages/node_store.rs b/db4-storage/src/pages/node_store.rs index 3ba05c7789..947711b2b4 100644 --- a/db4-storage/src/pages/node_store.rs +++ b/db4-storage/src/pages/node_store.rs @@ -3,7 +3,10 @@ use crate::{ LocalPOS, api::nodes::{LockedNSSegment, NodeSegmentOps}, error::DBV4Error, - pages::layer_counter::LayerCounter, + pages::{ + layer_counter::LayerCounter, + locked::nodes::{LockedNodePage, WriteLockedNodePages}, + }, segments::node::MemNodeSegment, }; use parking_lot::RwLockWriteGuard; @@ -99,22 +102,22 @@ impl, EXT: Clone> NodeStorageInner &self.prop_meta } - // pub fn locked<'a>(&'a self) -> WriteLockedNodePages<'a, NS> { - // WriteLockedNodePages::new( - // self.pages - // .iter() - // .map(|(page_id, page)| { - // LockedNodePage::new( - // page_id, - // &self.num_nodes, - // self.max_page_len, - // page.as_ref(), - // page.head_mut(), - // ) - // }) - // .collect(), - // ) - // } + pub fn write_locked<'a>(&'a self) -> WriteLockedNodePages<'a, NS> { + WriteLockedNodePages::new( + self.pages + .iter() + .map(|(page_id, page)| { + LockedNodePage::new( + page_id, + &self.layer_counter, + self.max_page_len, + page.as_ref(), + page.head_mut(), + ) + }) + .collect(), + ) + } pub fn num_layers(&self) -> usize { self.layer_counter.len() @@ -252,6 +255,11 @@ impl, EXT: Clone> NodeStorageInner self.get_or_create_segment(new_len - 1); } + pub fn grow_to_num_nodes(&self, num_nodes: usize) { + let chunks_needed = (num_nodes + self.max_page_len - 1) / self.max_page_len; + self.grow(chunks_needed); + } + pub fn get_or_create_segment(&self, segment_id: usize) -> &Arc { if let Some(segment) = self.pages.get(segment_id) { return segment; diff --git a/db4-storage/src/properties/mod.rs b/db4-storage/src/properties/mod.rs index 6a3e161d3b..197be92932 100644 --- a/db4-storage/src/properties/mod.rs +++ b/db4-storage/src/properties/mod.rs @@ -13,12 +13,17 @@ use raphtory_core::{ storage::{PropColumn, TColumns, timeindex::TimeIndexEntry}, }; +use crate::segments::edge; + pub mod props_meta_writer; #[derive(Debug, Default)] pub struct Properties { c_properties: Vec, - t_index: Vec, + + additions: Vec, + deletions: Vec, + t_properties: TColumns, earliest: Option, latest: Option, @@ -81,7 +86,7 @@ impl Properties { } pub(crate) fn temporal_index(&self, row: usize) -> Option<&PropTimestamps> { - self.t_index.get(row) + self.additions.get(row) } pub fn has_node_properties(&self) -> bool { @@ -270,32 +275,43 @@ impl<'a> PropMutEntry<'a> { row }; - if self.properties.t_index.len() <= self.row { + if self.properties.additions.len() <= self.row { self.properties - .t_index + .additions .resize_with(self.row + 1, Default::default); } - let prop_timestamps = &mut self.properties.t_index[self.row]; + let prop_timestamps = &mut self.properties.additions[self.row]; prop_timestamps.props_ts.set(t, Some(t_prop_row)); self.properties.has_node_properties = true; self.properties.update_earliest_latest(t); } - pub(crate) fn append_edge_ts(&mut self, t: TimeIndexEntry, edge_id: ELID) { - if self.properties.t_index.len() <= self.row { + pub(crate) fn addition_timestamp(&mut self, t: TimeIndexEntry, edge_id: ELID) { + if self.properties.additions.len() <= self.row { self.properties - .t_index + .additions .resize_with(self.row + 1, Default::default); } self.properties.has_node_additions = true; - let prop_timestamps = &mut self.properties.t_index[self.row]; + let prop_timestamps = &mut self.properties.additions[self.row]; prop_timestamps.edge_ts.set(t, edge_id); self.properties.update_earliest_latest(t); } + pub(crate) fn deletion_timestamp(&mut self, t:TimeIndexEntry, edge_id: Option) { + if self.properties.deletions.len() <= self.row { + self.properties + .deletions + .resize_with(self.row + 1, Default::default); + } + + let prop_timestamps = &mut self.properties.deletions[self.row]; + prop_timestamps.edge_ts.set(t, edge_id.unwrap_or_default()); + } + pub(crate) fn append_const_props(&mut self, props: impl IntoIterator) { for (prop_id, prop) in props { if self.properties.c_properties.len() <= prop_id { @@ -311,7 +327,7 @@ impl<'a> PropMutEntry<'a> { impl<'a> PropEntry<'a> { pub fn timestamps(self) -> Option<&'a PropTimestamps> { - self.properties.t_index.get(self.row) + self.properties.additions.get(self.row) } pub(crate) fn prop(self, prop_id: usize) -> Option> { @@ -321,7 +337,7 @@ impl<'a> PropEntry<'a> { pub fn t_cell(self) -> &'a TCell> { self.properties - .t_index + .additions .get(self.row) .map_or(&TCell::Empty, |ts| &ts.props_ts) } diff --git a/db4-storage/src/segments/edge.rs b/db4-storage/src/segments/edge.rs index a5667fe4c6..36d11f87d3 100644 --- a/db4-storage/src/segments/edge.rs +++ b/db4-storage/src/segments/edge.rs @@ -145,6 +145,27 @@ impl MemEdgeSegment { prop_entry.append_t_props(ts, props) } + pub fn delete_edge_internal( + &mut self, + t: T, + edge_pos: impl Into, + src: impl Into, + dst: impl Into, + layer_id: usize, + ) { + let edge_pos = edge_pos.into(); + let src = src.into(); + let dst = dst.into(); + let t = TimeIndexEntry::new(t.t(), t.i()); + + // Ensure we have enough layers + self.ensure_layer(layer_id); + + let local_row = self.reserve_local_row(edge_pos, src, dst, layer_id); + let props = self.layers[layer_id].properties_mut(); + props.get_mut_entry(local_row).deletion_timestamp(t, None); + } + pub fn insert_static_edge_internal( &mut self, edge_pos: LocalPOS, diff --git a/db4-storage/src/segments/node.rs b/db4-storage/src/segments/node.rs index 590f94666b..09c3cfd7e9 100644 --- a/db4-storage/src/segments/node.rs +++ b/db4-storage/src/segments/node.rs @@ -241,7 +241,7 @@ impl MemNodeSegment { .get_mut_entry(row); let ts = TimeIndexEntry::new(t.t(), t.i()); - prop_mut_entry.append_edge_ts(ts, e_id); + prop_mut_entry.addition_timestamp(ts, e_id); } pub fn update_timestamp(&mut self, t: T, node_pos: LocalPOS, e_id: ELID) { diff --git a/db4-storage/src/segments/node_entry.rs b/db4-storage/src/segments/node_entry.rs index 25ba76861f..64c72efce4 100644 --- a/db4-storage/src/segments/node_entry.rs +++ b/db4-storage/src/segments/node_entry.rs @@ -16,7 +16,7 @@ use raphtory_core::{ entities::{LayerIds, edges::edge_ref::EdgeRef, properties::tprop::TPropCell}, storage::timeindex::{TimeIndexEntry, TimeIndexOps}, }; -use std::{iter::Empty, ops::Deref, sync::Arc}; +use std::{ops::Deref, sync::Arc}; use super::additions::MemAdditions; @@ -159,25 +159,6 @@ impl<'a> NodeRefOps<'a> for MemNodeRef<'a> { NodeTProps::new(self, layer_id, prop_id) } - fn temp_prop_rows( - self, - ) -> impl Iterator< - Item = ( - TimeIndexEntry, - usize, - impl Iterator)>, - ), - > + 'a { - // self.ns.as_ref().iter().enumerate().flat_map(|(layer_id, layer)| { - // let rows = layer.t_prop_rows(self.pos); - // }).flat_map(|t_prop| { - // t_prop. - // }) - - //TODO - std::iter::empty::<(_, _, Empty<_>)>() - } - fn degree(self, layers: &LayerIds, dir: Direction) -> usize { match layers { LayerIds::One(layer_id) => self.ns.degree(self.pos, *layer_id, dir), diff --git a/raphtory-api/src/core/entities/properties/prop/prop_enum.rs b/raphtory-api/src/core/entities/properties/prop/prop_enum.rs index e2505ecf68..a90ed51c6d 100644 --- a/raphtory-api/src/core/entities/properties/prop/prop_enum.rs +++ b/raphtory-api/src/core/entities/properties/prop/prop_enum.rs @@ -1,5 +1,5 @@ use crate::core::{ - entities::properties::prop::{prop_ref_enum::PropRef, PropType}, + entities::{properties::prop::{prop_ref_enum::PropRef, PropType}, GidRef}, storage::arc_str::ArcStr, }; use bigdecimal::{num_bigint::BigInt, BigDecimal}; @@ -49,6 +49,15 @@ pub enum Prop { Decimal(BigDecimal), } +impl From> for Prop { + fn from(value: GidRef<'_>) -> Self { + match value { + GidRef::U64(n) => Prop::U64(n), + GidRef::Str(s) => Prop::str(s), + } + } +} + impl<'a> From> for Prop { fn from(prop_ref: PropRef<'a>) -> Self { match prop_ref { diff --git a/raphtory-storage/src/core_ops.rs b/raphtory-storage/src/core_ops.rs index 437a69f3f8..cd10647d7e 100644 --- a/raphtory-storage/src/core_ops.rs +++ b/raphtory-storage/src/core_ops.rs @@ -229,7 +229,8 @@ pub trait CoreGraphOps: Send + Sync { fn constant_node_prop(&self, v: VID, id: usize) -> Option { let core_node_entry = self.core_node(v); // TODO: figure out how to expose the layer_id to the calling API - core_node_entry.prop(0, id) + // core_node_entry.prop(0, id) + None } /// Gets the keys of constant properties of a given node diff --git a/raphtory-storage/src/graph/locked.rs b/raphtory-storage/src/graph/locked.rs index af80094d38..35ce06079b 100644 --- a/raphtory-storage/src/graph/locked.rs +++ b/raphtory-storage/src/graph/locked.rs @@ -8,7 +8,7 @@ use raphtory_core::{ storage::{raw_edges::WriteLockedEdges, WriteLockedNodes}, }; use std::sync::Arc; -use storage::{Extension, ReadLockedEdges, ReadLockedNodes}; +use storage::{pages::locked::{edges::WriteLockedEdgePages, nodes::WriteLockedNodePages}, Extension, ReadLockedEdges, ReadLockedNodes}; #[derive(Debug)] pub struct LockedGraph { @@ -57,47 +57,3 @@ impl Clone for LockedGraph { } } -pub struct WriteLockedGraph<'a> { - pub nodes: WriteLockedNodes<'a>, - pub edges: WriteLockedEdges<'a>, - pub graph: &'a TemporalGraph, -} - -// impl<'a> WriteLockedGraph<'a> { -// pub(crate) fn new(graph: &'a TemporalGraph) -> Self { -// let nodes = graph.storage.nodes.write_lock(); -// let edges = graph.storage.edges.write_lock(); -// Self { -// nodes, -// edges, -// graph, -// } -// } - -// pub fn num_nodes(&self) -> usize { -// self.graph.storage.nodes.len() -// } -// pub fn resolve_node(&self, gid: GidRef) -> Result, InvalidNodeId> { -// self.graph -// .logical_to_physical -// .get_or_init(gid, || self.graph.storage.nodes.next_id()) -// } - -// pub fn resolve_node_type(&self, node_type: Option<&str>) -> MaybeNew { -// node_type -// .map(|node_type| self.graph.node_meta.get_or_create_node_type_id(node_type)) -// .unwrap_or_else(|| MaybeNew::Existing(0)) -// } - -// pub fn num_shards(&self) -> usize { -// self.nodes.num_shards().max(self.edges.num_shards()) -// } - -// pub fn edges_mut(&mut self) -> &mut WriteLockedEdges<'a> { -// &mut self.edges -// } - -// pub fn graph(&self) -> &TemporalGraph { -// self.graph -// } -// } diff --git a/raphtory-storage/src/graph/nodes/node_entry.rs b/raphtory-storage/src/graph/nodes/node_entry.rs index e0e24ad3fb..709e024fcd 100644 --- a/raphtory-storage/src/graph/nodes/node_entry.rs +++ b/raphtory-storage/src/graph/nodes/node_entry.rs @@ -1,8 +1,11 @@ +use std::ops::Range; + use crate::graph::nodes::{node_ref::NodeStorageRef, node_storage_ops::NodeStorageOps}; use raphtory_api::core::{ entities::{edges::edge_ref::EdgeRef, properties::prop::Prop, GidRef, LayerIds, VID}, Direction, }; +use raphtory_core::storage::timeindex::TimeIndexEntry; use storage::{ api::nodes::{self, NodeEntryOps}, utils::Iter2, @@ -130,4 +133,11 @@ impl<'a, 'b: 'a> NodeStorageOps<'a> for &'a NodeStorageEntry<'b> { fn constant_prop_layer(self, layer_id: usize, prop_id: usize) -> Option { self.as_ref().constant_prop_layer(layer_id, prop_id) } + + fn temp_prop_rows_range( + self, + w: Option>, + ) -> impl Iterator)> { + self.as_ref().temp_prop_rows_range(w) + } } diff --git a/raphtory-storage/src/graph/nodes/node_storage_ops.rs b/raphtory-storage/src/graph/nodes/node_storage_ops.rs index 124e98c7f8..4f4ea92509 100644 --- a/raphtory-storage/src/graph/nodes/node_storage_ops.rs +++ b/raphtory-storage/src/graph/nodes/node_storage_ops.rs @@ -2,8 +2,8 @@ use raphtory_api::core::{ entities::{edges::edge_ref::EdgeRef, properties::prop::Prop, GidRef, LayerIds, VID}, Direction, }; -use raphtory_core::entities::LayerVariants; -use std::borrow::Cow; +use raphtory_core::{entities::LayerVariants, storage::timeindex::TimeIndexEntry}; +use std::{borrow::Cow, ops::Range}; use storage::{api::nodes::NodeRefOps, NodeEntryRef}; pub trait NodeStorageOps<'a>: Copy + Sized + Send + Sync + 'a { @@ -57,6 +57,15 @@ pub trait NodeStorageOps<'a>: Copy + Sized + Send + Sync + 'a { self.layer_ids_iter(layer_ids) .filter_map(move |id| Some((id, self.constant_prop_layer(id, prop_id)?))) } + + fn temp_prop_rows_range( + self, + w: Option>, + ) -> impl Iterator)>; + + fn temp_prop_rows(self) -> impl Iterator)> { + self.temp_prop_rows_range(None) + } } impl<'a> NodeStorageOps<'a> for NodeEntryRef<'a> { @@ -121,4 +130,12 @@ impl<'a> NodeStorageOps<'a> for NodeEntryRef<'a> { fn constant_prop_layer(self, layer_id: usize, prop_id: usize) -> Option { NodeRefOps::c_prop(self, layer_id, prop_id) } + + fn temp_prop_rows_range( + self, + w: Option>, + ) -> impl Iterator)> { + NodeRefOps::temp_prop_rows(self, w) + } + } diff --git a/raphtory-storage/src/mutation/addition_ops.rs b/raphtory-storage/src/mutation/addition_ops.rs index 5a6de24fe4..512a2573bc 100644 --- a/raphtory-storage/src/mutation/addition_ops.rs +++ b/raphtory-storage/src/mutation/addition_ops.rs @@ -1,7 +1,8 @@ use crate::{ - graph::{graph::GraphStorage, locked::WriteLockedGraph}, + graph::{graph::GraphStorage}, mutation::MutationError, }; +use db4_graph::WriteLockedGraph; use raphtory_api::{ core::{ entities::{ @@ -17,6 +18,7 @@ use raphtory_core::{ storage::{raw_edges::WriteLockedEdges, WriteLockedNodes}, }; use std::sync::atomic::Ordering; +use storage::Extension; pub trait InternalAdditionOps { type Error: From; @@ -28,7 +30,9 @@ pub trait InternalAdditionOps { where Self: 'a; - fn write_lock(&self) -> Result; + fn write_lock( + &self, + ) -> Result, Self::Error>; fn write_lock_nodes(&self) -> Result; fn write_lock_edges(&self) -> Result; /// map layer name to id and allocate a new layer if needed @@ -284,7 +288,7 @@ impl InternalAdditionOps for GraphStorage { type WS<'b> = TGWriteSession<'b>; type AtomicAddEdge<'a> = TGWriteSession<'a>; - fn write_lock(&self) -> Result { + fn write_lock(&self) -> Result, Self::Error>{ self.mutable()?.write_lock() } @@ -355,11 +359,10 @@ impl InternalAdditionOps for TemporalGraph { type WS<'b> = TGWriteSession<'b>; type AtomicAddEdge<'a> = TGWriteSession<'a>; - fn write_lock(&self) -> Result { + fn write_lock(&self) -> Result, Self::Error> { // Ok(WriteLockedGraph::new(self)) todo!("remove this once we have a mutable graph storage"); } - fn write_lock_nodes(&self) -> Result { Ok(self.storage.nodes.write_lock()) } @@ -466,7 +469,7 @@ where G: 'a; #[inline] - fn write_lock(&self) -> Result { + fn write_lock(&self) -> Result, Self::Error> { self.base().write_lock() } diff --git a/raphtory-storage/src/mutation/addition_ops_ext.rs b/raphtory-storage/src/mutation/addition_ops_ext.rs index ab535a97ea..01df8596c4 100644 --- a/raphtory-storage/src/mutation/addition_ops_ext.rs +++ b/raphtory-storage/src/mutation/addition_ops_ext.rs @@ -1,6 +1,6 @@ use std::ops::DerefMut; -use db4_graph::TemporalGraph; +use db4_graph::{TemporalGraph, WriteLockedGraph}; use parking_lot::RwLockWriteGuard; use raphtory_api::core::{ entities::properties::{ @@ -18,15 +18,12 @@ use storage::{ persist::strategy::PersistentStrategy, properties::props_meta_writer::PropsMetaWriter, segments::{edge::MemEdgeSegment, node::MemNodeSegment}, - Layer, ES, NS, + Extension, ES, NS, }; -use crate::{ - graph::locked::WriteLockedGraph, - mutation::{ - addition_ops::{AtomicAdditionOps, InternalAdditionOps, SessionAdditionOps}, - MutationError, - }, +use crate::mutation::{ + addition_ops::{AtomicAdditionOps, InternalAdditionOps, SessionAdditionOps}, + MutationError, }; pub struct WriteS< @@ -158,21 +155,23 @@ impl<'a, EXT: Send + Sync> SessionAdditionOps for UnlockedSession<'a, EXT> { } } -impl, ES = ES>> InternalAdditionOps - for TemporalGraph -{ +impl InternalAdditionOps for TemporalGraph { type Error = MutationError; - type WS<'a> - = UnlockedSession<'a, EXT> - where - EXT: 'a; + type WS<'a> = UnlockedSession<'a, Extension>; - type AtomicAddEdge<'a> = - WriteS<'a, RwLockWriteGuard<'a, MemNodeSegment>, RwLockWriteGuard<'a, MemEdgeSegment>, EXT>; + type AtomicAddEdge<'a> = WriteS< + 'a, + RwLockWriteGuard<'a, MemNodeSegment>, + RwLockWriteGuard<'a, MemEdgeSegment>, + Extension, + >; - fn write_lock(&self) -> Result { - todo!() + fn write_lock( + &self, + ) -> Result, Self::Error> { + let locked_g = self.write_locked_graph(); + Ok(locked_g) } fn write_lock_nodes(&self) -> Result { diff --git a/raphtory/Cargo.toml b/raphtory/Cargo.toml index 2be465d61c..a3362e0c8b 100644 --- a/raphtory/Cargo.toml +++ b/raphtory/Cargo.toml @@ -18,6 +18,7 @@ homepage.workspace = true raphtory-api = { path = "../raphtory-api", version = "0.15.1" } raphtory-core = { path = "../raphtory-core", version = "0.15.1" } raphtory-storage = { path = "../raphtory-storage", version = "0.15.1" } +db4-graph.workspace = true storage.workspace = true iter-enum = { workspace = true, features = ["rayon"] } arrow-array = { workspace = true, features = ["chrono-tz"] } diff --git a/raphtory/src/db/api/view/internal/time_semantics/event_semantics.rs b/raphtory/src/db/api/view/internal/time_semantics/event_semantics.rs index 9a9fa238db..6feaf5f43d 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/event_semantics.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/event_semantics.rs @@ -96,7 +96,7 @@ impl NodeTimeSemanticsOps for EventSemantics { _view: G, w: Range, ) -> impl Iterator)> + Send + Sync + 'graph { - node.temp_prop_rows_window(TimeIndexEntry::range(w)) + node.temp_prop_rows_range(Some(TimeIndexEntry::range(w))) .map(|(t, row)| { ( t, diff --git a/raphtory/src/db/api/view/internal/time_semantics/persistent_semantics.rs b/raphtory/src/db/api/view/internal/time_semantics/persistent_semantics.rs index 0e9fa28789..3d3e6ff5b4 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/persistent_semantics.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/persistent_semantics.rs @@ -217,7 +217,7 @@ impl NodeTimeSemanticsOps for PersistentSemantics { node: NodeStorageRef<'graph>, _view: G, ) -> impl Iterator)> + Send + Sync + 'graph { - node.temp_prop_rows().map(|(t, row)| { + node.temp_prop_rows_range().map(|(t, row)| { ( t, row.into_iter().filter_map(|(i, v)| Some((i, v?))).collect(), diff --git a/raphtory/src/io/arrow/df_loaders.rs b/raphtory/src/io/arrow/df_loaders.rs index 8fd25ee156..a89990e932 100644 --- a/raphtory/src/io/arrow/df_loaders.rs +++ b/raphtory/src/io/arrow/df_loaders.rs @@ -23,7 +23,13 @@ use raphtory_api::{ }; use raphtory_storage::mutation::addition_ops::SessionAdditionOps; use rayon::prelude::*; -use std::{collections::HashMap, sync::atomic::Ordering}; +use std::{ + collections::HashMap, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }, +}; #[cfg(feature = "python")] fn build_progress_bar(des: String, num_rows: usize) -> Result { @@ -94,11 +100,6 @@ pub(crate) fn load_nodes_from_df< let cache = graph.get_cache(); let mut write_locked_graph = graph.write_lock().map_err(into_graph_err)?; - let cache_shards = cache.map(|cache| { - (0..write_locked_graph.num_shards()) - .map(|_| cache.fork()) - .collect::>() - }); let mut start_id = session .reserve_event_ids(df_view.num_rows) @@ -147,12 +148,9 @@ pub(crate) fn load_nodes_from_df< Ok::<(), LoadError>(()) })?; - let g = write_locked_graph.graph; - let update_time = |time| g.update_time(time); + let update_time = |time| write_locked_graph.update_time(time); - write_locked_graph - .nodes - .resize(write_locked_graph.num_nodes()); + write_locked_graph.resize_chunks_to_num_nodes(); write_locked_graph .nodes @@ -168,44 +166,26 @@ pub(crate) fn load_nodes_from_df< .zip(node_col.iter()) .enumerate() { - let shard_id = shard.shard_id(); - let node_exists = if let Some(mut_node) = shard.get_mut(*vid) { - mut_node.init(*vid, gid); - mut_node.node_type = *node_type; + if let Some(mut_node) = shard.resolve_pos(*vid) { + let mut writer = shard.writer(); + writer.store_node_id_and_node_type(mut_node, 0, gid, *node_type, 0); + // mut_node.init(*vid, gid); + // mut_node.node_type = *node_type; t_props.clear(); t_props.extend(prop_cols.iter_row(idx)); c_props.clear(); c_props.extend(const_prop_cols.iter_row(idx)); c_props.extend_from_slice(&shared_constant_properties); - - if let Some(caches) = cache_shards.as_ref() { - let cache = &caches[shard_id]; - cache.add_node_update( - TimeIndexEntry(time, start_id + idx), - *vid, - &t_props, - ); - cache.add_node_cprops(*vid, &c_props); - } - - for (id, prop) in c_props.drain(..) { - mut_node.add_constant_prop(id, prop)?; - } - - true - } else { - false + writer.update_c_props(mut_node, 0, c_props.drain(..), 0); + writer.add_props( + TimeIndexEntry(time, start_id + idx), + mut_node, + 0, + t_props.drain(..), + 0, + ); }; - - if node_exists { - let t = TimeIndexEntry(time, start_id + idx); - update_time(t); - let prop_i = shard.t_prop_log_mut().push(t_props.drain(..))?; - if let Some(mut_node) = shard.get_mut(*vid) { - mut_node.update_t_prop_time(t, prop_i); - } - } } Ok::<_, GraphError>(()) })?; @@ -270,11 +250,6 @@ pub(crate) fn load_edges_from_df< let cache = graph.get_cache(); let mut write_locked_graph = graph.write_lock().map_err(into_graph_err)?; - let cache_shards = cache.map(|cache| { - (0..write_locked_graph.num_shards()) - .map(|_| cache.fork()) - .collect::>() - }); for chunk in df_view.chunks { let df = chunk?; @@ -337,15 +312,14 @@ pub(crate) fn load_edges_from_df< Ok::<(), LoadError>(()) })?; - write_locked_graph - .nodes - .resize(write_locked_graph.num_nodes()); + write_locked_graph.resize_chunks_to_num_nodes(); // resolve all the edges eid_col_resolved.resize_with(df.len(), Default::default); let eid_col_shared = atomic_usize_from_mut_slice(cast_slice_mut(&mut eid_col_resolved)); let g = write_locked_graph.graph; - let next_edge_id = || g.storage.edges.next_id(); + let next_edge_id: Arc = Arc::new(AtomicUsize::new(g.internal_num_edges())); + let next_edge_id = || next_edge_id.fetch_add(1, Ordering::Relaxed); let update_time = |time| g.update_time(time); write_locked_graph .nodes @@ -364,17 +338,7 @@ pub(crate) fn load_edges_from_df< src_node.init(*src, src_gid); update_time(TimeIndexEntry(time, start_idx + row)); let eid = match src_node.find_edge_eid(*dst, &LayerIds::All) { - None => { - let eid = next_edge_id(); - if let Some(cache_shards) = cache_shards.as_ref() { - cache_shards[shard_id].resolve_edge( - MaybeNew::New(eid), - *src, - *dst, - ); - } - eid - } + None => next_edge_id(), Some(eid) => eid, }; src_node.update_time( @@ -442,12 +406,6 @@ pub(crate) fn load_edges_from_df< c_props.extend(const_prop_cols.iter_row(idx)); c_props.extend_from_slice(&shared_constant_properties); - if let Some(caches) = cache_shards.as_ref() { - let cache = &caches[shard_id]; - cache.add_edge_update(t, *eid, &t_props, *layer); - cache.add_edge_cprops(*eid, *layer, &c_props); - } - if !t_props.is_empty() || !c_props.is_empty() { let edge_layer = edge.layer_mut(*layer); @@ -466,11 +424,6 @@ pub(crate) fn load_edges_from_df< if let Some(cache) = cache { cache.write()?; } - if let Some(cache_shards) = cache_shards.as_ref() { - for cache in cache_shards { - cache.write()?; - } - } start_idx += df.len(); #[cfg(feature = "python")] diff --git a/raphtory/src/serialise/serialise.rs b/raphtory/src/serialise/serialise.rs index b695d3ab1f..fac1c05353 100644 --- a/raphtory/src/serialise/serialise.rs +++ b/raphtory/src/serialise/serialise.rs @@ -188,7 +188,7 @@ impl StableEncode for GraphStorage { let node = nodes.node(VID(node_id)); graph.new_node(node.id(), node.vid(), node.node_type_id()); - for (time, row) in node.temp_prop_rows() { + for (time, row) in node.temp_prop_rows_range() { graph.update_node_tprops( node.vid(), time, From 5d2549c87192b4ea3f4aeafe8eac8c700ede430f Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Fri, 27 Jun 2025 16:23:10 +0100 Subject: [PATCH 041/321] get rid of useless function --- db4-graph/src/lib.rs | 2 +- db4-storage/src/lib.rs | 2 - .../src/entities/graph/logical_to_physical.rs | 48 ------------------- .../src/mutation/addition_ops_ext.rs | 2 +- 4 files changed, 2 insertions(+), 52 deletions(-) diff --git a/db4-graph/src/lib.rs b/db4-graph/src/lib.rs index 1a29c0ac81..5b32cbe049 100644 --- a/db4-graph/src/lib.rs +++ b/db4-graph/src/lib.rs @@ -224,7 +224,7 @@ impl<'a, EXT: PersistentStrategy, ES = ES>> WriteLockedGraph<' } pub fn resolve_node(&self, gid: GidRef) -> Result, InvalidNodeId> { - self.graph.logical_to_physical.get_or_init_vid(gid, || { + self.graph.logical_to_physical.get_or_init(gid, || { VID(self.num_nodes.fetch_add(1, atomic::Ordering::Relaxed)) }) } diff --git a/db4-storage/src/lib.rs b/db4-storage/src/lib.rs index 72fc69018b..6b77c543b1 100644 --- a/db4-storage/src/lib.rs +++ b/db4-storage/src/lib.rs @@ -60,8 +60,6 @@ pub mod error { IO(#[from] std::io::Error), #[error("Serde error: {0}")] Serde(#[from] serde_json::Error), - // #[error("Load error: {0}")] - // LoadError(#[from] LoadError), #[error("Arrow-rs error: {0}")] ArrowRS(#[from] arrow_schema::ArrowError), #[error("Parquet error: {0}")] diff --git a/raphtory-core/src/entities/graph/logical_to_physical.rs b/raphtory-core/src/entities/graph/logical_to_physical.rs index 6685e38bd5..4f5b797edf 100644 --- a/raphtory-core/src/entities/graph/logical_to_physical.rs +++ b/raphtory-core/src/entities/graph/logical_to_physical.rs @@ -159,54 +159,6 @@ impl Mapping { } } - // we assume pre validation of gids - pub fn get_or_init_vid<'a>( - &self, - gid: GidRef, - f_init: impl FnOnce() -> VID, - ) -> Result, InvalidNodeId> { - let map = self.map.get_or_init(|| match &gid { - GidRef::U64(_) => Map::U64(FxDashMap::default()), - GidRef::Str(_) => Map::Str(FxDashMap::default()), - }); - match gid { - GidRef::U64(id) => map - .as_u64() - .map(|m| { - m.get(&id) - .map(|vid| MaybeNew::Existing(*vid)) - .unwrap_or_else(|| match m.entry(id) { - Entry::Occupied(occupied_entry) => { - MaybeNew::Existing(*occupied_entry.get()) - } - Entry::Vacant(vacant_entry) => { - let vid = f_init(); - vacant_entry.insert(vid); - MaybeNew::New(vid) - } - }) - }) - .ok_or(InvalidNodeId::InvalidNodeIdU64(id)), - GidRef::Str(id) => map - .as_str() - .map(|m| { - m.get(id) - .map(|vid| MaybeNew::Existing(*vid)) - .unwrap_or_else(|| match m.entry(id.to_owned()) { - Entry::Occupied(occupied_entry) => { - MaybeNew::Existing(*occupied_entry.get()) - } - Entry::Vacant(vacant_entry) => { - let vid = f_init(); - vacant_entry.insert(vid); - MaybeNew::New(vid) - } - }) - }) - .ok_or_else(|| InvalidNodeId::InvalidNodeIdStr(id.into())), - } - } - pub fn validate_gids<'a>( &self, gids: impl IntoIterator>, diff --git a/raphtory-storage/src/mutation/addition_ops_ext.rs b/raphtory-storage/src/mutation/addition_ops_ext.rs index 01df8596c4..88b7edfadc 100644 --- a/raphtory-storage/src/mutation/addition_ops_ext.rs +++ b/raphtory-storage/src/mutation/addition_ops_ext.rs @@ -192,7 +192,7 @@ impl InternalAdditionOps for TemporalGraph { NodeRef::External(id) => { let id = self .logical_to_physical - .get_or_init_vid(id, || { + .get_or_init(id, || { // When initializing a new node, reserve node_id as a const prop. // Done here since the id type is not known until node creation. reserve_node_id_as_prop(self.node_meta(), id); From 2b8a09d958ad8fa90b7a2226d3197242d99fe878 Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Fri, 27 Jun 2025 17:21:34 +0100 Subject: [PATCH 042/321] more fixes for df_loaders --- db4-graph/src/lib.rs | 37 ++++--- db4-storage/src/pages/locked/edges.rs | 6 ++ db4-storage/src/pages/node_page/writer.rs | 10 ++ db4-storage/src/pages/node_store.rs | 5 - raphtory/src/io/arrow/df_loaders.rs | 114 +++++++++++----------- 5 files changed, 99 insertions(+), 73 deletions(-) diff --git a/db4-graph/src/lib.rs b/db4-graph/src/lib.rs index 5b32cbe049..fca50fe8a6 100644 --- a/db4-graph/src/lib.rs +++ b/db4-graph/src/lib.rs @@ -17,11 +17,8 @@ use raphtory_core::{ graph::{ logical_to_physical::{InvalidNodeId, Mapping}, tgraph::InvalidLayer, - timer::{MinCounter, TimeCounterTrait}, - }, - nodes::node_ref::NodeRef, - properties::graph_meta::GraphMeta, - GidRef, LayerIds, VID, + timer::{MaxCounter, MinCounter, TimeCounterTrait}, + }, nodes::node_ref::NodeRef, properties::graph_meta::GraphMeta, GidRef, LayerIds, EID, VID }, storage::timeindex::{AsTime, TimeIndexEntry}, }; @@ -52,8 +49,8 @@ pub struct TemporalGraph { event_counter: AtomicUsize, graph_meta: Arc, - earliest: MinCounter, - latest: MinCounter, + pub earliest: Arc, + pub latest: Arc, } impl, ES = ES>> TemporalGraph { @@ -211,6 +208,7 @@ pub struct WriteLockedGraph<'a, EXT> { pub edges: WriteLockedEdgePages<'a, storage::ES>, pub graph: &'a TemporalGraph, pub num_nodes: Arc, + pub num_edges: Arc, } impl<'a, EXT: PersistentStrategy, ES = ES>> WriteLockedGraph<'a, EXT> { @@ -220,6 +218,7 @@ impl<'a, EXT: PersistentStrategy, ES = ES>> WriteLockedGraph<' edges: graph.storage.edges().write_locked().into(), graph, num_nodes: Arc::new(AtomicUsize::new(graph.internal_num_nodes())), + num_edges: Arc::new(AtomicUsize::new(graph.internal_num_edges())), } } @@ -229,6 +228,12 @@ impl<'a, EXT: PersistentStrategy, ES = ES>> WriteLockedGraph<' }) } + pub fn next_edge_id(&self) -> usize { + self.graph + .event_counter + .fetch_add(1, atomic::Ordering::Relaxed) + } + pub fn num_nodes(&self) -> usize { self.num_nodes.load(atomic::Ordering::Relaxed) } @@ -241,15 +246,21 @@ impl<'a, EXT: PersistentStrategy, ES = ES>> WriteLockedGraph<' pub fn resize_chunks_to_num_nodes(&mut self) { let num_nodes = self.num_nodes(); - self.graph.storage().nodes().grow_to_num_nodes(num_nodes); + let (chunks_needed, _) = self.graph.storage.nodes().resolve_pos(VID(num_nodes - 1)); + self.graph.storage().nodes().grow(chunks_needed + 1); std::mem::take(&mut self.nodes); self.nodes = self.graph.storage.nodes().write_locked().into(); } - #[inline] - pub fn update_time(&self, time: TimeIndexEntry) { - let t = time.t(); - self.graph.earliest.update(t); - self.graph.latest.update(t); + pub fn resize_chunks_to_num_edges(&mut self) { + let num_edges = self.graph.internal_num_edges(); + let (chunks_needed, _) = self.graph.storage.edges().resolve_pos(EID(num_edges - 1)); + self.graph.storage().edges().grow(chunks_needed + 1); + std::mem::take(&mut self.edges); + self.edges = self.graph.storage.edges().write_locked().into(); + } + + pub fn earliest_latest(&self) -> (Arc, Arc) { + (self.graph.earliest.clone(), self.graph.latest.clone()) } } diff --git a/db4-storage/src/pages/locked/edges.rs b/db4-storage/src/pages/locked/edges.rs index 412be52624..211e331271 100644 --- a/db4-storage/src/pages/locked/edges.rs +++ b/db4-storage/src/pages/locked/edges.rs @@ -56,6 +56,12 @@ pub struct WriteLockedEdgePages<'a, ES> { writers: Vec>, } +impl Default for WriteLockedEdgePages<'_, ES> { + fn default() -> Self { + Self { writers: Vec::new() } + } +} + impl<'a, EXT, ES: EdgeSegmentOps> WriteLockedEdgePages<'a, ES> { pub fn new(writers: Vec>) -> Self { Self { writers } diff --git a/db4-storage/src/pages/node_page/writer.rs b/db4-storage/src/pages/node_page/writer.rs index e48385d2fa..a7e03564be 100644 --- a/db4-storage/src/pages/node_page/writer.rs +++ b/db4-storage/src/pages/node_page/writer.rs @@ -158,6 +158,16 @@ impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> NodeWri lsn, ); } + + pub fn store_node_id( + &mut self, + pos: LocalPOS, + layer_id: usize, + gid: GidRef<'_>, + lsn: u64, + ) { + self.update_c_props(pos, layer_id, [(1, gid.into())], lsn); + } } impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> Drop diff --git a/db4-storage/src/pages/node_store.rs b/db4-storage/src/pages/node_store.rs index 947711b2b4..d846dbe9ea 100644 --- a/db4-storage/src/pages/node_store.rs +++ b/db4-storage/src/pages/node_store.rs @@ -255,11 +255,6 @@ impl, EXT: Clone> NodeStorageInner self.get_or_create_segment(new_len - 1); } - pub fn grow_to_num_nodes(&self, num_nodes: usize) { - let chunks_needed = (num_nodes + self.max_page_len - 1) / self.max_page_len; - self.grow(chunks_needed); - } - pub fn get_or_create_segment(&self, segment_id: usize) -> &Arc { if let Some(segment) = self.pages.get(segment_id) { return segment; diff --git a/raphtory/src/io/arrow/df_loaders.rs b/raphtory/src/io/arrow/df_loaders.rs index a89990e932..e4d712c151 100644 --- a/raphtory/src/io/arrow/df_loaders.rs +++ b/raphtory/src/io/arrow/df_loaders.rs @@ -11,6 +11,7 @@ use crate::{ serialise::incremental::InternalCache, }; use bytemuck::checked::cast_slice_mut; +use bzip2::write; #[cfg(feature = "python")] use kdam::{Bar, BarBuilder, BarExt}; use raphtory_api::{ @@ -18,15 +19,15 @@ use raphtory_api::{ core::{ entities::{properties::prop::PropType, EID}, storage::{dict_mapper::MaybeNew, timeindex::TimeIndexEntry}, - Direction, }, }; +use raphtory_core::{entities::graph::timer::TimeCounterTrait, storage::timeindex::AsTime}; use raphtory_storage::mutation::addition_ops::SessionAdditionOps; use rayon::prelude::*; use std::{ collections::HashMap, sync::{ - atomic::{AtomicUsize, Ordering}, + atomic::{AtomicBool, AtomicUsize, Ordering}, Arc, }, }; @@ -148,14 +149,16 @@ pub(crate) fn load_nodes_from_df< Ok::<(), LoadError>(()) })?; - let update_time = |time| write_locked_graph.update_time(time); + + let (earliest, latest) = write_locked_graph.earliest_latest(); + let update_time = |time:TimeIndexEntry| { let time = time.t(); earliest.update(time); latest.update(time); }; write_locked_graph.resize_chunks_to_num_nodes(); write_locked_graph .nodes .par_iter_mut() - .try_for_each(|mut shard| { + .try_for_each(|shard| { let mut t_props = vec![]; let mut c_props = vec![]; @@ -169,8 +172,6 @@ pub(crate) fn load_nodes_from_df< if let Some(mut_node) = shard.resolve_pos(*vid) { let mut writer = shard.writer(); writer.store_node_id_and_node_type(mut_node, 0, gid, *node_type, 0); - // mut_node.init(*vid, gid); - // mut_node.node_type = *node_type; t_props.clear(); t_props.extend(prop_cols.iter_row(idx)); @@ -247,6 +248,7 @@ pub(crate) fn load_edges_from_df< let mut src_col_resolved = vec![]; let mut dst_col_resolved = vec![]; let mut eid_col_resolved: Vec = vec![]; + let mut eids_exist: Vec = vec![]; // exists or needs to be created let cache = graph.get_cache(); let mut write_locked_graph = graph.write_lock().map_err(into_graph_err)?; @@ -316,15 +318,18 @@ pub(crate) fn load_edges_from_df< // resolve all the edges eid_col_resolved.resize_with(df.len(), Default::default); + eids_exist.resize_with(df.len(), Default::default); let eid_col_shared = atomic_usize_from_mut_slice(cast_slice_mut(&mut eid_col_resolved)); - let g = write_locked_graph.graph; - let next_edge_id: Arc = Arc::new(AtomicUsize::new(g.internal_num_edges())); - let next_edge_id = || next_edge_id.fetch_add(1, Ordering::Relaxed); - let update_time = |time| g.update_time(time); + + let next_edge_id: Arc = write_locked_graph.num_edges.clone(); + let next_edge_id = || {next_edge_id.fetch_add(1, Ordering::Relaxed)}; + + let (earliest, latest) = write_locked_graph.earliest_latest(); + let update_time = |time:TimeIndexEntry| { let time = time.t(); earliest.update(time); latest.update(time); }; write_locked_graph .nodes .par_iter_mut() - .for_each(|mut shard| { + .for_each(|locked_page| { for (row, ((((src, src_gid), dst), time), layer)) in src_col_resolved .iter() .zip(src_col.iter()) @@ -333,20 +338,32 @@ pub(crate) fn load_edges_from_df< .zip(layer_col_resolved.iter()) .enumerate() { - let shard_id = shard.shard_id(); - if let Some(src_node) = shard.get_mut(*src) { - src_node.init(*src, src_gid); - update_time(TimeIndexEntry(time, start_idx + row)); - let eid = match src_node.find_edge_eid(*dst, &LayerIds::All) { - None => next_edge_id(), - Some(eid) => eid, - }; - src_node.update_time( - TimeIndexEntry(time, start_idx + row), - eid.with_layer(*layer), - ); - src_node.add_edge(*dst, Direction::OUT, *layer, eid); - eid_col_shared[row].store(eid.0, Ordering::Relaxed); + if let Some(src_pos) = locked_page.resolve_pos(*src) { + let t = TimeIndexEntry(time, start_idx + row); + update_time(t); + let mut writer = locked_page.writer(); + writer.store_node_id(src_pos, 0, src_gid, 0); + if let Some(edge_id) = writer.get_out_edge(src_pos, *dst, 0) { + eid_col_shared[row].store(edge_id.0, Ordering::Relaxed); + eids_exist[row].store(true, Ordering::Relaxed); + } else { + let edge_id = EID(next_edge_id()); + writer.add_static_outbound_edge( + src_pos, + *dst, + edge_id.with_layer(*layer), + 0, + ); + writer.add_outbound_edge( + t, + src_pos, + *dst, + edge_id.with_layer(*layer), + 0, + ); // FIXME: when we update this to work with layers use the correct layer + eid_col_shared[row].store(edge_id.0, Ordering::Relaxed); + eids_exist[row].store(false, Ordering::Relaxed); + } } } }); @@ -355,7 +372,7 @@ pub(crate) fn load_edges_from_df< write_locked_graph .nodes .par_iter_mut() - .for_each(|mut shard| { + .for_each(|shard| { for (row, ((((src, (dst, dst_gid)), eid), time), layer)) in src_col_resolved .iter() .zip(dst_col_resolved.iter().zip(dst_col.iter())) @@ -364,41 +381,36 @@ pub(crate) fn load_edges_from_df< .zip(layer_col_resolved.iter()) .enumerate() { - if let Some(node) = shard.get_mut(*dst) { - node.init(*dst, dst_gid); - node.update_time( - TimeIndexEntry(time, row + start_idx), - eid.with_layer(*layer), - ); - node.add_edge(*src, Direction::IN, *layer, *eid) + if let Some(dst_pos) = shard.resolve_pos(*dst) { + let t = TimeIndexEntry(time, start_idx + row); + let mut writer = shard.writer(); + writer.store_node_id(dst_pos, 0, dst_gid, 0); + writer.add_static_inbound_edge(dst_pos, *src, eid.with_layer(*layer), 0); + writer.add_inbound_edge(t, dst_pos, *src, eid.with_layer(*layer), 0); } } }); + write_locked_graph.resize_chunks_to_num_edges(); + write_locked_graph .edges .par_iter_mut() - .try_for_each(|mut shard| { + .try_for_each(|shard| { let mut t_props = vec![]; let mut c_props = vec![]; - for (idx, ((((src, dst), time), eid), layer)) in src_col_resolved + for (idx, (((((src, dst), time), eid), layer), exists)) in src_col_resolved .iter() .zip(dst_col_resolved.iter()) .zip(time_col.iter()) .zip(eid_col_resolved.iter()) - .zip(layer_col_resolved.iter()) + .zip(layer_col_resolved.iter()).zip(eids_exist.iter().map(|exists| exists.load(Ordering::Relaxed))) .enumerate() { - let shard_id = shard.shard_id(); - if let Some(mut edge) = shard.get_mut(*eid) { - let edge_store = edge.edge_store_mut(); - if !edge_store.initialised() { - edge_store.src = *src; - edge_store.dst = *dst; - edge_store.eid = *eid; - } + if let Some(eid_pos) = shard.resolve_pos(*eid) { let t = TimeIndexEntry(time, start_idx + idx); - edge.additions_mut(*layer).insert(t); + let mut writer = shard.writer(); + t_props.clear(); t_props.extend(prop_cols.iter_row(idx)); @@ -406,17 +418,9 @@ pub(crate) fn load_edges_from_df< c_props.extend(const_prop_cols.iter_row(idx)); c_props.extend_from_slice(&shared_constant_properties); - if !t_props.is_empty() || !c_props.is_empty() { - let edge_layer = edge.layer_mut(*layer); + writer.update_c_props(eid_pos, *src, *dst, *layer, c_props.drain(..)); + writer.add_edge(t, Some(eid_pos), *src, *dst, t_props.drain(..), *layer, 0, Some(exists)); - for (id, prop) in t_props.drain(..) { - edge_layer.add_prop(t, id, prop)?; - } - - for (id, prop) in c_props.drain(..) { - edge_layer.update_constant_prop(id, prop)?; - } - } } } Ok::<(), GraphError>(()) From f2241c26cd3d65c42aedaba49e20c11384a8b8f1 Mon Sep 17 00:00:00 2001 From: Fadhil Abubaker Date: Fri, 27 Jun 2025 12:27:15 -0400 Subject: [PATCH 043/321] Fix compiler issues --- db4-storage/src/segments/node_entry.rs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/db4-storage/src/segments/node_entry.rs b/db4-storage/src/segments/node_entry.rs index 25ba76861f..08e0a20772 100644 --- a/db4-storage/src/segments/node_entry.rs +++ b/db4-storage/src/segments/node_entry.rs @@ -16,7 +16,7 @@ use raphtory_core::{ entities::{LayerIds, edges::edge_ref::EdgeRef, properties::tprop::TPropCell}, storage::timeindex::{TimeIndexEntry, TimeIndexOps}, }; -use std::{iter::Empty, ops::Deref, sync::Arc}; +use std::{iter::Empty, ops::{Deref, Range}, sync::Arc}; use super::additions::MemAdditions; @@ -161,21 +161,17 @@ impl<'a> NodeRefOps<'a> for MemNodeRef<'a> { fn temp_prop_rows( self, + w: Option>, ) -> impl Iterator< Item = ( TimeIndexEntry, usize, - impl Iterator)>, + Vec<(usize, Prop)>, ), > + 'a { - // self.ns.as_ref().iter().enumerate().flat_map(|(layer_id, layer)| { - // let rows = layer.t_prop_rows(self.pos); - // }).flat_map(|t_prop| { - // t_prop. - // }) - - //TODO - std::iter::empty::<(_, _, Empty<_>)>() + // TODO: Implement this properly + // For now, return empty iterator to satisfy the trait + std::iter::empty() } fn degree(self, layers: &LayerIds, dir: Direction) -> usize { From f34de9c827ef545fb7ac88f1d3e720c7f7c29a5f Mon Sep 17 00:00:00 2001 From: Fadhil Abubaker Date: Fri, 27 Jun 2025 12:57:14 -0400 Subject: [PATCH 044/321] Add GIDResolverOps trait and MappingResolver --- db4-storage/Cargo.toml | 2 +- db4-storage/src/lib.rs | 1 + db4-storage/src/resolver/mapping_resolver.rs | 55 ++++++++++++++++++++ db4-storage/src/resolver/mod.rs | 36 +++++++++++++ 4 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 db4-storage/src/resolver/mapping_resolver.rs create mode 100644 db4-storage/src/resolver/mod.rs diff --git a/db4-storage/Cargo.toml b/db4-storage/Cargo.toml index 5ae48498db..b40190e01d 100644 --- a/db4-storage/Cargo.toml +++ b/db4-storage/Cargo.toml @@ -47,4 +47,4 @@ rayon.workspace = true [features] test-utils = ["proptest", "tempfile", "chrono"] -default = ["test-utils"] \ No newline at end of file +default = ["test-utils"] diff --git a/db4-storage/src/lib.rs b/db4-storage/src/lib.rs index 298cb6dced..932316e84f 100644 --- a/db4-storage/src/lib.rs +++ b/db4-storage/src/lib.rs @@ -26,6 +26,7 @@ pub mod persist; pub mod properties; pub mod segments; pub mod utils; +pub mod resolver; pub type Extension = (); pub type NS

= NodeSegmentView

; diff --git a/db4-storage/src/resolver/mapping_resolver.rs b/db4-storage/src/resolver/mapping_resolver.rs new file mode 100644 index 0000000000..f205731c4d --- /dev/null +++ b/db4-storage/src/resolver/mapping_resolver.rs @@ -0,0 +1,55 @@ +use std::path::Path; +use raphtory_api::core::{ + entities::{GidRef, VID, GidType}, + storage::dict_mapper::MaybeNew, +}; +use raphtory_core::entities::graph::logical_to_physical::{Mapping}; +use crate::resolver::{GIDResolverOps, GIDResolverError}; + +pub struct MappingResolver { + mapping: Mapping, +} + +impl GIDResolverOps for MappingResolver { + fn new(_path: impl AsRef) -> Result { + Ok(Self { mapping: Mapping::new() }) + } + + fn len(&self) -> usize { + self.mapping.len() + } + + fn dtype(&self) -> Option { + self.mapping.dtype() + } + + fn set(&self, gid: GidRef, vid: VID) -> Result<(), GIDResolverError> { + self.mapping.set(gid, vid)?; + Ok(()) + } + + fn get_or_init( + &self, + gid: GidRef, + next_id: impl FnOnce() -> VID, + ) -> Result, GIDResolverError> { + let result = self.mapping.get_or_init(gid, next_id)?; + Ok(result) + } + + fn validate_gids<'a>( + &self, + gids: impl IntoIterator>, + ) -> Result<(), GIDResolverError> { + let result = self.mapping.validate_gids(gids)?; + Ok(result) + } + + fn get_str(&self, gid: &str) -> Option { + self.mapping.get_str(gid) + } + + fn get_u64(&self, gid: u64) -> Option { + self.mapping.get_u64(gid) + } +} diff --git a/db4-storage/src/resolver/mod.rs b/db4-storage/src/resolver/mod.rs new file mode 100644 index 0000000000..15ba7730b8 --- /dev/null +++ b/db4-storage/src/resolver/mod.rs @@ -0,0 +1,36 @@ +use std::path::Path; + +use raphtory_api::core::{ + entities::{GidRef, VID, GidType}, + storage::dict_mapper::MaybeNew, +}; +use raphtory_core::entities::graph::logical_to_physical::{InvalidNodeId}; +use crate::error::DBV4Error; + +pub mod mapping_resolver; + +#[derive(thiserror::Error, Debug)] +pub enum GIDResolverError { + #[error(transparent)] + Database(#[from] DBV4Error), + #[error(transparent)] + InvalidNodeId(#[from] InvalidNodeId), +} + +pub trait GIDResolverOps { + fn new(path: impl AsRef) -> Result where Self: Sized; + fn len(&self) -> usize; + fn dtype(&self) -> Option; + fn set(&self, gid: GidRef, vid: VID) -> Result<(), GIDResolverError>; + fn get_or_init( + &self, + gid: GidRef, + next_id: impl FnOnce() -> VID, + ) -> Result, GIDResolverError>; + fn validate_gids<'a>( + &self, + gids: impl IntoIterator>, + ) -> Result<(), GIDResolverError>; + fn get_str(&self, gid: &str) -> Option; + fn get_u64(&self, gid: u64) -> Option; +} From 9b80d8e78ea90cc4bc199697e3252cb4005d0166 Mon Sep 17 00:00:00 2001 From: Fadhil Abubaker Date: Fri, 27 Jun 2025 13:02:08 -0400 Subject: [PATCH 045/321] Add import --- db4-graph/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db4-graph/src/lib.rs b/db4-graph/src/lib.rs index f278fda2c3..1951195c6c 100644 --- a/db4-graph/src/lib.rs +++ b/db4-graph/src/lib.rs @@ -26,7 +26,7 @@ use raphtory_core::{ }; use storage::{ error::DBV4Error, persist::strategy::PersistentStrategy, Extension, Layer, ReadLockedLayer, ES, - NS, GIDResolver, + NS, resolver::GIDResolverOps, GIDResolver, }; pub mod entries; From 8dd016cfb2323649f7d7e55694c595e4659178eb Mon Sep 17 00:00:00 2001 From: Fadhil Abubaker Date: Fri, 27 Jun 2025 13:27:19 -0400 Subject: [PATCH 046/321] Handle errors correctly in resolve_node --- db4-graph/src/lib.rs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/db4-graph/src/lib.rs b/db4-graph/src/lib.rs index a763dddc60..8d0d7deb8c 100644 --- a/db4-graph/src/lib.rs +++ b/db4-graph/src/lib.rs @@ -26,10 +26,11 @@ use raphtory_core::{ storage::timeindex::{AsTime, TimeIndexEntry}, }; use storage::{ - error::DBV4Error, persist::strategy::PersistentStrategy, Extension, - Layer, ReadLockedLayer, ES, - NS, resolver::GIDResolverOps, GIDResolver, pages::locked::{edges::WriteLockedEdgePages, nodes::WriteLockedNodePages}, + persist::strategy::PersistentStrategy, + resolver::{GIDResolverError, GIDResolverOps}, + Extension, GIDResolver, Layer, + ReadLockedLayer, ES, NS }; pub mod entries; @@ -226,9 +227,15 @@ impl<'a, EXT: PersistentStrategy, ES = ES>> WriteLockedGraph<' } pub fn resolve_node(&self, gid: GidRef) -> Result, InvalidNodeId> { - self.graph.logical_to_physical.get_or_init(gid, || { + let result = self.graph.logical_to_physical.get_or_init(gid, || { VID(self.num_nodes.fetch_add(1, atomic::Ordering::Relaxed)) - }) + }); + + match result { + Ok(vid) => Ok(vid), + Err(GIDResolverError::Database(e)) => panic!("Database error: {}", e), + Err(GIDResolverError::InvalidNodeId(e)) => Err(e), + } } pub fn num_nodes(&self) -> usize { From c4f1bfc4c28adb664ccac0a689afb6d65cb9fead Mon Sep 17 00:00:00 2001 From: Fadhil Abubaker Date: Fri, 27 Jun 2025 14:24:32 -0400 Subject: [PATCH 047/321] Add mapping from GIDResolverError to MutationError --- db4-graph/src/lib.rs | 2 +- db4-storage/src/lib.rs | 16 ++++++---------- db4-storage/src/resolver/mapping_resolver.rs | 1 + db4-storage/src/resolver/mod.rs | 4 ++-- raphtory-storage/src/core_ops.rs | 1 + .../src/mutation/addition_ops_ext.rs | 10 ++++------ raphtory-storage/src/mutation/mod.rs | 11 ++++++++++- 7 files changed, 25 insertions(+), 20 deletions(-) diff --git a/db4-graph/src/lib.rs b/db4-graph/src/lib.rs index 8d0d7deb8c..66ff26d8eb 100644 --- a/db4-graph/src/lib.rs +++ b/db4-graph/src/lib.rs @@ -233,7 +233,7 @@ impl<'a, EXT: PersistentStrategy, ES = ES>> WriteLockedGraph<' match result { Ok(vid) => Ok(vid), - Err(GIDResolverError::Database(e)) => panic!("Database error: {}", e), + Err(GIDResolverError::DBV4Error(e)) => panic!("Database error: {}", e), Err(GIDResolverError::InvalidNodeId(e)) => Err(e), } } diff --git a/db4-storage/src/lib.rs b/db4-storage/src/lib.rs index be846ebd0d..702e21c3f0 100644 --- a/db4-storage/src/lib.rs +++ b/db4-storage/src/lib.rs @@ -1,21 +1,17 @@ use std::path::Path; use crate::{ - gen_t_props::GenTProps, - gen_ts::GenericTimeOps, - pages::{ - GraphStore, ReadLockedGraphStore, edge_store::ReadLockedEdgeStorage, - node_store::ReadLockedNodeStorage, - }, - segments::{ + gen_t_props::GenTProps, gen_ts::GenericTimeOps, pages::{ + edge_store::ReadLockedEdgeStorage, node_store::ReadLockedNodeStorage, + GraphStore, ReadLockedGraphStore + }, resolver::mapping_resolver::MappingResolver, segments::{ edge::EdgeSegmentView, edge_entry::{MemEdgeEntry, MemEdgeRef}, node::NodeSegmentView, node_entry::{MemNodeEntry, MemNodeRef}, - }, + } }; use raphtory_api::core::entities::{EID, VID}; -use raphtory_core::entities::graph::logical_to_physical::Mapping; use segments::{edge::MemEdgeSegment, node::MemNodeSegment}; pub mod api; @@ -33,7 +29,7 @@ pub type NS

= NodeSegmentView

; pub type ES

= EdgeSegmentView

; pub type Layer = GraphStore, EdgeSegmentView, EXT>; -pub type GIDResolver = Mapping; +pub type GIDResolver = MappingResolver; pub type ReadLockedLayer = ReadLockedGraphStore; pub type ReadLockedNodes

= ReadLockedNodeStorage; diff --git a/db4-storage/src/resolver/mapping_resolver.rs b/db4-storage/src/resolver/mapping_resolver.rs index f205731c4d..1d56c84fbf 100644 --- a/db4-storage/src/resolver/mapping_resolver.rs +++ b/db4-storage/src/resolver/mapping_resolver.rs @@ -6,6 +6,7 @@ use raphtory_api::core::{ use raphtory_core::entities::graph::logical_to_physical::{Mapping}; use crate::resolver::{GIDResolverOps, GIDResolverError}; +#[derive(Debug)] pub struct MappingResolver { mapping: Mapping, } diff --git a/db4-storage/src/resolver/mod.rs b/db4-storage/src/resolver/mod.rs index 15ba7730b8..20244bdf24 100644 --- a/db4-storage/src/resolver/mod.rs +++ b/db4-storage/src/resolver/mod.rs @@ -4,7 +4,7 @@ use raphtory_api::core::{ entities::{GidRef, VID, GidType}, storage::dict_mapper::MaybeNew, }; -use raphtory_core::entities::graph::logical_to_physical::{InvalidNodeId}; +use raphtory_core::entities::graph::logical_to_physical::InvalidNodeId; use crate::error::DBV4Error; pub mod mapping_resolver; @@ -12,7 +12,7 @@ pub mod mapping_resolver; #[derive(thiserror::Error, Debug)] pub enum GIDResolverError { #[error(transparent)] - Database(#[from] DBV4Error), + DBV4Error(#[from] DBV4Error), #[error(transparent)] InvalidNodeId(#[from] InvalidNodeId), } diff --git a/raphtory-storage/src/core_ops.rs b/raphtory-storage/src/core_ops.rs index cd10647d7e..0db3afe7c6 100644 --- a/raphtory-storage/src/core_ops.rs +++ b/raphtory-storage/src/core_ops.rs @@ -26,6 +26,7 @@ use std::{ iter, sync::{atomic::Ordering, Arc}, }; +use storage::resolver::GIDResolverOps; /// Check if two Graph views point at the same underlying storage pub fn is_view_compatible(g1: &impl CoreGraphOps, g2: &impl CoreGraphOps) -> bool { diff --git a/raphtory-storage/src/mutation/addition_ops_ext.rs b/raphtory-storage/src/mutation/addition_ops_ext.rs index 88b7edfadc..a7e62952a1 100644 --- a/raphtory-storage/src/mutation/addition_ops_ext.rs +++ b/raphtory-storage/src/mutation/addition_ops_ext.rs @@ -19,6 +19,7 @@ use storage::{ properties::props_meta_writer::PropsMetaWriter, segments::{edge::MemEdgeSegment, node::MemNodeSegment}, Extension, ES, NS, + resolver::{GIDResolverOps}, }; use crate::mutation::{ @@ -200,8 +201,7 @@ impl InternalAdditionOps for TemporalGraph { self.node_count .fetch_add(1, std::sync::atomic::Ordering::Relaxed) .into() - }) - .map_err(MutationError::InvalidNodeId)?; + })?; Ok(id) } @@ -221,10 +221,8 @@ impl InternalAdditionOps for TemporalGraph { &self, gids: impl IntoIterator>, ) -> Result<(), Self::Error> { - Ok(self - .logical_to_physical - .validate_gids(gids) - .map_err(MutationError::InvalidNodeId)?) + self.logical_to_physical.validate_gids(gids)?; + Ok(()) } fn write_session(&self) -> Result, Self::Error> { diff --git a/raphtory-storage/src/mutation/mod.rs b/raphtory-storage/src/mutation/mod.rs index 37fa9cc386..370f36b862 100644 --- a/raphtory-storage/src/mutation/mod.rs +++ b/raphtory-storage/src/mutation/mod.rs @@ -18,7 +18,7 @@ use raphtory_core::entities::{ }, }; use std::sync::Arc; -use storage::error::DBV4Error; +use storage::{error::DBV4Error, resolver::GIDResolverError}; use thiserror::Error; pub mod addition_ops; @@ -56,6 +56,15 @@ pub enum MutationError { DBV4Error(#[from] DBV4Error), } +impl From for MutationError { + fn from(error: GIDResolverError) -> Self { + match error { + GIDResolverError::DBV4Error(e) => MutationError::DBV4Error(e), + GIDResolverError::InvalidNodeId(e) => MutationError::InvalidNodeId(e), + } + } +} + pub trait InheritMutationOps: Base {} impl InheritAdditionOps for G {} From 0d42f5ce898f9f2ea5075d754cb892f250186c2b Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Sun, 29 Jun 2025 16:39:58 +0100 Subject: [PATCH 048/321] df_loaders --- db4-graph/src/lib.rs | 5 +- db4-storage/src/api/edges.rs | 4 +- db4-storage/src/pages/edge_page/writer.rs | 7 +- db4-storage/src/pages/edge_store.rs | 11 +- db4-storage/src/pages/locked/edges.rs | 11 +- db4-storage/src/pages/locked/nodes.rs | 6 +- db4-storage/src/pages/mod.rs | 7 +- db4-storage/src/pages/node_page/writer.rs | 8 +- db4-storage/src/properties/mod.rs | 2 +- .../entities/properties/prop/prop_enum.rs | 5 +- raphtory-core/src/storage/node_entry.rs | 6 +- .../src/graph/edges/edge_entry.rs | 16 +- raphtory-storage/src/graph/locked.rs | 6 +- .../src/graph/nodes/node_entry.rs | 10 +- .../src/graph/nodes/node_storage_ops.rs | 9 +- raphtory-storage/src/graph/nodes/nodes.rs | 1 - raphtory-storage/src/mutation/addition_ops.rs | 11 +- .../src/mutation/addition_ops_ext.rs | 4 +- raphtory-storage/src/mutation/deletion_ops.rs | 2 +- .../src/mutation/property_addition_ops.rs | 2 +- .../internal/time_semantics/filtered_edge.rs | 4 +- raphtory/src/io/arrow/df_loaders.rs | 165 ++++++++---------- 22 files changed, 148 insertions(+), 154 deletions(-) diff --git a/db4-graph/src/lib.rs b/db4-graph/src/lib.rs index fca50fe8a6..b5bb6ed69c 100644 --- a/db4-graph/src/lib.rs +++ b/db4-graph/src/lib.rs @@ -18,7 +18,10 @@ use raphtory_core::{ logical_to_physical::{InvalidNodeId, Mapping}, tgraph::InvalidLayer, timer::{MaxCounter, MinCounter, TimeCounterTrait}, - }, nodes::node_ref::NodeRef, properties::graph_meta::GraphMeta, GidRef, LayerIds, EID, VID + }, + nodes::node_ref::NodeRef, + properties::graph_meta::GraphMeta, + GidRef, LayerIds, EID, VID, }, storage::timeindex::{AsTime, TimeIndexEntry}, }; diff --git a/db4-storage/src/api/edges.rs b/db4-storage/src/api/edges.rs index 0d7aa6750f..58530cf291 100644 --- a/db4-storage/src/api/edges.rs +++ b/db4-storage/src/api/edges.rs @@ -7,7 +7,7 @@ use std::{ use parking_lot::{RwLockReadGuard, RwLockWriteGuard, lock_api::ArcRwLockReadGuard}; use raphtory_api::core::entities::properties::{meta::Meta, prop::Prop, tprop::TPropOps}; use raphtory_core::{ - entities::{LayerIds, EID, VID}, + entities::{EID, LayerIds, VID}, storage::timeindex::{TimeIndexEntry, TimeIndexOps}, }; use rayon::iter::ParallelIterator; @@ -142,7 +142,7 @@ pub trait EdgeRefOps<'a>: Copy + Clone + Send + Sync { fn edge(self, layer_id: usize) -> Option<(VID, VID)>; - fn has_layer_inner(self, layer_id: usize) -> bool{ + fn has_layer_inner(self, layer_id: usize) -> bool { self.edge(layer_id).is_some() } diff --git a/db4-storage/src/pages/edge_page/writer.rs b/db4-storage/src/pages/edge_page/writer.rs index 0e0dcf6d09..252f0d0eaa 100644 --- a/db4-storage/src/pages/edge_page/writer.rs +++ b/db4-storage/src/pages/edge_page/writer.rs @@ -105,11 +105,14 @@ impl<'a, MP: DerefMut, ES: EdgeSegmentOps> EdgeWriter<' pub fn update_c_props( &mut self, edge_pos: LocalPOS, - src: impl Into, - dst: impl Into, + src_dst: Option<(VID, VID)>, layer_id: usize, props: impl IntoIterator, ) { + let (src, dst) = src_dst + .or_else(|| self.page.get_edge(edge_pos, layer_id, self.writer.deref())) + .expect("Edge must exist for updating properties"); + self.writer .update_const_properties(edge_pos, src, dst, layer_id, props); } diff --git a/db4-storage/src/pages/edge_store.rs b/db4-storage/src/pages/edge_store.rs index 43a6847815..06ce20f956 100644 --- a/db4-storage/src/pages/edge_store.rs +++ b/db4-storage/src/pages/edge_store.rs @@ -9,7 +9,14 @@ use std::{ use super::{edge_page::writer::EdgeWriter, resolve_pos}; use crate::{ - api::edges::{EdgeSegmentOps, LockedESegment}, error::DBV4Error, pages::{layer_counter::LayerCounter, locked::edges::{LockedEdgePage, WriteLockedEdgePages}}, segments::edge::MemEdgeSegment, LocalPOS + LocalPOS, + api::edges::{EdgeSegmentOps, LockedESegment}, + error::DBV4Error, + pages::{ + layer_counter::LayerCounter, + locked::edges::{LockedEdgePage, WriteLockedEdgePages}, + }, + segments::edge::MemEdgeSegment, }; use parking_lot::{RwLock, RwLockWriteGuard}; use raphtory_api::core::entities::{EID, VID, properties::meta::Meta}; @@ -39,7 +46,6 @@ pub struct ReadLockedEdgeStorage, EXT> { } impl, EXT: Clone + Send + Sync> ReadLockedEdgeStorage { - pub fn storage(&self) -> &EdgeStorageInner { &self.storage } @@ -214,7 +220,6 @@ impl, EXT: Clone + Send + Sync> EdgeStorageI lock }); - let mut layer_counts = vec![]; for (_, page) in pages.iter() { diff --git a/db4-storage/src/pages/locked/edges.rs b/db4-storage/src/pages/locked/edges.rs index 211e331271..794cdba72e 100644 --- a/db4-storage/src/pages/locked/edges.rs +++ b/db4-storage/src/pages/locked/edges.rs @@ -1,7 +1,10 @@ use std::{ops::DerefMut, sync::atomic::AtomicUsize}; use crate::{ - api::edges::EdgeSegmentOps, pages::{edge_page::writer::EdgeWriter, layer_counter::LayerCounter, resolve_pos}, segments::edge::MemEdgeSegment, LocalPOS + LocalPOS, + api::edges::EdgeSegmentOps, + pages::{edge_page::writer::EdgeWriter, layer_counter::LayerCounter, resolve_pos}, + segments::edge::MemEdgeSegment, }; use parking_lot::RwLockWriteGuard; use raphtory_core::entities::EID; @@ -56,9 +59,11 @@ pub struct WriteLockedEdgePages<'a, ES> { writers: Vec>, } -impl Default for WriteLockedEdgePages<'_, ES> { +impl Default for WriteLockedEdgePages<'_, ES> { fn default() -> Self { - Self { writers: Vec::new() } + Self { + writers: Vec::new(), + } } } diff --git a/db4-storage/src/pages/locked/nodes.rs b/db4-storage/src/pages/locked/nodes.rs index 432e95dde6..963f754bf8 100644 --- a/db4-storage/src/pages/locked/nodes.rs +++ b/db4-storage/src/pages/locked/nodes.rs @@ -58,9 +58,11 @@ pub struct WriteLockedNodePages<'a, NS> { writers: Vec>, } -impl <'a, NS> Default for WriteLockedNodePages<'_, NS> { +impl<'a, NS> Default for WriteLockedNodePages<'_, NS> { fn default() -> Self { - Self { writers: Vec::new() } + Self { + writers: Vec::new(), + } } } diff --git a/db4-storage/src/pages/mod.rs b/db4-storage/src/pages/mod.rs index 6b35b532f4..d653daed31 100644 --- a/db4-storage/src/pages/mod.rs +++ b/db4-storage/src/pages/mod.rs @@ -267,7 +267,12 @@ impl< .expect("Internal Error, EID should be checked at this point!"); let prop_writer = PropsMetaWriter::constant(self.edge_meta(), props.into_iter())?; - edge_writer.update_c_props(edge_pos, src, dst, layer, prop_writer.into_props_const()?); + edge_writer.update_c_props( + edge_pos, + Some((src, dst)), + layer, + prop_writer.into_props_const()?, + ); Ok(()) } diff --git a/db4-storage/src/pages/node_page/writer.rs b/db4-storage/src/pages/node_page/writer.rs index a7e03564be..154c6dd5ec 100644 --- a/db4-storage/src/pages/node_page/writer.rs +++ b/db4-storage/src/pages/node_page/writer.rs @@ -159,13 +159,7 @@ impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> NodeWri ); } - pub fn store_node_id( - &mut self, - pos: LocalPOS, - layer_id: usize, - gid: GidRef<'_>, - lsn: u64, - ) { + pub fn store_node_id(&mut self, pos: LocalPOS, layer_id: usize, gid: GidRef<'_>, lsn: u64) { self.update_c_props(pos, layer_id, [(1, gid.into())], lsn); } } diff --git a/db4-storage/src/properties/mod.rs b/db4-storage/src/properties/mod.rs index 197be92932..52d19ea6c8 100644 --- a/db4-storage/src/properties/mod.rs +++ b/db4-storage/src/properties/mod.rs @@ -301,7 +301,7 @@ impl<'a> PropMutEntry<'a> { self.properties.update_earliest_latest(t); } - pub(crate) fn deletion_timestamp(&mut self, t:TimeIndexEntry, edge_id: Option) { + pub(crate) fn deletion_timestamp(&mut self, t: TimeIndexEntry, edge_id: Option) { if self.properties.deletions.len() <= self.row { self.properties .deletions diff --git a/raphtory-api/src/core/entities/properties/prop/prop_enum.rs b/raphtory-api/src/core/entities/properties/prop/prop_enum.rs index a90ed51c6d..5deff1d743 100644 --- a/raphtory-api/src/core/entities/properties/prop/prop_enum.rs +++ b/raphtory-api/src/core/entities/properties/prop/prop_enum.rs @@ -1,5 +1,8 @@ use crate::core::{ - entities::{properties::prop::{prop_ref_enum::PropRef, PropType}, GidRef}, + entities::{ + properties::prop::{prop_ref_enum::PropRef, PropType}, + GidRef, + }, storage::arc_str::ArcStr, }; use bigdecimal::{num_bigint::BigInt, BigDecimal}; diff --git a/raphtory-core/src/storage/node_entry.rs b/raphtory-core/src/storage/node_entry.rs index 0d851bf8ab..bdd3b8d217 100644 --- a/raphtory-core/src/storage/node_entry.rs +++ b/raphtory-core/src/storage/node_entry.rs @@ -3,7 +3,9 @@ use crate::entities::{nodes::node_store::NodeStore, properties::tprop::TPropCell use itertools::Itertools; use raphtory_api::core::{ entities::{ - edges::edge_ref::EdgeRef, properties::{prop::Prop, tprop::TPropOps}, GidRef, LayerIds + edges::edge_ref::EdgeRef, + properties::{prop::Prop, tprop::TPropOps}, + GidRef, LayerIds, }, storage::timeindex::TimeIndexEntry, Direction, @@ -134,7 +136,7 @@ impl<'a> NodePtr<'a> { .map(move |(t, row)| (*t, MemRow::new(self.t_props_log, *row))) } - pub fn id(&self) -> GidRef { + pub fn id(&self) -> GidRef { self.node.global_id().into() } } diff --git a/raphtory-storage/src/graph/edges/edge_entry.rs b/raphtory-storage/src/graph/edges/edge_entry.rs index daee4e4314..be9f6c8f80 100644 --- a/raphtory-storage/src/graph/edges/edge_entry.rs +++ b/raphtory-storage/src/graph/edges/edge_entry.rs @@ -1,10 +1,8 @@ use std::ops::Range; -use crate::graph::edges::{ - edge_ref::EdgeStorageRef, edge_storage_ops::EdgeStorageOps} -; +use crate::graph::edges::{edge_ref::EdgeStorageRef, edge_storage_ops::EdgeStorageOps}; use raphtory_api::core::entities::properties::{prop::Prop, tprop::TPropOps}; -use raphtory_core::{entities::{LayerIds, EID, VID}}; +use raphtory_core::entities::{LayerIds, EID, VID}; use storage::{api::edges::EdgeEntryOps, EdgeEntry, EdgeEntryRef}; #[cfg(feature = "storage")] @@ -72,11 +70,17 @@ impl<'a, 'b: 'a> EdgeStorageOps<'a> for &'a EdgeStorageEntry<'b> { fn updates_iter( self, layer_ids: &'a LayerIds, - ) -> impl Iterator, storage::EdgeAdditions<'a>)> + 'a { + ) -> impl Iterator< + Item = ( + usize, + storage::EdgeAdditions<'a>, + storage::EdgeAdditions<'a>, + ), + > + 'a { self.as_ref().updates_iter(layer_ids) } - fn additions(self, layer_id: usize) -> storage::EdgeAdditions<'a>{ + fn additions(self, layer_id: usize) -> storage::EdgeAdditions<'a> { self.as_ref().additions(layer_id) } diff --git a/raphtory-storage/src/graph/locked.rs b/raphtory-storage/src/graph/locked.rs index 35ce06079b..e619256c05 100644 --- a/raphtory-storage/src/graph/locked.rs +++ b/raphtory-storage/src/graph/locked.rs @@ -8,7 +8,10 @@ use raphtory_core::{ storage::{raw_edges::WriteLockedEdges, WriteLockedNodes}, }; use std::sync::Arc; -use storage::{pages::locked::{edges::WriteLockedEdgePages, nodes::WriteLockedNodePages}, Extension, ReadLockedEdges, ReadLockedNodes}; +use storage::{ + pages::locked::{edges::WriteLockedEdgePages, nodes::WriteLockedNodePages}, + Extension, ReadLockedEdges, ReadLockedNodes, +}; #[derive(Debug)] pub struct LockedGraph { @@ -56,4 +59,3 @@ impl Clone for LockedGraph { } } } - diff --git a/raphtory-storage/src/graph/nodes/node_entry.rs b/raphtory-storage/src/graph/nodes/node_entry.rs index 709e024fcd..360be13dcc 100644 --- a/raphtory-storage/src/graph/nodes/node_entry.rs +++ b/raphtory-storage/src/graph/nodes/node_entry.rs @@ -114,26 +114,26 @@ impl<'a, 'b: 'a> NodeStorageOps<'a> for &'a NodeStorageEntry<'b> { fn find_edge(self, dst: VID, layer_ids: &LayerIds) -> Option { self.as_ref().find_edge(dst, layer_ids) } - + fn layer_ids_iter( self, layer_ids: &'a LayerIds, ) -> impl Iterator + Send + Sync + 'a { self.as_ref().layer_ids_iter(layer_ids) } - + fn deletions(self, layer_id: usize) -> storage::NodeAdditions<'a> { self.as_ref().deletions(layer_id) } - + fn temporal_prop_layer(self, layer_id: usize, prop_id: usize) -> storage::NodeTProps<'a> { self.as_ref().temporal_prop_layer(layer_id, prop_id) } - + fn constant_prop_layer(self, layer_id: usize, prop_id: usize) -> Option { self.as_ref().constant_prop_layer(layer_id, prop_id) } - + fn temp_prop_rows_range( self, w: Option>, diff --git a/raphtory-storage/src/graph/nodes/node_storage_ops.rs b/raphtory-storage/src/graph/nodes/node_storage_ops.rs index 4f4ea92509..556e8f09e2 100644 --- a/raphtory-storage/src/graph/nodes/node_storage_ops.rs +++ b/raphtory-storage/src/graph/nodes/node_storage_ops.rs @@ -128,14 +128,13 @@ impl<'a> NodeStorageOps<'a> for NodeEntryRef<'a> { } fn constant_prop_layer(self, layer_id: usize, prop_id: usize) -> Option { - NodeRefOps::c_prop(self, layer_id, prop_id) + NodeRefOps::c_prop(self, layer_id, prop_id) } fn temp_prop_rows_range( - self, - w: Option>, - ) -> impl Iterator)> { + self, + w: Option>, + ) -> impl Iterator)> { NodeRefOps::temp_prop_rows(self, w) } - } diff --git a/raphtory-storage/src/graph/nodes/nodes.rs b/raphtory-storage/src/graph/nodes/nodes.rs index b9ed0b973f..d0cc4963bb 100644 --- a/raphtory-storage/src/graph/nodes/nodes.rs +++ b/raphtory-storage/src/graph/nodes/nodes.rs @@ -14,7 +14,6 @@ pub struct NodesStorage { } impl NodesStorage { - pub fn new(storage: Arc>) -> Self { Self { storage } } diff --git a/raphtory-storage/src/mutation/addition_ops.rs b/raphtory-storage/src/mutation/addition_ops.rs index 512a2573bc..7e0f929f3f 100644 --- a/raphtory-storage/src/mutation/addition_ops.rs +++ b/raphtory-storage/src/mutation/addition_ops.rs @@ -1,7 +1,4 @@ -use crate::{ - graph::{graph::GraphStorage}, - mutation::MutationError, -}; +use crate::{graph::graph::GraphStorage, mutation::MutationError}; use db4_graph::WriteLockedGraph; use raphtory_api::{ core::{ @@ -30,9 +27,7 @@ pub trait InternalAdditionOps { where Self: 'a; - fn write_lock( - &self, - ) -> Result, Self::Error>; + fn write_lock(&self) -> Result, Self::Error>; fn write_lock_nodes(&self) -> Result; fn write_lock_edges(&self) -> Result; /// map layer name to id and allocate a new layer if needed @@ -288,7 +283,7 @@ impl InternalAdditionOps for GraphStorage { type WS<'b> = TGWriteSession<'b>; type AtomicAddEdge<'a> = TGWriteSession<'a>; - fn write_lock(&self) -> Result, Self::Error>{ + fn write_lock(&self) -> Result, Self::Error> { self.mutable()?.write_lock() } diff --git a/raphtory-storage/src/mutation/addition_ops_ext.rs b/raphtory-storage/src/mutation/addition_ops_ext.rs index 88b7edfadc..7af9446e46 100644 --- a/raphtory-storage/src/mutation/addition_ops_ext.rs +++ b/raphtory-storage/src/mutation/addition_ops_ext.rs @@ -167,9 +167,7 @@ impl InternalAdditionOps for TemporalGraph { Extension, >; - fn write_lock( - &self, - ) -> Result, Self::Error> { + fn write_lock(&self) -> Result, Self::Error> { let locked_g = self.write_locked_graph(); Ok(locked_g) } diff --git a/raphtory-storage/src/mutation/deletion_ops.rs b/raphtory-storage/src/mutation/deletion_ops.rs index 4d240314af..f24c092d3b 100644 --- a/raphtory-storage/src/mutation/deletion_ops.rs +++ b/raphtory-storage/src/mutation/deletion_ops.rs @@ -25,7 +25,7 @@ pub trait InternalDeletionOps { ) -> Result<(), Self::Error>; } -impl InternalDeletionOps for db4_graph::TemporalGraph { +impl InternalDeletionOps for db4_graph::TemporalGraph { type Error = MutationError; fn internal_delete_edge( diff --git a/raphtory-storage/src/mutation/property_addition_ops.rs b/raphtory-storage/src/mutation/property_addition_ops.rs index 8ce98b629f..46889338d5 100644 --- a/raphtory-storage/src/mutation/property_addition_ops.rs +++ b/raphtory-storage/src/mutation/property_addition_ops.rs @@ -176,7 +176,7 @@ impl InternalPropertyAdditionOps for TemporalGraph { } } -impl InternalPropertyAdditionOps for db4_graph::TemporalGraph { +impl InternalPropertyAdditionOps for db4_graph::TemporalGraph { type Error = MutationError; fn internal_add_properties( diff --git a/raphtory/src/db/api/view/internal/time_semantics/filtered_edge.rs b/raphtory/src/db/api/view/internal/time_semantics/filtered_edge.rs index c9f3ecd162..80d7c4a784 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/filtered_edge.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/filtered_edge.rs @@ -11,9 +11,7 @@ use raphtory_api::core::{ storage::timeindex::{TimeIndexEntry, TimeIndexOps}, }; use raphtory_storage::graph::edges::{ - edge_ref::EdgeStorageRef, - edge_storage_ops::EdgeStorageOps, - edges::EdgesStorage, + edge_ref::EdgeStorageRef, edge_storage_ops::EdgeStorageOps, edges::EdgesStorage, }; use rayon::iter::ParallelIterator; use std::ops::Range; diff --git a/raphtory/src/io/arrow/df_loaders.rs b/raphtory/src/io/arrow/df_loaders.rs index e4d712c151..faf354bcd9 100644 --- a/raphtory/src/io/arrow/df_loaders.rs +++ b/raphtory/src/io/arrow/df_loaders.rs @@ -1,5 +1,5 @@ use crate::{ - core::entities::{nodes::node_ref::AsNodeRef, LayerIds}, + core::entities::nodes::node_ref::AsNodeRef, db::api::view::StaticGraphViewOps, errors::{into_graph_err, GraphError, LoadError}, io::arrow::{ @@ -11,7 +11,6 @@ use crate::{ serialise::incremental::InternalCache, }; use bytemuck::checked::cast_slice_mut; -use bzip2::write; #[cfg(feature = "python")] use kdam::{Bar, BarBuilder, BarExt}; use raphtory_api::{ @@ -149,9 +148,12 @@ pub(crate) fn load_nodes_from_df< Ok::<(), LoadError>(()) })?; - let (earliest, latest) = write_locked_graph.earliest_latest(); - let update_time = |time:TimeIndexEntry| { let time = time.t(); earliest.update(time); latest.update(time); }; + let update_time = |time: TimeIndexEntry| { + let time = time.t(); + earliest.update(time); + latest.update(time); + }; write_locked_graph.resize_chunks_to_num_nodes(); @@ -170,6 +172,8 @@ pub(crate) fn load_nodes_from_df< .enumerate() { if let Some(mut_node) = shard.resolve_pos(*vid) { + let t = TimeIndexEntry(time, start_id + idx); + update_time(t); let mut writer = shard.writer(); writer.store_node_id_and_node_type(mut_node, 0, gid, *node_type, 0); t_props.clear(); @@ -179,13 +183,7 @@ pub(crate) fn load_nodes_from_df< c_props.extend(const_prop_cols.iter_row(idx)); c_props.extend_from_slice(&shared_constant_properties); writer.update_c_props(mut_node, 0, c_props.drain(..), 0); - writer.add_props( - TimeIndexEntry(time, start_id + idx), - mut_node, - 0, - t_props.drain(..), - 0, - ); + writer.add_props(t, mut_node, 0, t_props.drain(..), 0); }; } Ok::<_, GraphError>(()) @@ -322,10 +320,14 @@ pub(crate) fn load_edges_from_df< let eid_col_shared = atomic_usize_from_mut_slice(cast_slice_mut(&mut eid_col_resolved)); let next_edge_id: Arc = write_locked_graph.num_edges.clone(); - let next_edge_id = || {next_edge_id.fetch_add(1, Ordering::Relaxed)}; + let next_edge_id = || next_edge_id.fetch_add(1, Ordering::Relaxed); let (earliest, latest) = write_locked_graph.earliest_latest(); - let update_time = |time:TimeIndexEntry| { let time = time.t(); earliest.update(time); latest.update(time); }; + let update_time = |time: TimeIndexEntry| { + let time = time.t(); + earliest.update(time); + latest.update(time); + }; write_locked_graph .nodes .par_iter_mut() @@ -369,27 +371,24 @@ pub(crate) fn load_edges_from_df< }); // link the destinations - write_locked_graph - .nodes - .par_iter_mut() - .for_each(|shard| { - for (row, ((((src, (dst, dst_gid)), eid), time), layer)) in src_col_resolved - .iter() - .zip(dst_col_resolved.iter().zip(dst_col.iter())) - .zip(eid_col_resolved.iter()) - .zip(time_col.iter()) - .zip(layer_col_resolved.iter()) - .enumerate() - { - if let Some(dst_pos) = shard.resolve_pos(*dst) { - let t = TimeIndexEntry(time, start_idx + row); - let mut writer = shard.writer(); - writer.store_node_id(dst_pos, 0, dst_gid, 0); - writer.add_static_inbound_edge(dst_pos, *src, eid.with_layer(*layer), 0); - writer.add_inbound_edge(t, dst_pos, *src, eid.with_layer(*layer), 0); - } + write_locked_graph.nodes.par_iter_mut().for_each(|shard| { + for (row, ((((src, (dst, dst_gid)), eid), time), layer)) in src_col_resolved + .iter() + .zip(dst_col_resolved.iter().zip(dst_col.iter())) + .zip(eid_col_resolved.iter()) + .zip(time_col.iter()) + .zip(layer_col_resolved.iter()) + .enumerate() + { + if let Some(dst_pos) = shard.resolve_pos(*dst) { + let t = TimeIndexEntry(time, start_idx + row); + let mut writer = shard.writer(); + writer.store_node_id(dst_pos, 0, dst_gid, 0); + writer.add_static_inbound_edge(dst_pos, *src, eid.with_layer(*layer), 0); + writer.add_inbound_edge(t, dst_pos, *src, eid.with_layer(*layer), 0); } - }); + } + }); write_locked_graph.resize_chunks_to_num_edges(); @@ -404,7 +403,12 @@ pub(crate) fn load_edges_from_df< .zip(dst_col_resolved.iter()) .zip(time_col.iter()) .zip(eid_col_resolved.iter()) - .zip(layer_col_resolved.iter()).zip(eids_exist.iter().map(|exists| exists.load(Ordering::Relaxed))) + .zip(layer_col_resolved.iter()) + .zip( + eids_exist + .iter() + .map(|exists| exists.load(Ordering::Relaxed)), + ) .enumerate() { if let Some(eid_pos) = shard.resolve_pos(*eid) { @@ -418,9 +422,22 @@ pub(crate) fn load_edges_from_df< c_props.extend(const_prop_cols.iter_row(idx)); c_props.extend_from_slice(&shared_constant_properties); - writer.update_c_props(eid_pos, *src, *dst, *layer, c_props.drain(..)); - writer.add_edge(t, Some(eid_pos), *src, *dst, t_props.drain(..), *layer, 0, Some(exists)); - + writer.update_c_props( + eid_pos, + Some((*src, *dst)), + *layer, + c_props.drain(..), + ); + writer.add_edge( + t, + Some(eid_pos), + *src, + *dst, + t_props.drain(..), + *layer, + 0, + Some(exists), + ); } } Ok::<(), GraphError>(()) @@ -525,11 +542,6 @@ pub(crate) fn load_node_props_from_df< let cache = graph.get_cache(); let mut write_locked_graph = graph.write_lock().map_err(into_graph_err)?; - let cache_shards = cache.map(|cache| { - (0..write_locked_graph.num_shards()) - .map(|_| cache.fork()) - .collect::>() - }); for chunk in df_view.chunks { let df = chunk?; @@ -568,14 +580,12 @@ pub(crate) fn load_node_props_from_df< Ok::<(), LoadError>(()) })?; - write_locked_graph - .nodes - .resize(write_locked_graph.num_nodes()); + write_locked_graph.resize_chunks_to_num_nodes(); write_locked_graph .nodes .par_iter_mut() - .try_for_each(|mut shard| { + .try_for_each(|shard| { let mut c_props = vec![]; for (idx, ((vid, node_type), gid)) in node_col_resolved @@ -584,23 +594,14 @@ pub(crate) fn load_node_props_from_df< .zip(node_col.iter()) .enumerate() { - let shard_id = shard.shard_id(); - if let Some(mut_node) = shard.get_mut(*vid) { - mut_node.init(*vid, gid); - mut_node.node_type = *node_type; + if let Some(mut_node) = shard.resolve_pos(*vid) { + let mut writer = shard.writer(); + writer.store_node_id_and_node_type(mut_node, 0, gid, *node_type, 0); c_props.clear(); c_props.extend(const_prop_cols.iter_row(idx)); c_props.extend_from_slice(&shared_constant_properties); - - if let Some(caches) = cache_shards.as_ref() { - let cache = &caches[shard_id]; - cache.add_node_cprops(*vid, &c_props); - } - - for (id, prop) in c_props.drain(..) { - mut_node.add_constant_prop(id, prop)?; - } + writer.update_c_props(mut_node, 0, c_props.drain(..), 0); }; } Ok::<_, GraphError>(()) @@ -653,13 +654,7 @@ pub(crate) fn load_edges_props_from_df< let mut dst_col_resolved = vec![]; let mut eid_col_resolved = vec![]; - let cache = graph.get_cache(); let mut write_locked_graph = graph.write_lock().map_err(into_graph_err)?; - let cache_shards = cache.map(|cache| { - (0..write_locked_graph.num_shards()) - .map(|_| cache.fork()) - .collect::>() - }); let g = write_locked_graph.graph; @@ -711,9 +706,12 @@ pub(crate) fn load_edges_props_from_df< Ok::<(), LoadError>(()) })?; + write_locked_graph.resize_chunks_to_num_nodes(); + // resolve all the edges eid_col_resolved.resize_with(df.len(), Default::default); let eid_col_shared = atomic_usize_from_mut_slice(cast_slice_mut(&mut eid_col_resolved)); + write_locked_graph .nodes .par_iter_mut() @@ -723,10 +721,10 @@ pub(crate) fn load_edges_props_from_df< .zip(dst_col_resolved.iter()) .enumerate() { - if let Some(src_node) = shard.get(*src) { - // we know this is here - let EID(eid) = src_node - .find_edge_eid(*dst, &LayerIds::All) + if let Some(src_node) = shard.resolve_pos(*src) { + let writer = shard.writer(); + let EID(eid) = writer + .get_out_edge(src_node, *dst, 0) .ok_or(LoadError::MissingEdgeError(*src, *dst))?; eid_col_shared[row].store(eid, Ordering::Relaxed); } @@ -737,45 +735,24 @@ pub(crate) fn load_edges_props_from_df< write_locked_graph .edges .par_iter_mut() - .try_for_each(|mut shard| { + .try_for_each(|shard| { let mut c_props = vec![]; for (idx, (eid, layer)) in eid_col_resolved .iter() .zip(layer_col_resolved.iter()) .enumerate() { - let shard_id = shard.shard_id(); - if let Some(mut edge) = shard.get_mut(*eid) { + if let Some(eid_pos) = shard.resolve_pos(*eid) { + let mut writer = shard.writer(); c_props.clear(); c_props.extend(const_prop_cols.iter_row(idx)); c_props.extend_from_slice(&shared_constant_properties); - - if let Some(caches) = cache_shards.as_ref() { - let cache = &caches[shard_id]; - cache.add_edge_cprops(*eid, *layer, &c_props); - } - - if !c_props.is_empty() { - let edge_layer = edge.layer_mut(*layer); - - for (id, prop) in c_props.drain(..) { - edge_layer.update_constant_prop(id, prop)?; - } - } + writer.update_c_props(eid_pos, None, *layer, c_props.drain(..)); } } Ok::<(), GraphError>(()) })?; - if let Some(cache) = cache { - cache.write()?; - } - if let Some(cache_shards) = cache_shards.as_ref() { - for cache in cache_shards { - cache.write()?; - } - } - #[cfg(feature = "python")] let _ = pb.update(df.len()); } From 2c02059657c274db084151893ae139cdbb320357 Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Mon, 30 Jun 2025 09:30:29 +0100 Subject: [PATCH 049/321] fix update_c_props for edges on the writers --- db4-storage/src/pages/edge_page/writer.rs | 7 ++----- db4-storage/src/pages/mod.rs | 7 +------ raphtory/src/io/arrow/df_loaders.rs | 13 +++++-------- 3 files changed, 8 insertions(+), 19 deletions(-) diff --git a/db4-storage/src/pages/edge_page/writer.rs b/db4-storage/src/pages/edge_page/writer.rs index 252f0d0eaa..0e0dcf6d09 100644 --- a/db4-storage/src/pages/edge_page/writer.rs +++ b/db4-storage/src/pages/edge_page/writer.rs @@ -105,14 +105,11 @@ impl<'a, MP: DerefMut, ES: EdgeSegmentOps> EdgeWriter<' pub fn update_c_props( &mut self, edge_pos: LocalPOS, - src_dst: Option<(VID, VID)>, + src: impl Into, + dst: impl Into, layer_id: usize, props: impl IntoIterator, ) { - let (src, dst) = src_dst - .or_else(|| self.page.get_edge(edge_pos, layer_id, self.writer.deref())) - .expect("Edge must exist for updating properties"); - self.writer .update_const_properties(edge_pos, src, dst, layer_id, props); } diff --git a/db4-storage/src/pages/mod.rs b/db4-storage/src/pages/mod.rs index d653daed31..6b35b532f4 100644 --- a/db4-storage/src/pages/mod.rs +++ b/db4-storage/src/pages/mod.rs @@ -267,12 +267,7 @@ impl< .expect("Internal Error, EID should be checked at this point!"); let prop_writer = PropsMetaWriter::constant(self.edge_meta(), props.into_iter())?; - edge_writer.update_c_props( - edge_pos, - Some((src, dst)), - layer, - prop_writer.into_props_const()?, - ); + edge_writer.update_c_props(edge_pos, src, dst, layer, prop_writer.into_props_const()?); Ok(()) } diff --git a/raphtory/src/io/arrow/df_loaders.rs b/raphtory/src/io/arrow/df_loaders.rs index faf354bcd9..00b4003a1d 100644 --- a/raphtory/src/io/arrow/df_loaders.rs +++ b/raphtory/src/io/arrow/df_loaders.rs @@ -422,12 +422,7 @@ pub(crate) fn load_edges_from_df< c_props.extend(const_prop_cols.iter_row(idx)); c_props.extend_from_slice(&shared_constant_properties); - writer.update_c_props( - eid_pos, - Some((*src, *dst)), - *layer, - c_props.drain(..), - ); + writer.update_c_props(eid_pos, *src, *dst, *layer, c_props.drain(..)); writer.add_edge( t, Some(eid_pos), @@ -737,9 +732,11 @@ pub(crate) fn load_edges_props_from_df< .par_iter_mut() .try_for_each(|shard| { let mut c_props = vec![]; - for (idx, (eid, layer)) in eid_col_resolved + for (idx, (((eid, layer), src), dst)) in eid_col_resolved .iter() .zip(layer_col_resolved.iter()) + .zip(&src_col_resolved) + .zip(&dst_col_resolved) .enumerate() { if let Some(eid_pos) = shard.resolve_pos(*eid) { @@ -747,7 +744,7 @@ pub(crate) fn load_edges_props_from_df< c_props.clear(); c_props.extend(const_prop_cols.iter_row(idx)); c_props.extend_from_slice(&shared_constant_properties); - writer.update_c_props(eid_pos, None, *layer, c_props.drain(..)); + writer.update_c_props(eid_pos, *src, *dst, *layer, c_props.drain(..)); } } Ok::<(), GraphError>(()) From b372924faac9fece64b2e1b791c5ab71f0685bee Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Mon, 30 Jun 2025 14:16:50 +0100 Subject: [PATCH 050/321] materialize compiles --- db4-graph/Cargo.toml | 1 + db4-graph/src/lib.rs | 117 ++++++----- db4-storage/src/pages/edge_page/writer.rs | 15 +- db4-storage/src/pages/edge_store.rs | 30 ++- db4-storage/src/pages/layer_counter.rs | 35 +++- db4-storage/src/pages/locked/edges.rs | 6 +- db4-storage/src/pages/locked/nodes.rs | 6 +- db4-storage/src/pages/mod.rs | 58 ++++-- db4-storage/src/pages/node_page/writer.rs | 27 ++- db4-storage/src/pages/node_store.rs | 23 ++- .../entities/properties/prop/prop_type.rs | 4 +- .../src/graph/nodes/node_entry.rs | 12 +- .../src/graph/nodes/node_storage_ops.rs | 18 +- .../src/mutation/addition_ops_ext.rs | 9 +- .../graph/storage_ops/time_semantics.rs | 19 +- raphtory/src/db/api/storage/storage.rs | 114 +--------- raphtory/src/db/api/view/graph.rs | 195 ++++++++++-------- raphtory/src/io/arrow/df_loaders.rs | 24 +-- 18 files changed, 394 insertions(+), 319 deletions(-) diff --git a/db4-graph/Cargo.toml b/db4-graph/Cargo.toml index c8fcb61858..3ac8bd6f93 100644 --- a/db4-graph/Cargo.toml +++ b/db4-graph/Cargo.toml @@ -17,3 +17,4 @@ storage.workspace = true raphtory-api.workspace = true raphtory-core.workspace = true parking_lot.workspace = true +uuid.workspace = true diff --git a/db4-graph/src/lib.rs b/db4-graph/src/lib.rs index b5bb6ed69c..7eedade131 100644 --- a/db4-graph/src/lib.rs +++ b/db4-graph/src/lib.rs @@ -1,4 +1,5 @@ use std::{ + env::temp_dir, path::{Path, PathBuf}, sync::{ atomic::{self, AtomicUsize}, @@ -6,7 +7,6 @@ use std::{ }, }; -// use crate::entries::node::UnlockedNodeEntry; use raphtory_api::core::{ entities::{self, properties::meta::Meta}, input::input_node::InputNode, @@ -17,16 +17,18 @@ use raphtory_core::{ graph::{ logical_to_physical::{InvalidNodeId, Mapping}, tgraph::InvalidLayer, - timer::{MaxCounter, MinCounter, TimeCounterTrait}, }, nodes::node_ref::NodeRef, properties::graph_meta::GraphMeta, GidRef, LayerIds, EID, VID, }, - storage::timeindex::{AsTime, TimeIndexEntry}, + storage::timeindex::TimeIndexEntry, }; use storage::{ - pages::locked::{edges::WriteLockedEdgePages, nodes::WriteLockedNodePages}, + pages::{ + layer_counter::GraphStats, + locked::{edges::WriteLockedEdgePages, nodes::WriteLockedNodePages}, + }, persist::strategy::PersistentStrategy, Extension, Layer, ReadLockedLayer, ES, NS, }; @@ -34,35 +36,48 @@ use storage::{ pub mod entries; pub mod mutation; +const DEFAULT_MAX_PAGE_LEN_NODES: usize = 1000; +const DEFAULT_MAX_PAGE_LEN_EDGES: usize = 1000; + #[derive(Debug)] pub struct TemporalGraph { - graph_dir: PathBuf, // mapping between logical and physical ids pub logical_to_physical: Mapping, pub node_count: AtomicUsize, - - max_page_len_nodes: usize, - max_page_len_edges: usize, - storage: Arc>, + pub graph_meta: Arc, + graph_dir: PathBuf, +} - edge_meta: Arc, - node_meta: Arc, - - event_counter: AtomicUsize, - graph_meta: Arc, - - pub earliest: Arc, - pub latest: Arc, +fn random_temp_dir() -> PathBuf { + temp_dir().join(format!("raphtory-{}", uuid::Uuid::new_v4())) } impl, ES = ES>> TemporalGraph { - // pub fn node(&self, vid: VID) -> UnlockedNodeEntry { - // UnlockedNodeEntry::new(vid, self) - // } + pub fn new(path: Option) -> Self { + Self::new_with_meta(path, Meta::new(), Meta::new()) + } + + pub fn new_with_meta(path: Option, node_meta: Meta, edge_meta: Meta) -> Self { + let graph_dir = path.unwrap_or_else(random_temp_dir); + let storage = Layer::new_with_meta( + graph_dir.clone(), + DEFAULT_MAX_PAGE_LEN_NODES, + DEFAULT_MAX_PAGE_LEN_EDGES, + node_meta, + edge_meta, + ); + Self { + graph_dir, + logical_to_physical: Mapping::new(), + node_count: AtomicUsize::new(0), + storage: Arc::new(storage), + graph_meta: Arc::new(GraphMeta::default()), + } + } pub fn read_event_counter(&self) -> usize { - self.event_counter.load(atomic::Ordering::Relaxed) + self.storage().read_event_id() } pub fn storage(&self) -> &Arc> { @@ -103,24 +118,26 @@ impl, ES = ES>> TemporalGraph { self.storage.read_locked() } - pub fn edge_meta(&self) -> &Arc { - &self.edge_meta + pub fn edge_meta(&self) -> &Meta { + self.storage().edge_meta() } - pub fn node_meta(&self) -> &Arc { - &self.node_meta + pub fn node_meta(&self) -> &Meta { + self.storage().node_meta() } pub fn graph_dir(&self) -> &Path { &self.graph_dir } - pub fn max_page_len_nodes(&self) -> usize { - self.max_page_len_nodes + #[inline] + pub fn graph_earliest_time(&self) -> Option { + Some(self.storage().earliest()).filter(|t| *t != i64::MAX) } - pub fn max_page_len_edges(&self) -> usize { - self.max_page_len_edges + #[inline] + pub fn graph_latest_time(&self) -> Option { + Some(self.storage().latest()).filter(|t| *t != i64::MIN) } pub fn layer_ids(&self, key: entities::Layer) -> Result { @@ -128,19 +145,19 @@ impl, ES = ES>> TemporalGraph { entities::Layer::None => Ok(LayerIds::None), entities::Layer::All => Ok(LayerIds::All), entities::Layer::Default => Ok(LayerIds::One(0)), - entities::Layer::One(id) => match self.edge_meta.get_layer_id(&id) { + entities::Layer::One(id) => match self.edge_meta().get_layer_id(&id) { Some(id) => Ok(LayerIds::One(id)), None => Err(InvalidLayer::new( id, - Self::get_valid_layers(&self.edge_meta), + Self::get_valid_layers(self.edge_meta()), )), }, entities::Layer::Multiple(ids) => { let mut new_layers = ids .iter() .map(|id| { - self.edge_meta.get_layer_id(id).ok_or_else(|| { - InvalidLayer::new(id.clone(), Self::get_valid_layers(&self.edge_meta)) + self.edge_meta().get_layer_id(id).ok_or_else(|| { + InvalidLayer::new(id.clone(), Self::get_valid_layers(self.edge_meta())) }) }) .collect::, InvalidLayer>>()?; @@ -175,14 +192,14 @@ impl, ES = ES>> TemporalGraph { entities::Layer::None => LayerIds::None, entities::Layer::All => LayerIds::All, entities::Layer::Default => LayerIds::One(0), - entities::Layer::One(id) => match self.edge_meta.get_layer_id(&id) { + entities::Layer::One(id) => match self.edge_meta().get_layer_id(&id) { Some(id) => LayerIds::One(id), None => LayerIds::None, }, entities::Layer::Multiple(ids) => { let mut new_layers = ids .iter() - .flat_map(|id| self.edge_meta.get_layer_id(id)) + .flat_map(|id| self.edge_meta().get_layer_id(id)) .collect::>(); let num_layers = self.num_layers(); let num_new_layers = new_layers.len(); @@ -204,6 +221,10 @@ impl, ES = ES>> TemporalGraph { pub fn write_locked_graph<'a>(&'a self) -> WriteLockedGraph<'a, EXT> { WriteLockedGraph::new(self) } + + pub fn update_time(&self, earliest: TimeIndexEntry) { + todo!() + } } pub struct WriteLockedGraph<'a, EXT> { @@ -231,39 +252,39 @@ impl<'a, EXT: PersistentStrategy, ES = ES>> WriteLockedGraph<' }) } - pub fn next_edge_id(&self) -> usize { - self.graph - .event_counter - .fetch_add(1, atomic::Ordering::Relaxed) - } - pub fn num_nodes(&self) -> usize { self.num_nodes.load(atomic::Ordering::Relaxed) } + pub fn num_edges(&self) -> usize { + self.num_edges.load(atomic::Ordering::Relaxed) + } + pub fn resolve_node_type(&self, node_type: Option<&str>) -> MaybeNew { node_type - .map(|node_type| self.graph.node_meta.get_or_create_node_type_id(node_type)) + .map(|node_type| self.graph.node_meta().get_or_create_node_type_id(node_type)) .unwrap_or_else(|| MaybeNew::Existing(0)) } - pub fn resize_chunks_to_num_nodes(&mut self) { - let num_nodes = self.num_nodes(); + pub fn resize_chunks_to_num_nodes(&mut self, num_nodes: usize) { let (chunks_needed, _) = self.graph.storage.nodes().resolve_pos(VID(num_nodes - 1)); self.graph.storage().nodes().grow(chunks_needed + 1); std::mem::take(&mut self.nodes); self.nodes = self.graph.storage.nodes().write_locked().into(); } - pub fn resize_chunks_to_num_edges(&mut self) { - let num_edges = self.graph.internal_num_edges(); + pub fn resize_chunks_to_num_edges(&mut self, num_edges: usize) { let (chunks_needed, _) = self.graph.storage.edges().resolve_pos(EID(num_edges - 1)); self.graph.storage().edges().grow(chunks_needed + 1); std::mem::take(&mut self.edges); self.edges = self.graph.storage.edges().write_locked().into(); } - pub fn earliest_latest(&self) -> (Arc, Arc) { - (self.graph.earliest.clone(), self.graph.latest.clone()) + pub fn edge_stats(&self) -> &Arc { + self.graph.storage().edges().stats() + } + + pub fn node_stats(&self) -> &Arc { + self.graph.storage().nodes().stats() } } diff --git a/db4-storage/src/pages/edge_page/writer.rs b/db4-storage/src/pages/edge_page/writer.rs index 0e0dcf6d09..cdc55e4985 100644 --- a/db4-storage/src/pages/edge_page/writer.rs +++ b/db4-storage/src/pages/edge_page/writer.rs @@ -1,7 +1,7 @@ use std::ops::DerefMut; use crate::{ - LocalPOS, api::edges::EdgeSegmentOps, pages::layer_counter::LayerCounter, + LocalPOS, api::edges::EdgeSegmentOps, pages::layer_counter::GraphStats, segments::edge::MemEdgeSegment, }; use raphtory_api::core::entities::{VID, properties::prop::Prop}; @@ -10,11 +10,11 @@ use raphtory_core::storage::timeindex::AsTime; pub struct EdgeWriter<'a, MP: DerefMut, ES: EdgeSegmentOps> { pub page: &'a ES, pub writer: MP, - pub global_num_edges: &'a LayerCounter, + pub global_num_edges: &'a GraphStats, } impl<'a, MP: DerefMut, ES: EdgeSegmentOps> EdgeWriter<'a, MP, ES> { - pub fn new(global_num_edges: &'a LayerCounter, page: &'a ES, writer: MP) -> Self { + pub fn new(global_num_edges: &'a GraphStats, page: &'a ES, writer: MP) -> Self { Self { page, writer, @@ -54,13 +54,20 @@ impl<'a, MP: DerefMut, ES: EdgeSegmentOps> EdgeWriter<' pub fn delete_edge( &mut self, t: T, - edge_pos: LocalPOS, + edge_pos: Option, src: impl Into, dst: impl Into, layer_id: usize, lsn: u64, + exists_hint: Option, // used when edge_pos is Some but the is not counted, this is used in the bulk loader ) { self.writer.as_mut()[layer_id].set_lsn(lsn); + + if exists_hint == Some(false) && edge_pos.is_some() { + self.new_local_pos(layer_id); // increment the counts, this is triggered from the bulk loader + } + + let edge_pos = edge_pos.unwrap_or_else(|| self.new_local_pos(layer_id)); self.writer .delete_edge_internal(t, edge_pos, src, dst, layer_id); } diff --git a/db4-storage/src/pages/edge_store.rs b/db4-storage/src/pages/edge_store.rs index 06ce20f956..5da16f05bc 100644 --- a/db4-storage/src/pages/edge_store.rs +++ b/db4-storage/src/pages/edge_store.rs @@ -1,10 +1,7 @@ use std::{ collections::HashMap, path::{Path, PathBuf}, - sync::{ - Arc, - atomic::{self, AtomicUsize}, - }, + sync::Arc, }; use super::{edge_page::writer::EdgeWriter, resolve_pos}; @@ -13,7 +10,7 @@ use crate::{ api::edges::{EdgeSegmentOps, LockedESegment}, error::DBV4Error, pages::{ - layer_counter::LayerCounter, + layer_counter::GraphStats, locked::edges::{LockedEdgePage, WriteLockedEdgePages}, }, segments::edge::MemEdgeSegment, @@ -31,7 +28,7 @@ const N: usize = 32; #[derive(Debug)] pub struct EdgeStorageInner { pages: boxcar::Vec>, - layer_counter: LayerCounter, + layer_counter: Arc, free_pages: Box<[RwLock; N]>, edges_path: PathBuf, max_page_len: usize, @@ -99,19 +96,32 @@ impl, EXT: Clone + Send + Sync> EdgeStorageI &self.prop_meta } - pub fn new(edges_path: impl AsRef, max_page_len: usize, ext: EXT) -> Self { + pub fn stats(&self) -> &Arc { + &self.layer_counter + } + + pub fn new_with_meta( + edges_path: impl AsRef, + max_page_len: usize, + edge_meta: Meta, + ext: EXT, + ) -> Self { let free_pages = (0..N).map(RwLock::new).collect::>(); Self { pages: boxcar::Vec::new(), - layer_counter: LayerCounter::new(), + layer_counter: GraphStats::new().into(), free_pages: free_pages.try_into().unwrap(), edges_path: edges_path.as_ref().to_path_buf(), max_page_len, - prop_meta: Arc::new(Meta::new()), + prop_meta: edge_meta.into(), ext, } } + pub fn new(edges_path: impl AsRef, max_page_len: usize, ext: EXT) -> Self { + Self::new_with_meta(edges_path, max_page_len, Meta::new(), ext) + } + pub fn pages(&self) -> &boxcar::Vec> { &self.pages } @@ -236,7 +246,7 @@ impl, EXT: Clone + Send + Sync> EdgeStorageI pages, edges_path: edges_path.to_path_buf(), max_page_len, - layer_counter: LayerCounter::from(layer_counts), + layer_counter: GraphStats::from(layer_counts).into(), free_pages: free_pages.try_into().unwrap(), prop_meta: meta, ext, diff --git a/db4-storage/src/pages/layer_counter.rs b/db4-storage/src/pages/layer_counter.rs index 23aa68a5c0..8386849244 100644 --- a/db4-storage/src/pages/layer_counter.rs +++ b/db4-storage/src/pages/layer_counter.rs @@ -1,19 +1,27 @@ use std::sync::atomic::AtomicUsize; +use raphtory_core::entities::graph::timer::{MaxCounter, MinCounter, TimeCounterTrait}; + #[derive(Debug)] -pub struct LayerCounter { +pub struct GraphStats { layers: boxcar::Vec, + earliest: MinCounter, + latest: MaxCounter, } -impl> From for LayerCounter { +impl> From for GraphStats { fn from(iter: I) -> Self { let counts = iter.into_iter().map(|c| AtomicUsize::new(c)).collect(); let layers = boxcar::Vec::from(counts); - Self { layers } + Self { + layers, + earliest: MinCounter::new(), + latest: MaxCounter::new(), + } } } -impl LayerCounter { +impl GraphStats { pub fn new() -> Self { let layers = boxcar::Vec::new(); for _ in 0..16 { @@ -23,13 +31,30 @@ impl LayerCounter { std::thread::yield_now(); } } - Self { layers } + Self { + layers, + earliest: MinCounter::new(), + latest: MaxCounter::new(), + } } pub fn len(&self) -> usize { self.layers.count() } + pub fn update_time(&self, t: i64) { + self.earliest.update(t); + self.latest.update(t); + } + + pub fn earliest(&self) -> i64 { + self.earliest.get() + } + + pub fn latest(&self) -> i64 { + self.latest.get() + } + pub fn increment(&self, layer_id: usize) -> usize { let counter = self.get_or_create_layer(layer_id); counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed) diff --git a/db4-storage/src/pages/locked/edges.rs b/db4-storage/src/pages/locked/edges.rs index 794cdba72e..79eb9f4901 100644 --- a/db4-storage/src/pages/locked/edges.rs +++ b/db4-storage/src/pages/locked/edges.rs @@ -3,7 +3,7 @@ use std::{ops::DerefMut, sync::atomic::AtomicUsize}; use crate::{ LocalPOS, api::edges::EdgeSegmentOps, - pages::{edge_page::writer::EdgeWriter, layer_counter::LayerCounter, resolve_pos}, + pages::{edge_page::writer::EdgeWriter, layer_counter::GraphStats, resolve_pos}, segments::edge::MemEdgeSegment, }; use parking_lot::RwLockWriteGuard; @@ -14,7 +14,7 @@ pub struct LockedEdgePage<'a, ES> { page_id: usize, max_page_len: usize, page: &'a ES, - num_edges: &'a LayerCounter, + num_edges: &'a GraphStats, lock: RwLockWriteGuard<'a, MemEdgeSegment>, } @@ -23,7 +23,7 @@ impl<'a, EXT, ES: EdgeSegmentOps> LockedEdgePage<'a, ES> { page_id: usize, max_page_len: usize, page: &'a ES, - num_edges: &'a LayerCounter, + num_edges: &'a GraphStats, lock: RwLockWriteGuard<'a, MemEdgeSegment>, ) -> Self { Self { diff --git a/db4-storage/src/pages/locked/nodes.rs b/db4-storage/src/pages/locked/nodes.rs index 963f754bf8..57e02ad0ad 100644 --- a/db4-storage/src/pages/locked/nodes.rs +++ b/db4-storage/src/pages/locked/nodes.rs @@ -1,7 +1,7 @@ use crate::{ LocalPOS, api::nodes::NodeSegmentOps, - pages::{layer_counter::LayerCounter, node_page::writer::NodeWriter, resolve_pos}, + pages::{layer_counter::GraphStats, node_page::writer::NodeWriter, resolve_pos}, segments::node::MemNodeSegment, }; use parking_lot::RwLockWriteGuard; @@ -12,7 +12,7 @@ use std::ops::DerefMut; pub struct LockedNodePage<'a, NS> { page_id: usize, max_page_len: usize, - layer_counter: &'a LayerCounter, + layer_counter: &'a GraphStats, page: &'a NS, lock: RwLockWriteGuard<'a, MemNodeSegment>, } @@ -20,7 +20,7 @@ pub struct LockedNodePage<'a, NS> { impl<'a, EXT, NS: NodeSegmentOps> LockedNodePage<'a, NS> { pub fn new( page_id: usize, - layer_counter: &'a LayerCounter, + layer_counter: &'a GraphStats, max_page_len: usize, page: &'a NS, lock: RwLockWriteGuard<'a, MemNodeSegment>, diff --git a/db4-storage/src/pages/mod.rs b/db4-storage/src/pages/mod.rs index 6b35b532f4..a227071cfd 100644 --- a/db4-storage/src/pages/mod.rs +++ b/db4-storage/src/pages/mod.rs @@ -3,7 +3,7 @@ use std::{ path::Path, sync::{ Arc, - atomic::{self, AtomicI64, AtomicUsize}, + atomic::{self, AtomicUsize}, }, }; @@ -29,7 +29,7 @@ use raphtory_api::core::{ }; use raphtory_core::{ entities::{EID, ELID, VID}, - storage::timeindex::{AsTime, TimeIndexEntry}, + storage::timeindex::TimeIndexEntry, utils::time::{InputTime, TryIntoInputTime}, }; use serde::{Deserialize, Serialize}; @@ -53,8 +53,6 @@ pub const NODE_TYPE_PROP_KEY: &str = "_raphtory_node_type"; pub struct GraphStore { nodes: Arc>, edges: Arc>, - earliest: AtomicI64, - latest: AtomicI64, event_id: AtomicUsize, _ext: EXT, } @@ -98,16 +96,23 @@ impl< self.edges.edge_meta() } + pub fn set_event_id(&self, event_id: usize) { + self.event_id.store(event_id, atomic::Ordering::Relaxed); + } + pub fn node_meta(&self) -> &Meta { self.nodes.node_meta() } pub fn earliest(&self) -> i64 { - self.earliest.load(atomic::Ordering::Relaxed) + self.nodes + .stats() + .earliest() + .min(self.edges.stats().earliest()) } pub fn latest(&self) -> i64 { - self.latest.load(atomic::Ordering::Relaxed) + self.nodes.stats().latest().max(self.edges.stats().latest()) } pub fn load(graph_dir: impl AsRef) -> Result { @@ -132,37 +137,37 @@ impl< ext.clone(), )?); - let earliest = AtomicI64::new(edges.earliest().map(|t| t.t()).unwrap_or_default()); - let latest = AtomicI64::new(edges.latest().map(|t| t.t()).unwrap_or_default()); let t_len = edges.t_len(); Ok(Self { nodes, edges, - earliest, - latest, event_id: AtomicUsize::new(t_len), _ext: ext, }) } - pub fn new( + pub fn new_with_meta( graph_dir: impl AsRef, max_page_len_nodes: usize, max_page_len_edges: usize, + node_meta: Meta, + edge_meta: Meta, ) -> Self { let nodes_path = graph_dir.as_ref().join("nodes"); let edges_path = graph_dir.as_ref().join("edges"); let ext = EXT::default(); - let nodes = Arc::new(NodeStorageInner::new( + let nodes = Arc::new(NodeStorageInner::new_with_meta( nodes_path, max_page_len_nodes, + node_meta.into(), ext.clone(), )); - let edges = Arc::new(EdgeStorageInner::new( + let edges = Arc::new(EdgeStorageInner::new_with_meta( edges_path, max_page_len_edges, + edge_meta.into(), ext.clone(), )); // Reserve node_type as a const prop on init @@ -176,18 +181,31 @@ impl< max_page_len_edges, }; - write_graph_meta(&graph_dir, graph_meta); + write_graph_meta(&graph_dir, graph_meta) + .expect("Unrecoverable! Failed to write graph meta"); Self { - nodes: nodes.clone(), - edges: edges.clone(), - earliest: AtomicI64::new(0), - latest: AtomicI64::new(0), + nodes, + edges, event_id: AtomicUsize::new(0), _ext: ext, } } + pub fn new( + graph_dir: impl AsRef, + max_page_len_nodes: usize, + max_page_len_edges: usize, + ) -> Self { + Self::new_with_meta( + graph_dir, + max_page_len_nodes, + max_page_len_edges, + Meta::new(), + Meta::new(), + ) + } + pub fn add_edge( &self, t: T, @@ -253,6 +271,10 @@ impl< Ok(t) } + pub fn read_event_id(&self) -> usize { + self.event_id.load(atomic::Ordering::Relaxed) + } + pub fn update_edge_const_props>( &self, eid: impl Into, diff --git a/db4-storage/src/pages/node_page/writer.rs b/db4-storage/src/pages/node_page/writer.rs index 154c6dd5ec..83e6021631 100644 --- a/db4-storage/src/pages/node_page/writer.rs +++ b/db4-storage/src/pages/node_page/writer.rs @@ -1,11 +1,11 @@ use crate::{ - LocalPOS, api::nodes::NodeSegmentOps, pages::layer_counter::LayerCounter, + LocalPOS, api::nodes::NodeSegmentOps, pages::layer_counter::GraphStats, segments::node::MemNodeSegment, }; use raphtory_api::core::entities::{EID, VID, properties::prop::Prop}; use raphtory_core::{ entities::{ELID, GidRef}, - storage::timeindex::AsTime, + storage::timeindex::{AsTime, TimeIndexEntry}, }; use std::ops::DerefMut; @@ -13,11 +13,11 @@ use std::ops::DerefMut; pub struct NodeWriter<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> { pub page: &'a NS, pub writer: MP, // TODO: rename to m_segment - pub l_counter: &'a LayerCounter, + pub l_counter: &'a GraphStats, } impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> NodeWriter<'a, MP, NS> { - pub fn new(page: &'a NS, global_num_nodes: &'a LayerCounter, writer: MP) -> Self { + pub fn new(page: &'a NS, global_num_nodes: &'a GraphStats, writer: MP) -> Self { Self { page, writer, @@ -55,6 +55,9 @@ impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> NodeWri lsn: u64, ) { let src_pos = src_pos.into(); + if let Some(t) = t { + self.l_counter.update_time(t.t()); + } let e_id = e_id.into(); let layer_id = e_id.layer(); @@ -95,6 +98,9 @@ impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> NodeWri lsn: u64, ) { let e_id = e_id.into(); + if let Some(t) = t { + self.l_counter.update_time(t.t()); + } let layer = e_id.layer(); let dst_pos = dst_pos.into(); let is_new_node = self.writer.add_inbound_edge(t, dst_pos, src, e_id, lsn); @@ -113,6 +119,7 @@ impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> NodeWri lsn: u64, ) { self.writer.as_mut()[layer_id].set_lsn(lsn); + self.l_counter.update_time(t.t()); self.writer.add_props(t, pos, layer_id, props); } @@ -129,6 +136,7 @@ impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> NodeWri pub fn update_timestamp(&mut self, t: T, pos: LocalPOS, e_id: ELID, lsn: u64) { let layer_id = e_id.layer(); + self.l_counter.update_time(t.t()); self.writer.as_mut()[layer_id].set_lsn(lsn); self.writer.update_timestamp(t, pos, e_id); } @@ -162,6 +170,17 @@ impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> NodeWri pub fn store_node_id(&mut self, pos: LocalPOS, layer_id: usize, gid: GidRef<'_>, lsn: u64) { self.update_c_props(pos, layer_id, [(1, gid.into())], lsn); } + + pub fn update_deletion_time( + &self, + t: TimeIndexEntry, + node: LocalPOS, + other: VID, + layer: ELID, + lsn: u64, + ) { + todo!() + } } impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> Drop diff --git a/db4-storage/src/pages/node_store.rs b/db4-storage/src/pages/node_store.rs index d846dbe9ea..df765b211b 100644 --- a/db4-storage/src/pages/node_store.rs +++ b/db4-storage/src/pages/node_store.rs @@ -4,7 +4,7 @@ use crate::{ api::nodes::{LockedNSSegment, NodeSegmentOps}, error::DBV4Error, pages::{ - layer_counter::LayerCounter, + layer_counter::GraphStats, locked::nodes::{LockedNodePage, WriteLockedNodePages}, }, segments::node::MemNodeSegment, @@ -24,7 +24,7 @@ use std::{ #[derive(Debug)] pub struct NodeStorageInner { pages: boxcar::Vec>, - layer_counter: LayerCounter, + layer_counter: Arc, nodes_path: PathBuf, max_page_len: usize, prop_meta: Arc, @@ -88,12 +88,21 @@ impl, EXT: Clone> NodeStorageInner } pub fn new(nodes_path: impl AsRef, max_page_len: usize, ext: EXT) -> Self { + Self::new_with_meta(nodes_path, max_page_len, Meta::new(), ext) + } + + pub fn new_with_meta( + nodes_path: impl AsRef, + max_page_len: usize, + node_meta: Meta, + ext: EXT, + ) -> Self { Self { pages: boxcar::Vec::new(), - layer_counter: LayerCounter::new(), + layer_counter: GraphStats::new().into(), nodes_path: nodes_path.as_ref().to_path_buf(), max_page_len, - prop_meta: Arc::new(Meta::new()), + prop_meta: node_meta.into(), ext, } } @@ -154,6 +163,10 @@ impl, EXT: Clone> NodeStorageInner self.layer_counter.get(layer_id) } + pub fn stats(&self) -> &Arc { + &self.layer_counter + } + pub fn pages(&self) -> &boxcar::Vec> { &self.pages } @@ -231,7 +244,7 @@ impl, EXT: Clone> NodeStorageInner pages, nodes_path: nodes_path.to_path_buf(), max_page_len, - layer_counter: LayerCounter::from(layer_counts), + layer_counter: GraphStats::from(layer_counts).into(), prop_meta: meta, ext, }) diff --git a/raphtory-api/src/core/entities/properties/prop/prop_type.rs b/raphtory-api/src/core/entities/properties/prop/prop_type.rs index 2f31ea4c62..e1530fa1ce 100644 --- a/raphtory-api/src/core/entities/properties/prop/prop_type.rs +++ b/raphtory-api/src/core/entities/properties/prop/prop_type.rs @@ -308,9 +308,7 @@ pub fn check_for_unification(l: &PropType, r: &PropType) -> Option { .and_then(|l_d_type| check_for_unification(r_d_type, l_d_type)) })); for check in inner_checks { - if !check { - return Some(false); - } else { + if check { return Some(true); } } diff --git a/raphtory-storage/src/graph/nodes/node_entry.rs b/raphtory-storage/src/graph/nodes/node_entry.rs index 360be13dcc..80c6dab200 100644 --- a/raphtory-storage/src/graph/nodes/node_entry.rs +++ b/raphtory-storage/src/graph/nodes/node_entry.rs @@ -87,8 +87,12 @@ impl<'a, 'b: 'a> NodeStorageOps<'a> for &'a NodeStorageEntry<'b> { self.as_ref().degree(layers, dir) } - fn additions(self, layer_ids: usize) -> storage::NodeAdditions<'a> { - self.as_ref().additions(layer_ids) + fn layer_additions(self, layer_ids: usize) -> storage::NodeAdditions<'a> { + self.as_ref().layer_additions(layer_ids) + } + + fn additions(self) -> storage::NodeAdditions<'a> { + self.as_ref().additions() } fn edges_iter( @@ -140,4 +144,8 @@ impl<'a, 'b: 'a> NodeStorageOps<'a> for &'a NodeStorageEntry<'b> { ) -> impl Iterator)> { self.as_ref().temp_prop_rows_range(w) } + + fn tprop(self, prop_id: usize) -> storage::NodeTProps<'a> { + self.as_ref().tprop(prop_id) + } } diff --git a/raphtory-storage/src/graph/nodes/node_storage_ops.rs b/raphtory-storage/src/graph/nodes/node_storage_ops.rs index 556e8f09e2..c7c59a3a2b 100644 --- a/raphtory-storage/src/graph/nodes/node_storage_ops.rs +++ b/raphtory-storage/src/graph/nodes/node_storage_ops.rs @@ -6,6 +6,8 @@ use raphtory_core::{entities::LayerVariants, storage::timeindex::TimeIndexEntry} use std::{borrow::Cow, ops::Range}; use storage::{api::nodes::NodeRefOps, NodeEntryRef}; +static ALL_LAYERS: &LayerIds = &LayerIds::All; + pub trait NodeStorageOps<'a>: Copy + Sized + Send + Sync + 'a { fn degree(self, layers: &LayerIds, dir: Direction) -> usize; @@ -32,7 +34,9 @@ pub trait NodeStorageOps<'a>: Copy + Sized + Send + Sync + 'a { layer_ids: &'a LayerIds, ) -> impl Iterator + Send + Sync + 'a; - fn additions(self, layer_id: usize) -> storage::NodeAdditions<'a>; + fn layer_additions(self, layer_id: usize) -> storage::NodeAdditions<'a>; + + fn additions(self) -> storage::NodeAdditions<'a>; fn deletions(self, layer_id: usize) -> storage::NodeAdditions<'a>; @@ -47,6 +51,8 @@ pub trait NodeStorageOps<'a>: Copy + Sized + Send + Sync + 'a { .map(move |id| (id, self.temporal_prop_layer(id, prop_id))) } + fn tprop(self, prop_id: usize) -> storage::NodeTProps<'a>; + fn constant_prop_layer(self, layer_id: usize, prop_id: usize) -> Option; fn constant_prop_iter( @@ -119,10 +125,18 @@ impl<'a> NodeStorageOps<'a> for NodeEntryRef<'a> { NodeRefOps::layer_additions(self, layer_id) } - fn additions(self, layer_ids: usize) -> storage::NodeAdditions<'a> { + fn layer_additions(self, layer_ids: usize) -> storage::NodeAdditions<'a> { NodeRefOps::layer_additions(self, layer_ids) } + fn additions(self) -> storage::NodeAdditions<'a> { + NodeRefOps::additions(self, ALL_LAYERS) + } + + fn tprop(self, prop_id: usize) -> storage::NodeTProps<'a> { + NodeRefOps::t_prop(self, &ALL_LAYERS, prop_id) + } + fn temporal_prop_layer(self, layer_id: usize, prop_id: usize) -> storage::NodeTProps<'a> { NodeRefOps::temporal_prop_layer(self, layer_id, prop_id) } diff --git a/raphtory-storage/src/mutation/addition_ops_ext.rs b/raphtory-storage/src/mutation/addition_ops_ext.rs index 7af9446e46..7d8767c8eb 100644 --- a/raphtory-storage/src/mutation/addition_ops_ext.rs +++ b/raphtory-storage/src/mutation/addition_ops_ext.rs @@ -10,7 +10,9 @@ use raphtory_api::core::{ storage::dict_mapper::MaybeNew, }; use raphtory_core::{ - entities::{nodes::node_ref::NodeRef, GidRef, EID, ELID, VID}, + entities::{ + graph::tgraph::TooManyLayers, nodes::node_ref::NodeRef, GidRef, EID, ELID, MAX_LAYER, VID, + }, storage::{raw_edges::WriteLockedEdges, timeindex::TimeIndexEntry, WriteLockedNodes}, }; use storage::{ @@ -182,6 +184,11 @@ impl InternalAdditionOps for TemporalGraph { fn resolve_layer(&self, layer: Option<&str>) -> Result, Self::Error> { let id = self.edge_meta().get_or_create_layer_id(layer); + if let MaybeNew::New(id) = id { + if id > MAX_LAYER { + Err(TooManyLayers)?; + } + } Ok(id) } diff --git a/raphtory/src/db/api/storage/graph/storage_ops/time_semantics.rs b/raphtory/src/db/api/storage/graph/storage_ops/time_semantics.rs index f572be830c..f94a3bd782 100644 --- a/raphtory/src/db/api/storage/graph/storage_ops/time_semantics.rs +++ b/raphtory/src/db/api/storage/graph/storage_ops/time_semantics.rs @@ -16,7 +16,10 @@ use raphtory_api::{ use raphtory_core::utils::iter::GenLockedIter; use raphtory_storage::{ core_ops::CoreGraphOps, - graph::{edges::edge_storage_ops::EdgeStorageOps, nodes::node_storage_ops::NodeStorageOps}, + graph::{ + edges::edge_storage_ops::EdgeStorageOps, locked::LockedGraph, + nodes::node_storage_ops::NodeStorageOps, + }, }; use rayon::iter::ParallelIterator; use std::ops::{Deref, Range}; @@ -41,20 +44,18 @@ impl GraphTimeSemanticsOps for GraphStorage { #[inline] fn earliest_time_global(&self) -> Option { match self { - GraphStorage::Mem(storage) => storage.graph.graph_earliest_time(), - GraphStorage::Unlocked(storage) => storage.graph_earliest_time(), - #[cfg(feature = "storage")] - GraphStorage::Disk(storage) => storage.inner.earliest(), + GraphStorage::Mem(LockedGraph { graph, .. }) | GraphStorage::Unlocked(graph) => { + graph.graph_earliest_time() + } } } #[inline] fn latest_time_global(&self) -> Option { match self { - GraphStorage::Mem(storage) => storage.graph.graph_latest_time(), - GraphStorage::Unlocked(storage) => storage.graph_latest_time(), - #[cfg(feature = "storage")] - GraphStorage::Disk(storage) => storage.inner.latest(), + GraphStorage::Mem(LockedGraph { graph, .. }) | GraphStorage::Unlocked(graph) => { + graph.graph_latest_time() + } } } diff --git a/raphtory/src/db/api/storage/storage.rs b/raphtory/src/db/api/storage/storage.rs index c3385e6617..985b1750c7 100644 --- a/raphtory/src/db/api/storage/storage.rs +++ b/raphtory/src/db/api/storage/storage.rs @@ -1,12 +1,13 @@ #[cfg(feature = "search")] use crate::search::graph_index::GraphIndex; use crate::{ - core::entities::{graph::tgraph::TemporalGraph, nodes::node_ref::NodeRef}, + core::entities::nodes::node_ref::NodeRef, db::api::view::{ internal::{InheritEdgeHistoryFilter, InheritNodeHistoryFilter, InternalStorageOps}, Base, InheritViewOps, }, }; +use db4_graph::{TemporalGraph, WriteLockedGraph}; use raphtory_api::core::{ entities::{EID, VID}, storage::{dict_mapper::MaybeNew, timeindex::TimeIndexEntry}, @@ -15,15 +16,17 @@ use raphtory_storage::{ graph::graph::GraphStorage, mutation::addition_ops::{AtomicAdditionOps, SessionAdditionOps, TGWriteSession}, }; -use serde::{Deserialize, Serialize}; use std::{ fmt::{Display, Formatter}, path::PathBuf, sync::Arc, }; +use storage::Extension; use tracing::info; use crate::{db::api::view::IndexSpec, errors::GraphError}; +#[cfg(feature = "search")] +use once_cell::sync::OnceCell; use raphtory_api::core::entities::{ properties::prop::{Prop, PropType}, GidRef, @@ -34,29 +37,18 @@ use raphtory_core::{ }; use raphtory_storage::{ core_ops::InheritCoreGraphOps, - graph::{locked::WriteLockedGraph, nodes::node_storage_ops::NodeStorageOps}, layer_ops::InheritLayerOps, mutation::{ addition_ops::InternalAdditionOps, deletion_ops::InternalDeletionOps, property_addition_ops::InternalPropertyAdditionOps, }, }; -#[cfg(feature = "proto")] -use { - crate::serialise::incremental::{GraphWriter, InternalCache}, - once_cell::sync::OnceCell, -}; -#[derive(Debug, Default, Serialize, Deserialize)] +#[derive(Debug, Default)] pub struct Storage { graph: GraphStorage, - #[cfg(feature = "proto")] - #[serde(skip)] - pub(crate) cache: OnceCell, #[cfg(feature = "search")] - #[serde(skip)] pub(crate) index: OnceCell, - // vector index } impl InheritLayerOps for Storage {} @@ -83,9 +75,7 @@ const IN_MEMORY_INDEX_NOT_PERSISTED: &str = "In-memory index not persisted. Not impl Storage { pub(crate) fn new(num_locks: usize) -> Self { Self { - graph: GraphStorage::Unlocked(Arc::new(TemporalGraph::new(num_locks))), - #[cfg(feature = "proto")] - cache: OnceCell::new(), + graph: GraphStorage::Unlocked(Arc::new(TemporalGraph::new(None))), #[cfg(feature = "search")] index: OnceCell::new(), } @@ -94,21 +84,11 @@ impl Storage { pub(crate) fn from_inner(graph: GraphStorage) -> Self { Self { graph, - #[cfg(feature = "proto")] - cache: OnceCell::new(), #[cfg(feature = "search")] index: OnceCell::new(), } } - #[cfg(feature = "proto")] - #[inline] - fn if_cache(&self, map_fn: impl FnOnce(&GraphWriter)) { - if let Some(cache) = self.cache.get() { - map_fn(cache) - } - } - #[cfg(feature = "search")] #[inline] fn if_index( @@ -141,10 +121,9 @@ impl Storage { &self, index_spec: IndexSpec, ) -> Result<&GraphIndex, GraphError> { - let index = self.index.get_or_try_init(|| { - let cached_graph_path = self.get_cache().map(|cache| cache.folder.get_base_path()); - GraphIndex::create(&self.graph, false, cached_graph_path) - })?; + let index = self + .index + .get_or_try_init(|| GraphIndex::create(&self.graph, false, None))?; index.update(&self.graph, index_spec)?; Ok(index) } @@ -253,10 +232,6 @@ impl<'a> SessionAdditionOps for StorageWriteSession<'a> { .session .resolve_graph_property(prop, dtype.clone(), is_static)?; - #[cfg(feature = "proto")] - self.storage.if_cache(|cache| { - cache.resolve_graph_property(prop, id, dtype, is_static); - }); Ok(id) } @@ -270,11 +245,6 @@ impl<'a> SessionAdditionOps for StorageWriteSession<'a> { .session .resolve_node_property(prop, dtype.clone(), is_static)?; - #[cfg(feature = "proto")] - self.storage.if_cache(|cache| { - cache.resolve_node_property(prop, id, &dtype, is_static); - }); - Ok(id) } @@ -288,11 +258,6 @@ impl<'a> SessionAdditionOps for StorageWriteSession<'a> { .session .resolve_edge_property(prop, dtype.clone(), is_static)?; - #[cfg(feature = "proto")] - self.storage.if_cache(|cache| { - cache.resolve_edge_property(prop, id, &dtype, is_static); - }); - Ok(id) } @@ -304,10 +269,6 @@ impl<'a> SessionAdditionOps for StorageWriteSession<'a> { ) -> Result<(), Self::Error> { self.session.internal_add_node(t, v, props)?; - #[cfg(feature = "proto")] - self.storage - .if_cache(|cache| cache.add_node_update(t, v, props)); - #[cfg(feature = "search")] self.storage.if_index(|index| { index.add_node_update(&self.storage.graph, t, MaybeNew::New(v), props) @@ -325,11 +286,6 @@ impl<'a> SessionAdditionOps for StorageWriteSession<'a> { layer: usize, ) -> Result, Self::Error> { let id = self.session.internal_add_edge(t, src, dst, props, layer)?; - #[cfg(feature = "proto")] - self.storage.if_cache(|cache| { - cache.resolve_edge(id, src, dst); - cache.add_edge_update(t, id.inner(), props, layer); - }); #[cfg(feature = "search")] self.storage .if_index(|index| index.add_edge_update(&self.storage.graph, id, t, layer, props))?; @@ -346,9 +302,6 @@ impl<'a> SessionAdditionOps for StorageWriteSession<'a> { self.session .internal_add_edge_update(t, edge, props, layer)?; - #[cfg(feature = "proto")] - self.storage - .if_cache(|cache| cache.add_edge_update(t, edge, props, layer)); #[cfg(feature = "search")] self.storage.if_index(|index| { index.add_edge_update( @@ -369,7 +322,7 @@ impl InternalAdditionOps for Storage { type WS<'a> = StorageWriteSession<'a>; type AtomicAddEdge<'a> = StorageWriteSession<'a>; - fn write_lock(&self) -> Result { + fn write_lock(&self) -> Result, Self::Error> { Ok(self.graph.write_lock()?) } @@ -384,9 +337,6 @@ impl InternalAdditionOps for Storage { fn resolve_layer(&self, layer: Option<&str>) -> Result, Self::Error> { let id = self.graph.resolve_layer(layer)?; - #[cfg(feature = "proto")] - self.if_cache(|cache| cache.resolve_layer(layer, id)); - Ok(id) } @@ -396,9 +346,6 @@ impl InternalAdditionOps for Storage { NodeRef::External(gid) => { let id = self.resolve_node(id)?; - #[cfg(feature = "proto")] - self.if_cache(|cache| cache.resolve_node(id, gid)); - Ok(id) } } @@ -411,15 +358,6 @@ impl InternalAdditionOps for Storage { ) -> Result, MaybeNew)>, Self::Error> { let node_and_type = self.graph.resolve_node_and_type(id, node_type)?; - #[cfg(feature = "proto")] - self.if_cache(|cache| { - use raphtory_storage::core_ops::CoreGraphOps; - - let (vid, _) = node_and_type.inner(); - let node_entry = self.core_node(vid.inner()); - cache.resolve_node_and_type(node_and_type, node_type, node_entry.id()) - }); - Ok(node_and_type) } @@ -470,18 +408,12 @@ impl InternalPropertyAdditionOps for Storage { ) -> Result<(), GraphError> { self.graph.internal_add_properties(t, props)?; - #[cfg(feature = "proto")] - self.if_cache(|cache| cache.add_graph_tprops(t, props)); - Ok(()) } fn internal_add_constant_properties(&self, props: &[(usize, Prop)]) -> Result<(), GraphError> { self.graph.internal_add_constant_properties(props)?; - #[cfg(feature = "proto")] - self.if_cache(|cache| cache.add_graph_cprops(props)); - Ok(()) } @@ -491,9 +423,6 @@ impl InternalPropertyAdditionOps for Storage { ) -> Result<(), GraphError> { self.graph.internal_update_constant_properties(props)?; - #[cfg(feature = "proto")] - self.if_cache(|cache| cache.add_graph_cprops(props)); - Ok(()) } @@ -505,9 +434,6 @@ impl InternalPropertyAdditionOps for Storage { self.graph .internal_add_constant_node_properties(vid, props)?; - #[cfg(feature = "proto")] - self.if_cache(|cache| cache.add_node_cprops(vid, props)); - #[cfg(feature = "search")] self.if_index(|index| index.add_node_constant_properties(vid, props))?; @@ -522,9 +448,6 @@ impl InternalPropertyAdditionOps for Storage { self.graph .internal_update_constant_node_properties(vid, props)?; - #[cfg(feature = "proto")] - self.if_cache(|cache| cache.add_node_cprops(vid, props)); - #[cfg(feature = "search")] self.if_index(|index| index.update_node_constant_properties(vid, props))?; @@ -540,9 +463,6 @@ impl InternalPropertyAdditionOps for Storage { self.graph .internal_add_constant_edge_properties(eid, layer, props)?; - #[cfg(feature = "proto")] - self.if_cache(|cache| cache.add_edge_cprops(eid, layer, props)); - #[cfg(feature = "search")] self.if_index(|index| index.add_edge_constant_properties(eid, layer, props))?; @@ -558,9 +478,6 @@ impl InternalPropertyAdditionOps for Storage { self.graph .internal_update_constant_edge_properties(eid, layer, props)?; - #[cfg(feature = "proto")] - self.if_cache(|cache| cache.add_edge_cprops(eid, layer, props)); - #[cfg(feature = "search")] self.if_index(|index| index.update_edge_constant_properties(eid, layer, props))?; @@ -579,12 +496,6 @@ impl InternalDeletionOps for Storage { ) -> Result, GraphError> { let eid = self.graph.internal_delete_edge(t, src, dst, layer)?; - #[cfg(feature = "proto")] - self.if_cache(|cache| { - cache.resolve_edge(eid, src, dst); - cache.delete_edge(eid.inner(), t, layer); - }); - Ok(eid) } @@ -596,9 +507,6 @@ impl InternalDeletionOps for Storage { ) -> Result<(), GraphError> { self.graph.internal_delete_existing_edge(t, eid, layer)?; - #[cfg(feature = "proto")] - self.if_cache(|cache| cache.delete_edge(eid, t, layer)); - Ok(()) } } diff --git a/raphtory/src/db/api/view/graph.rs b/raphtory/src/db/api/view/graph.rs index e182a05cb7..da5e8eb2b1 100644 --- a/raphtory/src/db/api/view/graph.rs +++ b/raphtory/src/db/api/view/graph.rs @@ -1,6 +1,6 @@ use crate::{ core::{ - entities::{graph::tgraph::TemporalGraph, nodes::node_ref::AsNodeRef, LayerIds, VID}, + entities::{nodes::node_ref::AsNodeRef, LayerIds, VID}, storage::timeindex::AsTime, }, db::{ @@ -30,10 +30,14 @@ use crate::{ }; use ahash::HashSet; use chrono::{DateTime, Utc}; +use db4_graph::TemporalGraph; use raphtory_api::{ atomic_extra::atomic_usize_from_mut_slice, core::{ - entities::{properties::meta::PropMapper, EID}, + entities::{ + properties::meta::{Meta, PropMapper}, + EID, + }, storage::{arc_str::ArcStr, timeindex::TimeIndexEntry}, Direction, }, @@ -45,10 +49,7 @@ use raphtory_storage::{ edges::edge_storage_ops::EdgeStorageOps, graph::GraphStorage, nodes::node_storage_ops::NodeStorageOps, }, - mutation::{ - addition_ops::{InternalAdditionOps, SessionAdditionOps}, - MutationError, - }, + mutation::{addition_ops::InternalAdditionOps, MutationError}, }; use rayon::prelude::*; use rustc_hash::FxHashSet; @@ -233,20 +234,19 @@ impl<'graph, G: GraphView + 'graph> GraphViewOps<'graph> for G { fn materialize(&self) -> Result { let storage = self.core_graph().lock(); - let mut g = TemporalGraph::default(); - - // Copy all graph properties - g.graph_meta = self.graph_meta().deep_clone(); // preserve all property mappings - g.node_meta - .set_const_prop_meta(self.node_meta().const_prop_meta().deep_clone()); - g.node_meta - .set_temporal_prop_meta(self.node_meta().temporal_prop_meta().deep_clone()); - g.edge_meta - .set_const_prop_meta(self.edge_meta().const_prop_meta().deep_clone()); - g.edge_meta - .set_temporal_prop_meta(self.edge_meta().temporal_prop_meta().deep_clone()); + let mut node_meta = Meta::new(); + let mut edge_meta = Meta::new(); + + node_meta.set_const_prop_meta(self.node_meta().const_prop_meta().deep_clone()); + node_meta.set_temporal_prop_meta(self.node_meta().temporal_prop_meta().deep_clone()); + edge_meta.set_const_prop_meta(self.edge_meta().const_prop_meta().deep_clone()); + edge_meta.set_temporal_prop_meta(self.edge_meta().temporal_prop_meta().deep_clone()); + + let mut g = TemporalGraph::new_with_meta(None, node_meta, edge_meta); + // Copy all graph properties + g.graph_meta = self.graph_meta().deep_clone().into(); let layer_map: Vec<_> = match self.layer_ids() { LayerIds::None => { @@ -258,7 +258,7 @@ impl<'graph, G: GraphView + 'graph> GraphViewOps<'graph> for G { let layers = storage.edge_meta().layer_meta().get_keys(); for id in 0..layers.len() { let new_id = g - .resolve_layer_inner(Some(&layers[id])) + .resolve_layer(Some(&layers[id])) .map_err(MutationError::from)? .inner(); layer_map[id] = new_id; @@ -268,7 +268,7 @@ impl<'graph, G: GraphView + 'graph> GraphViewOps<'graph> for G { LayerIds::One(l_id) => { let mut layer_map = vec![0; self.unfiltered_num_layers()]; let new_id = g - .resolve_layer_inner(Some(&storage.edge_meta().get_layer_name_by_id(*l_id))) + .resolve_layer(Some(&storage.edge_meta().get_layer_name_by_id(*l_id))) .map_err(MutationError::from)?; layer_map[*l_id] = new_id.inner(); layer_map @@ -278,7 +278,7 @@ impl<'graph, G: GraphView + 'graph> GraphViewOps<'graph> for G { let layers = storage.edge_meta().layer_meta().get_keys(); for id in ids { let new_id = g - .resolve_layer_inner(Some(&layers[id])) + .resolve_layer(Some(&layers[id])) .map_err(MutationError::from)? .inner(); layer_map[id] = new_id; @@ -300,8 +300,7 @@ impl<'graph, G: GraphView + 'graph> GraphViewOps<'graph> for G { }; // Set event counter to be the same as old graph to avoid any possibility for duplicate event ids - g.event_counter - .fetch_max(storage.read_event_id(), Ordering::Relaxed); + g.storage().set_event_id(storage.read_event_id()); let g = GraphStorage::from(g); @@ -310,7 +309,8 @@ impl<'graph, G: GraphView + 'graph> GraphViewOps<'graph> for G { { // scope for the write lock let mut new_storage = g.write_lock()?; - new_storage.nodes.resize(self.count_nodes()); + + new_storage.resize_chunks_to_num_nodes(self.count_nodes()); let mut node_map = vec![VID::default(); storage.unfiltered_num_nodes()]; let node_map_shared = @@ -320,62 +320,91 @@ impl<'graph, G: GraphView + 'graph> GraphViewOps<'graph> for G { for (index, node) in self.nodes().iter().enumerate() { let new_id = VID(index); let gid = node.id(); - if let Some(mut new_node) = shard.set(new_id, gid.as_ref()) { - node_map_shared[node.node.index()].store(index, Ordering::Relaxed); + node_map_shared[node.node.index()].store(new_id.index(), Ordering::Relaxed); + if let Some(node_pos) = shard.resolve_pos(new_id) { + let mut writer = shard.writer(); if let Some(node_type) = node.node_type() { let new_type_id = g .node_meta() .node_type_meta() .get_or_create_id(&node_type) .inner(); - new_node.node_store_mut().node_type = new_type_id; + writer.store_node_id_and_node_type( + node_pos, + 0, + gid.as_ref(), + new_type_id, + 0, + ); } - g_session.set_node(gid.as_ref(), new_id)?; - for (t, rows) in node.rows() { - let prop_offset = new_node.t_props_log_mut().push(rows)?; - new_node.node_store_mut().update_t_prop_time(t, prop_offset); + for (t, row) in node.rows() { + writer.add_props(t, node_pos, 0, row, 0); } - for c_prop_id in node.const_prop_ids() { - if let Some(prop_value) = node.get_const_prop(c_prop_id) { - new_node - .node_store_mut() - .add_constant_prop(c_prop_id, prop_value)?; - } - } + writer.update_c_props( + node_pos, + 0, + node.const_prop_ids() + .filter_map(|id| node.get_const_prop(id).map(|prop| (id, prop))), + 0, + ); } } Ok::<(), MutationError>(()) })?; + new_storage.resize_chunks_to_num_edges(self.count_edges()); + new_storage.edges.par_iter_mut().try_for_each(|mut shard| { for (eid, edge) in self.edges().iter().enumerate() { - if let Some(mut new_edge) = shard.get_mut(EID(eid)) { - let edge_store = new_edge.edge_store_mut(); - edge_store.src = node_map[edge.edge.src().index()]; - edge_store.dst = node_map[edge.edge.dst().index()]; - edge_store.eid = EID(eid); + let src = node_map[edge.edge.src().index()]; + let dst = node_map[edge.edge.dst().index()]; + let eid = EID(eid); + if let Some(edge_pos) = shard.resolve_pos(eid) { + let mut writer = shard.writer(); + // make the edge for the first time + writer.add_static_edge(Some(edge_pos), src, dst, 0, 0, Some(false)); + for edge in edge.explode_layers() { let layer = layer_map[edge.edge.layer().unwrap()]; - let additions = new_edge.additions_mut(layer); for edge in edge.explode() { let t = edge.edge.time().unwrap(); - additions.insert(t); + writer.add_edge( + t, + Some(edge_pos), + src, + dst, + [], + layer, + 0, + Some(true), + ); } for t_prop in edge.properties().temporal().values() { let prop_id = t_prop.id(); for (t, prop_value) in t_prop.iter_indexed() { - new_edge.layer_mut(layer).add_prop(t, prop_id, prop_value)?; - } - } - for c_prop in edge.const_prop_ids() { - if let Some(prop_value) = edge.get_const_prop(c_prop) { - new_edge - .layer_mut(layer) - .add_constant_prop(c_prop, prop_value)?; + writer.add_edge( + t, + Some(edge_pos), + src, + dst, + [(prop_id, prop_value)], + layer, + 0, + Some(true), + ); } } + writer.update_c_props( + edge_pos, + src, + dst, + layer, + edge.const_prop_ids().filter_map(move |prop_id| { + edge.get_const_prop(prop_id).map(|prop| (prop_id, prop)) + }), + ); } let time_semantics = self.edge_time_semantics(); @@ -385,7 +414,7 @@ impl<'graph, G: GraphView + 'graph> GraphViewOps<'graph> for G { self, self.layer_ids(), ) { - new_edge.deletions_mut(layer_map[layer]).insert(t); + writer.delete_edge(t, Some(edge_pos), src, dst, layer, 0, Some(true)); } } } @@ -394,33 +423,34 @@ impl<'graph, G: GraphView + 'graph> GraphViewOps<'graph> for G { new_storage.nodes.par_iter_mut().try_for_each(|mut shard| { for (eid, edge) in self.edges().iter().enumerate() { - if let Some(src_node) = shard.get_mut(node_map[edge.edge.src().index()]) { - for e in edge.explode() { + let eid = EID(eid); + for e in edge.explode() { + if let Some(node_pos) = shard.resolve_pos(node_map[edge.edge.src().index()]) + { + let mut writer = shard.writer(); + let t = e.time_and_index().expect("exploded edge should have time"); let l = layer_map[e.edge.layer().unwrap()]; - src_node.update_time(t, EID(eid).with_layer(l)); - } - for ee in edge.explode_layers() { - src_node.add_edge( + writer.add_outbound_edge( + t, + node_pos, node_map[edge.edge.dst().index()], - Direction::OUT, - layer_map[ee.edge.layer().unwrap()], - EID(eid), + eid.with_layer(l), + 0, ); } - } - if let Some(dst_node) = shard.get_mut(node_map[edge.edge.dst().index()]) { - for e in edge.explode() { + if let Some(node_pos) = shard.resolve_pos(node_map[edge.edge.dst().index()]) + { + let mut writer = shard.writer(); + let t = e.time_and_index().expect("exploded edge should have time"); let l = layer_map[e.edge.layer().unwrap()]; - dst_node.update_time(t, EID(eid).with_layer(l)); - } - for ee in edge.explode_layers() { - dst_node.add_edge( + writer.add_inbound_edge( + t, + node_pos, node_map[edge.edge.src().index()], - Direction::IN, - layer_map[ee.edge.layer().unwrap()], - EID(eid), + eid.with_layer(l), + 0, ); } } @@ -432,17 +462,16 @@ impl<'graph, G: GraphView + 'graph> GraphViewOps<'graph> for G { self, self.layer_ids(), ) { - if let Some(src_node) = shard.get_mut(node_map[edge.edge.src().index()]) { - src_node.update_time( - t, - edge.edge.pid().with_layer_deletion(layer_map[layer]), - ); + let src = node_map[edge.edge.src().index()]; + let dst = node_map[edge.edge.dst().index()]; + + if let Some(node_pos) = shard.resolve_pos(src) { + let mut writer = shard.writer(); + writer.update_deletion_time(t, node_pos, dst, eid.with_layer(layer), 0); } - if let Some(dst_node) = shard.get_mut(node_map[edge.edge.dst().index()]) { - dst_node.update_time( - t, - edge.edge.pid().with_layer_deletion(layer_map[layer]), - ); + if let Some(node_pos) = shard.resolve_pos(dst) { + let mut writer = shard.writer(); + writer.update_deletion_time(t, node_pos, src, eid.with_layer(layer), 0); } } } diff --git a/raphtory/src/io/arrow/df_loaders.rs b/raphtory/src/io/arrow/df_loaders.rs index 00b4003a1d..953b65f564 100644 --- a/raphtory/src/io/arrow/df_loaders.rs +++ b/raphtory/src/io/arrow/df_loaders.rs @@ -20,7 +20,7 @@ use raphtory_api::{ storage::{dict_mapper::MaybeNew, timeindex::TimeIndexEntry}, }, }; -use raphtory_core::{entities::graph::timer::TimeCounterTrait, storage::timeindex::AsTime}; +use raphtory_core::storage::timeindex::AsTime; use raphtory_storage::mutation::addition_ops::SessionAdditionOps; use rayon::prelude::*; use std::{ @@ -148,14 +148,13 @@ pub(crate) fn load_nodes_from_df< Ok::<(), LoadError>(()) })?; - let (earliest, latest) = write_locked_graph.earliest_latest(); + let node_stats = write_locked_graph.node_stats().clone(); let update_time = |time: TimeIndexEntry| { let time = time.t(); - earliest.update(time); - latest.update(time); + node_stats.update_time(time); }; - write_locked_graph.resize_chunks_to_num_nodes(); + write_locked_graph.resize_chunks_to_num_nodes(write_locked_graph.num_nodes()); write_locked_graph .nodes @@ -312,7 +311,7 @@ pub(crate) fn load_edges_from_df< Ok::<(), LoadError>(()) })?; - write_locked_graph.resize_chunks_to_num_nodes(); + write_locked_graph.resize_chunks_to_num_nodes(write_locked_graph.num_nodes()); // resolve all the edges eid_col_resolved.resize_with(df.len(), Default::default); @@ -322,12 +321,6 @@ pub(crate) fn load_edges_from_df< let next_edge_id: Arc = write_locked_graph.num_edges.clone(); let next_edge_id = || next_edge_id.fetch_add(1, Ordering::Relaxed); - let (earliest, latest) = write_locked_graph.earliest_latest(); - let update_time = |time: TimeIndexEntry| { - let time = time.t(); - earliest.update(time); - latest.update(time); - }; write_locked_graph .nodes .par_iter_mut() @@ -342,7 +335,6 @@ pub(crate) fn load_edges_from_df< { if let Some(src_pos) = locked_page.resolve_pos(*src) { let t = TimeIndexEntry(time, start_idx + row); - update_time(t); let mut writer = locked_page.writer(); writer.store_node_id(src_pos, 0, src_gid, 0); if let Some(edge_id) = writer.get_out_edge(src_pos, *dst, 0) { @@ -390,7 +382,7 @@ pub(crate) fn load_edges_from_df< } }); - write_locked_graph.resize_chunks_to_num_edges(); + write_locked_graph.resize_chunks_to_num_edges(write_locked_graph.num_edges()); write_locked_graph .edges @@ -575,7 +567,7 @@ pub(crate) fn load_node_props_from_df< Ok::<(), LoadError>(()) })?; - write_locked_graph.resize_chunks_to_num_nodes(); + write_locked_graph.resize_chunks_to_num_nodes(write_locked_graph.num_nodes()); write_locked_graph .nodes @@ -701,7 +693,7 @@ pub(crate) fn load_edges_props_from_df< Ok::<(), LoadError>(()) })?; - write_locked_graph.resize_chunks_to_num_nodes(); + write_locked_graph.resize_chunks_to_num_nodes(write_locked_graph.num_nodes()); // resolve all the edges eid_col_resolved.resize_with(df.len(), Default::default); From 92273ae56f360737248f9b130c813fc24f848afd Mon Sep 17 00:00:00 2001 From: Fadhil Abubaker Date: Mon, 30 Jun 2025 10:22:36 -0400 Subject: [PATCH 051/321] Initialize GIDResolver correctly in db4-graph (#2159) --- db4-graph/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/db4-graph/src/lib.rs b/db4-graph/src/lib.rs index 3d7a7f1da8..f7a207bcf2 100644 --- a/db4-graph/src/lib.rs +++ b/db4-graph/src/lib.rs @@ -62,6 +62,7 @@ impl, ES = ES>> TemporalGraph { pub fn new_with_meta(path: Option, node_meta: Meta, edge_meta: Meta) -> Self { let graph_dir = path.unwrap_or_else(random_temp_dir); + let gid_resolver_dir = graph_dir.join("gid_resolver"); let storage = Layer::new_with_meta( graph_dir.clone(), DEFAULT_MAX_PAGE_LEN_NODES, @@ -71,7 +72,7 @@ impl, ES = ES>> TemporalGraph { ); Self { graph_dir, - logical_to_physical: Mapping::new(), + logical_to_physical: GIDResolver::new(gid_resolver_dir).unwrap(), node_count: AtomicUsize::new(0), storage: Arc::new(storage), graph_meta: Arc::new(GraphMeta::default()), From 3d941b26805e7f3763ab107be5d209c90c233fbd Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Mon, 30 Jun 2025 16:36:42 +0100 Subject: [PATCH 052/321] more fixes for interop --- .../src/db/api/view/internal/materialize.rs | 3 +- .../time_semantics/event_semantics.rs | 18 +- .../time_semantics/persistent_semantics.rs | 10 +- raphtory/src/db/graph/graph.rs | 21 +- raphtory/src/db/graph/views/deletion_graph.rs | 3 +- raphtory/src/serialise/incremental.rs | 4 +- raphtory/src/serialise/serialise.rs | 1151 +---------------- 7 files changed, 28 insertions(+), 1182 deletions(-) diff --git a/raphtory/src/db/api/view/internal/materialize.rs b/raphtory/src/db/api/view/internal/materialize.rs index 034c677b69..f51d3c946b 100644 --- a/raphtory/src/db/api/view/internal/materialize.rs +++ b/raphtory/src/db/api/view/internal/materialize.rs @@ -19,14 +19,13 @@ use chrono::{DateTime, Utc}; use enum_dispatch::enum_dispatch; use raphtory_api::{core::entities::properties::prop::PropType, iter::BoxedLIter, GraphType}; use raphtory_storage::{graph::graph::GraphStorage, mutation::InheritMutationOps}; -use serde::{Deserialize, Serialize}; use std::ops::Range; #[enum_dispatch(GraphTimeSemanticsOps)] #[enum_dispatch(TemporalPropertiesOps)] #[enum_dispatch(TemporalPropertyViewOps)] #[enum_dispatch(ConstantPropertiesOps)] -#[derive(Serialize, Deserialize, Clone)] +#[derive(Clone)] pub enum MaterializedGraph { EventGraph(Graph), PersistentGraph(PersistentGraph), diff --git a/raphtory/src/db/api/view/internal/time_semantics/event_semantics.rs b/raphtory/src/db/api/view/internal/time_semantics/event_semantics.rs index 6feaf5f43d..96b20ade50 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/event_semantics.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/event_semantics.rs @@ -80,14 +80,7 @@ impl NodeTimeSemanticsOps for EventSemantics { node: NodeStorageRef<'graph>, _view: G, ) -> impl Iterator)> + Send + Sync + 'graph { - node.temp_prop_rows().map(|(t, row)| { - ( - t, - row.into_iter() - .filter_map(|(id, prop)| Some((id, prop?))) - .collect(), - ) - }) + node.temp_prop_rows().map(|(t, _, row)| (t, row)) } fn node_updates_window<'graph, G: GraphView + 'graph>( @@ -97,14 +90,7 @@ impl NodeTimeSemanticsOps for EventSemantics { w: Range, ) -> impl Iterator)> + Send + Sync + 'graph { node.temp_prop_rows_range(Some(TimeIndexEntry::range(w))) - .map(|(t, row)| { - ( - t, - row.into_iter() - .filter_map(|(id, prop)| Some((id, prop?))) - .collect(), - ) - }) + .map(|(t, _, row)| (t, row)) } fn node_valid<'graph, G: GraphView + 'graph>( diff --git a/raphtory/src/db/api/view/internal/time_semantics/persistent_semantics.rs b/raphtory/src/db/api/view/internal/time_semantics/persistent_semantics.rs index 3d3e6ff5b4..9ddbb50cfb 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/persistent_semantics.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/persistent_semantics.rs @@ -217,10 +217,10 @@ impl NodeTimeSemanticsOps for PersistentSemantics { node: NodeStorageRef<'graph>, _view: G, ) -> impl Iterator)> + Send + Sync + 'graph { - node.temp_prop_rows_range().map(|(t, row)| { + node.temp_prop_rows().map(|(t, _, row)| { ( t, - row.into_iter().filter_map(|(i, v)| Some((i, v?))).collect(), + row ) }) } @@ -259,11 +259,11 @@ impl NodeTimeSemanticsOps for PersistentSemantics { .into_iter() .map(move |row| (TimeIndexEntry::start(start), row)) .chain( - node.temp_prop_rows_window(TimeIndexEntry::range(w)) - .map(|(t, row)| { + node.temp_prop_rows_range(Some(TimeIndexEntry::range(w))) + .map(|(t, _, row)| { ( t, - row.into_iter().filter_map(|(i, v)| Some((i, v?))).collect(), + row ) }), ) diff --git a/raphtory/src/db/graph/graph.rs b/raphtory/src/db/graph/graph.rs index f1fc81863e..91a1114b23 100644 --- a/raphtory/src/db/graph/graph.rs +++ b/raphtory/src/db/graph/graph.rs @@ -38,7 +38,6 @@ use raphtory_storage::{ mutation::InheritMutationOps, }; use rayon::prelude::*; -use serde::{Deserialize, Serialize}; use std::{ collections::HashSet, fmt::{Display, Formatter}, @@ -48,7 +47,7 @@ use std::{ }; #[repr(transparent)] -#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[derive(Debug, Clone, Default)] pub struct Graph { pub(crate) inner: Arc, } @@ -604,7 +603,7 @@ mod db_tests { utils::logging::global_info_logger, }; use raphtory_core::utils::time::{ParseTimeError, TryIntoTime}; - use raphtory_storage::{core_ops::CoreGraphOps, mutation::addition_ops::InternalAdditionOps}; + use raphtory_storage::{core_ops::CoreGraphOps, graph::nodes::node_storage_ops::NodeStorageOps, mutation::addition_ops::InternalAdditionOps}; use rayon::join; use std::{ collections::{HashMap, HashSet}, @@ -1710,12 +1709,12 @@ mod db_tests { .nodes() .node(VID(id)) .temp_prop_rows() - .map(|(t, row)| (t, row.into_iter().map(|(_, p)| p).collect::>())) + .map(|(t, _, row)| (t, row.into_iter().map(|(_, p)| p).collect::>())) .collect::>(); let expected = vec![( TimeIndexEntry::new(id as i64, id), - vec![Some(Prop::U64((id as u64) + 1))], + vec![Prop::U64((id as u64) + 1)], )]; assert_eq!(actual, expected); } @@ -1741,21 +1740,21 @@ mod db_tests { .core_graph() .nodes() .node(vid) - .temp_prop_rows_window(range) - .map(|(t, row)| (t, row.into_iter().map(|(_, p)| p).collect::>())) + .temp_prop_rows_range(Some(range)) + .map(|(t, _, row)| (t, row.into_iter().map(|(_, p)| p).collect::>())) .collect::>() }; let actual = get_rows(VID(0), TimeIndexEntry::new(2, 0)..TimeIndexEntry::new(3, 0)); - let expected = vec![(TimeIndexEntry::new(2, 2), vec![Some(Prop::U64(3))])]; + let expected = vec![(TimeIndexEntry::new(2, 2), vec![Prop::U64(3)])]; assert_eq!(actual, expected); let actual = get_rows(VID(0), TimeIndexEntry::new(0, 0)..TimeIndexEntry::new(3, 0)); let expected = vec![ - (TimeIndexEntry::new(0, 0), vec![Some(Prop::U64(1))]), - (TimeIndexEntry::new(1, 1), vec![Some(Prop::U64(2))]), - (TimeIndexEntry::new(2, 2), vec![Some(Prop::U64(3))]), + (TimeIndexEntry::new(0, 0), vec![Prop::U64(1)]), + (TimeIndexEntry::new(1, 1), vec![Prop::U64(2)]), + (TimeIndexEntry::new(2, 2), vec![Prop::U64(3)]), ]; assert_eq!(actual, expected); diff --git a/raphtory/src/db/graph/views/deletion_graph.rs b/raphtory/src/db/graph/views/deletion_graph.rs index 41754756cc..a9b22caf2a 100644 --- a/raphtory/src/db/graph/views/deletion_graph.rs +++ b/raphtory/src/db/graph/views/deletion_graph.rs @@ -26,7 +26,6 @@ use raphtory_storage::{ }, mutation::InheritMutationOps, }; -use serde::{Deserialize, Serialize}; use std::{ fmt::{Display, Formatter}, iter, @@ -42,7 +41,7 @@ use std::{ /// the edge is not considered active at the start of the window, even if there are simultaneous addition events. /// /// -#[derive(Clone, Debug, Serialize, Deserialize, Default)] +#[derive(Clone, Debug, Default)] pub struct PersistentGraph(pub(crate) Arc); impl Static for PersistentGraph {} diff --git a/raphtory/src/serialise/incremental.rs b/raphtory/src/serialise/incremental.rs index 97b7d3b6d6..e34f4681f8 100644 --- a/raphtory/src/serialise/incremental.rs +++ b/raphtory/src/serialise/incremental.rs @@ -247,13 +247,11 @@ pub trait InternalCache { impl InternalCache for Storage { fn init_cache(&self, path: &GraphFolder) -> Result<(), GraphError> { - self.cache - .get_or_try_init(|| GraphWriter::new(path.clone()))?; Ok(()) } fn get_cache(&self) -> Option<&GraphWriter> { - self.cache.get() + None } } diff --git a/raphtory/src/serialise/serialise.rs b/raphtory/src/serialise/serialise.rs index fac1c05353..e1db2094e8 100644 --- a/raphtory/src/serialise/serialise.rs +++ b/raphtory/src/serialise/serialise.rs @@ -188,17 +188,17 @@ impl StableEncode for GraphStorage { let node = nodes.node(VID(node_id)); graph.new_node(node.id(), node.vid(), node.node_type_id()); - for (time, row) in node.temp_prop_rows_range() { + for (time, _, row) in node.temp_prop_rows() { graph.update_node_tprops( node.vid(), time, - row.into_iter().filter_map(|(id, prop)| Some((id, prop?))), + row.into_iter(), ); } graph.update_node_cprops( node.vid(), - (0..n_const_meta.len()).flat_map(|i| node.prop(i).map(|v| (i, v))), + (0..n_const_meta.len()).flat_map(|i| node.constant_prop_layer(0, i).map(|v| (i, v))), ); } @@ -282,319 +282,8 @@ impl StableEncode for MaterializedGraph { impl InternalStableDecode for TemporalGraph { fn decode_from_proto(graph: &proto::Graph) -> Result { - let storage = Self::default(); - graph.metas.par_iter().for_each(|meta| { - if let Some(meta) = meta.meta.as_ref() { - match meta { - Meta::NewNodeType(node_type) => { - storage - .node_meta - .node_type_meta() - .set_id(node_type.name.as_str(), node_type.id as usize); - } - Meta::NewNodeCprop(node_cprop) => { - let p_type = node_cprop.prop_type(); - storage.node_meta.const_prop_meta().set_id_and_dtype( - node_cprop.name.as_str(), - node_cprop.id as usize, - p_type, - ) - } - Meta::NewNodeTprop(node_tprop) => { - let p_type = node_tprop.prop_type(); - storage.node_meta.temporal_prop_meta().set_id_and_dtype( - node_tprop.name.as_str(), - node_tprop.id as usize, - p_type, - ) - } - Meta::NewGraphCprop(graph_cprop) => storage - .graph_meta - .const_prop_meta() - .set_id(graph_cprop.name.as_str(), graph_cprop.id as usize), - Meta::NewGraphTprop(graph_tprop) => { - let p_type = graph_tprop.prop_type(); - storage.graph_meta.temporal_prop_meta().set_id_and_dtype( - graph_tprop.name.as_str(), - graph_tprop.id as usize, - p_type, - ) - } - Meta::NewLayer(new_layer) => storage - .edge_meta - .layer_meta() - .set_id(new_layer.name.as_str(), new_layer.id as usize), - Meta::NewEdgeCprop(edge_cprop) => { - let p_type = edge_cprop.prop_type(); - storage.edge_meta.const_prop_meta().set_id_and_dtype( - edge_cprop.name.as_str(), - edge_cprop.id as usize, - p_type, - ) - } - Meta::NewEdgeTprop(edge_tprop) => { - let p_type = edge_tprop.prop_type(); - storage.edge_meta.temporal_prop_meta().set_id_and_dtype( - edge_tprop.name.as_str(), - edge_tprop.id as usize, - p_type, - ) - } - } - } - }); - - let new_edge_property_types = storage - .write_lock_edges()? - .into_par_iter_mut() - .map(|mut shard| { - let mut const_prop_types = - vec![PropType::Empty; storage.edge_meta.const_prop_meta().len()]; - let mut temporal_prop_types = - vec![PropType::Empty; storage.edge_meta.temporal_prop_meta().len()]; - - for edge in graph.edges.iter() { - if let Some(mut new_edge) = shard.get_mut(edge.eid()) { - let edge_store = new_edge.edge_store_mut(); - edge_store.src = edge.src(); - edge_store.dst = edge.dst(); - edge_store.eid = edge.eid(); - } - } - for update in graph.updates.iter() { - if let Some(update) = update.update.as_ref() { - match update { - Update::DelEdge(del_edge) => { - if let Some(mut edge_mut) = shard.get_mut(del_edge.eid()) { - edge_mut - .deletions_mut(del_edge.layer_id()) - .insert(del_edge.time()); - storage.update_time(del_edge.time()); - } - } - Update::UpdateEdgeCprops(update) => { - if let Some(mut edge_mut) = shard.get_mut(update.eid()) { - let edge_layer = edge_mut.layer_mut(update.layer_id()); - for prop_update in update.props() { - let (id, prop) = prop_update?; - let prop = storage.process_prop_value(&prop); - if let Ok(new_type) = unify_types( - &const_prop_types[id], - &prop.dtype(), - &mut false, - ) { - const_prop_types[id] = new_type; // the original types saved in protos are now incomplete we need to update them - } - edge_layer.update_constant_prop(id, prop)?; - } - } - } - Update::UpdateEdgeTprops(update) => { - if let Some(mut edge_mut) = shard.get_mut(update.eid()) { - edge_mut - .additions_mut(update.layer_id()) - .insert(update.time()); - if update.has_props() { - let edge_layer = edge_mut.layer_mut(update.layer_id()); - for prop_update in update.props() { - let (id, prop) = prop_update?; - let prop = storage.process_prop_value(&prop); - if let Ok(new_type) = unify_types( - &temporal_prop_types[id], - &prop.dtype(), - &mut false, - ) { - temporal_prop_types[id] = new_type; - // the original types saved in protos are now incomplete we need to update them - } - edge_layer.add_prop(update.time(), id, prop)?; - } - } - storage.update_time(update.time()) - } - } - _ => {} - } - } - } - Ok::<_, GraphError>((const_prop_types, temporal_prop_types)) - }) - .try_reduce_with(|(l_const, l_temp), (r_const, r_temp)| { - unify_property_types(&l_const, &r_const, &l_temp, &r_temp) - }) - .transpose()?; - - if let Some((const_prop_types, temp_prop_types)) = new_edge_property_types { - update_meta( - const_prop_types, - temp_prop_types, - storage.edge_meta.const_prop_meta(), - storage.edge_meta.temporal_prop_meta(), - ); - } - - let new_nodes_property_types = storage - .write_lock_nodes()? - .into_par_iter_mut() - .map(|mut shard| { - let mut const_prop_types = - vec![PropType::Empty; storage.node_meta.const_prop_meta().len()]; - let mut temporal_prop_types = - vec![PropType::Empty; storage.node_meta.temporal_prop_meta().len()]; - - for node in graph.nodes.iter() { - let vid = VID(node.vid as usize); - let gid = match node.gid.as_ref().unwrap() { - Gid::GidStr(name) => GidRef::Str(name), - Gid::GidU64(gid) => GidRef::U64(*gid), - }; - if let Some(mut node_store) = shard.set(vid, gid) { - storage.logical_to_physical.set(gid, vid)?; - node_store.node_store_mut().node_type = node.type_id as usize; - } - } - let edges = storage.storage.edges.read_lock(); - for edge in edges.iter() { - if let Some(src) = shard.get_mut(edge.src()) { - for layer in edge.layer_ids_iter(&LayerIds::All) { - src.add_edge(edge.dst(), Direction::OUT, layer, edge.eid()); - for t in edge.additions(layer).iter() { - src.update_time(t, edge.eid().with_layer(layer)); - } - for t in edge.deletions(layer).iter() { - src.update_time(t, edge.eid().with_layer_deletion(layer)); - } - } - } - if let Some(dst) = shard.get_mut(edge.dst()) { - for layer in edge.layer_ids_iter(&LayerIds::All) { - dst.add_edge(edge.src(), Direction::IN, layer, edge.eid()); - for t in edge.additions(layer).iter() { - dst.update_time(t, edge.eid().with_layer(layer)); - } - for t in edge.deletions(layer).iter() { - dst.update_time(t, edge.eid().with_layer_deletion(layer)); - } - } - } - } - for update in graph.updates.iter() { - if let Some(update) = update.update.as_ref() { - match update { - Update::UpdateNodeCprops(update) => { - if let Some(node) = shard.get_mut(update.vid()) { - for prop_update in update.props() { - let (id, prop) = prop_update?; - let prop = storage.process_prop_value(&prop); - if let Ok(new_type) = unify_types( - &const_prop_types[id], - &prop.dtype(), - &mut false, - ) { - const_prop_types[id] = new_type; // the original types saved in protos are now incomplete we need to update them - } - node.update_constant_prop(id, prop)?; - } - } - } - Update::UpdateNodeTprops(update) => { - if let Some(mut node) = shard.get_mut_entry(update.vid()) { - let mut props = vec![]; - for prop_update in update.props() { - let (id, prop) = prop_update?; - let prop = storage.process_prop_value(&prop); - if let Ok(new_type) = unify_types( - &temporal_prop_types[id], - &prop.dtype(), - &mut false, - ) { - temporal_prop_types[id] = new_type; // the original types saved in protos are now incomplete we need to update them - } - props.push((id, prop)); - } - - if props.is_empty() { - node.node_store_mut() - .update_t_prop_time(update.time(), None); - } else { - let prop_offset = node.t_props_log_mut().push(props)?; - node.node_store_mut() - .update_t_prop_time(update.time(), prop_offset); - } - - storage.update_time(update.time()) - } - } - Update::UpdateNodeType(update) => { - if let Some(node) = shard.get_mut(update.vid()) { - node.node_type = update.type_id(); - } - } - _ => {} - } - } - } - Ok::<_, GraphError>((const_prop_types, temporal_prop_types)) - }) - .try_reduce_with(|(l_const, l_temp), (r_const, r_temp)| { - unify_property_types(&l_const, &r_const, &l_temp, &r_temp) - }) - .transpose()?; - - if let Some((const_prop_types, temp_prop_types)) = new_nodes_property_types { - update_meta( - const_prop_types, - temp_prop_types, - storage.node_meta.const_prop_meta(), - storage.node_meta.temporal_prop_meta(), - ); - } - - let graph_prop_new_types = graph - .updates - .par_iter() - .map(|update| { - let mut const_prop_types = - vec![PropType::Empty; storage.graph_meta.const_prop_meta().len()]; - let mut graph_prop_types = - vec![PropType::Empty; storage.graph_meta.temporal_prop_meta().len()]; - if let Some(update) = update.update.as_ref() { - match update { - Update::UpdateGraphCprops(props) => { - let c_props = proto_ext::collect_props(&props.properties)?; - for (id, prop) in &c_props { - const_prop_types[*id] = prop.dtype(); - } - storage.internal_update_constant_properties(&c_props)?; - } - Update::UpdateGraphTprops(props) => { - let time = TimeIndexEntry(props.time, props.secondary as usize); - let t_props = proto_ext::collect_props(&props.properties)?; - for (id, prop) in &t_props { - graph_prop_types[*id] = prop.dtype(); - } - storage.internal_add_properties(time, &t_props)?; - } - _ => {} - } - } - Ok::<_, GraphError>((const_prop_types, graph_prop_types)) - }) - .try_reduce_with(|(l_const, l_temp), (r_const, r_temp)| { - unify_property_types(&l_const, &r_const, &l_temp, &r_temp) - }) - .transpose()?; - - if let Some((const_prop_types, temp_prop_types)) = graph_prop_new_types { - update_meta( - const_prop_types, - temp_prop_types, - &PropMapper::default(), - storage.graph_meta.temporal_prop_meta(), - ); - } - Ok(storage) + todo!("remove this stuff!") } } @@ -636,848 +325,24 @@ fn unify_property_types( impl InternalStableDecode for GraphStorage { fn decode_from_proto(graph: &proto::Graph) -> Result { - Ok(GraphStorage::Unlocked(Arc::new( - TemporalGraph::decode_from_proto(graph)?, - ))) + todo!("remove this stuff!") } } impl InternalStableDecode for MaterializedGraph { fn decode_from_proto(graph: &proto::Graph) -> Result { - let storage = GraphStorage::decode_from_proto(graph)?; - let graph = match graph.graph_type() { - proto::GraphType::Event => Self::EventGraph(Graph::from_internal_graph(storage)), - proto::GraphType::Persistent => { - Self::PersistentGraph(PersistentGraph::from_internal_graph(storage)) - } - }; - Ok(graph) + todo!("remove this stuff!") } } impl InternalStableDecode for Graph { fn decode_from_proto(graph: &proto::Graph) -> Result { - match graph.graph_type() { - proto::GraphType::Event => { - let storage = GraphStorage::decode_from_proto(graph)?; - Ok(Graph::from_internal_graph(storage)) - } - proto::GraphType::Persistent => Err(GraphError::GraphLoadError), - } + todo!("remove this stuff!") } } impl InternalStableDecode for PersistentGraph { fn decode_from_proto(graph: &proto::Graph) -> Result { - match graph.graph_type() { - proto::GraphType::Event => Err(GraphError::GraphLoadError), - proto::GraphType::Persistent => { - let storage = GraphStorage::decode_from_proto(graph)?; - Ok(PersistentGraph::from_internal_graph(storage)) - } - } - } -} - -#[cfg(test)] -mod proto_test { - use std::{collections::HashMap, path::PathBuf}; - - use arrow_array::types::{Int32Type, UInt8Type}; - use tempfile::TempDir; - - use super::*; - use crate::{ - db::{ - api::{mutation::DeletionOps, properties::internal::ConstantPropertiesOps}, - graph::graph::assert_graph_equal, - }, - prelude::*, - serialise::{metadata::assert_metadata_correct, proto::GraphType, ProtoGraph}, - test_utils::{build_edge_list, build_graph_from_edge_list}, - }; - use chrono::{DateTime, NaiveDateTime}; - use proptest::proptest; - use raphtory_api::core::storage::arc_str::ArcStr; - - #[test] - fn prev_proto_str() { - let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .parent() - .map(|p| p.join("raphtory/resources/test/old_proto/str")) - .unwrap(); - - let graph = Graph::decode(path).unwrap(); - - let nodes = graph - .nodes() - .properties() - .into_iter() - .flat_map(|(_, props)| props.into_iter()) - .collect::>(); - assert_eq!( - nodes, - vec![ - ("a".into(), Some("a".into())), - ("z".into(), Some("a".into())), - ("a".into(), None) - ] - ); - } - #[test] - fn can_read_previous_proto() { - let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .parent() - .map(|p| p.join("raphtory/resources/test/old_proto/all_props")) - .unwrap(); - - let graph = Graph::decode(path).unwrap(); - - let actual: HashMap<_, _> = graph - .const_prop_keys() - .map(|key| { - let props = graph - .nodes() - .properties() - .into_iter() - .map(|(_, prop)| prop.get(&key)) - .collect::>(); - (key, props) - }) - .collect(); - - let expected: HashMap>> = [ - ( - "name".into(), - vec![ - Some("Alice".into()), - Some("Alice".into()), - Some("Alice".into()), - ], - ), - ( - "age".into(), - vec![ - Some(Prop::U32(47)), - Some(Prop::U32(47)), - Some(Prop::U32(47)), - ], - ), - ("doc".into(), vec![None, None, None]), - ( - "dtime".into(), - vec![ - Some(Prop::DTime( - DateTime::parse_from_rfc3339("2021-09-09T01:46:39Z") - .unwrap() - .into(), - )), - Some(Prop::DTime( - DateTime::parse_from_rfc3339("2021-09-09T01:46:39Z") - .unwrap() - .into(), - )), - Some(Prop::DTime( - DateTime::parse_from_rfc3339("2021-09-09T01:46:39Z") - .unwrap() - .into(), - )), - ], - ), - ( - "score".into(), - vec![ - Some(Prop::I32(27)), - Some(Prop::I32(27)), - Some(Prop::I32(27)), - ], - ), - ("graph".into(), vec![None, None, None]), - ("p_graph".into(), vec![None, None, None]), - ( - "time".into(), - vec![ - Some(Prop::NDTime( - NaiveDateTime::parse_from_str("+10000-09-09 01:46:39", "%Y-%m-%d %H:%M:%S") - .expect("Failed to parse time"), - )), - Some(Prop::NDTime( - NaiveDateTime::parse_from_str("+10000-09-09 01:46:39", "%Y-%m-%d %H:%M:%S") - .expect("Failed to parse time"), - )), - Some(Prop::NDTime( - NaiveDateTime::parse_from_str("+10000-09-09 01:46:39", "%Y-%m-%d %H:%M:%S") - .expect("Failed to parse time"), - )), - ], - ), - ( - "is_adult".into(), - vec![ - Some(Prop::Bool(true)), - Some(Prop::Bool(true)), - Some(Prop::Bool(true)), - ], - ), - ( - "height".into(), - vec![ - Some(Prop::F32(1.75)), - Some(Prop::F32(1.75)), - Some(Prop::F32(1.75)), - ], - ), - ( - "weight".into(), - vec![ - Some(Prop::F64(75.5)), - Some(Prop::F64(75.5)), - Some(Prop::F64(75.5)), - ], - ), - ( - "children".into(), - vec![ - Some(Prop::List( - vec![Prop::str("Bob"), Prop::str("Charlie")].into(), - )), - Some(Prop::List( - vec![Prop::str("Bob"), Prop::str("Charlie")].into(), - )), - Some(Prop::List( - vec![Prop::str("Bob"), Prop::str("Charlie")].into(), - )), - ], - ), - ( - "properties".into(), - vec![ - Some(Prop::map(vec![ - ("is_adult", Prop::Bool(true)), - ("weight", Prop::F64(75.5)), - ( - "children", - Prop::List(vec![Prop::str("Bob"), Prop::str("Charlie")].into()), - ), - ("height", Prop::F32(1.75)), - ("name", Prop::str("Alice")), - ("age", Prop::U32(47)), - ("score", Prop::I32(27)), - ])), - Some(Prop::map(vec![ - ("is_adult", Prop::Bool(true)), - ("age", Prop::U32(47)), - ("name", Prop::str("Alice")), - ("score", Prop::I32(27)), - ("height", Prop::F32(1.75)), - ( - "children", - Prop::List(vec![Prop::str("Bob"), Prop::str("Charlie")].into()), - ), - ("weight", Prop::F64(75.5)), - ])), - Some(Prop::map(vec![ - ("weight", Prop::F64(75.5)), - ("name", Prop::str("Alice")), - ("age", Prop::U32(47)), - ("height", Prop::F32(1.75)), - ("score", Prop::I32(27)), - ( - "children", - Prop::List(vec![Prop::str("Bob"), Prop::str("Charlie")].into()), - ), - ("is_adult", Prop::Bool(true)), - ])), - ], - ), - ] - .into_iter() - .collect(); - - let check_prop_mapper = |pm: &PropMapper| { - assert_eq!( - pm.get_id("properties").and_then(|id| pm.get_dtype(id)), - Some(PropType::map([ - ("is_adult", PropType::Bool), - ("weight", PropType::F64), - ("children", PropType::List(Box::new(PropType::Str))), - ("height", PropType::F32), - ("name", PropType::Str), - ("age", PropType::U32), - ("score", PropType::I32), - ])) - ); - assert_eq!( - pm.get_id("children").and_then(|id| pm.get_dtype(id)), - Some(PropType::List(Box::new(PropType::Str))) - ); - }; - - let pm = graph.node_meta().const_prop_meta(); - check_prop_mapper(pm); - - let pm = graph.edge_meta().temporal_prop_meta(); - check_prop_mapper(pm); - - let pm = graph.graph_meta().temporal_prop_meta(); - check_prop_mapper(pm); - - let mut vec1 = actual.keys().collect::>(); - let mut vec2 = expected.keys().collect::>(); - vec1.sort(); - vec2.sort(); - assert_eq!(vec1, vec2); - for (key, actual_props) in actual.iter() { - let expected_props = expected.get(key).unwrap(); - assert_eq!(actual_props, expected_props, "Key: {}", key); - } - } - - #[test] - fn node_no_props() { - let tempdir = TempDir::new().unwrap(); - let temp_file = tempdir.path().join("graph"); - let g1 = Graph::new(); - g1.add_node(1, "Alice", NO_PROPS, None).unwrap(); - g1.encode(&temp_file).unwrap(); - let g2 = Graph::decode(&temp_file).unwrap(); - assert_graph_equal(&g1, &g2); - } - - #[test] - fn node_with_props() { - let tempdir = TempDir::new().unwrap(); - let temp_file = tempdir.path().join("graph"); - let g1 = Graph::new(); - g1.add_node(1, "Alice", NO_PROPS, None).unwrap(); - g1.add_node(2, "Bob", [("age", Prop::U32(47))], None) - .unwrap(); - g1.encode(&temp_file).unwrap(); - let g2 = Graph::decode(&temp_file).unwrap(); - assert_graph_equal(&g1, &g2); - } - - #[cfg(feature = "search")] - #[test] - fn test_node_name() { - let g = Graph::new(); - g.add_edge(1, "ben", "hamza", NO_PROPS, None).unwrap(); - g.add_edge(2, "haaroon", "hamza", NO_PROPS, None).unwrap(); - g.add_edge(3, "ben", "haaroon", NO_PROPS, None).unwrap(); - let temp_file = TempDir::new().unwrap(); - - g.encode(&temp_file).unwrap(); - let g2 = MaterializedGraph::load_cached(&temp_file).unwrap(); - assert_eq!(g2.nodes().name().collect_vec(), ["ben", "hamza", "haaroon"]); - let node_names: Vec<_> = g2.nodes().iter().map(|n| n.name()).collect(); - assert_eq!(node_names, ["ben", "hamza", "haaroon"]); - let g2_m = g2.materialize().unwrap(); - assert_eq!( - g2_m.nodes().name().collect_vec(), - ["ben", "hamza", "haaroon"] - ); - let g3 = g.materialize().unwrap(); - assert_eq!(g3.nodes().name().collect_vec(), ["ben", "hamza", "haaroon"]); - let node_names: Vec<_> = g3.nodes().iter().map(|n| n.name()).collect(); - assert_eq!(node_names, ["ben", "hamza", "haaroon"]); - - let temp_file = TempDir::new().unwrap(); - g3.encode(&temp_file).unwrap(); - let g4 = MaterializedGraph::decode(&temp_file).unwrap(); - assert_eq!(g4.nodes().name().collect_vec(), ["ben", "hamza", "haaroon"]); - let node_names: Vec<_> = g4.nodes().iter().map(|n| n.name()).collect(); - assert_eq!(node_names, ["ben", "hamza", "haaroon"]); - } - - #[test] - fn node_with_const_props() { - let tempdir = TempDir::new().unwrap(); - let temp_file = tempdir.path().join("graph"); - let g1 = Graph::new(); - g1.add_node(1, "Alice", NO_PROPS, None).unwrap(); - let n1 = g1 - .add_node(2, "Bob", [("age", Prop::U32(47))], None) - .unwrap(); - - n1.update_constant_properties([("name", Prop::Str("Bob".into()))]) - .expect("Failed to update constant properties"); - - g1.encode(&temp_file).unwrap(); - let g2 = Graph::decode(&temp_file).unwrap(); - assert_graph_equal(&g1, &g2); - } - - #[test] - fn edge_no_props() { - let tempdir = TempDir::new().unwrap(); - let temp_file = tempdir.path().join("graph"); - let g1 = Graph::new(); - g1.add_node(1, "Alice", NO_PROPS, None).unwrap(); - g1.add_node(2, "Bob", NO_PROPS, None).unwrap(); - g1.add_edge(3, "Alice", "Bob", NO_PROPS, None).unwrap(); - g1.encode(&temp_file).unwrap(); - let g2 = Graph::decode(&temp_file).unwrap(); - assert_graph_equal(&g1, &g2); - } - - #[test] - fn edge_no_props_delete() { - let tempdir = TempDir::new().unwrap(); - let temp_file = tempdir.path().join("graph"); - let g1 = Graph::new().persistent_graph(); - g1.add_edge(3, "Alice", "Bob", NO_PROPS, None).unwrap(); - g1.delete_edge(19, "Alice", "Bob", None).unwrap(); - g1.encode(&temp_file).unwrap(); - let g2 = PersistentGraph::decode(&temp_file).unwrap(); - assert_graph_equal(&g1, &g2); - - let edge = g2.edge("Alice", "Bob").expect("Failed to get edge"); - let deletions = edge.deletions().to_vec(); - assert_eq!(deletions, vec![19]); - } - - #[test] - fn edge_t_props() { - let tempdir = TempDir::new().unwrap(); - let temp_file = tempdir.path().join("graph"); - let g1 = Graph::new(); - g1.add_node(1, "Alice", NO_PROPS, None).unwrap(); - g1.add_node(2, "Bob", NO_PROPS, None).unwrap(); - g1.add_edge(3, "Alice", "Bob", [("kind", "friends")], None) - .unwrap(); - - g1.add_edge( - 3, - "Alice", - "Bob", - [("image", Prop::from_arr::(vec![3i32, 5]))], - None, - ) - .unwrap(); - g1.encode(&temp_file).unwrap(); - let g2 = Graph::decode(&temp_file).unwrap(); - assert_graph_equal(&g1, &g2); - } - - #[test] - fn edge_const_props() { - let tempdir = TempDir::new().unwrap(); - let temp_file = tempdir.path().join("graph"); - let g1 = Graph::new(); - let e1 = g1.add_edge(3, "Alice", "Bob", NO_PROPS, None).unwrap(); - e1.update_constant_properties([("friends", true)], None) - .expect("Failed to update constant properties"); - g1.encode(&temp_file).unwrap(); - let g2 = Graph::decode(&temp_file).unwrap(); - assert_graph_equal(&g1, &g2); - } - - #[test] - fn edge_layers() { - let tempdir = TempDir::new().unwrap(); - let temp_file = tempdir.path().join("graph"); - let g1 = Graph::new(); - g1.add_edge(7, "Alice", "Bob", NO_PROPS, Some("one")) - .unwrap(); - g1.add_edge(7, "Bob", "Charlie", [("friends", false)], Some("two")) - .unwrap(); - g1.encode(&temp_file).unwrap(); - let g2 = Graph::decode(&temp_file).unwrap(); - assert_graph_equal(&g1, &g2); - } - - #[test] - fn test_all_the_t_props_on_node() { - let mut props = vec![]; - write_props_to_vec(&mut props); - - let tempdir = TempDir::new().unwrap(); - let temp_file = tempdir.path().join("graph"); - let g1 = Graph::new(); - g1.add_node(1, "Alice", props.clone(), None).unwrap(); - g1.encode(&temp_file).unwrap(); - let g2 = Graph::decode(&temp_file).unwrap(); - assert_graph_equal(&g1, &g2); - - let node = g2.node("Alice").expect("Failed to get node"); - - assert!(props.into_iter().all(|(name, expected)| { - node.properties() - .temporal() - .get(name) - .filter(|prop_view| { - let (t, prop) = prop_view.iter().next().expect("Failed to get prop"); - prop == expected && t == 1 - }) - .is_some() - })) - } - - #[test] - fn test_all_the_t_props_on_edge() { - let mut props = vec![]; - write_props_to_vec(&mut props); - - let tempdir = TempDir::new().unwrap(); - let temp_file = tempdir.path().join("graph"); - let g1 = Graph::new(); - g1.add_edge(1, "Alice", "Bob", props.clone(), None).unwrap(); - g1.encode(&temp_file).unwrap(); - let g2 = Graph::decode(&temp_file).unwrap(); - assert_graph_equal(&g1, &g2); - - let edge = g2.edge("Alice", "Bob").expect("Failed to get edge"); - - assert!(props.into_iter().all(|(name, expected)| { - edge.properties() - .temporal() - .get(name) - .filter(|prop_view| { - let (t, prop) = prop_view.iter().next().expect("Failed to get prop"); - prop == expected && t == 1 - }) - .is_some() - })) - } - - #[test] - fn test_all_the_const_props_on_edge() { - let mut props = vec![]; - write_props_to_vec(&mut props); - - let tempdir = TempDir::new().unwrap(); - let temp_file = tempdir.path().join("graph"); - let g1 = Graph::new(); - let e = g1.add_edge(1, "Alice", "Bob", NO_PROPS, Some("a")).unwrap(); - e.update_constant_properties(props.clone(), Some("a")) - .expect("Failed to update constant properties"); - g1.encode(&temp_file).unwrap(); - let g2 = Graph::decode(&temp_file).unwrap(); - assert_graph_equal(&g1, &g2); - - let edge = g2 - .edge("Alice", "Bob") - .expect("Failed to get edge") - .layers("a") - .unwrap(); - - for (new, old) in edge.properties().constant().iter().zip(props.iter()) { - assert_eq!(new.0, old.0); - assert_eq!(new.1, old.1); - } - } - - #[test] - fn test_all_the_const_props_on_node() { - let mut props = vec![]; - write_props_to_vec(&mut props); - - let tempdir = TempDir::new().unwrap(); - let temp_file = tempdir.path().join("graph"); - let g1 = Graph::new(); - let n = g1.add_node(1, "Alice", NO_PROPS, None).unwrap(); - n.update_constant_properties(props.clone()) - .expect("Failed to update constant properties"); - g1.encode(&temp_file).unwrap(); - let g2 = Graph::decode(&temp_file).unwrap(); - assert_graph_equal(&g1, &g2); - - let node = g2.node("Alice").expect("Failed to get node"); - - assert!(props.into_iter().all(|(name, expected)| { - node.properties() - .constant() - .get(name) - .filter(|prop| prop == &expected) - .is_some() - })) - } - - #[test] - fn graph_const_properties() { - let mut props = vec![]; - write_props_to_vec(&mut props); - - let g1 = Graph::new(); - g1.add_constant_properties(props.clone()) - .expect("Failed to add constant properties"); - - let tempdir = TempDir::new().unwrap(); - let temp_file = tempdir.path().join("graph"); - g1.encode(&temp_file).unwrap(); - let g2 = Graph::decode(&temp_file).unwrap(); - assert_graph_equal(&g1, &g2); - - props.into_iter().for_each(|(name, prop)| { - let id = g2.get_const_prop_id(name).expect("Failed to get prop id"); - assert_eq!(prop, g2.get_const_prop(id).expect("Failed to get prop")); - }); - } - - #[test] - fn graph_temp_properties() { - let mut props = vec![]; - write_props_to_vec(&mut props); - - let g1 = Graph::new(); - for t in 0..props.len() { - g1.add_properties(t as i64, props[t..t + 1].to_vec()) - .expect("Failed to add constant properties"); - } - - let tempdir = TempDir::new().unwrap(); - let temp_file = tempdir.path().join("graph"); - g1.encode(&temp_file).unwrap(); - let g2 = Graph::decode(&temp_file).unwrap(); - assert_graph_equal(&g1, &g2); - - props - .into_iter() - .enumerate() - .for_each(|(expected_t, (name, expected))| { - for (t, prop) in g2 - .properties() - .temporal() - .get(name) - .expect("Failed to get prop view") - { - assert_eq!(prop, expected); - assert_eq!(t, expected_t as i64); - } - }); - } - - #[test] - fn manually_test_append() { - let mut graph1 = proto::Graph::default(); - graph1.set_graph_type(GraphType::Event); - graph1.new_node(GidRef::Str("1"), VID(0), 0); - graph1.new_node(GidRef::Str("2"), VID(1), 0); - graph1.new_edge(VID(0), VID(1), EID(0)); - graph1.update_edge_tprops( - EID(0), - TimeIndexEntry::start(1), - 0, - iter::empty::<(usize, Prop)>(), - ); - let mut bytes1 = graph1.encode_to_vec(); - - let mut graph2 = proto::Graph::default(); - graph2.new_node(GidRef::Str("3"), VID(2), 0); - graph2.new_edge(VID(0), VID(2), EID(1)); - graph2.update_edge_tprops( - EID(1), - TimeIndexEntry::start(2), - 0, - iter::empty::<(usize, Prop)>(), - ); - bytes1.extend(graph2.encode_to_vec()); - - let graph = Graph::decode_from_bytes(&bytes1).unwrap(); - assert_eq!(graph.nodes().name().collect_vec(), ["1", "2", "3"]); - assert_eq!( - graph.edges().id().collect_vec(), - [ - (GID::Str("1".to_string()), GID::Str("2".to_string())), - (GID::Str("1".to_string()), GID::Str("3".to_string())) - ] - ) - } - - #[test] - fn test_string_interning() { - let g = Graph::new(); - let n = g.add_node(0, 1, [("test", "test")], None).unwrap(); - - n.add_updates(1, [("test", "test")]).unwrap(); - n.add_updates(2, [("test", "test")]).unwrap(); - - let values = n - .properties() - .temporal() - .get("test") - .unwrap() - .values() - .map(|v| v.unwrap_str()) - .collect_vec(); - assert_eq!(values, ["test", "test", "test"]); - for w in values.windows(2) { - assert_eq!(w[0].as_ptr(), w[1].as_ptr()); - } - - let proto = g.encode_to_proto(); - let g2 = Graph::decode_from_proto(&proto).unwrap(); - let node_view = g2.node(1).unwrap(); - - let values = node_view - .properties() - .temporal() - .get("test") - .unwrap() - .values() - .map(|v| v.unwrap_str()) - .collect_vec(); - assert_eq!(values, ["test", "test", "test"]); - for w in values.windows(2) { - assert_eq!(w[0].as_ptr(), w[1].as_ptr()); - } - } - - #[test] - fn test_incremental_writing_on_graph() { - let g = Graph::new(); - let mut props = vec![]; - write_props_to_vec(&mut props); - let temp_cache_file = tempfile::tempdir().unwrap(); - let folder = GraphFolder::from(&temp_cache_file); - - g.cache(&temp_cache_file).unwrap(); - - assert_metadata_correct(&folder, &g); - - for t in 0..props.len() { - g.add_properties(t as i64, props[t..t + 1].to_vec()) - .expect("Failed to add constant properties"); - } - g.write_updates().unwrap(); - - g.add_constant_properties(props.clone()) - .expect("Failed to add constant properties"); - g.write_updates().unwrap(); - - let n = g.add_node(1, "Alice", NO_PROPS, None).unwrap(); - n.update_constant_properties(props.clone()) - .expect("Failed to update constant properties"); - g.write_updates().unwrap(); - - let e = g.add_edge(1, "Alice", "Bob", NO_PROPS, Some("a")).unwrap(); - e.update_constant_properties(props.clone(), Some("a")) - .expect("Failed to update constant properties"); - g.write_updates().unwrap(); - - assert_metadata_correct(&folder, &g); - - g.add_edge(2, "Alice", "Bob", props.clone(), None).unwrap(); - g.add_node(1, "Charlie", props.clone(), None).unwrap(); - g.write_updates().unwrap(); - - g.add_edge(7, "Alice", "Bob", NO_PROPS, Some("one")) - .unwrap(); - g.add_edge(7, "Bob", "Charlie", [("friends", false)], Some("two")) - .unwrap(); - g.write_updates().unwrap(); - let g2 = Graph::decode(&temp_cache_file).unwrap(); - assert_graph_equal(&g, &g2); - - assert_metadata_correct(&folder, &g); - } - - #[test] - fn test_incremental_writing_on_persistent_graph() { - let g = PersistentGraph::new(); - let mut props = vec![]; - write_props_to_vec(&mut props); - let temp_cache_file = tempfile::tempdir().unwrap(); - let folder = GraphFolder::from(&temp_cache_file); - - g.cache(&temp_cache_file).unwrap(); - - for t in 0..props.len() { - g.add_properties(t as i64, props[t..t + 1].to_vec()) - .expect("Failed to add constant properties"); - } - g.write_updates().unwrap(); - - g.add_constant_properties(props.clone()) - .expect("Failed to add constant properties"); - g.write_updates().unwrap(); - - let n = g.add_node(1, "Alice", NO_PROPS, None).unwrap(); - n.update_constant_properties(props.clone()) - .expect("Failed to update constant properties"); - g.write_updates().unwrap(); - - let e = g.add_edge(1, "Alice", "Bob", NO_PROPS, Some("a")).unwrap(); - e.update_constant_properties(props.clone(), Some("a")) - .expect("Failed to update constant properties"); - g.write_updates().unwrap(); - - assert_metadata_correct(&folder, &g); - - g.add_edge(2, "Alice", "Bob", props.clone(), None).unwrap(); - g.add_node(1, "Charlie", props.clone(), None).unwrap(); - g.write_updates().unwrap(); - - g.add_edge(7, "Alice", "Bob", NO_PROPS, Some("one")) - .unwrap(); - g.add_edge(7, "Bob", "Charlie", [("friends", false)], Some("two")) - .unwrap(); - g.write_updates().unwrap(); - - let g2 = PersistentGraph::decode(&temp_cache_file).unwrap(); - - assert_graph_equal(&g, &g2); - - assert_metadata_correct(&folder, &g); - } - - // we rely on this to make sure writing no updates does not actually write anything to file - #[test] - fn empty_proto_is_empty_bytes() { - let proto = ProtoGraph::default(); - let bytes = proto.encode_to_vec(); - assert!(bytes.is_empty()) - } - - #[test] - fn encode_decode_prop_test() { - proptest!(|(edges in build_edge_list(100, 100))| { - let g = build_graph_from_edge_list(&edges); - let bytes = g.encode_to_vec(); - let g2 = Graph::decode_from_bytes(&bytes).unwrap(); - assert_graph_equal(&g, &g2); - }) - } - - fn write_props_to_vec(props: &mut Vec<(&str, Prop)>) { - props.push(("name", Prop::Str("Alice".into()))); - props.push(("age", Prop::U32(47))); - props.push(("score", Prop::I32(27))); - props.push(("is_adult", Prop::Bool(true))); - props.push(("height", Prop::F32(1.75))); - props.push(("weight", Prop::F64(75.5))); - props.push(( - "children", - Prop::List(Arc::new(vec![ - Prop::Str("Bob".into()), - Prop::Str("Charlie".into()), - ])), - )); - props.push(( - "properties", - Prop::map(props.iter().map(|(k, v)| (ArcStr::from(*k), v.clone()))), - )); - let fmt = "%Y-%m-%d %H:%M:%S"; - props.push(( - "time", - Prop::NDTime( - NaiveDateTime::parse_from_str("+10000-09-09 01:46:39", fmt) - .expect("Failed to parse time"), - ), - )); - - props.push(( - "dtime", - Prop::DTime( - DateTime::parse_from_rfc3339("2021-09-09T01:46:39Z") - .unwrap() - .into(), - ), - )); - - props.push(( - "array", - Prop::from_arr::(vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]), - )); + todo!("remove this stuff!") } } From 3cdeb28cd120c5b222e679a9ec0d5c6172608a8f Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Mon, 30 Jun 2025 17:35:31 +0100 Subject: [PATCH 053/321] split the edge timestamps and from t_prop timestamps in MemAdditions and TimeCell --- db4-storage/src/gen_ts.rs | 17 ++++++--- db4-storage/src/segments/additions.rs | 48 +++++++++++++++++++------- db4-storage/src/segments/edge_entry.rs | 23 ++++++------ db4-storage/src/segments/node_entry.rs | 29 ++++++++++------ 4 files changed, 78 insertions(+), 39 deletions(-) diff --git a/db4-storage/src/gen_ts.rs b/db4-storage/src/gen_ts.rs index 8864f2f2c4..cb967353d0 100644 --- a/db4-storage/src/gen_ts.rs +++ b/db4-storage/src/gen_ts.rs @@ -41,11 +41,18 @@ where { type TimeCell: TimeIndexOps<'a, IndexType = TimeIndexEntry>; - fn layer_time_cells( + fn t_props_time_cells( self, layer_id: usize, range: Option<(TimeIndexEntry, TimeIndexEntry)>, ) -> impl Iterator + 'a; + + fn additions_time_cells( + self, + layer_id: usize, + range: Option<(TimeIndexEntry, TimeIndexEntry)>, + ) -> impl Iterator + 'a; + fn num_layers(&self) -> usize; fn time_cells>( @@ -55,16 +62,16 @@ where ) -> impl Iterator + 'a { match layer_ids.borrow() { LayerIds::None => Iter4::I(std::iter::empty()), - LayerIds::One(layer_id) => Iter4::J(self.layer_time_cells(*layer_id, range)), + LayerIds::One(layer_id) => Iter4::J(self.t_props_time_cells(*layer_id, range)), LayerIds::All => Iter4::K( (0..self.num_layers()) - .flat_map(move |layer_id| self.layer_time_cells(layer_id, range)), + .flat_map(move |layer_id| self.t_props_time_cells(layer_id, range)), ), LayerIds::Multiple(layers) => Iter4::L( layers .clone() .into_iter() - .flat_map(move |layer_id| self.layer_time_cells(layer_id, range)), + .flat_map(move |layer_id| self.t_props_time_cells(layer_id, range)), ), } } @@ -93,7 +100,7 @@ impl<'a, Ref: WithTimeCells<'a> + 'a> GenericTimeOps<'a, Ref> { match self.layer_id { Either::Left(layer_ids) => Either::Left(self.node.time_cells(layer_ids, self.range)), Either::Right(layer_id) => { - Either::Right(self.node.layer_time_cells(layer_id, self.range)) + Either::Right(self.node.t_props_time_cells(layer_id, self.range)) } } } diff --git a/db4-storage/src/segments/additions.rs b/db4-storage/src/segments/additions.rs index 2b2041349e..4f82d38c0c 100644 --- a/db4-storage/src/segments/additions.rs +++ b/db4-storage/src/segments/additions.rs @@ -1,16 +1,30 @@ use std::ops::Range; use raphtory_core::{ - entities::nodes::node_store::PropTimestamps, + entities::{ELID, properties::tcell::TCell}, storage::timeindex::{TimeIndexEntry, TimeIndexOps, TimeIndexWindow}, }; -use crate::utils::Iter2; +use crate::utils::Iter4; #[derive(Clone, Debug)] pub enum MemAdditions<'a> { - Props(&'a PropTimestamps), - Window(TimeIndexWindow<'a, TimeIndexEntry, PropTimestamps>), + Edges(&'a TCell), + Props(&'a TCell>), + WEdges(TimeIndexWindow<'a, TimeIndexEntry, TCell>), + WProps(TimeIndexWindow<'a, TimeIndexEntry, TCell>>), +} + +impl<'a> From<&'a TCell> for MemAdditions<'a> { + fn from(edges: &'a TCell) -> Self { + MemAdditions::Edges(edges) + } +} + +impl<'a> From<&'a TCell>> for MemAdditions<'a> { + fn from(props: &'a TCell>) -> Self { + MemAdditions::Props(props) + } } impl<'a> TimeIndexOps<'a> for MemAdditions<'a> { @@ -21,35 +35,45 @@ impl<'a> TimeIndexOps<'a> for MemAdditions<'a> { fn active(&self, w: Range) -> bool { match self { MemAdditions::Props(props) => props.active(w), - MemAdditions::Window(window) => window.active(w), + MemAdditions::Edges(edges) => edges.active(w), + MemAdditions::WProps(window) => window.active(w), + MemAdditions::WEdges(window) => window.active(w), } } fn range(&self, w: Range) -> Self::RangeType { match self { - MemAdditions::Props(props) => MemAdditions::Window(props.range(w)), - MemAdditions::Window(window) => MemAdditions::Window(window.range(w)), + MemAdditions::Props(props) => MemAdditions::WProps(props.range(w)), + MemAdditions::Edges(edges) => MemAdditions::WEdges(edges.range(w)), + MemAdditions::WProps(window) => MemAdditions::WProps(window.range(w)), + MemAdditions::WEdges(window) => MemAdditions::WEdges(window.range(w)), } } fn iter(self) -> impl Iterator + Send + Sync + 'a { match self { - MemAdditions::Props(props) => Iter2::I1(props.iter()), - MemAdditions::Window(window) => Iter2::I2(window.iter()), + MemAdditions::Props(props) => Iter4::I(props.iter().map(|(k, _)| *k)), + MemAdditions::Edges(edges) => Iter4::J(edges.iter().map(|(k, _)| *k)), + MemAdditions::WProps(window) => Iter4::K(window.iter()), + MemAdditions::WEdges(window) => Iter4::L(window.iter()), } } fn iter_rev(self) -> impl Iterator + Send + Sync + 'a { match self { - MemAdditions::Props(props) => Iter2::I1(props.iter_rev()), - MemAdditions::Window(window) => Iter2::I2(window.iter_rev()), + MemAdditions::Props(props) => Iter4::I(props.iter_rev()), + MemAdditions::Edges(edges) => Iter4::J(edges.iter_rev()), + MemAdditions::WProps(window) => Iter4::K(window.iter_rev()), + MemAdditions::WEdges(window) => Iter4::L(window.iter_rev()), } } fn len(&self) -> usize { match self { MemAdditions::Props(props) => props.len(), - MemAdditions::Window(window) => window.len(), + MemAdditions::Edges(edges) => edges.len(), + MemAdditions::WProps(window) => window.len(), + MemAdditions::WEdges(window) => window.len(), } } } diff --git a/db4-storage/src/segments/edge_entry.rs b/db4-storage/src/segments/edge_entry.rs index 82a2aec1fd..4b1ce4a198 100644 --- a/db4-storage/src/segments/edge_entry.rs +++ b/db4-storage/src/segments/edge_entry.rs @@ -75,26 +75,27 @@ impl<'a> MemEdgeRef<'a> { impl<'a> WithTimeCells<'a> for MemEdgeRef<'a> { type TimeCell = MemAdditions<'a>; - fn layer_time_cells( + fn t_props_time_cells( self, layer_id: usize, range: Option<(TimeIndexEntry, TimeIndexEntry)>, ) -> impl Iterator + 'a { + let t_cell = MemAdditions::Props(self.es.as_ref()[layer_id].additions(self.pos).props_ts()); std::iter::once( range - .map(|(start, end)| { - MemAdditions::Window( - self.es.as_ref()[layer_id] - .additions(self.pos) - .range(start..end), - ) - }) - .unwrap_or_else(|| { - MemAdditions::Props(self.es.as_ref()[layer_id].additions(self.pos)) - }), + .map(|(start, end)| t_cell.range(start..end)) + .unwrap_or_else(|| t_cell), ) } + fn additions_time_cells( + self, + _layer_id: usize, + _range: Option<(TimeIndexEntry, TimeIndexEntry)>, + ) -> impl Iterator + 'a { + std::iter::empty() + } + fn num_layers(&self) -> usize { self.es.as_ref().len() } diff --git a/db4-storage/src/segments/node_entry.rs b/db4-storage/src/segments/node_entry.rs index 64c72efce4..0b069dc04d 100644 --- a/db4-storage/src/segments/node_entry.rs +++ b/db4-storage/src/segments/node_entry.rs @@ -70,23 +70,30 @@ impl<'a> MemNodeRef<'a> { impl<'a> WithTimeCells<'a> for MemNodeRef<'a> { type TimeCell = MemAdditions<'a>; - fn layer_time_cells( + fn t_props_time_cells( self, layer_id: usize, range: Option<(TimeIndexEntry, TimeIndexEntry)>, ) -> impl Iterator + 'a { + let t_cell = MemAdditions::Props(self.ns.as_ref()[layer_id].additions(self.pos).props_ts()); std::iter::once( range - .map(|(start, end)| { - MemAdditions::Window( - self.ns.as_ref()[layer_id] - .additions(self.pos) - .range(start..end), - ) - }) - .unwrap_or_else(|| { - MemAdditions::Props(self.ns.as_ref()[layer_id].additions(self.pos)) - }), + .map(|(start, end)| t_cell.range(start..end)) + .unwrap_or_else(|| t_cell), + ) + } + + fn additions_time_cells( + self, + layer_id: usize, + range: Option<(TimeIndexEntry, TimeIndexEntry)>, + ) -> impl Iterator + 'a { + let additions = + MemAdditions::Edges(self.ns.as_ref()[layer_id].additions(self.pos).edge_ts()); + std::iter::once( + range + .map(|(start, end)| additions.range(start..end)) + .unwrap_or_else(|| additions), ) } From 8674268d5273d31d1252343e40ead62ca5fbfb4d Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Tue, 1 Jul 2025 12:21:09 +0100 Subject: [PATCH 054/321] simplify and split the node prop additions and additions from edges --- db4-graph/src/lib.rs | 3 +- db4-storage/src/api/edges.rs | 9 - db4-storage/src/api/nodes.rs | 13 +- db4-storage/src/gen_ts.rs | 156 +++++++++++------- db4-storage/src/lib.rs | 20 ++- db4-storage/src/pages/mod.rs | 2 +- db4-storage/src/pages/test_utils/checkers.rs | 23 ++- db4-storage/src/resolver/mapping_resolver.rs | 12 +- db4-storage/src/resolver/mod.rs | 8 +- db4-storage/src/segments/additions.rs | 30 ++++ db4-storage/src/segments/edge_entry.rs | 14 +- db4-storage/src/segments/node_entry.rs | 27 ++- .../src/mutation/addition_ops_ext.rs | 22 ++- .../time_semantics/persistent_semantics.rs | 14 +- raphtory/src/db/graph/graph.rs | 5 +- raphtory/src/serialise/serialise.rs | 10 +- 16 files changed, 208 insertions(+), 160 deletions(-) diff --git a/db4-graph/src/lib.rs b/db4-graph/src/lib.rs index f7a207bcf2..3b36fb7036 100644 --- a/db4-graph/src/lib.rs +++ b/db4-graph/src/lib.rs @@ -31,8 +31,7 @@ use storage::{ }, persist::strategy::PersistentStrategy, resolver::{GIDResolverError, GIDResolverOps}, - Extension, GIDResolver, Layer, - ReadLockedLayer, ES, NS + Extension, GIDResolver, Layer, ReadLockedLayer, ES, NS, }; pub mod entries; diff --git a/db4-storage/src/api/edges.rs b/db4-storage/src/api/edges.rs index 58530cf291..358756613a 100644 --- a/db4-storage/src/api/edges.rs +++ b/db4-storage/src/api/edges.rs @@ -119,12 +119,6 @@ pub trait LockedESegment: Send + Sync + std::fmt::Debug { ) -> impl ParallelIterator> + Send + Sync + 'a; } -#[derive(Debug)] -pub struct ReadLockedES { - es: Arc, - head: ES::ArcLockedSegment, -} - pub trait EdgeEntryOps<'a>: Send + Sync { type Ref<'b>: EdgeRefOps<'b> where @@ -148,13 +142,10 @@ pub trait EdgeRefOps<'a>: Copy + Clone + Send + Sync { fn internal_num_layers(self) -> usize; - fn additions(self, layer_ids: &'a LayerIds) -> Self::Additions; - fn layer_additions(self, layer_id: usize) -> Self::Additions; fn c_prop(self, layer_id: usize, prop_id: usize) -> Option; - fn t_prop(self, layer_id: &'a LayerIds, prop_id: usize) -> Self::TProps; fn layer_t_prop(self, layer_id: usize, prop_id: usize) -> Self::TProps; fn src(&self) -> VID; diff --git a/db4-storage/src/api/nodes.rs b/db4-storage/src/api/nodes.rs index f681da2adf..00af3a7d4e 100644 --- a/db4-storage/src/api/nodes.rs +++ b/db4-storage/src/api/nodes.rs @@ -146,6 +146,8 @@ pub trait NodeEntryOps<'a>: Send + Sync + 'a { pub trait NodeRefOps<'a>: Copy + Clone + Send + Sync + 'a { type Additions: TimeIndexOps<'a>; + type EdgeAdditions: TimeIndexOps<'a>; + type TProps: TPropOps<'a>; fn out_edges(self, layer_id: usize) -> impl Iterator + Send + Sync + 'a; @@ -286,23 +288,16 @@ pub trait NodeRefOps<'a>: Copy + Clone + Send + Sync + 'a { self.inb_edges_sorted(layer_id).map(|(v, _)| v) } - fn additions(self, layers: &'a LayerIds) -> Self::Additions; + fn edge_additions(self, layer_id: usize) -> Self::EdgeAdditions; - fn layer_additions(self, layer_id: usize) -> Self::Additions; + fn node_additions(self, layer_id: usize) -> Self::Additions; fn c_prop(self, layer_id: usize, prop_id: usize) -> Option; fn c_prop_str(self, layer_id: usize, prop_id: usize) -> Option<&'a str>; - fn t_prop(self, layer_id: &'a LayerIds, prop_id: usize) -> Self::TProps; - fn temporal_prop_layer(self, layer_id: usize, prop_id: usize) -> Self::TProps; - fn t_props(self, layer_id: &'a LayerIds) -> impl Iterator { - (0..self.node_meta().temporal_prop_meta().len()) - .map(move |prop_id| (prop_id, self.t_prop(layer_id, prop_id))) - } - fn degree(self, layers: &LayerIds, dir: Direction) -> usize; fn find_edge(&self, dst: VID, layers: &LayerIds) -> Option; diff --git a/db4-storage/src/gen_ts.rs b/db4-storage/src/gen_ts.rs index cb967353d0..10904535a1 100644 --- a/db4-storage/src/gen_ts.rs +++ b/db4-storage/src/gen_ts.rs @@ -1,36 +1,33 @@ -use std::{borrow::Borrow, ops::Range}; +use std::ops::Range; -use either::Either; use itertools::Itertools; -use raphtory_core::{ - entities::LayerIds, - storage::timeindex::{TimeIndexEntry, TimeIndexOps}, -}; - -use crate::utils::Iter4; +use raphtory_core::storage::timeindex::{TimeIndexEntry, TimeIndexOps}; // TODO: split the Node time operations into edge additions and property additions #[derive(Clone, Copy)] pub struct GenericTimeOps<'a, Ref> { range: Option<(TimeIndexEntry, TimeIndexEntry)>, - layer_id: Either<&'a LayerIds, usize>, + layer_id: usize, node: Ref, + _mark: std::marker::PhantomData<&'a ()>, } impl<'a, Ref> GenericTimeOps<'a, Ref> { - pub fn new(node: Ref, layer_id: &'a LayerIds) -> Self { + pub fn new_with_layer(node: Ref, layer_id: usize) -> Self { Self { range: None, - layer_id: Either::Left(layer_id), + layer_id, node, + _mark: std::marker::PhantomData, } } - pub fn new_with_layer(node: Ref, layer_id: usize) -> Self { + pub fn new_additions_with_layer(node: Ref, layer_id: usize) -> Self { Self { range: None, - layer_id: Either::Right(layer_id), + layer_id, node, + _mark: std::marker::PhantomData, } } } @@ -41,68 +38,118 @@ where { type TimeCell: TimeIndexOps<'a, IndexType = TimeIndexEntry>; - fn t_props_time_cells( + fn t_props_tc( self, layer_id: usize, range: Option<(TimeIndexEntry, TimeIndexEntry)>, ) -> impl Iterator + 'a; - fn additions_time_cells( + fn additions_tc( self, layer_id: usize, range: Option<(TimeIndexEntry, TimeIndexEntry)>, ) -> impl Iterator + 'a; fn num_layers(&self) -> usize; +} + +#[derive(Clone, Copy)] +pub struct EdgeAdditionCellsRef<'a, Ref: WithTimeCells<'a> + 'a> { + node: Ref, + _mark: std::marker::PhantomData<&'a ()>, +} + +impl<'a, Ref: WithTimeCells<'a> + 'a> EdgeAdditionCellsRef<'a, Ref> { + pub fn new(node: Ref) -> Self { + Self { + node, + _mark: std::marker::PhantomData, + } + } +} + +impl<'a, Ref: WithTimeCells<'a> + 'a> WithTimeCells<'a> for EdgeAdditionCellsRef<'a, Ref> { + type TimeCell = Ref::TimeCell; - fn time_cells>( + fn t_props_tc( self, - layer_ids: B, + _layer_id: usize, + _range: Option<(TimeIndexEntry, TimeIndexEntry)>, + ) -> impl Iterator + 'a { + std::iter::empty() + } + + fn additions_tc( + self, + layer_id: usize, range: Option<(TimeIndexEntry, TimeIndexEntry)>, ) -> impl Iterator + 'a { - match layer_ids.borrow() { - LayerIds::None => Iter4::I(std::iter::empty()), - LayerIds::One(layer_id) => Iter4::J(self.t_props_time_cells(*layer_id, range)), - LayerIds::All => Iter4::K( - (0..self.num_layers()) - .flat_map(move |layer_id| self.t_props_time_cells(layer_id, range)), - ), - LayerIds::Multiple(layers) => Iter4::L( - layers - .clone() - .into_iter() - .flat_map(move |layer_id| self.t_props_time_cells(layer_id, range)), - ), + self.node.additions_tc(layer_id, range) + } + + fn num_layers(&self) -> usize { + self.node.num_layers() + } +} + +#[derive(Clone, Copy)] +pub struct PropAdditionCellsRef<'a, Ref: WithTimeCells<'a> + 'a> { + node: Ref, + _mark: std::marker::PhantomData<&'a ()>, +} + +impl<'a, Ref: WithTimeCells<'a> + 'a> PropAdditionCellsRef<'a, Ref> { + pub fn new(node: Ref) -> Self { + Self { + node, + _mark: std::marker::PhantomData, } } +} + +impl<'a, Ref: WithTimeCells<'a> + 'a> WithTimeCells<'a> for PropAdditionCellsRef<'a, Ref> { + type TimeCell = Ref::TimeCell; - fn into_iter>( + fn t_props_tc( self, - layer_ids: B, + layer_id: usize, range: Option<(TimeIndexEntry, TimeIndexEntry)>, - ) -> impl Iterator + Send + Sync + 'a { - let iters = self.time_cells(layer_ids, range); - iters.map(|cell| cell.iter()).kmerge() + ) -> impl Iterator + 'a { + self.node.t_props_tc(layer_id, range) } - fn into_iter_rev>( + fn additions_tc( self, - layer_ids: B, - range: Option<(TimeIndexEntry, TimeIndexEntry)>, - ) -> impl Iterator + Send + Sync + 'a { - let iters = self.time_cells(layer_ids, range); - iters.map(|cell| cell.iter_rev()).kmerge_by(|a, b| a > b) + _layer_id: usize, + _range: Option<(TimeIndexEntry, TimeIndexEntry)>, + ) -> impl Iterator + 'a { + std::iter::empty() + } + + fn num_layers(&self) -> usize { + self.node.num_layers() } } impl<'a, Ref: WithTimeCells<'a> + 'a> GenericTimeOps<'a, Ref> { pub fn time_cells(self) -> impl Iterator + 'a { - match self.layer_id { - Either::Left(layer_ids) => Either::Left(self.node.time_cells(layer_ids, self.range)), - Either::Right(layer_id) => { - Either::Right(self.node.t_props_time_cells(layer_id, self.range)) - } - } + let range = self.range; + let layer_id = self.layer_id; + + let t_cells = self.node.t_props_tc(layer_id, range); + let a_cells = self.node.additions_tc(layer_id, range); + + t_cells.chain(a_cells) + } + + fn into_iter(self) -> impl Iterator + Send + Sync + 'a { + let iters = self.time_cells(); + iters.map(|cell| cell.iter()).kmerge() + } + + fn into_iter_rev(self) -> impl Iterator + Send + Sync + 'a { + let iters = self.time_cells(); + iters.map(|cell| cell.iter_rev()).kmerge_by(|a, b| a > b) } } @@ -119,6 +166,7 @@ impl<'a, Ref: WithTimeCells<'a> + 'a> TimeIndexOps<'a> for GenericTimeOps<'a, Re range: Some((w.start, w.end)), node: self.node, layer_id: self.layer_id, + _mark: std::marker::PhantomData, } } @@ -131,21 +179,11 @@ impl<'a, Ref: WithTimeCells<'a> + 'a> TimeIndexOps<'a> for GenericTimeOps<'a, Re } fn iter(self) -> impl Iterator + Send + Sync + 'a { - match self.layer_id { - Either::Left(layer_id) => Either::Left(self.node.into_iter(layer_id, self.range)), - Either::Right(layer_id) => { - Either::Right(self.node.into_iter(LayerIds::One(layer_id), self.range)) - } - } + self.into_iter() } fn iter_rev(self) -> impl Iterator + Send + Sync + 'a { - match self.layer_id { - Either::Left(layer_id) => Either::Left(self.node.into_iter_rev(layer_id, self.range)), - Either::Right(layer_id) => { - Either::Right(self.node.into_iter_rev(LayerIds::One(layer_id), self.range)) - } - } + self.into_iter_rev() } fn len(&self) -> usize { diff --git a/db4-storage/src/lib.rs b/db4-storage/src/lib.rs index 702e21c3f0..f76c58c26b 100644 --- a/db4-storage/src/lib.rs +++ b/db4-storage/src/lib.rs @@ -1,15 +1,19 @@ use std::path::Path; use crate::{ - gen_t_props::GenTProps, gen_ts::GenericTimeOps, pages::{ - edge_store::ReadLockedEdgeStorage, node_store::ReadLockedNodeStorage, - GraphStore, ReadLockedGraphStore - }, resolver::mapping_resolver::MappingResolver, segments::{ + gen_t_props::GenTProps, + gen_ts::{EdgeAdditionCellsRef, GenericTimeOps, PropAdditionCellsRef}, + pages::{ + GraphStore, ReadLockedGraphStore, edge_store::ReadLockedEdgeStorage, + node_store::ReadLockedNodeStorage, + }, + resolver::mapping_resolver::MappingResolver, + segments::{ edge::EdgeSegmentView, edge_entry::{MemEdgeEntry, MemEdgeRef}, node::NodeSegmentView, node_entry::{MemNodeEntry, MemNodeRef}, - } + }, }; use raphtory_api::core::entities::{EID, VID}; use segments::{edge::MemEdgeSegment, node::MemNodeSegment}; @@ -20,9 +24,9 @@ pub mod gen_ts; pub mod pages; pub mod persist; pub mod properties; +pub mod resolver; pub mod segments; pub mod utils; -pub mod resolver; pub type Extension = (); pub type NS

= NodeSegmentView

; @@ -40,7 +44,9 @@ pub type EdgeEntry<'a> = MemEdgeEntry<'a, parking_lot::RwLockReadGuard<'a, MemEd pub type NodeEntryRef<'a> = MemNodeRef<'a>; pub type EdgeEntryRef<'a> = MemEdgeRef<'a>; -pub type NodeAdditions<'a> = GenericTimeOps<'a, MemNodeRef<'a>>; +pub type NodePropAdditions<'a> = GenericTimeOps<'a, PropAdditionCellsRef<'a, MemNodeRef<'a>>>; +pub type NodeEdgeAdditions<'a> = GenericTimeOps<'a, EdgeAdditionCellsRef<'a, MemNodeRef<'a>>>; + pub type EdgeAdditions<'a> = GenericTimeOps<'a, MemEdgeRef<'a>>; pub type NodeTProps<'a> = GenTProps<'a, MemNodeRef<'a>>; pub type EdgeTProps<'a> = GenTProps<'a, MemEdgeRef<'a>>; diff --git a/db4-storage/src/pages/mod.rs b/db4-storage/src/pages/mod.rs index a227071cfd..c4639ba60e 100644 --- a/db4-storage/src/pages/mod.rs +++ b/db4-storage/src/pages/mod.rs @@ -543,7 +543,7 @@ mod test { let node = g.nodes().node(3); let node_entry = node.as_ref(); - let actual: Vec<_> = node_entry.additions(&LayerIds::One(0)).iter_t().collect(); + let actual: Vec<_> = node_entry.edge_additions(0).iter_t().collect(); assert_eq!(actual, vec![4]); }; diff --git a/db4-storage/src/pages/test_utils/checkers.rs b/db4-storage/src/pages/test_utils/checkers.rs index 114ca327ae..7c0442c871 100644 --- a/db4-storage/src/pages/test_utils/checkers.rs +++ b/db4-storage/src/pages/test_utils/checkers.rs @@ -9,7 +9,7 @@ use raphtory_api::core::{ storage::dict_mapper::MaybeNew, }; use raphtory_core::{ - entities::{ELID, LayerIds, VID}, + entities::{ELID, VID}, storage::timeindex::TimeIndexOps, }; use rayon::prelude::*; @@ -259,7 +259,11 @@ pub fn check_graph_with_nodes_support< for (node, ts_expected) in ts_for_nodes { let ne = graph.nodes().node(node); let node_entry = ne.as_ref(); - let actual: Vec<_> = node_entry.additions(&LayerIds::One(0)).iter_t().collect(); + let actual: Vec<_> = node_entry + .edge_additions(0) + .iter_t() + .merge(node_entry.node_additions(0).iter_t()) + .collect(); assert_eq!( actual, ts_expected, "Expected node additions for node ({node:?})", @@ -327,7 +331,7 @@ pub fn check_graph_with_nodes_support< let ne = graph.nodes().node(node); let node_entry = ne.as_ref(); let actual_props = node_entry - .t_prop(&LayerIds::One(0), prop_id) + .temporal_prop_layer(0, prop_id) .iter_t() .collect::>(); @@ -445,8 +449,11 @@ pub fn check_graph_with_props_support< .unwrap_or_else(|| panic!("Failed to get edge ({:?}, {:?}) from graph", src, dst)); let edge = graph.edges().edge(edge); let e = edge.as_ref(); - let layer_id = LayerIds::One(0); - let actual_props = e.t_prop(&layer_id, prop_id).iter_t().collect::>(); + let layer_id = 0; + let actual_props = e + .layer_t_prop(layer_id, prop_id) + .iter_t() + .collect::>(); assert_eq!( actual_props, props, @@ -462,7 +469,7 @@ pub fn check_graph_with_props_support< .const_prop_meta() .get_id(name) .unwrap_or_else(|| panic!("Failed to get prop id for {}", name)); - let actual_props = e.c_prop(0, prop_id); + let actual_props = e.c_prop(layer_id, prop_id); assert_eq!( actual_props.as_ref(), Some(prop), @@ -480,9 +487,11 @@ pub fn check_graph_with_props_support< for (node_id, ts) in node_groups { let node = graph.nodes().node(node_id); let node_entry = node.as_ref(); + let actual_additions_ts = node_entry - .additions(&LayerIds::One(0)) + .edge_additions(0) .iter_t() + .merge(node_entry.node_additions(0).iter_t()) .collect::>(); assert_eq!( diff --git a/db4-storage/src/resolver/mapping_resolver.rs b/db4-storage/src/resolver/mapping_resolver.rs index 1d56c84fbf..0029aa01e0 100644 --- a/db4-storage/src/resolver/mapping_resolver.rs +++ b/db4-storage/src/resolver/mapping_resolver.rs @@ -1,10 +1,10 @@ -use std::path::Path; +use crate::resolver::{GIDResolverError, GIDResolverOps}; use raphtory_api::core::{ - entities::{GidRef, VID, GidType}, + entities::{GidRef, GidType, VID}, storage::dict_mapper::MaybeNew, }; -use raphtory_core::entities::graph::logical_to_physical::{Mapping}; -use crate::resolver::{GIDResolverOps, GIDResolverError}; +use raphtory_core::entities::graph::logical_to_physical::Mapping; +use std::path::Path; #[derive(Debug)] pub struct MappingResolver { @@ -13,7 +13,9 @@ pub struct MappingResolver { impl GIDResolverOps for MappingResolver { fn new(_path: impl AsRef) -> Result { - Ok(Self { mapping: Mapping::new() }) + Ok(Self { + mapping: Mapping::new(), + }) } fn len(&self) -> usize { diff --git a/db4-storage/src/resolver/mod.rs b/db4-storage/src/resolver/mod.rs index 20244bdf24..d1549af8ba 100644 --- a/db4-storage/src/resolver/mod.rs +++ b/db4-storage/src/resolver/mod.rs @@ -1,11 +1,11 @@ use std::path::Path; +use crate::error::DBV4Error; use raphtory_api::core::{ - entities::{GidRef, VID, GidType}, + entities::{GidRef, GidType, VID}, storage::dict_mapper::MaybeNew, }; use raphtory_core::entities::graph::logical_to_physical::InvalidNodeId; -use crate::error::DBV4Error; pub mod mapping_resolver; @@ -18,7 +18,9 @@ pub enum GIDResolverError { } pub trait GIDResolverOps { - fn new(path: impl AsRef) -> Result where Self: Sized; + fn new(path: impl AsRef) -> Result + where + Self: Sized; fn len(&self) -> usize; fn dtype(&self) -> Option; fn set(&self, gid: GidRef, vid: VID) -> Result<(), GIDResolverError>; diff --git a/db4-storage/src/segments/additions.rs b/db4-storage/src/segments/additions.rs index 4f82d38c0c..3be3c63217 100644 --- a/db4-storage/src/segments/additions.rs +++ b/db4-storage/src/segments/additions.rs @@ -27,6 +27,36 @@ impl<'a> From<&'a TCell>> for MemAdditions<'a> { } } +impl<'a> MemAdditions<'a> { + pub fn edge_events(self) -> impl Iterator + Send + Sync + 'a { + match self { + MemAdditions::Edges(edges) => Iter4::I(edges.iter().map(|(k, v)| (*k, *v))), + MemAdditions::WEdges(TimeIndexWindow::All(ti)) => { + Iter4::J(ti.iter().map(|(k, v)| (*k, *v))) + } + MemAdditions::WEdges(TimeIndexWindow::Range { timeindex, range }) => { + Iter4::K(timeindex.iter_window(range).map(|(k, v)| (*k, *v))) + } + _ => Iter4::L(std::iter::empty()), + } + } + + pub fn edge_events_rev( + self, + ) -> impl Iterator + Send + Sync + 'a { + match self { + MemAdditions::Edges(edges) => Iter4::I(edges.iter().map(|(k, v)| (*k, *v)).rev()), + MemAdditions::WEdges(TimeIndexWindow::All(ti)) => { + Iter4::J(ti.iter().map(|(k, v)| (*k, *v)).rev()) + } + MemAdditions::WEdges(TimeIndexWindow::Range { timeindex, range }) => { + Iter4::K(timeindex.iter_window(range).map(|(k, v)| (*k, *v)).rev()) + } + _ => Iter4::L(std::iter::empty()), + } + } +} + impl<'a> TimeIndexOps<'a> for MemAdditions<'a> { type IndexType = TimeIndexEntry; diff --git a/db4-storage/src/segments/edge_entry.rs b/db4-storage/src/segments/edge_entry.rs index 4b1ce4a198..13b132baf3 100644 --- a/db4-storage/src/segments/edge_entry.rs +++ b/db4-storage/src/segments/edge_entry.rs @@ -1,6 +1,6 @@ use raphtory_api::core::entities::properties::prop::Prop; use raphtory_core::{ - entities::{EID, LayerIds, Multiple, VID, properties::tprop::TPropCell}, + entities::{EID, Multiple, VID, properties::tprop::TPropCell}, storage::timeindex::{TimeIndexEntry, TimeIndexOps}, }; @@ -75,7 +75,7 @@ impl<'a> MemEdgeRef<'a> { impl<'a> WithTimeCells<'a> for MemEdgeRef<'a> { type TimeCell = MemAdditions<'a>; - fn t_props_time_cells( + fn t_props_tc( self, layer_id: usize, range: Option<(TimeIndexEntry, TimeIndexEntry)>, @@ -88,7 +88,7 @@ impl<'a> WithTimeCells<'a> for MemEdgeRef<'a> { ) } - fn additions_time_cells( + fn additions_tc( self, _layer_id: usize, _range: Option<(TimeIndexEntry, TimeIndexEntry)>, @@ -132,10 +132,6 @@ impl<'a> EdgeRefOps<'a> for MemEdgeRef<'a> { .map(|entry| (entry.src, entry.dst)) } - fn additions(self, layer_id: &'a LayerIds) -> Self::Additions { - EdgeAdditions::new(self, layer_id) - } - fn layer_additions(self, layer_id: usize) -> Self::Additions { EdgeAdditions::new_with_layer(self, layer_id) } @@ -144,10 +140,6 @@ impl<'a> EdgeRefOps<'a> for MemEdgeRef<'a> { self.es.as_ref()[layer_id].c_prop(self.pos, prop_id) } - fn t_prop(self, layer_id: &'a LayerIds, prop_id: usize) -> Self::TProps { - EdgeTProps::new(self, layer_id, prop_id) - } - fn layer_t_prop(self, layer_id: usize, prop_id: usize) -> Self::TProps { EdgeTProps::new_with_layer(self, layer_id, prop_id) } diff --git a/db4-storage/src/segments/node_entry.rs b/db4-storage/src/segments/node_entry.rs index 0b069dc04d..2dd823fe6f 100644 --- a/db4-storage/src/segments/node_entry.rs +++ b/db4-storage/src/segments/node_entry.rs @@ -1,8 +1,8 @@ use crate::{ - LocalPOS, NodeAdditions, NodeTProps, + LocalPOS, NodeEdgeAdditions, NodePropAdditions, NodeTProps, api::nodes::{NodeEntryOps, NodeRefOps}, gen_t_props::WithTProps, - gen_ts::WithTimeCells, + gen_ts::{EdgeAdditionCellsRef, PropAdditionCellsRef, WithTimeCells}, segments::node::MemNodeSegment, }; use raphtory_api::core::{ @@ -70,7 +70,7 @@ impl<'a> MemNodeRef<'a> { impl<'a> WithTimeCells<'a> for MemNodeRef<'a> { type TimeCell = MemAdditions<'a>; - fn t_props_time_cells( + fn t_props_tc( self, layer_id: usize, range: Option<(TimeIndexEntry, TimeIndexEntry)>, @@ -83,7 +83,7 @@ impl<'a> WithTimeCells<'a> for MemNodeRef<'a> { ) } - fn additions_time_cells( + fn additions_tc( self, layer_id: usize, range: Option<(TimeIndexEntry, TimeIndexEntry)>, @@ -123,7 +123,8 @@ impl<'a> WithTProps<'a> for MemNodeRef<'a> { } impl<'a> NodeRefOps<'a> for MemNodeRef<'a> { - type Additions = NodeAdditions<'a>; + type Additions = NodePropAdditions<'a>; + type EdgeAdditions = NodeEdgeAdditions<'a>; type TProps = NodeTProps<'a>; fn node_meta(&self) -> &Arc { @@ -150,10 +151,6 @@ impl<'a> NodeRefOps<'a> for MemNodeRef<'a> { self.ns.inb_edges(self.pos, layer_id) } - fn additions(self, layer_ids: &'a LayerIds) -> Self::Additions { - NodeAdditions::new(self, layer_ids) - } - fn c_prop(self, layer_id: usize, prop_id: usize) -> Option { self.ns.as_ref()[layer_id].c_prop(self.pos, prop_id) } @@ -162,8 +159,12 @@ impl<'a> NodeRefOps<'a> for MemNodeRef<'a> { self.ns.as_ref()[layer_id].c_prop_str(self.pos, prop_id) } - fn t_prop(self, layer_id: &'a LayerIds, prop_id: usize) -> Self::TProps { - NodeTProps::new(self, layer_id, prop_id) + fn node_additions(self, layer_id: usize) -> Self::Additions { + NodePropAdditions::new_with_layer(PropAdditionCellsRef::new(self), layer_id) + } + + fn edge_additions(self, layer_id: usize) -> Self::EdgeAdditions { + NodeEdgeAdditions::new_additions_with_layer(EdgeAdditionCellsRef::new(self), layer_id) } fn degree(self, layers: &LayerIds, dir: Direction) -> usize { @@ -189,10 +190,6 @@ impl<'a> NodeRefOps<'a> for MemNodeRef<'a> { eid.map(|eid| EdgeRef::new_outgoing(eid, src_id, dst)) } - fn layer_additions(self, layer_id: usize) -> Self::Additions { - NodeAdditions::new_with_layer(self, layer_id) - } - fn temporal_prop_layer(self, layer_id: usize, prop_id: usize) -> Self::TProps { NodeTProps::new_with_layer(self, layer_id, prop_id) } diff --git a/raphtory-storage/src/mutation/addition_ops_ext.rs b/raphtory-storage/src/mutation/addition_ops_ext.rs index 6012b2bc52..46fa993557 100644 --- a/raphtory-storage/src/mutation/addition_ops_ext.rs +++ b/raphtory-storage/src/mutation/addition_ops_ext.rs @@ -19,9 +19,9 @@ use storage::{ pages::{session::WriteSession, NODE_ID_PROP_KEY}, persist::strategy::PersistentStrategy, properties::props_meta_writer::PropsMetaWriter, + resolver::GIDResolverOps, segments::{edge::MemEdgeSegment, node::MemNodeSegment}, Extension, ES, NS, - resolver::{GIDResolverOps}, }; use crate::mutation::{ @@ -196,17 +196,15 @@ impl InternalAdditionOps for TemporalGraph { fn resolve_node(&self, id: NodeRef) -> Result, Self::Error> { match id { NodeRef::External(id) => { - let id = self - .logical_to_physical - .get_or_init(id, || { - // When initializing a new node, reserve node_id as a const prop. - // Done here since the id type is not known until node creation. - reserve_node_id_as_prop(self.node_meta(), id); - - self.node_count - .fetch_add(1, std::sync::atomic::Ordering::Relaxed) - .into() - })?; + let id = self.logical_to_physical.get_or_init(id, || { + // When initializing a new node, reserve node_id as a const prop. + // Done here since the id type is not known until node creation. + reserve_node_id_as_prop(self.node_meta(), id); + + self.node_count + .fetch_add(1, std::sync::atomic::Ordering::Relaxed) + .into() + })?; Ok(id) } diff --git a/raphtory/src/db/api/view/internal/time_semantics/persistent_semantics.rs b/raphtory/src/db/api/view/internal/time_semantics/persistent_semantics.rs index 9ddbb50cfb..a86412dde3 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/persistent_semantics.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/persistent_semantics.rs @@ -217,12 +217,7 @@ impl NodeTimeSemanticsOps for PersistentSemantics { node: NodeStorageRef<'graph>, _view: G, ) -> impl Iterator)> + Send + Sync + 'graph { - node.temp_prop_rows().map(|(t, _, row)| { - ( - t, - row - ) - }) + node.temp_prop_rows().map(|(t, _, row)| (t, row)) } fn node_updates_window<'graph, G: GraphViewOps<'graph>>( @@ -260,12 +255,7 @@ impl NodeTimeSemanticsOps for PersistentSemantics { .map(move |row| (TimeIndexEntry::start(start), row)) .chain( node.temp_prop_rows_range(Some(TimeIndexEntry::range(w))) - .map(|(t, _, row)| { - ( - t, - row - ) - }), + .map(|(t, _, row)| (t, row)), ) } diff --git a/raphtory/src/db/graph/graph.rs b/raphtory/src/db/graph/graph.rs index 91a1114b23..7be2426fe2 100644 --- a/raphtory/src/db/graph/graph.rs +++ b/raphtory/src/db/graph/graph.rs @@ -603,7 +603,10 @@ mod db_tests { utils::logging::global_info_logger, }; use raphtory_core::utils::time::{ParseTimeError, TryIntoTime}; - use raphtory_storage::{core_ops::CoreGraphOps, graph::nodes::node_storage_ops::NodeStorageOps, mutation::addition_ops::InternalAdditionOps}; + use raphtory_storage::{ + core_ops::CoreGraphOps, graph::nodes::node_storage_ops::NodeStorageOps, + mutation::addition_ops::InternalAdditionOps, + }; use rayon::join; use std::{ collections::{HashMap, HashSet}, diff --git a/raphtory/src/serialise/serialise.rs b/raphtory/src/serialise/serialise.rs index e1db2094e8..6cdeb5dcb6 100644 --- a/raphtory/src/serialise/serialise.rs +++ b/raphtory/src/serialise/serialise.rs @@ -189,16 +189,13 @@ impl StableEncode for GraphStorage { graph.new_node(node.id(), node.vid(), node.node_type_id()); for (time, _, row) in node.temp_prop_rows() { - graph.update_node_tprops( - node.vid(), - time, - row.into_iter(), - ); + graph.update_node_tprops(node.vid(), time, row.into_iter()); } graph.update_node_cprops( node.vid(), - (0..n_const_meta.len()).flat_map(|i| node.constant_prop_layer(0, i).map(|v| (i, v))), + (0..n_const_meta.len()) + .flat_map(|i| node.constant_prop_layer(0, i).map(|v| (i, v))), ); } @@ -282,7 +279,6 @@ impl StableEncode for MaterializedGraph { impl InternalStableDecode for TemporalGraph { fn decode_from_proto(graph: &proto::Graph) -> Result { - todo!("remove this stuff!") } } From 2a18801ebf44e6c2f6c7ab173e699a3603e20977 Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Tue, 1 Jul 2025 14:34:50 +0100 Subject: [PATCH 055/321] project compiles --- db4-storage/src/gen_ts.rs | 47 +++++++- db4-storage/src/segments/additions.rs | 13 ++- .../src/graph/edges/edge_storage_ops.rs | 11 +- .../src/graph/nodes/node_entry.rs | 18 +-- .../src/graph/nodes/node_storage_ops.rs | 22 ++-- .../internal/time_semantics/filtered_node.rs | 106 +++++------------- .../time_semantics/persistent_semantics.rs | 7 +- 7 files changed, 110 insertions(+), 114 deletions(-) diff --git a/db4-storage/src/gen_ts.rs b/db4-storage/src/gen_ts.rs index 10904535a1..072537ecc1 100644 --- a/db4-storage/src/gen_ts.rs +++ b/db4-storage/src/gen_ts.rs @@ -1,10 +1,15 @@ use std::ops::Range; use itertools::Itertools; -use raphtory_core::storage::timeindex::{TimeIndexEntry, TimeIndexOps}; +use raphtory_core::{ + entities::ELID, + storage::timeindex::{TimeIndexEntry, TimeIndexOps}, +}; + +use crate::{NodeEntryRef, segments::additions::MemAdditions}; // TODO: split the Node time operations into edge additions and property additions -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug)] pub struct GenericTimeOps<'a, Ref> { range: Option<(TimeIndexEntry, TimeIndexEntry)>, layer_id: usize, @@ -53,7 +58,20 @@ where fn num_layers(&self) -> usize; } -#[derive(Clone, Copy)] +pub trait WithEdgeEvents<'a>: WithTimeCells<'a> { + type TimeCell: EdgeEventOps<'a>; +} + +impl<'a> WithEdgeEvents<'a> for NodeEntryRef<'a> { + type TimeCell = MemAdditions<'a>; +} + +pub trait EdgeEventOps<'a>: TimeIndexOps<'a, IndexType = TimeIndexEntry> { + fn edge_events(self) -> impl Iterator + Send + Sync + 'a; + fn edge_events_rev(self) -> impl Iterator + Send + Sync + 'a; +} + +#[derive(Clone, Copy, Debug)] pub struct EdgeAdditionCellsRef<'a, Ref: WithTimeCells<'a> + 'a> { node: Ref, _mark: std::marker::PhantomData<&'a ()>, @@ -92,7 +110,7 @@ impl<'a, Ref: WithTimeCells<'a> + 'a> WithTimeCells<'a> for EdgeAdditionCellsRef } } -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug)] pub struct PropAdditionCellsRef<'a, Ref: WithTimeCells<'a> + 'a> { node: Ref, _mark: std::marker::PhantomData<&'a ()>, @@ -131,6 +149,27 @@ impl<'a, Ref: WithTimeCells<'a> + 'a> WithTimeCells<'a> for PropAdditionCellsRef } } +impl<'a, Ref: WithEdgeEvents<'a> + 'a> GenericTimeOps<'a, EdgeAdditionCellsRef<'a, Ref>> +where + >::TimeCell: EdgeEventOps<'a>, +{ + pub fn edge_events(self) -> impl Iterator + Send + Sync + 'a { + self.node + .additions_tc(self.layer_id, self.range) + .map(|t_cell| t_cell.edge_events()) + .kmerge_by(|a, b| a < b) + } + + pub fn edge_events_rev( + self, + ) -> impl Iterator + Send + Sync + 'a { + self.node + .additions_tc(self.layer_id, self.range) + .map(|t_cell| t_cell.edge_events_rev()) + .kmerge_by(|a, b| a > b) + } +} + impl<'a, Ref: WithTimeCells<'a> + 'a> GenericTimeOps<'a, Ref> { pub fn time_cells(self) -> impl Iterator + 'a { let range = self.range; diff --git a/db4-storage/src/segments/additions.rs b/db4-storage/src/segments/additions.rs index 3be3c63217..c23a45baf8 100644 --- a/db4-storage/src/segments/additions.rs +++ b/db4-storage/src/segments/additions.rs @@ -5,7 +5,10 @@ use raphtory_core::{ storage::timeindex::{TimeIndexEntry, TimeIndexOps, TimeIndexWindow}, }; -use crate::utils::Iter4; +use crate::{ + gen_ts::{EdgeEventOps, WithEdgeEvents}, + utils::Iter4, +}; #[derive(Clone, Debug)] pub enum MemAdditions<'a> { @@ -27,8 +30,8 @@ impl<'a> From<&'a TCell>> for MemAdditions<'a> { } } -impl<'a> MemAdditions<'a> { - pub fn edge_events(self) -> impl Iterator + Send + Sync + 'a { +impl<'a> EdgeEventOps<'a> for MemAdditions<'a> { + fn edge_events(self) -> impl Iterator + Send + Sync + 'a { match self { MemAdditions::Edges(edges) => Iter4::I(edges.iter().map(|(k, v)| (*k, *v))), MemAdditions::WEdges(TimeIndexWindow::All(ti)) => { @@ -41,9 +44,7 @@ impl<'a> MemAdditions<'a> { } } - pub fn edge_events_rev( - self, - ) -> impl Iterator + Send + Sync + 'a { + fn edge_events_rev(self) -> impl Iterator + Send + Sync + 'a { match self { MemAdditions::Edges(edges) => Iter4::I(edges.iter().map(|(k, v)| (*k, *v)).rev()), MemAdditions::WEdges(TimeIndexWindow::All(ti)) => { diff --git a/raphtory-storage/src/graph/edges/edge_storage_ops.rs b/raphtory-storage/src/graph/edges/edge_storage_ops.rs index 00a354acfd..b23f98cbcd 100644 --- a/raphtory-storage/src/graph/edges/edge_storage_ops.rs +++ b/raphtory-storage/src/graph/edges/edge_storage_ops.rs @@ -184,7 +184,16 @@ pub trait EdgeStorageOps<'a>: Copy + Sized + Send + Sync + 'a { impl<'a> EdgeStorageOps<'a> for storage::EdgeEntryRef<'a> { fn added(self, layer_ids: &LayerIds, w: Range) -> bool { - EdgeRefOps::additions(self, layer_ids).active_t(w) + match layer_ids { + LayerIds::None => false, + LayerIds::All => self + .additions_iter(&LayerIds::All) + .any(|(_, t_index)| t_index.active_t(w.clone())), + LayerIds::One(l_id) => self.layer_additions(*l_id).active_t(w), + LayerIds::Multiple(layers) => layers + .iter() + .any(|l_id| self.added(&LayerIds::One(l_id), w.clone())), + } } fn has_layer(self, layer_ids: &LayerIds) -> bool { diff --git a/raphtory-storage/src/graph/nodes/node_entry.rs b/raphtory-storage/src/graph/nodes/node_entry.rs index 80c6dab200..4377c44004 100644 --- a/raphtory-storage/src/graph/nodes/node_entry.rs +++ b/raphtory-storage/src/graph/nodes/node_entry.rs @@ -87,11 +87,7 @@ impl<'a, 'b: 'a> NodeStorageOps<'a> for &'a NodeStorageEntry<'b> { self.as_ref().degree(layers, dir) } - fn layer_additions(self, layer_ids: usize) -> storage::NodeAdditions<'a> { - self.as_ref().layer_additions(layer_ids) - } - - fn additions(self) -> storage::NodeAdditions<'a> { + fn additions(self) -> storage::NodePropAdditions<'a> { self.as_ref().additions() } @@ -126,10 +122,6 @@ impl<'a, 'b: 'a> NodeStorageOps<'a> for &'a NodeStorageEntry<'b> { self.as_ref().layer_ids_iter(layer_ids) } - fn deletions(self, layer_id: usize) -> storage::NodeAdditions<'a> { - self.as_ref().deletions(layer_id) - } - fn temporal_prop_layer(self, layer_id: usize, prop_id: usize) -> storage::NodeTProps<'a> { self.as_ref().temporal_prop_layer(layer_id, prop_id) } @@ -148,4 +140,12 @@ impl<'a, 'b: 'a> NodeStorageOps<'a> for &'a NodeStorageEntry<'b> { fn tprop(self, prop_id: usize) -> storage::NodeTProps<'a> { self.as_ref().tprop(prop_id) } + + fn node_additions(self, layer_id: usize) -> storage::NodePropAdditions<'a> { + self.as_ref().node_additions(layer_id) + } + + fn node_edge_additions(self, layer_id: usize) -> storage::NodeEdgeAdditions<'a> { + self.as_ref().node_edge_additions(layer_id) + } } diff --git a/raphtory-storage/src/graph/nodes/node_storage_ops.rs b/raphtory-storage/src/graph/nodes/node_storage_ops.rs index c7c59a3a2b..a2bdb4f095 100644 --- a/raphtory-storage/src/graph/nodes/node_storage_ops.rs +++ b/raphtory-storage/src/graph/nodes/node_storage_ops.rs @@ -6,8 +6,6 @@ use raphtory_core::{entities::LayerVariants, storage::timeindex::TimeIndexEntry} use std::{borrow::Cow, ops::Range}; use storage::{api::nodes::NodeRefOps, NodeEntryRef}; -static ALL_LAYERS: &LayerIds = &LayerIds::All; - pub trait NodeStorageOps<'a>: Copy + Sized + Send + Sync + 'a { fn degree(self, layers: &LayerIds, dir: Direction) -> usize; @@ -34,11 +32,11 @@ pub trait NodeStorageOps<'a>: Copy + Sized + Send + Sync + 'a { layer_ids: &'a LayerIds, ) -> impl Iterator + Send + Sync + 'a; - fn layer_additions(self, layer_id: usize) -> storage::NodeAdditions<'a>; + fn node_additions(self, layer_id: usize) -> storage::NodePropAdditions<'a>; - fn additions(self) -> storage::NodeAdditions<'a>; + fn node_edge_additions(self, layer_id: usize) -> storage::NodeEdgeAdditions<'a>; - fn deletions(self, layer_id: usize) -> storage::NodeAdditions<'a>; + fn additions(self) -> storage::NodePropAdditions<'a>; fn temporal_prop_layer(self, layer_id: usize, prop_id: usize) -> storage::NodeTProps<'a>; @@ -121,20 +119,20 @@ impl<'a> NodeStorageOps<'a> for NodeEntryRef<'a> { } } - fn deletions(self, layer_id: usize) -> storage::NodeAdditions<'a> { - NodeRefOps::layer_additions(self, layer_id) + fn node_additions(self, layer_ids: usize) -> storage::NodePropAdditions<'a> { + NodeRefOps::node_additions(self, layer_ids) } - fn layer_additions(self, layer_ids: usize) -> storage::NodeAdditions<'a> { - NodeRefOps::layer_additions(self, layer_ids) + fn node_edge_additions(self, layer_id: usize) -> storage::NodeEdgeAdditions<'a> { + NodeRefOps::edge_additions(self, layer_id) } - fn additions(self) -> storage::NodeAdditions<'a> { - NodeRefOps::additions(self, ALL_LAYERS) + fn additions(self) -> storage::NodePropAdditions<'a> { + NodeRefOps::node_additions(self, 0) } fn tprop(self, prop_id: usize) -> storage::NodeTProps<'a> { - NodeRefOps::t_prop(self, &ALL_LAYERS, prop_id) + NodeRefOps::temporal_prop_layer(self, 0, prop_id) } fn temporal_prop_layer(self, layer_id: usize, prop_id: usize) -> storage::NodeTProps<'a> { diff --git a/raphtory/src/db/api/view/internal/time_semantics/filtered_node.rs b/raphtory/src/db/api/view/internal/time_semantics/filtered_node.rs index 85125d9309..b865b86586 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/filtered_node.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/filtered_node.rs @@ -8,35 +8,34 @@ use raphtory_api::core::{ storage::timeindex::{TimeIndexEntry, TimeIndexOps}, Direction, }; -use raphtory_core::storage::timeindex::TimeIndexWindow; use raphtory_storage::graph::{ - edges::edge_storage_ops::EdgeStorageOps, - nodes::{node_additions::NodeAdditions, node_storage_ops::NodeStorageOps}, + edges::edge_storage_ops::EdgeStorageOps, nodes::node_storage_ops::NodeStorageOps, }; use std::ops::Range; #[derive(Debug, Clone)] pub struct NodeHistory<'a, G> { - pub(crate) additions: NodeAdditions<'a>, + pub(crate) edge_history: storage::NodeEdgeAdditions<'a>, + pub(crate) additions: storage::NodePropAdditions<'a>, pub(crate) view: G, } #[derive(Debug, Clone)] pub struct NodeEdgeHistory<'a, G> { - pub(crate) additions: NodeAdditions<'a>, + pub(crate) additions: storage::NodeEdgeAdditions<'a>, pub(crate) view: G, } #[derive(Debug, Clone)] pub struct NodePropHistory<'a, G> { - pub(crate) additions: NodeAdditions<'a>, + pub(crate) additions: storage::NodePropAdditions<'a>, pub(crate) view: G, } impl<'a, G: Clone> NodeHistory<'a, G> { pub fn edge_history(&self) -> NodeEdgeHistory<'a, G> { NodeEdgeHistory { - additions: self.additions.clone(), + additions: self.edge_history.clone(), view: self.view.clone(), } } @@ -124,21 +123,7 @@ impl<'a, G: GraphViewOps<'a>> TimeIndexOps<'a> for NodePropHistory<'a, G> { type RangeType = Self; fn active(&self, w: Range) -> bool { - let history = &self.additions; - match history { - NodeAdditions::Mem(h) => h.props_ts().active(w), - NodeAdditions::Range(h) => match h { - TimeIndexWindow::Empty => false, - TimeIndexWindow::Range { timeindex, range } => { - let start = range.start.max(w.start); - let end = range.end.min(w.end).max(start); - timeindex.props_ts().active(start..end) - } - TimeIndexWindow::All(h) => h.props_ts().active(w), - }, - #[cfg(feature = "storage")] - NodeAdditions::Col(h) => h.with_range(w).prop_events().any(|t| !t.is_empty()), - } + self.additions.active(w) } fn range(&self, w: Range) -> Self::RangeType { @@ -150,41 +135,19 @@ impl<'a, G: GraphViewOps<'a>> TimeIndexOps<'a> for NodePropHistory<'a, G> { } fn iter(self) -> impl Iterator + Send + Sync + 'a { - self.additions.prop_events() + self.additions.iter() } fn iter_rev(self) -> impl Iterator + Send + Sync + 'a { - self.additions.prop_events_rev() + self.additions.iter_rev() } fn len(&self) -> usize { - match &self.additions { - NodeAdditions::Mem(additions) => additions.props_ts.len(), - NodeAdditions::Range(additions) => match additions { - TimeIndexWindow::Empty => 0, - TimeIndexWindow::Range { timeindex, range } => { - (&timeindex.props_ts).range(range.clone()).len() - } - TimeIndexWindow::All(timeindex) => timeindex.props_ts.len(), - }, - #[cfg(feature = "storage")] - NodeAdditions::Col(additions) => additions.clone().prop_events().map(|t| t.len()).sum(), - } + self.additions.len() } fn is_empty(&self) -> bool { - match &self.additions { - NodeAdditions::Mem(additions) => additions.props_ts.is_empty(), - NodeAdditions::Range(additions) => match additions { - TimeIndexWindow::Empty => true, - TimeIndexWindow::Range { timeindex, range } => { - (&timeindex.props_ts).range(range.clone()).is_empty() - } - TimeIndexWindow::All(timeindex) => timeindex.props_ts.is_empty(), - }, - #[cfg(feature = "storage")] - NodeAdditions::Col(additions) => additions.clone().prop_events().all(|t| t.is_empty()), - } + self.additions.is_empty() } } @@ -214,20 +177,7 @@ impl<'a, G: GraphViewOps<'a>> TimeIndexOps<'a> for NodeEdgeHistory<'a, G> { fn len(&self) -> usize { if matches!(self.view.filter_state(), FilterState::Neither) { - match &self.additions { - NodeAdditions::Mem(additions) => additions.edge_ts.len(), - NodeAdditions::Range(additions) => match additions { - TimeIndexWindow::Empty => 0, - TimeIndexWindow::Range { timeindex, range } => { - (&timeindex.edge_ts).range(range.clone()).len() - } - TimeIndexWindow::All(timeindex) => timeindex.edge_ts.len(), - }, - #[cfg(feature = "storage")] - NodeAdditions::Col(additions) => { - additions.clone().edge_events().map(|t| t.len()).sum() - } - } + self.additions.len() } else { self.history().count() } @@ -235,20 +185,7 @@ impl<'a, G: GraphViewOps<'a>> TimeIndexOps<'a> for NodeEdgeHistory<'a, G> { fn is_empty(&self) -> bool { if matches!(self.view.filter_state(), FilterState::Neither) { - match &self.additions { - NodeAdditions::Mem(additions) => additions.edge_ts.is_empty(), - NodeAdditions::Range(additions) => match additions { - TimeIndexWindow::Empty => true, - TimeIndexWindow::Range { timeindex, range } => { - (&timeindex.edge_ts).range(range.clone()).is_empty() - } - TimeIndexWindow::All(timeindex) => timeindex.edge_ts.is_empty(), - }, - #[cfg(feature = "storage")] - NodeAdditions::Col(additions) => { - additions.clone().edge_events().all(|t| t.is_empty()) - } - } + self.additions.is_empty() } else { self.history().next().is_none() } @@ -264,9 +201,14 @@ impl<'b, G: GraphViewOps<'b>> TimeIndexOps<'b> for NodeHistory<'b, G> { } fn range(&self, w: Range) -> Self { + let edge_history = self.edge_history.range(w.clone()); let additions = self.additions.range(w); let view = self.view.clone(); - NodeHistory { additions, view } + NodeHistory { + edge_history, + additions, + view, + } } fn iter(self) -> impl Iterator + Send + Sync + 'b { @@ -290,8 +232,14 @@ impl<'b, G: GraphViewOps<'b>> TimeIndexOps<'b> for NodeHistory<'b, G> { pub trait FilteredNodeStorageOps<'a>: NodeStorageOps<'a> { fn history(self, view: G) -> NodeHistory<'a, G> { - let additions = self.additions(); - NodeHistory { additions, view } + // FIXME: new storage supports multiple layers, but this is hardcoded to layer 0 as there is no information in history about which layer the node belongs to. + let additions = self.node_additions(0); + let edge_history = self.node_edge_additions(0); + NodeHistory { + edge_history, + additions, + view, + } } fn filtered_edges_iter>( diff --git a/raphtory/src/db/api/view/internal/time_semantics/persistent_semantics.rs b/raphtory/src/db/api/view/internal/time_semantics/persistent_semantics.rs index a86412dde3..310eebac15 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/persistent_semantics.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/persistent_semantics.rs @@ -159,7 +159,7 @@ impl NodeTimeSemanticsOps for PersistentSemantics { w: Range, ) -> Option { let additions = node.additions(); - let mut earliest = additions.prop_events().next().map(|t| t.t()); + let mut earliest = additions.iter().next().map(|t| t.t()); if let Some(earliest) = earliest { if earliest <= w.start { return Some(w.start); @@ -230,12 +230,13 @@ impl NodeTimeSemanticsOps for PersistentSemantics { let first_row = if node .additions() .range(TimeIndexEntry::range(i64::MIN..start)) - .prop_events() + .iter() .next() .is_some() { Some( - node.tprops() + (0.._view.node_meta().temporal_prop_meta().len()) + .map(|prop_id| (prop_id, node.tprop(prop_id))) .filter_map(|(i, tprop)| { if tprop.active_t(start..start.saturating_add(1)) { None From 8920b0d22fc708eee187c2915581938e1562c669 Mon Sep 17 00:00:00 2001 From: Fadhil Abubaker Date: Tue, 1 Jul 2025 11:19:11 -0400 Subject: [PATCH 056/321] Change next_id to FnMut (#2162) Change next_id() to FnMut --- db4-storage/src/resolver/mapping_resolver.rs | 2 +- db4-storage/src/resolver/mod.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/db4-storage/src/resolver/mapping_resolver.rs b/db4-storage/src/resolver/mapping_resolver.rs index 0029aa01e0..3741559053 100644 --- a/db4-storage/src/resolver/mapping_resolver.rs +++ b/db4-storage/src/resolver/mapping_resolver.rs @@ -34,7 +34,7 @@ impl GIDResolverOps for MappingResolver { fn get_or_init( &self, gid: GidRef, - next_id: impl FnOnce() -> VID, + next_id: impl FnMut() -> VID, ) -> Result, GIDResolverError> { let result = self.mapping.get_or_init(gid, next_id)?; Ok(result) diff --git a/db4-storage/src/resolver/mod.rs b/db4-storage/src/resolver/mod.rs index d1549af8ba..28b3862258 100644 --- a/db4-storage/src/resolver/mod.rs +++ b/db4-storage/src/resolver/mod.rs @@ -27,7 +27,7 @@ pub trait GIDResolverOps { fn get_or_init( &self, gid: GidRef, - next_id: impl FnOnce() -> VID, + next_id: impl FnMut() -> VID, ) -> Result, GIDResolverError>; fn validate_gids<'a>( &self, From e868ce17fafc7697794402a1752eaeccea3dbefe Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Tue, 1 Jul 2025 17:54:20 +0100 Subject: [PATCH 057/321] can add one edge from raphtory::Graph --- db4-graph/src/lib.rs | 16 + db4-storage/src/pages/mod.rs | 4 + db4-storage/src/pages/session.rs | 2 +- raphtory-storage/src/graph/graph.rs | 3 +- raphtory-storage/src/mutation/addition_ops.rs | 293 ++---------------- .../src/mutation/addition_ops_ext.rs | 27 +- raphtory/src/db/api/mutation/addition_ops.rs | 2 +- raphtory/src/db/api/storage/storage.rs | 49 +-- raphtory/src/db/graph/graph.rs | 14 +- 9 files changed, 110 insertions(+), 300 deletions(-) diff --git a/db4-graph/src/lib.rs b/db4-graph/src/lib.rs index 3b36fb7036..5e0e734ea6 100644 --- a/db4-graph/src/lib.rs +++ b/db4-graph/src/lib.rs @@ -50,6 +50,12 @@ pub struct TemporalGraph { graph_dir: PathBuf, } +impl Default for TemporalGraph { + fn default() -> Self { + Self::new(None) + } +} + fn random_temp_dir() -> PathBuf { temp_dir().join(format!("raphtory-{}", uuid::Uuid::new_v4())) } @@ -61,6 +67,12 @@ impl, ES = ES>> TemporalGraph { pub fn new_with_meta(path: Option, node_meta: Meta, edge_meta: Meta) -> Self { let graph_dir = path.unwrap_or_else(random_temp_dir); + std::fs::create_dir_all(&graph_dir).unwrap_or_else(|_| { + panic!( + "Failed to create graph directory at {}", + graph_dir.display() + ) + }); let gid_resolver_dir = graph_dir.join("gid_resolver"); let storage = Layer::new_with_meta( graph_dir.clone(), @@ -106,6 +118,10 @@ impl, ES = ES>> TemporalGraph { } } + pub fn next_event_id(&self) -> usize { + self.storage().next_event_id() + } + #[inline] pub fn internal_num_nodes(&self) -> usize { self.storage.nodes().num_nodes() diff --git a/db4-storage/src/pages/mod.rs b/db4-storage/src/pages/mod.rs index c4639ba60e..8e83df2a97 100644 --- a/db4-storage/src/pages/mod.rs +++ b/db4-storage/src/pages/mod.rs @@ -275,6 +275,10 @@ impl< self.event_id.load(atomic::Ordering::Relaxed) } + pub fn next_event_id(&self) -> usize { + self.event_id.fetch_add(1, atomic::Ordering::Relaxed) + } + pub fn update_edge_const_props>( &self, eid: impl Into, diff --git a/db4-storage/src/pages/session.rs b/db4-storage/src/pages/session.rs index 7b527ced89..ce3c4cf0b1 100644 --- a/db4-storage/src/pages/session.rs +++ b/db4-storage/src/pages/session.rs @@ -66,7 +66,7 @@ impl< let e_id = edge.inner(); let layer = e_id.layer(); - assert!(layer > 0, "Edge must be in a layer greater than 0"); + // assert!(layer > 0, "Edge must be in a layer greater than 0"); let (_, src_pos) = self.graph.nodes().resolve_pos(src); let (_, dst_pos) = self.graph.nodes().resolve_pos(dst); diff --git a/raphtory-storage/src/graph/graph.rs b/raphtory-storage/src/graph/graph.rs index 935cce2b41..c7134452bd 100644 --- a/raphtory-storage/src/graph/graph.rs +++ b/raphtory-storage/src/graph/graph.rs @@ -48,8 +48,7 @@ impl From for GraphStorage { impl Default for GraphStorage { fn default() -> Self { - // GraphStorage::Unlocked(Arc::new(TemporalGraph::default())) - todo!("does this even make sense? GraphStorage::default() is not a valid graph, it should be created with a valid graph directory"); + GraphStorage::Unlocked(Arc::new(TemporalGraph::default())) } } diff --git a/raphtory-storage/src/mutation/addition_ops.rs b/raphtory-storage/src/mutation/addition_ops.rs index 7e0f929f3f..a7ee2884d4 100644 --- a/raphtory-storage/src/mutation/addition_ops.rs +++ b/raphtory-storage/src/mutation/addition_ops.rs @@ -1,5 +1,12 @@ -use crate::{graph::graph::GraphStorage, mutation::MutationError}; +use crate::{ + graph::graph::GraphStorage, + mutation::{ + addition_ops_ext::{UnlockedSession, WriteS}, + MutationError, + }, +}; use db4_graph::WriteLockedGraph; +use parking_lot::RwLockWriteGuard; use raphtory_api::{ core::{ entities::{ @@ -11,11 +18,13 @@ use raphtory_api::{ inherit::Base, }; use raphtory_core::{ - entities::{graph::tgraph::TemporalGraph, nodes::node_ref::NodeRef, ELID}, + entities::{nodes::node_ref::NodeRef, ELID}, storage::{raw_edges::WriteLockedEdges, WriteLockedNodes}, }; -use std::sync::atomic::Ordering; -use storage::Extension; +use storage::{ + segments::{edge::MemEdgeSegment, node::MemNodeSegment}, + Extension, +}; pub trait InternalAdditionOps { type Error: From; @@ -55,7 +64,7 @@ pub trait InternalAdditionOps { dst: VID, e_id: Option, layer_id: usize, - ) -> Self::AtomicAddEdge<'_>; + ) -> Result, Self::Error>; fn validate_edge_props>( &self, @@ -133,155 +142,16 @@ pub trait SessionAdditionOps: Send + Sync { ) -> Result<(), Self::Error>; } -#[derive(Clone, Copy)] -pub struct TGWriteSession<'a> { - tg: &'a TemporalGraph, -} - -impl AtomicAdditionOps for TGWriteSession<'_> { - /// add edge update - fn internal_add_edge( - &mut self, - _t: TimeIndexEntry, - _src: impl Into, - _dst: impl Into, - _lsn: u64, - _layer: usize, - _props: impl IntoIterator, - ) -> MaybeNew { - todo!("Atomic addition operations are not implemented for TGWriteSession"); - } - - fn store_node_id_as_prop(&mut self, id: NodeRef, vid: impl Into) { - todo!("set_node_id is not implemented for TGWriteSession"); - } -} - -impl<'a> SessionAdditionOps for TGWriteSession<'a> { - type Error = MutationError; - - /// get the sequence id for the next event - fn next_event_id(&self) -> Result { - Ok(self.tg.event_counter.fetch_add(1, Ordering::Relaxed)) - } - - fn reserve_event_ids(&self, num_ids: usize) -> Result { - Ok(self.tg.event_counter.fetch_add(num_ids, Ordering::Relaxed)) - } - - fn set_node(&self, gid: GidRef, vid: VID) -> Result<(), Self::Error> { - Ok(self.tg.logical_to_physical.set(gid, vid)?) - } - - /// map property key to internal id, allocating new property if needed - fn resolve_graph_property( - &self, - prop: &str, - dtype: PropType, - is_static: bool, - ) -> Result, Self::Error> { - Ok(self - .tg - .graph_meta - .resolve_property(prop, dtype, is_static)?) - } - - /// map property key to internal id, allocating new property if needed and checking property type. - /// returns `None` if the type does not match - fn resolve_node_property( - &self, - prop: &str, - dtype: PropType, - is_static: bool, - ) -> Result, Self::Error> { - Ok(self.tg.node_meta.resolve_prop_id(prop, dtype, is_static)?) - } - - fn resolve_edge_property( - &self, - prop: &str, - dtype: PropType, - is_static: bool, - ) -> Result, Self::Error> { - Ok(self.tg.edge_meta.resolve_prop_id(prop, dtype, is_static)?) - } - - /// add node update - fn internal_add_node( - &self, - t: TimeIndexEntry, - v: VID, - props: &[(usize, Prop)], - ) -> Result<(), Self::Error> { - self.tg.update_time(t); - let mut entry = self.tg.storage.get_node_mut(v); - let mut node = entry.to_mut(); - let prop_i = node - .t_props_log_mut() - .push(props.iter().map(|(prop_id, prop)| { - let prop = self.tg.process_prop_value(prop); - (*prop_id, prop) - })) - .map_err(MutationError::from)?; - node.node_store_mut().update_t_prop_time(t, prop_i); - Ok(()) - } - - /// add edge update - fn internal_add_edge( - &self, - t: TimeIndexEntry, - src: VID, - dst: VID, - props: &[(usize, Prop)], - layer: usize, - ) -> Result, Self::Error> { - let edge = self.tg.link_nodes(src, dst, t, layer, false); - edge.try_map(|mut edge| { - let eid = edge.eid(); - let mut edge = edge.as_mut(); - edge.additions_mut(layer).insert(t); - if !props.is_empty() { - let edge_layer = edge.layer_mut(layer); - for (prop_id, prop) in props { - let prop = self.tg.process_prop_value(prop); - edge_layer - .add_prop(t, *prop_id, prop) - .map_err(MutationError::from)?; - } - } - Ok(eid) - }) - } - - /// add update for an existing edge - fn internal_add_edge_update( - &self, - t: TimeIndexEntry, - edge: EID, - props: &[(usize, Prop)], - layer: usize, - ) -> Result<(), Self::Error> { - let mut edge = self.tg.link_edge(edge, t, layer, false); - let mut edge = edge.as_mut(); - edge.additions_mut(layer).insert(t); - if !props.is_empty() { - let edge_layer = edge.layer_mut(layer); - for (prop_id, prop) in props { - let prop = self.tg.process_prop_value(prop); - edge_layer - .add_prop(t, *prop_id, prop) - .map_err(MutationError::from)? - } - } - Ok(()) - } -} - impl InternalAdditionOps for GraphStorage { type Error = MutationError; - type WS<'b> = TGWriteSession<'b>; - type AtomicAddEdge<'a> = TGWriteSession<'a>; + type WS<'b> = UnlockedSession<'b>; + + type AtomicAddEdge<'a> = WriteS< + 'a, + RwLockWriteGuard<'a, MemNodeSegment>, + RwLockWriteGuard<'a, MemEdgeSegment>, + Extension, + >; fn write_lock(&self) -> Result, Self::Error> { self.mutable()?.write_lock() @@ -296,9 +166,7 @@ impl InternalAdditionOps for GraphStorage { } fn resolve_layer(&self, layer: Option<&str>) -> Result, Self::Error> { - // let id = self.mutable()?.resolve_layer_inner(layer)?; - // Ok(id) - todo!("remove this once we have a mutable graph storage"); + self.mutable()?.resolve_layer(layer) } fn resolve_node(&self, id: NodeRef) -> Result, Self::Error> { @@ -314,20 +182,17 @@ impl InternalAdditionOps for GraphStorage { } fn write_session(&self) -> Result, Self::Error> { - // Ok(TGWriteSession { - // tg: self.mutable()?, - // }) - todo!("remove this once we have a mutable graph storage"); + self.mutable()?.write_session() } fn atomic_add_edge( &self, - _src: VID, - _dst: VID, - _e_id: Option, - _layer_id: usize, - ) -> Self::AtomicAddEdge<'_> { - self.write_session().unwrap() + src: VID, + dst: VID, + e_id: Option, + layer_id: usize, + ) -> Result, Self::Error> { + self.mutable()?.atomic_add_edge(src, dst, e_id, layer_id) } fn validate_edge_props>( @@ -348,102 +213,6 @@ impl InternalAdditionOps for GraphStorage { } } -impl InternalAdditionOps for TemporalGraph { - type Error = MutationError; - - type WS<'b> = TGWriteSession<'b>; - type AtomicAddEdge<'a> = TGWriteSession<'a>; - - fn write_lock(&self) -> Result, Self::Error> { - // Ok(WriteLockedGraph::new(self)) - todo!("remove this once we have a mutable graph storage"); - } - fn write_lock_nodes(&self) -> Result { - Ok(self.storage.nodes.write_lock()) - } - - fn write_lock_edges(&self) -> Result { - Ok(self.storage.edges.write_lock()) - } - - /// map layer name to id and allocate a new layer if needed - fn resolve_layer(&self, layer: Option<&str>) -> Result, Self::Error> { - let id = self - .resolve_layer_inner(layer) - .map_err(MutationError::from)?; - Ok(id) - } - - /// map external node id to internal id, allocating a new empty node if needed - fn resolve_node(&self, id: NodeRef) -> Result, Self::Error> { - Ok(self.resolve_node_inner(id)?) - } - - /// resolve a node and corresponding type, outer MaybeNew tracks whether the type assignment is new for the node even if both node and type already existed. - fn resolve_node_and_type( - &self, - id: NodeRef, - node_type: &str, - ) -> Result, MaybeNew)>, Self::Error> { - let vid = self.resolve_node(id)?; - let mut entry = self.storage.get_node_mut(vid.inner()); - let mut entry_ref = entry.to_mut(); - let node_store = entry_ref.node_store_mut(); - if node_store.node_type == 0 { - let node_type_id = self.node_meta.get_or_create_node_type_id(node_type); - node_store.update_node_type(node_type_id.inner()); - Ok(MaybeNew::New((vid, node_type_id))) - } else { - let node_type_id = self - .node_meta - .get_node_type_id(node_type) - .filter(|&node_type| node_type == node_store.node_type) - .ok_or(MutationError::NodeTypeError)?; - Ok(MaybeNew::Existing((vid, MaybeNew::Existing(node_type_id)))) - } - } - - fn write_session(&self) -> Result, Self::Error> { - Ok(TGWriteSession { tg: self }) - } - - fn atomic_add_edge( - &self, - _src: VID, - _dst: VID, - _e_id: Option, - _layer_id: usize, - ) -> Self::AtomicAddEdge<'_> { - TGWriteSession { tg: self } - } - - fn validate_gids<'a>( - &self, - gids: impl IntoIterator>, - ) -> Result<(), Self::Error> { - self.logical_to_physical - .validate_gids(gids) - .map_err(MutationError::from) - } - - fn validate_edge_props>( - &self, - is_static: bool, - prop: impl ExactSizeIterator, - ) -> Result, Self::Error> { - let session = self.write_session()?; - let properties = prop - .map(|(name, prop)| { - let dtype = prop.dtype(); - session - .resolve_edge_property(name.as_ref(), dtype, is_static) - .map(|id| (id.inner(), prop)) - }) - .collect::, _>>()?; - Ok(properties) - } -} - pub trait InheritAdditionOps: Base {} impl InternalAdditionOps for G @@ -509,7 +278,7 @@ where dst: VID, e_id: Option, layer_id: usize, - ) -> Self::AtomicAddEdge<'_> { + ) -> Result, Self::Error> { self.base().atomic_add_edge(src, dst, e_id, layer_id) } diff --git a/raphtory-storage/src/mutation/addition_ops_ext.rs b/raphtory-storage/src/mutation/addition_ops_ext.rs index 46fa993557..6055f01139 100644 --- a/raphtory-storage/src/mutation/addition_ops_ext.rs +++ b/raphtory-storage/src/mutation/addition_ops_ext.rs @@ -36,11 +36,11 @@ pub struct WriteS< EXT: PersistentStrategy, ES = ES>, > { static_session: WriteSession<'a, MNS, MES, NS, ES, EXT>, - layer: Option, ES, EXT>>, } -pub struct UnlockedSession<'a, EXT> { - graph: &'a TemporalGraph, +#[derive(Clone, Copy, Debug)] +pub struct UnlockedSession<'a> { + graph: &'a TemporalGraph, } impl< @@ -85,11 +85,11 @@ impl< } } -impl<'a, EXT: Send + Sync> SessionAdditionOps for UnlockedSession<'a, EXT> { +impl<'a> SessionAdditionOps for UnlockedSession<'a> { type Error = MutationError; fn next_event_id(&self) -> Result { - todo!() + Ok(self.graph.next_event_id()) } fn reserve_event_ids(&self, num_ids: usize) -> Result { @@ -161,7 +161,7 @@ impl<'a, EXT: Send + Sync> SessionAdditionOps for UnlockedSession<'a, EXT> { impl InternalAdditionOps for TemporalGraph { type Error = MutationError; - type WS<'a> = UnlockedSession<'a, Extension>; + type WS<'a> = UnlockedSession<'a>; type AtomicAddEdge<'a> = WriteS< 'a, @@ -229,7 +229,7 @@ impl InternalAdditionOps for TemporalGraph { } fn write_session(&self) -> Result, Self::Error> { - todo!() + Ok(UnlockedSession { graph: self }) } fn atomic_add_edge( @@ -237,14 +237,11 @@ impl InternalAdditionOps for TemporalGraph { src: VID, dst: VID, e_id: Option, - layer_id: usize, - ) -> Self::AtomicAddEdge<'_> { - let static_session = self.storage().write_session(src, dst, e_id); - - WriteS { - static_session, - layer: None, - } + _layer_id: usize, + ) -> Result, Self::Error> { + Ok(WriteS { + static_session: self.storage().write_session(src, dst, e_id), + }) } fn validate_edge_props>( diff --git a/raphtory/src/db/api/mutation/addition_ops.rs b/raphtory/src/db/api/mutation/addition_ops.rs index 6fa9ff4d12..cae48bef9f 100644 --- a/raphtory/src/db/api/mutation/addition_ops.rs +++ b/raphtory/src/db/api/mutation/addition_ops.rs @@ -242,7 +242,7 @@ impl> + StaticGraphViewOps> Addit .inner(); let layer_id = self.resolve_layer(layer).map_err(into_graph_err)?.inner(); - let mut add_edge_op = self.atomic_add_edge(src_id, dst_id, None, layer_id); + let mut add_edge_op = self.atomic_add_edge(src_id, dst_id, None, layer_id).map_err(into_graph_err)?; let edge_id = add_edge_op.internal_add_edge(ti, src_id, dst_id, 0, layer_id, props); add_edge_op.store_node_id_as_prop(src.as_node_ref(), src_id); diff --git a/raphtory/src/db/api/storage/storage.rs b/raphtory/src/db/api/storage/storage.rs index 5fe59bd70c..76afb90a05 100644 --- a/raphtory/src/db/api/storage/storage.rs +++ b/raphtory/src/db/api/storage/storage.rs @@ -8,20 +8,27 @@ use crate::{ }, }; use db4_graph::{TemporalGraph, WriteLockedGraph}; +use parking_lot::RwLockWriteGuard; use raphtory_api::core::{ entities::{EID, VID}, storage::{dict_mapper::MaybeNew, timeindex::TimeIndexEntry}, }; use raphtory_storage::{ graph::graph::GraphStorage, - mutation::addition_ops::{AtomicAdditionOps, SessionAdditionOps, TGWriteSession}, + mutation::{ + addition_ops::{AtomicAdditionOps, SessionAdditionOps}, + addition_ops_ext::{UnlockedSession, WriteS}, + }, }; use std::{ fmt::{Display, Formatter}, path::PathBuf, sync::Arc, }; -use storage::Extension; +use storage::{ + segments::{edge::MemEdgeSegment, node::MemNodeSegment}, + Extension, +}; use tracing::info; use crate::{db::api::view::IndexSpec, errors::GraphError}; @@ -183,27 +190,33 @@ impl InheritEdgeHistoryFilter for Storage {} impl InheritViewOps for Storage {} -#[derive(Clone, Copy)] +#[derive(Clone)] pub struct StorageWriteSession<'a> { - session: TGWriteSession<'a>, + session: UnlockedSession<'a>, + storage: &'a Storage, +} + +pub struct AtomicAddEdgeSession<'a> { + session: WriteS<'a, RwLockWriteGuard<'a, MemNodeSegment>, RwLockWriteGuard<'a, MemEdgeSegment>, Extension>, storage: &'a Storage, } -impl AtomicAdditionOps for StorageWriteSession<'_> { +impl AtomicAdditionOps for AtomicAddEdgeSession<'_> { fn internal_add_edge( &mut self, - _t: TimeIndexEntry, - _src: impl Into, - _dst: impl Into, - _lsn: u64, - _layer: usize, - _props: impl IntoIterator, + t: TimeIndexEntry, + src: impl Into, + dst: impl Into, + lsn: u64, + layer: usize, + props: impl IntoIterator, ) -> MaybeNew { - todo!() + self.session + .internal_add_edge(t, src, dst, lsn, layer, props) } fn store_node_id_as_prop(&mut self, id: NodeRef, vid: impl Into) { - todo!("set_node_id is not implemented for StorageWriteSession"); + self.session.store_node_id_as_prop(id, vid); } } @@ -320,7 +333,7 @@ impl InternalAdditionOps for Storage { type Error = GraphError; type WS<'a> = StorageWriteSession<'a>; - type AtomicAddEdge<'a> = StorageWriteSession<'a>; + type AtomicAddEdge<'a> = AtomicAddEdgeSession<'a>; fn write_lock(&self) -> Result, Self::Error> { Ok(self.graph.write_lock()?) @@ -375,12 +388,12 @@ impl InternalAdditionOps for Storage { dst: VID, e_id: Option, layer_id: usize, - ) -> Self::AtomicAddEdge<'_> { - let session = self.graph.atomic_add_edge(src, dst, e_id, layer_id); - StorageWriteSession { + ) -> Result, Self::Error> { + let session = self.graph.atomic_add_edge(src, dst, e_id, layer_id)?; + Ok(AtomicAddEdgeSession{ session, storage: self, - } + }) } fn validate_edge_props>( diff --git a/raphtory/src/db/graph/graph.rs b/raphtory/src/db/graph/graph.rs index 7be2426fe2..b277bc5ddf 100644 --- a/raphtory/src/db/graph/graph.rs +++ b/raphtory/src/db/graph/graph.rs @@ -607,7 +607,7 @@ mod db_tests { core_ops::CoreGraphOps, graph::nodes::node_storage_ops::NodeStorageOps, mutation::addition_ops::InternalAdditionOps, }; - use rayon::join; + use rayon::{join, vec}; use std::{ collections::{HashMap, HashSet}, ops::Range, @@ -792,6 +792,18 @@ mod db_tests { assert_eq!(g.count_edges(), unique_edge_count); } + #[test] + fn simle_add_edge(){ + let edges = vec![(1, 1, 2), (2, 2, 3), (3, 3, 4)]; + + let g = Graph::new(); + for &(t, src, dst) in edges.iter() { + g.add_edge(t, src, dst, NO_PROPS, None).unwrap(); + } + + assert!(edges.iter().all(|&(_, src, dst)| g.has_edge(src, dst))) + } + #[quickcheck] fn add_edge_works(edges: Vec<(i64, u64, u64)>) -> bool { let g = Graph::new(); From 695e46f6ef9da1036be1783f538ae6173c472b59 Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Wed, 2 Jul 2025 16:37:47 +0100 Subject: [PATCH 058/321] db_tests.constant_properties passes --- db4-graph/src/lib.rs | 11 +- db4-storage/src/api/nodes.rs | 41 +- db4-storage/src/pages/edge_page/writer.rs | 6 +- db4-storage/src/pages/layer_counter.rs | 8 +- db4-storage/src/pages/mod.rs | 5 +- db4-storage/src/pages/node_page/writer.rs | 19 +- db4-storage/src/properties/mod.rs | 18 +- .../src/properties/props_meta_writer.rs | 12 +- db4-storage/src/resolver/mapping_resolver.rs | 4 +- db4-storage/src/resolver/mod.rs | 4 +- db4-storage/src/segments/additions.rs | 5 +- db4-storage/src/segments/edge.rs | 5 +- db4-storage/src/segments/edge_entry.rs | 4 +- raphtory-api/src/core/storage/dict_mapper.rs | 6 +- raphtory-storage/src/core_ops.rs | 4 +- raphtory-storage/src/graph/graph.rs | 385 +----------------- raphtory-storage/src/mutation/addition_ops.rs | 71 +++- .../src/mutation/addition_ops_ext.rs | 39 +- .../src/mutation/property_addition_ops.rs | 24 +- raphtory/src/db/api/mutation/addition_ops.rs | 174 +++++--- raphtory/src/db/api/storage/storage.rs | 33 +- .../time_semantics/event_semantics.rs | 21 +- raphtory/src/db/graph/edge.rs | 35 +- raphtory/src/db/graph/graph.rs | 26 +- raphtory/src/db/graph/node.rs | 21 +- raphtory/src/serialise/parquet/edges.rs | 5 +- raphtory/src/serialise/serialise.rs | 2 +- 27 files changed, 410 insertions(+), 578 deletions(-) diff --git a/db4-graph/src/lib.rs b/db4-graph/src/lib.rs index 5e0e734ea6..56e82947b8 100644 --- a/db4-graph/src/lib.rs +++ b/db4-graph/src/lib.rs @@ -37,8 +37,8 @@ use storage::{ pub mod entries; pub mod mutation; -const DEFAULT_MAX_PAGE_LEN_NODES: usize = 1000; -const DEFAULT_MAX_PAGE_LEN_EDGES: usize = 1000; +const DEFAULT_MAX_PAGE_LEN_NODES: usize = 10; +const DEFAULT_MAX_PAGE_LEN_EDGES: usize = 10; #[derive(Debug)] pub struct TemporalGraph { @@ -62,7 +62,10 @@ fn random_temp_dir() -> PathBuf { impl, ES = ES>> TemporalGraph { pub fn new(path: Option) -> Self { - Self::new_with_meta(path, Meta::new(), Meta::new()) + let node_meta = Meta::new(); + let edge_meta = Meta::new(); + edge_meta.get_or_create_layer_id(Some("static_graph")); + Self::new_with_meta(path, node_meta, edge_meta) } pub fn new_with_meta(path: Option, node_meta: Meta, edge_meta: Meta) -> Self { @@ -103,7 +106,7 @@ impl, ES = ES>> TemporalGraph { } pub fn num_layers(&self) -> usize { - self.storage.nodes().num_layers() + self.storage.nodes().num_layers() - 1 } #[inline] diff --git a/db4-storage/src/api/nodes.rs b/db4-storage/src/api/nodes.rs index 00af3a7d4e..7aa4465bcf 100644 --- a/db4-storage/src/api/nodes.rs +++ b/db4-storage/src/api/nodes.rs @@ -144,9 +144,9 @@ pub trait NodeEntryOps<'a>: Send + Sync + 'a { } pub trait NodeRefOps<'a>: Copy + Clone + Send + Sync + 'a { - type Additions: TimeIndexOps<'a>; + type Additions: TimeIndexOps<'a, IndexType = TimeIndexEntry>; - type EdgeAdditions: TimeIndexOps<'a>; + type EdgeAdditions: TimeIndexOps<'a, IndexType = TimeIndexEntry>; type TProps: TPropOps<'a>; @@ -229,30 +229,43 @@ pub trait NodeRefOps<'a>: Copy + Clone + Send + Sync + 'a { let w = w.clone(); let mut time_ordered_iter = (0..self.node_meta().temporal_prop_meta().len()) .map(move |prop_id| { + let additions = self.node_additions(layer_id); + let additions = w + .clone() + .map(|w| Iter2::I1(additions.range(w).iter())) + .unwrap_or_else(|| Iter2::I2(additions.iter())); + self.temporal_prop_layer(layer_id, prop_id) .iter_inner(w.clone()) - .map(move |(t, prop)| (t, (prop_id, prop))) + .merge_join_by(additions, |(t1, _), t2| t1 <= t2) + .map(move |result| match result { + either::Either::Left((l, prop)) => (l, Some((prop_id, prop))), + either::Either::Right(r) => (r, None), + }) }) .kmerge_by(|(t1, _), (t2, _)| t1 <= t2); - if let Some((mut current_time, (prop_id, prop))) = time_ordered_iter.next() { - let mut current_row = vec![(prop_id, prop)]; + let mut done = false; + if let Some((mut current_time, maybe_prop)) = time_ordered_iter.next() { + let mut current_row = Vec::from_iter(maybe_prop); Iter2::I2(std::iter::from_fn(move || { - while let Some((t, (prop_id, prop))) = time_ordered_iter.next() { + if done { + return None; + } + while let Some((t, maybe_prop)) = time_ordered_iter.next() { if t == current_time { - current_row.push((prop_id, prop)); + current_row.extend(maybe_prop); } else { let row = std::mem::take(&mut current_row); + let out = Some((current_time, layer_id, row)); + current_row.extend(maybe_prop); current_time = t; - return Some((current_time, layer_id, row)); + return out; } } - if !current_row.is_empty() { - let row = std::mem::take(&mut current_row); - Some((current_time, layer_id, row)) - } else { - None - } + done = true; + let row = std::mem::take(&mut current_row); + Some((current_time, layer_id, row)) })) } else { Iter2::I1(std::iter::empty()) diff --git a/db4-storage/src/pages/edge_page/writer.rs b/db4-storage/src/pages/edge_page/writer.rs index cdc55e4985..444496063b 100644 --- a/db4-storage/src/pages/edge_page/writer.rs +++ b/db4-storage/src/pages/edge_page/writer.rs @@ -1,4 +1,4 @@ -use std::ops::DerefMut; +use std::{borrow::Borrow, ops::DerefMut}; use crate::{ LocalPOS, api::edges::EdgeSegmentOps, pages::layer_counter::GraphStats, @@ -109,13 +109,13 @@ impl<'a, MP: DerefMut, ES: EdgeSegmentOps> EdgeWriter<' self.page.get_edge(edge_pos, layer_id, self.writer.deref()) } - pub fn update_c_props( + pub fn update_c_props>( &mut self, edge_pos: LocalPOS, src: impl Into, dst: impl Into, layer_id: usize, - props: impl IntoIterator, + props: impl IntoIterator, ) { self.writer .update_const_properties(edge_pos, src, dst, layer_id, props); diff --git a/db4-storage/src/pages/layer_counter.rs b/db4-storage/src/pages/layer_counter.rs index 8386849244..47e59bc261 100644 --- a/db4-storage/src/pages/layer_counter.rs +++ b/db4-storage/src/pages/layer_counter.rs @@ -24,13 +24,7 @@ impl> From for GraphStats { impl GraphStats { pub fn new() -> Self { let layers = boxcar::Vec::new(); - for _ in 0..16 { - let id = layers.push_with(|_| AtomicUsize::new(0)); - while layers.get(id).is_none() { - // wait for the layer to be created - std::thread::yield_now(); - } - } + layers.push_with(|_| AtomicUsize::new(0)); Self { layers, earliest: MinCounter::new(), diff --git a/db4-storage/src/pages/mod.rs b/db4-storage/src/pages/mod.rs index 8e83df2a97..6a9fb2e8b0 100644 --- a/db4-storage/src/pages/mod.rs +++ b/db4-storage/src/pages/mod.rs @@ -375,7 +375,10 @@ impl< WriteSession::new(node_writers, edge_writer, self) } - fn node_writer(&self, node_segment: usize) -> NodeWriter, NS> { + pub fn node_writer( + &self, + node_segment: usize, + ) -> NodeWriter, NS> { self.nodes().writer(node_segment) } diff --git a/db4-storage/src/pages/node_page/writer.rs b/db4-storage/src/pages/node_page/writer.rs index 83e6021631..65e1e84aae 100644 --- a/db4-storage/src/pages/node_page/writer.rs +++ b/db4-storage/src/pages/node_page/writer.rs @@ -1,6 +1,8 @@ use crate::{ - LocalPOS, api::nodes::NodeSegmentOps, pages::layer_counter::GraphStats, - segments::node::MemNodeSegment, + LocalPOS, + api::nodes::NodeSegmentOps, + pages::layer_counter::GraphStats, + segments::node::{self, MemNodeSegment}, }; use raphtory_api::core::entities::{EID, VID, properties::prop::Prop}; use raphtory_core::{ @@ -162,13 +164,13 @@ impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> NodeWri self.update_c_props( pos, layer_id, - [(0, Prop::U64(node_type as u64)), (1, gid.into())], + node_info_as_props(Some(gid), Some(node_type)), lsn, ); } pub fn store_node_id(&mut self, pos: LocalPOS, layer_id: usize, gid: GidRef<'_>, lsn: u64) { - self.update_c_props(pos, layer_id, [(1, gid.into())], lsn); + self.update_c_props(pos, layer_id, node_info_as_props(Some(gid), None), lsn); } pub fn update_deletion_time( @@ -183,6 +185,15 @@ impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> NodeWri } } +pub fn node_info_as_props( + gid: Option, + node_type: Option, +) -> impl Iterator { + gid.into_iter() + .map(|g| (1, g.into())) + .chain(node_type.into_iter().map(|nt| (0, Prop::U64(nt as u64)))) +} + impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> Drop for NodeWriter<'a, MP, NS> { diff --git a/db4-storage/src/properties/mod.rs b/db4-storage/src/properties/mod.rs index 52d19ea6c8..9980a2951e 100644 --- a/db4-storage/src/properties/mod.rs +++ b/db4-storage/src/properties/mod.rs @@ -1,3 +1,5 @@ +use std::borrow::Borrow; + use bigdecimal::ToPrimitive; use polars_arrow::array::{Array, BooleanArray, PrimitiveArray, Utf8ViewArray}; use raphtory_api::core::entities::properties::{ @@ -13,8 +15,6 @@ use raphtory_core::{ storage::{PropColumn, TColumns, timeindex::TimeIndexEntry}, }; -use crate::segments::edge; - pub mod props_meta_writer; #[derive(Debug, Default)] @@ -312,15 +312,19 @@ impl<'a> PropMutEntry<'a> { prop_timestamps.edge_ts.set(t, edge_id.unwrap_or_default()); } - pub(crate) fn append_const_props(&mut self, props: impl IntoIterator) { - for (prop_id, prop) in props { - if self.properties.c_properties.len() <= prop_id { + pub(crate) fn append_const_props>( + &mut self, + props: impl IntoIterator, + ) { + for prop in props { + let (prop_id, prop) = prop.borrow(); + if self.properties.c_properties.len() <= *prop_id { self.properties .c_properties .resize_with(prop_id + 1, Default::default); } - let const_props = &mut self.properties.c_properties[prop_id]; - let _ = const_props.set(self.row, prop); + let const_props = &mut self.properties.c_properties[*prop_id]; + let _ = const_props.set(self.row, prop.clone()); } } } diff --git a/db4-storage/src/properties/props_meta_writer.rs b/db4-storage/src/properties/props_meta_writer.rs index b23d47bb96..eac64eb861 100644 --- a/db4-storage/src/properties/props_meta_writer.rs +++ b/db4-storage/src/properties/props_meta_writer.rs @@ -30,14 +30,14 @@ pub enum PropEntry<'a, PN: AsRef + 'a> { impl<'a, PN: AsRef> PropsMetaWriter<'a, PN> { pub fn temporal( meta: &'a Meta, - props: impl ExactSizeIterator, + props: impl Iterator, ) -> Result { Self::new(meta, meta.temporal_prop_meta(), props) } pub fn constant( meta: &'a Meta, - props: impl ExactSizeIterator, + props: impl Iterator, ) -> Result { Self::new(meta, meta.const_prop_meta(), props) } @@ -45,11 +45,15 @@ impl<'a, PN: AsRef> PropsMetaWriter<'a, PN> { pub fn new( meta: &'a Meta, prop_mapper: &'a PropMapper, - props: impl ExactSizeIterator, + props: impl Iterator, ) -> Result { let locked_meta = prop_mapper.locked(); - let mut in_props = Vec::with_capacity(props.len()); + let mut in_props = props + .size_hint() + .1 + .map(Vec::with_capacity) + .unwrap_or_default(); let mut no_type_changes = true; for (prop_name, prop) in props { diff --git a/db4-storage/src/resolver/mapping_resolver.rs b/db4-storage/src/resolver/mapping_resolver.rs index 3741559053..3ed168b6ce 100644 --- a/db4-storage/src/resolver/mapping_resolver.rs +++ b/db4-storage/src/resolver/mapping_resolver.rs @@ -31,10 +31,10 @@ impl GIDResolverOps for MappingResolver { Ok(()) } - fn get_or_init( + fn get_or_init VID>( &self, gid: GidRef, - next_id: impl FnMut() -> VID, + next_id: NFN, ) -> Result, GIDResolverError> { let result = self.mapping.get_or_init(gid, next_id)?; Ok(result) diff --git a/db4-storage/src/resolver/mod.rs b/db4-storage/src/resolver/mod.rs index 28b3862258..6555186ea5 100644 --- a/db4-storage/src/resolver/mod.rs +++ b/db4-storage/src/resolver/mod.rs @@ -24,10 +24,10 @@ pub trait GIDResolverOps { fn len(&self) -> usize; fn dtype(&self) -> Option; fn set(&self, gid: GidRef, vid: VID) -> Result<(), GIDResolverError>; - fn get_or_init( + fn get_or_init VID>( &self, gid: GidRef, - next_id: impl FnMut() -> VID, + next_id: NFN, ) -> Result, GIDResolverError>; fn validate_gids<'a>( &self, diff --git a/db4-storage/src/segments/additions.rs b/db4-storage/src/segments/additions.rs index c23a45baf8..e28029e806 100644 --- a/db4-storage/src/segments/additions.rs +++ b/db4-storage/src/segments/additions.rs @@ -5,10 +5,7 @@ use raphtory_core::{ storage::timeindex::{TimeIndexEntry, TimeIndexOps, TimeIndexWindow}, }; -use crate::{ - gen_ts::{EdgeEventOps, WithEdgeEvents}, - utils::Iter4, -}; +use crate::{gen_ts::EdgeEventOps, utils::Iter4}; #[derive(Clone, Debug)] pub enum MemAdditions<'a> { diff --git a/db4-storage/src/segments/edge.rs b/db4-storage/src/segments/edge.rs index 36d11f87d3..f95c38a106 100644 --- a/db4-storage/src/segments/edge.rs +++ b/db4-storage/src/segments/edge.rs @@ -1,4 +1,5 @@ use std::{ + borrow::Borrow, ops::{Deref, DerefMut}, sync::{ Arc, @@ -232,13 +233,13 @@ impl MemEdgeSegment { row.either(|a| a, |a| a) } - pub fn update_const_properties( + pub fn update_const_properties>( &mut self, edge_pos: impl Into, src: impl Into, dst: impl Into, layer_id: usize, - props: impl IntoIterator, + props: impl IntoIterator, ) { let edge_pos = edge_pos.into(); let src = src.into(); diff --git a/db4-storage/src/segments/edge_entry.rs b/db4-storage/src/segments/edge_entry.rs index 13b132baf3..b8177a0d2d 100644 --- a/db4-storage/src/segments/edge_entry.rs +++ b/db4-storage/src/segments/edge_entry.rs @@ -127,7 +127,9 @@ impl<'a> EdgeRefOps<'a> for MemEdgeRef<'a> { type TProps = EdgeTProps<'a>; fn edge(self, layer_id: usize) -> Option<(VID, VID)> { - self.es.as_ref()[layer_id] + self.es + .as_ref() + .get(layer_id)? //.get(layer_id)? .get(&self.pos) .map(|entry| (entry.src, entry.dst)) } diff --git a/raphtory-api/src/core/storage/dict_mapper.rs b/raphtory-api/src/core/storage/dict_mapper.rs index 6f4c6814c2..c1bcfcb031 100644 --- a/raphtory-api/src/core/storage/dict_mapper.rs +++ b/raphtory-api/src/core/storage/dict_mapper.rs @@ -242,9 +242,9 @@ impl DictMapper { pub fn get_name(&self, id: usize) -> ArcStr { let guard = self.reverse_map.read(); - guard.get(id).cloned().expect(&format!( - "internal ids should always be mapped to a name {id}" - )) + guard.get(id).cloned().unwrap_or_else(|| { + panic!("internal ids should always be mapped to a name {id}\n{self:?}") + }) } pub fn get_keys(&self) -> ArcReadLockedVec { diff --git a/raphtory-storage/src/core_ops.rs b/raphtory-storage/src/core_ops.rs index 0db3afe7c6..5d334f95b7 100644 --- a/raphtory-storage/src/core_ops.rs +++ b/raphtory-storage/src/core_ops.rs @@ -229,9 +229,7 @@ pub trait CoreGraphOps: Send + Sync { /// The property value if it exists. fn constant_node_prop(&self, v: VID, id: usize) -> Option { let core_node_entry = self.core_node(v); - // TODO: figure out how to expose the layer_id to the calling API - // core_node_entry.prop(0, id) - None + core_node_entry.constant_prop_layer(0, id) } /// Gets the keys of constant properties of a given node diff --git a/raphtory-storage/src/graph/graph.rs b/raphtory-storage/src/graph/graph.rs index c7134452bd..3432b0ee55 100644 --- a/raphtory-storage/src/graph/graph.rs +++ b/raphtory-storage/src/graph/graph.rs @@ -243,390 +243,15 @@ impl GraphStorage { pub fn layer_ids_iter(&self, layer_ids: &LayerIds) -> impl Iterator { match layer_ids { LayerIds::None => LayerVariants::None(iter::empty()), - LayerIds::All => LayerVariants::All(0..self.unfiltered_num_layers()), + LayerIds::All => LayerVariants::All(1..=self.unfiltered_num_layers()), LayerIds::One(id) => LayerVariants::One(iter::once(*id)), LayerIds::Multiple(ids) => LayerVariants::Multiple(ids.clone().into_iter()), } } - // - // pub fn into_nodes_iter<'graph, G: GraphViewOps<'graph>>( - // self, - // view: G, - // node_list: NodeList, - // type_filter: Option>, - // ) -> BoxedLIter<'graph, VID> { - // node_list - // .into_iter() - // .filter(move |&vid| { - // let node = self.node_entry(vid); - // type_filter - // .as_ref() - // .map_or(true, |type_filter| type_filter[node.node_type_id()]) - // && view.filter_node(node.as_ref()) - // }) - // .into_dyn_boxed() - // } - // - // pub fn nodes_par<'a, 'graph: 'a, G: GraphViewOps<'graph>>( - // &'a self, - // view: &'a G, - // type_filter: Option<&'a Arc<[bool]>>, - // ) -> impl ParallelIterator + 'a { - // let nodes = self.nodes(); - // view.node_list().into_par_iter().filter(move |&vid| { - // let node = nodes.node(vid); - // type_filter.map_or(true, |type_filter| type_filter[node.node_type_id()]) - // && view.filter_node(node) - // }) - // } - // - // pub fn into_nodes_par<'graph, G: GraphViewOps<'graph>>( - // self, - // view: G, - // node_list: NodeList, - // type_filter: Option>, - // ) -> impl ParallelIterator + 'graph { - // node_list.into_par_iter().filter(move |&vid| { - // let node = self.node_entry(vid); - // type_filter - // .as_ref() - // .map_or(true, |type_filter| type_filter[node.node_type_id()]) - // && view.filter_node(node.as_ref()) - // }) - // } - // - // pub fn edges_iter<'graph, G: GraphViewOps<'graph>>( - // &'graph self, - // view: &'graph G, - // ) -> impl Iterator + Send + 'graph { - // let iter = self.edges().iter(view.layer_ids()); - // - // let filtered = match view.filter_state() { - // FilterState::Neither => FilterVariants::Neither(iter), - // FilterState::Both => { - // let nodes = self.nodes(); - // FilterVariants::Both(iter.filter(move |e| { - // view.filter_edge(e.as_ref(), view.layer_ids()) - // && view.filter_node(nodes.node(e.src())) - // && view.filter_node(nodes.node(e.dst())) - // })) - // } - // FilterState::Nodes => { - // let nodes = self.nodes(); - // FilterVariants::Nodes(iter.filter(move |e| { - // view.filter_node(nodes.node(e.src())) && view.filter_node(nodes.node(e.dst())) - // })) - // } - // FilterState::Edges | FilterState::BothIndependent => FilterVariants::Edges( - // iter.filter(|e| view.filter_edge(e.as_ref(), view.layer_ids())), - // ), - // }; - // filtered.map(|e| e.out_ref()) - // } - // - // pub fn into_edges_iter<'graph, G: GraphViewOps<'graph>>( - // self, - // view: G, - // ) -> impl Iterator + Send + 'graph { - // match view.node_list() { - // NodeList::List { elems } => { - // return elems - // .into_iter() - // .flat_map(move |v| { - // self.clone() - // .into_node_edges_iter(v, Direction::OUT, view.clone()) - // }) - // .into_dyn_boxed() - // } - // _ => {} - // } - // let edges = self.owned_edges(); - // let nodes = self.owned_nodes(); - // - // match edges { - // EdgesStorage::Mem(edges) => { - // let iter = (0..edges.len()).map(EID); - // let filtered = match view.filter_state() { - // FilterState::Neither => { - // FilterVariants::Neither(iter.map(move |eid| edges.get_mem(eid).out_ref())) - // } - // FilterState::Both => FilterVariants::Both(iter.filter_map(move |e| { - // let e = EdgeStorageRef::Mem(edges.get_mem(e)); - // (view.filter_edge(e, view.layer_ids()) - // && view.filter_node(nodes.node_entry(e.src())) - // && view.filter_node(nodes.node_entry(e.dst()))) - // .then(|| e.out_ref()) - // })), - // FilterState::Nodes => FilterVariants::Nodes(iter.filter_map(move |e| { - // let e = EdgeStorageRef::Mem(edges.get_mem(e)); - // (view.filter_node(nodes.node_entry(e.src())) - // && view.filter_node(nodes.node_entry(e.dst()))) - // .then(|| e.out_ref()) - // })), - // FilterState::Edges | FilterState::BothIndependent => { - // FilterVariants::Edges(iter.filter_map(move |e| { - // let e = EdgeStorageRef::Mem(edges.get_mem(e)); - // view.filter_edge(e, view.layer_ids()).then(|| e.out_ref()) - // })) - // } - // }; - // filtered.into_dyn_boxed() - // } - // #[cfg(feature = "storage")] - // EdgesStorage::Disk(edges) => { - // let edges_clone = edges.clone(); - // let iter = edges_clone.into_iter_refs(view.layer_ids().clone()); - // let filtered = match view.filter_state() { - // FilterState::Neither => FilterVariants::Neither(iter), - // FilterState::Both => FilterVariants::Both(iter.filter_map(move |e| { - // let edge = EdgeStorageRef::Disk(edges.get(e.pid())); - // if !view.filter_edge(edge, view.layer_ids()) { - // return None; - // } - // let src = nodes.node_entry(e.src()); - // if !view.filter_node(src) { - // return None; - // } - // let dst = nodes.node_entry(e.dst()); - // if !view.filter_node(dst) { - // return None; - // } - // Some(e) - // })), - // FilterState::Nodes => FilterVariants::Nodes(iter.filter_map(move |e| { - // let src = nodes.node_entry(e.src()); - // if !view.filter_node(src) { - // return None; - // } - // let dst = nodes.node_entry(e.dst()); - // if !view.filter_node(dst) { - // return None; - // } - // Some(e) - // })), - // FilterState::Edges | FilterState::BothIndependent => { - // FilterVariants::Edges(iter.filter_map(move |e| { - // let edge = EdgeStorageRef::Disk(edges.get(e.pid())); - // if !view.filter_edge(edge, view.layer_ids()) { - // return None; - // } - // Some(e) - // })) - // } - // }; - // filtered.into_dyn_boxed() - // } - // } - // } - // - // pub fn edges_par<'graph, G: GraphViewOps<'graph>>( - // &'graph self, - // view: &'graph G, - // ) -> impl ParallelIterator + 'graph { - // self.edges() - // .par_iter(view.layer_ids()) - // .filter(|edge| match view.filter_state() { - // FilterState::Neither => true, - // FilterState::Both => { - // let src = self.node_entry(edge.src()); - // let dst = self.node_entry(edge.dst()); - // view.filter_edge(edge.as_ref(), view.layer_ids()) - // && view.filter_node(src.as_ref()) - // && view.filter_node(dst.as_ref()) - // } - // FilterState::Nodes => { - // let src = self.node_entry(edge.src()); - // let dst = self.node_entry(edge.dst()); - // view.filter_node(src.as_ref()) && view.filter_node(dst.as_ref()) - // } - // FilterState::Edges | FilterState::BothIndependent => { - // view.filter_edge(edge.as_ref(), view.layer_ids()) - // } - // }) - // .map(|e| e.out_ref()) - // } - // - // pub fn into_edges_par<'graph, G: GraphViewOps<'graph>>( - // self, - // view: G, - // ) -> impl ParallelIterator + 'graph { - // let edges = self.owned_edges(); - // let nodes = self.owned_nodes(); - // - // match edges { - // EdgesStorage::Mem(edges) => { - // let iter = (0..edges.len()).into_par_iter().map(EID); - // let filtered = match view.filter_state() { - // FilterState::Neither => FilterVariants::Neither( - // iter.map(move |eid| edges.get_mem(eid).as_edge_ref()), - // ), - // FilterState::Both => FilterVariants::Both(iter.filter_map(move |e| { - // let e = EdgeStorageRef::Mem(edges.get_mem(e)); - // (view.filter_edge(e, view.layer_ids()) - // && view.filter_node(nodes.node_entry(e.src())) - // && view.filter_node(nodes.node_entry(e.dst()))) - // .then(|| e.out_ref()) - // })), - // FilterState::Nodes => FilterVariants::Nodes(iter.filter_map(move |e| { - // let e = EdgeStorageRef::Mem(edges.get_mem(e)); - // (view.filter_node(nodes.node_entry(e.src())) - // && view.filter_node(nodes.node_entry(e.dst()))) - // .then(|| e.out_ref()) - // })), - // FilterState::Edges | FilterState::BothIndependent => { - // FilterVariants::Edges(iter.filter_map(move |e| { - // let e = EdgeStorageRef::Mem(edges.get_mem(e)); - // view.filter_edge(e, view.layer_ids()).then(|| e.out_ref()) - // })) - // } - // }; - // #[cfg(feature = "storage")] - // { - // StorageVariants::Mem(filtered) - // } - // #[cfg(not(feature = "storage"))] - // { - // filtered - // } - // } - // #[cfg(feature = "storage")] - // EdgesStorage::Disk(edges) => { - // let edges_clone = edges.clone(); - // let iter = edges_clone.into_par_iter_refs(view.layer_ids().clone()); - // let filtered = match view.filter_state() { - // FilterState::Neither => FilterVariants::Neither( - // iter.map(move |eid| EdgeStorageRef::Disk(edges.get(eid)).out_ref()), - // ), - // FilterState::Both => FilterVariants::Both(iter.filter_map(move |eid| { - // let e = EdgeStorageRef::Disk(edges.get(eid)); - // if !view.filter_edge(e, view.layer_ids()) { - // return None; - // } - // let src = nodes.node_entry(e.src()); - // if !view.filter_node(src) { - // return None; - // } - // let dst = nodes.node_entry(e.dst()); - // if !view.filter_node(dst) { - // return None; - // } - // Some(e.out_ref()) - // })), - // FilterState::Nodes => FilterVariants::Nodes(iter.filter_map(move |eid| { - // let e = EdgeStorageRef::Disk(edges.get(eid)); - // let src = nodes.node_entry(e.src()); - // if !view.filter_node(src) { - // return None; - // } - // let dst = nodes.node_entry(e.dst()); - // if !view.filter_node(dst) { - // return None; - // } - // Some(e.out_ref()) - // })), - // FilterState::Edges | FilterState::BothIndependent => { - // FilterVariants::Edges(iter.filter_map(move |eid| { - // let e = EdgeStorageRef::Disk(edges.get(eid)); - // if !view.filter_edge(e, view.layer_ids()) { - // return None; - // } - // Some(e.out_ref()) - // })) - // } - // }; - // StorageVariants::Disk(filtered) - // } - // } - // } - // - // pub fn node_neighbours_iter<'a, 'graph: 'a, G: GraphViewOps<'graph>>( - // &'a self, - // node: VID, - // dir: Direction, - // view: &'a G, - // ) -> impl Iterator + Send + 'a { - // self.node_edges_iter(node, dir, view) - // .map(|e| e.remote()) - // .dedup() - // } - // - // pub fn into_node_neighbours_iter<'graph, G: GraphViewOps<'graph>>( - // self, - // node: VID, - // dir: Direction, - // view: G, - // ) -> impl Iterator + 'graph { - // self.into_node_edges_iter(node, dir, view) - // .map(|e| e.remote()) - // .dedup() - // } - // - // #[inline] - // pub fn node_degree<'graph, G: GraphViewOps<'graph>>( - // &self, - // node: VID, - // dir: Direction, - // view: &G, - // ) -> usize { - // if matches!(view.filter_state(), FilterState::Neither) { - // self.node_entry(node).degree(view.layer_ids(), dir) - // } else { - // self.node_neighbours_iter(node, dir, view).count() - // } - // } - // - // pub fn node_edges_iter<'a, 'graph: 'a, G: GraphViewOps<'graph>>( - // &'a self, - // node: VID, - // dir: Direction, - // view: &'a G, - // ) -> impl Iterator + 'a { - // let source = self.node_entry(node); - // let layers = view.layer_ids(); - // let iter = source.into_edges_iter(layers, dir); - // match view.filter_state() { - // FilterState::Neither => FilterVariants::Neither(iter), - // FilterState::Both => FilterVariants::Both(iter.filter(|&e| { - // view.filter_edge(self.edge_entry(e.pid()).as_ref(), view.layer_ids()) - // && view.filter_node(self.node_entry(e.remote()).as_ref()) - // })), - // FilterState::Nodes => FilterVariants::Nodes( - // iter.filter(|e| view.filter_node(self.node_entry(e.remote()).as_ref())), - // ), - // FilterState::Edges | FilterState::BothIndependent => { - // FilterVariants::Edges(iter.filter(|&e| { - // view.filter_edge(self.edge_entry(e.pid()).as_ref(), view.layer_ids()) - // })) - // } - // } - // } - // - // pub fn into_node_edges_iter<'graph, G: GraphViewOps<'graph>>( - // self, - // node: VID, - // dir: Direction, - // view: G, - // ) -> impl Iterator + 'graph { - // let layers = view.layer_ids().clone(); - // let local = self.owned_node(node); - // let iter = local.into_edges_iter(layers, dir); - // - // match view.filter_state() { - // FilterState::Neither => FilterVariants::Neither(iter), - // FilterState::Both => FilterVariants::Both(iter.filter(move |&e| { - // view.filter_edge(self.edge_entry(e.pid()).as_ref(), view.layer_ids()) - // && view.filter_node(self.node_entry(e.remote()).as_ref()) - // })), - // FilterState::Nodes => FilterVariants::Nodes( - // iter.filter(move |e| view.filter_node(self.node_entry(e.remote()).as_ref())), - // ), - // FilterState::Edges | FilterState::BothIndependent => { - // FilterVariants::Edges(iter.filter(move |&e| { - // view.filter_edge(self.edge_entry(e.pid()).as_ref(), view.layer_ids()) - // })) - // } - // } - // } + + pub fn unfiltered_layer_ids(&self) -> impl Iterator { + 1..=self.unfiltered_num_layers() + } pub fn node_meta(&self) -> &Meta { match self { diff --git a/raphtory-storage/src/mutation/addition_ops.rs b/raphtory-storage/src/mutation/addition_ops.rs index a7ee2884d4..8c3e6ad074 100644 --- a/raphtory-storage/src/mutation/addition_ops.rs +++ b/raphtory-storage/src/mutation/addition_ops.rs @@ -10,7 +10,10 @@ use parking_lot::RwLockWriteGuard; use raphtory_api::{ core::{ entities::{ - properties::prop::{Prop, PropType}, + properties::{ + meta::Meta, + prop::{Prop, PropType}, + }, GidRef, EID, VID, }, storage::{dict_mapper::MaybeNew, timeindex::TimeIndexEntry}, @@ -32,7 +35,7 @@ pub trait InternalAdditionOps { where Self: 'a; - type AtomicAddEdge<'a>: AtomicAdditionOps + type AtomicAddEdge<'a>: AtomicEdgeAddition where Self: 'a; @@ -66,14 +69,24 @@ pub trait InternalAdditionOps { layer_id: usize, ) -> Result, Self::Error>; - fn validate_edge_props>( + fn internal_add_node( + &self, + t: TimeIndexEntry, + v: impl Into, + gid: Option, + node_type: Option, + props: impl IntoIterator, + ) -> Result<(), Self::Error>; + + fn validate_props>( &self, is_static: bool, - prop: impl ExactSizeIterator, + meta: &Meta, + prop: impl Iterator, ) -> Result, Self::Error>; } -pub trait AtomicAdditionOps: Send + Sync { +pub trait AtomicEdgeAddition: Send + Sync { /// add edge update fn internal_add_edge( &mut self, @@ -89,6 +102,16 @@ pub trait AtomicAdditionOps: Send + Sync { fn store_node_id_as_prop(&mut self, id: NodeRef, vid: impl Into); } +pub trait AtomicNodeAddition: Send + Sync { + /// add node update + fn internal_add_node( + &mut self, + t: TimeIndexEntry, + v: impl Into, + props: impl IntoIterator, + ) -> Result<(), MutationError>; +} + pub trait SessionAdditionOps: Send + Sync { type Error: From; /// get the sequence id for the next event @@ -195,13 +218,26 @@ impl InternalAdditionOps for GraphStorage { self.mutable()?.atomic_add_edge(src, dst, e_id, layer_id) } - fn validate_edge_props>( + fn internal_add_node( + &self, + t: TimeIndexEntry, + v: impl Into, + gid: Option, + node_type: Option, + props: impl IntoIterator, + ) -> Result<(), Self::Error> { + self.mutable()? + .internal_add_node(t, v, gid, node_type, props) + } + + fn validate_props>( &self, is_static: bool, - prop: impl ExactSizeIterator, + meta: &Meta, + prop: impl Iterator, ) -> Result, Self::Error> { self.mutable()? - .validate_edge_props(is_static, prop) + .validate_props(is_static, meta, prop) .map_err(MutationError::from) } @@ -283,12 +319,25 @@ where } #[inline] - fn validate_edge_props>( + fn internal_add_node( + &self, + t: TimeIndexEntry, + v: impl Into, + gid: Option, + node_type: Option, + props: impl IntoIterator, + ) -> Result<(), Self::Error> { + self.base().internal_add_node(t, v, gid, node_type, props) + } + + #[inline] + fn validate_props>( &self, is_static: bool, - prop: impl ExactSizeIterator, + meta: &Meta, + prop: impl Iterator, ) -> Result, Self::Error> { - self.base().validate_edge_props(is_static, prop) + self.base().validate_props(is_static, meta, prop) } #[inline] diff --git a/raphtory-storage/src/mutation/addition_ops_ext.rs b/raphtory-storage/src/mutation/addition_ops_ext.rs index 6055f01139..d07328723c 100644 --- a/raphtory-storage/src/mutation/addition_ops_ext.rs +++ b/raphtory-storage/src/mutation/addition_ops_ext.rs @@ -16,7 +16,11 @@ use raphtory_core::{ storage::{raw_edges::WriteLockedEdges, timeindex::TimeIndexEntry, WriteLockedNodes}, }; use storage::{ - pages::{session::WriteSession, NODE_ID_PROP_KEY}, + pages::{ + node_page::writer::{node_info_as_props, NodeWriter}, + session::WriteSession, + NODE_ID_PROP_KEY, + }, persist::strategy::PersistentStrategy, properties::props_meta_writer::PropsMetaWriter, resolver::GIDResolverOps, @@ -25,7 +29,7 @@ use storage::{ }; use crate::mutation::{ - addition_ops::{AtomicAdditionOps, InternalAdditionOps, SessionAdditionOps}, + addition_ops::{AtomicEdgeAddition, InternalAdditionOps, SessionAdditionOps}, MutationError, }; @@ -48,7 +52,7 @@ impl< MNS: DerefMut + Send + Sync, MES: DerefMut + Send + Sync, EXT: PersistentStrategy, ES = ES>, - > AtomicAdditionOps for WriteS<'a, MNS, MES, EXT> + > AtomicEdgeAddition for WriteS<'a, MNS, MES, EXT> { fn internal_add_edge( &mut self, @@ -64,7 +68,7 @@ impl< let eid = self .static_session .add_static_edge(src, dst, lsn) - .map(|eid| eid.with_layer(0)); + .map(|eid| eid.with_layer(layer)); self.static_session .add_edge_into_layer(t, src, dst, eid, lsn, props); @@ -244,18 +248,37 @@ impl InternalAdditionOps for TemporalGraph { }) } - fn validate_edge_props>( + fn internal_add_node( + &self, + t: TimeIndexEntry, + v: impl Into, + gid: Option, + node_type: Option, + props: impl IntoIterator, + ) -> Result<(), Self::Error> { + let v = v.into(); + let (segment, node_pos) = self.storage().nodes().resolve_pos(v); + let mut node_writer = self.storage().node_writer(segment); + let node_info = node_info_as_props(gid, node_type); + node_writer.add_props(t, node_pos, 0, props.into_iter(), 0); + node_writer.update_c_props(node_pos, 0, node_info, 0); + + Ok(()) + } + + fn validate_props>( &self, is_static: bool, - props: impl ExactSizeIterator, + meta: &Meta, + props: impl Iterator, ) -> Result, Self::Error> { if is_static { - let prop_ids = PropsMetaWriter::constant(self.edge_meta(), props) + let prop_ids = PropsMetaWriter::constant(meta, props) .and_then(|pmw| pmw.into_props_const()) .map_err(MutationError::DBV4Error)?; Ok(prop_ids) } else { - let prop_ids = PropsMetaWriter::temporal(self.edge_meta(), props) + let prop_ids = PropsMetaWriter::temporal(meta, props) .and_then(|pmw| pmw.into_props_temporal()) .map_err(MutationError::DBV4Error)?; Ok(prop_ids) diff --git a/raphtory-storage/src/mutation/property_addition_ops.rs b/raphtory-storage/src/mutation/property_addition_ops.rs index 46889338d5..78a9d6be73 100644 --- a/raphtory-storage/src/mutation/property_addition_ops.rs +++ b/raphtory-storage/src/mutation/property_addition_ops.rs @@ -13,6 +13,7 @@ use raphtory_api::{ inherit::Base, }; use raphtory_core::entities::graph::tgraph::TemporalGraph; +use storage::Extension; pub trait InternalPropertyAdditionOps { type Error: From; @@ -176,7 +177,7 @@ impl InternalPropertyAdditionOps for TemporalGraph { } } -impl InternalPropertyAdditionOps for db4_graph::TemporalGraph { +impl InternalPropertyAdditionOps for db4_graph::TemporalGraph { type Error = MutationError; fn internal_add_properties( @@ -203,7 +204,10 @@ impl InternalPropertyAdditionOps for db4_graph::TemporalGraph { vid: VID, props: &[(usize, Prop)], ) -> Result<(), Self::Error> { - todo!() + let (segment_id, node_pos) = self.storage().nodes().resolve_pos(vid); + let mut writer = self.storage().nodes().writer(segment_id); + writer.update_c_props(node_pos, 0, props.iter().cloned(), 0); + Ok(()) } fn internal_update_constant_node_properties( @@ -220,7 +224,13 @@ impl InternalPropertyAdditionOps for db4_graph::TemporalGraph { layer: usize, props: &[(usize, Prop)], ) -> Result<(), Self::Error> { - todo!() + let (_, edge_pos) = self.storage().edges().resolve_pos(eid); + let mut writer = self.storage().edge_writer(eid); + let (src, dst) = writer.get_edge(layer, edge_pos).unwrap_or_else(|| { + panic!("Edge with EID {eid:?} not found in layer {layer}"); + }); + writer.update_c_props(edge_pos, src, dst, layer, props); + Ok(()) } fn internal_update_constant_edge_properties( @@ -229,7 +239,13 @@ impl InternalPropertyAdditionOps for db4_graph::TemporalGraph { layer: usize, props: &[(usize, Prop)], ) -> Result<(), Self::Error> { - todo!() + let (_, edge_pos) = self.storage().edges().resolve_pos(eid); + let mut writer = self.storage().edge_writer(eid); + let (src, dst) = writer.get_edge(layer, edge_pos).unwrap_or_else(|| { + panic!("Edge with EID {eid:?} not found in layer {layer}"); + }); + writer.update_c_props(edge_pos, src, dst, layer, props); + Ok(()) } } diff --git a/raphtory/src/db/api/mutation/addition_ops.rs b/raphtory/src/db/api/mutation/addition_ops.rs index cae48bef9f..07a6311828 100644 --- a/raphtory/src/db/api/mutation/addition_ops.rs +++ b/raphtory/src/db/api/mutation/addition_ops.rs @@ -6,6 +6,7 @@ use crate::{ db::{ api::{ mutation::{time_from_input_session, CollectProperties, TryIntoInputTime}, + state::ops::node, view::StaticGraphViewOps, }, graph::{edge::EdgeView, node::NodeView}, @@ -18,7 +19,7 @@ use raphtory_api::core::{ storage::dict_mapper::MaybeNew::{Existing, New}, }; use raphtory_storage::mutation::addition_ops::{ - AtomicAdditionOps, InternalAdditionOps, SessionAdditionOps, + AtomicEdgeAddition, InternalAdditionOps, SessionAdditionOps, }; pub trait AdditionOps: StaticGraphViewOps + InternalAdditionOps> { @@ -44,28 +45,47 @@ pub trait AdditionOps: StaticGraphViewOps + InternalAdditionOps( + fn add_node< + V: AsNodeRef, + T: TryIntoInputTime, + PN: AsRef, + P: Into, + PII: IntoIterator, + >( &self, t: T, v: V, - props: PI, + props: PII, node_type: Option<&str>, ) -> Result, GraphError>; - fn create_node( + fn create_node< + V: AsNodeRef, + T: TryIntoInputTime, + PN: AsRef, + P: Into, + PI: ExactSizeIterator, + PII: IntoIterator, + >( &self, t: T, v: V, - props: PI, + props: PII, node_type: Option<&str>, ) -> Result, GraphError>; - fn add_node_with_custom_time_format( + fn add_node_with_custom_time_format< + V: AsNodeRef, + PN: AsRef, + P: Into, + PI: ExactSizeIterator, + PII: IntoIterator, + >( &self, t: &str, fmt: &str, v: V, - props: PI, + props: PII, node_type: Option<&str>, ) -> Result, GraphError> { let time: i64 = t.parse_time(fmt)?; @@ -129,80 +149,124 @@ pub trait AdditionOps: StaticGraphViewOps + InternalAdditionOps> + StaticGraphViewOps> AdditionOps for G { - fn add_node( + fn add_node< + V: AsNodeRef, + T: TryIntoInputTime, + PN: AsRef, + P: Into, + PII: IntoIterator, + >( &self, t: T, v: V, - props: PI, + props: PII, node_type: Option<&str>, ) -> Result, GraphError> { let session = self.write_session().map_err(|err| err.into())?; + self.validate_gids( + [v.as_node_ref()] + .iter() + .filter_map(|node_ref| node_ref.as_gid_ref().left()), + ) + .map_err(into_graph_err)?; + + let props = self + .validate_props( + false, + self.node_meta(), + props.into_iter().map(|(k, v)| (k, v.into())), + ) + .map_err(into_graph_err)?; let ti = time_from_input_session(&session, t)?; - let properties = props.collect_properties(|name, dtype| { - Ok(session - .resolve_node_property(name, dtype, false) - .map_err(into_graph_err)? - .inner()) - })?; - let v_id = match node_type { + let (node_id, node_type) = match node_type { None => self .resolve_node(v.as_node_ref()) .map_err(into_graph_err)? + .map(|node_id| (node_id, None)) .inner(), Some(node_type) => { - let (v_id, _) = self + let node_id = self .resolve_node_and_type(v.as_node_ref(), node_type) - .map_err(into_graph_err)? - .inner(); - v_id.inner() + .map_err(into_graph_err)?; + node_id + .map(|(node_id, node_type)| (node_id.inner(), Some(node_type.inner()))) + .inner() } }; - session - .internal_add_node(ti, v_id, &properties) - .map_err(into_graph_err)?; - Ok(NodeView::new_internal(self.clone(), v_id)) + + self.internal_add_node( + ti, + node_id, + v.as_node_ref().as_gid_ref().left(), + node_type, + props, + ) + .map_err(into_graph_err)?; + + Ok(NodeView::new_internal(self.clone(), node_id)) } - fn create_node( + fn create_node< + V: AsNodeRef, + T: TryIntoInputTime, + PN: AsRef, + P: Into, + PI: ExactSizeIterator, + PII: IntoIterator, + >( &self, t: T, v: V, - props: PI, + props: PII, node_type: Option<&str>, ) -> Result, GraphError> { let session = self.write_session().map_err(|err| err.into())?; + self.validate_gids( + [v.as_node_ref()] + .iter() + .filter_map(|node_ref| node_ref.as_gid_ref().left()), + ) + .map_err(into_graph_err)?; + + let props = self + .validate_props( + false, + self.node_meta(), + props.into_iter().map(|(k, v)| (k, v.into())), + ) + .map_err(into_graph_err)?; let ti = time_from_input_session(&session, t)?; - let v_id = match node_type { - None => self.resolve_node(v.as_node_ref()).map_err(into_graph_err)?, + let node_id = match node_type { + None => self + .resolve_node(v.as_node_ref()) + .map_err(into_graph_err)? + .map(|node_id| (node_id, None)), Some(node_type) => { - let (v_id, _) = self + let node_id = self .resolve_node_and_type(v.as_node_ref(), node_type) - .map_err(into_graph_err)? - .inner(); - v_id + .map_err(into_graph_err)?; + node_id.map(|(node_id, node_type)| (node_id.inner(), Some(node_type.inner()))) } }; - match v_id { - New(id) => { - let properties = props.collect_properties(|name, dtype| { - Ok(session - .resolve_node_property(name, dtype, false) - .map_err(into_graph_err)? - .inner()) - })?; + let is_new = node_id.is_new(); + let (node_id, node_type) = node_id.inner(); - session - .internal_add_node(ti, id, &properties) - .map_err(into_graph_err)?; - - Ok(NodeView::new_internal(self.clone(), id)) - } - Existing(id) => { - let node_id = self.node(id).unwrap().id(); - Err(GraphError::NodeExistsError(node_id)) - } + if !is_new { + let node_id = self.node(node_id).unwrap().id(); + return Err(GraphError::NodeExistsError(node_id)); } + + self.internal_add_node( + ti, + node_id, + v.as_node_ref().as_gid_ref().left(), + node_type, + props, + ) + .map_err(into_graph_err)?; + + Ok(NodeView::new_internal(self.clone(), node_id)) } fn add_edge< @@ -228,7 +292,11 @@ impl> + StaticGraphViewOps> Addit ) .map_err(into_graph_err)?; let props = self - .validate_edge_props(false, props.into_iter().map(|(k, v)| (k, v.into()))) + .validate_props( + false, + self.edge_meta(), + props.into_iter().map(|(k, v)| (k, v.into())), + ) .map_err(into_graph_err)?; let ti = time_from_input_session(&session, t)?; @@ -242,7 +310,9 @@ impl> + StaticGraphViewOps> Addit .inner(); let layer_id = self.resolve_layer(layer).map_err(into_graph_err)?.inner(); - let mut add_edge_op = self.atomic_add_edge(src_id, dst_id, None, layer_id).map_err(into_graph_err)?; + let mut add_edge_op = self + .atomic_add_edge(src_id, dst_id, None, layer_id) + .map_err(into_graph_err)?; let edge_id = add_edge_op.internal_add_edge(ti, src_id, dst_id, 0, layer_id, props); add_edge_op.store_node_id_as_prop(src.as_node_ref(), src_id); diff --git a/raphtory/src/db/api/storage/storage.rs b/raphtory/src/db/api/storage/storage.rs index 76afb90a05..53ad8c2e82 100644 --- a/raphtory/src/db/api/storage/storage.rs +++ b/raphtory/src/db/api/storage/storage.rs @@ -10,13 +10,13 @@ use crate::{ use db4_graph::{TemporalGraph, WriteLockedGraph}; use parking_lot::RwLockWriteGuard; use raphtory_api::core::{ - entities::{EID, VID}, + entities::{properties::meta::Meta, EID, VID}, storage::{dict_mapper::MaybeNew, timeindex::TimeIndexEntry}, }; use raphtory_storage::{ graph::graph::GraphStorage, mutation::{ - addition_ops::{AtomicAdditionOps, SessionAdditionOps}, + addition_ops::{AtomicEdgeAddition, SessionAdditionOps}, addition_ops_ext::{UnlockedSession, WriteS}, }, }; @@ -197,11 +197,16 @@ pub struct StorageWriteSession<'a> { } pub struct AtomicAddEdgeSession<'a> { - session: WriteS<'a, RwLockWriteGuard<'a, MemNodeSegment>, RwLockWriteGuard<'a, MemEdgeSegment>, Extension>, + session: WriteS< + 'a, + RwLockWriteGuard<'a, MemNodeSegment>, + RwLockWriteGuard<'a, MemEdgeSegment>, + Extension, + >, storage: &'a Storage, } -impl AtomicAdditionOps for AtomicAddEdgeSession<'_> { +impl AtomicEdgeAddition for AtomicAddEdgeSession<'_> { fn internal_add_edge( &mut self, t: TimeIndexEntry, @@ -390,18 +395,30 @@ impl InternalAdditionOps for Storage { layer_id: usize, ) -> Result, Self::Error> { let session = self.graph.atomic_add_edge(src, dst, e_id, layer_id)?; - Ok(AtomicAddEdgeSession{ + Ok(AtomicAddEdgeSession { session, storage: self, }) } - fn validate_edge_props>( + fn internal_add_node( + &self, + t: TimeIndexEntry, + v: impl Into, + gid: Option, + node_type: Option, + props: impl IntoIterator, + ) -> Result<(), Self::Error> { + Ok(self.graph.internal_add_node(t, v, gid, node_type, props)?) + } + + fn validate_props>( &self, is_static: bool, - prop: impl ExactSizeIterator, + meta: &Meta, + prop: impl Iterator, ) -> Result, Self::Error> { - Ok(self.graph.validate_edge_props(is_static, prop)?) + Ok(self.graph.validate_props(is_static, meta, prop)?) } fn validate_gids<'a>( diff --git a/raphtory/src/db/api/view/internal/time_semantics/event_semantics.rs b/raphtory/src/db/api/view/internal/time_semantics/event_semantics.rs index 96b20ade50..925d36e0e0 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/event_semantics.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/event_semantics.rs @@ -711,17 +711,20 @@ impl EdgeTimeSemanticsOps for EventSemantics { let layer_ids = view.layer_ids(); match layer_ids { LayerIds::None => return None, - LayerIds::All => match view.unfiltered_num_layers() { - 0 => return None, - 1 => { - return if layer_filter(0) { - e.constant_prop_layer(0, prop_id) - } else { - None + LayerIds::All => { + let unfiltered_num_layers = view.unfiltered_num_layers(); + match unfiltered_num_layers { + 0 => return None, + 1 => { + return if layer_filter(0) { + e.constant_prop_layer(1, prop_id) + } else { + None + } } + _ => {} } - _ => {} - }, + } LayerIds::One(layer_id) => { return if layer_filter(*layer_id) { e.constant_prop_layer(*layer_id, prop_id) diff --git a/raphtory/src/db/graph/edge.rs b/raphtory/src/db/graph/edge.rs index 2036787dbc..76e12dec47 100644 --- a/raphtory/src/db/graph/edge.rs +++ b/raphtory/src/db/graph/edge.rs @@ -339,9 +339,9 @@ impl EdgeView { /// fails unless the layer matches the edge view. If the edge view is not restricted /// to a single layer, 'None' sets the properties on the default layer and 'Some("name")' /// sets the properties on layer '"name"' and fails if that layer doesn't exist. - pub fn add_constant_properties( + pub fn add_constant_properties, P: Into>( &self, - properties: C, + properties: impl IntoIterator, layer: Option<&str>, ) -> Result<(), GraphError> { let input_layer_id = self.resolve_layer(layer, false)?; @@ -356,14 +356,11 @@ impl EdgeView { dst: self.dst().name(), }); } - let properties: Vec<(usize, Prop)> = properties.collect_properties(|name, dtype| { - Ok(self - .graph - .write_session() - .and_then(|s| s.resolve_edge_property(name, dtype, true)) - .map_err(into_graph_err)? - .inner()) - })?; + let properties = self.graph.core_graph().validate_props( + true, + self.graph.edge_meta(), + properties.into_iter().map(|(n, p)| (n, p.into())), + )?; self.graph .internal_add_constant_edge_properties(self.edge.pid(), input_layer_id, &properties) @@ -371,20 +368,18 @@ impl EdgeView { Ok(()) } - pub fn update_constant_properties( + pub fn update_constant_properties, P: Into>( &self, - props: C, + props: impl IntoIterator, layer: Option<&str>, ) -> Result<(), GraphError> { let input_layer_id = self.resolve_layer(layer, false).map_err(into_graph_err)?; - let properties: Vec<(usize, Prop)> = props.collect_properties(|name, dtype| { - Ok(self - .graph - .write_session() - .and_then(|s| s.resolve_edge_property(name, dtype, true)) - .map_err(into_graph_err)? - .inner()) - })?; + + let properties = self.graph.core_graph().validate_props( + true, + self.graph.edge_meta(), + props.into_iter().map(|(n, p)| (n, p.into())), + )?; self.graph .internal_update_constant_edge_properties(self.edge.pid(), input_layer_id, &properties) diff --git a/raphtory/src/db/graph/graph.rs b/raphtory/src/db/graph/graph.rs index b277bc5ddf..847029f3e7 100644 --- a/raphtory/src/db/graph/graph.rs +++ b/raphtory/src/db/graph/graph.rs @@ -793,7 +793,7 @@ mod db_tests { } #[test] - fn simle_add_edge(){ + fn simle_add_edge() { let edges = vec![(1, 1, 2), (2, 2, 3), (3, 3, 4)]; let g = Graph::new(); @@ -1552,19 +1552,23 @@ mod db_tests { .is_err()); assert_eq!( - v11.properties().constant().keys().collect::>(), - vec!["a", "b", "c"] - ); - assert!(v22.properties().constant().keys().next().is_none()); - assert!(v33.properties().constant().keys().next().is_none()); - assert_eq!( - v44.properties().constant().keys().collect::>(), - vec!["e"] + v11.properties().constant().values().collect::>(), + vec![ + Some(Prop::U64(11)), + Some(Prop::I64(11)), + Some(Prop::U32(11)), + None, + None + ], ); assert_eq!( - v55.properties().constant().keys().collect::>(), - vec!["f"] + v11.properties().constant().keys().collect::>(), + vec!["a", "b", "c", "e", "f"] ); + assert_eq!(v22.properties().constant().keys().count(), 5); + assert_eq!(v33.properties().constant().keys().count(), 5); + assert_eq!(v44.properties().constant().keys().count(), 5); + assert_eq!(v55.properties().constant().keys().count(), 5); assert_eq!( edge1111.properties().constant().keys().collect::>(), vec!["d", "a"] // all edges get all ids anyhow diff --git a/raphtory/src/db/graph/node.rs b/raphtory/src/db/graph/node.rs index bf0cab4eb1..b8f5709a2a 100644 --- a/raphtory/src/db/graph/node.rs +++ b/raphtory/src/db/graph/node.rs @@ -37,7 +37,9 @@ use raphtory_api::core::{ storage::{arc_str::ArcStr, timeindex::TimeIndexEntry}, }; use raphtory_storage::{ - core_ops::CoreGraphOps, graph::graph::GraphStorage, mutation::addition_ops::SessionAdditionOps, + core_ops::CoreGraphOps, + graph::graph::GraphStorage, + mutation::addition_ops::{InternalAdditionOps, SessionAdditionOps}, }; use std::{ fmt, @@ -402,18 +404,15 @@ impl<'graph, G: GraphViewOps<'graph>, GH: GraphViewOps<'graph>> BaseNodeViewOps< } impl NodeView<'static, G, G> { - pub fn add_constant_properties( + pub fn add_constant_properties, P: Into>( &self, - properties: C, + props: impl IntoIterator, ) -> Result<(), GraphError> { - let properties: Vec<(usize, Prop)> = properties.collect_properties(|name, dtype| { - Ok(self - .graph - .write_session() - .and_then(|s| s.resolve_node_property(name, dtype, true)) - .map_err(into_graph_err)? - .inner()) - })?; + let properties = self.graph.core_graph().validate_props( + true, + self.graph.node_meta(), + props.into_iter().map(|(n, p)| (n, p.into())), + )?; self.graph .internal_add_constant_node_properties(self.node, &properties) .map_err(into_graph_err) diff --git a/raphtory/src/serialise/parquet/edges.rs b/raphtory/src/serialise/parquet/edges.rs index 781639a97d..84cf5fdb5b 100644 --- a/raphtory/src/serialise/parquet/edges.rs +++ b/raphtory/src/serialise/parquet/edges.rs @@ -92,7 +92,7 @@ pub(crate) fn encode_edge_deletions( .into_iter() .map(EID) .flat_map(|eid| { - (0..g.unfiltered_num_layers()).flat_map(move |layer_id| { + g.unfiltered_layer_ids().flat_map(move |layer_id| { let edge = g_edges.edge(eid); let edge_ref = edge.out_ref(); GenLockedIter::from(edge, |edge| { @@ -146,7 +146,8 @@ pub(crate) fn encode_edge_cprop( .map(EID) .flat_map(|eid| { let edge_ref = g.core_edge(eid).out_ref(); - layers.clone().map(move |l_id| edge_ref.at_layer(l_id)) + g.unfiltered_layer_ids() + .map(move |l_id| edge_ref.at_layer(l_id)) }) .map(|edge| ParquetCEdge(EdgeView::new(g, edge))) .chunks(row_group_size) diff --git a/raphtory/src/serialise/serialise.rs b/raphtory/src/serialise/serialise.rs index 6cdeb5dcb6..d142fc5a89 100644 --- a/raphtory/src/serialise/serialise.rs +++ b/raphtory/src/serialise/serialise.rs @@ -226,7 +226,7 @@ impl StableEncode for GraphStorage { let edge = edges.edge(eid); let edge = edge.as_ref(); graph.new_edge(edge.src(), edge.dst(), eid); - for layer_id in 0..storage.unfiltered_num_layers() { + for layer_id in storage.unfiltered_layer_ids() { for (t, props) in zip_tprop_updates!((0..e_temporal_meta.len()) .map(|i| (i, edge.temporal_prop_layer(layer_id, i)))) From b5c6e7a3fb7fcb30297afe6c264a66c68e65ba75 Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Wed, 2 Jul 2025 21:17:22 +0100 Subject: [PATCH 059/321] fix windowing --- db4-graph/src/lib.rs | 4 ++- db4-storage/src/api/nodes.rs | 3 ++- db4-storage/src/pages/mod.rs | 32 +++++++++++++++++++++- db4-storage/src/pages/node_page/writer.rs | 5 +++- db4-storage/src/properties/mod.rs | 2 +- db4-storage/src/segments/additions.rs | 1 + db4-storage/src/segments/node.rs | 9 ++++--- raphtory-core/src/storage/timeindex.rs | 33 ++++++++++++++++++++++- 8 files changed, 79 insertions(+), 10 deletions(-) diff --git a/db4-graph/src/lib.rs b/db4-graph/src/lib.rs index 56e82947b8..3bd61e8922 100644 --- a/db4-graph/src/lib.rs +++ b/db4-graph/src/lib.rs @@ -57,7 +57,9 @@ impl Default for TemporalGraph { } fn random_temp_dir() -> PathBuf { - temp_dir().join(format!("raphtory-{}", uuid::Uuid::new_v4())) + temp_dir() + .join("raphtory_graphs") + .join(format!("raphtory-{}", uuid::Uuid::new_v4())) } impl, ES = ES>> TemporalGraph { diff --git a/db4-storage/src/api/nodes.rs b/db4-storage/src/api/nodes.rs index 7aa4465bcf..4b136fd67e 100644 --- a/db4-storage/src/api/nodes.rs +++ b/db4-storage/src/api/nodes.rs @@ -256,7 +256,8 @@ pub trait NodeRefOps<'a>: Copy + Clone + Send + Sync + 'a { if t == current_time { current_row.extend(maybe_prop); } else { - let row = std::mem::take(&mut current_row); + let mut row = std::mem::take(&mut current_row); + row.sort_unstable_by(|(a, _), (b, _)| a.cmp(b)); let out = Some((current_time, layer_id, row)); current_row.extend(maybe_prop); current_time = t; diff --git a/db4-storage/src/pages/mod.rs b/db4-storage/src/pages/mod.rs index 6a9fb2e8b0..df7de5c8a8 100644 --- a/db4-storage/src/pages/mod.rs +++ b/db4-storage/src/pages/mod.rs @@ -430,13 +430,15 @@ mod test { make_nodes, }, }; + use arrow::ipc::Time; + use bitvec::vec; use chrono::{DateTime, NaiveDateTime, Utc}; use core::panic; use proptest::prelude::*; use raphtory_api::core::entities::properties::prop::Prop; use raphtory_core::{ entities::{LayerIds, VID}, - storage::timeindex::TimeIndexOps, + storage::timeindex::{TimeIndexEntry, TimeIndexOps}, }; fn check_edges( @@ -581,6 +583,34 @@ mod test { check_edges(edges, 89, false); } + #[test] + fn node_temporal_props() { + let graph_dir = tempfile::tempdir().unwrap(); + let g = Layer::new(graph_dir.path(), 32, 32); + g.add_node_props::(1, 0, 0, vec![]) + .expect("Failed to add node props"); + g.add_node_props::(2, 0, 0, vec![]) + .expect("Failed to add node props"); + g.add_node_props::(3, 0, 0, vec![]) + .expect("Failed to add node props"); + g.add_node_props::(4, 0, 0, vec![]) + .expect("Failed to add node props"); + g.add_node_props::(8, 0, 0, vec![]) + .expect("Failed to add node props"); + + let node = g.nodes().node(0); + + let edge_ts = node.as_ref().edge_additions(0); + assert!(edge_ts.iter_t().collect::>().is_empty()); + let node_ts = node.as_ref().node_additions(0); + assert_eq!(node_ts.iter_t().collect::>(), vec![1, 2, 3, 4, 8]); + + let edge_ts = edge_ts.range_t(1..8); + assert!(edge_ts.iter_t().collect::>().is_empty()); + let node_ts = node_ts.range_t(1..8); + assert_eq!(node_ts.iter_t().collect::>(), vec![1, 2, 3, 4]); + } + #[test] fn add_one_edge_with_props() { let edges = make_edges(1, 1); diff --git a/db4-storage/src/pages/node_page/writer.rs b/db4-storage/src/pages/node_page/writer.rs index 65e1e84aae..4408ce1802 100644 --- a/db4-storage/src/pages/node_page/writer.rs +++ b/db4-storage/src/pages/node_page/writer.rs @@ -122,7 +122,10 @@ impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> NodeWri ) { self.writer.as_mut()[layer_id].set_lsn(lsn); self.l_counter.update_time(t.t()); - self.writer.add_props(t, pos, layer_id, props); + let is_new_node = self.writer.add_props(t, pos, layer_id, props); + if is_new_node && !self.page.check_node(pos, layer_id) { + self.l_counter.increment(layer_id); + } } pub fn update_c_props( diff --git a/db4-storage/src/properties/mod.rs b/db4-storage/src/properties/mod.rs index 9980a2951e..21e221f45c 100644 --- a/db4-storage/src/properties/mod.rs +++ b/db4-storage/src/properties/mod.rs @@ -217,7 +217,7 @@ impl Properties { // PropColumn::NDTime(lazy_vec) => todo!(), // PropColumn::DTime(lazy_vec) => todo!(), // PropColumn::Decimal(lazy_vec) => todo!(), - _ => todo!("Unsupported column type"), + _ => None, //todo!("Unsupported column type"), } } diff --git a/db4-storage/src/segments/additions.rs b/db4-storage/src/segments/additions.rs index e28029e806..82dc12372a 100644 --- a/db4-storage/src/segments/additions.rs +++ b/db4-storage/src/segments/additions.rs @@ -1,5 +1,6 @@ use std::ops::Range; +use itertools::Itertools; use raphtory_core::{ entities::{ELID, properties::tcell::TCell}, storage::timeindex::{TimeIndexEntry, TimeIndexOps, TimeIndexWindow}, diff --git a/db4-storage/src/segments/node.rs b/db4-storage/src/segments/node.rs index 09c3cfd7e9..43c356062c 100644 --- a/db4-storage/src/segments/node.rs +++ b/db4-storage/src/segments/node.rs @@ -257,14 +257,15 @@ impl MemNodeSegment { node_pos: LocalPOS, layer_id: usize, props: impl IntoIterator, - ) { + ) -> bool { let layer = self.get_or_create_layer(layer_id); - let row = layer - .reserve_local_row(node_pos) - .either(|a| a.row, |a| a.row); + let row = layer.reserve_local_row(node_pos); + let is_new = row.is_right(); + let row = row.either(|a| a.row, |a| a.row); let mut prop_mut_entry = self.layers[layer_id].properties_mut().get_mut_entry(row); let ts = TimeIndexEntry::new(t.t(), t.i()); prop_mut_entry.append_t_props(ts, props); + is_new } pub fn update_c_props( diff --git a/raphtory-core/src/storage/timeindex.rs b/raphtory-core/src/storage/timeindex.rs index 21f10f813e..226402872d 100644 --- a/raphtory-core/src/storage/timeindex.rs +++ b/raphtory-core/src/storage/timeindex.rs @@ -312,8 +312,9 @@ where TimeIndexWindow::Empty => TimeIndexWindow::Empty, TimeIndexWindow::Range { timeindex, range } => { let start = max(range.start, w.start); - let end = min(range.start, w.start); + let end = min(range.end, w.end); if end <= start { + println!("TimeIndexWindow::Range called with empty range: {:?} vs {range:?} {start:?}..{end:?}", w); TimeIndexWindow::Empty } else { TimeIndexWindow::Range { @@ -376,3 +377,33 @@ where } } } + +#[cfg(test)] +mod test { + use raphtory_api::core::storage::timeindex::TimeIndexEntry; + + use crate::{ + entities::properties::tcell::TCell, + storage::timeindex::{TimeIndex, TimeIndexOps, TimeIndexWindow}, + }; + + #[test] + fn window_of_window_not_empty() { + let mut cell: TCell<()> = TCell::default(); + cell.set(TimeIndexEntry::new(1, 0), ()); + cell.set(TimeIndexEntry::new(2, 0), ()); + cell.set(TimeIndexEntry::new(3, 0), ()); + cell.set(TimeIndexEntry::new(4, 0), ()); + cell.set(TimeIndexEntry::new(8, 0), ()); + + assert_eq!(cell.iter_t().count(), 5); + + let cell_ref = &cell; + let window = TimeIndexEntry::new(1, 0)..TimeIndexEntry::new(8, 0); + let w = TimeIndexOps::range(&cell_ref, window.clone()); + assert_eq!(w.clone().iter_t().count(), 4); + + let w = TimeIndexOps::range(&w, window.clone()); + assert_eq!(w.iter_t().count(), 4); + } +} From d6c2ac05092c41b053d44df39b96cea08f2d1569 Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Thu, 3 Jul 2025 13:32:18 +0100 Subject: [PATCH 060/321] fix num_layers and db_test.layers --- db4-graph/src/lib.rs | 2 +- db4-storage/src/api/nodes.rs | 5 +- db4-storage/src/gen_ts.rs | 82 +++++++++++++------ db4-storage/src/segments/mod.rs | 17 +++- db4-storage/src/segments/node_entry.rs | 6 +- raphtory-storage/src/core_ops.rs | 2 +- .../src/graph/nodes/node_entry.rs | 8 +- .../src/graph/nodes/node_storage_ops.rs | 19 +++-- .../src/mutation/addition_ops_ext.rs | 19 ++++- .../src/db/api/view/internal/filter_ops.rs | 1 + .../internal/time_semantics/filtered_node.rs | 7 +- 11 files changed, 125 insertions(+), 43 deletions(-) diff --git a/db4-graph/src/lib.rs b/db4-graph/src/lib.rs index 3bd61e8922..d804338083 100644 --- a/db4-graph/src/lib.rs +++ b/db4-graph/src/lib.rs @@ -167,7 +167,7 @@ impl, ES = ES>> TemporalGraph { match key { entities::Layer::None => Ok(LayerIds::None), entities::Layer::All => Ok(LayerIds::All), - entities::Layer::Default => Ok(LayerIds::One(0)), + entities::Layer::Default => Ok(LayerIds::One(1)), entities::Layer::One(id) => match self.edge_meta().get_layer_id(&id) { Some(id) => Ok(LayerIds::One(id)), None => Err(InvalidLayer::new( diff --git a/db4-storage/src/api/nodes.rs b/db4-storage/src/api/nodes.rs index 4b136fd67e..af9c48fe5f 100644 --- a/db4-storage/src/api/nodes.rs +++ b/db4-storage/src/api/nodes.rs @@ -27,6 +27,7 @@ use raphtory_core::{ use crate::{ LocalPOS, error::DBV4Error, + gen_ts::LayerIter, segments::node::MemNodeSegment, utils::{Iter2, Iter3, Iter4}, }; @@ -302,9 +303,9 @@ pub trait NodeRefOps<'a>: Copy + Clone + Send + Sync + 'a { self.inb_edges_sorted(layer_id).map(|(v, _)| v) } - fn edge_additions(self, layer_id: usize) -> Self::EdgeAdditions; + fn edge_additions>>(self, layer_id: L) -> Self::EdgeAdditions; - fn node_additions(self, layer_id: usize) -> Self::Additions; + fn node_additions>>(self, layer_id: L) -> Self::Additions; fn c_prop(self, layer_id: usize, prop_id: usize) -> Option; diff --git a/db4-storage/src/gen_ts.rs b/db4-storage/src/gen_ts.rs index 072537ecc1..0395ab545f 100644 --- a/db4-storage/src/gen_ts.rs +++ b/db4-storage/src/gen_ts.rs @@ -2,42 +2,68 @@ use std::ops::Range; use itertools::Itertools; use raphtory_core::{ - entities::ELID, + entities::{ELID, LayerIds}, storage::timeindex::{TimeIndexEntry, TimeIndexOps}, }; -use crate::{NodeEntryRef, segments::additions::MemAdditions}; +use crate::{NodeEntryRef, segments::additions::MemAdditions, utils::Iter2}; + +#[derive(Clone, Copy, Debug)] +pub enum LayerIter<'a> { + One(usize), + LRef(&'a LayerIds), +} + +pub static ALL_LAYERS: LayerIter<'static> = LayerIter::LRef(&LayerIds::All); + +impl<'a> LayerIter<'a> { + pub fn into_iter(self, num_layers: usize) -> impl Iterator + 'a { + match self { + LayerIter::One(id) => Iter2::I1(std::iter::once(id)), + LayerIter::LRef(layers) => Iter2::I2(layers.iter(num_layers)), + } + } +} + +impl From for LayerIter<'_> { + fn from(id: usize) -> Self { + LayerIter::One(id) + } +} + +impl<'a> From<&'a LayerIds> for LayerIter<'a> { + fn from(layers: &'a LayerIds) -> Self { + LayerIter::LRef(layers) + } +} // TODO: split the Node time operations into edge additions and property additions #[derive(Clone, Copy, Debug)] pub struct GenericTimeOps<'a, Ref> { range: Option<(TimeIndexEntry, TimeIndexEntry)>, - layer_id: usize, + layer_id: LayerIter<'a>, node: Ref, - _mark: std::marker::PhantomData<&'a ()>, } impl<'a, Ref> GenericTimeOps<'a, Ref> { - pub fn new_with_layer(node: Ref, layer_id: usize) -> Self { + pub fn new_with_layer(node: Ref, layer_id: impl Into>) -> Self { Self { range: None, - layer_id, + layer_id: layer_id.into(), node, - _mark: std::marker::PhantomData, } } - pub fn new_additions_with_layer(node: Ref, layer_id: usize) -> Self { + pub fn new_additions_with_layer(node: Ref, layer_id: impl Into>) -> Self { Self { range: None, - layer_id, + layer_id: layer_id.into(), node, - _mark: std::marker::PhantomData, } } } -pub trait WithTimeCells<'a>: Copy + Clone + Send + Sync +pub trait WithTimeCells<'a>: Copy + Clone + Send + Sync + std::fmt::Debug where Self: 'a, { @@ -154,18 +180,26 @@ where >::TimeCell: EdgeEventOps<'a>, { pub fn edge_events(self) -> impl Iterator + Send + Sync + 'a { - self.node - .additions_tc(self.layer_id, self.range) - .map(|t_cell| t_cell.edge_events()) + self.layer_id + .into_iter(self.node.num_layers()) + .flat_map(|layer_id| { + self.node + .additions_tc(layer_id, self.range) + .map(|t_cell| t_cell.edge_events()) + }) .kmerge_by(|a, b| a < b) } pub fn edge_events_rev( self, ) -> impl Iterator + Send + Sync + 'a { - self.node - .additions_tc(self.layer_id, self.range) - .map(|t_cell| t_cell.edge_events_rev()) + self.layer_id + .into_iter(self.node.num_layers()) + .flat_map(|layer_id| { + self.node + .additions_tc(layer_id, self.range) + .map(|t_cell| t_cell.edge_events_rev()) + }) .kmerge_by(|a, b| a > b) } } @@ -173,12 +207,13 @@ where impl<'a, Ref: WithTimeCells<'a> + 'a> GenericTimeOps<'a, Ref> { pub fn time_cells(self) -> impl Iterator + 'a { let range = self.range; - let layer_id = self.layer_id; - - let t_cells = self.node.t_props_tc(layer_id, range); - let a_cells = self.node.additions_tc(layer_id, range); - - t_cells.chain(a_cells) + self.layer_id + .into_iter(self.node.num_layers()) + .flat_map(move |layer_id| { + self.node + .t_props_tc(layer_id, range) + .chain(self.node.additions_tc(layer_id, range)) + }) } fn into_iter(self) -> impl Iterator + Send + Sync + 'a { @@ -205,7 +240,6 @@ impl<'a, Ref: WithTimeCells<'a> + 'a> TimeIndexOps<'a> for GenericTimeOps<'a, Re range: Some((w.start, w.end)), node: self.node, layer_id: self.layer_id, - _mark: std::marker::PhantomData, } } diff --git a/db4-storage/src/segments/mod.rs b/db4-storage/src/segments/mod.rs index b3b166eb44..2264bd5c79 100644 --- a/db4-storage/src/segments/mod.rs +++ b/db4-storage/src/segments/mod.rs @@ -10,6 +10,7 @@ use raphtory_core::{ }, storage::timeindex::TimeIndexEntry, }; +use rayon::prelude::*; use rustc_hash::FxHashMap; use crate::LocalPOS; @@ -53,7 +54,7 @@ impl Debug for SegmentContainer { } } -pub trait HasRow: Default { +pub trait HasRow: Default + Send + Sync { fn row(&self) -> usize; fn row_mut(&mut self) -> &mut usize; } @@ -183,6 +184,20 @@ impl SegmentContainer { }) } + pub fn all_entries_par( + &self, + ) -> impl ParallelIterator)> + '_ { + (0..self.items.len()).into_par_iter().map(move |l_pos| { + let exists = unsafe { self.items.get_unchecked(l_pos) }; + let l_pos = LocalPOS(l_pos); + let entry = (*exists).then(|| { + let entry = self.data.get(&l_pos).unwrap(); + (entry, self.properties().get_entry(entry.row())) + }); + (l_pos, entry) + }) + } + pub fn earliest(&self) -> Option { self.properties.earliest() } diff --git a/db4-storage/src/segments/node_entry.rs b/db4-storage/src/segments/node_entry.rs index 2dd823fe6f..b5a8c21aef 100644 --- a/db4-storage/src/segments/node_entry.rs +++ b/db4-storage/src/segments/node_entry.rs @@ -2,7 +2,7 @@ use crate::{ LocalPOS, NodeEdgeAdditions, NodePropAdditions, NodeTProps, api::nodes::{NodeEntryOps, NodeRefOps}, gen_t_props::WithTProps, - gen_ts::{EdgeAdditionCellsRef, PropAdditionCellsRef, WithTimeCells}, + gen_ts::{EdgeAdditionCellsRef, LayerIter, PropAdditionCellsRef, WithTimeCells}, segments::node::MemNodeSegment, }; use raphtory_api::core::{ @@ -159,11 +159,11 @@ impl<'a> NodeRefOps<'a> for MemNodeRef<'a> { self.ns.as_ref()[layer_id].c_prop_str(self.pos, prop_id) } - fn node_additions(self, layer_id: usize) -> Self::Additions { + fn node_additions>>(self, layer_id: L) -> Self::Additions { NodePropAdditions::new_with_layer(PropAdditionCellsRef::new(self), layer_id) } - fn edge_additions(self, layer_id: usize) -> Self::EdgeAdditions { + fn edge_additions>>(self, layer_id: L) -> Self::EdgeAdditions { NodeEdgeAdditions::new_additions_with_layer(EdgeAdditionCellsRef::new(self), layer_id) } diff --git a/raphtory-storage/src/core_ops.rs b/raphtory-storage/src/core_ops.rs index 5d334f95b7..81d9c96dab 100644 --- a/raphtory-storage/src/core_ops.rs +++ b/raphtory-storage/src/core_ops.rs @@ -137,7 +137,7 @@ pub trait CoreGraphOps: Send + Sync { let layer_ids = layer_ids.clone(); match layer_ids { LayerIds::None => Box::new(iter::empty()), - LayerIds::All => Box::new(self.edge_meta().layer_meta().get_keys().into_iter()), + LayerIds::All => Box::new(self.edge_meta().layer_meta().get_keys().into_iter().skip(1)), // first layer is static graph LayerIds::One(id) => { let name = self.edge_meta().layer_meta().get_name(id).clone(); Box::new(iter::once(name)) diff --git a/raphtory-storage/src/graph/nodes/node_entry.rs b/raphtory-storage/src/graph/nodes/node_entry.rs index 4377c44004..462818820b 100644 --- a/raphtory-storage/src/graph/nodes/node_entry.rs +++ b/raphtory-storage/src/graph/nodes/node_entry.rs @@ -8,6 +8,7 @@ use raphtory_api::core::{ use raphtory_core::storage::timeindex::TimeIndexEntry; use storage::{ api::nodes::{self, NodeEntryOps}, + gen_ts::LayerIter, utils::Iter2, NodeEntry, NodeEntryRef, }; @@ -141,11 +142,14 @@ impl<'a, 'b: 'a> NodeStorageOps<'a> for &'a NodeStorageEntry<'b> { self.as_ref().tprop(prop_id) } - fn node_additions(self, layer_id: usize) -> storage::NodePropAdditions<'a> { + fn node_additions>>(self, layer_id: L) -> storage::NodePropAdditions<'a> { self.as_ref().node_additions(layer_id) } - fn node_edge_additions(self, layer_id: usize) -> storage::NodeEdgeAdditions<'a> { + fn node_edge_additions>>( + self, + layer_id: L, + ) -> storage::NodeEdgeAdditions<'a> { self.as_ref().node_edge_additions(layer_id) } } diff --git a/raphtory-storage/src/graph/nodes/node_storage_ops.rs b/raphtory-storage/src/graph/nodes/node_storage_ops.rs index a2bdb4f095..53f7e80cf5 100644 --- a/raphtory-storage/src/graph/nodes/node_storage_ops.rs +++ b/raphtory-storage/src/graph/nodes/node_storage_ops.rs @@ -4,7 +4,7 @@ use raphtory_api::core::{ }; use raphtory_core::{entities::LayerVariants, storage::timeindex::TimeIndexEntry}; use std::{borrow::Cow, ops::Range}; -use storage::{api::nodes::NodeRefOps, NodeEntryRef}; +use storage::{api::nodes::NodeRefOps, gen_ts::LayerIter, NodeEntryRef}; pub trait NodeStorageOps<'a>: Copy + Sized + Send + Sync + 'a { fn degree(self, layers: &LayerIds, dir: Direction) -> usize; @@ -32,9 +32,12 @@ pub trait NodeStorageOps<'a>: Copy + Sized + Send + Sync + 'a { layer_ids: &'a LayerIds, ) -> impl Iterator + Send + Sync + 'a; - fn node_additions(self, layer_id: usize) -> storage::NodePropAdditions<'a>; + fn node_additions>>(self, layer_id: L) -> storage::NodePropAdditions<'a>; - fn node_edge_additions(self, layer_id: usize) -> storage::NodeEdgeAdditions<'a>; + fn node_edge_additions>>( + self, + layer_id: L, + ) -> storage::NodeEdgeAdditions<'a>; fn additions(self) -> storage::NodePropAdditions<'a>; @@ -119,11 +122,17 @@ impl<'a> NodeStorageOps<'a> for NodeEntryRef<'a> { } } - fn node_additions(self, layer_ids: usize) -> storage::NodePropAdditions<'a> { + fn node_additions>>( + self, + layer_ids: L, + ) -> storage::NodePropAdditions<'a> { NodeRefOps::node_additions(self, layer_ids) } - fn node_edge_additions(self, layer_id: usize) -> storage::NodeEdgeAdditions<'a> { + fn node_edge_additions>>( + self, + layer_id: L, + ) -> storage::NodeEdgeAdditions<'a> { NodeRefOps::edge_additions(self, layer_id) } diff --git a/raphtory-storage/src/mutation/addition_ops_ext.rs b/raphtory-storage/src/mutation/addition_ops_ext.rs index d07328723c..91139fa183 100644 --- a/raphtory-storage/src/mutation/addition_ops_ext.rs +++ b/raphtory-storage/src/mutation/addition_ops_ext.rs @@ -221,7 +221,24 @@ impl InternalAdditionOps for TemporalGraph { id: NodeRef, node_type: &str, ) -> Result, MaybeNew)>, Self::Error> { - todo!() + let vid = self.resolve_node(id)?; + let node_type_id = self.node_meta().get_or_create_node_type_id(node_type); + Ok(vid.map(|_| (vid, node_type_id))) + + // let mut entry = self.storage.get_node_mut(vid.inner()); + // let mut entry_ref = entry.to_mut(); + // let node_store = entry_ref.node_store_mut(); + // if node_store.node_type == 0 { + // node_store.update_node_type(node_type_id.inner()); + // Ok(MaybeNew::New((vid, node_type_id))) + // } else { + // let node_type_id = self + // .node_meta + // .get_node_type_id(node_type) + // .filter(|&node_type| node_type == node_store.node_type) + // .ok_or(MutationError::NodeTypeError)?; + // Ok(MaybeNew::Existing((vid, MaybeNew::Existing(node_type_id)))) + // } } fn validate_gids<'a>( diff --git a/raphtory/src/db/api/view/internal/filter_ops.rs b/raphtory/src/db/api/view/internal/filter_ops.rs index 905e9a8445..d4ae89f07b 100644 --- a/raphtory/src/db/api/view/internal/filter_ops.rs +++ b/raphtory/src/db/api/view/internal/filter_ops.rs @@ -5,6 +5,7 @@ use iter_enum::{ }; use raphtory_storage::graph::nodes::node_ref::NodeStorageRef; +#[derive(Debug)] pub enum FilterState { Neither, Both, diff --git a/raphtory/src/db/api/view/internal/time_semantics/filtered_node.rs b/raphtory/src/db/api/view/internal/time_semantics/filtered_node.rs index b865b86586..287cae5cbd 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/filtered_node.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/filtered_node.rs @@ -12,6 +12,7 @@ use raphtory_storage::graph::{ edges::edge_storage_ops::EdgeStorageOps, nodes::node_storage_ops::NodeStorageOps, }; use std::ops::Range; +use storage::gen_ts::ALL_LAYERS; #[derive(Debug, Clone)] pub struct NodeHistory<'a, G> { @@ -232,9 +233,9 @@ impl<'b, G: GraphViewOps<'b>> TimeIndexOps<'b> for NodeHistory<'b, G> { pub trait FilteredNodeStorageOps<'a>: NodeStorageOps<'a> { fn history(self, view: G) -> NodeHistory<'a, G> { - // FIXME: new storage supports multiple layers, but this is hardcoded to layer 0 as there is no information in history about which layer the node belongs to. - let additions = self.node_additions(0); - let edge_history = self.node_edge_additions(0); + // FIXME: new storage supports multiple layers, we can be specific about the layers here once NodeStorageOps is updated + let additions = self.node_additions(ALL_LAYERS); + let edge_history = self.node_edge_additions(ALL_LAYERS); NodeHistory { edge_history, additions, From 9ac8ede807c1045b1b1001c1c583a16c0c85299f Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Thu, 3 Jul 2025 14:09:04 +0100 Subject: [PATCH 061/321] more fixes for db_test --- db4-storage/src/api/nodes.rs | 7 +++---- raphtory-storage/src/graph/edges/edge_storage_ops.rs | 3 ++- raphtory/src/db/graph/graph.rs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/db4-storage/src/api/nodes.rs b/db4-storage/src/api/nodes.rs index af9c48fe5f..159a7e3b33 100644 --- a/db4-storage/src/api/nodes.rs +++ b/db4-storage/src/api/nodes.rs @@ -193,7 +193,7 @@ pub trait NodeRefOps<'a>: Copy + Clone + Send + Sync + 'a { .map(move |(v, e)| EdgeRef::new_incoming(e, v, src_pid)), |e1, e2| e1.remote() < e2.remote(), ) - .dedup(), + .dedup_by(|l, r| l.pid() == r.pid()), ), } } @@ -214,7 +214,7 @@ pub trait NodeRefOps<'a>: Copy + Clone + Send + Sync + 'a { .into_iter() .map(|layer_id| self.edges_dir(layer_id, dir)) .kmerge_by(|e1, e2| e1.remote() < e2.remote()) - .dedup(), + .dedup_by(|l, r| l.pid() == r.pid()), ), LayerIds::None => Iter4::L(std::iter::empty()), } @@ -334,8 +334,7 @@ pub trait NodeRefOps<'a>: Copy + Clone + Send + Sync + 'a { fn node_type_id(&self) -> usize { self.c_prop(0, 0) .and_then(|prop| prop.into_u64()) - .map(|id| id as usize) - .expect("Node type should be present") + .map_or(0, |id| id as usize) } fn internal_num_layers(&self) -> usize; diff --git a/raphtory-storage/src/graph/edges/edge_storage_ops.rs b/raphtory-storage/src/graph/edges/edge_storage_ops.rs index b23f98cbcd..88257e18f8 100644 --- a/raphtory-storage/src/graph/edges/edge_storage_ops.rs +++ b/raphtory-storage/src/graph/edges/edge_storage_ops.rs @@ -180,6 +180,7 @@ pub trait EdgeStorageOps<'a>: Copy + Sized + Send + Sync + 'a { self.layer_ids_iter(layer_ids) .filter_map(move |id| Some((id, self.constant_prop_layer(id, prop_id)?))) } + } impl<'a> EdgeStorageOps<'a> for storage::EdgeEntryRef<'a> { @@ -221,7 +222,7 @@ impl<'a> EdgeStorageOps<'a> for storage::EdgeEntryRef<'a> { match layer_ids { LayerIds::None => LayerVariants::None(std::iter::empty()), LayerIds::All => LayerVariants::All( - (0..self.internal_num_layers()).filter(move |&l| self.has_layer_inner(l)), + (1..self.internal_num_layers()).filter(move |&l| self.has_layer_inner(l)), ), LayerIds::One(id) => { LayerVariants::One(self.has_layer_inner(*id).then_some(*id).into_iter()) diff --git a/raphtory/src/db/graph/graph.rs b/raphtory/src/db/graph/graph.rs index 847029f3e7..6d596e9a91 100644 --- a/raphtory/src/db/graph/graph.rs +++ b/raphtory/src/db/graph/graph.rs @@ -2875,7 +2875,7 @@ mod db_tests { }) .collect::>(); - assert_eq!(layer_exploded, vec![(1, 2, 0), (1, 2, 1), (1, 2, 2)]); + assert_eq!(layer_exploded, vec![(1, 2, 1), (1, 2, 2), (1, 2, 3)]); }); } From 3e042ca640c652de3091f56404a2fe6ffbc76777 Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Thu, 3 Jul 2025 17:39:04 +0100 Subject: [PATCH 062/321] add edge deletion --- db4-storage/src/properties/mod.rs | 11 +++++++++++ raphtory-storage/src/graph/edges/edge_storage_ops.rs | 1 - 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/db4-storage/src/properties/mod.rs b/db4-storage/src/properties/mod.rs index 21e221f45c..1708976aca 100644 --- a/db4-storage/src/properties/mod.rs +++ b/db4-storage/src/properties/mod.rs @@ -29,6 +29,7 @@ pub struct Properties { latest: Option, has_node_additions: bool, has_node_properties: bool, + has_deletions: bool, } pub(crate) struct PropMutEntry<'a> { @@ -97,6 +98,10 @@ impl Properties { self.has_node_additions } + pub fn has_deletions(&self) -> bool { + self.has_deletions + } + pub(crate) fn column_as_array( &self, column: &PropColumn, @@ -308,6 +313,8 @@ impl<'a> PropMutEntry<'a> { .resize_with(self.row + 1, Default::default); } + self.properties.has_deletions = true; + let prop_timestamps = &mut self.properties.deletions[self.row]; prop_timestamps.edge_ts.set(t, edge_id.unwrap_or_default()); } @@ -334,6 +341,10 @@ impl<'a> PropEntry<'a> { self.properties.additions.get(self.row) } + pub fn deletions(self) -> Option<&'a PropTimestamps> { + self.properties.deletions.get(self.row) + } + pub(crate) fn prop(self, prop_id: usize) -> Option> { let t_cell = self.t_cell(); Some(TPropCell::new(t_cell, self.properties.t_column(prop_id))) diff --git a/raphtory-storage/src/graph/edges/edge_storage_ops.rs b/raphtory-storage/src/graph/edges/edge_storage_ops.rs index 88257e18f8..165bc1e030 100644 --- a/raphtory-storage/src/graph/edges/edge_storage_ops.rs +++ b/raphtory-storage/src/graph/edges/edge_storage_ops.rs @@ -180,7 +180,6 @@ pub trait EdgeStorageOps<'a>: Copy + Sized + Send + Sync + 'a { self.layer_ids_iter(layer_ids) .filter_map(move |id| Some((id, self.constant_prop_layer(id, prop_id)?))) } - } impl<'a> EdgeStorageOps<'a> for storage::EdgeEntryRef<'a> { From a78f903c3ebb905e398d38edb07219a29de8832e Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Fri, 4 Jul 2025 12:22:38 +0100 Subject: [PATCH 063/321] surface edge deletions to raphtory --- db4-storage/src/pages/node_page/writer.rs | 11 ++-- db4-storage/src/pages/session.rs | 61 +++++++++++++++++++ db4-storage/src/segments/additions.rs | 1 - raphtory-storage/src/mutation/addition_ops.rs | 13 +++- .../src/mutation/addition_ops_ext.rs | 25 +++++++- raphtory/src/db/api/mutation/addition_ops.rs | 2 +- raphtory/src/db/api/mutation/deletion_ops.rs | 29 ++++++--- raphtory/src/db/api/storage/storage.rs | 16 ++++- raphtory/src/db/api/view/graph.rs | 4 +- 9 files changed, 137 insertions(+), 25 deletions(-) diff --git a/db4-storage/src/pages/node_page/writer.rs b/db4-storage/src/pages/node_page/writer.rs index 4408ce1802..c57f4a1dcd 100644 --- a/db4-storage/src/pages/node_page/writer.rs +++ b/db4-storage/src/pages/node_page/writer.rs @@ -176,15 +176,14 @@ impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> NodeWri self.update_c_props(pos, layer_id, node_info_as_props(Some(gid), None), lsn); } - pub fn update_deletion_time( - &self, - t: TimeIndexEntry, + pub fn update_deletion_time( + &mut self, + t: T, node: LocalPOS, - other: VID, - layer: ELID, + e_id: ELID, lsn: u64, ) { - todo!() + } } diff --git a/db4-storage/src/pages/session.rs b/db4-storage/src/pages/session.rs index ce3c4cf0b1..bed1e194fd 100644 --- a/db4-storage/src/pages/session.rs +++ b/db4-storage/src/pages/session.rs @@ -4,6 +4,7 @@ use super::{ GraphStore, edge_page::writer::EdgeWriter, node_page::writer::WriterPair, resolve_pos, }; use crate::{ + LocalPOS, api::{edges::EdgeSegmentOps, nodes::NodeSegmentOps}, error::DBV4Error, pages::{NODE_ID_PROP_KEY, NODE_TYPE_PROP_KEY}, @@ -113,6 +114,66 @@ impl< } } + pub fn delete_edge_from_layer( + &mut self, + t: T, + src: impl Into, + dst: impl Into, + edge: MaybeNew, + lsn: u64, + ) { + let src = src.into(); + let dst = dst.into(); + let e_id = edge.inner(); + let layer = e_id.layer(); + + // assert!(layer > 0, "Edge must be in a layer greater than 0"); + + let (_, src_pos) = self.graph.nodes().resolve_pos(src); + let (_, dst_pos) = self.graph.nodes().resolve_pos(dst); + + if let Some(writer) = self.edge_writer.as_mut() { + let edge_max_page_len = writer.writer.get_or_create_layer(layer).max_page_len(); + let (_, edge_pos) = resolve_pos(e_id.edge, edge_max_page_len); + let exists = Some(!edge.is_new()); + + writer.delete_edge(t, Some(edge_pos), src, dst, layer, lsn, exists); + } else { + let mut writer = self.graph.edge_writer(e_id.edge); + let edge_max_page_len = writer.writer.get_or_create_layer(layer).max_page_len(); + let (_, edge_pos) = resolve_pos(e_id.edge, edge_max_page_len); + let exists = Some(!edge.is_new()); + + writer.delete_edge(t, Some(edge_pos), src, dst, layer, lsn, exists); + } + + let edge_id = edge.inner(); + + if edge_id.layer() > 0 { + if edge.is_new() + || self + .node_writers + .get_mut_src() + .get_out_edge(src_pos, dst, edge_id.layer()) + .is_none() + { + self.node_writers + .get_mut_src() + .add_outbound_edge(t, src_pos, dst, edge_id, lsn); + self.node_writers + .get_mut_dst() + .add_inbound_edge(t, dst_pos, src, edge_id, lsn); + } + + self.node_writers + .get_mut_src() + .update_deletion_time(t, src_pos, e_id, lsn); + self.node_writers + .get_mut_dst() + .update_deletion_time(t, dst_pos, e_id, lsn); + } + } + pub fn add_static_edge( &mut self, src: impl Into, diff --git a/db4-storage/src/segments/additions.rs b/db4-storage/src/segments/additions.rs index 82dc12372a..e28029e806 100644 --- a/db4-storage/src/segments/additions.rs +++ b/db4-storage/src/segments/additions.rs @@ -1,6 +1,5 @@ use std::ops::Range; -use itertools::Itertools; use raphtory_core::{ entities::{ELID, properties::tcell::TCell}, storage::timeindex::{TimeIndexEntry, TimeIndexOps, TimeIndexWindow}, diff --git a/raphtory-storage/src/mutation/addition_ops.rs b/raphtory-storage/src/mutation/addition_ops.rs index 8c3e6ad074..5f57df8311 100644 --- a/raphtory-storage/src/mutation/addition_ops.rs +++ b/raphtory-storage/src/mutation/addition_ops.rs @@ -35,7 +35,7 @@ pub trait InternalAdditionOps { where Self: 'a; - type AtomicAddEdge<'a>: AtomicEdgeAddition + type AtomicAddEdge<'a>: EdgeWriteLock where Self: 'a; @@ -86,7 +86,7 @@ pub trait InternalAdditionOps { ) -> Result, Self::Error>; } -pub trait AtomicEdgeAddition: Send + Sync { +pub trait EdgeWriteLock: Send + Sync { /// add edge update fn internal_add_edge( &mut self, @@ -98,6 +98,15 @@ pub trait AtomicEdgeAddition: Send + Sync { props: impl IntoIterator, ) -> MaybeNew; + fn internal_delete_edge( + &mut self, + t: TimeIndexEntry, + src: impl Into, + dst: impl Into, + lsn: u64, + layer: usize, + ) -> MaybeNew; + /// Stores id as a const prop within the node fn store_node_id_as_prop(&mut self, id: NodeRef, vid: impl Into); } diff --git a/raphtory-storage/src/mutation/addition_ops_ext.rs b/raphtory-storage/src/mutation/addition_ops_ext.rs index 91139fa183..ab7f9bd434 100644 --- a/raphtory-storage/src/mutation/addition_ops_ext.rs +++ b/raphtory-storage/src/mutation/addition_ops_ext.rs @@ -29,7 +29,7 @@ use storage::{ }; use crate::mutation::{ - addition_ops::{AtomicEdgeAddition, InternalAdditionOps, SessionAdditionOps}, + addition_ops::{EdgeWriteLock, InternalAdditionOps, SessionAdditionOps}, MutationError, }; @@ -52,7 +52,7 @@ impl< MNS: DerefMut + Send + Sync, MES: DerefMut + Send + Sync, EXT: PersistentStrategy, ES = ES>, - > AtomicEdgeAddition for WriteS<'a, MNS, MES, EXT> + > EdgeWriteLock for WriteS<'a, MNS, MES, EXT> { fn internal_add_edge( &mut self, @@ -73,7 +73,26 @@ impl< self.static_session .add_edge_into_layer(t, src, dst, eid, lsn, props); - // TODO: consider storing node id as const prop here? + eid + } + + fn internal_delete_edge( + &mut self, + t: TimeIndexEntry, + src: impl Into, + dst: impl Into, + lsn: u64, + layer: usize, + ) -> MaybeNew { + let src = src.into(); + let dst = dst.into(); + let eid = self + .static_session + .add_static_edge(src, dst, lsn) + .map(|eid| eid.with_layer(layer)); + + self.static_session + .delete_edge_from_layer(t, src, dst, eid, lsn); eid } diff --git a/raphtory/src/db/api/mutation/addition_ops.rs b/raphtory/src/db/api/mutation/addition_ops.rs index 07a6311828..7128d91267 100644 --- a/raphtory/src/db/api/mutation/addition_ops.rs +++ b/raphtory/src/db/api/mutation/addition_ops.rs @@ -19,7 +19,7 @@ use raphtory_api::core::{ storage::dict_mapper::MaybeNew::{Existing, New}, }; use raphtory_storage::mutation::addition_ops::{ - AtomicEdgeAddition, InternalAdditionOps, SessionAdditionOps, + EdgeWriteLock, InternalAdditionOps, SessionAdditionOps, }; pub trait AdditionOps: StaticGraphViewOps + InternalAdditionOps> { diff --git a/raphtory/src/db/api/mutation/deletion_ops.rs b/raphtory/src/db/api/mutation/deletion_ops.rs index ef41bcdf56..221beb96fd 100644 --- a/raphtory/src/db/api/mutation/deletion_ops.rs +++ b/raphtory/src/db/api/mutation/deletion_ops.rs @@ -11,7 +11,7 @@ use crate::{ }; use raphtory_api::core::entities::edges::edge_ref::EdgeRef; use raphtory_storage::mutation::{ - addition_ops::InternalAdditionOps, deletion_ops::InternalDeletionOps, + addition_ops::{EdgeWriteLock, InternalAdditionOps}, deletion_ops::InternalDeletionOps, }; pub trait DeletionOps: @@ -28,7 +28,14 @@ pub trait DeletionOps: layer: Option<&str>, ) -> Result, GraphError> { let session = self.write_session().map_err(|err| err.into())?; - let ti = time_from_input_session(&session, t).map_err(into_graph_err)?; + self.validate_gids( + [src.as_node_ref(), dst.as_node_ref()] + .iter() + .filter_map(|node_ref| node_ref.as_gid_ref().left()), + ) + .map_err(into_graph_err)?; + + let ti = time_from_input_session(&session, t)?; let src_id = self .resolve_node(src.as_node_ref()) .map_err(into_graph_err)? @@ -37,15 +44,21 @@ pub trait DeletionOps: .resolve_node(dst.as_node_ref()) .map_err(into_graph_err)? .inner(); - let layer = self.resolve_layer(layer).map_err(into_graph_err)?.inner(); - let eid = self - .internal_delete_edge(ti, src_id, dst_id, layer) - .map_err(into_graph_err)? - .inner(); + let layer_id = self.resolve_layer(layer).map_err(into_graph_err)?.inner(); + + let mut add_edge_op = self + .atomic_add_edge(src_id, dst_id, None, layer_id) + .map_err(into_graph_err)?; + let edge_id = add_edge_op.internal_delete_edge(ti, src_id, dst_id, 0, layer_id); + + add_edge_op.store_node_id_as_prop(src.as_node_ref(), src_id); + add_edge_op.store_node_id_as_prop(dst.as_node_ref(), dst_id); + Ok(EdgeView::new( self.clone(), - EdgeRef::new_outgoing(eid, src_id, dst_id).at_layer(layer), + EdgeRef::new_outgoing(edge_id.inner().edge, src_id, dst_id).at_layer(layer_id), )) + } fn delete_edge_with_custom_time_format( diff --git a/raphtory/src/db/api/storage/storage.rs b/raphtory/src/db/api/storage/storage.rs index 53ad8c2e82..7ce4cd78d3 100644 --- a/raphtory/src/db/api/storage/storage.rs +++ b/raphtory/src/db/api/storage/storage.rs @@ -16,7 +16,7 @@ use raphtory_api::core::{ use raphtory_storage::{ graph::graph::GraphStorage, mutation::{ - addition_ops::{AtomicEdgeAddition, SessionAdditionOps}, + addition_ops::{EdgeWriteLock, SessionAdditionOps}, addition_ops_ext::{UnlockedSession, WriteS}, }, }; @@ -206,7 +206,7 @@ pub struct AtomicAddEdgeSession<'a> { storage: &'a Storage, } -impl AtomicEdgeAddition for AtomicAddEdgeSession<'_> { +impl EdgeWriteLock for AtomicAddEdgeSession<'_> { fn internal_add_edge( &mut self, t: TimeIndexEntry, @@ -220,6 +220,18 @@ impl AtomicEdgeAddition for AtomicAddEdgeSession<'_> { .internal_add_edge(t, src, dst, lsn, layer, props) } + fn internal_delete_edge( + &mut self, + t: TimeIndexEntry, + src: impl Into, + dst: impl Into, + lsn: u64, + layer: usize, + ) -> MaybeNew { + self.session + .internal_delete_edge(t, src, dst, lsn, layer) + } + fn store_node_id_as_prop(&mut self, id: NodeRef, vid: impl Into) { self.session.store_node_id_as_prop(id, vid); } diff --git a/raphtory/src/db/api/view/graph.rs b/raphtory/src/db/api/view/graph.rs index da5e8eb2b1..10bef5b90c 100644 --- a/raphtory/src/db/api/view/graph.rs +++ b/raphtory/src/db/api/view/graph.rs @@ -467,11 +467,11 @@ impl<'graph, G: GraphView + 'graph> GraphViewOps<'graph> for G { if let Some(node_pos) = shard.resolve_pos(src) { let mut writer = shard.writer(); - writer.update_deletion_time(t, node_pos, dst, eid.with_layer(layer), 0); + writer.update_deletion_time(t, node_pos, eid.with_layer(layer), 0); } if let Some(node_pos) = shard.resolve_pos(dst) { let mut writer = shard.writer(); - writer.update_deletion_time(t, node_pos, src, eid.with_layer(layer), 0); + writer.update_deletion_time(t, node_pos, eid.with_layer(layer), 0); } } } From 2f8eb057d35427b0bfa45b12c614429fad9191ec Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Fri, 4 Jul 2025 14:32:44 +0100 Subject: [PATCH 064/321] simplify SegmentContainer properties --- db4-storage/src/pages/mod.rs | 14 +++++ db4-storage/src/pages/node_page/writer.rs | 10 +--- db4-storage/src/properties/mod.rs | 57 +++++++++++--------- db4-storage/src/segments/edge_entry.rs | 2 +- db4-storage/src/segments/mod.rs | 44 ++++++++------- db4-storage/src/segments/node_entry.rs | 5 +- raphtory/src/db/api/mutation/deletion_ops.rs | 4 +- raphtory/src/db/api/storage/storage.rs | 17 +++--- 8 files changed, 84 insertions(+), 69 deletions(-) diff --git a/db4-storage/src/pages/mod.rs b/db4-storage/src/pages/mod.rs index df7de5c8a8..fa0d612922 100644 --- a/db4-storage/src/pages/mod.rs +++ b/db4-storage/src/pages/mod.rs @@ -690,6 +690,20 @@ mod test { }); } + #[test] + fn add_multiple_edges_with_props_14() { + let node_fixture = NodeFixture { + temp_props: vec![ + (VID(0), 0, vec![]), + (VID(1), 1, vec![]), + (VID(0), 2, vec![]), + ], + const_props: vec![(VID(0), vec![])], + }; + + check_graph_with_nodes(13, 13, &node_fixture); + } + #[test] fn add_multiple_node_with_props_4() { let node_fixture = NodeFixture { diff --git a/db4-storage/src/pages/node_page/writer.rs b/db4-storage/src/pages/node_page/writer.rs index c57f4a1dcd..7e8da77b8d 100644 --- a/db4-storage/src/pages/node_page/writer.rs +++ b/db4-storage/src/pages/node_page/writer.rs @@ -176,15 +176,7 @@ impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> NodeWri self.update_c_props(pos, layer_id, node_info_as_props(Some(gid), None), lsn); } - pub fn update_deletion_time( - &mut self, - t: T, - node: LocalPOS, - e_id: ELID, - lsn: u64, - ) { - - } + pub fn update_deletion_time(&mut self, t: T, node: LocalPOS, e_id: ELID, lsn: u64) {} } pub fn node_info_as_props( diff --git a/db4-storage/src/properties/mod.rs b/db4-storage/src/properties/mod.rs index 1708976aca..7069fe0855 100644 --- a/db4-storage/src/properties/mod.rs +++ b/db4-storage/src/properties/mod.rs @@ -9,7 +9,6 @@ use raphtory_api::core::entities::properties::{ use raphtory_core::{ entities::{ ELID, - nodes::node_store::PropTimestamps, properties::{tcell::TCell, tprop::TPropCell}, }, storage::{PropColumn, TColumns, timeindex::TimeIndexEntry}, @@ -21,8 +20,9 @@ pub mod props_meta_writer; pub struct Properties { c_properties: Vec, - additions: Vec, - deletions: Vec, + additions: Vec>, + deletions: Vec>, + times_from_props: Vec>>, t_properties: TColumns, earliest: Option, @@ -38,7 +38,7 @@ pub(crate) struct PropMutEntry<'a> { } #[derive(Debug, Clone, Copy)] -pub struct PropEntry<'a> { +pub struct RowEntry<'a> { row: usize, properties: &'a Properties, } @@ -55,8 +55,8 @@ impl Properties { } } - pub(crate) fn get_entry(&self, row: usize) -> PropEntry { - PropEntry { + pub(crate) fn get_entry(&self, row: usize) -> RowEntry { + RowEntry { row, properties: self, } @@ -86,10 +86,18 @@ impl Properties { self.c_properties.len() } - pub(crate) fn temporal_index(&self, row: usize) -> Option<&PropTimestamps> { + pub(crate) fn additions(&self, row: usize) -> Option<&TCell> { self.additions.get(row) } + pub(crate) fn deletions(&self, row: usize) -> Option<&TCell> { + self.deletions.get(row) + } + + pub(crate) fn times_from_props(&self, row: usize) -> Option<&TCell>> { + self.times_from_props.get(row) + } + pub fn has_node_properties(&self) -> bool { self.has_node_properties } @@ -280,13 +288,13 @@ impl<'a> PropMutEntry<'a> { row }; - if self.properties.additions.len() <= self.row { + if self.properties.times_from_props.len() <= self.row { self.properties - .additions + .times_from_props .resize_with(self.row + 1, Default::default); } - let prop_timestamps = &mut self.properties.additions[self.row]; - prop_timestamps.props_ts.set(t, Some(t_prop_row)); + let prop_timestamps = &mut self.properties.times_from_props[self.row]; + prop_timestamps.set(t, Some(t_prop_row)); self.properties.has_node_properties = true; self.properties.update_earliest_latest(t); @@ -301,7 +309,7 @@ impl<'a> PropMutEntry<'a> { self.properties.has_node_additions = true; let prop_timestamps = &mut self.properties.additions[self.row]; - prop_timestamps.edge_ts.set(t, edge_id); + prop_timestamps.set(t, edge_id); self.properties.update_earliest_latest(t); } @@ -316,7 +324,7 @@ impl<'a> PropMutEntry<'a> { self.properties.has_deletions = true; let prop_timestamps = &mut self.properties.deletions[self.row]; - prop_timestamps.edge_ts.set(t, edge_id.unwrap_or_default()); + prop_timestamps.set(t, edge_id.unwrap_or_default()); } pub(crate) fn append_const_props>( @@ -336,15 +344,7 @@ impl<'a> PropMutEntry<'a> { } } -impl<'a> PropEntry<'a> { - pub fn timestamps(self) -> Option<&'a PropTimestamps> { - self.properties.additions.get(self.row) - } - - pub fn deletions(self) -> Option<&'a PropTimestamps> { - self.properties.deletions.get(self.row) - } - +impl<'a> RowEntry<'a> { pub(crate) fn prop(self, prop_id: usize) -> Option> { let t_cell = self.t_cell(); Some(TPropCell::new(t_cell, self.properties.t_column(prop_id))) @@ -352,8 +352,15 @@ impl<'a> PropEntry<'a> { pub fn t_cell(self) -> &'a TCell> { self.properties - .additions - .get(self.row) - .map_or(&TCell::Empty, |ts| &ts.props_ts) + .times_from_props(self.row) + .unwrap_or(&TCell::Empty) + } + + pub fn additions(self) -> &'a TCell { + self.properties.additions(self.row).unwrap_or(&TCell::Empty) + } + + pub fn deletions(self) -> &'a TCell { + self.properties.deletions(self.row).unwrap_or(&TCell::Empty) } } diff --git a/db4-storage/src/segments/edge_entry.rs b/db4-storage/src/segments/edge_entry.rs index b8177a0d2d..f65f02ca39 100644 --- a/db4-storage/src/segments/edge_entry.rs +++ b/db4-storage/src/segments/edge_entry.rs @@ -80,7 +80,7 @@ impl<'a> WithTimeCells<'a> for MemEdgeRef<'a> { layer_id: usize, range: Option<(TimeIndexEntry, TimeIndexEntry)>, ) -> impl Iterator + 'a { - let t_cell = MemAdditions::Props(self.es.as_ref()[layer_id].additions(self.pos).props_ts()); + let t_cell = MemAdditions::Props(self.es.as_ref()[layer_id].times_from_props(self.pos)); std::iter::once( range .map(|(start, end)| t_cell.range(start..end)) diff --git a/db4-storage/src/segments/mod.rs b/db4-storage/src/segments/mod.rs index 2264bd5c79..00a74de578 100644 --- a/db4-storage/src/segments/mod.rs +++ b/db4-storage/src/segments/mod.rs @@ -5,7 +5,7 @@ use either::Either; use raphtory_api::core::entities::properties::{meta::Meta, prop::Prop}; use raphtory_core::{ entities::{ - nodes::node_store::PropTimestamps, + ELID, properties::{tcell::TCell, tprop::TPropCell}, }, storage::timeindex::TimeIndexEntry, @@ -15,7 +15,7 @@ use rustc_hash::FxHashMap; use crate::LocalPOS; -use super::properties::{PropEntry, Properties}; +use super::properties::{Properties, RowEntry}; pub mod edge; pub mod node; @@ -160,7 +160,7 @@ impl SegmentContainer { self.data.len() } - pub fn row_entries(&self) -> impl Iterator { + pub fn row_entries(&self) -> impl Iterator { self.items.iter_ones().filter_map(move |l_pos| { let entry = self.data.get(&LocalPOS(l_pos))?; Some(( @@ -171,9 +171,7 @@ impl SegmentContainer { }) } - pub fn all_entries( - &self, - ) -> impl ExactSizeIterator)> { + pub fn all_entries(&self) -> impl ExactSizeIterator)> { self.items.iter().enumerate().map(move |(l_pos, exists)| { let l_pos = LocalPOS(l_pos); let entry = (*exists).then(|| { @@ -186,7 +184,7 @@ impl SegmentContainer { pub fn all_entries_par( &self, - ) -> impl ParallelIterator)> + '_ { + ) -> impl ParallelIterator)> + '_ { (0..self.items.len()).into_par_iter().map(move |l_pos| { let exists = unsafe { self.items.get_unchecked(l_pos) }; let l_pos = LocalPOS(l_pos); @@ -211,9 +209,9 @@ impl SegmentContainer { .flat_map(|(_, mp, _)| { let row = mp.row(); self.properties() - .temporal_index(row) + .times_from_props(row) .into_iter() - .flat_map(|entry| entry.props_ts.iter()) + .flat_map(|entry| entry.iter()) .filter_map(|(_, &v)| v) }) .collect::>() @@ -256,18 +254,24 @@ impl SegmentContainer { }) } - pub fn additions(&self, item_pos: LocalPOS) -> &PropTimestamps { + pub fn additions(&self, item_pos: LocalPOS) -> &TCell { self.data .get(&item_pos) - .and_then(|entry| { - let prop_entry = self.properties.get_entry(entry.row()); - prop_entry.timestamps() - }) - .unwrap_or(&EMPTY_PROP_TIMESTAMPS) + .and_then(|entry| self.properties.additions(entry.row())) + .unwrap_or(&TCell::Empty) } -} -const EMPTY_PROP_TIMESTAMPS: PropTimestamps = PropTimestamps { - edge_ts: TCell::Empty, - props_ts: TCell::Empty, -}; + pub fn deletions(&self, item_pos: LocalPOS) -> &TCell { + self.data + .get(&item_pos) + .and_then(|entry| self.properties.deletions(entry.row())) + .unwrap_or(&TCell::Empty) + } + + pub fn times_from_props(&self, item_pos: LocalPOS) -> &TCell> { + self.data + .get(&item_pos) + .and_then(|entry| self.properties.times_from_props(entry.row())) + .unwrap_or(&TCell::Empty) + } +} diff --git a/db4-storage/src/segments/node_entry.rs b/db4-storage/src/segments/node_entry.rs index b5a8c21aef..92163292eb 100644 --- a/db4-storage/src/segments/node_entry.rs +++ b/db4-storage/src/segments/node_entry.rs @@ -75,7 +75,7 @@ impl<'a> WithTimeCells<'a> for MemNodeRef<'a> { layer_id: usize, range: Option<(TimeIndexEntry, TimeIndexEntry)>, ) -> impl Iterator + 'a { - let t_cell = MemAdditions::Props(self.ns.as_ref()[layer_id].additions(self.pos).props_ts()); + let t_cell = MemAdditions::Props(self.ns.as_ref()[layer_id].times_from_props(self.pos)); std::iter::once( range .map(|(start, end)| t_cell.range(start..end)) @@ -88,8 +88,7 @@ impl<'a> WithTimeCells<'a> for MemNodeRef<'a> { layer_id: usize, range: Option<(TimeIndexEntry, TimeIndexEntry)>, ) -> impl Iterator + 'a { - let additions = - MemAdditions::Edges(self.ns.as_ref()[layer_id].additions(self.pos).edge_ts()); + let additions = MemAdditions::Edges(self.ns.as_ref()[layer_id].additions(self.pos)); std::iter::once( range .map(|(start, end)| additions.range(start..end)) diff --git a/raphtory/src/db/api/mutation/deletion_ops.rs b/raphtory/src/db/api/mutation/deletion_ops.rs index 221beb96fd..9a4f0f89a5 100644 --- a/raphtory/src/db/api/mutation/deletion_ops.rs +++ b/raphtory/src/db/api/mutation/deletion_ops.rs @@ -11,7 +11,8 @@ use crate::{ }; use raphtory_api::core::entities::edges::edge_ref::EdgeRef; use raphtory_storage::mutation::{ - addition_ops::{EdgeWriteLock, InternalAdditionOps}, deletion_ops::InternalDeletionOps, + addition_ops::{EdgeWriteLock, InternalAdditionOps}, + deletion_ops::InternalDeletionOps, }; pub trait DeletionOps: @@ -58,7 +59,6 @@ pub trait DeletionOps: self.clone(), EdgeRef::new_outgoing(edge_id.inner().edge, src_id, dst_id).at_layer(layer_id), )) - } fn delete_edge_with_custom_time_format( diff --git a/raphtory/src/db/api/storage/storage.rs b/raphtory/src/db/api/storage/storage.rs index 7ce4cd78d3..7a0f79a399 100644 --- a/raphtory/src/db/api/storage/storage.rs +++ b/raphtory/src/db/api/storage/storage.rs @@ -221,15 +221,14 @@ impl EdgeWriteLock for AtomicAddEdgeSession<'_> { } fn internal_delete_edge( - &mut self, - t: TimeIndexEntry, - src: impl Into, - dst: impl Into, - lsn: u64, - layer: usize, - ) -> MaybeNew { - self.session - .internal_delete_edge(t, src, dst, lsn, layer) + &mut self, + t: TimeIndexEntry, + src: impl Into, + dst: impl Into, + lsn: u64, + layer: usize, + ) -> MaybeNew { + self.session.internal_delete_edge(t, src, dst, lsn, layer) } fn store_node_id_as_prop(&mut self, id: NodeRef, vid: impl Into) { From 094736e8a9785e745cc1da90fe164dd6391f28e1 Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Fri, 4 Jul 2025 17:34:44 +0100 Subject: [PATCH 065/321] fix import_node --- raphtory/src/db/api/mutation/import_ops.rs | 38 +++++++++++++--------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/raphtory/src/db/api/mutation/import_ops.rs b/raphtory/src/db/api/mutation/import_ops.rs index 411425b85e..6b8846ed28 100644 --- a/raphtory/src/db/api/mutation/import_ops.rs +++ b/raphtory/src/db/api/mutation/import_ops.rs @@ -13,6 +13,7 @@ use crate::{ AdditionOps, DeletionOps, EdgeViewOps, GraphViewOps, NodeViewOps, PropertyAdditionOps, }, }; +use itertools::Itertools; use raphtory_api::core::{ entities::GID, storage::{arc_str::OptionAsStr, timeindex::AsTime}, @@ -326,39 +327,46 @@ fn import_node_internal< merge: bool, ) -> Result, GraphError> { let id = id.as_node_ref(); + let gid_ref = id.as_gid_ref().left(); + graph.validate_gids(gid_ref).map_err(into_graph_err)?; if !merge { if let Some(existing_node) = graph.node(id) { return Err(GraphError::NodeExistsError(existing_node.id())); } } - let node_internal = match node.node_type().as_str() { - None => graph.resolve_node(id).map_err(into_graph_err)?.inner(), + let (node_internal, node_type) = match node.node_type().as_str() { + None => ( + graph.resolve_node(id).map_err(into_graph_err)?.inner(), + None, + ), Some(node_type) => { - let (node_internal, _) = graph + let (node_internal, node_type) = graph .resolve_node_and_type(id, node_type) .map_err(into_graph_err)? .inner(); - node_internal.inner() + (node_internal.inner(), Some(node_type.inner())) } }; let session = graph.write_session().map_err(|err| err.into())?; - let keys = node.temporal_prop_keys().collect::>(); + let keys = node.graph.node_meta().temporal_prop_meta().get_keys(); for (t, row) in node.rows() { let t = time_from_input_session(&session, t)?; - let props = row - .into_iter() - .zip(&keys) - .map(|((_, prop), key)| { - let prop_id = session.resolve_node_property(key, prop.dtype(), false); - prop_id.map(|prop_id| (prop_id.inner(), prop)) - }) - .collect::, _>>() + let props = graph + .validate_props( + false, + graph.node_meta(), + row.into_iter().map(|(prop_id, prop)| { + let prop_key = &keys[prop_id]; + (prop_key, prop) + }), + ) .map_err(into_graph_err)?; - session - .internal_add_node(t, node_internal, &props) + + graph + .internal_add_node(t, node_internal, gid_ref, node_type, props) .map_err(into_graph_err)?; } From a46bc2a42ef4871b881fd77a26c1b27792219791 Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Mon, 7 Jul 2025 13:21:57 +0100 Subject: [PATCH 066/321] fix some issues with materialize --- db4-graph/src/lib.rs | 17 +++--- db4-storage/src/pages/layer_counter.rs | 16 +++++- db4-storage/src/pages/locked/edges.rs | 10 ++++ db4-storage/src/pages/locked/nodes.rs | 10 ++++ db4-storage/src/pages/node_page/writer.rs | 23 ++++---- db4-storage/src/pages/session.rs | 18 ++---- db4-storage/src/segments/node.rs | 7 ++- raphtory-storage/src/mutation/deletion_ops.rs | 8 ++- raphtory/src/db/api/mutation/addition_ops.rs | 3 +- raphtory/src/db/api/view/graph.rs | 55 ++++++++----------- raphtory/src/db/api/view/internal/list_ops.rs | 4 ++ raphtory/src/db/graph/graph.rs | 3 +- raphtory/src/io/arrow/df_loaders.rs | 7 +-- 13 files changed, 100 insertions(+), 81 deletions(-) diff --git a/db4-graph/src/lib.rs b/db4-graph/src/lib.rs index d804338083..02293c7756 100644 --- a/db4-graph/src/lib.rs +++ b/db4-graph/src/lib.rs @@ -14,10 +14,7 @@ use raphtory_api::core::{ }; use raphtory_core::{ entities::{ - graph::{ - logical_to_physical::{InvalidNodeId, Mapping}, - tgraph::InvalidLayer, - }, + graph::{logical_to_physical::InvalidNodeId, tgraph::InvalidLayer}, nodes::node_ref::NodeRef, properties::graph_meta::GraphMeta, GidRef, LayerIds, EID, VID, @@ -66,11 +63,11 @@ impl, ES = ES>> TemporalGraph { pub fn new(path: Option) -> Self { let node_meta = Meta::new(); let edge_meta = Meta::new(); - edge_meta.get_or_create_layer_id(Some("static_graph")); Self::new_with_meta(path, node_meta, edge_meta) } pub fn new_with_meta(path: Option, node_meta: Meta, edge_meta: Meta) -> Self { + edge_meta.get_or_create_layer_id(Some("static_graph")); let graph_dir = path.unwrap_or_else(random_temp_dir); std::fs::create_dir_all(&graph_dir).unwrap_or_else(|_| { panic!( @@ -246,7 +243,7 @@ impl, ES = ES>> TemporalGraph { } pub fn update_time(&self, earliest: TimeIndexEntry) { - todo!() + // self.storage.update_time(earliest); } } @@ -261,8 +258,8 @@ pub struct WriteLockedGraph<'a, EXT> { impl<'a, EXT: PersistentStrategy, ES = ES>> WriteLockedGraph<'a, EXT> { pub fn new(graph: &'a TemporalGraph) -> Self { WriteLockedGraph { - nodes: graph.storage.nodes().write_locked().into(), - edges: graph.storage.edges().write_locked().into(), + nodes: graph.storage.nodes().write_locked(), + edges: graph.storage.edges().write_locked(), graph, num_nodes: Arc::new(AtomicUsize::new(graph.internal_num_nodes())), num_edges: Arc::new(AtomicUsize::new(graph.internal_num_edges())), @@ -299,14 +296,14 @@ impl<'a, EXT: PersistentStrategy, ES = ES>> WriteLockedGraph<' let (chunks_needed, _) = self.graph.storage.nodes().resolve_pos(VID(num_nodes - 1)); self.graph.storage().nodes().grow(chunks_needed + 1); std::mem::take(&mut self.nodes); - self.nodes = self.graph.storage.nodes().write_locked().into(); + self.nodes = self.graph.storage.nodes().write_locked(); } pub fn resize_chunks_to_num_edges(&mut self, num_edges: usize) { let (chunks_needed, _) = self.graph.storage.edges().resolve_pos(EID(num_edges - 1)); self.graph.storage().edges().grow(chunks_needed + 1); std::mem::take(&mut self.edges); - self.edges = self.graph.storage.edges().write_locked().into(); + self.edges = self.graph.storage.edges().write_locked(); } pub fn edge_stats(&self) -> &Arc { diff --git a/db4-storage/src/pages/layer_counter.rs b/db4-storage/src/pages/layer_counter.rs index 47e59bc261..439338fbb6 100644 --- a/db4-storage/src/pages/layer_counter.rs +++ b/db4-storage/src/pages/layer_counter.rs @@ -11,8 +11,7 @@ pub struct GraphStats { impl> From for GraphStats { fn from(iter: I) -> Self { - let counts = iter.into_iter().map(|c| AtomicUsize::new(c)).collect(); - let layers = boxcar::Vec::from(counts); + let layers = iter.into_iter().map(AtomicUsize::new).collect(); Self { layers, earliest: MinCounter::new(), @@ -21,6 +20,12 @@ impl> From for GraphStats { } } +impl Default for GraphStats { + fn default() -> Self { + Self::new() + } +} + impl GraphStats { pub fn new() -> Self { let layers = boxcar::Vec::new(); @@ -36,6 +41,11 @@ impl GraphStats { self.layers.count() } + #[must_use] + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + pub fn update_time(&self, t: i64) { self.earliest.update(t); self.latest.update(t); @@ -64,7 +74,7 @@ impl GraphStats { return counter; } - if self.layers.count() >= layer_id + 1 { + if self.layers.count() > layer_id { // something has allocated the layer, wait for it to be added loop { if let Some(counter) = self.layers.get(layer_id) { diff --git a/db4-storage/src/pages/locked/edges.rs b/db4-storage/src/pages/locked/edges.rs index 79eb9f4901..7926f94cbe 100644 --- a/db4-storage/src/pages/locked/edges.rs +++ b/db4-storage/src/pages/locked/edges.rs @@ -54,6 +54,10 @@ impl<'a, EXT, ES: EdgeSegmentOps> LockedEdgePage<'a, ES> { None } } + + pub fn ensure_layer(&mut self, layer_id: usize) { + self.lock.get_or_create_layer(layer_id); + } } pub struct WriteLockedEdgePages<'a, ES> { writers: Vec>, @@ -83,4 +87,10 @@ impl<'a, EXT, ES: EdgeSegmentOps> WriteLockedEdgePages<'a, ES> pub fn into_par_iter(self) -> impl ParallelIterator> + 'a { self.writers.into_par_iter() } + + pub fn ensure_layer(&mut self, layer_id: usize) { + for writer in &mut self.writers { + writer.ensure_layer(layer_id); + } + } } diff --git a/db4-storage/src/pages/locked/nodes.rs b/db4-storage/src/pages/locked/nodes.rs index 57e02ad0ad..a19ec908cd 100644 --- a/db4-storage/src/pages/locked/nodes.rs +++ b/db4-storage/src/pages/locked/nodes.rs @@ -53,6 +53,10 @@ impl<'a, EXT, NS: NodeSegmentOps> LockedNodePage<'a, NS> { None } } + + pub fn ensure_layer(&mut self, layer_id: usize) { + self.lock.get_or_create_layer(layer_id); + } } pub struct WriteLockedNodePages<'a, NS> { writers: Vec>, @@ -82,4 +86,10 @@ impl<'a, EXT, NS: NodeSegmentOps> WriteLockedNodePages<'a, NS> pub fn into_par_iter(self) -> impl ParallelIterator> + 'a { self.writers.into_par_iter() } + + pub fn ensure_layer(&mut self, layer_id: usize) { + for writer in &mut self.writers { + writer.ensure_layer(layer_id); + } + } } diff --git a/db4-storage/src/pages/node_page/writer.rs b/db4-storage/src/pages/node_page/writer.rs index 7e8da77b8d..4c20bc92d1 100644 --- a/db4-storage/src/pages/node_page/writer.rs +++ b/db4-storage/src/pages/node_page/writer.rs @@ -1,13 +1,11 @@ use crate::{ - LocalPOS, - api::nodes::NodeSegmentOps, - pages::layer_counter::GraphStats, - segments::node::{self, MemNodeSegment}, + LocalPOS, api::nodes::NodeSegmentOps, pages::layer_counter::GraphStats, + segments::node::MemNodeSegment, }; use raphtory_api::core::entities::{EID, VID, properties::prop::Prop}; use raphtory_core::{ entities::{ELID, GidRef}, - storage::timeindex::{AsTime, TimeIndexEntry}, + storage::timeindex::AsTime, }; use std::ops::DerefMut; @@ -42,10 +40,11 @@ impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> NodeWri &mut self, src_pos: LocalPOS, dst: impl Into, - e_id: impl Into, + e_id: impl Into, lsn: u64, ) { - self.add_outbound_edge_inner::(None, src_pos, dst, e_id, lsn); + let e_id = e_id.into(); + self.add_outbound_edge_inner::(None, src_pos, dst, e_id.with_layer(0), lsn); } fn add_outbound_edge_inner( @@ -85,10 +84,11 @@ impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> NodeWri &mut self, dst_pos: LocalPOS, src: impl Into, - e_id: impl Into, + e_id: impl Into, lsn: u64, ) { - self.add_inbound_edge_inner::(None, dst_pos, src, e_id, lsn); + let e_id = e_id.into(); + self.add_inbound_edge_inner::(None, dst_pos, src, e_id.with_layer(0), lsn); } fn add_inbound_edge_inner( @@ -136,7 +136,10 @@ impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> NodeWri lsn: u64, ) { self.writer.as_mut()[layer_id].set_lsn(lsn); - self.writer.update_c_props(pos, layer_id, props); + let is_new_node = self.writer.update_c_props(pos, layer_id, props); + if is_new_node && !self.page.check_node(pos, layer_id) { + self.l_counter.increment(layer_id); + } } pub fn update_timestamp(&mut self, t: T, pos: LocalPOS, e_id: ELID, lsn: u64) { diff --git a/db4-storage/src/pages/session.rs b/db4-storage/src/pages/session.rs index bed1e194fd..dab9b57d15 100644 --- a/db4-storage/src/pages/session.rs +++ b/db4-storage/src/pages/session.rs @@ -204,18 +204,12 @@ impl< let edge_id = edge_id.as_eid(edge_writer.segment_id(), self.graph.edges().max_page_len()); - self.node_writers.get_mut_src().add_static_outbound_edge( - src_pos, - dst, - edge_id.with_layer(layer_id), - lsn, - ); - self.node_writers.get_mut_dst().add_static_inbound_edge( - dst_pos, - src, - edge_id.with_layer(layer_id), - lsn, - ); + self.node_writers + .get_mut_src() + .add_static_outbound_edge(src_pos, dst, edge_id, lsn); + self.node_writers + .get_mut_dst() + .add_static_inbound_edge(dst_pos, src, edge_id, lsn); MaybeNew::New(edge_id) } diff --git a/db4-storage/src/segments/node.rs b/db4-storage/src/segments/node.rs index 43c356062c..1c270e053b 100644 --- a/db4-storage/src/segments/node.rs +++ b/db4-storage/src/segments/node.rs @@ -273,12 +273,15 @@ impl MemNodeSegment { node_pos: LocalPOS, layer_id: usize, props: impl IntoIterator, - ) { + ) -> bool { let row = self.layers[layer_id] .reserve_local_row(node_pos) - .either(|a| a.row, |a| a.row); + .map_either(|a| a.row, |a| a.row); + let is_new = row.is_right(); + let row = row.either(|a| a, |a| a); let mut prop_mut_entry = self.layers[layer_id].properties_mut().get_mut_entry(row); prop_mut_entry.append_const_props(props); + is_new } pub fn latest(&self) -> Option { diff --git a/raphtory-storage/src/mutation/deletion_ops.rs b/raphtory-storage/src/mutation/deletion_ops.rs index f24c092d3b..c8d8400f32 100644 --- a/raphtory-storage/src/mutation/deletion_ops.rs +++ b/raphtory-storage/src/mutation/deletion_ops.rs @@ -7,6 +7,7 @@ use raphtory_api::{ inherit::Base, }; use raphtory_core::entities::graph::tgraph::TemporalGraph; +use storage::Extension; pub trait InternalDeletionOps { type Error: From; @@ -25,7 +26,7 @@ pub trait InternalDeletionOps { ) -> Result<(), Self::Error>; } -impl InternalDeletionOps for db4_graph::TemporalGraph { +impl InternalDeletionOps for db4_graph::TemporalGraph { type Error = MutationError; fn internal_delete_edge( @@ -35,7 +36,10 @@ impl InternalDeletionOps for db4_graph::TemporalGraph { dst: VID, layer: usize, ) -> Result, Self::Error> { - todo!() + let mut session = self.storage().write_session(src, dst, None); + let edge = session.add_static_edge(src, dst, 0); + session.delete_edge_from_layer(t, src, dst, edge.map(|eid| eid.with_layer(layer)), 0); + Ok(edge) } fn internal_delete_existing_edge( diff --git a/raphtory/src/db/api/mutation/addition_ops.rs b/raphtory/src/db/api/mutation/addition_ops.rs index 7128d91267..841b19b1e0 100644 --- a/raphtory/src/db/api/mutation/addition_ops.rs +++ b/raphtory/src/db/api/mutation/addition_ops.rs @@ -5,8 +5,7 @@ use crate::{ }, db::{ api::{ - mutation::{time_from_input_session, CollectProperties, TryIntoInputTime}, - state::ops::node, + mutation::{time_from_input_session, TryIntoInputTime}, view::StaticGraphViewOps, }, graph::{edge::EdgeView, node::NodeView}, diff --git a/raphtory/src/db/api/view/graph.rs b/raphtory/src/db/api/view/graph.rs index 10bef5b90c..68e035650e 100644 --- a/raphtory/src/db/api/view/graph.rs +++ b/raphtory/src/db/api/view/graph.rs @@ -254,33 +254,26 @@ impl<'graph, G: GraphView + 'graph> GraphViewOps<'graph> for G { vec![] } LayerIds::All => { - let mut layer_map = vec![0; self.unfiltered_num_layers()]; + let mut layer_map = vec![0; self.unfiltered_num_layers() + 1]; let layers = storage.edge_meta().layer_meta().get_keys(); for id in 0..layers.len() { - let new_id = g - .resolve_layer(Some(&layers[id])) - .map_err(MutationError::from)? - .inner(); + let new_id = g.resolve_layer(Some(&layers[id]))?.inner(); layer_map[id] = new_id; } layer_map } LayerIds::One(l_id) => { - let mut layer_map = vec![0; self.unfiltered_num_layers()]; - let new_id = g - .resolve_layer(Some(&storage.edge_meta().get_layer_name_by_id(*l_id))) - .map_err(MutationError::from)?; + let mut layer_map = vec![0; self.unfiltered_num_layers() + 1]; + let new_id = + g.resolve_layer(Some(&storage.edge_meta().get_layer_name_by_id(*l_id)))?; layer_map[*l_id] = new_id.inner(); layer_map } LayerIds::Multiple(ids) => { - let mut layer_map = vec![0; self.unfiltered_num_layers()]; + let mut layer_map = vec![0; self.unfiltered_num_layers() + 1]; let layers = storage.edge_meta().layer_meta().get_keys(); for id in ids { - let new_id = g - .resolve_layer(Some(&layers[id])) - .map_err(MutationError::from)? - .inner(); + let new_id = g.resolve_layer(Some(&layers[id]))?.inner(); layer_map[id] = new_id; } layer_map @@ -304,19 +297,18 @@ impl<'graph, G: GraphView + 'graph> GraphViewOps<'graph> for G { let g = GraphStorage::from(g); - let g_session = g.write_session()?; - { // scope for the write lock let mut new_storage = g.write_lock()?; new_storage.resize_chunks_to_num_nodes(self.count_nodes()); + // TODO: resize the number of layers for nodes when this makes sense let mut node_map = vec![VID::default(); storage.unfiltered_num_nodes()]; let node_map_shared = atomic_usize_from_mut_slice(bytemuck::cast_slice_mut(&mut node_map)); - new_storage.nodes.par_iter_mut().try_for_each(|mut shard| { + new_storage.nodes.par_iter_mut().try_for_each(|shard| { for (index, node) in self.nodes().iter().enumerate() { let new_id = VID(index); let gid = node.id(); @@ -336,6 +328,8 @@ impl<'graph, G: GraphView + 'graph> GraphViewOps<'graph> for G { new_type_id, 0, ); + } else { + writer.store_node_id(node_pos, 0, gid.as_ref(), 0); } for (t, row) in node.rows() { @@ -355,8 +349,11 @@ impl<'graph, G: GraphView + 'graph> GraphViewOps<'graph> for G { })?; new_storage.resize_chunks_to_num_edges(self.count_edges()); + for layer_id in layer_map.iter() { + new_storage.edges.ensure_layer(*layer_id); + } - new_storage.edges.par_iter_mut().try_for_each(|mut shard| { + new_storage.edges.par_iter_mut().try_for_each(|shard| { for (eid, edge) in self.edges().iter().enumerate() { let src = node_map[edge.edge.src().index()]; let dst = node_map[edge.edge.dst().index()]; @@ -421,7 +418,7 @@ impl<'graph, G: GraphView + 'graph> GraphViewOps<'graph> for G { Ok::<(), MutationError>(()) })?; - new_storage.nodes.par_iter_mut().try_for_each(|mut shard| { + new_storage.nodes.par_iter_mut().try_for_each(|shard| { for (eid, edge) in self.edges().iter().enumerate() { let eid = EID(eid); for e in edge.explode() { @@ -431,13 +428,9 @@ impl<'graph, G: GraphView + 'graph> GraphViewOps<'graph> for G { let t = e.time_and_index().expect("exploded edge should have time"); let l = layer_map[e.edge.layer().unwrap()]; - writer.add_outbound_edge( - t, - node_pos, - node_map[edge.edge.dst().index()], - eid.with_layer(l), - 0, - ); + let dst = node_map[edge.edge.dst().index()]; + writer.add_static_outbound_edge(node_pos, dst, eid, 0); + writer.add_outbound_edge(t, node_pos, dst, eid.with_layer(l), 0); } if let Some(node_pos) = shard.resolve_pos(node_map[edge.edge.dst().index()]) { @@ -445,13 +438,9 @@ impl<'graph, G: GraphView + 'graph> GraphViewOps<'graph> for G { let t = e.time_and_index().expect("exploded edge should have time"); let l = layer_map[e.edge.layer().unwrap()]; - writer.add_inbound_edge( - t, - node_pos, - node_map[edge.edge.src().index()], - eid.with_layer(l), - 0, - ); + let src = node_map[edge.edge.src().index()]; + writer.add_static_inbound_edge(node_pos, src, eid, 0); + writer.add_inbound_edge(t, node_pos, src, eid.with_layer(l), 0); } } diff --git a/raphtory/src/db/api/view/internal/list_ops.rs b/raphtory/src/db/api/view/internal/list_ops.rs index faaeaede1b..7a68c98983 100644 --- a/raphtory/src/db/api/view/internal/list_ops.rs +++ b/raphtory/src/db/api/view/internal/list_ops.rs @@ -91,6 +91,10 @@ impl + From + Send + Sync> List { List::List { elems } => elems.len(), } } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } } impl + From + Send + Sync + 'static> IntoIterator diff --git a/raphtory/src/db/graph/graph.rs b/raphtory/src/db/graph/graph.rs index 6d596e9a91..7c581ed591 100644 --- a/raphtory/src/db/graph/graph.rs +++ b/raphtory/src/db/graph/graph.rs @@ -1073,7 +1073,7 @@ mod db_tests { assert_eq!(src_id.to_string(), "X"); assert_eq!(dst_id.to_string(), "Y"); } - Err(e) => panic!("Unexpected error: {:?}", e), + Err(e) => panic!("Unexpected error: {e:?}"), Ok(_) => panic!("Expected error but got Ok"), } let mut nodes = gg.nodes().name().collect_vec(); @@ -4031,6 +4031,7 @@ mod db_tests { let gw = g.after(1); let gmw = gw.materialize().unwrap(); + assert_graph_equal(&gw, &gmw); } diff --git a/raphtory/src/io/arrow/df_loaders.rs b/raphtory/src/io/arrow/df_loaders.rs index 953b65f564..ad12f336be 100644 --- a/raphtory/src/io/arrow/df_loaders.rs +++ b/raphtory/src/io/arrow/df_loaders.rs @@ -342,12 +342,7 @@ pub(crate) fn load_edges_from_df< eids_exist[row].store(true, Ordering::Relaxed); } else { let edge_id = EID(next_edge_id()); - writer.add_static_outbound_edge( - src_pos, - *dst, - edge_id.with_layer(*layer), - 0, - ); + writer.add_static_outbound_edge(src_pos, *dst, edge_id, 0); writer.add_outbound_edge( t, src_pos, From 1fdd5b6678f1ebd472af699b36fd2f91ce7742e6 Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Mon, 7 Jul 2025 17:25:18 +0100 Subject: [PATCH 067/321] more fixes for edge deletions --- db4-graph/src/lib.rs | 6 ++ db4-storage/src/api/edges.rs | 4 +- db4-storage/src/gen_ts.rs | 78 ++++++++++++++++++- db4-storage/src/lib.rs | 3 +- db4-storage/src/pages/edge_page/writer.rs | 8 +- db4-storage/src/pages/locked/edges.rs | 2 + db4-storage/src/segments/edge_entry.rs | 23 +++++- db4-storage/src/segments/node_entry.rs | 13 ++++ raphtory-core/src/storage/raw_edges.rs | 1 + .../src/graph/edges/edge_entry.rs | 6 +- .../src/graph/edges/edge_storage_ops.rs | 10 +-- raphtory/src/db/api/view/graph.rs | 3 +- .../internal/time_semantics/filtered_edge.rs | 42 +++++++--- raphtory/src/db/graph/graph.rs | 10 +++ 14 files changed, 178 insertions(+), 31 deletions(-) diff --git a/db4-graph/src/lib.rs b/db4-graph/src/lib.rs index 02293c7756..651fc841f1 100644 --- a/db4-graph/src/lib.rs +++ b/db4-graph/src/lib.rs @@ -293,6 +293,9 @@ impl<'a, EXT: PersistentStrategy, ES = ES>> WriteLockedGraph<' } pub fn resize_chunks_to_num_nodes(&mut self, num_nodes: usize) { + if num_nodes == 0 { + return; + } let (chunks_needed, _) = self.graph.storage.nodes().resolve_pos(VID(num_nodes - 1)); self.graph.storage().nodes().grow(chunks_needed + 1); std::mem::take(&mut self.nodes); @@ -300,6 +303,9 @@ impl<'a, EXT: PersistentStrategy, ES = ES>> WriteLockedGraph<' } pub fn resize_chunks_to_num_edges(&mut self, num_edges: usize) { + if num_edges == 0 { + return; + } let (chunks_needed, _) = self.graph.storage.edges().resolve_pos(EID(num_edges - 1)); self.graph.storage().edges().grow(chunks_needed + 1); std::mem::take(&mut self.edges); diff --git a/db4-storage/src/api/edges.rs b/db4-storage/src/api/edges.rs index 358756613a..5e8a494db8 100644 --- a/db4-storage/src/api/edges.rs +++ b/db4-storage/src/api/edges.rs @@ -131,7 +131,8 @@ pub trait EdgeEntryOps<'a>: Send + Sync { } pub trait EdgeRefOps<'a>: Copy + Clone + Send + Sync { - type Additions: TimeIndexOps<'a>; + type Additions: TimeIndexOps<'a, IndexType = TimeIndexEntry>; + type Deletions: TimeIndexOps<'a, IndexType = TimeIndexEntry>; type TProps: TPropOps<'a>; fn edge(self, layer_id: usize) -> Option<(VID, VID)>; @@ -143,6 +144,7 @@ pub trait EdgeRefOps<'a>: Copy + Clone + Send + Sync { fn internal_num_layers(self) -> usize; fn layer_additions(self, layer_id: usize) -> Self::Additions; + fn layer_deletions(self, layer_id: usize) -> Self::Deletions; fn c_prop(self, layer_id: usize, prop_id: usize) -> Option; diff --git a/db4-storage/src/gen_ts.rs b/db4-storage/src/gen_ts.rs index 0395ab545f..22ded91876 100644 --- a/db4-storage/src/gen_ts.rs +++ b/db4-storage/src/gen_ts.rs @@ -37,7 +37,6 @@ impl<'a> From<&'a LayerIds> for LayerIter<'a> { } } -// TODO: split the Node time operations into edge additions and property additions #[derive(Clone, Copy, Debug)] pub struct GenericTimeOps<'a, Ref> { range: Option<(TimeIndexEntry, TimeIndexEntry)>, @@ -81,6 +80,12 @@ where range: Option<(TimeIndexEntry, TimeIndexEntry)>, ) -> impl Iterator + 'a; + fn deletions_tc( + self, + layer_id: usize, + range: Option<(TimeIndexEntry, TimeIndexEntry)>, + ) -> impl Iterator + 'a; + fn num_layers(&self) -> usize; } @@ -97,6 +102,53 @@ pub trait EdgeEventOps<'a>: TimeIndexOps<'a, IndexType = TimeIndexEntry> { fn edge_events_rev(self) -> impl Iterator + Send + Sync + 'a; } +#[derive(Clone, Copy, Debug)] +pub struct DeletionCellsRef<'a, Ref: WithTimeCells<'a> + 'a> { + node: Ref, + _mark: std::marker::PhantomData<&'a ()>, +} + +impl<'a, Ref: WithTimeCells<'a> + 'a> DeletionCellsRef<'a, Ref> { + pub fn new(node: Ref) -> Self { + Self { + node, + _mark: std::marker::PhantomData, + } + } +} + +impl<'a, Ref: WithTimeCells<'a> + 'a> WithTimeCells<'a> for DeletionCellsRef<'a, Ref> { + type TimeCell = Ref::TimeCell; + + fn t_props_tc( + self, + _layer_id: usize, + _range: Option<(TimeIndexEntry, TimeIndexEntry)>, + ) -> impl Iterator + 'a { + std::iter::empty() + } + + fn additions_tc( + self, + _layer_id: usize, + _range: Option<(TimeIndexEntry, TimeIndexEntry)>, + ) -> impl Iterator + 'a { + std::iter::empty() + } + + fn deletions_tc( + self, + layer_id: usize, + range: Option<(TimeIndexEntry, TimeIndexEntry)>, + ) -> impl Iterator + 'a { + self.node.deletions_tc(layer_id, range) + } + + fn num_layers(&self) -> usize { + self.node.num_layers() + } +} + #[derive(Clone, Copy, Debug)] pub struct EdgeAdditionCellsRef<'a, Ref: WithTimeCells<'a> + 'a> { node: Ref, @@ -131,6 +183,14 @@ impl<'a, Ref: WithTimeCells<'a> + 'a> WithTimeCells<'a> for EdgeAdditionCellsRef self.node.additions_tc(layer_id, range) } + fn deletions_tc( + self, + _layer_id: usize, + _range: Option<(TimeIndexEntry, TimeIndexEntry)>, + ) -> impl Iterator + 'a { + std::iter::empty() + } + fn num_layers(&self) -> usize { self.node.num_layers() } @@ -170,6 +230,14 @@ impl<'a, Ref: WithTimeCells<'a> + 'a> WithTimeCells<'a> for PropAdditionCellsRef std::iter::empty() } + fn deletions_tc( + self, + _layer_id: usize, + _range: Option<(TimeIndexEntry, TimeIndexEntry)>, + ) -> impl Iterator + 'a { + std::iter::empty() + } + fn num_layers(&self) -> usize { self.node.num_layers() } @@ -210,9 +278,11 @@ impl<'a, Ref: WithTimeCells<'a> + 'a> GenericTimeOps<'a, Ref> { self.layer_id .into_iter(self.node.num_layers()) .flat_map(move |layer_id| { - self.node - .t_props_tc(layer_id, range) - .chain(self.node.additions_tc(layer_id, range)) + self.node.t_props_tc(layer_id, range).chain( + self.node + .additions_tc(layer_id, range) + .chain(self.node.deletions_tc(layer_id, range)), + ) }) } diff --git a/db4-storage/src/lib.rs b/db4-storage/src/lib.rs index f76c58c26b..adf256cc93 100644 --- a/db4-storage/src/lib.rs +++ b/db4-storage/src/lib.rs @@ -2,7 +2,7 @@ use std::path::Path; use crate::{ gen_t_props::GenTProps, - gen_ts::{EdgeAdditionCellsRef, GenericTimeOps, PropAdditionCellsRef}, + gen_ts::{DeletionCellsRef, EdgeAdditionCellsRef, GenericTimeOps, PropAdditionCellsRef}, pages::{ GraphStore, ReadLockedGraphStore, edge_store::ReadLockedEdgeStorage, node_store::ReadLockedNodeStorage, @@ -48,6 +48,7 @@ pub type NodePropAdditions<'a> = GenericTimeOps<'a, PropAdditionCellsRef<'a, Mem pub type NodeEdgeAdditions<'a> = GenericTimeOps<'a, EdgeAdditionCellsRef<'a, MemNodeRef<'a>>>; pub type EdgeAdditions<'a> = GenericTimeOps<'a, MemEdgeRef<'a>>; +pub type EdgeDeletions<'a> = GenericTimeOps<'a, DeletionCellsRef<'a, MemEdgeRef<'a>>>; pub type NodeTProps<'a> = GenTProps<'a, MemNodeRef<'a>>; pub type EdgeTProps<'a> = GenTProps<'a, MemEdgeRef<'a>>; diff --git a/db4-storage/src/pages/edge_page/writer.rs b/db4-storage/src/pages/edge_page/writer.rs index 444496063b..89830a63a9 100644 --- a/db4-storage/src/pages/edge_page/writer.rs +++ b/db4-storage/src/pages/edge_page/writer.rs @@ -10,7 +10,7 @@ use raphtory_core::storage::timeindex::AsTime; pub struct EdgeWriter<'a, MP: DerefMut, ES: EdgeSegmentOps> { pub page: &'a ES, pub writer: MP, - pub global_num_edges: &'a GraphStats, + pub graph_stats: &'a GraphStats, } impl<'a, MP: DerefMut, ES: EdgeSegmentOps> EdgeWriter<'a, MP, ES> { @@ -18,7 +18,7 @@ impl<'a, MP: DerefMut, ES: EdgeSegmentOps> EdgeWriter<' Self { page, writer, - global_num_edges, + graph_stats: global_num_edges, } } @@ -46,6 +46,7 @@ impl<'a, MP: DerefMut, ES: EdgeSegmentOps> EdgeWriter<' } let edge_pos = edge_pos.unwrap_or_else(|| self.new_local_pos(layer_id)); + self.graph_stats.update_time(t.t()); self.writer .insert_edge_internal(t, edge_pos, src, dst, layer_id, props); edge_pos @@ -68,6 +69,7 @@ impl<'a, MP: DerefMut, ES: EdgeSegmentOps> EdgeWriter<' } let edge_pos = edge_pos.unwrap_or_else(|| self.new_local_pos(layer_id)); + self.graph_stats.update_time(t.t()); self.writer .delete_edge_internal(t, edge_pos, src, dst, layer_id); } @@ -98,7 +100,7 @@ impl<'a, MP: DerefMut, ES: EdgeSegmentOps> EdgeWriter<' } fn increment_layer_num_edges(&self, layer_id: usize) { - self.global_num_edges.increment(layer_id); + self.graph_stats.increment(layer_id); } pub fn contains_edge(&self, pos: LocalPOS, layer_id: usize) -> bool { diff --git a/db4-storage/src/pages/locked/edges.rs b/db4-storage/src/pages/locked/edges.rs index 7926f94cbe..e941c0e944 100644 --- a/db4-storage/src/pages/locked/edges.rs +++ b/db4-storage/src/pages/locked/edges.rs @@ -10,6 +10,7 @@ use parking_lot::RwLockWriteGuard; use raphtory_core::entities::EID; use rayon::prelude::*; +#[derive(Debug)] pub struct LockedEdgePage<'a, ES> { page_id: usize, max_page_len: usize, @@ -59,6 +60,7 @@ impl<'a, EXT, ES: EdgeSegmentOps> LockedEdgePage<'a, ES> { self.lock.get_or_create_layer(layer_id); } } +#[derive(Debug)] pub struct WriteLockedEdgePages<'a, ES> { writers: Vec>, } diff --git a/db4-storage/src/segments/edge_entry.rs b/db4-storage/src/segments/edge_entry.rs index f65f02ca39..9b25a6cbdc 100644 --- a/db4-storage/src/segments/edge_entry.rs +++ b/db4-storage/src/segments/edge_entry.rs @@ -5,10 +5,10 @@ use raphtory_core::{ }; use crate::{ - EdgeAdditions, EdgeTProps, LocalPOS, + EdgeAdditions, EdgeDeletions, EdgeTProps, LocalPOS, api::edges::{EdgeEntryOps, EdgeRefOps}, gen_t_props::WithTProps, - gen_ts::WithTimeCells, + gen_ts::{DeletionCellsRef, WithTimeCells}, }; use super::{additions::MemAdditions, edge::MemEdgeSegment}; @@ -96,6 +96,19 @@ impl<'a> WithTimeCells<'a> for MemEdgeRef<'a> { std::iter::empty() } + fn deletions_tc( + self, + layer_id: usize, + range: Option<(TimeIndexEntry, TimeIndexEntry)>, + ) -> impl Iterator + 'a { + let t_cell = MemAdditions::Edges(self.es.as_ref()[layer_id].deletions(self.pos)); + std::iter::once( + range + .map(|(start, end)| t_cell.range(start..end)) + .unwrap_or_else(|| t_cell), + ) + } + fn num_layers(&self) -> usize { self.es.as_ref().len() } @@ -124,6 +137,8 @@ impl<'a> WithTProps<'a> for MemEdgeRef<'a> { impl<'a> EdgeRefOps<'a> for MemEdgeRef<'a> { type Additions = EdgeAdditions<'a>; + type Deletions = EdgeDeletions<'a>; + type TProps = EdgeTProps<'a>; fn edge(self, layer_id: usize) -> Option<(VID, VID)> { @@ -138,6 +153,10 @@ impl<'a> EdgeRefOps<'a> for MemEdgeRef<'a> { EdgeAdditions::new_with_layer(self, layer_id) } + fn layer_deletions(self, layer_id: usize) -> Self::Deletions { + EdgeDeletions::new_with_layer(DeletionCellsRef::new(self), layer_id) + } + fn c_prop(self, layer_id: usize, prop_id: usize) -> Option { self.es.as_ref()[layer_id].c_prop(self.pos, prop_id) } diff --git a/db4-storage/src/segments/node_entry.rs b/db4-storage/src/segments/node_entry.rs index 92163292eb..915d41a42b 100644 --- a/db4-storage/src/segments/node_entry.rs +++ b/db4-storage/src/segments/node_entry.rs @@ -96,6 +96,19 @@ impl<'a> WithTimeCells<'a> for MemNodeRef<'a> { ) } + fn deletions_tc( + self, + layer_id: usize, + range: Option<(TimeIndexEntry, TimeIndexEntry)>, + ) -> impl Iterator + 'a { + let deletions = MemAdditions::Edges(self.ns.as_ref()[layer_id].deletions(self.pos)); + std::iter::once( + range + .map(|(start, end)| deletions.range(start..end)) + .unwrap_or_else(|| deletions), + ) + } + fn num_layers(&self) -> usize { self.ns.as_ref().len() } diff --git a/raphtory-core/src/storage/raw_edges.rs b/raphtory-core/src/storage/raw_edges.rs index 32e8aaeefd..00b3da1917 100644 --- a/raphtory-core/src/storage/raw_edges.rs +++ b/raphtory-core/src/storage/raw_edges.rs @@ -378,6 +378,7 @@ where } } +#[derive(Debug)] pub struct WriteLockedEdges<'a> { shards: Vec>, global_len: &'a AtomicUsize, diff --git a/raphtory-storage/src/graph/edges/edge_entry.rs b/raphtory-storage/src/graph/edges/edge_entry.rs index be9f6c8f80..a3c5ab0219 100644 --- a/raphtory-storage/src/graph/edges/edge_entry.rs +++ b/raphtory-storage/src/graph/edges/edge_entry.rs @@ -63,7 +63,7 @@ impl<'a, 'b: 'a> EdgeStorageOps<'a> for &'a EdgeStorageEntry<'b> { fn deletions_iter( self, layer_ids: &'a LayerIds, - ) -> impl Iterator)> + 'a { + ) -> impl Iterator)> + 'a { self.as_ref().deletions_iter(layer_ids) } @@ -74,7 +74,7 @@ impl<'a, 'b: 'a> EdgeStorageOps<'a> for &'a EdgeStorageEntry<'b> { Item = ( usize, storage::EdgeAdditions<'a>, - storage::EdgeAdditions<'a>, + storage::EdgeDeletions<'a>, ), > + 'a { self.as_ref().updates_iter(layer_ids) @@ -84,7 +84,7 @@ impl<'a, 'b: 'a> EdgeStorageOps<'a> for &'a EdgeStorageEntry<'b> { self.as_ref().additions(layer_id) } - fn deletions(self, layer_id: usize) -> storage::EdgeAdditions<'a> { + fn deletions(self, layer_id: usize) -> storage::EdgeDeletions<'a> { self.as_ref().deletions(layer_id) } diff --git a/raphtory-storage/src/graph/edges/edge_storage_ops.rs b/raphtory-storage/src/graph/edges/edge_storage_ops.rs index 165bc1e030..3712be184f 100644 --- a/raphtory-storage/src/graph/edges/edge_storage_ops.rs +++ b/raphtory-storage/src/graph/edges/edge_storage_ops.rs @@ -136,7 +136,7 @@ pub trait EdgeStorageOps<'a>: Copy + Sized + Send + Sync + 'a { fn deletions_iter( self, layer_ids: &'a LayerIds, - ) -> impl Iterator)> + 'a { + ) -> impl Iterator)> + 'a { self.layer_ids_iter(layer_ids) .map(move |id| (id, self.deletions(id))) } @@ -148,7 +148,7 @@ pub trait EdgeStorageOps<'a>: Copy + Sized + Send + Sync + 'a { Item = ( usize, storage::EdgeAdditions<'a>, - storage::EdgeAdditions<'a>, + storage::EdgeDeletions<'a>, ), > + 'a { self.layer_ids_iter(layer_ids) @@ -157,7 +157,7 @@ pub trait EdgeStorageOps<'a>: Copy + Sized + Send + Sync + 'a { fn additions(self, layer_id: usize) -> storage::EdgeAdditions<'a>; - fn deletions(self, layer_id: usize) -> storage::EdgeAdditions<'a>; + fn deletions(self, layer_id: usize) -> storage::EdgeDeletions<'a>; fn temporal_prop_layer(self, layer_id: usize, prop_id: usize) -> impl TPropOps<'a> + 'a; @@ -236,8 +236,8 @@ impl<'a> EdgeStorageOps<'a> for storage::EdgeEntryRef<'a> { EdgeRefOps::layer_additions(self, layer_id) } - fn deletions(self, layer_id: usize) -> storage::EdgeAdditions<'a> { - EdgeRefOps::layer_additions(self, layer_id) + fn deletions(self, layer_id: usize) -> storage::EdgeDeletions<'a> { + EdgeRefOps::layer_deletions(self, layer_id) } #[inline(always)] diff --git a/raphtory/src/db/api/view/graph.rs b/raphtory/src/db/api/view/graph.rs index 68e035650e..017480a6e3 100644 --- a/raphtory/src/db/api/view/graph.rs +++ b/raphtory/src/db/api/view/graph.rs @@ -349,7 +349,8 @@ impl<'graph, G: GraphView + 'graph> GraphViewOps<'graph> for G { })?; new_storage.resize_chunks_to_num_edges(self.count_edges()); - for layer_id in layer_map.iter() { + + for layer_id in &layer_map { new_storage.edges.ensure_layer(*layer_id); } diff --git a/raphtory/src/db/api/view/internal/time_semantics/filtered_edge.rs b/raphtory/src/db/api/view/internal/time_semantics/filtered_edge.rs index 80d7c4a784..78e670d540 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/filtered_edge.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/filtered_edge.rs @@ -17,14 +17,19 @@ use rayon::iter::ParallelIterator; use std::ops::Range; #[derive(Clone)] -pub struct FilteredEdgeTimeIndex<'graph, G> { +pub struct FilteredEdgeTimeIndex<'graph, G, TS> { eid: ELID, - time_index: storage::EdgeAdditions<'graph>, + time_index: TS, view: G, + _marker: std::marker::PhantomData<&'graph ()>, } -impl<'a, 'graph: 'a, G: GraphViewOps<'graph>> TimeIndexOps<'a> - for FilteredEdgeTimeIndex<'graph, G> +impl< + 'a, + 'graph: 'a, + TS: TimeIndexOps<'a, IndexType = TimeIndexEntry, RangeType = TS>, + G: GraphViewOps<'graph>, + > TimeIndexOps<'a> for FilteredEdgeTimeIndex<'graph, G, TS> { type IndexType = TimeIndexEntry; type RangeType = Self; @@ -50,6 +55,7 @@ impl<'a, 'graph: 'a, G: GraphViewOps<'graph>> TimeIndexOps<'a> eid: self.eid, time_index: self.time_index.range(w), view: self.view.clone(), + _marker: std::marker::PhantomData, } } @@ -83,7 +89,7 @@ impl<'a, 'graph: 'a, G: GraphViewOps<'graph>> TimeIndexOps<'a> fn len(&self) -> usize { if self.view.edge_history_filtered() { - self.iter().count() + self.clone().iter().count() } else { self.time_index.len() } @@ -160,7 +166,12 @@ pub trait FilteredEdgeStorageOps<'a>: EdgeStorageOps<'a> { self, view: G, layer_ids: &'a LayerIds, - ) -> impl Iterator)> { + ) -> impl Iterator< + Item = ( + usize, + FilteredEdgeTimeIndex<'a, G, storage::EdgeAdditions<'a>>, + ), + > { let eid = self.eid(); self.additions_iter(layer_ids) .map(move |(layer_id, additions)| { @@ -170,6 +181,7 @@ pub trait FilteredEdgeStorageOps<'a>: EdgeStorageOps<'a> { eid: eid.with_layer(layer_id), time_index: additions, view: view.clone(), + _marker: std::marker::PhantomData, }, ) }) @@ -179,7 +191,12 @@ pub trait FilteredEdgeStorageOps<'a>: EdgeStorageOps<'a> { self, view: G, layer_ids: &'a LayerIds, - ) -> impl Iterator)> { + ) -> impl Iterator< + Item = ( + usize, + FilteredEdgeTimeIndex<'a, G, storage::EdgeDeletions<'a>>, + ), + > { let eid = self.eid(); self.deletions_iter(layer_ids) .map(move |(layer_id, deletions)| { @@ -189,6 +206,7 @@ pub trait FilteredEdgeStorageOps<'a>: EdgeStorageOps<'a> { eid: eid.with_layer_deletion(layer_id), time_index: deletions, view: view.clone(), + _marker: std::marker::PhantomData, }, ) }) @@ -201,8 +219,8 @@ pub trait FilteredEdgeStorageOps<'a>: EdgeStorageOps<'a> { ) -> impl Iterator< Item = ( usize, - FilteredEdgeTimeIndex<'a, G>, - FilteredEdgeTimeIndex<'a, G>, + FilteredEdgeTimeIndex<'a, G, storage::EdgeAdditions<'a>>, + FilteredEdgeTimeIndex<'a, G, storage::EdgeDeletions<'a>>, ), > + 'a { self.layer_ids_iter(layer_ids).map(move |layer_id| { @@ -218,11 +236,12 @@ pub trait FilteredEdgeStorageOps<'a>: EdgeStorageOps<'a> { self, layer_id: usize, view: G, - ) -> FilteredEdgeTimeIndex<'a, G> { + ) -> FilteredEdgeTimeIndex<'a, G, storage::EdgeAdditions<'a>> { FilteredEdgeTimeIndex { eid: self.eid().with_layer(layer_id), time_index: self.additions(layer_id), view, + _marker: std::marker::PhantomData, } } @@ -230,11 +249,12 @@ pub trait FilteredEdgeStorageOps<'a>: EdgeStorageOps<'a> { self, layer_id: usize, view: G, - ) -> FilteredEdgeTimeIndex<'a, G> { + ) -> FilteredEdgeTimeIndex<'a, G, storage::EdgeDeletions<'a>> { FilteredEdgeTimeIndex { eid: self.eid().with_layer_deletion(layer_id), time_index: self.deletions(layer_id), view, + _marker: std::marker::PhantomData, } } diff --git a/raphtory/src/db/graph/graph.rs b/raphtory/src/db/graph/graph.rs index 7c581ed591..e708338ab9 100644 --- a/raphtory/src/db/graph/graph.rs +++ b/raphtory/src/db/graph/graph.rs @@ -4045,6 +4045,16 @@ mod db_tests { }) } + #[test] + fn materialize_window_delete_test() { + let g = Graph::new(); + g.delete_edge(0, 0, 0, Some("a")).unwrap(); + let w = 0..1; + let gw = g.window(w.start, w.end); + let gmw = gw.materialize().unwrap(); + assert_graph_equal(&gw, &gmw); + } + #[test] fn test_multilayer() { let g = Graph::new(); From 82a79dbcdb103f24a7b49b24fd6bc0e504331352 Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Mon, 7 Jul 2025 18:00:30 +0100 Subject: [PATCH 068/321] split additions from deletions for edges --- db4-storage/src/gen_ts.rs | 47 ++++++++++++++++++++++++++ db4-storage/src/lib.rs | 7 ++-- db4-storage/src/segments/edge_entry.rs | 5 ++- raphtory/src/db/graph/graph.rs | 2 ++ 4 files changed, 56 insertions(+), 5 deletions(-) diff --git a/db4-storage/src/gen_ts.rs b/db4-storage/src/gen_ts.rs index 22ded91876..8d39d78def 100644 --- a/db4-storage/src/gen_ts.rs +++ b/db4-storage/src/gen_ts.rs @@ -102,6 +102,53 @@ pub trait EdgeEventOps<'a>: TimeIndexOps<'a, IndexType = TimeIndexEntry> { fn edge_events_rev(self) -> impl Iterator + Send + Sync + 'a; } +#[derive(Clone, Copy, Debug)] +pub struct AdditionCellsRef<'a, Ref: WithTimeCells<'a> + 'a> { + node: Ref, + _mark: std::marker::PhantomData<&'a ()>, +} + +impl<'a, Ref: WithTimeCells<'a> + 'a> AdditionCellsRef<'a, Ref> { + pub fn new(node: Ref) -> Self { + Self { + node, + _mark: std::marker::PhantomData, + } + } +} + +impl<'a, Ref: WithTimeCells<'a> + 'a> WithTimeCells<'a> for AdditionCellsRef<'a, Ref> { + type TimeCell = Ref::TimeCell; + + fn t_props_tc( + self, + layer_id: usize, + range: Option<(TimeIndexEntry, TimeIndexEntry)>, + ) -> impl Iterator + 'a { + self.node.t_props_tc(layer_id, range) // Assuming t_props_tc is not used for additions + } + + fn additions_tc( + self, + _layer_id: usize, + _range: Option<(TimeIndexEntry, TimeIndexEntry)>, + ) -> impl Iterator + 'a { + std::iter::empty() + } + + fn deletions_tc( + self, + _layer_id: usize, + _range: Option<(TimeIndexEntry, TimeIndexEntry)>, + ) -> impl Iterator + 'a { + std::iter::empty() + } + + fn num_layers(&self) -> usize { + self.node.num_layers() + } +} + #[derive(Clone, Copy, Debug)] pub struct DeletionCellsRef<'a, Ref: WithTimeCells<'a> + 'a> { node: Ref, diff --git a/db4-storage/src/lib.rs b/db4-storage/src/lib.rs index adf256cc93..280972c9a0 100644 --- a/db4-storage/src/lib.rs +++ b/db4-storage/src/lib.rs @@ -2,7 +2,10 @@ use std::path::Path; use crate::{ gen_t_props::GenTProps, - gen_ts::{DeletionCellsRef, EdgeAdditionCellsRef, GenericTimeOps, PropAdditionCellsRef}, + gen_ts::{ + AdditionCellsRef, DeletionCellsRef, EdgeAdditionCellsRef, GenericTimeOps, + PropAdditionCellsRef, + }, pages::{ GraphStore, ReadLockedGraphStore, edge_store::ReadLockedEdgeStorage, node_store::ReadLockedNodeStorage, @@ -47,7 +50,7 @@ pub type EdgeEntryRef<'a> = MemEdgeRef<'a>; pub type NodePropAdditions<'a> = GenericTimeOps<'a, PropAdditionCellsRef<'a, MemNodeRef<'a>>>; pub type NodeEdgeAdditions<'a> = GenericTimeOps<'a, EdgeAdditionCellsRef<'a, MemNodeRef<'a>>>; -pub type EdgeAdditions<'a> = GenericTimeOps<'a, MemEdgeRef<'a>>; +pub type EdgeAdditions<'a> = GenericTimeOps<'a, AdditionCellsRef<'a, MemEdgeRef<'a>>>; pub type EdgeDeletions<'a> = GenericTimeOps<'a, DeletionCellsRef<'a, MemEdgeRef<'a>>>; pub type NodeTProps<'a> = GenTProps<'a, MemNodeRef<'a>>; pub type EdgeTProps<'a> = GenTProps<'a, MemEdgeRef<'a>>; diff --git a/db4-storage/src/segments/edge_entry.rs b/db4-storage/src/segments/edge_entry.rs index 9b25a6cbdc..28dfdd149b 100644 --- a/db4-storage/src/segments/edge_entry.rs +++ b/db4-storage/src/segments/edge_entry.rs @@ -8,7 +8,7 @@ use crate::{ EdgeAdditions, EdgeDeletions, EdgeTProps, LocalPOS, api::edges::{EdgeEntryOps, EdgeRefOps}, gen_t_props::WithTProps, - gen_ts::{DeletionCellsRef, WithTimeCells}, + gen_ts::{AdditionCellsRef, DeletionCellsRef, WithTimeCells}, }; use super::{additions::MemAdditions, edge::MemEdgeSegment}; @@ -130,7 +130,6 @@ impl<'a> WithTProps<'a> for MemEdgeRef<'a> { self.es.as_ref()[layer_id] .t_prop(edge_pos, prop_id) .into_iter() - .map(|t_prop| t_prop.into()) } } @@ -150,7 +149,7 @@ impl<'a> EdgeRefOps<'a> for MemEdgeRef<'a> { } fn layer_additions(self, layer_id: usize) -> Self::Additions { - EdgeAdditions::new_with_layer(self, layer_id) + EdgeAdditions::new_with_layer(AdditionCellsRef::new(self), layer_id) } fn layer_deletions(self, layer_id: usize) -> Self::Deletions { diff --git a/raphtory/src/db/graph/graph.rs b/raphtory/src/db/graph/graph.rs index e708338ab9..981e41ea80 100644 --- a/raphtory/src/db/graph/graph.rs +++ b/raphtory/src/db/graph/graph.rs @@ -4049,9 +4049,11 @@ mod db_tests { fn materialize_window_delete_test() { let g = Graph::new(); g.delete_edge(0, 0, 0, Some("a")).unwrap(); + println!("{g:#?}"); let w = 0..1; let gw = g.window(w.start, w.end); let gmw = gw.materialize().unwrap(); + // println!("{gmw:#?}"); assert_graph_equal(&gw, &gmw); } From 5b9e31c0c8540820c804b8cffb2ea8e5057cc10d Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Mon, 7 Jul 2025 22:02:57 +0100 Subject: [PATCH 069/321] more fixes for materialize --- db4-storage/src/pages/node_page/writer.rs | 4 +++- raphtory/src/db/api/view/graph.rs | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/db4-storage/src/pages/node_page/writer.rs b/db4-storage/src/pages/node_page/writer.rs index 4c20bc92d1..25de6b8afc 100644 --- a/db4-storage/src/pages/node_page/writer.rs +++ b/db4-storage/src/pages/node_page/writer.rs @@ -179,7 +179,9 @@ impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> NodeWri self.update_c_props(pos, layer_id, node_info_as_props(Some(gid), None), lsn); } - pub fn update_deletion_time(&mut self, t: T, node: LocalPOS, e_id: ELID, lsn: u64) {} + pub fn update_deletion_time(&mut self, t: T, node: LocalPOS, e_id: ELID, lsn: u64) { + self.update_timestamp(t, node, e_id, lsn); + } } pub fn node_info_as_props( diff --git a/raphtory/src/db/api/view/graph.rs b/raphtory/src/db/api/view/graph.rs index 017480a6e3..a57d1564c0 100644 --- a/raphtory/src/db/api/view/graph.rs +++ b/raphtory/src/db/api/view/graph.rs @@ -302,7 +302,9 @@ impl<'graph, G: GraphView + 'graph> GraphViewOps<'graph> for G { let mut new_storage = g.write_lock()?; new_storage.resize_chunks_to_num_nodes(self.count_nodes()); - // TODO: resize the number of layers for nodes when this makes sense + for layer_id in &layer_map { + new_storage.nodes.ensure_layer(*layer_id); + } let mut node_map = vec![VID::default(); storage.unfiltered_num_nodes()]; let node_map_shared = From 0318d3960a3798879591b43e7f19f23cddf9997d Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Tue, 8 Jul 2025 11:34:28 +0100 Subject: [PATCH 070/321] test with one deletion works with materialize --- db4-storage/src/pages/node_page/writer.rs | 8 +- db4-storage/src/pages/session.rs | 102 ++++++++++++---------- raphtory/src/db/api/view/graph.rs | 65 +++++++++++--- raphtory/src/db/graph/graph.rs | 2 +- raphtory/src/io/arrow/df_loaders.rs | 4 +- 5 files changed, 118 insertions(+), 63 deletions(-) diff --git a/db4-storage/src/pages/node_page/writer.rs b/db4-storage/src/pages/node_page/writer.rs index 25de6b8afc..6a1b8becf8 100644 --- a/db4-storage/src/pages/node_page/writer.rs +++ b/db4-storage/src/pages/node_page/writer.rs @@ -27,13 +27,13 @@ impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> NodeWri pub fn add_outbound_edge( &mut self, - t: T, + t: Option, src_pos: impl Into, dst: impl Into, e_id: impl Into, lsn: u64, ) { - self.add_outbound_edge_inner(Some(t), src_pos, dst, e_id, lsn); + self.add_outbound_edge_inner(t, src_pos, dst, e_id, lsn); } pub fn add_static_outbound_edge( @@ -71,13 +71,13 @@ impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> NodeWri pub fn add_inbound_edge( &mut self, - t: T, + t: Option, dst_pos: impl Into, src: impl Into, e_id: impl Into, lsn: u64, ) { - self.add_inbound_edge_inner(Some(t), dst_pos, src, e_id, lsn); + self.add_inbound_edge_inner(t, dst_pos, src, e_id, lsn); } pub fn add_static_inbound_edge( diff --git a/db4-storage/src/pages/session.rs b/db4-storage/src/pages/session.rs index dab9b57d15..7b3f61aaa7 100644 --- a/db4-storage/src/pages/session.rs +++ b/db4-storage/src/pages/session.rs @@ -97,12 +97,20 @@ impl< .get_out_edge(src_pos, dst, edge_id.layer()) .is_none() { - self.node_writers - .get_mut_src() - .add_outbound_edge(t, src_pos, dst, edge_id, lsn); - self.node_writers - .get_mut_dst() - .add_inbound_edge(t, dst_pos, src, edge_id, lsn); + self.node_writers.get_mut_src().add_outbound_edge( + Some(t), + src_pos, + dst, + edge_id, + lsn, + ); + self.node_writers.get_mut_dst().add_inbound_edge( + Some(t), + dst_pos, + src, + edge_id, + lsn, + ); } self.node_writers @@ -157,12 +165,20 @@ impl< .get_out_edge(src_pos, dst, edge_id.layer()) .is_none() { - self.node_writers - .get_mut_src() - .add_outbound_edge(t, src_pos, dst, edge_id, lsn); - self.node_writers - .get_mut_dst() - .add_inbound_edge(t, dst_pos, src, edge_id, lsn); + self.node_writers.get_mut_src().add_outbound_edge( + Some(t), + src_pos, + dst, + edge_id, + lsn, + ); + self.node_writers.get_mut_dst().add_inbound_edge( + Some(t), + dst_pos, + src, + edge_id, + lsn, + ); } self.node_writers @@ -248,41 +264,39 @@ impl< .update_timestamp(t, dst_pos, e_id, lsn); MaybeNew::Existing(e_id) - } else { - if let Some(e_id) = self - .node_writers + } else if let Some(e_id) = self + .node_writers + .get_mut_src() + .get_out_edge(src_pos, dst, layer) + { + let mut edge_writer = self.graph.edge_writer(e_id); + let (_, edge_pos) = self.graph.edges().resolve_pos(e_id); + let e_id = e_id.with_layer(layer); + + edge_writer.add_edge(t, Some(edge_pos), src, dst, props, layer, lsn, None); + self.node_writers .get_mut_src() - .get_out_edge(src_pos, dst, layer) - { - let mut edge_writer = self.graph.edge_writer(e_id); - let (_, edge_pos) = self.graph.edges().resolve_pos(e_id); - let e_id = e_id.with_layer(layer); + .update_timestamp(t, src_pos, e_id, lsn); + self.node_writers + .get_mut_dst() + .update_timestamp(t, dst_pos, e_id, lsn); - edge_writer.add_edge(t, Some(edge_pos), src, dst, props, layer, lsn, None); - self.node_writers - .get_mut_src() - .update_timestamp(t, src_pos, e_id, lsn); - self.node_writers - .get_mut_dst() - .update_timestamp(t, dst_pos, e_id, lsn); - - MaybeNew::Existing(e_id) - } else { - let mut edge_writer = self.graph.get_free_writer(); - let edge_id = edge_writer.add_edge(t, None, src, dst, props, layer, lsn, None); - let edge_id = - edge_id.as_eid(edge_writer.segment_id(), self.graph.edges().max_page_len()); - let edge_id = edge_id.with_layer(layer); - - self.node_writers - .get_mut_src() - .add_outbound_edge(t, src_pos, dst, edge_id, lsn); - self.node_writers - .get_mut_dst() - .add_inbound_edge(t, dst_pos, src, edge_id, lsn); + MaybeNew::Existing(e_id) + } else { + let mut edge_writer = self.graph.get_free_writer(); + let edge_id = edge_writer.add_edge(t, None, src, dst, props, layer, lsn, None); + let edge_id = + edge_id.as_eid(edge_writer.segment_id(), self.graph.edges().max_page_len()); + let edge_id = edge_id.with_layer(layer); - MaybeNew::New(edge_id) - } + self.node_writers + .get_mut_src() + .add_outbound_edge(Some(t), src_pos, dst, edge_id, lsn); + self.node_writers + .get_mut_dst() + .add_inbound_edge(Some(t), dst_pos, src, edge_id, lsn); + + MaybeNew::New(edge_id) } } diff --git a/raphtory/src/db/api/view/graph.rs b/raphtory/src/db/api/view/graph.rs index a57d1564c0..b5c27ce55c 100644 --- a/raphtory/src/db/api/view/graph.rs +++ b/raphtory/src/db/api/view/graph.rs @@ -424,26 +424,57 @@ impl<'graph, G: GraphView + 'graph> GraphViewOps<'graph> for G { new_storage.nodes.par_iter_mut().try_for_each(|shard| { for (eid, edge) in self.edges().iter().enumerate() { let eid = EID(eid); + let src_id = node_map[edge.edge.src().index()]; + let dst_id = node_map[edge.edge.dst().index()]; + + if let Some(node_pos) = shard.resolve_pos(src_id) { + let mut writer = shard.writer(); + writer.add_static_outbound_edge(node_pos, dst_id, eid, 0); + } + + if let Some(node_pos) = shard.resolve_pos(dst_id) { + let mut writer = shard.writer(); + writer.add_static_inbound_edge(node_pos, src_id, eid, 0); + } + + for e in edge.explode_layers() { + let layer = layer_map[e.edge.layer().unwrap()]; + if let Some(node_pos) = shard.resolve_pos(src_id) { + let mut writer = shard.writer(); + writer.add_outbound_edge::( + None, + node_pos, + dst_id, + eid.with_layer(layer), + 0, + ); + } + if let Some(node_pos) = shard.resolve_pos(dst_id) { + let mut writer = shard.writer(); + writer.add_inbound_edge::( + None, + node_pos, + src_id, + eid.with_layer(layer), + 0, + ); + } + } + for e in edge.explode() { - if let Some(node_pos) = shard.resolve_pos(node_map[edge.edge.src().index()]) - { + if let Some(node_pos) = shard.resolve_pos(src_id) { let mut writer = shard.writer(); let t = e.time_and_index().expect("exploded edge should have time"); let l = layer_map[e.edge.layer().unwrap()]; - let dst = node_map[edge.edge.dst().index()]; - writer.add_static_outbound_edge(node_pos, dst, eid, 0); - writer.add_outbound_edge(t, node_pos, dst, eid.with_layer(l), 0); + writer.update_timestamp(t, node_pos, eid.with_layer(l), 0); } - if let Some(node_pos) = shard.resolve_pos(node_map[edge.edge.dst().index()]) - { + if let Some(node_pos) = shard.resolve_pos(dst_id) { let mut writer = shard.writer(); let t = e.time_and_index().expect("exploded edge should have time"); let l = layer_map[e.edge.layer().unwrap()]; - let src = node_map[edge.edge.src().index()]; - writer.add_static_inbound_edge(node_pos, src, eid, 0); - writer.add_inbound_edge(t, node_pos, src, eid.with_layer(l), 0); + writer.update_timestamp(t, node_pos, eid.with_layer(l), 0); } } @@ -459,11 +490,21 @@ impl<'graph, G: GraphView + 'graph> GraphViewOps<'graph> for G { if let Some(node_pos) = shard.resolve_pos(src) { let mut writer = shard.writer(); - writer.update_deletion_time(t, node_pos, eid.with_layer(layer), 0); + writer.update_deletion_time( + t, + node_pos, + eid.with_layer_deletion(layer), + 0, + ); } if let Some(node_pos) = shard.resolve_pos(dst) { let mut writer = shard.writer(); - writer.update_deletion_time(t, node_pos, eid.with_layer(layer), 0); + writer.update_deletion_time( + t, + node_pos, + eid.with_layer_deletion(layer), + 0, + ); } } } diff --git a/raphtory/src/db/graph/graph.rs b/raphtory/src/db/graph/graph.rs index 981e41ea80..1e1a237b2b 100644 --- a/raphtory/src/db/graph/graph.rs +++ b/raphtory/src/db/graph/graph.rs @@ -4049,7 +4049,7 @@ mod db_tests { fn materialize_window_delete_test() { let g = Graph::new(); g.delete_edge(0, 0, 0, Some("a")).unwrap(); - println!("{g:#?}"); + // println!("{g:#?}"); let w = 0..1; let gw = g.window(w.start, w.end); let gmw = gw.materialize().unwrap(); diff --git a/raphtory/src/io/arrow/df_loaders.rs b/raphtory/src/io/arrow/df_loaders.rs index ad12f336be..fb62adc9aa 100644 --- a/raphtory/src/io/arrow/df_loaders.rs +++ b/raphtory/src/io/arrow/df_loaders.rs @@ -344,7 +344,7 @@ pub(crate) fn load_edges_from_df< let edge_id = EID(next_edge_id()); writer.add_static_outbound_edge(src_pos, *dst, edge_id, 0); writer.add_outbound_edge( - t, + Some(t), src_pos, *dst, edge_id.with_layer(*layer), @@ -372,7 +372,7 @@ pub(crate) fn load_edges_from_df< let mut writer = shard.writer(); writer.store_node_id(dst_pos, 0, dst_gid, 0); writer.add_static_inbound_edge(dst_pos, *src, eid.with_layer(*layer), 0); - writer.add_inbound_edge(t, dst_pos, *src, eid.with_layer(*layer), 0); + writer.add_inbound_edge(Some(t), dst_pos, *src, eid.with_layer(*layer), 0); } } }); From 38f362c550640268cda5f5bf9935789f564d7d78 Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Tue, 8 Jul 2025 12:12:18 +0100 Subject: [PATCH 071/321] fixing more issues with materialize --- .../graph/storage_ops/time_semantics.rs | 25 +++++++++++++++++-- .../api/view/exploded_edge_property_filter.rs | 10 -------- raphtory/src/db/api/view/graph.rs | 1 + .../internal/time_semantics/filtered_node.rs | 4 +-- raphtory/src/db/graph/graph.rs | 11 ++++++-- 5 files changed, 35 insertions(+), 16 deletions(-) diff --git a/raphtory/src/db/api/storage/graph/storage_ops/time_semantics.rs b/raphtory/src/db/api/storage/graph/storage_ops/time_semantics.rs index f94a3bd782..bcf8ff0406 100644 --- a/raphtory/src/db/api/storage/graph/storage_ops/time_semantics.rs +++ b/raphtory/src/db/api/storage/graph/storage_ops/time_semantics.rs @@ -23,6 +23,7 @@ use raphtory_storage::{ }; use rayon::iter::ParallelIterator; use std::ops::{Deref, Range}; +use storage::gen_ts::ALL_LAYERS; impl GraphTimeSemanticsOps for GraphStorage { fn node_time_semantics(&self) -> TimeSemantics { @@ -62,14 +63,34 @@ impl GraphTimeSemanticsOps for GraphStorage { fn earliest_time_window(&self, start: i64, end: i64) -> Option { self.nodes() .par_iter() - .flat_map(|node| node.additions().range_t(start..end).first_t()) + .flat_map_iter(|node| { + node.additions() + .range_t(start..end) + .first_t() + .into_iter() + .chain( + node.node_edge_additions(ALL_LAYERS) + .range_t(start..end) + .first_t(), + ) + }) .min() } fn latest_time_window(&self, start: i64, end: i64) -> Option { self.nodes() .par_iter() - .flat_map(|node| node.additions().range_t(start..end).last_t()) + .flat_map_iter(|node| { + node.additions() + .range_t(start..end) + .last_t() + .into_iter() + .chain( + node.node_edge_additions(ALL_LAYERS) + .range_t(start..end) + .last_t(), + ) + }) .max() } diff --git a/raphtory/src/db/api/view/exploded_edge_property_filter.rs b/raphtory/src/db/api/view/exploded_edge_property_filter.rs index 5e5cdffac6..325cf8e4fa 100644 --- a/raphtory/src/db/api/view/exploded_edge_property_filter.rs +++ b/raphtory/src/db/api/view/exploded_edge_property_filter.rs @@ -461,17 +461,7 @@ mod test { .filter_exploded_edges(PropertyFilterBuilder("int_prop".to_string()).gt(1i64)) .unwrap() .window(-1, 1); - println!( - "earliest: {:?}, latest: {:?}", - gfw.earliest_time(), - gfw.latest_time() - ); let gfwm = gfw.materialize().unwrap(); - println!( - "earliest: {:?}, latest: {:?}", - gfwm.earliest_time(), - gfwm.latest_time() - ); assert!(gfw.node(0).is_none()); assert!(gfwm.node(0).is_none()); assert_eq!(gfw.earliest_time(), None); diff --git a/raphtory/src/db/api/view/graph.rs b/raphtory/src/db/api/view/graph.rs index b5c27ce55c..ff18a2fab1 100644 --- a/raphtory/src/db/api/view/graph.rs +++ b/raphtory/src/db/api/view/graph.rs @@ -301,6 +301,7 @@ impl<'graph, G: GraphView + 'graph> GraphViewOps<'graph> for G { // scope for the write lock let mut new_storage = g.write_lock()?; + println!("NODE COUNT {}", self.count_nodes()); new_storage.resize_chunks_to_num_nodes(self.count_nodes()); for layer_id in &layer_map { new_storage.nodes.ensure_layer(*layer_id); diff --git a/raphtory/src/db/api/view/internal/time_semantics/filtered_node.rs b/raphtory/src/db/api/view/internal/time_semantics/filtered_node.rs index 287cae5cbd..095dabea8a 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/filtered_node.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/filtered_node.rs @@ -36,14 +36,14 @@ pub struct NodePropHistory<'a, G> { impl<'a, G: Clone> NodeHistory<'a, G> { pub fn edge_history(&self) -> NodeEdgeHistory<'a, G> { NodeEdgeHistory { - additions: self.edge_history.clone(), + additions: self.edge_history, view: self.view.clone(), } } pub fn prop_history(&self) -> NodePropHistory<'a, G> { NodePropHistory { - additions: self.additions.clone(), + additions: self.additions, view: self.view.clone(), } } diff --git a/raphtory/src/db/graph/graph.rs b/raphtory/src/db/graph/graph.rs index 1e1a237b2b..fa35128caa 100644 --- a/raphtory/src/db/graph/graph.rs +++ b/raphtory/src/db/graph/graph.rs @@ -4045,15 +4045,22 @@ mod db_tests { }) } + #[test] + fn materialize_one_edge_test() { + let g = Graph::new(); + g.add_edge(0, 0, 0, NO_PROPS, None).unwrap(); + let gw = g.window(-3, 9); + let gmw = gw.materialize().unwrap(); + assert_graph_equal(&gw, &gmw); + } + #[test] fn materialize_window_delete_test() { let g = Graph::new(); g.delete_edge(0, 0, 0, Some("a")).unwrap(); - // println!("{g:#?}"); let w = 0..1; let gw = g.window(w.start, w.end); let gmw = gw.materialize().unwrap(); - // println!("{gmw:#?}"); assert_graph_equal(&gw, &gmw); } From a2dc6a691d3562d580bf8e698cc6af4df9a14e27 Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Tue, 8 Jul 2025 13:17:26 +0100 Subject: [PATCH 072/321] fixing issues with edge const props --- .../src/mutation/addition_ops_ext.rs | 3 ++- raphtory/src/db/api/view/graph.rs | 8 +++++--- .../internal/time_semantics/event_semantics.rs | 4 ++-- raphtory/src/db/graph/graph.rs | 17 ++++++++++++++++- 4 files changed, 25 insertions(+), 7 deletions(-) diff --git a/raphtory-storage/src/mutation/addition_ops_ext.rs b/raphtory-storage/src/mutation/addition_ops_ext.rs index ab7f9bd434..24f0f7f186 100644 --- a/raphtory-storage/src/mutation/addition_ops_ext.rs +++ b/raphtory-storage/src/mutation/addition_ops_ext.rs @@ -16,6 +16,7 @@ use raphtory_core::{ storage::{raw_edges::WriteLockedEdges, timeindex::TimeIndexEntry, WriteLockedNodes}, }; use storage::{ + error::DBV4Error, pages::{ node_page::writer::{node_info_as_props, NodeWriter}, session::WriteSession, @@ -120,7 +121,7 @@ impl<'a> SessionAdditionOps for UnlockedSession<'a> { } fn set_node(&self, gid: GidRef, vid: VID) -> Result<(), Self::Error> { - todo!() + Ok(self.graph.logical_to_physical.set(gid, vid)?) } fn resolve_graph_property( diff --git a/raphtory/src/db/api/view/graph.rs b/raphtory/src/db/api/view/graph.rs index ff18a2fab1..6bb6982035 100644 --- a/raphtory/src/db/api/view/graph.rs +++ b/raphtory/src/db/api/view/graph.rs @@ -49,7 +49,10 @@ use raphtory_storage::{ edges::edge_storage_ops::EdgeStorageOps, graph::GraphStorage, nodes::node_storage_ops::NodeStorageOps, }, - mutation::{addition_ops::InternalAdditionOps, MutationError}, + mutation::{ + addition_ops::{InternalAdditionOps, SessionAdditionOps}, + MutationError, + }, }; use rayon::prelude::*; use rustc_hash::FxHashSet; @@ -300,8 +303,6 @@ impl<'graph, G: GraphView + 'graph> GraphViewOps<'graph> for G { { // scope for the write lock let mut new_storage = g.write_lock()?; - - println!("NODE COUNT {}", self.count_nodes()); new_storage.resize_chunks_to_num_nodes(self.count_nodes()); for layer_id in &layer_map { new_storage.nodes.ensure_layer(*layer_id); @@ -334,6 +335,7 @@ impl<'graph, G: GraphView + 'graph> GraphViewOps<'graph> for G { } else { writer.store_node_id(node_pos, 0, gid.as_ref(), 0); } + g.write_session()?.set_node(gid.as_ref(), new_id)?; for (t, row) in node.rows() { writer.add_props(t, node_pos, 0, row, 0); diff --git a/raphtory/src/db/api/view/internal/time_semantics/event_semantics.rs b/raphtory/src/db/api/view/internal/time_semantics/event_semantics.rs index 925d36e0e0..f895290c35 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/event_semantics.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/event_semantics.rs @@ -765,8 +765,8 @@ impl EdgeTimeSemanticsOps for EventSemantics { LayerIds::All => match view.unfiltered_num_layers() { 0 => return None, 1 => { - return if layer_filter(0) { - e.constant_prop_layer(0, prop_id) + return if layer_filter(1) { + e.constant_prop_layer(1, prop_id) } else { None } diff --git a/raphtory/src/db/graph/graph.rs b/raphtory/src/db/graph/graph.rs index fa35128caa..460505427b 100644 --- a/raphtory/src/db/graph/graph.rs +++ b/raphtory/src/db/graph/graph.rs @@ -4048,9 +4048,24 @@ mod db_tests { #[test] fn materialize_one_edge_test() { let g = Graph::new(); - g.add_edge(0, 0, 0, NO_PROPS, None).unwrap(); + let e = g.add_edge(0, 0, 0, NO_PROPS, Some("a")).unwrap(); + e.add_constant_properties([("0", Prop::I64(1))], Some("a")) + .unwrap(); + g.delete_edge(1, 0, 0, Some("a")).unwrap(); let gw = g.window(-3, 9); + let c_props = gw.edge(0, 0).unwrap().properties().constant().as_map(); + println!("window c_props: {c_props:?}"); let gmw = gw.materialize().unwrap(); + assert!(gmw.edge(0, 0).is_some()); + let c_props = gmw.edge(0, 0).unwrap().properties().constant().as_map(); + println!("materialized c_props: {c_props:?}"); + let edge = gw.edge(0, 0).unwrap(); + let c_props = edge + .const_prop_ids() + .filter_map(|prop_id| edge.get_const_prop(prop_id).map(|prop| (prop_id, prop))) + .collect::>(); + + println!("window c_props all {c_props:?}"); assert_graph_equal(&gw, &gmw); } From 044b9b2e949eb0ab0cec6ab743037dfed6d87045 Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Wed, 9 Jul 2025 10:06:38 +0100 Subject: [PATCH 073/321] fixed issues with materialize but ran into strange memory mapping problem --- db4-storage/src/api/nodes.rs | 29 +- db4-storage/src/pages/locked/nodes.rs | 1 + examples/python/enron/nx.html | 367 ++++++++++++------ .../src/mutation/addition_ops_ext.rs | 2 +- raphtory/src/db/api/view/graph.rs | 43 +- raphtory/src/db/graph/graph.rs | 118 +++++- 6 files changed, 388 insertions(+), 172 deletions(-) diff --git a/db4-storage/src/api/nodes.rs b/db4-storage/src/api/nodes.rs index 159a7e3b33..b9b7b5e143 100644 --- a/db4-storage/src/api/nodes.rs +++ b/db4-storage/src/api/nodes.rs @@ -27,7 +27,7 @@ use raphtory_core::{ use crate::{ LocalPOS, error::DBV4Error, - gen_ts::LayerIter, + gen_ts::{ALL_LAYERS, LayerIter}, segments::node::MemNodeSegment, utils::{Iter2, Iter3, Iter4}, }; @@ -228,23 +228,24 @@ pub trait NodeRefOps<'a>: Copy + Clone + Send + Sync + 'a { ) -> impl Iterator)> + 'a { (0..self.internal_num_layers()).flat_map(move |layer_id| { let w = w.clone(); + let additions = self.node_additions(layer_id); + let additions = w + .clone() + .map(|w| Iter2::I1(additions.range(w).iter())) + .unwrap_or_else(|| Iter2::I2(additions.iter())); + let mut time_ordered_iter = (0..self.node_meta().temporal_prop_meta().len()) .map(move |prop_id| { - let additions = self.node_additions(layer_id); - let additions = w - .clone() - .map(|w| Iter2::I1(additions.range(w).iter())) - .unwrap_or_else(|| Iter2::I2(additions.iter())); - self.temporal_prop_layer(layer_id, prop_id) .iter_inner(w.clone()) - .merge_join_by(additions, |(t1, _), t2| t1 <= t2) - .map(move |result| match result { - either::Either::Left((l, prop)) => (l, Some((prop_id, prop))), - either::Either::Right(r) => (r, None), - }) + .map(move |(t, prop)| (t, (prop_id, prop))) }) - .kmerge_by(|(t1, _), (t2, _)| t1 <= t2); + .kmerge_by(|(t1, (p_id1, _)), (t2, (p_id2, _))| (t1, p_id1) < (t2, p_id2)) + .merge_join_by(additions, |(t1, _), t2| t1 <= t2) + .map(move |result| match result { + either::Either::Left((l, (prop_id, prop))) => (l, Some((prop_id, prop))), + either::Either::Right(r) => (r, None), + }); let mut done = false; if let Some((mut current_time, maybe_prop)) = time_ordered_iter.next() { @@ -253,7 +254,7 @@ pub trait NodeRefOps<'a>: Copy + Clone + Send + Sync + 'a { if done { return None; } - while let Some((t, maybe_prop)) = time_ordered_iter.next() { + for (t, maybe_prop) in time_ordered_iter.by_ref() { if t == current_time { current_row.extend(maybe_prop); } else { diff --git a/db4-storage/src/pages/locked/nodes.rs b/db4-storage/src/pages/locked/nodes.rs index a19ec908cd..3c086ecc59 100644 --- a/db4-storage/src/pages/locked/nodes.rs +++ b/db4-storage/src/pages/locked/nodes.rs @@ -56,6 +56,7 @@ impl<'a, EXT, NS: NodeSegmentOps> LockedNodePage<'a, NS> { pub fn ensure_layer(&mut self, layer_id: usize) { self.lock.get_or_create_layer(layer_id); + self.layer_counter.get(layer_id); } } pub struct WriteLockedNodePages<'a, NS> { diff --git a/examples/python/enron/nx.html b/examples/python/enron/nx.html index d4d35188ac..8ef2dbb6cb 100644 --- a/examples/python/enron/nx.html +++ b/examples/python/enron/nx.html @@ -1,155 +1,272 @@ - - - - - - - -

-

-
+ - + -
-

+

- -
- -
- - - - \ No newline at end of file + diff --git a/raphtory-storage/src/mutation/addition_ops_ext.rs b/raphtory-storage/src/mutation/addition_ops_ext.rs index 24f0f7f186..b3d256bf96 100644 --- a/raphtory-storage/src/mutation/addition_ops_ext.rs +++ b/raphtory-storage/src/mutation/addition_ops_ext.rs @@ -297,7 +297,7 @@ impl InternalAdditionOps for TemporalGraph { let (segment, node_pos) = self.storage().nodes().resolve_pos(v); let mut node_writer = self.storage().node_writer(segment); let node_info = node_info_as_props(gid, node_type); - node_writer.add_props(t, node_pos, 0, props.into_iter(), 0); + node_writer.add_props(t, node_pos, 0, props, 0); node_writer.update_c_props(node_pos, 0, node_info, 0); Ok(()) diff --git a/raphtory/src/db/api/view/graph.rs b/raphtory/src/db/api/view/graph.rs index 6bb6982035..d587e1b1dc 100644 --- a/raphtory/src/db/api/view/graph.rs +++ b/raphtory/src/db/api/view/graph.rs @@ -31,6 +31,7 @@ use crate::{ use ahash::HashSet; use chrono::{DateTime, Utc}; use db4_graph::TemporalGraph; +use itertools::Itertools; use raphtory_api::{ atomic_extra::atomic_usize_from_mut_slice, core::{ @@ -384,20 +385,34 @@ impl<'graph, G: GraphView + 'graph> GraphViewOps<'graph> for G { Some(true), ); } - for t_prop in edge.properties().temporal().values() { - let prop_id = t_prop.id(); - for (t, prop_value) in t_prop.iter_indexed() { - writer.add_edge( - t, - Some(edge_pos), - src, - dst, - [(prop_id, prop_value)], - layer, - 0, - Some(true), - ); - } + //TODO: move this in edge.row() + for (t, t_props) in edge + .properties() + .temporal() + .values() + .map(|tp| { + let prop_id = tp.id(); + tp.iter_indexed() + .map(|(t, prop)| (t, prop_id, prop)) + .collect::>() + }) + .kmerge_by(|(t, _, _), (t2, _, _)| t <= t2) + .chunk_by(|(t, _, _)| *t) + .into_iter() + { + let props = t_props + .map(|(_, prop_id, prop)| (prop_id, prop)) + .collect::>(); + writer.add_edge( + t, + Some(edge_pos), + src, + dst, + props, + layer, + 0, + Some(true), + ); } writer.update_c_props( edge_pos, diff --git a/raphtory/src/db/graph/graph.rs b/raphtory/src/db/graph/graph.rs index 460505427b..55c07e5985 100644 --- a/raphtory/src/db/graph/graph.rs +++ b/raphtory/src/db/graph/graph.rs @@ -587,7 +587,10 @@ mod db_tests { graphgen::random_attachment::random_attachment, prelude::{AdditionOps, PropertyAdditionOps}, test_storage, - test_utils::{build_graph, build_graph_strat, test_graph}, + test_utils::{ + build_graph, build_graph_strat, test_graph, EdgeFixture, EdgeUpdatesFixture, + GraphFixture, NodeFixture, PropUpdatesFixture, + }, }; use bigdecimal::BigDecimal; use chrono::NaiveDateTime; @@ -4046,29 +4049,108 @@ mod db_tests { } #[test] - fn materialize_one_edge_test() { + fn materialize_temporal_properties_one_edge() { let g = Graph::new(); - let e = g.add_edge(0, 0, 0, NO_PROPS, Some("a")).unwrap(); - e.add_constant_properties([("0", Prop::I64(1))], Some("a")) - .unwrap(); - g.delete_edge(1, 0, 0, Some("a")).unwrap(); - let gw = g.window(-3, 9); - let c_props = gw.edge(0, 0).unwrap().properties().constant().as_map(); - println!("window c_props: {c_props:?}"); + g.add_edge( + 0, + 0, + 0, + [("3", Prop::I64(1)), ("0", Prop::str("baa"))], + Some("a"), + ) + .unwrap(); + + let gw = g.window(-9, 3); + let gmw = gw.materialize().unwrap(); + + assert_graph_equal(&gw, &gmw); + } + + #[test] + fn materialize_one_node() { + let g = Graph::new(); + g.add_node(0, 0, NO_PROPS, None).unwrap(); + + let n = g.node(0).unwrap(); + let hist = n.history(); + assert!(!hist.is_empty()); + let rows = n.rows().collect::>(); + assert!(!rows.is_empty()); + + let gw = g.window(0, 1); let gmw = gw.materialize().unwrap(); - assert!(gmw.edge(0, 0).is_some()); - let c_props = gmw.edge(0, 0).unwrap().properties().constant().as_map(); - println!("materialized c_props: {c_props:?}"); - let edge = gw.edge(0, 0).unwrap(); - let c_props = edge - .const_prop_ids() - .filter_map(|prop_id| edge.get_const_prop(prop_id).map(|prop| (prop_id, prop))) - .collect::>(); - println!("window c_props all {c_props:?}"); assert_graph_equal(&gw, &gmw); } + #[test] + fn materialize_some_edges() -> Result<(), GraphError> { + let edges1_props = EdgeUpdatesFixture { + props: PropUpdatesFixture { + t_props: vec![ + (2433054617899119663, vec![]), + ( + 5623371002478468619, + vec![("0".to_owned(), Prop::I64(-180204069376666762))], + ), + ], + c_props: vec![], + }, + deletions: vec![-3684372592923241629, 3668280323305195349], + }; + + let edges2_props = EdgeUpdatesFixture { + props: PropUpdatesFixture { + t_props: vec![ + ( + -7888823724540213280, + vec![("0".to_owned(), Prop::I64(1339447446033500001))], + ), + (-3792330935693192039, vec![]), + ( + 4049942931077033460, + vec![("0".to_owned(), Prop::I64(-544773539725842277))], + ), + (5085404190610173488, vec![]), + (1445770503123270290, vec![]), + (-5628624083683143619, vec![]), + (-394401628579820652, vec![]), + (-2398199704888544233, vec![]), + ], + c_props: vec![("0".to_owned(), Prop::I64(-1877019573933389749))], + }, + deletions: vec![ + 3969804007878301015, + 7040207277685112004, + 7380699292468575143, + 3332576590029503186, + -1107894292705275349, + 6647229517972286485, + 6359226207899406831, + ], + }; + + let edges: EdgeFixture = [ + ((2, 7, Some("b")), edges1_props), + ((7, 2, Some("a")), edges2_props), + ] + .into_iter() + .collect(); + + let w = -3619743214445905380..90323088878877991; + let graph_f = GraphFixture { + nodes: NodeFixture::default(), + edges, + }; + for _ in 0..200 { + let g = Graph::from(build_graph(&graph_f)); + let gw = g.window(w.start, w.end); + let gmw = gw.materialize()?; + assert_graph_equal(&gw, &gmw); + } + Ok(()) + } + #[test] fn materialize_window_delete_test() { let g = Graph::new(); From 24bdae1ce1c575c8ab424b49d39922acbf5d7e64 Mon Sep 17 00:00:00 2001 From: Fadhil Abubaker Date: Wed, 9 Jul 2025 10:44:39 -0400 Subject: [PATCH 074/321] Add WALOps, WalEntry and NoWAL (#2164) * Add WALOps and NoWAL * Add wait_for_sync * Rename WalRow -> WalRecord * Add derive(Debug) * Rename recover -> replay * Add WalEntry * Use usize instead of u64 for props * Use Cow for props in WalEntry * Convert all u64 to usize * Modify AddPropID entries to take Cow * Remove generic node/edge segment from WriteSession * Attach new edge_writer to WriteSession * Attach new edge_writer to WriteSession * Use self.edges() in get_free_writer * Add reserve method Wal to reserve LSNs * Add Checkpoint wal entry * Add rotate to WalOps --- db4-storage/Cargo.toml | 1 + db4-storage/src/lib.rs | 1 + db4-storage/src/pages/mod.rs | 13 +- db4-storage/src/pages/session.rs | 27 ++-- db4-storage/src/wal/entries.rs | 123 ++++++++++++++++++ db4-storage/src/wal/mod.rs | 46 +++++++ db4-storage/src/wal/no_wal.rs | 37 ++++++ raphtory-api/src/core/entities/mod.rs | 1 + raphtory-storage/src/mutation/addition_ops.rs | 7 +- .../src/mutation/addition_ops_ext.rs | 20 +-- raphtory/src/db/api/storage/storage.rs | 7 +- 11 files changed, 236 insertions(+), 47 deletions(-) create mode 100644 db4-storage/src/wal/entries.rs create mode 100644 db4-storage/src/wal/mod.rs create mode 100644 db4-storage/src/wal/no_wal.rs diff --git a/db4-storage/Cargo.toml b/db4-storage/Cargo.toml index b40190e01d..d5649062da 100644 --- a/db4-storage/Cargo.toml +++ b/db4-storage/Cargo.toml @@ -33,6 +33,7 @@ bytemuck.workspace = true rayon.workspace = true itertools.workspace = true thiserror.workspace = true +postcard.workspace = true proptest = {workspace = true, optional = true} tempfile = {workspace = true, optional = true} diff --git a/db4-storage/src/lib.rs b/db4-storage/src/lib.rs index 280972c9a0..9651300f18 100644 --- a/db4-storage/src/lib.rs +++ b/db4-storage/src/lib.rs @@ -29,6 +29,7 @@ pub mod persist; pub mod properties; pub mod resolver; pub mod segments; +pub mod wal; pub mod utils; pub type Extension = (); diff --git a/db4-storage/src/pages/mod.rs b/db4-storage/src/pages/mod.rs index fa0d612922..e0751af5c3 100644 --- a/db4-storage/src/pages/mod.rs +++ b/db4-storage/src/pages/mod.rs @@ -335,14 +335,7 @@ impl< src: VID, dst: VID, e_id: Option, - ) -> WriteSession< - '_, - RwLockWriteGuard, - RwLockWriteGuard, - NS, - ES, - EXT, - > { + ) -> WriteSession<'_, NS, ES, EXT> { let (src_chunk, _) = self.nodes.resolve_pos(src); let (dst_chunk, _) = self.nodes.resolve_pos(dst); @@ -386,8 +379,8 @@ impl< self.edges().get_writer(eid) } - pub fn get_free_writer(&self) -> EdgeWriter, ES> { - self.edges.get_free_writer() + pub fn get_free_writer(&self) -> EdgeWriter, ES> { + self.edges().get_free_writer() } } diff --git a/db4-storage/src/pages/session.rs b/db4-storage/src/pages/session.rs index 7b3f61aaa7..0fb207cdd8 100644 --- a/db4-storage/src/pages/session.rs +++ b/db4-storage/src/pages/session.rs @@ -10,6 +10,7 @@ use crate::{ pages::{NODE_ID_PROP_KEY, NODE_TYPE_PROP_KEY}, segments::{edge::MemEdgeSegment, node::MemNodeSegment}, }; +use parking_lot::RwLockWriteGuard; use raphtory_api::core::{ entities::properties::prop::{Prop, PropType}, storage::dict_mapper::MaybeNew, @@ -21,29 +22,25 @@ use raphtory_core::{ pub struct WriteSession< 'a, - MNS: DerefMut + 'a, - MES: DerefMut + 'a, NS: NodeSegmentOps, ES: EdgeSegmentOps, EXT, > { - node_writers: WriterPair<'a, MNS, NS>, - edge_writer: Option>, + node_writers: WriterPair<'a, RwLockWriteGuard<'a, MemNodeSegment>, NS>, + edge_writer: Option, ES>>, graph: &'a GraphStore, } impl< 'a, - MNS: DerefMut + 'a, - MES: DerefMut + 'a, NS: NodeSegmentOps, ES: EdgeSegmentOps, EXT: Clone + Default + Send + Sync, -> WriteSession<'a, MNS, MES, NS, ES, EXT> +> WriteSession<'a, NS, ES, EXT> { pub fn new( - node_writers: WriterPair<'a, MNS, NS>, - edge_writer: Option>, + node_writers: WriterPair<'a, RwLockWriteGuard<'a, MemNodeSegment>, NS>, + edge_writer: Option, ES>>, graph: &'a GraphStore, ) -> Self { Self { @@ -85,6 +82,7 @@ impl< let exists = Some(!edge.is_new()); writer.add_edge(t, Some(edge_pos), src, dst, props, layer, lsn, exists); + self.edge_writer = Some(writer); // Attach edge_writer to hold onto locks } let edge_id = edge.inner(); @@ -153,6 +151,7 @@ impl< let exists = Some(!edge.is_new()); writer.delete_edge(t, Some(edge_pos), src, dst, layer, lsn, exists); + self.edge_writer = Some(writer); // Attach edge_writer to hold onto locks } let edge_id = edge.inner(); @@ -213,6 +212,8 @@ impl< edge_writer.add_static_edge(Some(edge_pos), src, dst, layer_id, lsn, None); + self.edge_writer = Some(edge_writer); // Attach edge_writer to hold onto locks + MaybeNew::Existing(e_id) } else { let mut edge_writer = self.graph.get_free_writer(); @@ -220,6 +221,8 @@ impl< let edge_id = edge_id.as_eid(edge_writer.segment_id(), self.graph.edges().max_page_len()); + self.edge_writer = Some(edge_writer); // Attach edge_writer to hold onto locks + self.node_writers .get_mut_src() .add_static_outbound_edge(src_pos, dst, edge_id, lsn); @@ -263,6 +266,8 @@ impl< .get_mut_dst() .update_timestamp(t, dst_pos, e_id, lsn); + self.edge_writer = Some(edge_writer); // Attach edge_writer to hold onto locks + MaybeNew::Existing(e_id) } else if let Some(e_id) = self .node_writers @@ -281,6 +286,8 @@ impl< .get_mut_dst() .update_timestamp(t, dst_pos, e_id, lsn); + self.edge_writer = Some(edge_writer); // Attach edge_writer to hold onto locks + MaybeNew::Existing(e_id) } else { let mut edge_writer = self.graph.get_free_writer(); @@ -289,6 +296,8 @@ impl< edge_id.as_eid(edge_writer.segment_id(), self.graph.edges().max_page_len()); let edge_id = edge_id.with_layer(layer); + self.edge_writer = Some(edge_writer); // Attach edge_writer to hold onto locks + self.node_writers .get_mut_src() .add_outbound_edge(Some(t), src_pos, dst, edge_id, lsn); diff --git a/db4-storage/src/wal/entries.rs b/db4-storage/src/wal/entries.rs new file mode 100644 index 0000000000..4ca881f8a3 --- /dev/null +++ b/db4-storage/src/wal/entries.rs @@ -0,0 +1,123 @@ +use raphtory_core::{ + entities::{VID, EID, GID}, + storage::timeindex::TimeIndexEntry, +}; +use raphtory_api::core::entities::properties::prop::Prop; +use serde::{Serialize, Deserialize}; +use std::borrow::Cow; + +use crate::wal::LSN; + +#[derive(Debug, Serialize, Deserialize)] +pub enum WalEntry<'a> { + AddEdge(AddEdge<'a>), + AddNodeID(AddNodeID), + AddConstPropIDs(Vec>), + AddTemporalPropIDs(Vec>), + AddLayerID(AddLayerID), + Checkpoint(Checkpoint), +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct AddEdge<'a> { + pub t: TimeIndexEntry, + pub src: VID, + pub dst: VID, + pub eid: EID, + pub layer_id: usize, + pub t_props: Cow<'a, Vec<(usize, Prop)>>, + pub c_props: Cow<'a, Vec<(usize, Prop)>>, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct AddNodeID { + pub gid: GID, + pub vid: VID, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct AddNodeTypeID { + pub name: String, + pub id: usize, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct AddConstPropID<'a> { + pub name: Cow<'a, str>, + pub id: usize, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct AddTemporalPropID<'a> { + pub name: Cow<'a, str>, + pub id: usize, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct AddLayerID { + pub name: String, + pub id: usize, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Checkpoint { + pub lsn: LSN, +} + + +// Constructors +impl<'a> WalEntry<'a> { + pub fn add_edge( + t: TimeIndexEntry, + src: VID, + dst: VID, + eid: EID, + layer_id: usize, + t_props: Cow<'a, Vec<(usize, Prop)>>, + c_props: Cow<'a, Vec<(usize, Prop)>>, + ) -> WalEntry<'a> { + WalEntry::AddEdge(AddEdge { + t, + src, + dst, + eid, + layer_id, + t_props, + c_props, + }) + } + + pub fn add_node_id(gid: GID, vid: VID) -> WalEntry<'static> { + WalEntry::AddNodeID(AddNodeID { gid, vid }) + } + + pub fn add_const_prop_ids(props: Vec<(Cow<'a, str>, usize)>) -> WalEntry<'a> { + WalEntry::AddConstPropIDs( + props.into_iter() + .map(|(name, id)| AddConstPropID { name, id }) + .collect(), + ) + } + + pub fn add_temporal_prop_ids(props: Vec<(Cow<'a, str>, usize)>) -> WalEntry<'a> { + WalEntry::AddTemporalPropIDs( + props.into_iter() + .map(|(name, id)| AddTemporalPropID { name, id }) + .collect(), + ) + } + + pub fn add_layer_id(name: String, id: usize) -> WalEntry<'static> { + WalEntry::AddLayerID(AddLayerID { name, id }) + } +} + +impl<'a> WalEntry<'a> { + pub fn to_bytes(&self) -> Result, postcard::Error> { + postcard::to_stdvec(self) + } + + pub fn from_bytes(bytes: &[u8]) -> Result, postcard::Error> { + postcard::from_bytes(bytes) + } +} diff --git a/db4-storage/src/wal/mod.rs b/db4-storage/src/wal/mod.rs new file mode 100644 index 0000000000..fab33e90fa --- /dev/null +++ b/db4-storage/src/wal/mod.rs @@ -0,0 +1,46 @@ +use std::path::Path; + +use crate::error::DBV4Error; + +pub mod no_wal; +pub mod entries; + +pub type LSN = u64; + +#[derive(Debug)] +pub struct WalRecord { + pub lsn: LSN, + pub data: Vec, +} + +pub trait WalOps { + fn new(dir: impl AsRef) -> Result + where + Self: Sized; + + fn dir(&self) -> &Path; + + /// Reserves and returns the next available LSN without writing any data. + fn reserve(&self) -> LSN; + + /// Appends data to the WAL with the specified LSN. + /// The LSN must have been previously reserved. + fn append_with_lsn(&self, lsn: LSN, data: &[u8]) -> Result<(), DBV4Error>; + + /// Appends data to the WAL and returns the assigned LSN. + /// This is a convenience method that combines reserve() and append_with_lsn(). + fn append(&self, data: &[u8]) -> Result { + let lsn = self.reserve(); + self.append_with_lsn(lsn, data)?; + Ok(lsn) + } + + /// Blocks until the WAL has fsynced the given LSN to disk. + fn wait_for_sync(&self, lsn: LSN); + + /// Rotates the underlying WAL file. + /// `cutoff_lsn` acts as a hint for which records can be safely discarded during rotation. + fn rotate(&self, cutoff_lsn: LSN) -> Result<(), DBV4Error>; + + fn replay(dir: impl AsRef) -> impl Iterator>; +} diff --git a/db4-storage/src/wal/no_wal.rs b/db4-storage/src/wal/no_wal.rs new file mode 100644 index 0000000000..3acf22611a --- /dev/null +++ b/db4-storage/src/wal/no_wal.rs @@ -0,0 +1,37 @@ +use std::path::{Path, PathBuf}; + +use crate::error::DBV4Error; +use crate::wal::{LSN, WalOps, WalRecord}; + +pub struct NoWal { + dir: PathBuf, +} + +impl WalOps for NoWal { + fn new(dir: impl AsRef) -> Result { + Ok(Self { dir: dir.as_ref().to_path_buf() }) + } + + fn dir(&self) -> &Path { + &self.dir + } + + fn reserve(&self) -> LSN { + 0 + } + + fn append_with_lsn(&self, _lsn: LSN, _data: &[u8]) -> Result<(), DBV4Error> { + Ok(()) + } + + fn wait_for_sync(&self, _lsn: LSN) {} + + fn rotate(&self, _cutoff_lsn: LSN) -> Result<(), DBV4Error> { + Ok(()) + } + + fn replay(_dir: impl AsRef) -> impl Iterator> { + let error = "Recovery is not supported for NoWAL"; + std::iter::once(Err(DBV4Error::GenericFailure(error.to_string()))) + } +} diff --git a/raphtory-api/src/core/entities/mod.rs b/raphtory-api/src/core/entities/mod.rs index 16b0d64aa3..1be7a635d9 100644 --- a/raphtory-api/src/core/entities/mod.rs +++ b/raphtory-api/src/core/entities/mod.rs @@ -149,6 +149,7 @@ pub enum GID { U64(u64), Str(String), } + impl PartialEq for GID { fn eq(&self, other: &str) -> bool { match self { diff --git a/raphtory-storage/src/mutation/addition_ops.rs b/raphtory-storage/src/mutation/addition_ops.rs index 5f57df8311..a0900beb73 100644 --- a/raphtory-storage/src/mutation/addition_ops.rs +++ b/raphtory-storage/src/mutation/addition_ops.rs @@ -178,12 +178,7 @@ impl InternalAdditionOps for GraphStorage { type Error = MutationError; type WS<'b> = UnlockedSession<'b>; - type AtomicAddEdge<'a> = WriteS< - 'a, - RwLockWriteGuard<'a, MemNodeSegment>, - RwLockWriteGuard<'a, MemEdgeSegment>, - Extension, - >; + type AtomicAddEdge<'a> = WriteS<'a, Extension>; fn write_lock(&self) -> Result, Self::Error> { self.mutable()?.write_lock() diff --git a/raphtory-storage/src/mutation/addition_ops_ext.rs b/raphtory-storage/src/mutation/addition_ops_ext.rs index b3d256bf96..312d18cf6a 100644 --- a/raphtory-storage/src/mutation/addition_ops_ext.rs +++ b/raphtory-storage/src/mutation/addition_ops_ext.rs @@ -34,13 +34,8 @@ use crate::mutation::{ MutationError, }; -pub struct WriteS< - 'a, - MNS: DerefMut, - MES: DerefMut, - EXT: PersistentStrategy, ES = ES>, -> { - static_session: WriteSession<'a, MNS, MES, NS, ES, EXT>, +pub struct WriteS<'a, EXT: PersistentStrategy, ES = ES>> { + static_session: WriteSession<'a, NS, ES, EXT>, } #[derive(Clone, Copy, Debug)] @@ -50,10 +45,8 @@ pub struct UnlockedSession<'a> { impl< 'a, - MNS: DerefMut + Send + Sync, - MES: DerefMut + Send + Sync, EXT: PersistentStrategy, ES = ES>, - > EdgeWriteLock for WriteS<'a, MNS, MES, EXT> + > EdgeWriteLock for WriteS<'a, EXT> { fn internal_add_edge( &mut self, @@ -187,12 +180,7 @@ impl InternalAdditionOps for TemporalGraph { type WS<'a> = UnlockedSession<'a>; - type AtomicAddEdge<'a> = WriteS< - 'a, - RwLockWriteGuard<'a, MemNodeSegment>, - RwLockWriteGuard<'a, MemEdgeSegment>, - Extension, - >; + type AtomicAddEdge<'a> = WriteS<'a, Extension>; fn write_lock(&self) -> Result, Self::Error> { let locked_g = self.write_locked_graph(); diff --git a/raphtory/src/db/api/storage/storage.rs b/raphtory/src/db/api/storage/storage.rs index 7a0f79a399..da4eab16d5 100644 --- a/raphtory/src/db/api/storage/storage.rs +++ b/raphtory/src/db/api/storage/storage.rs @@ -197,12 +197,7 @@ pub struct StorageWriteSession<'a> { } pub struct AtomicAddEdgeSession<'a> { - session: WriteS< - 'a, - RwLockWriteGuard<'a, MemNodeSegment>, - RwLockWriteGuard<'a, MemEdgeSegment>, - Extension, - >, + session: WriteS<'a, Extension>, storage: &'a Storage, } From dcf50d129a53c63d977108217a433a83e1ecd312 Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Wed, 9 Jul 2025 16:20:17 +0100 Subject: [PATCH 075/321] replace some quickcheck with proptest and fix more issues for storage --- db4-graph/src/lib.rs | 2 +- db4-storage/src/api/edges.rs | 2 +- db4-storage/src/api/nodes.rs | 2 +- db4-storage/src/gen_t_props.rs | 3 +- db4-storage/src/pages/edge_store.rs | 5 +- db4-storage/src/pages/locked/edges.rs | 2 +- db4-storage/src/pages/mod.rs | 2 +- db4-storage/src/pages/node_store.rs | 5 +- db4-storage/src/pages/session.rs | 65 +-- raphtory-api/src/core/storage/dict_mapper.rs | 52 +-- raphtory-storage/src/mutation/addition_ops.rs | 4 +- .../src/mutation/addition_ops_ext.rs | 34 +- .../components/connected_components.rs | 120 +++--- raphtory/src/core/state/mod.rs | 36 +- raphtory/src/db/api/mutation/addition_ops.rs | 13 +- raphtory/src/db/api/mutation/deletion_ops.rs | 4 +- raphtory/src/db/api/storage/storage.rs | 8 +- raphtory/src/db/graph/graph.rs | 375 ++++++++++-------- raphtory/src/db/graph/views/window_graph.rs | 286 ++++++------- raphtory/src/serialise/parquet/mod.rs | 3 +- 20 files changed, 502 insertions(+), 521 deletions(-) diff --git a/db4-graph/src/lib.rs b/db4-graph/src/lib.rs index 651fc841f1..36fe731f32 100644 --- a/db4-graph/src/lib.rs +++ b/db4-graph/src/lib.rs @@ -273,7 +273,7 @@ impl<'a, EXT: PersistentStrategy, ES = ES>> WriteLockedGraph<' match result { Ok(vid) => Ok(vid), - Err(GIDResolverError::DBV4Error(e)) => panic!("Database error: {}", e), + Err(GIDResolverError::DBV4Error(e)) => panic!("Database error: {e}"), Err(GIDResolverError::InvalidNodeId(e)) => Err(e), } } diff --git a/db4-storage/src/api/edges.rs b/db4-storage/src/api/edges.rs index 5e8a494db8..13e1f0621b 100644 --- a/db4-storage/src/api/edges.rs +++ b/db4-storage/src/api/edges.rs @@ -116,7 +116,7 @@ pub trait LockedESegment: Send + Sync + std::fmt::Debug { fn edge_par_iter<'a, 'b: 'a>( &'a self, layer_ids: &'b LayerIds, - ) -> impl ParallelIterator> + Send + Sync + 'a; + ) -> impl ParallelIterator> + Sync + 'a; } pub trait EdgeEntryOps<'a>: Send + Sync { diff --git a/db4-storage/src/api/nodes.rs b/db4-storage/src/api/nodes.rs index b9b7b5e143..d00df2deca 100644 --- a/db4-storage/src/api/nodes.rs +++ b/db4-storage/src/api/nodes.rs @@ -27,7 +27,7 @@ use raphtory_core::{ use crate::{ LocalPOS, error::DBV4Error, - gen_ts::{ALL_LAYERS, LayerIter}, + gen_ts::LayerIter, segments::node::MemNodeSegment, utils::{Iter2, Iter3, Iter4}, }; diff --git a/db4-storage/src/gen_t_props.rs b/db4-storage/src/gen_t_props.rs index 2ca0f73a3f..de4023f366 100644 --- a/db4-storage/src/gen_t_props.rs +++ b/db4-storage/src/gen_t_props.rs @@ -81,8 +81,7 @@ impl<'a, Ref: WithTProps<'a>> GenTProps<'a, Ref> { impl<'a, Ref: WithTProps<'a> + 'a> TPropOps<'a> for GenTProps<'a, Ref> { fn last_before(&self, t: TimeIndexEntry) -> Option<(TimeIndexEntry, Prop)> { self.tprops(self.prop_id) - .map(|t_props| t_props.last_before(t)) - .flatten() + .filter_map(|t_props| t_props.last_before(t)) .max_by_key(|(t, _)| *t) } diff --git a/db4-storage/src/pages/edge_store.rs b/db4-storage/src/pages/edge_store.rs index 5da16f05bc..9a9949f56a 100644 --- a/db4-storage/src/pages/edge_store.rs +++ b/db4-storage/src/pages/edge_store.rs @@ -203,8 +203,7 @@ impl, EXT: Clone + Send + Sync> EdgeStorageI if first_p_id != 0 { return Err(DBV4Error::GenericFailure(format!( - "First page id is not 0 in {:?}", - edges_path + "First page id is not 0 in {edges_path:?}" ))); } @@ -279,7 +278,7 @@ impl, EXT: Clone + Send + Sync> EdgeStorageI return segment; } let count = self.pages.count(); - if count >= segment_id + 1 { + if count > segment_id { // something has allocated the segment, wait for it to be added loop { if let Some(segment) = self.pages.get(segment_id) { diff --git a/db4-storage/src/pages/locked/edges.rs b/db4-storage/src/pages/locked/edges.rs index e941c0e944..126aaabd78 100644 --- a/db4-storage/src/pages/locked/edges.rs +++ b/db4-storage/src/pages/locked/edges.rs @@ -1,4 +1,4 @@ -use std::{ops::DerefMut, sync::atomic::AtomicUsize}; +use std::ops::DerefMut; use crate::{ LocalPOS, diff --git a/db4-storage/src/pages/mod.rs b/db4-storage/src/pages/mod.rs index e0751af5c3..37939cb231 100644 --- a/db4-storage/src/pages/mod.rs +++ b/db4-storage/src/pages/mod.rs @@ -161,7 +161,7 @@ impl< let nodes = Arc::new(NodeStorageInner::new_with_meta( nodes_path, max_page_len_nodes, - node_meta.into(), + node_meta, ext.clone(), )); let edges = Arc::new(EdgeStorageInner::new_with_meta( diff --git a/db4-storage/src/pages/node_store.rs b/db4-storage/src/pages/node_store.rs index df765b211b..f8fc3602b8 100644 --- a/db4-storage/src/pages/node_store.rs +++ b/db4-storage/src/pages/node_store.rs @@ -223,8 +223,7 @@ impl, EXT: Clone> NodeStorageInner if first_p_id != 0 { return Err(DBV4Error::GenericFailure(format!( - "First page id is not 0 in {:?}", - nodes_path + "First page id is not 0 in {nodes_path:?}" ))); } @@ -273,7 +272,7 @@ impl, EXT: Clone> NodeStorageInner return segment; } let count = self.pages.count(); - if count >= segment_id + 1 { + if count > segment_id { // something has allocated the segment, wait for it to be added loop { if let Some(segment) = self.pages.get(segment_id) { diff --git a/db4-storage/src/pages/session.rs b/db4-storage/src/pages/session.rs index 0fb207cdd8..956512a89f 100644 --- a/db4-storage/src/pages/session.rs +++ b/db4-storage/src/pages/session.rs @@ -7,7 +7,7 @@ use crate::{ LocalPOS, api::{edges::EdgeSegmentOps, nodes::NodeSegmentOps}, error::DBV4Error, - pages::{NODE_ID_PROP_KEY, NODE_TYPE_PROP_KEY}, + pages::{NODE_ID_PROP_KEY, NODE_TYPE_PROP_KEY, node_page::writer::NodeWriter}, segments::{edge::MemEdgeSegment, node::MemNodeSegment}, }; use parking_lot::RwLockWriteGuard; @@ -50,6 +50,18 @@ impl< } } + pub fn resolve_node_pos(&self, vid: impl Into) -> LocalPOS { + self.graph.nodes().resolve_pos(vid.into()).1 + } + + pub fn node_id_prop_id(&self) -> usize { + self.graph + .node_meta() + .const_prop_meta() + .get_id(NODE_ID_PROP_KEY) + .unwrap() + } + pub fn add_edge_into_layer( &mut self, t: T, @@ -309,54 +321,7 @@ impl< } } - pub fn store_node_id_as_prop( - &mut self, - id: GidRef, - vid: impl Into, - ) -> Result<(), DBV4Error> { - let (prop_val, prop_dtype) = match id { - GidRef::U64(id) => (Prop::U64(id), PropType::U64), - GidRef::Str(id) => (Prop::Str(id.into()), PropType::Str), - }; - - self.store_node_const_prop(NODE_ID_PROP_KEY, prop_val, prop_dtype, vid) - } - - pub fn store_node_type_as_prop( - &mut self, - node_type: &str, - vid: impl Into, - ) -> Result<(), DBV4Error> { - let (prop_val, prop_dtype) = (Prop::Str(node_type.into()), PropType::Str); - - self.store_node_const_prop(NODE_TYPE_PROP_KEY, prop_val, prop_dtype, vid) - } - - fn store_node_const_prop( - &mut self, - prop_key: &str, - prop_val: Prop, - prop_dtype: PropType, - vid: impl Into, - ) -> Result<(), DBV4Error> { - let layer = 0; - let prop_id = self - .graph - .node_meta() - .const_prop_meta() - .get_and_validate(prop_key, prop_dtype)? - .ok_or_else(|| { - DBV4Error::GenericFailure(format!("{} const prop not found", prop_key)) - })?; - - let props = vec![(prop_id, prop_val)]; - let (_, local_pos) = self.graph.nodes().resolve_pos(vid); - let lsn = 0; // TODO: lsn should be passed in - - self.node_writers - .get_mut_src() - .update_c_props(local_pos, layer, props, lsn); - - Ok(()) + pub fn node_writers(&mut self) -> &mut WriterPair<'a, MNS, NS> { + &mut self.node_writers } } diff --git a/raphtory-api/src/core/storage/dict_mapper.rs b/raphtory-api/src/core/storage/dict_mapper.rs index c1bcfcb031..cddffaf531 100644 --- a/raphtory-api/src/core/storage/dict_mapper.rs +++ b/raphtory-api/src/core/storage/dict_mapper.rs @@ -269,7 +269,7 @@ impl DictMapper { #[cfg(test)] mod test { use crate::core::storage::dict_mapper::DictMapper; - use quickcheck_macros::quickcheck; + use proptest::prelude::*; use rand::seq::SliceRandom; use rayon::prelude::*; use std::collections::HashMap; @@ -284,30 +284,32 @@ mod test { assert_eq!(mapper.get_or_create_id("test").inner(), 0); } - #[quickcheck] - fn check_dict_mapper_concurrent_write(write: Vec) -> bool { - let n = 100; - let mapper: DictMapper = DictMapper::default(); - - // create n maps from strings to ids in parallel - let res: Vec> = (0..n) - .into_par_iter() - .map(|_| { - let mut ids: HashMap = Default::default(); - let mut rng = rand::thread_rng(); - let mut write_s = write.clone(); - write_s.shuffle(&mut rng); - for s in write_s { - let id = mapper.get_or_create_id(s.as_str()); - ids.insert(s, id.inner()); - } - ids - }) - .collect(); - - // check that all maps are the same and that all strings have been assigned an id - let res_0 = &res[0]; - res[1..n].iter().all(|v| res_0 == v) && write.iter().all(|v| mapper.get_id(v).is_some()) + #[test] + fn check_dict_mapper_concurrent_write() { + proptest!(|(write: Vec)| { + let n = 100; + let mapper: DictMapper = DictMapper::default(); + + // create n maps from strings to ids in parallel + let res: Vec> = (0..n) + .into_par_iter() + .map(|_| { + let mut ids: HashMap = Default::default(); + let mut rng = rand::thread_rng(); + let mut write_s = write.clone(); + write_s.shuffle(&mut rng); + for s in write_s { + let id = mapper.get_or_create_id(s.as_str()); + ids.insert(s, id.inner()); + } + ids + }) + .collect(); + + // check that all maps are the same and that all strings have been assigned an id + let res_0 = &res[0]; + prop_assert!(res[1..n].iter().all(|v| res_0 == v) && write.iter().all(|v| mapper.get_id(v).is_some())); + }); } // map 5 strings to 5 ids from 4 threads concurrently 1000 times diff --git a/raphtory-storage/src/mutation/addition_ops.rs b/raphtory-storage/src/mutation/addition_ops.rs index a0900beb73..3b317fb976 100644 --- a/raphtory-storage/src/mutation/addition_ops.rs +++ b/raphtory-storage/src/mutation/addition_ops.rs @@ -107,8 +107,8 @@ pub trait EdgeWriteLock: Send + Sync { layer: usize, ) -> MaybeNew; - /// Stores id as a const prop within the node - fn store_node_id_as_prop(&mut self, id: NodeRef, vid: impl Into); + fn store_src_node_info(&mut self, id: impl Into, node_id: Option); + fn store_dst_node_info(&mut self, id: impl Into, node_id: Option); } pub trait AtomicNodeAddition: Send + Sync { diff --git a/raphtory-storage/src/mutation/addition_ops_ext.rs b/raphtory-storage/src/mutation/addition_ops_ext.rs index 312d18cf6a..75ac6366e3 100644 --- a/raphtory-storage/src/mutation/addition_ops_ext.rs +++ b/raphtory-storage/src/mutation/addition_ops_ext.rs @@ -11,7 +11,9 @@ use raphtory_api::core::{ }; use raphtory_core::{ entities::{ - graph::tgraph::TooManyLayers, nodes::node_ref::NodeRef, GidRef, EID, ELID, MAX_LAYER, VID, + graph::tgraph::TooManyLayers, + nodes::node_ref::{AsNodeRef, NodeRef}, + GidRef, EID, ELID, MAX_LAYER, VID, }, storage::{raw_edges::WriteLockedEdges, timeindex::TimeIndexEntry, WriteLockedNodes}, }; @@ -91,14 +93,28 @@ impl< eid } - fn store_node_id_as_prop(&mut self, id: NodeRef, vid: impl Into) { - match id { - NodeRef::External(id) => { - let vid = vid.into(); - let _ = self.static_session.store_node_id_as_prop(id, vid); - } - NodeRef::Internal(_) => (), - } + fn store_src_node_info(&mut self, vid: impl Into, node_id: Option) { + if let Some(id) = node_id { + let pos = self.static_session.resolve_node_pos(vid); + let prop_id = self.static_session.node_id_prop_id(); + + self.static_session + .node_writers() + .get_mut_src() + .update_c_props(pos, 0, [(prop_id, id.into())], 0); + }; + } + + fn store_dst_node_info(&mut self, vid: impl Into, node_id: Option) { + if let Some(id) = node_id { + let pos = self.static_session.resolve_node_pos(vid); + let prop_id = self.static_session.node_id_prop_id(); + + self.static_session + .node_writers() + .get_mut_dst() + .update_c_props(pos, 0, [(prop_id, id.into())], 0); + }; } } diff --git a/raphtory/src/algorithms/components/connected_components.rs b/raphtory/src/algorithms/components/connected_components.rs index eb186d17a2..8ef1b10dee 100644 --- a/raphtory/src/algorithms/components/connected_components.rs +++ b/raphtory/src/algorithms/components/connected_components.rs @@ -83,9 +83,11 @@ where #[cfg(test)] mod cc_test { use super::*; - use crate::{db::api::mutation::AdditionOps, prelude::*, test_storage}; + use crate::{db::api::mutation::AdditionOps, prelude::*, test_storage, test_utils::test_graph}; + use db4_graph::TemporalGraph; use itertools::*; - use quickcheck_macros::quickcheck; + use proptest::prelude::*; + use raphtory_storage::{core_ops::CoreGraphOps, graph::graph::GraphStorage}; use std::{cmp::Reverse, collections::HashMap, iter::once}; #[test] @@ -237,63 +239,67 @@ mod cc_test { }); } - #[quickcheck] - fn circle_graph_edges(vs: Vec) { - if !vs.is_empty() { - let vs = vs.into_iter().unique().collect::>(); - - let _smallest = vs.iter().min().unwrap(); - - let first = vs[0]; - // pairs of nodes from vs one after the next - let edges = vs - .iter() - .zip(chain!(vs.iter().skip(1), once(&first))) - .map(|(a, b)| (*a, *b)) - .collect::>(); - - assert_eq!(edges[0].0, first); - assert_eq!(edges.last().unwrap().1, first); - } + #[test] + fn circle_graph_edges() { + proptest!(|(vs: Vec)| { + if !vs.is_empty() { + let vs = vs.into_iter().unique().collect::>(); + + let _smallest = vs.iter().min().unwrap(); + + let first = vs[0]; + // pairs of nodes from vs one after the next + let edges = vs + .iter() + .zip(chain!(vs.iter().skip(1), once(&first))) + .map(|(a, b)| (*a, *b)) + .collect::>(); + + assert_eq!(edges[0].0, first); + assert_eq!(edges.last().unwrap().1, first); + } + }); } - #[quickcheck] - fn circle_graph_the_smallest_value_is_the_cc(vs: Vec) { - if !vs.is_empty() { - let graph = Graph::new(); - - let vs = vs.into_iter().unique().collect::>(); - - let first = vs[0]; - - // pairs of nodes from vs one after the next - let edges = vs - .iter() - .zip(chain!(vs.iter().skip(1), once(&first))) - .map(|(a, b)| (*a, *b)) - .collect::>(); - - for (src, dst) in edges.iter() { - graph.add_edge(0, *src, *dst, NO_PROPS, None).unwrap(); + #[test] + fn circle_graph_the_smallest_value_is_the_cc() { + proptest!(|(vs: Vec)| { + if !vs.is_empty() { + let graph = Graph::new(); + + let vs = vs.into_iter().unique().collect::>(); + + let first = vs[0]; + + // pairs of nodes from vs one after the next + let edges = vs + .iter() + .zip(chain!(vs.iter().skip(1), once(&first))) + .map(|(a, b)| (*a, *b)) + .collect::>(); + + for (src, dst) in edges.iter() { + graph.add_edge(0, *src, *dst, NO_PROPS, None).unwrap(); + } + + test_storage!(&graph, |graph| { + // now we do connected community_detection over window 0..1 + let res = weakly_connected_components(graph, usize::MAX, None).groups(); + + let (cc, size) = res + .into_iter_groups() + .map(|(cc, group)| (cc, Reverse(group.len()))) + .sorted_by(|l, r| l.1.cmp(&r.1)) + .map(|(cc, count)| (cc, count.0)) + .take(1) + .next() + .unwrap(); + + assert_eq!(cc, 0); + + assert_eq!(size, edges.len()); + }); } - - test_storage!(&graph, |graph| { - // now we do connected community_detection over window 0..1 - let res = weakly_connected_components(graph, usize::MAX, None).groups(); - - let (cc, size) = res - .into_iter_groups() - .map(|(cc, group)| (cc, Reverse(group.len()))) - .sorted_by(|l, r| l.1.cmp(&r.1)) - .map(|(cc, count)| (cc, count.0)) - .take(1) - .next() - .unwrap(); - - assert_eq!(cc, 0); - - assert_eq!(size, edges.len()); - }); - } + }); } } diff --git a/raphtory/src/core/state/mod.rs b/raphtory/src/core/state/mod.rs index 121da45b77..ce5d70e839 100644 --- a/raphtory/src/core/state/mod.rs +++ b/raphtory/src/core/state/mod.rs @@ -12,7 +12,7 @@ impl StateType f #[cfg(test)] mod state_test { use itertools::Itertools; - use quickcheck_macros::quickcheck; + use proptest::prelude::*; use rand::Rng; use crate::{ @@ -24,23 +24,25 @@ mod state_test { prelude::NO_PROPS, }; - #[quickcheck] - fn check_merge_2_vecs(mut a: Vec, b: Vec) { - let len_a = a.len(); - let len_b = b.len(); - - merge_2_vecs(&mut a, &b, |ia, ib| *ia = usize::max(*ia, *ib)); - - assert_eq!(a.len(), usize::max(len_a, len_b)); - - for (i, expected) in a.iter().enumerate() { - match (a.get(i), b.get(i)) { - (Some(va), Some(vb)) => assert_eq!(*expected, usize::max(*va, *vb)), - (Some(va), None) => assert_eq!(*expected, *va), - (None, Some(vb)) => assert_eq!(*expected, *vb), - (None, None) => panic!("value should exist in either a or b"), + #[test] + fn check_merge_2_vecs() { + proptest!(|(mut a: Vec, b: Vec)| { + let len_a = a.len(); + let len_b = b.len(); + + merge_2_vecs(&mut a, &b, |ia, ib| *ia = usize::max(*ia, *ib)); + + assert_eq!(a.len(), usize::max(len_a, len_b)); + + for (i, expected) in a.iter().enumerate() { + match (a.get(i), b.get(i)) { + (Some(va), Some(vb)) => assert_eq!(*expected, usize::max(*va, *vb)), + (Some(va), None) => assert_eq!(*expected, *va), + (None, Some(vb)) => assert_eq!(*expected, *vb), + (None, None) => panic!("value should exist in either a or b"), + } } - } + }); } fn tiny_graph() -> Graph { diff --git a/raphtory/src/db/api/mutation/addition_ops.rs b/raphtory/src/db/api/mutation/addition_ops.rs index 841b19b1e0..291f7bb2ef 100644 --- a/raphtory/src/db/api/mutation/addition_ops.rs +++ b/raphtory/src/db/api/mutation/addition_ops.rs @@ -13,13 +13,8 @@ use crate::{ errors::{into_graph_err, GraphError}, prelude::{GraphViewOps, NodeViewOps}, }; -use raphtory_api::core::{ - entities::properties::prop::Prop, - storage::dict_mapper::MaybeNew::{Existing, New}, -}; -use raphtory_storage::mutation::addition_ops::{ - EdgeWriteLock, InternalAdditionOps, SessionAdditionOps, -}; +use raphtory_api::core::entities::properties::prop::Prop; +use raphtory_storage::mutation::addition_ops::{EdgeWriteLock, InternalAdditionOps}; pub trait AdditionOps: StaticGraphViewOps + InternalAdditionOps> { // TODO: Probably add vector reference here like add @@ -314,8 +309,8 @@ impl> + StaticGraphViewOps> Addit .map_err(into_graph_err)?; let edge_id = add_edge_op.internal_add_edge(ti, src_id, dst_id, 0, layer_id, props); - add_edge_op.store_node_id_as_prop(src.as_node_ref(), src_id); - add_edge_op.store_node_id_as_prop(dst.as_node_ref(), dst_id); + add_edge_op.store_src_node_info(src_id, src.as_node_ref().as_gid_ref().left()); + add_edge_op.store_dst_node_info(dst_id, dst.as_node_ref().as_gid_ref().left()); Ok(EdgeView::new( self.clone(), diff --git a/raphtory/src/db/api/mutation/deletion_ops.rs b/raphtory/src/db/api/mutation/deletion_ops.rs index 9a4f0f89a5..3dbf14f924 100644 --- a/raphtory/src/db/api/mutation/deletion_ops.rs +++ b/raphtory/src/db/api/mutation/deletion_ops.rs @@ -52,8 +52,8 @@ pub trait DeletionOps: .map_err(into_graph_err)?; let edge_id = add_edge_op.internal_delete_edge(ti, src_id, dst_id, 0, layer_id); - add_edge_op.store_node_id_as_prop(src.as_node_ref(), src_id); - add_edge_op.store_node_id_as_prop(dst.as_node_ref(), dst_id); + add_edge_op.store_src_node_info(src_id, src.as_node_ref().as_gid_ref().left()); + add_edge_op.store_dst_node_info(dst_id, dst.as_node_ref().as_gid_ref().left()); Ok(EdgeView::new( self.clone(), diff --git a/raphtory/src/db/api/storage/storage.rs b/raphtory/src/db/api/storage/storage.rs index da4eab16d5..6cf68a9265 100644 --- a/raphtory/src/db/api/storage/storage.rs +++ b/raphtory/src/db/api/storage/storage.rs @@ -226,8 +226,12 @@ impl EdgeWriteLock for AtomicAddEdgeSession<'_> { self.session.internal_delete_edge(t, src, dst, lsn, layer) } - fn store_node_id_as_prop(&mut self, id: NodeRef, vid: impl Into) { - self.session.store_node_id_as_prop(id, vid); + fn store_src_node_info(&mut self, id: impl Into, node_id: Option) { + self.session.store_src_node_info(id, node_id); + } + + fn store_dst_node_info(&mut self, id: impl Into, node_id: Option) { + self.session.store_dst_node_info(id, node_id); } } diff --git a/raphtory/src/db/graph/graph.rs b/raphtory/src/db/graph/graph.rs index 55c07e5985..45f5b3caf0 100644 --- a/raphtory/src/db/graph/graph.rs +++ b/raphtory/src/db/graph/graph.rs @@ -595,8 +595,8 @@ mod db_tests { use bigdecimal::BigDecimal; use chrono::NaiveDateTime; use itertools::Itertools; + use proptest::prelude::*; use proptest::{arbitrary::any, proptest}; - use quickcheck_macros::quickcheck; use raphtory_api::core::{ entities::{GID, VID}, storage::{ @@ -727,72 +727,81 @@ mod db_tests { }); } - #[quickcheck] - fn test_multithreaded_add_edge(edges: Vec<(u64, u64)>) -> bool { - let g = Graph::new(); - edges.par_iter().enumerate().for_each(|(t, (i, j))| { - g.add_edge(t as i64, *i, *j, NO_PROPS, None).unwrap(); + #[test] + fn test_multithreaded_add_edge() { + proptest!(|(edges: Vec<(u64, u64)>)| { + let g = Graph::new(); + edges.par_iter().enumerate().for_each(|(t, (i, j))| { + g.add_edge(t as i64, *i, *j, NO_PROPS, None).unwrap(); + }); + prop_assert!(edges.iter().all(|(i, j)| g.has_edge(*i, *j)) && g.count_temporal_edges() == edges.len()); }); - edges.iter().all(|(i, j)| g.has_edge(*i, *j)) && g.count_temporal_edges() == edges.len() } - #[quickcheck] - fn add_node_grows_graph_len(vs: Vec<(i64, u64)>) { - let g = Graph::new(); - - let expected_len = vs.iter().map(|(_, v)| v).sorted().dedup().count(); - for (t, v) in vs { - g.add_node(t, v, NO_PROPS, None) - .map_err(|err| error!("{:?}", err)) - .ok(); - } + #[test] + fn add_node_grows_graph_len() { + proptest!(|(vs: Vec<(i64, u64)>)| { + let g = Graph::new(); + + let expected_len = vs.iter().map(|(_, v)| v).sorted().dedup().count(); + for (t, v) in vs { + g.add_node(t, v, NO_PROPS, None) + .map_err(|err| error!("{:?}", err)) + .ok(); + } - assert_eq!(g.count_nodes(), expected_len) + prop_assert_eq!(g.count_nodes(), expected_len); + }); } - #[quickcheck] - fn add_node_gets_names(vs: Vec) -> bool { - global_info_logger(); - let g = Graph::new(); - - let expected_len = vs.iter().sorted().dedup().count(); - for (t, name) in vs.iter().enumerate() { - g.add_node(t as i64, name.clone(), NO_PROPS, None) - .map_err(|err| info!("{:?}", err)) - .ok(); - } + #[test] + fn add_node_gets_names() { + proptest!(|(vs: Vec)| { + global_info_logger(); + let g = Graph::new(); + + let expected_len = vs.iter().sorted().dedup().count(); + for (t, name) in vs.iter().enumerate() { + g.add_node(t as i64, name.clone(), NO_PROPS, None) + .map_err(|err| info!("{:?}", err)) + .ok(); + } - assert_eq!(g.count_nodes(), expected_len); + prop_assert_eq!(g.count_nodes(), expected_len); - vs.iter().all(|name| { - let v = g.node(name.clone()).unwrap(); - v.name() == name.clone() - }) + let res = vs.iter().all(|name| { + let v = g.node(name.clone()).unwrap(); + v.name() == name.clone() + }); + prop_assert!(res); + }); } - #[quickcheck] - fn add_edge_grows_graph_edge_len(edges: Vec<(i64, u64, u64)>) { - let g = Graph::new(); + #[test] + fn add_edge_grows_graph_edge_len() { + proptest!(|(edges: Vec<(i64, u64, u64)>)| { + let g = Graph::new(); - let unique_nodes_count = edges - .iter() - .flat_map(|(_, src, dst)| vec![src, dst]) - .sorted() - .dedup() - .count(); + let unique_nodes_count = edges + .iter() + .flat_map(|(_, src, dst)| vec![src, dst]) + .sorted() + .dedup() + .count(); - let unique_edge_count = edges - .iter() - .map(|(_, src, dst)| (src, dst)) - .unique() - .count(); + let unique_edge_count = edges + .iter() + .map(|(_, src, dst)| (src, dst)) + .unique() + .count(); - for (t, src, dst) in edges { - g.add_edge(t, src, dst, NO_PROPS, None).unwrap(); - } + for (t, src, dst) in edges { + g.add_edge(t, src, dst, NO_PROPS, None).unwrap(); + } - assert_eq!(g.count_nodes(), unique_nodes_count); - assert_eq!(g.count_edges(), unique_edge_count); + prop_assert_eq!(g.count_nodes(), unique_nodes_count); + prop_assert_eq!(g.count_edges(), unique_edge_count); + }); } #[test] @@ -807,26 +816,30 @@ mod db_tests { assert!(edges.iter().all(|&(_, src, dst)| g.has_edge(src, dst))) } - #[quickcheck] - fn add_edge_works(edges: Vec<(i64, u64, u64)>) -> bool { - let g = Graph::new(); - for &(t, src, dst) in edges.iter() { - g.add_edge(t, src, dst, NO_PROPS, None).unwrap(); - } + #[test] + fn add_edge_works() { + proptest!(|(edges: Vec<(i64, u64, u64)>)| { + let g = Graph::new(); + for &(t, src, dst) in edges.iter() { + g.add_edge(t, src, dst, NO_PROPS, None).unwrap(); + } - edges.iter().all(|&(_, src, dst)| g.has_edge(src, dst)) + prop_assert!(edges.iter().all(|&(_, src, dst)| g.has_edge(src, dst))); + }); } - #[quickcheck] - fn get_edge_works(edges: Vec<(i64, u64, u64)>) -> bool { - let g = Graph::new(); - for &(t, src, dst) in edges.iter() { - g.add_edge(t, src, dst, NO_PROPS, None).unwrap(); - } + #[test] + fn get_edge_works() { + proptest!(|(edges: Vec<(i64, u64, u64)>)| { + let g = Graph::new(); + for &(t, src, dst) in edges.iter() { + g.add_edge(t, src, dst, NO_PROPS, None).unwrap(); + } - edges - .iter() - .all(|&(_, src, dst)| g.edge(src, dst).is_some()) + prop_assert!(edges + .iter() + .all(|&(_, src, dst)| g.edge(src, dst).is_some())); + }); } #[test] @@ -1003,7 +1016,7 @@ mod db_tests { vec!["Q", "R"], ); } - Err(e) => panic!("Unexpected error: {:?}", e), + Err(e) => panic!("Unexpected error: {e:?}"), Ok(_) => panic!("Expected error but got Ok"), } let mut nodes = gg.nodes().name().collect_vec(); @@ -2517,32 +2530,34 @@ mod db_tests { prop = Prop::U16(65535); assert_eq!(format!("{}", prop), "65535"); - prop = Prop::F32(3.14159); - assert_eq!(format!("{}", prop), "3.14159"); + prop = Prop::F32(3.24159); + assert_eq!(format!("{}", prop), "3.24159"); - prop = Prop::F64(3.141592653589793); - assert_eq!(format!("{}", prop), "3.141592653589793"); + prop = Prop::F64(3.241592653589793); + assert_eq!(format!("{}", prop), "3.241592653589793"); prop = Prop::Bool(true); assert_eq!(format!("{}", prop), "true"); } - #[quickcheck] - fn test_graph_constant_props(u64_props: HashMap) -> bool { - let g = Graph::new(); + #[test] + fn test_graph_constant_props() { + proptest!(|(u64_props: HashMap)| { + let g = Graph::new(); - let as_props = u64_props - .into_iter() - .map(|(name, value)| (name, Prop::U64(value))) - .collect::>(); + let as_props = u64_props + .into_iter() + .map(|(name, value)| (name, Prop::U64(value))) + .collect::>(); - g.add_constant_properties(as_props.clone()).unwrap(); + g.add_constant_properties(as_props.clone()).unwrap(); - let props_map = as_props.into_iter().collect::>(); + let props_map = as_props.into_iter().collect::>(); - props_map - .into_iter() - .all(|(name, value)| g.properties().constant().get(&name).unwrap() == value) + prop_assert!(props_map + .into_iter() + .all(|(name, value)| g.properties().constant().get(&name).unwrap() == value)); + }); } #[test] @@ -2586,90 +2601,94 @@ mod db_tests { ); } - #[quickcheck] - fn test_graph_constant_props_names(u64_props: HashMap) -> bool { - let g = Graph::new(); + #[test] + fn test_graph_constant_props_names() { + proptest!(|(u64_props: HashMap)| { + let g = Graph::new(); - let as_props = u64_props - .into_iter() - .map(|(name, value)| (name.into(), Prop::U64(value))) - .collect::>(); + let as_props = u64_props + .into_iter() + .map(|(name, value)| (name.into(), Prop::U64(value))) + .collect::>(); - g.add_constant_properties(as_props.clone()).unwrap(); + g.add_constant_properties(as_props.clone()).unwrap(); - let props_names = as_props - .into_iter() - .map(|(name, _)| name) - .collect::>(); + let props_names = as_props + .into_iter() + .map(|(name, _)| name) + .collect::>(); - g.properties().constant().keys().collect::>() == props_names + prop_assert_eq!(g.properties().constant().keys().collect::>(), props_names); + }); } - #[quickcheck] - fn test_graph_temporal_props(str_props: HashMap) -> bool { - global_info_logger(); - let g = Graph::new(); + #[test] + fn test_graph_temporal_props() { + proptest!(|(str_props: HashMap)| { + global_info_logger(); + let g = Graph::new(); - let (t0, t1) = (1, 2); + let (t0, t1) = (1, 2); - let (t0_props, t1_props): (Vec<_>, Vec<_>) = str_props - .iter() - .enumerate() - .map(|(i, props)| { - let (name, value) = props; - let value = Prop::from(value); - (name.as_str().into(), value, i % 2) - }) - .partition(|(_, _, i)| *i == 0); + let (t0_props, t1_props): (Vec<_>, Vec<_>) = str_props + .iter() + .enumerate() + .map(|(i, props)| { + let (name, value) = props; + let value = Prop::from(value); + (name.as_str().into(), value, i % 2) + }) + .partition(|(_, _, i)| *i == 0); - let t0_props: HashMap = t0_props - .into_iter() - .map(|(name, value, _)| (name, value)) - .collect(); + let t0_props: HashMap = t0_props + .into_iter() + .map(|(name, value, _)| (name, value)) + .collect(); - let t1_props: HashMap = t1_props - .into_iter() - .map(|(name, value, _)| (name, value)) - .collect(); + let t1_props: HashMap = t1_props + .into_iter() + .map(|(name, value, _)| (name, value)) + .collect(); - g.add_properties(t0, t0_props.clone()).unwrap(); - g.add_properties(t1, t1_props.clone()).unwrap(); + g.add_properties(t0, t0_props.clone()).unwrap(); + g.add_properties(t1, t1_props.clone()).unwrap(); - let check = t0_props.iter().all(|(name, value)| { - g.properties().temporal().get(name).unwrap().at(t0) == Some(value.clone()) - }) && t1_props.iter().all(|(name, value)| { - g.properties().temporal().get(name).unwrap().at(t1) == Some(value.clone()) - }); - if !check { - error!("failed time-specific comparison for {:?}", str_props); - return false; - } - let check = check - && g.at(t0) - .properties() - .temporal() - .iter_latest() - .map(|(k, v)| (k.clone(), v)) - .collect::>() - == t0_props; - if !check { - error!("failed latest value comparison for {:?} at t0", str_props); - return false; - } - let check = check - && t1_props.iter().all(|(k, ve)| { - g.at(t1) + let check = t0_props.iter().all(|(name, value)| { + g.properties().temporal().get(name).unwrap().at(t0) == Some(value.clone()) + }) && t1_props.iter().all(|(name, value)| { + g.properties().temporal().get(name).unwrap().at(t1) == Some(value.clone()) + }); + if !check { + error!("failed time-specific comparison for {:?}", str_props); + prop_assert!(false); + } + let check = check + && g.at(t0) .properties() .temporal() - .get(k) - .and_then(|v| v.latest()) - == Some(ve.clone()) - }); - if !check { - error!("failed latest value comparison for {:?} at t1", str_props); - return false; - } - check + .iter_latest() + .map(|(k, v)| (k.clone(), v)) + .collect::>() + == t0_props; + if !check { + error!("failed latest value comparison for {:?} at t0", str_props); + prop_assert!(false); + } + let check = check + && t1_props.iter().all(|(k, ve)| { + g.at(t1) + .properties() + .temporal() + .get(k) + .and_then(|v| v.latest()) + == Some(ve.clone()) + }); + if !check { + error!("failed latest value comparison for {:?} at t1", str_props); + prop_assert!(false); + } + prop_assert!(check); + }); } #[test] @@ -3131,17 +3150,19 @@ mod db_tests { }); } - #[quickcheck] - fn node_from_id_is_consistent(nodes: Vec) -> bool { - let g = Graph::new(); - for v in nodes.iter() { - g.add_node(0, *v, NO_PROPS, None).unwrap(); - } - g.nodes() - .name() - .into_iter_values() - .map(|name| g.node(name)) - .all(|v| v.is_some()) + #[test] + fn node_from_id_is_consistent() { + proptest!(|(nodes: Vec)| { + let g = Graph::new(); + for v in nodes.iter() { + g.add_node(0, *v, NO_PROPS, None).unwrap(); + } + prop_assert!(g.nodes() + .name() + .into_iter_values() + .map(|name| g.node(name)) + .all(|v| v.is_some())); + }); } #[test] @@ -3158,9 +3179,11 @@ mod db_tests { .all(|v| v.is_some())) } - #[quickcheck] - fn exploded_edge_times_is_consistent(edges: Vec<(u64, u64, Vec)>, offset: i64) -> bool { - check_exploded_edge_times_is_consistent(edges, offset) + #[test] + fn exploded_edge_times_is_consistent() { + proptest!(|(edges: Vec<(u64, u64, Vec)>, offset: i64)| { + prop_assert!(check_exploded_edge_times_is_consistent(edges, offset)); + }); } #[test] @@ -4142,12 +4165,12 @@ mod db_tests { nodes: NodeFixture::default(), edges, }; - for _ in 0..200 { - let g = Graph::from(build_graph(&graph_f)); - let gw = g.window(w.start, w.end); - let gmw = gw.materialize()?; - assert_graph_equal(&gw, &gmw); - } + + let g = Graph::from(build_graph(&graph_f)); + let gw = g.window(w.start, w.end); + let gmw = gw.materialize()?; + assert_graph_equal(&gw, &gmw); + Ok(()) } diff --git a/raphtory/src/db/graph/views/window_graph.rs b/raphtory/src/db/graph/views/window_graph.rs index 1c3ad4aa88..0c564e51f9 100644 --- a/raphtory/src/db/graph/views/window_graph.rs +++ b/raphtory/src/db/graph/views/window_graph.rs @@ -568,9 +568,8 @@ mod views_test { db::graph::graph::assert_graph_equal, prelude::*, test_storage, test_utils::test_graph, }; use itertools::Itertools; - use quickcheck::TestResult; - use quickcheck_macros::quickcheck; - use rand::prelude::*; + use proptest::prelude::*; + use rand::{prelude::*, Rng}; use raphtory_api::core::{entities::GID, utils::logging::global_info_logger}; use rayon::prelude::*; #[cfg(feature = "storage")] @@ -689,51 +688,41 @@ mod views_test { }); } - #[quickcheck] - fn windowed_graph_has_node(mut vs: Vec<(i64, u64)>) -> TestResult { - global_info_logger(); - if vs.is_empty() { - return TestResult::discard(); - } + #[test] + fn windowed_graph_has_node() { + proptest!(|(mut vs: Vec<(i64, u64)>)| { + global_info_logger(); + prop_assume!(!vs.is_empty()); - vs.sort_by_key(|v| v.1); // Sorted by node - vs.dedup_by_key(|v| v.1); // Have each node only once to avoid headaches - vs.sort_by_key(|v| v.0); // Sorted by time + vs.sort_by_key(|v| v.1); // Sorted by node + vs.dedup_by_key(|v| v.1); // Have each node only once to avoid headaches + vs.sort_by_key(|v| v.0); // Sorted by time - let rand_start_index = thread_rng().gen_range(0..vs.len()); - let rand_end_index = thread_rng().gen_range(rand_start_index..vs.len()); + let rand_start_index = thread_rng().gen_range(0..vs.len()); + let rand_end_index = thread_rng().gen_range(rand_start_index..vs.len()); - let g = Graph::new(); + let g = Graph::new(); - for (t, v) in &vs { - g.add_node(*t, *v, NO_PROPS, None) - .map_err(|err| error!("{:?}", err)) - .ok(); - } + for (t, v) in &vs { + g.add_node(*t, *v, NO_PROPS, None) + .map_err(|err| error!("{:?}", err)) + .ok(); + } - let start = vs.get(rand_start_index).expect("start index in range").0; - let end = vs.get(rand_end_index).expect("end index in range").0; + let start = vs.get(rand_start_index).expect("start index in range").0; + let end = vs.get(rand_end_index).expect("end index in range").0; - let wg = g.window(start, end); + let wg = g.window(start, end); - let rand_test_index: usize = thread_rng().gen_range(0..vs.len()); + let rand_test_index: usize = thread_rng().gen_range(0..vs.len()); - let (i, v) = vs.get(rand_test_index).expect("test index in range"); - if (start..end).contains(i) { - if wg.has_node(*v) { - TestResult::passed() + let (i, v) = vs.get(rand_test_index).expect("test index in range"); + if (start..end).contains(i) { + prop_assert!(wg.has_node(*v), "Node {:?} was not in window {:?}", (i, v), start..end); } else { - TestResult::error(format!( - "Node {:?} was not in window {:?}", - (i, v), - start..end - )) + prop_assert!(!wg.has_node(*v), "Node {:?} was in window {:?}", (i, v), start..end); } - } else if !wg.has_node(*v) { - TestResult::passed() - } else { - TestResult::error(format!("Node {:?} was in window {:?}", (i, v), start..end)) - } + }); } // FIXME: Issue #46 @@ -785,158 +774,141 @@ mod views_test { // TestResult::error(format!("Node {:?} was in window {:?}", (i, v), start..end)) // } // } - #[quickcheck] - fn windowed_graph_has_edge(mut edges: Vec<(i64, (u64, u64))>) -> TestResult { - if edges.is_empty() { - return TestResult::discard(); - } + #[test] + fn windowed_graph_has_edge() { + proptest!(|(mut edges: Vec<(i64, (u64, u64))>)| { + prop_assume!(!edges.is_empty()); - edges.sort_by_key(|e| e.1); // Sorted by edge - edges.dedup_by_key(|e| e.1); // Have each edge only once to avoid headaches - edges.sort_by_key(|e| e.0); // Sorted by time + edges.sort_by_key(|e| e.1); // Sorted by edge + edges.dedup_by_key(|e| e.1); // Have each edge only once to avoid headaches + edges.sort_by_key(|e| e.0); // Sorted by time - let rand_start_index = thread_rng().gen_range(0..edges.len()); - let rand_end_index = thread_rng().gen_range(rand_start_index..edges.len()); + let rand_start_index = thread_rng().gen_range(0..edges.len()); + let rand_end_index = thread_rng().gen_range(rand_start_index..edges.len()); - let g = Graph::new(); + let g = Graph::new(); - for (t, e) in &edges { - g.add_edge(*t, e.0, e.1, NO_PROPS, None).unwrap(); - } + for (t, e) in &edges { + g.add_edge(*t, e.0, e.1, NO_PROPS, None).unwrap(); + } - let start = edges.get(rand_start_index).expect("start index in range").0; - let end = edges.get(rand_end_index).expect("end index in range").0; + let start = edges.get(rand_start_index).expect("start index in range").0; + let end = edges.get(rand_end_index).expect("end index in range").0; - let wg = g.window(start, end); + let wg = g.window(start, end); - let rand_test_index: usize = thread_rng().gen_range(0..edges.len()); + let rand_test_index: usize = thread_rng().gen_range(0..edges.len()); - let (i, e) = edges.get(rand_test_index).expect("test index in range"); - if (start..end).contains(i) { - if wg.has_edge(e.0, e.1) { - TestResult::passed() + let (i, e) = edges.get(rand_test_index).expect("test index in range"); + if (start..end).contains(i) { + prop_assert!(wg.has_edge(e.0, e.1), "Edge {:?} was not in window {:?}", (i, e), start..end); } else { - TestResult::error(format!( - "Edge {:?} was not in window {:?}", - (i, e), - start..end - )) + prop_assert!(!wg.has_edge(e.0, e.1), "Edge {:?} was in window {:?}", (i, e), start..end); } - } else if !wg.has_edge(e.0, e.1) { - TestResult::passed() - } else { - TestResult::error(format!("Edge {:?} was in window {:?}", (i, e), start..end)) - } + }); } #[cfg(feature = "storage")] - #[quickcheck] - fn windowed_disk_graph_has_edge(mut edges: Vec<(i64, (u64, u64))>) -> TestResult { - if edges.is_empty() { - return TestResult::discard(); - } + #[test] + fn windowed_disk_graph_has_edge() { + proptest!(|(mut edges: Vec<(i64, (u64, u64))>)| { + prop_assume!(!edges.is_empty()); - edges.sort_by_key(|e| e.1); // Sorted by edge - edges.dedup_by_key(|e| e.1); // Have each edge only once to avoid headaches - edges.sort_by_key(|e| e.0); // Sorted by time + edges.sort_by_key(|e| e.1); // Sorted by edge + edges.dedup_by_key(|e| e.1); // Have each edge only once to avoid headaches + edges.sort_by_key(|e| e.0); // Sorted by time - let rand_start_index = thread_rng().gen_range(0..edges.len()); - let rand_end_index = thread_rng().gen_range(rand_start_index..edges.len()); + let rand_start_index = thread_rng().gen_range(0..edges.len()); + let rand_end_index = thread_rng().gen_range(rand_start_index..edges.len()); - let g = Graph::new(); + let g = Graph::new(); - for (t, e) in &edges { - g.add_edge(*t, e.0, e.1, NO_PROPS, None).unwrap(); - } - let test_dir = TempDir::new().unwrap(); - let g = g.persist_as_disk_graph(test_dir.path()).unwrap(); + for (t, e) in &edges { + g.add_edge(*t, e.0, e.1, NO_PROPS, None).unwrap(); + } - let start = edges.get(rand_start_index).expect("start index in range").0; - let end = edges.get(rand_end_index).expect("end index in range").0; + let test_dir = TempDir::new().unwrap(); + let disk_graph = g.persist_as_disk_graph(test_dir.path()).unwrap(); - let wg = g.window(start, end); + let start = edges.get(rand_start_index).expect("start index in range").0; + let end = edges.get(rand_end_index).expect("end index in range").0; - let rand_test_index: usize = thread_rng().gen_range(0..edges.len()); + let wg = disk_graph.window(start, end); - let (i, e) = edges.get(rand_test_index).expect("test index in range"); - if (start..end).contains(i) { - if wg.has_edge(e.0, e.1) { - TestResult::passed() + let rand_test_index: usize = thread_rng().gen_range(0..edges.len()); + + let (i, e) = edges.get(rand_test_index).expect("test index in range"); + if (start..end).contains(i) { + prop_assert!(wg.has_edge(e.0, e.1), "Edge {:?} was not in window {:?}", (i, e), start..end); } else { - TestResult::error(format!( - "Edge {:?} was not in window {:?}", - (i, e), - start..end - )) + prop_assert!(!wg.has_edge(e.0, e.1), "Edge {:?} was in window {:?}", (i, e), start..end); } - } else if !wg.has_edge(e.0, e.1) { - TestResult::passed() - } else { - TestResult::error(format!("Edge {:?} was in window {:?}", (i, e), start..end)) - } + }); } - #[quickcheck] - fn windowed_graph_edge_count( - mut edges: Vec<(i64, (u64, u64))>, - window: Range, - ) -> TestResult { - global_info_logger(); - if window.end < window.start { - return TestResult::discard(); - } - edges.sort_by_key(|e| e.1); // Sorted by edge - edges.dedup_by_key(|e| e.1); // Have each edge only once to avoid headaches - - let true_edge_count = edges.iter().filter(|e| window.contains(&e.0)).count(); + #[test] + fn windowed_graph_edge_count() { + proptest!(|(mut edges: Vec<(i64, (u64, u64))>, window: Range)| { + global_info_logger(); + prop_assume!(window.end >= window.start); - let g = Graph::new(); + edges.sort_by_key(|e| e.1); // Sorted by edge + edges.dedup_by_key(|e| e.1); // Have each edge only once to avoid headaches - for (t, e) in &edges { - g.add_edge(*t, e.0, e.1, [("test".to_owned(), Prop::Bool(true))], None) - .unwrap(); - } + let true_edge_count = edges.iter().filter(|e| window.contains(&e.0)).count(); - let wg = g.window(window.start, window.end); - if wg.count_edges() != true_edge_count { - info!( - "failed, g.num_edges() = {}, true count = {}", - wg.count_edges(), - true_edge_count - ); - info!("g.edges() = {:?}", wg.edges().iter().collect_vec()); - } - TestResult::from_bool(wg.count_edges() == true_edge_count) - } + let g = Graph::new(); - #[quickcheck] - fn trivial_window_has_all_edges(edges: Vec<(i64, u64, u64)>) -> bool { - let g = Graph::new(); - edges - .into_par_iter() - .filter(|e| e.0 < i64::MAX) - .for_each(|(t, src, dst)| { - g.add_edge(t, src, dst, [("test".to_owned(), Prop::Bool(true))], None) + for (t, e) in &edges { + g.add_edge(*t, e.0, e.1, [("test".to_owned(), Prop::Bool(true))], None) .unwrap(); - }); - let w = g.window(i64::MIN, i64::MAX); - g.edges() - .iter() - .all(|e| w.has_edge(e.src().id(), e.dst().id())) + } + + let wg = g.window(window.start, window.end); + if wg.count_edges() != true_edge_count { + info!( + "failed, g.num_edges() = {}, true count = {}", + wg.count_edges(), + true_edge_count + ); + info!("g.edges() = {:?}", wg.edges().iter().collect_vec()); + } + prop_assert_eq!(wg.count_edges(), true_edge_count); + }); } - #[quickcheck] - fn large_node_in_window(dsts: Vec) -> bool { - let dsts: Vec = dsts.into_iter().unique().collect(); - let n = dsts.len(); - let g = Graph::new(); + #[test] + fn trivial_window_has_all_edges() { + proptest!(|(edges: Vec<(i64, u64, u64)>)| { + let g = Graph::new(); + edges + .into_par_iter() + .filter(|e| e.0 < i64::MAX) + .for_each(|(t, src, dst)| { + g.add_edge(t, src, dst, [("test".to_owned(), Prop::Bool(true))], None) + .unwrap(); + }); + let w = g.window(i64::MIN, i64::MAX); + prop_assert!(g.edges() + .iter() + .all(|e| w.has_edge(e.src().id(), e.dst().id()))); + }); + } - for dst in dsts { - let t = 1; - g.add_edge(t, 0, dst, NO_PROPS, None).unwrap(); - } - let w = g.window(i64::MIN, i64::MAX); - w.count_edges() == n + #[test] + fn large_node_in_window() { + proptest!(|(dsts: Vec)| { + let dsts: Vec = dsts.into_iter().unique().collect(); + let n = dsts.len(); + let g = Graph::new(); + + for dst in dsts { + let t = 1; + g.add_edge(t, 0, dst, NO_PROPS, None).unwrap(); + } + let w = g.window(i64::MIN, i64::MAX); + prop_assert_eq!(w.count_edges(), n); + }); } #[test] diff --git a/raphtory/src/serialise/parquet/mod.rs b/raphtory/src/serialise/parquet/mod.rs index d27e45a82f..cac7787d15 100644 --- a/raphtory/src/serialise/parquet/mod.rs +++ b/raphtory/src/serialise/parquet/mod.rs @@ -266,8 +266,7 @@ fn decode_graph_storage( if g_type != expected_gt { return Err(GraphError::LoadFailure(format!( - "Expected graph type {:?}, got {:?}", - expected_gt, g_type + "Expected graph type {expected_gt:?}, got {g_type:?}" ))); } From 7352098f573a7907f294bba0857159e6ffd98260 Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Thu, 10 Jul 2025 14:31:06 +0100 Subject: [PATCH 076/321] more fixes for updating properties --- db4-graph/Cargo.toml | 1 + db4-graph/src/lib.rs | 2 +- db4-storage/src/api/nodes.rs | 4 +- db4-storage/src/lib.rs | 10 ++-- db4-storage/src/pages/mod.rs | 39 ++++++-------- db4-storage/src/pages/session.rs | 31 +++++------ db4-storage/src/pages/test_utils/checkers.rs | 12 +---- db4-storage/src/pages/test_utils/props.rs | 7 +-- db4-storage/src/persist/strategy.rs | 6 +-- db4-storage/src/resolver/mapping_resolver.rs | 3 +- db4-storage/src/segments/edge.rs | 9 ++-- db4-storage/src/segments/mod.rs | 2 +- db4-storage/src/segments/node.rs | 9 ++-- raphtory-storage/src/mutation/addition_ops.rs | 9 +++- .../src/mutation/addition_ops_ext.rs | 46 ++++++---------- raphtory/src/db/api/mutation/addition_ops.rs | 11 +++- raphtory/src/db/api/storage/storage.rs | 13 ++++- .../src/db/api/view/edge_property_filter.rs | 2 +- raphtory/src/db/graph/edge.rs | 53 +++++++++++++------ raphtory/src/db/graph/graph.rs | 22 +++++--- raphtory/src/db/graph/node.rs | 28 ++++++---- 21 files changed, 171 insertions(+), 148 deletions(-) diff --git a/db4-graph/Cargo.toml b/db4-graph/Cargo.toml index 3ac8bd6f93..3a7c044dfc 100644 --- a/db4-graph/Cargo.toml +++ b/db4-graph/Cargo.toml @@ -18,3 +18,4 @@ raphtory-api.workspace = true raphtory-core.workspace = true parking_lot.workspace = true uuid.workspace = true +tempfile.workspace = true diff --git a/db4-graph/src/lib.rs b/db4-graph/src/lib.rs index 36fe731f32..684e75afd2 100644 --- a/db4-graph/src/lib.rs +++ b/db4-graph/src/lib.rs @@ -76,7 +76,7 @@ impl, ES = ES>> TemporalGraph { ) }); let gid_resolver_dir = graph_dir.join("gid_resolver"); - let storage = Layer::new_with_meta( + let storage: Layer = Layer::new_with_meta( graph_dir.clone(), DEFAULT_MAX_PAGE_LEN_NODES, DEFAULT_MAX_PAGE_LEN_EDGES, diff --git a/db4-storage/src/api/nodes.rs b/db4-storage/src/api/nodes.rs index d00df2deca..1ed6374014 100644 --- a/db4-storage/src/api/nodes.rs +++ b/db4-storage/src/api/nodes.rs @@ -186,10 +186,10 @@ pub trait NodeRefOps<'a>: Copy + Clone + Send + Sync + 'a { .map(move |(v, e)| EdgeRef::new_incoming(e, v, src_pid)), ), Direction::BOTH => Iter3::K( - self.out_edges(layer_id) + self.out_edges_sorted(layer_id) .map(move |(v, e)| EdgeRef::new_outgoing(e, src_pid, v)) .merge_by( - self.inb_edges(layer_id) + self.inb_edges_sorted(layer_id) .map(move |(v, e)| EdgeRef::new_incoming(e, v, src_pid)), |e1, e2| e1.remote() < e2.remote(), ) diff --git a/db4-storage/src/lib.rs b/db4-storage/src/lib.rs index 9651300f18..7c4ad1d45f 100644 --- a/db4-storage/src/lib.rs +++ b/db4-storage/src/lib.rs @@ -29,19 +29,19 @@ pub mod persist; pub mod properties; pub mod resolver; pub mod segments; -pub mod wal; pub mod utils; +pub mod wal; pub type Extension = (); pub type NS

= NodeSegmentView

; pub type ES

= EdgeSegmentView

; -pub type Layer = GraphStore, EdgeSegmentView, EXT>; +pub type Layer

= GraphStore, ES

, P>; pub type GIDResolver = MappingResolver; -pub type ReadLockedLayer = ReadLockedGraphStore; -pub type ReadLockedNodes

= ReadLockedNodeStorage; -pub type ReadLockedEdges

= ReadLockedEdgeStorage; +pub type ReadLockedLayer

= ReadLockedGraphStore, ES

, P>; +pub type ReadLockedNodes

= ReadLockedNodeStorage, P>; +pub type ReadLockedEdges

= ReadLockedEdgeStorage, P>; pub type NodeEntry<'a> = MemNodeEntry<'a, parking_lot::RwLockReadGuard<'a, MemNodeSegment>>; pub type EdgeEntry<'a> = MemEdgeEntry<'a, parking_lot::RwLockReadGuard<'a, MemEdgeSegment>>; diff --git a/db4-storage/src/pages/mod.rs b/db4-storage/src/pages/mod.rs index 37939cb231..a707aeeeb7 100644 --- a/db4-storage/src/pages/mod.rs +++ b/db4-storage/src/pages/mod.rs @@ -1,5 +1,4 @@ use std::{ - ops::DerefMut, path::Path, sync::{ Arc, @@ -9,7 +8,10 @@ use std::{ use crate::{ LocalPOS, - api::{edges::EdgeSegmentOps, nodes::NodeSegmentOps}, + api::{ + edges::EdgeSegmentOps, + nodes::{NodeEntryOps, NodeRefOps, NodeSegmentOps}, + }, error::DBV4Error, pages::{edge_store::ReadLockedEdgeStorage, node_store::ReadLockedNodeStorage}, properties::props_meta_writer::PropsMetaWriter, @@ -17,6 +19,7 @@ use crate::{ }; use edge_page::writer::EdgeWriter; use edge_store::EdgeStorageInner; +use either::Either; use node_page::writer::{NodeWriter, WriterPair}; use node_store::NodeStorageInner; use parking_lot::RwLockWriteGuard; @@ -49,6 +52,8 @@ pub mod test_utils; pub const NODE_ID_PROP_KEY: &str = "_raphtory_node_id"; pub const NODE_TYPE_PROP_KEY: &str = "_raphtory_node_type"; +// graph // (node/edges) // segment // layer_ids (0, 1, 2, ...) // actual graphy bits + #[derive(Debug)] pub struct GraphStore { nodes: Arc>, @@ -167,7 +172,7 @@ impl< let edges = Arc::new(EdgeStorageInner::new_with_meta( edges_path, max_page_len_edges, - edge_meta.into(), + edge_meta, ext.clone(), )); // Reserve node_type as a const prop on init @@ -247,16 +252,11 @@ impl< fn internal_delete_edge( &self, t: TimeIndexEntry, - src: impl Into, - dst: impl Into, + edge: Either<(VID, VID), EID>, + layer: usize, lsn: u64, ) -> Result<(), DBV4Error> { - let src = src.into(); - let dst = dst.into(); - let mut session = self.write_session(src, dst, None); - todo!("Implement internal_delete_edge"); - // session.internal_delete_edge(t, src, dst, lsn, 0)?; - Ok(()) + todo!() } fn as_time_index_entry(&self, t: T) -> Result { @@ -415,7 +415,7 @@ pub fn resolve_pos>(i: I, max_page_len: usize) -> (usize, mod test { use super::GraphStore; use crate::{ - Layer, + Extension, Layer, api::nodes::{NodeEntryOps, NodeRefOps}, pages::test_utils::{ AddEdge, Fixture, NodeFixture, check_edges_support, check_graph_with_nodes_support, @@ -423,16 +423,11 @@ mod test { make_nodes, }, }; - use arrow::ipc::Time; - use bitvec::vec; use chrono::{DateTime, NaiveDateTime, Utc}; use core::panic; use proptest::prelude::*; use raphtory_api::core::entities::properties::prop::Prop; - use raphtory_core::{ - entities::{LayerIds, VID}, - storage::timeindex::{TimeIndexEntry, TimeIndexOps}, - }; + use raphtory_core::{entities::VID, storage::timeindex::TimeIndexOps}; fn check_edges( edges: Vec<(impl Into, impl Into)>, @@ -447,7 +442,7 @@ mod test { .collect(); check_edges_support(edges, par_load, false, |graph_dir| { - Layer::new(graph_dir, chunk_size, chunk_size) + Layer::::new(graph_dir, chunk_size, chunk_size) }) } @@ -457,7 +452,7 @@ mod test { par_load: bool, ) { check_edges_support(edges, par_load, false, |graph_dir| { - Layer::new(graph_dir, chunk_size, chunk_size) + Layer::::new(graph_dir, chunk_size, chunk_size) }) } @@ -529,7 +524,7 @@ mod test { #[test] fn test_add_one_edge_get_num_nodes() { let graph_dir = tempfile::tempdir().unwrap(); - let g = Layer::new(graph_dir.path(), 32, 32); + let g = Layer::::new(graph_dir.path(), 32, 32); g.add_edge(4, 7, 3).unwrap(); assert_eq!(g.nodes().num_nodes(), 2); } @@ -579,7 +574,7 @@ mod test { #[test] fn node_temporal_props() { let graph_dir = tempfile::tempdir().unwrap(); - let g = Layer::new(graph_dir.path(), 32, 32); + let g = Layer::::new(graph_dir.path(), 32, 32); g.add_node_props::(1, 0, 0, vec![]) .expect("Failed to add node props"); g.add_node_props::(2, 0, 0, vec![]) diff --git a/db4-storage/src/pages/session.rs b/db4-storage/src/pages/session.rs index 956512a89f..db19724633 100644 --- a/db4-storage/src/pages/session.rs +++ b/db4-storage/src/pages/session.rs @@ -1,31 +1,20 @@ -use std::ops::DerefMut; - use super::{ GraphStore, edge_page::writer::EdgeWriter, node_page::writer::WriterPair, resolve_pos, }; use crate::{ LocalPOS, api::{edges::EdgeSegmentOps, nodes::NodeSegmentOps}, - error::DBV4Error, - pages::{NODE_ID_PROP_KEY, NODE_TYPE_PROP_KEY, node_page::writer::NodeWriter}, + pages::NODE_ID_PROP_KEY, segments::{edge::MemEdgeSegment, node::MemNodeSegment}, }; use parking_lot::RwLockWriteGuard; -use raphtory_api::core::{ - entities::properties::prop::{Prop, PropType}, - storage::dict_mapper::MaybeNew, -}; +use raphtory_api::core::{entities::properties::prop::Prop, storage::dict_mapper::MaybeNew}; use raphtory_core::{ - entities::{EID, ELID, GidRef, VID}, + entities::{EID, ELID, VID}, storage::timeindex::AsTime, }; -pub struct WriteSession< - 'a, - NS: NodeSegmentOps, - ES: EdgeSegmentOps, - EXT, -> { +pub struct WriteSession<'a, NS: NodeSegmentOps, ES: EdgeSegmentOps, EXT> { node_writers: WriterPair<'a, RwLockWriteGuard<'a, MemNodeSegment>, NS>, edge_writer: Option, ES>>, graph: &'a GraphStore, @@ -219,13 +208,15 @@ impl< .get_mut_src() .get_out_edge(src_pos, dst, layer_id) { - let mut edge_writer = self.graph.edge_writer(e_id); + // If edge_writer is not set, we need to create a new one + if self.edge_writer.is_none() { + self.edge_writer = Some(self.graph.edge_writer(e_id)); + } + let edge_writer = self.edge_writer.as_mut().unwrap(); let (_, edge_pos) = self.graph.edges().resolve_pos(e_id); edge_writer.add_static_edge(Some(edge_pos), src, dst, layer_id, lsn, None); - self.edge_writer = Some(edge_writer); // Attach edge_writer to hold onto locks - MaybeNew::Existing(e_id) } else { let mut edge_writer = self.graph.get_free_writer(); @@ -321,7 +312,9 @@ impl< } } - pub fn node_writers(&mut self) -> &mut WriterPair<'a, MNS, NS> { + pub fn node_writers( + &mut self, + ) -> &mut WriterPair<'a, RwLockWriteGuard<'a, MemNodeSegment>, NS> { &mut self.node_writers } } diff --git a/db4-storage/src/pages/test_utils/checkers.rs b/db4-storage/src/pages/test_utils/checkers.rs index 7c0442c871..631dfd0acf 100644 --- a/db4-storage/src/pages/test_utils/checkers.rs +++ b/db4-storage/src/pages/test_utils/checkers.rs @@ -163,18 +163,10 @@ pub fn check_edges_support< let adj = entry.as_ref(); let out_nbrs: Vec<_> = adj.out_nbrs_sorted(layer_id).collect(); - assert_eq!( - out_nbrs, exp_out, - "{stage} node: {:?} layer: {}", - n, layer_id - ); + assert_eq!(out_nbrs, exp_out, "{stage} node: {n:?} layer: {layer_id}"); let in_nbrs: Vec<_> = adj.inb_nbrs_sorted(layer_id).collect(); - assert_eq!( - in_nbrs, exp_inb, - "{stage} node: {:?} layer: {}", - n, layer_id - ); + assert_eq!(in_nbrs, exp_inb, "{stage} node: {n:?} layer: {layer_id}"); for (exp_dst, eid) in adj.out_edges(layer_id) { let elid = ELID::new(eid, layer_id); diff --git a/db4-storage/src/pages/test_utils/props.rs b/db4-storage/src/pages/test_utils/props.rs index e08823a9ce..28b63f2981 100644 --- a/db4-storage/src/pages/test_utils/props.rs +++ b/db4-storage/src/pages/test_utils/props.rs @@ -1,11 +1,8 @@ use bigdecimal::BigDecimal; use chrono::{DateTime, NaiveDateTime, Utc}; use itertools::Itertools; -use proptest::{collection, prelude::*}; -use raphtory_api::core::entities::properties::{ - prop::{DECIMAL_MAX, Prop, PropType}, - tprop::TPropOps, -}; +use proptest::prelude::*; +use raphtory_api::core::entities::properties::prop::{DECIMAL_MAX, Prop, PropType}; use std::collections::HashMap; pub fn prop_type() -> impl Strategy { diff --git a/db4-storage/src/persist/strategy.rs b/db4-storage/src/persist/strategy.rs index 5449e3344f..173f3b228d 100644 --- a/db4-storage/src/persist/strategy.rs +++ b/db4-storage/src/persist/strategy.rs @@ -2,7 +2,7 @@ use std::ops::DerefMut; use crate::segments::{ edge::{EdgeSegmentView, MemEdgeSegment}, - node::MemNodeSegment, + node::{MemNodeSegment, NodeSegmentView}, }; pub trait PersistentStrategy: Default + Clone + std::fmt::Debug + Send + Sync + 'static { @@ -23,8 +23,8 @@ pub trait PersistentStrategy: Default + Clone + std::fmt::Debug + Send + Sync + } impl PersistentStrategy for () { - type ES = EdgeSegmentView; - type NS = MemNodeSegment; + type ES = EdgeSegmentView; + type NS = NodeSegmentView; fn persist_node_page>( &self, diff --git a/db4-storage/src/resolver/mapping_resolver.rs b/db4-storage/src/resolver/mapping_resolver.rs index 3ed168b6ce..1542b873c8 100644 --- a/db4-storage/src/resolver/mapping_resolver.rs +++ b/db4-storage/src/resolver/mapping_resolver.rs @@ -44,8 +44,7 @@ impl GIDResolverOps for MappingResolver { &self, gids: impl IntoIterator>, ) -> Result<(), GIDResolverError> { - let result = self.mapping.validate_gids(gids)?; - Ok(result) + Ok(self.mapping.validate_gids(gids)?) } fn get_str(&self, gid: &str) -> Option { diff --git a/db4-storage/src/segments/edge.rs b/db4-storage/src/segments/edge.rs index f95c38a106..4cd8157430 100644 --- a/db4-storage/src/segments/edge.rs +++ b/db4-storage/src/segments/edge.rs @@ -22,6 +22,7 @@ use crate::{ LocalPOS, api::edges::{EdgeSegmentOps, LockedESegment}, error::DBV4Error, + persist::strategy::PersistentStrategy, properties::PropMutEntry, segments::edge_entry::MemEdgeRef, utils::Iter4, @@ -288,7 +289,7 @@ impl MemEdgeSegment { // Update EdgeSegmentView implementation to use multiple layers #[derive(Debug)] -pub struct EdgeSegmentView { +pub struct EdgeSegmentView { segment: Arc>, segment_id: usize, num_edges: AtomicUsize, @@ -368,8 +369,8 @@ impl LockedESegment for ArcLockedSegmentView { } } -impl EdgeSegmentOps for EdgeSegmentView { - type Extension = (); +impl>> EdgeSegmentOps for EdgeSegmentView

{ + type Extension = P; type Entry<'a> = MemEdgeEntry<'a, parking_lot::RwLockReadGuard<'a, MemEdgeSegment>>; @@ -412,7 +413,7 @@ impl EdgeSegmentOps for EdgeSegmentView { .into(), segment_id: page_id, num_edges: AtomicUsize::new(0), - _ext: (), + _ext, } } diff --git a/db4-storage/src/segments/mod.rs b/db4-storage/src/segments/mod.rs index 00a74de578..de05830c06 100644 --- a/db4-storage/src/segments/mod.rs +++ b/db4-storage/src/segments/mod.rs @@ -85,7 +85,7 @@ impl SegmentContainer { } pub fn set_item(&mut self, item_pos: LocalPOS) { - self.items.set(item_pos.0 as usize, true); + self.items.set(item_pos.0, true); } pub fn max_page_len(&self) -> usize { diff --git a/db4-storage/src/segments/node.rs b/db4-storage/src/segments/node.rs index 1c270e053b..07d32873a3 100644 --- a/db4-storage/src/segments/node.rs +++ b/db4-storage/src/segments/node.rs @@ -21,6 +21,7 @@ use crate::{ LocalPOS, api::nodes::{LockedNSSegment, NodeSegmentOps}, error::DBV4Error, + persist::strategy::PersistentStrategy, segments::node_entry::{MemNodeEntry, MemNodeRef}, }; @@ -302,7 +303,7 @@ impl MemNodeSegment { } #[derive(Debug)] -pub struct NodeSegmentView { +pub struct NodeSegmentView { inner: Arc>, segment_id: usize, _ext: EXT, @@ -322,8 +323,8 @@ impl LockedNSSegment for ArcLockedSegmentView { } } -impl NodeSegmentOps for NodeSegmentView { - type Extension = (); +impl>> NodeSegmentOps for NodeSegmentView

{ + type Extension = P; type Entry<'a> = MemNodeEntry<'a, parking_lot::RwLockReadGuard<'a, MemNodeSegment>>; @@ -365,7 +366,7 @@ impl NodeSegmentOps for NodeSegmentView { inner: parking_lot::RwLock::new(MemNodeSegment::new(page_id, max_page_len, meta)) .into(), segment_id: page_id, - _ext: (), + _ext, } } diff --git a/raphtory-storage/src/mutation/addition_ops.rs b/raphtory-storage/src/mutation/addition_ops.rs index 3b317fb976..16ec11c0f3 100644 --- a/raphtory-storage/src/mutation/addition_ops.rs +++ b/raphtory-storage/src/mutation/addition_ops.rs @@ -87,14 +87,21 @@ pub trait InternalAdditionOps { } pub trait EdgeWriteLock: Send + Sync { + fn internal_add_static_edge( + &mut self, + src: impl Into, + dst: impl Into, + lsn: u64, + ) -> MaybeNew; + /// add edge update fn internal_add_edge( &mut self, t: TimeIndexEntry, src: impl Into, dst: impl Into, + eid: MaybeNew, lsn: u64, - layer: usize, props: impl IntoIterator, ) -> MaybeNew; diff --git a/raphtory-storage/src/mutation/addition_ops_ext.rs b/raphtory-storage/src/mutation/addition_ops_ext.rs index 75ac6366e3..63207c4074 100644 --- a/raphtory-storage/src/mutation/addition_ops_ext.rs +++ b/raphtory-storage/src/mutation/addition_ops_ext.rs @@ -45,27 +45,25 @@ pub struct UnlockedSession<'a> { graph: &'a TemporalGraph, } -impl< - 'a, - EXT: PersistentStrategy, ES = ES>, - > EdgeWriteLock for WriteS<'a, EXT> -{ +impl<'a, EXT: PersistentStrategy, ES = ES>> EdgeWriteLock for WriteS<'a, EXT> { + fn internal_add_static_edge( + &mut self, + src: impl Into, + dst: impl Into, + lsn: u64, + ) -> MaybeNew { + self.static_session.add_static_edge(src, dst, lsn) + } + fn internal_add_edge( &mut self, t: TimeIndexEntry, src: impl Into, dst: impl Into, + eid: MaybeNew, lsn: u64, - layer: usize, props: impl IntoIterator, ) -> MaybeNew { - let src = src.into(); - let dst = dst.into(); - let eid = self - .static_session - .add_static_edge(src, dst, lsn) - .map(|eid| eid.with_layer(layer)); - self.static_session .add_edge_into_layer(t, src, dst, eid, lsn, props); @@ -248,21 +246,6 @@ impl InternalAdditionOps for TemporalGraph { let vid = self.resolve_node(id)?; let node_type_id = self.node_meta().get_or_create_node_type_id(node_type); Ok(vid.map(|_| (vid, node_type_id))) - - // let mut entry = self.storage.get_node_mut(vid.inner()); - // let mut entry_ref = entry.to_mut(); - // let node_store = entry_ref.node_store_mut(); - // if node_store.node_type == 0 { - // node_store.update_node_type(node_type_id.inner()); - // Ok(MaybeNew::New((vid, node_type_id))) - // } else { - // let node_type_id = self - // .node_meta - // .get_node_type_id(node_type) - // .filter(|&node_type| node_type == node_store.node_type) - // .ok_or(MutationError::NodeTypeError)?; - // Ok(MaybeNew::Existing((vid, MaybeNew::Existing(node_type_id)))) - // } } fn validate_gids<'a>( @@ -300,10 +283,11 @@ impl InternalAdditionOps for TemporalGraph { let v = v.into(); let (segment, node_pos) = self.storage().nodes().resolve_pos(v); let mut node_writer = self.storage().node_writer(segment); - let node_info = node_info_as_props(gid, node_type); node_writer.add_props(t, node_pos, 0, props, 0); - node_writer.update_c_props(node_pos, 0, node_info, 0); - + if gid.is_some() || node_type.is_some() { + let node_info = node_info_as_props(gid, node_type); + node_writer.update_c_props(node_pos, 0, node_info, 0); + } Ok(()) } diff --git a/raphtory/src/db/api/mutation/addition_ops.rs b/raphtory/src/db/api/mutation/addition_ops.rs index 291f7bb2ef..31d799ea81 100644 --- a/raphtory/src/db/api/mutation/addition_ops.rs +++ b/raphtory/src/db/api/mutation/addition_ops.rs @@ -307,7 +307,16 @@ impl> + StaticGraphViewOps> Addit let mut add_edge_op = self .atomic_add_edge(src_id, dst_id, None, layer_id) .map_err(into_graph_err)?; - let edge_id = add_edge_op.internal_add_edge(ti, src_id, dst_id, 0, layer_id, props); + + let edge_id = add_edge_op.internal_add_static_edge(src_id, dst_id, 0); + let edge_id = add_edge_op.internal_add_edge( + ti, + src_id, + dst_id, + edge_id.map(|eid| eid.with_layer(layer_id)), + 0, + props, + ); add_edge_op.store_src_node_info(src_id, src.as_node_ref().as_gid_ref().left()); add_edge_op.store_dst_node_info(dst_id, dst.as_node_ref().as_gid_ref().left()); diff --git a/raphtory/src/db/api/storage/storage.rs b/raphtory/src/db/api/storage/storage.rs index 6cf68a9265..8ef8ebcdba 100644 --- a/raphtory/src/db/api/storage/storage.rs +++ b/raphtory/src/db/api/storage/storage.rs @@ -207,12 +207,21 @@ impl EdgeWriteLock for AtomicAddEdgeSession<'_> { t: TimeIndexEntry, src: impl Into, dst: impl Into, + e_id: MaybeNew, lsn: u64, - layer: usize, props: impl IntoIterator, ) -> MaybeNew { self.session - .internal_add_edge(t, src, dst, lsn, layer, props) + .internal_add_edge(t, src, dst, e_id, lsn, props) + } + + fn internal_add_static_edge( + &mut self, + src: impl Into, + dst: impl Into, + lsn: u64, + ) -> MaybeNew { + self.session.internal_add_static_edge(src, dst, lsn) } fn internal_delete_edge( diff --git a/raphtory/src/db/api/view/edge_property_filter.rs b/raphtory/src/db/api/view/edge_property_filter.rs index 30b791b181..e40313d0a6 100644 --- a/raphtory/src/db/api/view/edge_property_filter.rs +++ b/raphtory/src/db/api/view/edge_property_filter.rs @@ -201,7 +201,7 @@ mod test { #[test] fn test_filter_eq() { proptest!(|( - edges in build_edge_list(100, 100), v in any::() + edges in build_edge_list(10, 10), v in any::() )| { let g = build_graph_from_edge_list(&edges); let filter = PropertyFilter::eq(PropertyRef::Property("int_prop".to_string()), v); diff --git a/raphtory/src/db/graph/edge.rs b/raphtory/src/db/graph/edge.rs index 76e12dec47..eb346019e9 100644 --- a/raphtory/src/db/graph/edge.rs +++ b/raphtory/src/db/graph/edge.rs @@ -11,9 +11,7 @@ use crate::{ }, db::{ api::{ - mutation::{ - time_from_input, time_from_input_session, CollectProperties, TryIntoInputTime, - }, + mutation::{time_from_input, time_from_input_session, TryIntoInputTime}, properties::{ internal::{ConstantPropertiesOps, TemporalPropertiesOps, TemporalPropertyViewOps}, Properties, @@ -32,13 +30,13 @@ use crate::{ use itertools::Itertools; use raphtory_api::core::{ entities::properties::prop::PropType, - storage::{arc_str::ArcStr, timeindex::TimeIndexEntry}, + storage::{arc_str::ArcStr, dict_mapper::MaybeNew, timeindex::TimeIndexEntry}, }; use raphtory_core::entities::graph::tgraph::InvalidLayer; use raphtory_storage::{ graph::edges::edge_storage_ops::EdgeStorageOps, mutation::{ - addition_ops::{InternalAdditionOps, SessionAdditionOps}, + addition_ops::{EdgeWriteLock, InternalAdditionOps, SessionAdditionOps}, deletion_ops::InternalDeletionOps, property_addition_ops::InternalPropertyAdditionOps, }, @@ -387,26 +385,49 @@ impl EdgeView { Ok(()) } - pub fn add_updates( + pub fn add_updates< + T: TryIntoInputTime, + PN: AsRef, + PI: Into, + PII: IntoIterator, + >( &self, time: T, - props: C, + props: PII, layer: Option<&str>, ) -> Result<(), GraphError> { let session = self.graph.write_session().map_err(into_graph_err)?; let t = time_from_input_session(&session, time)?; let layer_id = self.resolve_layer(layer, true)?; - let properties: Vec<(usize, Prop)> = props.collect_properties(|name, dtype| { - Ok(session - .resolve_edge_property(name, dtype, false) - .map_err(into_graph_err)? - .inner()) - })?; - - session - .internal_add_edge_update(t, self.edge.pid(), &properties, layer_id) + + let props = self + .graph + .validate_props( + false, + self.graph.edge_meta(), + props.into_iter().map(|(k, v)| (k, v.into())), + ) .map_err(into_graph_err)?; + + let src = self.src().node; + let dst = self.dst().node; + + let e_id = self.edge.pid(); + let mut writer = self + .graph + .atomic_add_edge(src, dst, Some(e_id), layer_id) + .map_err(into_graph_err)?; + + writer.internal_add_edge( + t, + src, + dst, + MaybeNew::New(e_id.with_layer(layer_id)), + 0, + props, + ); + Ok(()) } } diff --git a/raphtory/src/db/graph/graph.rs b/raphtory/src/db/graph/graph.rs index 45f5b3caf0..1e8a35b8de 100644 --- a/raphtory/src/db/graph/graph.rs +++ b/raphtory/src/db/graph/graph.rs @@ -596,7 +596,6 @@ mod db_tests { use chrono::NaiveDateTime; use itertools::Itertools; use proptest::prelude::*; - use proptest::{arbitrary::any, proptest}; use raphtory_api::core::{ entities::{GID, VID}, storage::{ @@ -610,7 +609,7 @@ mod db_tests { core_ops::CoreGraphOps, graph::nodes::node_storage_ops::NodeStorageOps, mutation::addition_ops::InternalAdditionOps, }; - use rayon::{join, vec}; + use rayon::join; use std::{ collections::{HashMap, HashSet}, ops::Range, @@ -3181,7 +3180,15 @@ mod db_tests { #[test] fn exploded_edge_times_is_consistent() { - proptest!(|(edges: Vec<(u64, u64, Vec)>, offset: i64)| { + let edges = proptest::collection::vec( + ( + 0u64..100, + 0u64..100, + proptest::collection::vec(-1000i64..1000i64, 1..40), + ), + 1..400, + ); + proptest!(|(edges in edges, offset in -1000i64..1000i64)| { prop_assert!(check_exploded_edge_times_is_consistent(edges, offset)); }); } @@ -3238,18 +3245,18 @@ mod db_tests { .map(|ee| { check( ee.earliest_time() == ee.latest_time(), - format!("times mismatched for {:?}", ee), + format!("times mismatched for {ee:?}"), ); // times are the same for exploded edge let t = ee.earliest_time().unwrap(); check( ee.at(t).is_active(), - format!("exploded edge {:?} inactive at {}", ee, t), + format!("exploded edge {ee:?} inactive at {t}"), ); let t_test = t.saturating_add(offset); if t_test != t && t_test < i64::MAX && t_test > i64::MIN { check( !ee.at(t_test).is_active(), - format!("exploded edge {:?} active at {}", ee, t_test), + format!("exploded edge {ee:?} active at {t_test}"), ); } t @@ -3266,8 +3273,7 @@ mod db_tests { check( actual_edges == edges, format!( - "actual edges didn't match input actual: {:?}, expected: {:?}", - actual_edges, edges + "actual edges didn't match input actual: {actual_edges:?}, expected: {edges:?}" ), ); correct diff --git a/raphtory/src/db/graph/node.rs b/raphtory/src/db/graph/node.rs index b8f5709a2a..68cd30cf0b 100644 --- a/raphtory/src/db/graph/node.rs +++ b/raphtory/src/db/graph/node.rs @@ -442,21 +442,29 @@ impl NodeView<'static .map_err(into_graph_err) } - pub fn add_updates( + pub fn add_updates< + T: TryIntoInputTime, + PN: AsRef, + PI: Into, + PII: IntoIterator, + >( &self, time: T, - props: C, + props: PII, ) -> Result<(), GraphError> { let session = self.graph.write_session().map_err(|err| err.into())?; let t = time_from_input_session(&session, time)?; - let properties: Vec<(usize, Prop)> = props.collect_properties(|name, dtype| { - Ok(session - .resolve_node_property(name, dtype, false) - .map_err(into_graph_err)? - .inner()) - })?; - session - .internal_add_node(t, self.node, &properties) + let props = self + .graph + .validate_props( + false, + self.graph.node_meta(), + props.into_iter().map(|(k, v)| (k, v.into())), + ) + .map_err(into_graph_err)?; + let vid = self.node; + self.graph + .internal_add_node(t, vid, None, None, props) .map_err(into_graph_err) } } From 7ace6984fe0521946441868ac1004057511c2cc4 Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Thu, 10 Jul 2025 17:01:45 +0100 Subject: [PATCH 077/321] add support for path in Graph, if non provided use a default that gets deleted --- db4-graph/src/lib.rs | 82 ++++++++++++++----- db4-storage/src/api/nodes.rs | 6 -- .../src/model/schema/node_schema.rs | 2 +- raphtory-storage/src/mutation/deletion_ops.rs | 8 +- raphtory/src/db/api/storage/storage.rs | 27 ++++-- raphtory/src/db/api/view/graph.rs | 2 +- raphtory/src/db/graph/graph.rs | 35 ++++++-- raphtory/src/python/graph/graph.rs | 8 +- 8 files changed, 124 insertions(+), 46 deletions(-) diff --git a/db4-graph/src/lib.rs b/db4-graph/src/lib.rs index 684e75afd2..238622c176 100644 --- a/db4-graph/src/lib.rs +++ b/db4-graph/src/lib.rs @@ -1,5 +1,4 @@ use std::{ - env::temp_dir, path::{Path, PathBuf}, sync::{ atomic::{self, AtomicUsize}, @@ -30,6 +29,7 @@ use storage::{ resolver::{GIDResolverError, GIDResolverOps}, Extension, GIDResolver, Layer, ReadLockedLayer, ES, NS, }; +use tempfile::TempDir; pub mod entries; pub mod mutation; @@ -44,40 +44,80 @@ pub struct TemporalGraph { pub node_count: AtomicUsize, storage: Arc>, pub graph_meta: Arc, - graph_dir: PathBuf, + graph_dir: GraphDir, } -impl Default for TemporalGraph { +#[derive(Debug)] +pub enum GraphDir { + Temp(TempDir), + Path(PathBuf), +} + +impl<'a> From<&'a Path> for GraphDir { + fn from(path: &'a Path) -> Self { + GraphDir::Path(path.to_path_buf()) + } +} + +impl Default for GraphDir { fn default() -> Self { - Self::new(None) + GraphDir::Temp(tempfile::tempdir().expect("Failed to create temporary directory")) } } -fn random_temp_dir() -> PathBuf { - temp_dir() - .join("raphtory_graphs") - .join(format!("raphtory-{}", uuid::Uuid::new_v4())) +impl AsRef for GraphDir { + fn as_ref(&self) -> &Path { + match self { + GraphDir::Temp(temp_dir) => temp_dir.path(), + GraphDir::Path(path) => path, + } + } +} + +impl Default for TemporalGraph { + fn default() -> Self { + Self::new() + } } impl, ES = ES>> TemporalGraph { - pub fn new(path: Option) -> Self { + pub fn new() -> Self { let node_meta = Meta::new(); let edge_meta = Meta::new(); - Self::new_with_meta(path, node_meta, edge_meta) + Self::new_with_meta(Default::default(), node_meta, edge_meta) } - pub fn new_with_meta(path: Option, node_meta: Meta, edge_meta: Meta) -> Self { - edge_meta.get_or_create_layer_id(Some("static_graph")); - let graph_dir = path.unwrap_or_else(random_temp_dir); - std::fs::create_dir_all(&graph_dir).unwrap_or_else(|_| { - panic!( - "Failed to create graph directory at {}", - graph_dir.display() - ) + pub fn new_with_path(path: impl AsRef) -> Self { + let node_meta = Meta::new(); + let edge_meta = Meta::new(); + Self::new_with_meta(path.as_ref().into(), node_meta, edge_meta) + } + + pub fn load_from_path(path: impl AsRef) -> Self { + let graph_dir: GraphDir = path.as_ref().into(); + let storage = Layer::load(graph_dir.as_ref()) + .unwrap_or_else(|_| panic!("Failed to load graph from path: {graph_dir:?}")); + let gid_resolver_dir = graph_dir.as_ref().join("gid_resolver"); + let resolver = GIDResolver::new(&gid_resolver_dir).unwrap_or_else(|_| { + panic!("Failed to load GID resolver from path: {gid_resolver_dir:?}") }); - let gid_resolver_dir = graph_dir.join("gid_resolver"); + let node_count = AtomicUsize::new(storage.nodes().num_nodes()); + Self { + graph_dir, + logical_to_physical: resolver, + node_count, + storage: Arc::new(storage), + graph_meta: Arc::new(GraphMeta::default()), + } + } + + pub fn new_with_meta(graph_dir: GraphDir, node_meta: Meta, edge_meta: Meta) -> Self { + edge_meta.get_or_create_layer_id(Some("static_graph")); + std::fs::create_dir_all(&graph_dir) + .unwrap_or_else(|_| panic!("Failed to create graph directory at {graph_dir:?}")); + let gid_resolver_dir = graph_dir.as_ref().join("gid_resolver"); let storage: Layer = Layer::new_with_meta( - graph_dir.clone(), + graph_dir.as_ref(), DEFAULT_MAX_PAGE_LEN_NODES, DEFAULT_MAX_PAGE_LEN_EDGES, node_meta, @@ -147,7 +187,7 @@ impl, ES = ES>> TemporalGraph { } pub fn graph_dir(&self) -> &Path { - &self.graph_dir + self.graph_dir.as_ref() } #[inline] diff --git a/db4-storage/src/api/nodes.rs b/db4-storage/src/api/nodes.rs index 1ed6374014..584ceda33c 100644 --- a/db4-storage/src/api/nodes.rs +++ b/db4-storage/src/api/nodes.rs @@ -114,12 +114,6 @@ pub trait LockedNSSegment: std::fmt::Debug + Send + Sync { fn entry_ref<'a>(&'a self, pos: impl Into) -> Self::EntryRef<'a>; } -#[derive(Debug)] -pub struct ReadLockedNS { - ns: Arc, - head: NS::ArcLockedSegment, -} - pub trait NodeEntryOps<'a>: Send + Sync + 'a { type Ref<'b>: NodeRefOps<'b> where diff --git a/raphtory-graphql/src/model/schema/node_schema.rs b/raphtory-graphql/src/model/schema/node_schema.rs index 6652c9da75..906ff8958e 100644 --- a/raphtory-graphql/src/model/schema/node_schema.rs +++ b/raphtory-graphql/src/model/schema/node_schema.rs @@ -134,7 +134,7 @@ mod test { #[test] fn aggregate_schema() -> Result<(), GraphError> { - let g = Graph::new_with_shards(2); + let g = Graph::new(); g.add_node( 0, diff --git a/raphtory-storage/src/mutation/deletion_ops.rs b/raphtory-storage/src/mutation/deletion_ops.rs index c8d8400f32..3a8b84c789 100644 --- a/raphtory-storage/src/mutation/deletion_ops.rs +++ b/raphtory-storage/src/mutation/deletion_ops.rs @@ -48,7 +48,13 @@ impl InternalDeletionOps for db4_graph::TemporalGraph { eid: EID, layer: usize, ) -> Result<(), Self::Error> { - todo!() + let mut writer = self.storage().edge_writer(eid); + let (_, edge_pos) = self.storage().edges().resolve_pos(eid); + let (src, dst) = writer.get_edge(0, edge_pos).unwrap_or_else(|| { + panic!("Internal Error: Edge {eid:?} not found in storage"); + }); + writer.delete_edge(t, Some(edge_pos), src, dst, layer, 0, None); + Ok(()) } } diff --git a/raphtory/src/db/api/storage/storage.rs b/raphtory/src/db/api/storage/storage.rs index 8ef8ebcdba..c696962513 100644 --- a/raphtory/src/db/api/storage/storage.rs +++ b/raphtory/src/db/api/storage/storage.rs @@ -22,7 +22,7 @@ use raphtory_storage::{ }; use std::{ fmt::{Display, Formatter}, - path::PathBuf, + path::{Path, PathBuf}, sync::Arc, }; use storage::{ @@ -80,9 +80,26 @@ impl Base for Storage { const IN_MEMORY_INDEX_NOT_PERSISTED: &str = "In-memory index not persisted. Not supported"; impl Storage { - pub(crate) fn new(num_locks: usize) -> Self { + pub(crate) fn new() -> Self { Self { - graph: GraphStorage::Unlocked(Arc::new(TemporalGraph::new(None))), + graph: GraphStorage::Unlocked(Arc::new(TemporalGraph::default())), + #[cfg(feature = "search")] + index: OnceCell::new(), + } + } + + pub(crate) fn new_at_path(path: impl AsRef) -> Self { + Self { + graph: GraphStorage::Unlocked(Arc::new(TemporalGraph::new_with_path(path))), + #[cfg(feature = "search")] + index: OnceCell::new(), + } + } + + pub(crate) fn load_from(path: impl AsRef) -> Self { + let graph = GraphStorage::Unlocked(Arc::new(TemporalGraph::load_from_path(path))); + Self { + graph, #[cfg(feature = "search")] index: OnceCell::new(), } @@ -543,9 +560,7 @@ impl InternalDeletionOps for Storage { dst: VID, layer: usize, ) -> Result, GraphError> { - let eid = self.graph.internal_delete_edge(t, src, dst, layer)?; - - Ok(eid) + Ok(self.graph.internal_delete_edge(t, src, dst, layer)?) } fn internal_delete_existing_edge( diff --git a/raphtory/src/db/api/view/graph.rs b/raphtory/src/db/api/view/graph.rs index d587e1b1dc..a312f63a62 100644 --- a/raphtory/src/db/api/view/graph.rs +++ b/raphtory/src/db/api/view/graph.rs @@ -248,7 +248,7 @@ impl<'graph, G: GraphView + 'graph> GraphViewOps<'graph> for G { edge_meta.set_const_prop_meta(self.edge_meta().const_prop_meta().deep_clone()); edge_meta.set_temporal_prop_meta(self.edge_meta().temporal_prop_meta().deep_clone()); - let mut g = TemporalGraph::new_with_meta(None, node_meta, edge_meta); + let mut g = TemporalGraph::new_with_meta(Default::default(), node_meta, edge_meta); // Copy all graph properties g.graph_meta = self.graph_meta().deep_clone().into(); diff --git a/raphtory/src/db/graph/graph.rs b/raphtory/src/db/graph/graph.rs index 1e8a35b8de..7912ef98a2 100644 --- a/raphtory/src/db/graph/graph.rs +++ b/raphtory/src/db/graph/graph.rs @@ -43,6 +43,7 @@ use std::{ fmt::{Display, Formatter}, hint::black_box, ops::Deref, + path::Path, sync::Arc, }; @@ -535,14 +536,36 @@ impl Graph { } } - /// Create a new graph with specified number of shards + /// Create a new graph at a specific path /// - /// Returns: + /// # Arguments + /// * `path` - The path to the storage location + /// # Returns + /// A raphtory graph with storage at the specified path + /// # Example + /// ``` + /// use raphtory::prelude::Graph; + /// let g = Graph::new_at_path("/path/to/storage"); + /// ``` + pub fn new_at_path(path: impl AsRef) -> Self { + Self { + inner: Arc::new(Storage::new_at_path(path)), + } + } + + /// Load a graph from a specific path + /// # Arguments + /// * `path` - The path to the storage location + /// # Returns + /// A raphtory graph loaded from the specified path + /// # Example + /// ``` + /// use raphtory::prelude::Graph; + /// let g = Graph::load_from_path("/path/to/storage"); /// - /// A raphtory graph - pub fn new_with_shards(num_shards: usize) -> Self { + pub fn load_from_path(path: impl AsRef) -> Self { Self { - inner: Arc::new(Storage::new(num_shards)), + inner: Arc::new(Storage::load_from(path)), } } @@ -2247,7 +2270,7 @@ mod db_tests { #[test] fn node_properties() -> Result<(), GraphError> { - let g = Graph::new_with_shards(2); + let g = Graph::new(); g.add_node( 0, diff --git a/raphtory/src/python/graph/graph.rs b/raphtory/src/python/graph/graph.rs index fb064a95a6..a8dc19e6ec 100644 --- a/raphtory/src/python/graph/graph.rs +++ b/raphtory/src/python/graph/graph.rs @@ -146,11 +146,11 @@ impl PyGraphEncoder { #[pymethods] impl PyGraph { #[new] - #[pyo3(signature = (num_shards = None))] - pub fn py_new(num_shards: Option) -> (Self, PyGraphView) { - let graph = match num_shards { + #[pyo3(signature = (path = None))] + pub fn py_new(path: Option) -> (Self, PyGraphView) { + let graph = match path { None => Graph::new(), - Some(num_shards) => Graph::new_with_shards(num_shards), + Some(path) => Graph::new_at_path(path), }; ( Self { From 11f3b38c70301ed31b90fec8ec8b0579d5c73477 Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Fri, 11 Jul 2025 16:03:43 +0100 Subject: [PATCH 078/321] more fixes to enable loading props from parquet --- db4-graph/src/lib.rs | 32 +--------- db4-storage/src/properties/mod.rs | 7 ++- raphtory-storage/src/mutation/addition_ops.rs | 17 ++++++ .../src/mutation/addition_ops_ext.rs | 14 ++++- raphtory/src/io/arrow/df_loaders.rs | 60 ++++++++++--------- raphtory/src/io/parquet_loaders.rs | 4 +- .../src/python/graph/io/pandas_loaders.rs | 4 +- raphtory/src/serialise/parquet/mod.rs | 18 ++++++ 8 files changed, 90 insertions(+), 66 deletions(-) diff --git a/db4-graph/src/lib.rs b/db4-graph/src/lib.rs index 238622c176..e6332d3a70 100644 --- a/db4-graph/src/lib.rs +++ b/db4-graph/src/lib.rs @@ -166,7 +166,7 @@ impl, ES = ES>> TemporalGraph { #[inline] pub fn internal_num_nodes(&self) -> usize { - self.storage.nodes().num_nodes() + self.node_count.load(atomic::Ordering::Relaxed) } #[inline] @@ -291,8 +291,6 @@ pub struct WriteLockedGraph<'a, EXT> { pub nodes: WriteLockedNodePages<'a, storage::NS>, pub edges: WriteLockedEdgePages<'a, storage::ES>, pub graph: &'a TemporalGraph, - pub num_nodes: Arc, - pub num_edges: Arc, } impl<'a, EXT: PersistentStrategy, ES = ES>> WriteLockedGraph<'a, EXT> { @@ -301,35 +299,11 @@ impl<'a, EXT: PersistentStrategy, ES = ES>> WriteLockedGraph<' nodes: graph.storage.nodes().write_locked(), edges: graph.storage.edges().write_locked(), graph, - num_nodes: Arc::new(AtomicUsize::new(graph.internal_num_nodes())), - num_edges: Arc::new(AtomicUsize::new(graph.internal_num_edges())), } } - pub fn resolve_node(&self, gid: GidRef) -> Result, InvalidNodeId> { - let result = self.graph.logical_to_physical.get_or_init(gid, || { - VID(self.num_nodes.fetch_add(1, atomic::Ordering::Relaxed)) - }); - - match result { - Ok(vid) => Ok(vid), - Err(GIDResolverError::DBV4Error(e)) => panic!("Database error: {e}"), - Err(GIDResolverError::InvalidNodeId(e)) => Err(e), - } - } - - pub fn num_nodes(&self) -> usize { - self.num_nodes.load(atomic::Ordering::Relaxed) - } - - pub fn num_edges(&self) -> usize { - self.num_edges.load(atomic::Ordering::Relaxed) - } - - pub fn resolve_node_type(&self, node_type: Option<&str>) -> MaybeNew { - node_type - .map(|node_type| self.graph.node_meta().get_or_create_node_type_id(node_type)) - .unwrap_or_else(|| MaybeNew::Existing(0)) + pub fn graph(&self) -> &TemporalGraph { + self.graph } pub fn resize_chunks_to_num_nodes(&mut self, num_nodes: usize) { diff --git a/db4-storage/src/properties/mod.rs b/db4-storage/src/properties/mod.rs index 7069fe0855..71245f9a98 100644 --- a/db4-storage/src/properties/mod.rs +++ b/db4-storage/src/properties/mod.rs @@ -339,7 +339,12 @@ impl<'a> PropMutEntry<'a> { .resize_with(prop_id + 1, Default::default); } let const_props = &mut self.properties.c_properties[*prop_id]; - let _ = const_props.set(self.row, prop.clone()); + if let Err(err) = const_props.set(self.row, prop.clone()) { + panic!( + "Failed to set constant property {prop_id} for row {}: {err}", + self.row + ); + } } } } diff --git a/raphtory-storage/src/mutation/addition_ops.rs b/raphtory-storage/src/mutation/addition_ops.rs index 16ec11c0f3..2ccd5d14ce 100644 --- a/raphtory-storage/src/mutation/addition_ops.rs +++ b/raphtory-storage/src/mutation/addition_ops.rs @@ -53,6 +53,23 @@ pub trait InternalAdditionOps { node_type: &str, ) -> Result, MaybeNew)>, Self::Error>; + fn resolve_node_and_type_fast( + &self, + id: NodeRef, + node_type: Option<&str>, + ) -> Result<(VID, usize), Self::Error> { + match node_type { + Some(node_type) => { + let (vid, node_type_id) = self.resolve_node_and_type(id, node_type)?.inner(); + Ok((vid.inner(), node_type_id.inner())) + } + None => { + let vid = self.resolve_node(id)?.inner(); + Ok((vid, 0)) + } + } + } + /// validate the GidRef is the correct type fn validate_gids<'a>( &self, diff --git a/raphtory-storage/src/mutation/addition_ops_ext.rs b/raphtory-storage/src/mutation/addition_ops_ext.rs index 63207c4074..c472418cb8 100644 --- a/raphtory-storage/src/mutation/addition_ops_ext.rs +++ b/raphtory-storage/src/mutation/addition_ops_ext.rs @@ -124,7 +124,9 @@ impl<'a> SessionAdditionOps for UnlockedSession<'a> { } fn reserve_event_ids(&self, num_ids: usize) -> Result { - todo!() + let event_id = self.graph.storage().read_event_id(); + self.graph.storage().set_event_id(event_id + num_ids); + Ok(event_id) } fn set_node(&self, gid: GidRef, vid: VID) -> Result<(), Self::Error> { @@ -146,7 +148,10 @@ impl<'a> SessionAdditionOps for UnlockedSession<'a> { dtype: PropType, is_static: bool, ) -> Result, Self::Error> { - todo!() + Ok(self + .graph + .node_meta() + .resolve_prop_id(prop, dtype, is_static)?) } fn resolve_edge_property( @@ -155,7 +160,10 @@ impl<'a> SessionAdditionOps for UnlockedSession<'a> { dtype: PropType, is_static: bool, ) -> Result, Self::Error> { - todo!() + Ok(self + .graph + .edge_meta() + .resolve_prop_id(prop, dtype, is_static)?) } fn internal_add_node( diff --git a/raphtory/src/io/arrow/df_loaders.rs b/raphtory/src/io/arrow/df_loaders.rs index fb62adc9aa..e842f193ad 100644 --- a/raphtory/src/io/arrow/df_loaders.rs +++ b/raphtory/src/io/arrow/df_loaders.rs @@ -21,7 +21,7 @@ use raphtory_api::{ }, }; use raphtory_core::storage::timeindex::AsTime; -use raphtory_storage::mutation::addition_ops::SessionAdditionOps; +use raphtory_storage::mutation::addition_ops::{InternalAdditionOps, SessionAdditionOps}; use rayon::prelude::*; use std::{ collections::HashMap, @@ -56,7 +56,7 @@ fn process_shared_properties( } pub(crate) fn load_nodes_from_df< - G: StaticGraphViewOps + PropertyAdditionOps + AdditionOps + InternalCache, + G: StaticGraphViewOps + PropertyAdditionOps + AdditionOps + InternalCache + std::fmt::Debug, >( df_view: DFView>>, time: &str, @@ -98,7 +98,6 @@ pub(crate) fn load_nodes_from_df< let mut node_col_resolved = vec![]; let mut node_type_col_resolved = vec![]; - let cache = graph.get_cache(); let mut write_locked_graph = graph.write_lock().map_err(into_graph_err)?; let mut start_id = session @@ -136,15 +135,14 @@ pub(crate) fn load_nodes_from_df< .zip(node_type_col_resolved.par_iter_mut()) .try_for_each(|(((gid, resolved), node_type), node_type_resolved)| { let gid = gid.ok_or(LoadError::FatalError)?; - let vid = write_locked_graph - .resolve_node(gid) + + let (vid, res_node_type) = write_locked_graph + .graph() + .resolve_node_and_type_fast(gid.as_node_ref(), node_type) .map_err(|_| LoadError::FatalError)?; - let node_type_res = write_locked_graph.resolve_node_type(node_type).inner(); - *node_type_resolved = node_type_res; - if let Some(cache) = cache { - cache.resolve_node(vid, gid); - } - *resolved = vid.inner(); + *resolved = vid; + *node_type_resolved = res_node_type; + Ok::<(), LoadError>(()) })?; @@ -154,7 +152,8 @@ pub(crate) fn load_nodes_from_df< node_stats.update_time(time); }; - write_locked_graph.resize_chunks_to_num_nodes(write_locked_graph.num_nodes()); + write_locked_graph + .resize_chunks_to_num_nodes(write_locked_graph.graph().internal_num_nodes()); write_locked_graph .nodes @@ -286,7 +285,8 @@ pub(crate) fn load_edges_from_df< .try_for_each(|(gid, resolved)| { let gid = gid.ok_or(LoadError::FatalError)?; let vid = write_locked_graph - .resolve_node(gid) + .graph() + .resolve_node(gid.as_node_ref()) .map_err(|_| LoadError::FatalError)?; if let Some(cache) = cache { cache.resolve_node(vid, gid); @@ -302,7 +302,8 @@ pub(crate) fn load_edges_from_df< .try_for_each(|(gid, resolved)| { let gid = gid.ok_or(LoadError::FatalError)?; let vid = write_locked_graph - .resolve_node(gid) + .graph() + .resolve_node(gid.as_node_ref()) .map_err(|_| LoadError::FatalError)?; if let Some(cache) = cache { cache.resolve_node(vid, gid); @@ -311,14 +312,16 @@ pub(crate) fn load_edges_from_df< Ok::<(), LoadError>(()) })?; - write_locked_graph.resize_chunks_to_num_nodes(write_locked_graph.num_nodes()); + write_locked_graph + .resize_chunks_to_num_nodes(write_locked_graph.graph().internal_num_nodes()); // resolve all the edges eid_col_resolved.resize_with(df.len(), Default::default); eids_exist.resize_with(df.len(), Default::default); let eid_col_shared = atomic_usize_from_mut_slice(cast_slice_mut(&mut eid_col_resolved)); - let next_edge_id: Arc = write_locked_graph.num_edges.clone(); + let next_edge_id: Arc = + AtomicUsize::new(write_locked_graph.graph().internal_num_edges()).into(); let next_edge_id = || next_edge_id.fetch_add(1, Ordering::Relaxed); write_locked_graph @@ -377,7 +380,8 @@ pub(crate) fn load_edges_from_df< } }); - write_locked_graph.resize_chunks_to_num_edges(write_locked_graph.num_edges()); + write_locked_graph + .resize_chunks_to_num_edges(write_locked_graph.graph().internal_num_edges()); write_locked_graph .edges @@ -487,7 +491,7 @@ pub(crate) fn load_edge_deletions_from_df< pub(crate) fn load_node_props_from_df< 'a, - G: StaticGraphViewOps + PropertyAdditionOps + AdditionOps + InternalCache, + G: StaticGraphViewOps + PropertyAdditionOps + AdditionOps + InternalCache + std::fmt::Debug, >( df_view: DFView>>, node_id: &str, @@ -522,7 +526,6 @@ pub(crate) fn load_node_props_from_df< let mut node_col_resolved = vec![]; let mut node_type_col_resolved = vec![]; - let cache = graph.get_cache(); let mut write_locked_graph = graph.write_lock().map_err(into_graph_err)?; for chunk in df_view.chunks { @@ -550,19 +553,17 @@ pub(crate) fn load_node_props_from_df< .zip(node_type_col_resolved.par_iter_mut()) .try_for_each(|(((gid, resolved), node_type), node_type_resolved)| { let gid = gid.ok_or(LoadError::FatalError)?; - let vid = write_locked_graph - .resolve_node(gid) + let (vid, res_node_type) = write_locked_graph + .graph() + .resolve_node_and_type_fast(gid.as_node_ref(), node_type) .map_err(|_| LoadError::FatalError)?; - let node_type_res = write_locked_graph.resolve_node_type(node_type).inner(); - *node_type_resolved = node_type_res; - if let Some(cache) = cache { - cache.resolve_node(vid, gid); - } - *resolved = vid.inner(); + *resolved = vid; + *node_type_resolved = res_node_type; Ok::<(), LoadError>(()) })?; - write_locked_graph.resize_chunks_to_num_nodes(write_locked_graph.num_nodes()); + write_locked_graph + .resize_chunks_to_num_nodes(write_locked_graph.graph().internal_num_nodes()); write_locked_graph .nodes @@ -688,7 +689,8 @@ pub(crate) fn load_edges_props_from_df< Ok::<(), LoadError>(()) })?; - write_locked_graph.resize_chunks_to_num_nodes(write_locked_graph.num_nodes()); + write_locked_graph + .resize_chunks_to_num_nodes(write_locked_graph.graph().internal_num_nodes()); // resolve all the edges eid_col_resolved.resize_with(df.len(), Default::default); diff --git a/raphtory/src/io/parquet_loaders.rs b/raphtory/src/io/parquet_loaders.rs index 13fd73cf46..3acb81265b 100644 --- a/raphtory/src/io/parquet_loaders.rs +++ b/raphtory/src/io/parquet_loaders.rs @@ -32,7 +32,7 @@ use pometry_storage::RAError; use raphtory_api::core::entities::properties::prop::Prop; pub fn load_nodes_from_parquet< - G: StaticGraphViewOps + PropertyAdditionOps + AdditionOps + InternalCache, + G: StaticGraphViewOps + PropertyAdditionOps + AdditionOps + InternalCache + std::fmt::Debug, >( graph: &G, parquet_path: &Path, @@ -116,7 +116,7 @@ pub fn load_edges_from_parquet< } pub fn load_node_props_from_parquet< - G: StaticGraphViewOps + PropertyAdditionOps + AdditionOps + InternalCache, + G: StaticGraphViewOps + PropertyAdditionOps + AdditionOps + InternalCache + std::fmt::Debug, >( graph: &G, parquet_path: &Path, diff --git a/raphtory/src/python/graph/io/pandas_loaders.rs b/raphtory/src/python/graph/io/pandas_loaders.rs index 9ba40edbb4..aaa9123435 100644 --- a/raphtory/src/python/graph/io/pandas_loaders.rs +++ b/raphtory/src/python/graph/io/pandas_loaders.rs @@ -23,7 +23,7 @@ pub(crate) fn convert_py_prop_args(properties: Option<&[PyBackedStr]>) -> Option pub(crate) fn load_nodes_from_pandas< 'py, - G: StaticGraphViewOps + PropertyAdditionOps + AdditionOps + InternalCache, + G: StaticGraphViewOps + PropertyAdditionOps + AdditionOps + InternalCache + std::fmt::Debug, >( graph: &G, df: &Bound<'py, PyAny>, @@ -97,7 +97,7 @@ pub(crate) fn load_edges_from_pandas< pub(crate) fn load_node_props_from_pandas< 'py, - G: StaticGraphViewOps + PropertyAdditionOps + AdditionOps + InternalCache, + G: StaticGraphViewOps + PropertyAdditionOps + AdditionOps + InternalCache + std::fmt::Debug, >( graph: &G, df: &Bound<'py, PyAny>, diff --git a/raphtory/src/serialise/parquet/mod.rs b/raphtory/src/serialise/parquet/mod.rs index cac7787d15..061ae954c8 100644 --- a/raphtory/src/serialise/parquet/mod.rs +++ b/raphtory/src/serialise/parquet/mod.rs @@ -872,6 +872,24 @@ mod test { build_and_check_parquet_encoding(nodes.into()); }); } + + #[test] + fn write_nodes_any_props_to_parquet_1() { + let nodes = NodeFixture( + [( + 0, + NodeUpdatesFixture { + props: PropUpdatesFixture { + t_props: vec![(0, vec![])], + c_props: vec![("2".to_string(), Prop::U8(0))], + }, + node_type: Some("one"), + }, + )] + .into(), + ); + build_and_check_parquet_encoding(nodes.into()); + } #[test] fn write_edges_any_props_to_parquet() { proptest!(|(edges in build_edge_list_dyn(10, 10, true))| { From f77e723c0f2db736b0dbdf1c571f7d2aa80ca561 Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Sat, 12 Jul 2025 20:07:22 +0100 Subject: [PATCH 079/321] fixing graph loading from parquet --- db4-graph/src/lib.rs | 2 +- db4-storage/src/segments/edge_entry.rs | 8 +++++- raphtory/src/db/api/mutation/deletion_ops.rs | 2 ++ raphtory/src/io/arrow/df_loaders.rs | 29 ++++++++++--------- raphtory/src/serialise/parquet/edges.rs | 3 +- raphtory/src/serialise/parquet/mod.rs | 30 ++++++++++++++++++++ 6 files changed, 58 insertions(+), 16 deletions(-) diff --git a/db4-graph/src/lib.rs b/db4-graph/src/lib.rs index e6332d3a70..339e6ddb7f 100644 --- a/db4-graph/src/lib.rs +++ b/db4-graph/src/lib.rs @@ -26,7 +26,7 @@ use storage::{ locked::{edges::WriteLockedEdgePages, nodes::WriteLockedNodePages}, }, persist::strategy::PersistentStrategy, - resolver::{GIDResolverError, GIDResolverOps}, + resolver::GIDResolverOps, Extension, GIDResolver, Layer, ReadLockedLayer, ES, NS, }; use tempfile::TempDir; diff --git a/db4-storage/src/segments/edge_entry.rs b/db4-storage/src/segments/edge_entry.rs index 28dfdd149b..54c900e580 100644 --- a/db4-storage/src/segments/edge_entry.rs +++ b/db4-storage/src/segments/edge_entry.rs @@ -168,7 +168,13 @@ impl<'a> EdgeRefOps<'a> for MemEdgeRef<'a> { self.es.as_ref()[0] .get(&self.pos) .map(|entry| entry.src) - .expect("Edge must have a source vertex") + .unwrap_or_else(|| { + panic!( + "Edge must have a source vertex at position {:?} on segment_id: {}", + self.pos, + self.es.as_ref()[0].segment_id() + ) + }) } fn dst(&self) -> VID { diff --git a/raphtory/src/db/api/mutation/deletion_ops.rs b/raphtory/src/db/api/mutation/deletion_ops.rs index 3dbf14f924..cf3a483712 100644 --- a/raphtory/src/db/api/mutation/deletion_ops.rs +++ b/raphtory/src/db/api/mutation/deletion_ops.rs @@ -50,7 +50,9 @@ pub trait DeletionOps: let mut add_edge_op = self .atomic_add_edge(src_id, dst_id, None, layer_id) .map_err(into_graph_err)?; + let edge_id = add_edge_op.internal_delete_edge(ti, src_id, dst_id, 0, layer_id); + println!("ADDED EDGE {edge_id:?} as {src_id:?} -> {dst_id:?}"); add_edge_op.store_src_node_info(src_id, src.as_node_ref().as_gid_ref().left()); add_edge_op.store_dst_node_info(dst_id, dst.as_node_ref().as_gid_ref().left()); diff --git a/raphtory/src/io/arrow/df_loaders.rs b/raphtory/src/io/arrow/df_loaders.rs index e842f193ad..2dd5ee2f27 100644 --- a/raphtory/src/io/arrow/df_loaders.rs +++ b/raphtory/src/io/arrow/df_loaders.rs @@ -320,9 +320,9 @@ pub(crate) fn load_edges_from_df< eids_exist.resize_with(df.len(), Default::default); let eid_col_shared = atomic_usize_from_mut_slice(cast_slice_mut(&mut eid_col_resolved)); - let next_edge_id: Arc = + let num_edges: Arc = AtomicUsize::new(write_locked_graph.graph().internal_num_edges()).into(); - let next_edge_id = || next_edge_id.fetch_add(1, Ordering::Relaxed); + let next_edge_id = || num_edges.fetch_add(1, Ordering::Relaxed); write_locked_graph .nodes @@ -340,22 +340,25 @@ pub(crate) fn load_edges_from_df< let t = TimeIndexEntry(time, start_idx + row); let mut writer = locked_page.writer(); writer.store_node_id(src_pos, 0, src_gid, 0); - if let Some(edge_id) = writer.get_out_edge(src_pos, *dst, 0) { + let edge_id = if let Some(edge_id) = writer.get_out_edge(src_pos, *dst, 0) { eid_col_shared[row].store(edge_id.0, Ordering::Relaxed); eids_exist[row].store(true, Ordering::Relaxed); + edge_id } else { let edge_id = EID(next_edge_id()); writer.add_static_outbound_edge(src_pos, *dst, edge_id, 0); - writer.add_outbound_edge( - Some(t), - src_pos, - *dst, - edge_id.with_layer(*layer), - 0, - ); // FIXME: when we update this to work with layers use the correct layer eid_col_shared[row].store(edge_id.0, Ordering::Relaxed); eids_exist[row].store(false, Ordering::Relaxed); - } + edge_id + }; + + writer.add_outbound_edge( + Some(t), + src_pos, + *dst, + edge_id.with_layer(*layer), + 0, + ); // FIXME: when we update this to work with layers use the correct layer } } }); @@ -380,8 +383,7 @@ pub(crate) fn load_edges_from_df< } }); - write_locked_graph - .resize_chunks_to_num_edges(write_locked_graph.graph().internal_num_edges()); + write_locked_graph.resize_chunks_to_num_edges(num_edges.load(Ordering::Relaxed)); write_locked_graph .edges @@ -413,6 +415,7 @@ pub(crate) fn load_edges_from_df< c_props.extend(const_prop_cols.iter_row(idx)); c_props.extend_from_slice(&shared_constant_properties); + writer.add_static_edge(Some(eid_pos), *src, *dst, 0, 0, Some(exists)); writer.update_c_props(eid_pos, *src, *dst, *layer, c_props.drain(..)); writer.add_edge( t, diff --git a/raphtory/src/serialise/parquet/edges.rs b/raphtory/src/serialise/parquet/edges.rs index 84cf5fdb5b..435fa85992 100644 --- a/raphtory/src/serialise/parquet/edges.rs +++ b/raphtory/src/serialise/parquet/edges.rs @@ -40,6 +40,7 @@ pub(crate) fn encode_edge_tprop( .into_iter() .map(EID) .flat_map(|eid| { + println!("encoding {eid:?}"); let edge_ref = g.core_edge(eid).out_ref(); EdgeView::new(g, edge_ref).explode() }) @@ -100,7 +101,7 @@ pub(crate) fn encode_edge_deletions( }) .map(move |deletions| ParquetDelEdge { del: deletions, - layer: &layers[layer_id], + layer: &layers[layer_id - 1], edge: EdgeView::new(g, edge_ref), }) }) diff --git a/raphtory/src/serialise/parquet/mod.rs b/raphtory/src/serialise/parquet/mod.rs index 061ae954c8..0e645fb7bb 100644 --- a/raphtory/src/serialise/parquet/mod.rs +++ b/raphtory/src/serialise/parquet/mod.rs @@ -897,6 +897,36 @@ mod test { }); } + #[test] + fn write_edges_any_props_to_parquet_1() { + let edges = EdgeFixture( + [ + ( + (0, 0, Some("a")), + EdgeUpdatesFixture { + props: PropUpdatesFixture { + t_props: vec![], + c_props: vec![], + }, + deletions: vec![0], + }, + ), + ( + (0, 1, Some("a")), + EdgeUpdatesFixture { + props: PropUpdatesFixture { + t_props: vec![], + c_props: vec![], + }, + deletions: vec![0], + }, + ), + ] + .into(), + ); + build_and_check_parquet_encoding(edges.into()); + } + #[test] fn write_graph_to_parquet() { proptest!(|(edges in build_graph_strat(10, 10, true))| { From d1a4256579d460b2f659b654530c6a7bc2382d52 Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Mon, 14 Jul 2025 17:50:18 +0100 Subject: [PATCH 080/321] can write/read from parquet in memory storage --- db4-storage/src/api/edges.rs | 10 +- db4-storage/src/pages/edge_page/writer.rs | 58 +++++--- db4-storage/src/pages/edge_store.rs | 100 +++++++++---- db4-storage/src/pages/mod.rs | 18 +-- db4-storage/src/pages/session.rs | 133 +++--------------- db4-storage/src/pages/test_utils/checkers.rs | 42 +++--- db4-storage/src/pages/test_utils/fixtures.rs | 17 +++ db4-storage/src/segments/edge.rs | 26 ++-- db4-storage/src/segments/edge_entry.rs | 13 +- db4-storage/src/wal/entries.rs | 13 +- db4-storage/src/wal/mod.rs | 2 +- db4-storage/src/wal/no_wal.rs | 10 +- raphtory-storage/src/graph/edges/edges.rs | 18 +++ raphtory-storage/src/graph/locked.rs | 31 +--- raphtory-storage/src/mutation/deletion_ops.rs | 2 +- raphtory/src/db/api/mutation/deletion_ops.rs | 1 - raphtory/src/db/api/view/graph.rs | 26 +--- raphtory/src/io/arrow/df_loaders.rs | 15 +- raphtory/src/serialise/parquet/edges.rs | 31 ++-- raphtory/src/serialise/parquet/mod.rs | 41 +++++- 20 files changed, 286 insertions(+), 321 deletions(-) diff --git a/db4-storage/src/api/edges.rs b/db4-storage/src/api/edges.rs index 13e1f0621b..eb688828b9 100644 --- a/db4-storage/src/api/edges.rs +++ b/db4-storage/src/api/edges.rs @@ -87,14 +87,8 @@ pub trait EdgeSegmentOps: Send + Sync + std::fmt::Debug { &'a self, edge_pos: LP, layer_id: usize, - ) -> Option> { - let edge_pos = edge_pos.into(); - if self.head().contains_edge(edge_pos, layer_id) { - Some(self.entry(edge_pos)) - } else { - None - } - } + locked_head: Option>, + ) -> Option>; fn locked(self: &Arc) -> Self::ArcLockedSegment; } diff --git a/db4-storage/src/pages/edge_page/writer.rs b/db4-storage/src/pages/edge_page/writer.rs index 89830a63a9..75fea17edf 100644 --- a/db4-storage/src/pages/edge_page/writer.rs +++ b/db4-storage/src/pages/edge_page/writer.rs @@ -7,13 +7,19 @@ use crate::{ use raphtory_api::core::entities::{VID, properties::prop::Prop}; use raphtory_core::storage::timeindex::AsTime; -pub struct EdgeWriter<'a, MP: DerefMut, ES: EdgeSegmentOps> { +pub struct EdgeWriter< + 'a, + MP: DerefMut + std::fmt::Debug, + ES: EdgeSegmentOps, +> { pub page: &'a ES, pub writer: MP, pub graph_stats: &'a GraphStats, } -impl<'a, MP: DerefMut, ES: EdgeSegmentOps> EdgeWriter<'a, MP, ES> { +impl<'a, MP: DerefMut + std::fmt::Debug, ES: EdgeSegmentOps> + EdgeWriter<'a, MP, ES> +{ pub fn new(global_num_edges: &'a GraphStats, page: &'a ES, writer: MP) -> Self { Self { page, @@ -31,47 +37,47 @@ impl<'a, MP: DerefMut, ES: EdgeSegmentOps> EdgeWriter<' pub fn add_edge( &mut self, t: T, - edge_pos: Option, + edge_pos: LocalPOS, src: impl Into, dst: impl Into, props: impl IntoIterator, layer_id: usize, lsn: u64, - exists_hint: Option, // used when edge_pos is Some but the is not counted, this is used in the bulk loader ) -> LocalPOS { - self.writer.as_mut()[layer_id].set_lsn(lsn); - - if exists_hint == Some(false) && edge_pos.is_some() { - self.new_local_pos(layer_id); // increment the counts, this is triggered from the bulk loader + let existing_edge = self + .page + .contains_edge(edge_pos, layer_id, self.writer.deref()); + if !existing_edge { + self.increment_layer_num_edges(layer_id); } - - let edge_pos = edge_pos.unwrap_or_else(|| self.new_local_pos(layer_id)); self.graph_stats.update_time(t.t()); self.writer - .insert_edge_internal(t, edge_pos, src, dst, layer_id, props); + .insert_edge_internal(t, edge_pos, src, dst, layer_id, props, lsn); edge_pos } pub fn delete_edge( &mut self, t: T, - edge_pos: Option, + edge_pos: LocalPOS, src: impl Into, dst: impl Into, layer_id: usize, lsn: u64, - exists_hint: Option, // used when edge_pos is Some but the is not counted, this is used in the bulk loader ) { - self.writer.as_mut()[layer_id].set_lsn(lsn); - - if exists_hint == Some(false) && edge_pos.is_some() { - self.new_local_pos(layer_id); // increment the counts, this is triggered from the bulk loader + let existing_edge = self + .page + .contains_edge(edge_pos, layer_id, self.writer.deref()); + if !existing_edge { + self.increment_layer_num_edges(layer_id); } - let edge_pos = edge_pos.unwrap_or_else(|| self.new_local_pos(layer_id)); + let src = src.into(); + let dst = dst.into(); + self.graph_stats.update_time(t.t()); self.writer - .delete_edge_internal(t, edge_pos, src, dst, layer_id); + .delete_edge_internal(t, edge_pos, src, dst, layer_id, lsn); } pub fn add_static_edge( @@ -79,11 +85,10 @@ impl<'a, MP: DerefMut, ES: EdgeSegmentOps> EdgeWriter<' edge_pos: Option, src: impl Into, dst: impl Into, - layer_id: usize, lsn: u64, exists_hint: Option, // used when edge_pos is Some but the is not counted, this is used in the bulk loader ) -> LocalPOS { - self.writer.as_mut()[layer_id].set_lsn(lsn); + let layer_id = 0; // assuming layer_id 0 for static edges, adjust as needed if exists_hint == Some(false) && edge_pos.is_some() { self.new_local_pos(layer_id); // increment the counts, this is triggered from the bulk loader @@ -91,7 +96,7 @@ impl<'a, MP: DerefMut, ES: EdgeSegmentOps> EdgeWriter<' let edge_pos = edge_pos.unwrap_or_else(|| self.new_local_pos(layer_id)); self.writer - .insert_static_edge_internal(edge_pos, src, dst, layer_id); + .insert_static_edge_internal(edge_pos, src, dst, layer_id, lsn); edge_pos } @@ -119,12 +124,19 @@ impl<'a, MP: DerefMut, ES: EdgeSegmentOps> EdgeWriter<' layer_id: usize, props: impl IntoIterator, ) { + let existing_edge = self + .page + .contains_edge(edge_pos, layer_id, self.writer.deref()); + + if !existing_edge { + self.increment_layer_num_edges(layer_id); + } self.writer .update_const_properties(edge_pos, src, dst, layer_id, props); } } -impl<'a, MP: DerefMut, ES: EdgeSegmentOps> Drop +impl<'a, MP: DerefMut + std::fmt::Debug, ES: EdgeSegmentOps> Drop for EdgeWriter<'a, MP, ES> { fn drop(&mut self) { diff --git a/db4-storage/src/pages/edge_store.rs b/db4-storage/src/pages/edge_store.rs index 9a9949f56a..21fecc04cc 100644 --- a/db4-storage/src/pages/edge_store.rs +++ b/db4-storage/src/pages/edge_store.rs @@ -7,7 +7,7 @@ use std::{ use super::{edge_page::writer::EdgeWriter, resolve_pos}; use crate::{ LocalPOS, - api::edges::{EdgeSegmentOps, LockedESegment}, + api::edges::{EdgeRefOps, EdgeSegmentOps, LockedESegment}, error::DBV4Error, pages::{ layer_counter::GraphStats, @@ -27,7 +27,7 @@ const N: usize = 32; #[derive(Debug)] pub struct EdgeStorageInner { - pages: boxcar::Vec>, + segments: boxcar::Vec>, layer_counter: Arc, free_pages: Box<[RwLock; N]>, edges_path: PathBuf, @@ -51,7 +51,8 @@ impl, EXT: Clone + Send + Sync> ReadLockedEd &self, e_id: impl Into, ) -> <::ArcLockedSegment as LockedESegment>::EntryRef<'_> { - let (page_id, pos) = self.storage.resolve_pos(e_id.into()); + let e_id = e_id.into(); + let (page_id, pos) = self.storage.resolve_pos(e_id); let locked_page = &self.locked_pages[page_id]; locked_page.entry_ref(pos) } @@ -77,12 +78,28 @@ impl, EXT: Clone + Send + Sync> ReadLockedEd .par_iter() .flat_map(move |page| page.edge_par_iter(layer_ids)) } + + /// Returns an iterator over the segments of the edge store, where each segment is + /// a tuple of the segment index and an iterator over the entries in that segment. + pub fn segmented_par_iter( + &self, + ) -> impl ParallelIterator)> + '_ { + self.locked_pages + .par_iter() + .enumerate() + .map(move |(segment_id, page)| { + ( + segment_id, + page.edge_iter(&LayerIds::All).map(|e| e.edge_id()), + ) + }) + } } impl, EXT: Clone + Send + Sync> EdgeStorageInner { pub fn locked(self: &Arc) -> ReadLockedEdgeStorage { let locked_pages = self - .pages + .segments .iter() .map(|(_, segment)| segment.locked()) .collect::>(); @@ -108,7 +125,7 @@ impl, EXT: Clone + Send + Sync> EdgeStorageI ) -> Self { let free_pages = (0..N).map(RwLock::new).collect::>(); Self { - pages: boxcar::Vec::new(), + segments: boxcar::Vec::new(), layer_counter: GraphStats::new().into(), free_pages: free_pages.try_into().unwrap(), edges_path: edges_path.as_ref().to_path_buf(), @@ -123,7 +140,7 @@ impl, EXT: Clone + Send + Sync> EdgeStorageI } pub fn pages(&self) -> &boxcar::Vec> { - &self.pages + &self.segments } pub fn edges_path(&self) -> &Path { @@ -131,16 +148,16 @@ impl, EXT: Clone + Send + Sync> EdgeStorageI } pub fn earliest(&self) -> Option { - Iterator::min(self.pages.iter().filter_map(|(_, page)| page.earliest())) + Iterator::min(self.segments.iter().filter_map(|(_, page)| page.earliest())) // see : https://github.com/rust-lang/rust-analyzer/issues/10653 } pub fn latest(&self) -> Option { - Iterator::max(self.pages.iter().filter_map(|(_, page)| page.latest())) + Iterator::max(self.segments.iter().filter_map(|(_, page)| page.latest())) } pub fn t_len(&self) -> usize { - self.pages.iter().map(|(_, page)| page.t_len()).sum() + self.segments.iter().map(|(_, page)| page.t_len()).sum() } pub fn prop_meta(&self) -> &Arc { @@ -242,7 +259,7 @@ impl, EXT: Clone + Send + Sync> EdgeStorageI } Ok(Self { - pages, + segments: pages, edges_path: edges_path.to_path_buf(), max_page_len, layer_counter: GraphStats::from(layer_counts).into(), @@ -257,7 +274,7 @@ impl, EXT: Clone + Send + Sync> EdgeStorageI } pub fn push_new_page(&self) -> usize { - let segment_id = self.pages.push_with(|segment_id| { + let segment_id = self.segments.push_with(|segment_id| { Arc::new(ES::new( segment_id, self.max_page_len, @@ -267,21 +284,21 @@ impl, EXT: Clone + Send + Sync> EdgeStorageI )) }); - while self.pages.get(segment_id).is_none() { + while self.segments.get(segment_id).is_none() { // wait } segment_id } pub fn get_or_create_segment(&self, segment_id: usize) -> &Arc { - if let Some(segment) = self.pages.get(segment_id) { + if let Some(segment) = self.segments.get(segment_id) { return segment; } - let count = self.pages.count(); + let count = self.segments.count(); if count > segment_id { // something has allocated the segment, wait for it to be added loop { - if let Some(segment) = self.pages.get(segment_id) { + if let Some(segment) = self.segments.get(segment_id) { return segment; } else { // wait for the segment to be created @@ -290,10 +307,10 @@ impl, EXT: Clone + Send + Sync> EdgeStorageI } } else { // we need to create the segment - self.pages.reserve(segment_id + 1 - count); + self.segments.reserve(segment_id + 1 - count); loop { - let new_segment_id = self.pages.push_with(|segment_id| { + let new_segment_id = self.segments.push_with(|segment_id| { Arc::new(ES::new( segment_id, self.max_page_len, @@ -305,7 +322,7 @@ impl, EXT: Clone + Send + Sync> EdgeStorageI if new_segment_id >= segment_id { loop { - if let Some(segment) = self.pages.get(segment_id) { + if let Some(segment) = self.segments.get(segment_id) { return segment; } else { // wait for the segment to be created @@ -323,7 +340,7 @@ impl, EXT: Clone + Send + Sync> EdgeStorageI pub fn write_locked<'a>(&'a self) -> WriteLockedEdgePages<'a, ES> { WriteLockedEdgePages::new( - self.pages + self.segments .iter() .map(|(page_id, page)| { LockedEdgePage::new( @@ -342,7 +359,7 @@ impl, EXT: Clone + Send + Sync> EdgeStorageI let layer = e_id.layer(); let e_id = e_id.edge; let (chunk, local_edge) = resolve_pos(e_id, self.max_page_len); - let page = self.pages.get(chunk)?; + let page = self.segments.get(chunk)?; page.get_edge(local_edge, layer, page.head()) } @@ -350,7 +367,7 @@ impl, EXT: Clone + Send + Sync> EdgeStorageI let e_id = e_id.into(); let (page_id, local_edge) = resolve_pos(e_id, self.max_page_len); let page = self - .pages + .segments .get(page_id) .expect("Internal error: page not found"); page.entry(local_edge) @@ -395,7 +412,7 @@ impl, EXT: Clone + Send + Sync> EdgeStorageI .take(3) .filter_map(|lock| lock.try_read()) .filter_map(|page_id| { - let page = self.pages.get(*page_id)?; + let page = self.segments.get(*page_id)?; let guard = page.try_head_mut()?; if page.num_edges() < self.max_page_len { Some((page, guard)) @@ -411,7 +428,7 @@ impl, EXT: Clone + Send + Sync> EdgeStorageI // not lucky, go wait on your slot loop { let mut slot = self.free_pages[slot_idx].write(); - match self.pages.get(*slot).map(|page| (page, page.head_mut())) { + match self.segments.get(*slot).map(|page| (page, page.head_mut())) { Some((edge_page, writer)) if edge_page.num_edges() < self.max_page_len => { return EdgeWriter::new(&self.layer_counter, edge_page, writer); } @@ -424,22 +441,45 @@ impl, EXT: Clone + Send + Sync> EdgeStorageI } pub fn par_iter(&self, layer: usize) -> impl ParallelIterator> + '_ { - (0..self.pages.count()) + (0..self.segments.count()) .into_par_iter() - .filter_map(move |page_id| self.pages.get(page_id)) + .filter_map(move |page_id| self.segments.get(page_id)) .flat_map(move |page| { (0..page.num_edges()) .into_par_iter() - .filter_map(move |local_edge| page.layer_entry(local_edge, layer)) + .filter_map(move |local_edge| { + page.layer_entry(local_edge, layer, Some(page.head())) + }) }) } pub fn iter(&self, layer: usize) -> impl Iterator> + '_ { - (0..self.pages.count()) - .filter_map(move |page_id| self.pages.get(page_id)) + (0..self.segments.count()) + .filter_map(move |page_id| self.segments.get(page_id)) .flat_map(move |page| { - (0..page.num_edges()) - .filter_map(move |local_edge| page.layer_entry(local_edge, layer)) + (0..page.num_edges()).filter_map(move |local_edge| { + page.layer_entry(local_edge, layer, Some(page.head())) + }) + }) + } + + /// Returns an iterator over the segments of the edge store, where each segment is + /// a tuple of the segment index and an iterator over the entries in that segment. + pub fn segmented_par_iter( + &self, + ) -> impl ParallelIterator)> + '_ { + let max_page_len = self.max_page_len; + (0..self.segments.count()) + .into_par_iter() + .filter_map(move |segment_id| { + self.segments.get(segment_id).map(move |page| { + ( + segment_id, + (0..page.num_edges()).map(move |edge_pos| { + LocalPOS(edge_pos).as_eid(segment_id, max_page_len) + }), + ) + }) }) } } diff --git a/db4-storage/src/pages/mod.rs b/db4-storage/src/pages/mod.rs index a707aeeeb7..6953958fba 100644 --- a/db4-storage/src/pages/mod.rs +++ b/db4-storage/src/pages/mod.rs @@ -245,20 +245,13 @@ impl< let src = src.into(); let dst = dst.into(); let mut session = self.write_session(src, dst, None); - let elid = session.internal_add_edge(t, src, dst, lsn, 0, props); + let elid = session + .add_static_edge(src, dst, lsn) + .map(|eid| eid.with_layer(0)); + session.add_edge_into_layer(t, src, dst, elid, lsn, props); Ok(elid) } - fn internal_delete_edge( - &self, - t: TimeIndexEntry, - edge: Either<(VID, VID), EID>, - layer: usize, - lsn: u64, - ) -> Result<(), DBV4Error> { - todo!() - } - fn as_time_index_entry(&self, t: T) -> Result { let input_time = t.try_into_input_time()?; let t = match input_time { @@ -743,7 +736,7 @@ mod test { const_props: vec![ (VID(0), vec![]), (VID(8), vec![("422".to_owned(), Prop::U8(0))]), - (VID(8), vec![("422".to_owned(), Prop::U8(30))]), + (VID(8), vec![("423".to_owned(), Prop::U8(30))]), ], }; check_graph_with_nodes(43, 94, &node_fixture); @@ -791,7 +784,6 @@ mod test { ("431".to_owned(), Prop::F64(-2.7522071060615837e-76)), ("68".to_owned(), Prop::F64(-2.32248037343811e44)), ("620".to_owned(), Prop::I64(1574788428164567343)), - ("574".to_owned(), Prop::I64(-6212197184834902986)), ], ), ], diff --git a/db4-storage/src/pages/session.rs b/db4-storage/src/pages/session.rs index db19724633..98ddd8abe4 100644 --- a/db4-storage/src/pages/session.rs +++ b/db4-storage/src/pages/session.rs @@ -73,52 +73,40 @@ impl< if let Some(writer) = self.edge_writer.as_mut() { let edge_max_page_len = writer.writer.get_or_create_layer(layer).max_page_len(); let (_, edge_pos) = resolve_pos(e_id.edge, edge_max_page_len); - let exists = Some(!edge.is_new()); - writer.add_edge(t, Some(edge_pos), src, dst, props, layer, lsn, exists); + writer.add_edge(t, edge_pos, src, dst, props, layer, lsn); } else { let mut writer = self.graph.edge_writer(e_id.edge); let edge_max_page_len = writer.writer.get_or_create_layer(layer).max_page_len(); let (_, edge_pos) = resolve_pos(e_id.edge, edge_max_page_len); - let exists = Some(!edge.is_new()); - writer.add_edge(t, Some(edge_pos), src, dst, props, layer, lsn, exists); + writer.add_edge(t, edge_pos, src, dst, props, layer, lsn); self.edge_writer = Some(writer); // Attach edge_writer to hold onto locks } let edge_id = edge.inner(); - if edge_id.layer() > 0 { - if edge.is_new() - || self - .node_writers - .get_mut_src() - .get_out_edge(src_pos, dst, edge_id.layer()) - .is_none() - { - self.node_writers.get_mut_src().add_outbound_edge( - Some(t), - src_pos, - dst, - edge_id, - lsn, - ); - self.node_writers.get_mut_dst().add_inbound_edge( - Some(t), - dst_pos, - src, - edge_id, - lsn, - ); - } - + if edge.is_new() + || self + .node_writers + .get_mut_src() + .get_out_edge(src_pos, dst, edge_id.layer()) + .is_none() + { self.node_writers .get_mut_src() - .update_timestamp(t, src_pos, e_id, lsn); + .add_outbound_edge(Some(t), src_pos, dst, edge_id, lsn); self.node_writers .get_mut_dst() - .update_timestamp(t, dst_pos, e_id, lsn); + .add_inbound_edge(Some(t), dst_pos, src, edge_id, lsn); } + + self.node_writers + .get_mut_src() + .update_timestamp(t, src_pos, e_id, lsn); + self.node_writers + .get_mut_dst() + .update_timestamp(t, dst_pos, e_id, lsn); } pub fn delete_edge_from_layer( @@ -142,16 +130,14 @@ impl< if let Some(writer) = self.edge_writer.as_mut() { let edge_max_page_len = writer.writer.get_or_create_layer(layer).max_page_len(); let (_, edge_pos) = resolve_pos(e_id.edge, edge_max_page_len); - let exists = Some(!edge.is_new()); - writer.delete_edge(t, Some(edge_pos), src, dst, layer, lsn, exists); + writer.delete_edge(t, edge_pos, src, dst, layer, lsn); } else { let mut writer = self.graph.edge_writer(e_id.edge); let edge_max_page_len = writer.writer.get_or_create_layer(layer).max_page_len(); let (_, edge_pos) = resolve_pos(e_id.edge, edge_max_page_len); - let exists = Some(!edge.is_new()); - writer.delete_edge(t, Some(edge_pos), src, dst, layer, lsn, exists); + writer.delete_edge(t, edge_pos, src, dst, layer, lsn); self.edge_writer = Some(writer); // Attach edge_writer to hold onto locks } @@ -215,12 +201,12 @@ impl< let edge_writer = self.edge_writer.as_mut().unwrap(); let (_, edge_pos) = self.graph.edges().resolve_pos(e_id); - edge_writer.add_static_edge(Some(edge_pos), src, dst, layer_id, lsn, None); + edge_writer.add_static_edge(Some(edge_pos), src, dst, lsn, Some(true)); MaybeNew::Existing(e_id) } else { let mut edge_writer = self.graph.get_free_writer(); - let edge_id = edge_writer.add_static_edge(None, src, dst, layer_id, lsn, None); + let edge_id = edge_writer.add_static_edge(None, src, dst, lsn, Some(false)); let edge_id = edge_id.as_eid(edge_writer.segment_id(), self.graph.edges().max_page_len()); @@ -237,81 +223,6 @@ impl< } } - pub fn internal_add_edge( - &mut self, - t: T, - src: impl Into, - dst: impl Into, - lsn: u64, - layer: usize, - props: impl IntoIterator, - ) -> MaybeNew { - let src = src.into(); - let dst = dst.into(); - - let (_, src_pos) = self.graph.nodes().resolve_pos(src); - let (_, dst_pos) = self.graph.nodes().resolve_pos(dst); - - if let Some(e_id) = self - .node_writers - .get_mut_src() - .get_out_edge(src_pos, dst, layer) - { - let mut edge_writer = self.graph.edge_writer(e_id); - let (_, edge_pos) = self.graph.edges().resolve_pos(e_id); - edge_writer.add_edge(t, Some(edge_pos), src, dst, props, layer, lsn, None); - let e_id = e_id.with_layer(layer); - - self.node_writers - .get_mut_src() - .update_timestamp(t, src_pos, e_id, lsn); - self.node_writers - .get_mut_dst() - .update_timestamp(t, dst_pos, e_id, lsn); - - self.edge_writer = Some(edge_writer); // Attach edge_writer to hold onto locks - - MaybeNew::Existing(e_id) - } else if let Some(e_id) = self - .node_writers - .get_mut_src() - .get_out_edge(src_pos, dst, layer) - { - let mut edge_writer = self.graph.edge_writer(e_id); - let (_, edge_pos) = self.graph.edges().resolve_pos(e_id); - let e_id = e_id.with_layer(layer); - - edge_writer.add_edge(t, Some(edge_pos), src, dst, props, layer, lsn, None); - self.node_writers - .get_mut_src() - .update_timestamp(t, src_pos, e_id, lsn); - self.node_writers - .get_mut_dst() - .update_timestamp(t, dst_pos, e_id, lsn); - - self.edge_writer = Some(edge_writer); // Attach edge_writer to hold onto locks - - MaybeNew::Existing(e_id) - } else { - let mut edge_writer = self.graph.get_free_writer(); - let edge_id = edge_writer.add_edge(t, None, src, dst, props, layer, lsn, None); - let edge_id = - edge_id.as_eid(edge_writer.segment_id(), self.graph.edges().max_page_len()); - let edge_id = edge_id.with_layer(layer); - - self.edge_writer = Some(edge_writer); // Attach edge_writer to hold onto locks - - self.node_writers - .get_mut_src() - .add_outbound_edge(Some(t), src_pos, dst, edge_id, lsn); - self.node_writers - .get_mut_dst() - .add_inbound_edge(Some(t), dst_pos, src, edge_id, lsn); - - MaybeNew::New(edge_id) - } - } - pub fn node_writers( &mut self, ) -> &mut WriterPair<'a, RwLockWriteGuard<'a, MemNodeSegment>, NS> { diff --git a/db4-storage/src/pages/test_utils/checkers.rs b/db4-storage/src/pages/test_utils/checkers.rs index 631dfd0acf..41a62fc3e2 100644 --- a/db4-storage/src/pages/test_utils/checkers.rs +++ b/db4-storage/src/pages/test_utils/checkers.rs @@ -201,7 +201,7 @@ pub fn check_edges_support< check("post-drop", &edges, &graph); } Err(e) => { - panic!("Failed to load graph: {:?}", e); + panic!("Failed to load graph: {e:?}"); } } } @@ -228,13 +228,13 @@ pub fn check_graph_with_nodes_support< for (node, t, t_props) in temp_props { let err = graph.add_node_props(*t, *node, 0, t_props.clone()); - assert!(err.is_ok(), "Failed to add node: {:?}", err); + assert!(err.is_ok(), "Failed to add node: {err:?}"); } for (node, const_props) in const_props { let err = graph.update_node_const_props(*node, 0, const_props.clone()); - assert!(err.is_ok(), "Failed to add node: {:?}", err); + assert!(err.is_ok(), "Failed to add node: {err:?}"); } let check_fn = |temp_props: &[(VID, i64, Vec<(String, Prop)>)], @@ -242,7 +242,7 @@ pub fn check_graph_with_nodes_support< graph: &GraphStore| { let mut ts_for_nodes = HashMap::new(); for (node, t, _) in temp_props { - ts_for_nodes.entry(*node).or_insert_with(|| vec![]).push(*t); + ts_for_nodes.entry(*node).or_insert_with(Vec::new).push(*t); } ts_for_nodes.iter_mut().for_each(|(_, ts)| { ts.sort_unstable(); @@ -268,7 +268,7 @@ pub fn check_graph_with_nodes_support< for (name, prop) in const_props { const_props_values .entry((node, name)) - .or_insert_with(|| HashSet::new()) + .or_insert_with(HashSet::new) .insert(prop.clone()); } } @@ -280,8 +280,8 @@ pub fn check_graph_with_nodes_support< let prop_id = graph .node_meta() .const_prop_meta() - .get_id(&name) - .unwrap_or_else(|| panic!("Failed to get prop id for {}", name)); + .get_id(name) + .unwrap_or_else(|| panic!("Failed to get prop id for {name}")); let actual_props = node_entry.c_prop(0, prop_id); if !const_props.is_empty() { @@ -289,9 +289,7 @@ pub fn check_graph_with_nodes_support< .unwrap_or_else(|| panic!("Failed to get prop {name} for {node:?}")); assert!( const_props.contains(&actual_prop), - "failed to get const prop {name} for {node:?}, expected {:?}, got {:?}", - const_props, - actual_prop + "failed to get const prop {name} for {node:?}, expected {const_props:?}, got {actual_prop:?}" ); } } @@ -304,7 +302,7 @@ pub fn check_graph_with_nodes_support< for (prop_name, prop) in t_props { let prop_values = nod_t_prop_groups .entry((node, prop_name)) - .or_insert_with(|| vec![]); + .or_insert_with(Vec::new); prop_values.push((t, prop.clone())); } } @@ -317,8 +315,8 @@ pub fn check_graph_with_nodes_support< let prop_id = graph .node_meta() .temporal_prop_meta() - .get_id(&prop_name) - .unwrap_or_else(|| panic!("Failed to get prop id for {}", prop_name)); + .get_id(prop_name) + .unwrap_or_else(|| panic!("Failed to get prop id for {prop_name}")); let ne = graph.nodes().node(node); let node_entry = ne.as_ref(); @@ -329,8 +327,7 @@ pub fn check_graph_with_nodes_support< assert_eq!( actual_props, props, - "Expected properties for node ({:?}) to be {:?}, but got {:?}", - node, props, actual_props + "Expected properties for node ({node:?}) to be {props:?}, but got {actual_props:?}" ); } }; @@ -361,7 +358,7 @@ pub fn check_graph_with_props_support< for (src, dst, t, t_props, _, _) in edges { let err = graph.add_edge_props(*t, *src, *dst, t_props.clone(), 0); - assert!(err.is_ok(), "Failed to add edge: {:?}", err); + assert!(err.is_ok(), "Failed to add edge: {err:?}"); } // Add const props @@ -370,14 +367,13 @@ pub fn check_graph_with_props_support< let eid = graph .nodes() .get_edge(*src, *dst, layer_id) - .unwrap_or_else(|| panic!("Failed to get edge ({:?}, {:?}) from graph", src, dst)); + .unwrap_or_else(|| panic!("Failed to get edge ({src:?}, {dst:?}) from graph")); let elid = ELID::new(eid, layer_id); let res = graph.update_edge_const_props(elid, const_props.clone()); assert!( res.is_ok(), - "Failed to update edge const props: {:?} {src:?} -> {dst:?}", - res + "Failed to update edge const props: {res:?} {src:?} -> {dst:?}" ); } @@ -396,7 +392,7 @@ pub fn check_graph_with_props_support< for (prop_name, prop) in t_props { let prop_values = edge_groups .entry((src, dst, prop_name)) - .or_insert_with(|| vec![]); + .or_insert_with(Vec::new); prop_values.push((t, prop.clone())); } } @@ -412,7 +408,7 @@ pub fn check_graph_with_props_support< let t = *t; // Include src additions - node_groups.entry(src).or_insert_with(|| vec![]).push(t); + node_groups.entry(src).or_default().push(t); // Self-edges don't have dst additions, so skip if src == dst { @@ -420,7 +416,7 @@ pub fn check_graph_with_props_support< } // Include dst additions - node_groups.entry(dst).or_insert_with(|| vec![]).push(t); + node_groups.entry(dst).or_default().push(t); } node_groups.iter_mut().for_each(|(_, ts)| { @@ -432,7 +428,7 @@ pub fn check_graph_with_props_support< let prop_id = graph .edge_meta() .temporal_prop_meta() - .get_id(&prop_name) + .get_id(prop_name) .unwrap_or_else(|| panic!("Failed to get prop id for {}", prop_name)); let edge = graph diff --git a/db4-storage/src/pages/test_utils/fixtures.rs b/db4-storage/src/pages/test_utils/fixtures.rs index ed4f313116..bd3e8108b9 100644 --- a/db4-storage/src/pages/test_utils/fixtures.rs +++ b/db4-storage/src/pages/test_utils/fixtures.rs @@ -3,6 +3,8 @@ use raphtory_api::core::entities::properties::prop::Prop; use raphtory_core::entities::VID; use std::collections::HashMap; +use crate::segments::node; + use super::props::{make_props, prop_type}; pub type AddEdge = ( @@ -71,6 +73,21 @@ pub fn make_nodes(num_nodes: usize) -> impl Strategy { let const_props = proptest::collection::vec(((0..num_nodes).prop_map(VID), c_props), 1..=num_nodes); + let const_props = const_props.prop_map(|mut nodes_with_const| { + nodes_with_const.sort_by(|(vid, _), (vid2, _)| vid.cmp(vid2)); + nodes_with_const + .chunk_by(|(vid, _), (vid2, _)| *vid == *vid2) + .map(|stuff| { + let props = stuff + .iter() + .flat_map(|(_, values)| values.clone()) + .collect::>(); + let vid = stuff[0].0; + (vid, props.into_iter().collect::>()) + }) + .collect() + }); + (temp_props, const_props).prop_map(|(temp_props, const_props)| NodeFixture { temp_props, const_props, diff --git a/db4-storage/src/segments/edge.rs b/db4-storage/src/segments/edge.rs index 4cd8157430..fe3eead52f 100644 --- a/db4-storage/src/segments/edge.rs +++ b/db4-storage/src/segments/edge.rs @@ -130,6 +130,7 @@ impl MemEdgeSegment { dst: impl Into, layer_id: usize, props: impl IntoIterator, + lsn: u64, ) { let edge_pos = edge_pos.into(); let src = src.into(); @@ -137,6 +138,7 @@ impl MemEdgeSegment { // Ensure we have enough layers self.ensure_layer(layer_id); + self.layers[layer_id].set_lsn(lsn); let local_row = self.reserve_local_row(edge_pos, src, dst, layer_id); @@ -154,6 +156,7 @@ impl MemEdgeSegment { src: impl Into, dst: impl Into, layer_id: usize, + lsn: u64, ) { let edge_pos = edge_pos.into(); let src = src.into(); @@ -162,6 +165,7 @@ impl MemEdgeSegment { // Ensure we have enough layers self.ensure_layer(layer_id); + self.layers[layer_id].set_lsn(lsn); let local_row = self.reserve_local_row(edge_pos, src, dst, layer_id); let props = self.layers[layer_id].properties_mut(); @@ -174,12 +178,14 @@ impl MemEdgeSegment { src: impl Into, dst: impl Into, layer_id: usize, + lsn: u64, ) { let src = src.into(); let dst = dst.into(); // Ensure we have enough layers self.ensure_layer(layer_id); + self.layers[layer_id].set_lsn(lsn); self.reserve_local_row(edge_pos, src, dst, layer_id); } @@ -256,16 +262,6 @@ impl MemEdgeSegment { prop_entry.append_const_props(props) } - pub fn insert_edge( - &mut self, - edge_pos: LocalPOS, - src: impl Into, - dst: impl Into, - layer_id: usize, - ) { - self.insert_edge_internal(0, edge_pos, src, dst, layer_id, []); - } - pub fn contains_edge(&self, edge_pos: LocalPOS, layer_id: usize) -> bool { self.layers .get(layer_id) @@ -479,12 +475,14 @@ impl>> EdgeSegmentOps for EdgeSegm &'a self, edge_pos: LP, layer_id: usize, + locked_head: Option>, ) -> Option> { let edge_pos = edge_pos.into(); - let locked_head = self.head(); - let layer = locked_head.as_ref().get(layer_id)?; - let has_edge = layer.items().get(edge_pos.0).is_some_and(|item| *item); - has_edge.then(|| MemEdgeEntry::new(edge_pos, locked_head)) + locked_head.and_then(|locked_head| { + let layer = locked_head.as_ref().get(layer_id)?; + let has_edge = layer.items().get(edge_pos.0).is_some_and(|item| *item); + has_edge.then(|| MemEdgeEntry::new(edge_pos, locked_head)) + }) } fn locked(self: &Arc) -> Self::ArcLockedSegment { diff --git a/db4-storage/src/segments/edge_entry.rs b/db4-storage/src/segments/edge_entry.rs index 54c900e580..22f76e33ef 100644 --- a/db4-storage/src/segments/edge_entry.rs +++ b/db4-storage/src/segments/edge_entry.rs @@ -1,6 +1,9 @@ use raphtory_api::core::entities::properties::prop::Prop; use raphtory_core::{ - entities::{EID, Multiple, VID, properties::tprop::TPropCell}, + entities::{ + EID, Multiple, VID, + properties::{tcell::TCell, tprop::TPropCell}, + }, storage::timeindex::{TimeIndexEntry, TimeIndexOps}, }; @@ -101,7 +104,13 @@ impl<'a> WithTimeCells<'a> for MemEdgeRef<'a> { layer_id: usize, range: Option<(TimeIndexEntry, TimeIndexEntry)>, ) -> impl Iterator + 'a { - let t_cell = MemAdditions::Edges(self.es.as_ref()[layer_id].deletions(self.pos)); + let deletions = self + .es + .as_ref() + .get(layer_id) + .map(|layer| layer.deletions(self.pos)) + .unwrap_or(&TCell::Empty); + let t_cell = MemAdditions::Edges(deletions); std::iter::once( range .map(|(start, end)| t_cell.range(start..end)) diff --git a/db4-storage/src/wal/entries.rs b/db4-storage/src/wal/entries.rs index 4ca881f8a3..e7da2d1cc8 100644 --- a/db4-storage/src/wal/entries.rs +++ b/db4-storage/src/wal/entries.rs @@ -1,9 +1,9 @@ +use raphtory_api::core::entities::properties::prop::Prop; use raphtory_core::{ - entities::{VID, EID, GID}, + entities::{EID, GID, VID}, storage::timeindex::TimeIndexEntry, }; -use raphtory_api::core::entities::properties::prop::Prop; -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; use std::borrow::Cow; use crate::wal::LSN; @@ -64,7 +64,6 @@ pub struct Checkpoint { pub lsn: LSN, } - // Constructors impl<'a> WalEntry<'a> { pub fn add_edge( @@ -93,7 +92,8 @@ impl<'a> WalEntry<'a> { pub fn add_const_prop_ids(props: Vec<(Cow<'a, str>, usize)>) -> WalEntry<'a> { WalEntry::AddConstPropIDs( - props.into_iter() + props + .into_iter() .map(|(name, id)| AddConstPropID { name, id }) .collect(), ) @@ -101,7 +101,8 @@ impl<'a> WalEntry<'a> { pub fn add_temporal_prop_ids(props: Vec<(Cow<'a, str>, usize)>) -> WalEntry<'a> { WalEntry::AddTemporalPropIDs( - props.into_iter() + props + .into_iter() .map(|(name, id)| AddTemporalPropID { name, id }) .collect(), ) diff --git a/db4-storage/src/wal/mod.rs b/db4-storage/src/wal/mod.rs index fab33e90fa..b001750e07 100644 --- a/db4-storage/src/wal/mod.rs +++ b/db4-storage/src/wal/mod.rs @@ -2,8 +2,8 @@ use std::path::Path; use crate::error::DBV4Error; -pub mod no_wal; pub mod entries; +pub mod no_wal; pub type LSN = u64; diff --git a/db4-storage/src/wal/no_wal.rs b/db4-storage/src/wal/no_wal.rs index 3acf22611a..f9bfc22829 100644 --- a/db4-storage/src/wal/no_wal.rs +++ b/db4-storage/src/wal/no_wal.rs @@ -1,7 +1,9 @@ use std::path::{Path, PathBuf}; -use crate::error::DBV4Error; -use crate::wal::{LSN, WalOps, WalRecord}; +use crate::{ + error::DBV4Error, + wal::{LSN, WalOps, WalRecord}, +}; pub struct NoWal { dir: PathBuf, @@ -9,7 +11,9 @@ pub struct NoWal { impl WalOps for NoWal { fn new(dir: impl AsRef) -> Result { - Ok(Self { dir: dir.as_ref().to_path_buf() }) + Ok(Self { + dir: dir.as_ref().to_path_buf(), + }) } fn dir(&self) -> &Path { diff --git a/raphtory-storage/src/graph/edges/edges.rs b/raphtory-storage/src/graph/edges/edges.rs index c71bf7b815..a3c6b6f36d 100644 --- a/raphtory-storage/src/graph/edges/edges.rs +++ b/raphtory-storage/src/graph/edges/edges.rs @@ -69,6 +69,24 @@ impl<'a> EdgesStorageRef<'a> { } } + pub fn segmented_par_iter( + self, + ) -> impl ParallelIterator + use<'a>)> + 'a { + match self { + EdgesStorageRef::Mem(storage) => Iter2::I1( + storage + .segmented_par_iter() + .map(|(segment, iter)| (segment, Iter2::I1(iter))), + ), + EdgesStorageRef::Unlocked(edges) => Iter2::I2( + edges + .storage() + .segmented_par_iter() + .map(|(segment, iter)| (segment, Iter2::I2(iter))), + ), + } + } + #[inline] pub fn count(self, layers: &LayerIds) -> usize { match self { diff --git a/raphtory-storage/src/graph/locked.rs b/raphtory-storage/src/graph/locked.rs index e619256c05..bf3351924c 100644 --- a/raphtory-storage/src/graph/locked.rs +++ b/raphtory-storage/src/graph/locked.rs @@ -1,17 +1,6 @@ use db4_graph::TemporalGraph; -use raphtory_api::core::{ - entities::{GidRef, VID}, - storage::dict_mapper::MaybeNew, -}; -use raphtory_core::{ - entities::graph::logical_to_physical::InvalidNodeId, - storage::{raw_edges::WriteLockedEdges, WriteLockedNodes}, -}; use std::sync::Arc; -use storage::{ - pages::locked::{edges::WriteLockedEdgePages, nodes::WriteLockedNodePages}, - Extension, ReadLockedEdges, ReadLockedNodes, -}; +use storage::{Extension, ReadLockedEdges, ReadLockedNodes}; #[derive(Debug)] pub struct LockedGraph { @@ -20,24 +9,6 @@ pub struct LockedGraph { pub graph: Arc, } -// impl<'de> serde::Deserialize<'de> for LockedGraph { -// fn deserialize(deserializer: D) -> Result -// where -// D: serde::Deserializer<'de>, -// { -// TemporalGraph::deserialize(deserializer).map(|graph| LockedGraph::new(Arc::new(graph))) -// } -// } -// -// impl serde::Serialize for LockedGraph { -// fn serialize(&self, serializer: S) -> Result -// where -// S: serde::Serializer, -// { -// self.graph.serialize(serializer) -// } -// } - impl LockedGraph { pub fn new(graph: Arc) -> Self { let nodes = Arc::new(graph.storage().nodes().locked()); diff --git a/raphtory-storage/src/mutation/deletion_ops.rs b/raphtory-storage/src/mutation/deletion_ops.rs index 3a8b84c789..54da5cbade 100644 --- a/raphtory-storage/src/mutation/deletion_ops.rs +++ b/raphtory-storage/src/mutation/deletion_ops.rs @@ -53,7 +53,7 @@ impl InternalDeletionOps for db4_graph::TemporalGraph { let (src, dst) = writer.get_edge(0, edge_pos).unwrap_or_else(|| { panic!("Internal Error: Edge {eid:?} not found in storage"); }); - writer.delete_edge(t, Some(edge_pos), src, dst, layer, 0, None); + writer.delete_edge(t, edge_pos, src, dst, layer, 0); Ok(()) } } diff --git a/raphtory/src/db/api/mutation/deletion_ops.rs b/raphtory/src/db/api/mutation/deletion_ops.rs index cf3a483712..e25b1ca190 100644 --- a/raphtory/src/db/api/mutation/deletion_ops.rs +++ b/raphtory/src/db/api/mutation/deletion_ops.rs @@ -52,7 +52,6 @@ pub trait DeletionOps: .map_err(into_graph_err)?; let edge_id = add_edge_op.internal_delete_edge(ti, src_id, dst_id, 0, layer_id); - println!("ADDED EDGE {edge_id:?} as {src_id:?} -> {dst_id:?}"); add_edge_op.store_src_node_info(src_id, src.as_node_ref().as_gid_ref().left()); add_edge_op.store_dst_node_info(dst_id, dst.as_node_ref().as_gid_ref().left()); diff --git a/raphtory/src/db/api/view/graph.rs b/raphtory/src/db/api/view/graph.rs index a312f63a62..58b378c94b 100644 --- a/raphtory/src/db/api/view/graph.rs +++ b/raphtory/src/db/api/view/graph.rs @@ -368,22 +368,13 @@ impl<'graph, G: GraphView + 'graph> GraphViewOps<'graph> for G { if let Some(edge_pos) = shard.resolve_pos(eid) { let mut writer = shard.writer(); // make the edge for the first time - writer.add_static_edge(Some(edge_pos), src, dst, 0, 0, Some(false)); + writer.add_static_edge(Some(edge_pos), src, dst, 0, Some(false)); for edge in edge.explode_layers() { let layer = layer_map[edge.edge.layer().unwrap()]; for edge in edge.explode() { let t = edge.edge.time().unwrap(); - writer.add_edge( - t, - Some(edge_pos), - src, - dst, - [], - layer, - 0, - Some(true), - ); + writer.add_edge(t, edge_pos, src, dst, [], layer, 0); } //TODO: move this in edge.row() for (t, t_props) in edge @@ -403,16 +394,7 @@ impl<'graph, G: GraphView + 'graph> GraphViewOps<'graph> for G { let props = t_props .map(|(_, prop_id, prop)| (prop_id, prop)) .collect::>(); - writer.add_edge( - t, - Some(edge_pos), - src, - dst, - props, - layer, - 0, - Some(true), - ); + writer.add_edge(t, edge_pos, src, dst, props, layer, 0); } writer.update_c_props( edge_pos, @@ -432,7 +414,7 @@ impl<'graph, G: GraphView + 'graph> GraphViewOps<'graph> for G { self, self.layer_ids(), ) { - writer.delete_edge(t, Some(edge_pos), src, dst, layer, 0, Some(true)); + writer.delete_edge(t, edge_pos, src, dst, layer, 0); } } } diff --git a/raphtory/src/io/arrow/df_loaders.rs b/raphtory/src/io/arrow/df_loaders.rs index 2dd5ee2f27..433c4cdb0f 100644 --- a/raphtory/src/io/arrow/df_loaders.rs +++ b/raphtory/src/io/arrow/df_loaders.rs @@ -377,7 +377,7 @@ pub(crate) fn load_edges_from_df< let t = TimeIndexEntry(time, start_idx + row); let mut writer = shard.writer(); writer.store_node_id(dst_pos, 0, dst_gid, 0); - writer.add_static_inbound_edge(dst_pos, *src, eid.with_layer(*layer), 0); + writer.add_static_inbound_edge(dst_pos, *src, *eid, 0); writer.add_inbound_edge(Some(t), dst_pos, *src, eid.with_layer(*layer), 0); } } @@ -415,18 +415,9 @@ pub(crate) fn load_edges_from_df< c_props.extend(const_prop_cols.iter_row(idx)); c_props.extend_from_slice(&shared_constant_properties); - writer.add_static_edge(Some(eid_pos), *src, *dst, 0, 0, Some(exists)); + writer.add_static_edge(Some(eid_pos), *src, *dst, 0, Some(exists)); writer.update_c_props(eid_pos, *src, *dst, *layer, c_props.drain(..)); - writer.add_edge( - t, - Some(eid_pos), - *src, - *dst, - t_props.drain(..), - *layer, - 0, - Some(exists), - ); + writer.add_edge(t, eid_pos, *src, *dst, t_props.drain(..), *layer, 0); } } Ok::<(), GraphError>(()) diff --git a/raphtory/src/serialise/parquet/edges.rs b/raphtory/src/serialise/parquet/edges.rs index 435fa85992..4a7eda26a4 100644 --- a/raphtory/src/serialise/parquet/edges.rs +++ b/raphtory/src/serialise/parquet/edges.rs @@ -5,10 +5,7 @@ use crate::{ }; use arrow_schema::{DataType, Field}; use model::ParquetCEdge; -use raphtory_api::{ - core::{entities::EID, storage::timeindex::TimeIndexOps}, - iter::IntoDynBoxed, -}; +use raphtory_api::{core::storage::timeindex::TimeIndexOps, iter::IntoDynBoxed}; use raphtory_storage::{ core_ops::CoreGraphOps, graph::{edges::edge_storage_ops::EdgeStorageOps, graph::GraphStorage}, @@ -19,10 +16,10 @@ pub(crate) fn encode_edge_tprop( g: &GraphStorage, path: impl AsRef, ) -> Result<(), GraphError> { - run_encode( + run_encode_indexed( g, g.edge_meta().temporal_prop_meta(), - g.unfiltered_num_edges(), + g.edges().segmented_par_iter(), path, EDGES_T_PATH, |id_type| { @@ -35,12 +32,11 @@ pub(crate) fn encode_edge_tprop( }, |edges, g, decoder, writer| { let row_group_size = 100_000; + let edges = edges.collect::>(); for edge_rows in edges .into_iter() - .map(EID) .flat_map(|eid| { - println!("encoding {eid:?}"); let edge_ref = g.core_edge(eid).out_ref(); EdgeView::new(g, edge_ref).explode() }) @@ -64,10 +60,10 @@ pub(crate) fn encode_edge_deletions( g: &GraphStorage, path: impl AsRef, ) -> Result<(), GraphError> { - run_encode( + run_encode_indexed( g, g.edge_meta().temporal_prop_meta(), - g.unfiltered_num_edges(), + g.edges().segmented_par_iter(), path, EDGES_D_PATH, |id_type| { @@ -91,7 +87,6 @@ pub(crate) fn encode_edge_deletions( for edge_rows in edges .into_iter() - .map(EID) .flat_map(|eid| { g.unfiltered_layer_ids().flat_map(move |layer_id| { let edge = g_edges.edge(eid); @@ -125,10 +120,10 @@ pub(crate) fn encode_edge_cprop( g: &GraphStorage, path: impl AsRef, ) -> Result<(), GraphError> { - run_encode( + run_encode_indexed( g, g.edge_meta().const_prop_meta(), - g.unfiltered_num_edges(), + g.edges().segmented_par_iter(), path, EDGES_C_PATH, |id_type| { @@ -139,16 +134,16 @@ pub(crate) fn encode_edge_cprop( ] }, |edges, g, decoder, writer| { - let row_group_size = 100_000.min(edges.len()); - let layers = 0..g.unfiltered_num_layers(); + let row_group_size = 100_000; for edge_rows in edges .into_iter() - .map(EID) .flat_map(|eid| { let edge_ref = g.core_edge(eid).out_ref(); - g.unfiltered_layer_ids() - .map(move |l_id| edge_ref.at_layer(l_id)) + EdgeView::new(g, edge_ref) + .explode_layers() + .into_iter() + .map(|e| e.edge) }) .map(|edge| ParquetCEdge(EdgeView::new(g, edge))) .chunks(row_group_size) diff --git a/raphtory/src/serialise/parquet/mod.rs b/raphtory/src/serialise/parquet/mod.rs index 0e645fb7bb..1b668b5bc6 100644 --- a/raphtory/src/serialise/parquet/mod.rs +++ b/raphtory/src/serialise/parquet/mod.rs @@ -153,6 +153,41 @@ pub(crate) fn run_encode( Ok(()) } +pub(crate) fn run_encode_indexed>( + g: &GraphStorage, + meta: &PropMapper, + items: impl ParallelIterator, + path: impl AsRef, + suffix: &str, + default_fields_fn: impl Fn(&DataType) -> Vec, + encode_fn: impl Fn(II, &GraphStorage, &mut Decoder, &mut ArrowWriter) -> Result<(), GraphError> + + Sync, +) -> Result<(), GraphError> { + let schema = derive_schema(meta, g.id_type(), default_fields_fn)?; + let root_dir = path.as_ref().join(suffix); + std::fs::create_dir_all(&root_dir)?; + + let num_digits = 8; + + items.try_for_each(|(chunk, items)| { + let props = WriterProperties::builder() + .set_compression(Compression::SNAPPY) + .build(); + + let node_file = File::create(root_dir.join(format!("{chunk:0num_digits$}.parquet")))?; + let mut writer = ArrowWriter::try_new(node_file, schema.clone(), Some(props))?; + + let mut decoder = ReaderBuilder::new(schema.clone()).build_decoder()?; + + encode_fn(items, g, &mut decoder, &mut writer)?; + + writer.close()?; + Ok::<_, GraphError>(()) + })?; + + Ok(()) +} + pub(crate) fn derive_schema( prop_meta: &PropMapper, id_type: Option, @@ -905,17 +940,17 @@ mod test { (0, 0, Some("a")), EdgeUpdatesFixture { props: PropUpdatesFixture { - t_props: vec![], + t_props: vec![(0, vec![])], c_props: vec![], }, deletions: vec![0], }, ), ( - (0, 1, Some("a")), + (0, 1, Some("b")), EdgeUpdatesFixture { props: PropUpdatesFixture { - t_props: vec![], + t_props: vec![(0, vec![])], c_props: vec![], }, deletions: vec![0], From e51e9273db306d832581cb0826a7f35cfad4bc70 Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Tue, 15 Jul 2025 14:06:15 +0100 Subject: [PATCH 081/321] minor changes to avoid graph errors on complex properties --- db4-graph/src/lib.rs | 4 ++-- raphtory-storage/src/graph/nodes/nodes_ref.rs | 7 +++++-- raphtory/src/lib.rs | 17 +++++++++-------- raphtory/src/serialise/parquet/mod.rs | 2 +- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/db4-graph/src/lib.rs b/db4-graph/src/lib.rs index 339e6ddb7f..e28d22f757 100644 --- a/db4-graph/src/lib.rs +++ b/db4-graph/src/lib.rs @@ -166,12 +166,12 @@ impl, ES = ES>> TemporalGraph { #[inline] pub fn internal_num_nodes(&self) -> usize { - self.node_count.load(atomic::Ordering::Relaxed) + self.storage.nodes().layer_num_nodes(0) } #[inline] pub fn internal_num_edges(&self) -> usize { - self.storage.edges().num_edges() + self.storage.edges().num_edges_layer(0) } pub fn read_locked(self: &Arc) -> ReadLockedLayer { diff --git a/raphtory-storage/src/graph/nodes/nodes_ref.rs b/raphtory-storage/src/graph/nodes/nodes_ref.rs index c2749e7e88..6a0b13b7a8 100644 --- a/raphtory-storage/src/graph/nodes/nodes_ref.rs +++ b/raphtory-storage/src/graph/nodes/nodes_ref.rs @@ -45,11 +45,14 @@ impl<'a> NodesStorageEntry<'a> { } } + pub fn is_empty(&self) -> bool { + self.len() == 0 + } pub fn par_iter(&self) -> impl ParallelIterator> { - for_all_variants!(self, nodes => nodes.par_iter().map(|n| n.into())) + for_all_variants!(self, nodes => nodes.par_iter()) } pub fn iter(&self) -> impl Iterator> { - for_all_variants!(self, nodes => nodes.iter().map(|n| n.into())) + for_all_variants!(self, nodes => nodes.iter()) } } diff --git a/raphtory/src/lib.rs b/raphtory/src/lib.rs index 016b49725b..8089651a6c 100644 --- a/raphtory/src/lib.rs +++ b/raphtory/src/lib.rs @@ -302,14 +302,15 @@ mod test_utils { // PropType::Decimal { scale }, decimal breaks the tests because of polars-parquet ]); - leaf.prop_recursive(3, 10, 10, |inner| { - let dict = proptest::collection::hash_map(r"\w{1,10}", inner.clone(), 1..10) - .prop_map(PropType::map); - let list = inner - .clone() - .prop_map(|p_type| PropType::List(Box::new(p_type))); - prop_oneof![inner, list, dict] - }) + // leaf.prop_recursive(3, 10, 10, |inner| { + // let dict = proptest::collection::hash_map(r"\w{1,10}", inner.clone(), 1..10) + // .prop_map(PropType::map); + // let list = inner + // .clone() + // .prop_map(|p_type| PropType::List(Box::new(p_type))); + // prop_oneof![inner, list, dict] + // }) + leaf } #[derive(Debug, Clone)] diff --git a/raphtory/src/serialise/parquet/mod.rs b/raphtory/src/serialise/parquet/mod.rs index 1b668b5bc6..33c385a25f 100644 --- a/raphtory/src/serialise/parquet/mod.rs +++ b/raphtory/src/serialise/parquet/mod.rs @@ -893,7 +893,7 @@ mod test { } #[test] - fn write_nodes_no_props_to_parquet() { + fn write_graph_no_props_to_parquet() { let nf = PropUpdatesFixture { t_props: vec![(1, vec![])], c_props: vec![], From 1a1c991d9d7ed26bb2c0da755a57114a9dd17554 Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Tue, 15 Jul 2025 18:09:29 +0100 Subject: [PATCH 082/321] minor changes --- db4-storage/src/lib.rs | 20 +++++++++++++++++++- db4-storage/src/properties/mod.rs | 3 +-- db4-storage/src/segments/mod.rs | 4 ++++ 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/db4-storage/src/lib.rs b/db4-storage/src/lib.rs index 7c4ad1d45f..45d3884111 100644 --- a/db4-storage/src/lib.rs +++ b/db4-storage/src/lib.rs @@ -1,4 +1,4 @@ -use std::path::Path; +use std::path::{Path, PathBuf}; use crate::{ gen_t_props::GenTProps, @@ -128,3 +128,21 @@ pub fn calculate_size_recursive(path: &Path) -> Result { } Ok(size) } + +pub fn collect_tree_paths(path: &Path) -> Vec { + let mut paths = Vec::new(); + if path.is_dir() { + for entry in std::fs::read_dir(path).unwrap() { + let entry = entry.unwrap(); + let entry_path = entry.path(); + if entry_path.is_dir() { + paths.extend(collect_tree_paths(&entry_path)); + } else { + paths.push(entry_path); + } + } + } else { + paths.push(path.to_path_buf()); + } + paths +} diff --git a/db4-storage/src/properties/mod.rs b/db4-storage/src/properties/mod.rs index 71245f9a98..f979a20a99 100644 --- a/db4-storage/src/properties/mod.rs +++ b/db4-storage/src/properties/mod.rs @@ -284,8 +284,7 @@ impl<'a> PropMutEntry<'a> { { t_prop_row } else { - let row = self.properties.t_properties.push_null(); - row + self.properties.t_properties.push_null() }; if self.properties.times_from_props.len() <= self.row { diff --git a/db4-storage/src/segments/mod.rs b/db4-storage/src/segments/mod.rs index de05830c06..3c95246f33 100644 --- a/db4-storage/src/segments/mod.rs +++ b/db4-storage/src/segments/mod.rs @@ -160,6 +160,10 @@ impl SegmentContainer { self.data.len() } + pub fn is_empty(&self) -> bool { + self.data.is_empty() + } + pub fn row_entries(&self) -> impl Iterator { self.items.iter_ones().filter_map(move |l_pos| { let entry = self.data.get(&LocalPOS(l_pos))?; From b71668bf7289fce03cf67abcddb7440de3e2df9a Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Wed, 16 Jul 2025 17:28:06 +0100 Subject: [PATCH 083/321] fixes to recover layers --- db4-graph/src/lib.rs | 10 +--- db4-storage/src/api/nodes.rs | 6 ++- db4-storage/src/pages/edge_store.rs | 6 +-- db4-storage/src/pages/mod.rs | 18 ++++++-- db4-storage/src/pages/node_store.rs | 46 +++++++++++++------ db4-storage/src/segments/node.rs | 4 +- .../src/core/entities/properties/meta.rs | 7 +++ raphtory-api/src/core/storage/dict_mapper.rs | 2 +- raphtory-api/src/core/storage/timeindex.rs | 4 +- .../src/mutation/addition_ops_ext.rs | 5 ++ 10 files changed, 71 insertions(+), 37 deletions(-) diff --git a/db4-graph/src/lib.rs b/db4-graph/src/lib.rs index e28d22f757..b110e904fb 100644 --- a/db4-graph/src/lib.rs +++ b/db4-graph/src/lib.rs @@ -1,21 +1,15 @@ use std::{ path::{Path, PathBuf}, - sync::{ - atomic::{self, AtomicUsize}, - Arc, - }, + sync::{atomic::AtomicUsize, Arc}, }; use raphtory_api::core::{ entities::{self, properties::meta::Meta}, input::input_node::InputNode, - storage::dict_mapper::MaybeNew, }; use raphtory_core::{ entities::{ - graph::{logical_to_physical::InvalidNodeId, tgraph::InvalidLayer}, - nodes::node_ref::NodeRef, - properties::graph_meta::GraphMeta, + graph::tgraph::InvalidLayer, nodes::node_ref::NodeRef, properties::graph_meta::GraphMeta, GidRef, LayerIds, EID, VID, }, storage::timeindex::TimeIndexEntry, diff --git a/db4-storage/src/api/nodes.rs b/db4-storage/src/api/nodes.rs index 584ceda33c..197cb68b82 100644 --- a/db4-storage/src/api/nodes.rs +++ b/db4-storage/src/api/nodes.rs @@ -49,7 +49,8 @@ pub trait NodeSegmentOps: Send + Sync + std::fmt::Debug { fn load( page_id: usize, max_page_len: usize, - meta: Arc, + node_meta: Arc, + edge_meta: Arc, path: impl AsRef, ext: Self::Extension, ) -> Result @@ -58,7 +59,8 @@ pub trait NodeSegmentOps: Send + Sync + std::fmt::Debug { fn new( page_id: usize, max_page_len: usize, - meta: Arc, + node_meta: Arc, + edge_meta: Arc, path: impl AsRef, ext: Self::Extension, ) -> Self; diff --git a/db4-storage/src/pages/edge_store.rs b/db4-storage/src/pages/edge_store.rs index 21fecc04cc..c59ff84056 100644 --- a/db4-storage/src/pages/edge_store.rs +++ b/db4-storage/src/pages/edge_store.rs @@ -120,7 +120,7 @@ impl, EXT: Clone + Send + Sync> EdgeStorageI pub fn new_with_meta( edges_path: impl AsRef, max_page_len: usize, - edge_meta: Meta, + edge_meta: Arc, ext: EXT, ) -> Self { let free_pages = (0..N).map(RwLock::new).collect::>(); @@ -130,13 +130,13 @@ impl, EXT: Clone + Send + Sync> EdgeStorageI free_pages: free_pages.try_into().unwrap(), edges_path: edges_path.as_ref().to_path_buf(), max_page_len, - prop_meta: edge_meta.into(), + prop_meta: edge_meta, ext, } } pub fn new(edges_path: impl AsRef, max_page_len: usize, ext: EXT) -> Self { - Self::new_with_meta(edges_path, max_page_len, Meta::new(), ext) + Self::new_with_meta(edges_path, max_page_len, Meta::new().into(), ext) } pub fn pages(&self) -> &boxcar::Vec> { diff --git a/db4-storage/src/pages/mod.rs b/db4-storage/src/pages/mod.rs index 6953958fba..cae06d443b 100644 --- a/db4-storage/src/pages/mod.rs +++ b/db4-storage/src/pages/mod.rs @@ -131,17 +131,21 @@ impl< let ext = EXT::default(); - let nodes = Arc::new(NodeStorageInner::load( - nodes_path, - max_page_len_nodes, - ext.clone(), - )?); let edges = Arc::new(EdgeStorageInner::load( edges_path, max_page_len_edges, ext.clone(), )?); + let edge_meta = edges.edge_meta().clone(); + + let nodes = Arc::new(NodeStorageInner::load( + nodes_path, + max_page_len_nodes, + edge_meta, + ext.clone(), + )?); + let t_len = edges.t_len(); Ok(Self { @@ -163,10 +167,14 @@ impl< let edges_path = graph_dir.as_ref().join("edges"); let ext = EXT::default(); + let node_meta = Arc::new(node_meta); + let edge_meta = Arc::new(edge_meta); + let nodes = Arc::new(NodeStorageInner::new_with_meta( nodes_path, max_page_len_nodes, node_meta, + edge_meta.clone(), ext.clone(), )); let edges = Arc::new(EdgeStorageInner::new_with_meta( diff --git a/db4-storage/src/pages/node_store.rs b/db4-storage/src/pages/node_store.rs index f8fc3602b8..2a673fdd3e 100644 --- a/db4-storage/src/pages/node_store.rs +++ b/db4-storage/src/pages/node_store.rs @@ -27,7 +27,8 @@ pub struct NodeStorageInner { layer_counter: Arc, nodes_path: PathBuf, max_page_len: usize, - prop_meta: Arc, + node_meta: Arc, + edge_meta: Arc, ext: EXT, } @@ -87,14 +88,11 @@ impl, EXT: Clone> NodeStorageInner } } - pub fn new(nodes_path: impl AsRef, max_page_len: usize, ext: EXT) -> Self { - Self::new_with_meta(nodes_path, max_page_len, Meta::new(), ext) - } - pub fn new_with_meta( nodes_path: impl AsRef, max_page_len: usize, - node_meta: Meta, + node_meta: Arc, + edge_meta: Arc, ext: EXT, ) -> Self { Self { @@ -102,13 +100,14 @@ impl, EXT: Clone> NodeStorageInner layer_counter: GraphStats::new().into(), nodes_path: nodes_path.as_ref().to_path_buf(), max_page_len, - prop_meta: node_meta.into(), + node_meta, + edge_meta, ext, } } pub fn node_meta(&self) -> &Arc { - &self.prop_meta + &self.node_meta } pub fn write_locked<'a>(&'a self) -> WriteLockedNodePages<'a, NS> { @@ -142,7 +141,7 @@ impl, EXT: Clone> NodeStorageInner } pub fn prop_meta(&self) -> &Arc { - &self.prop_meta + &self.node_meta } #[inline(always)] @@ -178,11 +177,12 @@ impl, EXT: Clone> NodeStorageInner pub fn load( nodes_path: impl AsRef, max_page_len: usize, + edge_meta: Arc, ext: EXT, ) -> Result { let nodes_path = nodes_path.as_ref(); - let meta = Arc::new(Meta::new()); + let node_meta = Arc::new(Meta::new()); let mut pages = std::fs::read_dir(nodes_path)? .filter(|entry| { entry @@ -197,8 +197,15 @@ impl, EXT: Clone> NodeStorageInner .path() .file_stem() .and_then(|name| name.to_str().and_then(|name| name.parse::().ok()))?; - let page = NS::load(page_id, max_page_len, meta.clone(), nodes_path, ext.clone()) - .map(|page| (page_id, page)); + let page = NS::load( + page_id, + max_page_len, + node_meta.clone(), + edge_meta.clone(), + nodes_path, + ext.clone(), + ) + .map(|page| (page_id, page)); Some(page) }) .collect::, _>>()?; @@ -212,7 +219,14 @@ impl, EXT: Clone> NodeStorageInner let pages = (0..=max_page) .map(|page_id| { let np = pages.remove(&page_id).unwrap_or_else(|| { - NS::new(page_id, max_page_len, meta.clone(), nodes_path, ext.clone()) + NS::new( + page_id, + max_page_len, + node_meta.clone(), + edge_meta.clone(), + nodes_path, + ext.clone(), + ) }); Arc::new(np) }) @@ -244,7 +258,8 @@ impl, EXT: Clone> NodeStorageInner nodes_path: nodes_path.to_path_buf(), max_page_len, layer_counter: GraphStats::from(layer_counts).into(), - prop_meta: meta, + node_meta, + edge_meta, ext, }) } @@ -291,7 +306,8 @@ impl, EXT: Clone> NodeStorageInner Arc::new(NS::new( segment_id, self.max_page_len, - self.prop_meta.clone(), + self.node_meta.clone(), + self.edge_meta.clone(), self.nodes_path.clone(), self.ext.clone(), )) diff --git a/db4-storage/src/segments/node.rs b/db4-storage/src/segments/node.rs index 07d32873a3..2655c069f1 100644 --- a/db4-storage/src/segments/node.rs +++ b/db4-storage/src/segments/node.rs @@ -345,7 +345,8 @@ impl>> NodeSegmentOps for NodeSegm fn load( _page_id: usize, _max_page_len: usize, - _meta: Arc, + _node_meta: Arc, + _edge_meta: Arc, _path: impl AsRef, _ext: Self::Extension, ) -> Result @@ -359,6 +360,7 @@ impl>> NodeSegmentOps for NodeSegm page_id: usize, max_page_len: usize, meta: Arc, + _edge_meta: Arc, _path: impl AsRef, _ext: Self::Extension, ) -> Self { diff --git a/raphtory-api/src/core/entities/properties/meta.rs b/raphtory-api/src/core/entities/properties/meta.rs index 01b3d536bc..8ccb92d3cd 100644 --- a/raphtory-api/src/core/entities/properties/meta.rs +++ b/raphtory-api/src/core/entities/properties/meta.rs @@ -35,6 +35,13 @@ impl Default for Meta { } impl Meta { + pub fn layer_iter(&self) -> impl Iterator + use<'_> { + (0..self.meta_layer.len()).map(move |id| { + let name = self.meta_layer.get_name(id); + (id, name) + }) + } + pub fn set_const_prop_meta(&mut self, meta: PropMapper) { self.meta_prop_constant = meta; } diff --git a/raphtory-api/src/core/storage/dict_mapper.rs b/raphtory-api/src/core/storage/dict_mapper.rs index cddffaf531..c28f093c81 100644 --- a/raphtory-api/src/core/storage/dict_mapper.rs +++ b/raphtory-api/src/core/storage/dict_mapper.rs @@ -211,7 +211,7 @@ impl DictMapper { } pub fn get_id(&self, name: &str) -> Option { - self.map.read().get(name).map(|id| *id) + self.map.read().get(name).copied() } /// Explicitly set the id for a key (useful for initialising the map in parallel) diff --git a/raphtory-api/src/core/storage/timeindex.rs b/raphtory-api/src/core/storage/timeindex.rs index a4ecd575ff..cde9c15d13 100644 --- a/raphtory-api/src/core/storage/timeindex.rs +++ b/raphtory-api/src/core/storage/timeindex.rs @@ -52,13 +52,13 @@ pub trait TimeIndexOps<'a>: Sized + Clone + Send + Sync + 'a { #[inline] fn active_t(&self, w: Range) -> bool { - self.active(Self::IndexType::range(w)) + self.active(::range(w)) } fn range(&self, w: Range) -> Self::RangeType; fn range_t(&self, w: Range) -> Self::RangeType { - self.range(Self::IndexType::range(w)) + self.range(::range(w)) } fn first_t(&self) -> Option { diff --git a/raphtory-storage/src/mutation/addition_ops_ext.rs b/raphtory-storage/src/mutation/addition_ops_ext.rs index c472418cb8..87747a3929 100644 --- a/raphtory-storage/src/mutation/addition_ops_ext.rs +++ b/raphtory-storage/src/mutation/addition_ops_ext.rs @@ -219,6 +219,11 @@ impl InternalAdditionOps for TemporalGraph { fn resolve_layer(&self, layer: Option<&str>) -> Result, Self::Error> { let id = self.edge_meta().get_or_create_layer_id(layer); + // TODO: we replicate the layer id in the node meta as well, perhaps layer meta should be common + self.node_meta().layer_meta().set_id( + self.edge_meta().layer_meta().get_name(id.inner()), + id.inner(), + ); if let MaybeNew::New(id) = id { if id > MAX_LAYER { Err(TooManyLayers)?; From b21be2125206858ed1264097de9ca88956fcc008 Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Thu, 17 Jul 2025 12:12:32 +0100 Subject: [PATCH 084/321] minor fixes --- db4-storage/src/pages/mod.rs | 6 +- db4-storage/src/pages/test_utils/checkers.rs | 80 ++++++++----------- db4-storage/src/pages/test_utils/fixtures.rs | 2 - raphtory-storage/src/core_ops.rs | 5 +- raphtory-storage/src/mutation/addition_ops.rs | 6 +- .../src/mutation/addition_ops_ext.rs | 15 +--- 6 files changed, 40 insertions(+), 74 deletions(-) diff --git a/db4-storage/src/pages/mod.rs b/db4-storage/src/pages/mod.rs index cae06d443b..6883a2e8f5 100644 --- a/db4-storage/src/pages/mod.rs +++ b/db4-storage/src/pages/mod.rs @@ -8,10 +8,7 @@ use std::{ use crate::{ LocalPOS, - api::{ - edges::EdgeSegmentOps, - nodes::{NodeEntryOps, NodeRefOps, NodeSegmentOps}, - }, + api::{edges::EdgeSegmentOps, nodes::NodeSegmentOps}, error::DBV4Error, pages::{edge_store::ReadLockedEdgeStorage, node_store::ReadLockedNodeStorage}, properties::props_meta_writer::PropsMetaWriter, @@ -19,7 +16,6 @@ use crate::{ }; use edge_page::writer::EdgeWriter; use edge_store::EdgeStorageInner; -use either::Either; use node_page::writer::{NodeWriter, WriterPair}; use node_store::NodeStorageInner; use parking_lot::RwLockWriteGuard; diff --git a/db4-storage/src/pages/test_utils/checkers.rs b/db4-storage/src/pages/test_utils/checkers.rs index 41a62fc3e2..a97f6222ac 100644 --- a/db4-storage/src/pages/test_utils/checkers.rs +++ b/db4-storage/src/pages/test_utils/checkers.rs @@ -4,10 +4,7 @@ use std::{ }; use itertools::Itertools; -use raphtory_api::core::{ - entities::properties::{prop::Prop, tprop::TPropOps}, - storage::dict_mapper::MaybeNew, -}; +use raphtory_api::core::entities::properties::{prop::Prop, tprop::TPropOps}; use raphtory_core::{ entities::{ELID, VID}, storage::timeindex::TimeIndexOps, @@ -43,6 +40,19 @@ pub fn check_edges_support< let graph_dir = tempfile::tempdir().unwrap(); let graph = make_graph(graph_dir.path()); let mut nodes = HashSet::new(); + for (_, _, layer) in &edges { + if let Some(layer) = layer { + for layer in 0..=*layer { + let name = layer.to_string(); + graph + .edge_meta() + .get_or_create_layer_id(Some(name.as_ref())); + graph + .node_meta() + .get_or_create_layer_id(Some(name.as_ref())); + } + } + } for (src, dst, _) in &edges { nodes.insert(*src); @@ -56,15 +66,11 @@ pub fn check_edges_support< let lsn = 0; let timestamp = 0; - if let Some(layer_id) = layer_id { - let mut session = graph.write_session(*src, *dst, None); - let eid = session.add_static_edge(*src, *dst, lsn); - let elid = eid.map(|eid| eid.with_layer(*layer_id)); - - session.add_edge_into_layer(timestamp, *src, *dst, elid, lsn, []); - } else { - let _ = graph.add_edge(timestamp, *src, *dst)?; - } + let layer_id = layer_id.unwrap_or(0); + let mut session = graph.write_session(*src, *dst, None); + let eid = session.add_static_edge(*src, *dst, lsn); + let elid = eid.map(|eid| eid.with_layer(layer_id)); + session.add_edge_into_layer(timestamp, *src, *dst, elid, lsn, []); Ok::<_, DBV4Error>(()) }) @@ -76,22 +82,12 @@ pub fn check_edges_support< let lsn = 0; let timestamp = 0; - if let Some(layer_id) = layer_id { - let mut session = graph.write_session(*src, *dst, None); - let eid = session.add_static_edge(*src, *dst, lsn).inner(); - let elid = eid.with_layer(*layer_id); - - session.add_edge_into_layer( - timestamp, - *src, - *dst, - MaybeNew::Existing(elid), - lsn, - [], - ); - } else { - let _ = graph.add_edge(timestamp, *src, *dst)?; - } + let layer_id = layer_id.unwrap_or(0); + + let mut session = graph.write_session(*src, *dst, None); + let eid = session.add_static_edge(*src, *dst, lsn); + let elid = eid.map(|e| e.with_layer(layer_id)); + session.add_edge_into_layer(timestamp, *src, *dst, elid, lsn, []); Ok::<_, DBV4Error>(()) }) @@ -172,16 +168,16 @@ pub fn check_edges_support< let elid = ELID::new(eid, layer_id); let (src, dst) = edges.get_edge(elid).unwrap(); - assert_eq!(src, n, "{stage} layer: {}", layer_id); - assert_eq!(dst, exp_dst, "{stage} layer: {}", layer_id); + assert_eq!(src, n, "{stage} layer: {layer_id}"); + assert_eq!(dst, exp_dst, "{stage} layer: {layer_id}"); } for (exp_src, eid) in adj.inb_edges(layer_id) { let elid = ELID::new(eid, layer_id); let (src, dst) = edges.get_edge(elid).unwrap(); - assert_eq!(src, exp_src, "{stage} layer: {}", layer_id); - assert_eq!(dst, n, "{stage} layer: {}", layer_id); + assert_eq!(src, exp_src, "{stage} layer: {layer_id}"); + assert_eq!(dst, n, "{stage} layer: {layer_id}"); } } } @@ -429,12 +425,12 @@ pub fn check_graph_with_props_support< .edge_meta() .temporal_prop_meta() .get_id(prop_name) - .unwrap_or_else(|| panic!("Failed to get prop id for {}", prop_name)); + .unwrap_or_else(|| panic!("Failed to get prop id for {prop_name}")); let edge = graph .nodes() .get_edge(src, dst, 0) - .unwrap_or_else(|| panic!("Failed to get edge ({:?}, {:?}) from graph", src, dst)); + .unwrap_or_else(|| panic!("Failed to get edge ({src:?}, {dst:?}) from graph")); let edge = graph.edges().edge(edge); let e = edge.as_ref(); let layer_id = 0; @@ -445,8 +441,7 @@ pub fn check_graph_with_props_support< assert_eq!( actual_props, props, - "Expected properties for edge ({:?}, {:?}) to be {:?}, but got {:?}", - src, dst, props, actual_props + "Expected properties for edge ({src:?}, {dst:?}) to be {props:?}, but got {actual_props:?}" ); // Check const props @@ -456,16 +451,12 @@ pub fn check_graph_with_props_support< .edge_meta() .const_prop_meta() .get_id(name) - .unwrap_or_else(|| panic!("Failed to get prop id for {}", name)); + .unwrap_or_else(|| panic!("Failed to get prop id for {name}")); let actual_props = e.c_prop(layer_id, prop_id); assert_eq!( actual_props.as_ref(), Some(prop), - "Expected const properties for edge ({:?}, {:?}) to be {:?}, but got {:?}", - src, - dst, - prop, - actual_props + "Expected const properties for edge ({src:?}, {dst:?}) to be {prop:?}, but got {actual_props:?}" ); } } @@ -484,8 +475,7 @@ pub fn check_graph_with_props_support< assert_eq!( actual_additions_ts, ts, - "Expected node additions for node ({:?}) to be {:?}, but got {:?}", - node_id, ts, actual_additions_ts + "Expected node additions for node ({node_id:?}) to be {ts:?}, but got {actual_additions_ts:?}" ); } }; diff --git a/db4-storage/src/pages/test_utils/fixtures.rs b/db4-storage/src/pages/test_utils/fixtures.rs index bd3e8108b9..bcf27636e3 100644 --- a/db4-storage/src/pages/test_utils/fixtures.rs +++ b/db4-storage/src/pages/test_utils/fixtures.rs @@ -3,8 +3,6 @@ use raphtory_api::core::entities::properties::prop::Prop; use raphtory_core::entities::VID; use std::collections::HashMap; -use crate::segments::node; - use super::props::{make_props, prop_type}; pub type AddEdge = ( diff --git a/raphtory-storage/src/core_ops.rs b/raphtory-storage/src/core_ops.rs index 81d9c96dab..d53ac2e35d 100644 --- a/raphtory-storage/src/core_ops.rs +++ b/raphtory-storage/src/core_ops.rs @@ -22,10 +22,7 @@ use raphtory_core::{ }, storage::locked_view::LockedView, }; -use std::{ - iter, - sync::{atomic::Ordering, Arc}, -}; +use std::{iter, sync::Arc}; use storage::resolver::GIDResolverOps; /// Check if two Graph views point at the same underlying storage diff --git a/raphtory-storage/src/mutation/addition_ops.rs b/raphtory-storage/src/mutation/addition_ops.rs index 2ccd5d14ce..a76b8cee17 100644 --- a/raphtory-storage/src/mutation/addition_ops.rs +++ b/raphtory-storage/src/mutation/addition_ops.rs @@ -6,7 +6,6 @@ use crate::{ }, }; use db4_graph::WriteLockedGraph; -use parking_lot::RwLockWriteGuard; use raphtory_api::{ core::{ entities::{ @@ -24,10 +23,7 @@ use raphtory_core::{ entities::{nodes::node_ref::NodeRef, ELID}, storage::{raw_edges::WriteLockedEdges, WriteLockedNodes}, }; -use storage::{ - segments::{edge::MemEdgeSegment, node::MemNodeSegment}, - Extension, -}; +use storage::Extension; pub trait InternalAdditionOps { type Error: From; diff --git a/raphtory-storage/src/mutation/addition_ops_ext.rs b/raphtory-storage/src/mutation/addition_ops_ext.rs index 87747a3929..5c460b0e8c 100644 --- a/raphtory-storage/src/mutation/addition_ops_ext.rs +++ b/raphtory-storage/src/mutation/addition_ops_ext.rs @@ -1,7 +1,4 @@ -use std::ops::DerefMut; - use db4_graph::{TemporalGraph, WriteLockedGraph}; -use parking_lot::RwLockWriteGuard; use raphtory_api::core::{ entities::properties::{ meta::Meta, @@ -11,23 +8,15 @@ use raphtory_api::core::{ }; use raphtory_core::{ entities::{ - graph::tgraph::TooManyLayers, - nodes::node_ref::{AsNodeRef, NodeRef}, - GidRef, EID, ELID, MAX_LAYER, VID, + graph::tgraph::TooManyLayers, nodes::node_ref::NodeRef, GidRef, EID, ELID, MAX_LAYER, VID, }, storage::{raw_edges::WriteLockedEdges, timeindex::TimeIndexEntry, WriteLockedNodes}, }; use storage::{ - error::DBV4Error, - pages::{ - node_page::writer::{node_info_as_props, NodeWriter}, - session::WriteSession, - NODE_ID_PROP_KEY, - }, + pages::{node_page::writer::node_info_as_props, session::WriteSession, NODE_ID_PROP_KEY}, persist::strategy::PersistentStrategy, properties::props_meta_writer::PropsMetaWriter, resolver::GIDResolverOps, - segments::{edge::MemEdgeSegment, node::MemNodeSegment}, Extension, ES, NS, }; From ac5c3baf200bb4848548ca083c4406d5dde4ff1b Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Mon, 21 Jul 2025 18:03:17 +0100 Subject: [PATCH 085/321] various changes to improve loading and track est_size accross segments --- db4-graph/src/lib.rs | 9 +- db4-storage/Cargo.toml | 13 +- db4-storage/src/lib.rs | 2 +- db4-storage/src/pages/edge_page/writer.rs | 2 +- db4-storage/src/pages/layer_counter.rs | 6 +- db4-storage/src/pages/locked/nodes.rs | 10 +- db4-storage/src/pages/node_page/writer.rs | 31 +-- db4-storage/src/pages/node_store.rs | 18 +- db4-storage/src/persist/strategy.rs | 4 +- db4-storage/src/properties/mod.rs | 5 +- db4-storage/src/segments/edge.rs | 131 ++++++++++- db4-storage/src/segments/mod.rs | 17 +- db4-storage/src/segments/node.rs | 210 ++++++++++++++++-- .../src/core/entities/properties/meta.rs | 2 +- .../entities/properties/prop/prop_enum.rs | 2 +- raphtory-core/Cargo.toml | 5 +- .../src/entities/graph/logical_to_physical.rs | 164 +++++++++++++- .../src/entities/nodes/node_store.rs | 4 +- .../src/entities/nodes/structure/adj.rs | 16 +- .../src/entities/nodes/structure/adjset.rs | 23 +- raphtory/Cargo.toml | 5 +- raphtory/src/io/arrow/df_loaders.rs | 112 +++++++--- raphtory/src/io/arrow/node_col.rs | 36 ++- raphtory/src/io/parquet_loaders.rs | 70 ++++-- 24 files changed, 760 insertions(+), 137 deletions(-) diff --git a/db4-graph/src/lib.rs b/db4-graph/src/lib.rs index b110e904fb..7cf0ab4009 100644 --- a/db4-graph/src/lib.rs +++ b/db4-graph/src/lib.rs @@ -28,8 +28,8 @@ use tempfile::TempDir; pub mod entries; pub mod mutation; -const DEFAULT_MAX_PAGE_LEN_NODES: usize = 10; -const DEFAULT_MAX_PAGE_LEN_EDGES: usize = 10; +const DEFAULT_MAX_PAGE_LEN_NODES: usize = 50_000; +const DEFAULT_MAX_PAGE_LEN_EDGES: usize = 50_000; #[derive(Debug)] pub struct TemporalGraph { @@ -106,7 +106,8 @@ impl, ES = ES>> TemporalGraph { } pub fn new_with_meta(graph_dir: GraphDir, node_meta: Meta, edge_meta: Meta) -> Self { - edge_meta.get_or_create_layer_id(Some("static_graph")); + edge_meta.get_or_create_layer_id(Some("staticgraph")); + node_meta.get_or_create_layer_id(Some("staticgraph")); std::fs::create_dir_all(&graph_dir) .unwrap_or_else(|_| panic!("Failed to create graph directory at {graph_dir:?}")); let gid_resolver_dir = graph_dir.as_ref().join("gid_resolver"); @@ -160,7 +161,7 @@ impl, ES = ES>> TemporalGraph { #[inline] pub fn internal_num_nodes(&self) -> usize { - self.storage.nodes().layer_num_nodes(0) + self.logical_to_physical.len() } #[inline] diff --git a/db4-storage/Cargo.toml b/db4-storage/Cargo.toml index d5649062da..0852a3ea92 100644 --- a/db4-storage/Cargo.toml +++ b/db4-storage/Cargo.toml @@ -12,10 +12,10 @@ edition = "2024" [dependencies] raphtory-api.workspace = true -raphtory-core = {workspace = true} +raphtory-core = { workspace = true } # db4-common = {path = "../db4-common"} -bitvec.workspace = true +bitvec = { workspace = true, features = ["serde"] } bigdecimal.workspace = true polars-arrow.workspace = true rustc-hash.workspace = true @@ -35,16 +35,17 @@ itertools.workspace = true thiserror.workspace = true postcard.workspace = true -proptest = {workspace = true, optional = true} -tempfile = {workspace = true, optional = true} -iter-enum = {workspace = true, features = ["rayon"]} -chrono = {workspace = true, optional = true} +proptest = { workspace = true, optional = true } +tempfile = { workspace = true, optional = true } +iter-enum = { workspace = true, features = ["rayon"] } +chrono = { workspace = true, optional = true } [dev-dependencies] proptest.workspace = true tempfile.workspace = true chrono.workspace = true rayon.workspace = true +bincode.workspace = true [features] test-utils = ["proptest", "tempfile", "chrono"] diff --git a/db4-storage/src/lib.rs b/db4-storage/src/lib.rs index 45d3884111..0c201fc797 100644 --- a/db4-storage/src/lib.rs +++ b/db4-storage/src/lib.rs @@ -91,7 +91,7 @@ pub mod error { } } -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize)] #[repr(transparent)] pub struct LocalPOS(pub usize); diff --git a/db4-storage/src/pages/edge_page/writer.rs b/db4-storage/src/pages/edge_page/writer.rs index 75fea17edf..1e6c0de226 100644 --- a/db4-storage/src/pages/edge_page/writer.rs +++ b/db4-storage/src/pages/edge_page/writer.rs @@ -141,7 +141,7 @@ impl<'a, MP: DerefMut + std::fmt::Debug, ES: EdgeSegmen { fn drop(&mut self) { if let Err(err) = self.page.notify_write(self.writer.deref_mut()) { - println!("Failed to persist {}, err: {}", self.segment_id(), err) + eprintln!("Failed to persist {}, err: {}", self.segment_id(), err) } } } diff --git a/db4-storage/src/pages/layer_counter.rs b/db4-storage/src/pages/layer_counter.rs index 439338fbb6..2a7e8ed156 100644 --- a/db4-storage/src/pages/layer_counter.rs +++ b/db4-storage/src/pages/layer_counter.rs @@ -11,7 +11,7 @@ pub struct GraphStats { impl> From for GraphStats { fn from(iter: I) -> Self { - let layers = iter.into_iter().map(AtomicUsize::new).collect(); + let layers = iter.into_iter().map(|_| Default::default()).collect(); Self { layers, earliest: MinCounter::new(), @@ -29,7 +29,7 @@ impl Default for GraphStats { impl GraphStats { pub fn new() -> Self { let layers = boxcar::Vec::new(); - layers.push_with(|_| AtomicUsize::new(0)); + layers.push_with(|_| Default::default()); Self { layers, earliest: MinCounter::new(), @@ -89,7 +89,7 @@ impl GraphStats { self.layers.reserve(layer_id + 1 - self.layers.count()); loop { - let new_layer_id = self.layers.push_with(|_| AtomicUsize::new(0)); + let new_layer_id = self.layers.push_with(|_| Default::default()); if new_layer_id >= layer_id { loop { if let Some(counter) = self.layers.get(layer_id) { diff --git a/db4-storage/src/pages/locked/nodes.rs b/db4-storage/src/pages/locked/nodes.rs index 3c086ecc59..dc6a020e26 100644 --- a/db4-storage/src/pages/locked/nodes.rs +++ b/db4-storage/src/pages/locked/nodes.rs @@ -34,6 +34,10 @@ impl<'a, EXT, NS: NodeSegmentOps> LockedNodePage<'a, NS> { } } + pub fn segment(&self) -> &NS { + self.page + } + #[inline(always)] pub fn writer(&mut self) -> NodeWriter<'_, &mut MemNodeSegment, NS> { NodeWriter::new(self.page, self.layer_counter, self.lock.deref_mut()) @@ -63,7 +67,7 @@ pub struct WriteLockedNodePages<'a, NS> { writers: Vec>, } -impl<'a, NS> Default for WriteLockedNodePages<'_, NS> { +impl Default for WriteLockedNodePages<'_, NS> { fn default() -> Self { Self { writers: Vec::new(), @@ -93,4 +97,8 @@ impl<'a, EXT, NS: NodeSegmentOps> WriteLockedNodePages<'a, NS> writer.ensure_layer(layer_id); } } + + pub fn len(&self) -> usize { + self.writers.len() + } } diff --git a/db4-storage/src/pages/node_page/writer.rs b/db4-storage/src/pages/node_page/writer.rs index 6a1b8becf8..48e6fa52f5 100644 --- a/db4-storage/src/pages/node_page/writer.rs +++ b/db4-storage/src/pages/node_page/writer.rs @@ -12,7 +12,7 @@ use std::ops::DerefMut; #[derive(Debug)] pub struct NodeWriter<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> { pub page: &'a NS, - pub writer: MP, // TODO: rename to m_segment + pub mut_segment: MP, pub l_counter: &'a GraphStats, } @@ -20,7 +20,7 @@ impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> NodeWri pub fn new(page: &'a NS, global_num_nodes: &'a GraphStats, writer: MP) -> Self { Self { page, - writer, + mut_segment: writer, l_counter: global_num_nodes, } } @@ -62,7 +62,9 @@ impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> NodeWri let e_id = e_id.into(); let layer_id = e_id.layer(); - let is_new_node = self.writer.add_outbound_edge(t, src_pos, dst, e_id, lsn); + let is_new_node = self + .mut_segment + .add_outbound_edge(t, src_pos, dst, e_id, lsn); if is_new_node && !self.page.check_node(src_pos, layer_id) { self.l_counter.increment(layer_id); @@ -105,7 +107,9 @@ impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> NodeWri } let layer = e_id.layer(); let dst_pos = dst_pos.into(); - let is_new_node = self.writer.add_inbound_edge(t, dst_pos, src, e_id, lsn); + let is_new_node = self + .mut_segment + .add_inbound_edge(t, dst_pos, src, e_id, lsn); if is_new_node && !self.page.check_node(dst_pos, layer) { self.l_counter.increment(layer); @@ -120,9 +124,9 @@ impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> NodeWri props: impl IntoIterator, lsn: u64, ) { - self.writer.as_mut()[layer_id].set_lsn(lsn); + self.mut_segment.as_mut()[layer_id].set_lsn(lsn); self.l_counter.update_time(t.t()); - let is_new_node = self.writer.add_props(t, pos, layer_id, props); + let is_new_node = self.mut_segment.add_props(t, pos, layer_id, props); if is_new_node && !self.page.check_node(pos, layer_id) { self.l_counter.increment(layer_id); } @@ -135,8 +139,8 @@ impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> NodeWri props: impl IntoIterator, lsn: u64, ) { - self.writer.as_mut()[layer_id].set_lsn(lsn); - let is_new_node = self.writer.update_c_props(pos, layer_id, props); + self.mut_segment.as_mut()[layer_id].set_lsn(lsn); + let is_new_node = self.mut_segment.update_c_props(pos, layer_id, props); if is_new_node && !self.page.check_node(pos, layer_id) { self.l_counter.increment(layer_id); } @@ -145,18 +149,18 @@ impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> NodeWri pub fn update_timestamp(&mut self, t: T, pos: LocalPOS, e_id: ELID, lsn: u64) { let layer_id = e_id.layer(); self.l_counter.update_time(t.t()); - self.writer.as_mut()[layer_id].set_lsn(lsn); - self.writer.update_timestamp(t, pos, e_id); + self.mut_segment.as_mut()[layer_id].set_lsn(lsn); + self.mut_segment.update_timestamp(t, pos, e_id); } pub fn get_out_edge(&self, pos: LocalPOS, dst: VID, layer_id: usize) -> Option { self.page - .get_out_edge(pos, dst, layer_id, self.writer.deref()) + .get_out_edge(pos, dst, layer_id, self.mut_segment.deref()) } pub fn get_inb_edge(&self, pos: LocalPOS, src: VID, layer_id: usize) -> Option { self.page - .get_inb_edge(pos, src, layer_id, self.writer.deref()) + .get_inb_edge(pos, src, layer_id, self.mut_segment.deref()) } pub fn store_node_id_and_node_type( @@ -197,9 +201,8 @@ impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> Drop for NodeWriter<'a, MP, NS> { fn drop(&mut self) { - // S::persist_node_page(self.est_size, self.page, self.writer.deref_mut()); self.page - .notify_write(self.writer.deref_mut()) + .notify_write(self.mut_segment.deref_mut()) .expect("Failed to persist node page"); } } diff --git a/db4-storage/src/pages/node_store.rs b/db4-storage/src/pages/node_store.rs index 2a673fdd3e..48e5ba846a 100644 --- a/db4-storage/src/pages/node_store.rs +++ b/db4-storage/src/pages/node_store.rs @@ -24,7 +24,7 @@ use std::{ #[derive(Debug)] pub struct NodeStorageInner { pages: boxcar::Vec>, - layer_counter: Arc, + stats: Arc, nodes_path: PathBuf, max_page_len: usize, node_meta: Arc, @@ -97,7 +97,7 @@ impl, EXT: Clone> NodeStorageInner ) -> Self { Self { pages: boxcar::Vec::new(), - layer_counter: GraphStats::new().into(), + stats: GraphStats::new().into(), nodes_path: nodes_path.as_ref().to_path_buf(), max_page_len, node_meta, @@ -117,7 +117,7 @@ impl, EXT: Clone> NodeStorageInner .map(|(page_id, page)| { LockedNodePage::new( page_id, - &self.layer_counter, + &self.stats, self.max_page_len, page.as_ref(), page.head_mut(), @@ -128,7 +128,7 @@ impl, EXT: Clone> NodeStorageInner } pub fn num_layers(&self) -> usize { - self.layer_counter.len() + self.stats.len() } pub fn node<'a>(&'a self, node: impl Into) -> NS::Entry<'a> { @@ -151,19 +151,19 @@ impl, EXT: Clone> NodeStorageInner ) -> NodeWriter<'a, RwLockWriteGuard<'a, MemNodeSegment>, NS> { let segment = self.get_or_create_segment(segment_id); let head = segment.head_mut(); - NodeWriter::new(segment, &self.layer_counter, head) + NodeWriter::new(segment, &self.stats, head) } pub fn num_nodes(&self) -> usize { - self.layer_counter.get(0) + self.stats.get(0) } pub fn layer_num_nodes(&self, layer_id: usize) -> usize { - self.layer_counter.get(layer_id) + self.stats.get(layer_id) } pub fn stats(&self) -> &Arc { - &self.layer_counter + &self.stats } pub fn pages(&self) -> &boxcar::Vec> { @@ -257,7 +257,7 @@ impl, EXT: Clone> NodeStorageInner pages, nodes_path: nodes_path.to_path_buf(), max_page_len, - layer_counter: GraphStats::from(layer_counts).into(), + stats: GraphStats::from(layer_counts).into(), node_meta, edge_meta, ext, diff --git a/db4-storage/src/persist/strategy.rs b/db4-storage/src/persist/strategy.rs index 173f3b228d..0ef44d613d 100644 --- a/db4-storage/src/persist/strategy.rs +++ b/db4-storage/src/persist/strategy.rs @@ -8,7 +8,7 @@ use crate::segments::{ pub trait PersistentStrategy: Default + Clone + std::fmt::Debug + Send + Sync + 'static { type NS; type ES; - fn persist_node_page>( + fn persist_node_segment>( &self, node_page: &Self::NS, writer: MP, @@ -26,7 +26,7 @@ impl PersistentStrategy for () { type ES = EdgeSegmentView; type NS = NodeSegmentView; - fn persist_node_page>( + fn persist_node_segment>( &self, _node_page: &Self::NS, _writer: MP, diff --git a/db4-storage/src/properties/mod.rs b/db4-storage/src/properties/mod.rs index f979a20a99..3ba887acb7 100644 --- a/db4-storage/src/properties/mod.rs +++ b/db4-storage/src/properties/mod.rs @@ -16,7 +16,7 @@ use raphtory_core::{ pub mod props_meta_writer; -#[derive(Debug, Default)] +#[derive(Debug, Default, serde::Serialize)] pub struct Properties { c_properties: Vec, @@ -30,6 +30,7 @@ pub struct Properties { has_node_additions: bool, has_node_properties: bool, has_deletions: bool, + pub additions_count: usize, } pub(crate) struct PropMutEntry<'a> { @@ -255,6 +256,7 @@ impl Properties { } fn update_earliest_latest(&mut self, t: TimeIndexEntry) { + self.additions_count += 1; let earliest = self.earliest.get_or_insert(t); if t < *earliest { *earliest = t; @@ -324,6 +326,7 @@ impl<'a> PropMutEntry<'a> { let prop_timestamps = &mut self.properties.deletions[self.row]; prop_timestamps.set(t, edge_id.unwrap_or_default()); + self.properties.update_earliest_latest(t); } pub(crate) fn append_const_props>( diff --git a/db4-storage/src/segments/edge.rs b/db4-storage/src/segments/edge.rs index fe3eead52f..895233cb36 100644 --- a/db4-storage/src/segments/edge.rs +++ b/db4-storage/src/segments/edge.rs @@ -50,16 +50,18 @@ impl HasRow for MemPageEntry { #[derive(Debug)] pub struct MemEdgeSegment { layers: Vec>, + est_size: usize, } impl>> From for MemEdgeSegment { fn from(inner: I) -> Self { let layers: Vec<_> = inner.into_iter().collect(); + let est_size = layers.iter().map(|seg| seg.est_size()).sum(); assert!( !layers.is_empty(), "MemEdgeSegment must have at least one layer" ); - Self { layers } + Self { layers, est_size } } } @@ -79,6 +81,7 @@ impl MemEdgeSegment { pub fn new(segment_id: usize, max_page_len: usize, meta: Arc) -> Self { Self { layers: vec![SegmentContainer::new(segment_id, max_page_len, meta)], + est_size: 0, } } @@ -86,6 +89,24 @@ impl MemEdgeSegment { self.layers[0].meta() } + pub fn swap_out_layers(&mut self) -> Vec> { + let layers = self + .as_mut() + .iter_mut() + .map(|head_guard| { + let mut old_head = SegmentContainer::new( + head_guard.segment_id(), + head_guard.max_page_len(), + head_guard.meta().clone(), + ); + std::mem::swap(&mut *head_guard, &mut old_head); + old_head + }) + .collect::>(); + self.est_size = 0; // Reset estimated size after swapping out layers + layers + } + pub fn get_or_create_layer(&mut self, layer_id: usize) -> &mut SegmentContainer { if layer_id >= self.layers.len() { let max_page_len = self.layers[0].max_page_len(); @@ -103,7 +124,7 @@ impl MemEdgeSegment { } pub fn est_size(&self) -> usize { - self.layers.iter().map(|seg| seg.est_size()).sum::() + self.est_size } pub fn lsn(&self) -> u64 { @@ -138,6 +159,7 @@ impl MemEdgeSegment { // Ensure we have enough layers self.ensure_layer(layer_id); + let est_size = self.layers[layer_id].est_size(); self.layers[layer_id].set_lsn(lsn); let local_row = self.reserve_local_row(edge_pos, src, dst, layer_id); @@ -146,7 +168,9 @@ impl MemEdgeSegment { .properties_mut() .get_mut_entry(local_row); let ts = TimeIndexEntry::new(t.t(), t.i()); - prop_entry.append_t_props(ts, props) + prop_entry.append_t_props(ts, props); + let layer_est_size = self.layers[layer_id].est_size(); + self.est_size += layer_est_size.saturating_sub(est_size); } pub fn delete_edge_internal( @@ -165,11 +189,14 @@ impl MemEdgeSegment { // Ensure we have enough layers self.ensure_layer(layer_id); + let est_size = self.layers[layer_id].est_size(); self.layers[layer_id].set_lsn(lsn); let local_row = self.reserve_local_row(edge_pos, src, dst, layer_id); let props = self.layers[layer_id].properties_mut(); props.get_mut_entry(local_row).deletion_timestamp(t, None); + let layer_est_size = self.layers[layer_id].est_size(); + self.est_size += layer_est_size.saturating_sub(est_size); } pub fn insert_static_edge_internal( @@ -186,8 +213,11 @@ impl MemEdgeSegment { // Ensure we have enough layers self.ensure_layer(layer_id); self.layers[layer_id].set_lsn(lsn); + let est_size = self.layers[layer_id].est_size(); self.reserve_local_row(edge_pos, src, dst, layer_id); + let layer_est_size = self.layers[layer_id].est_size(); + self.est_size += layer_est_size.saturating_sub(est_size); } fn ensure_layer(&mut self, layer_id: usize) { @@ -254,12 +284,16 @@ impl MemEdgeSegment { // Ensure we have enough layers self.ensure_layer(layer_id); + let est_size = self.layers[layer_id].est_size(); let local_row = self.reserve_local_row(edge_pos, src, dst, layer_id); let mut prop_entry: PropMutEntry<'_> = self.layers[layer_id] .properties_mut() .get_mut_entry(local_row); - prop_entry.append_const_props(props) + prop_entry.append_const_props(props); + + let layer_est_size = self.layers[layer_id].est_size() + 8; + self.est_size += layer_est_size.saturating_sub(est_size); } pub fn contains_edge(&self, edge_pos: LocalPOS, layer_id: usize) -> bool { @@ -501,3 +535,92 @@ impl>> EdgeSegmentOps for EdgeSegm .map_or(0, |layer| layer.len()) } } + +#[cfg(test)] +mod test { + use raphtory_api::core::entities::properties::prop::PropType; + + #[test] + fn est_size_changes() { + use super::*; + use raphtory_api::core::entities::properties::meta::Meta; + + let meta = Arc::new(Meta::default()); + let mut segment = MemEdgeSegment::new(1, 100, meta.clone()); + + assert_eq!(segment.est_size(), 0); + + segment.insert_edge_internal( + TimeIndexEntry::new(1, 0), + LocalPOS(0), + 1, + 2, + 0, + vec![(0, Prop::from("test"))], + 1, + ); + + let est_size1 = segment.est_size(); + + assert!(est_size1 > 0); + + segment.delete_edge_internal(TimeIndexEntry::new(2, 3), LocalPOS(0), 5, 3, 0, 0); + + let est_size2 = segment.est_size(); + + assert!( + est_size2 > est_size1, + "Expected size to increase after deletion, but it did not." + ); + + // same edge insertion again to check size increase + segment.insert_edge_internal( + TimeIndexEntry::new(3, 0), + LocalPOS(1), + 4, + 6, + 0, + vec![(0, Prop::from("test2"))], + 1, + ); + + let est_size3 = segment.est_size(); + assert!( + est_size3 > est_size2, + "Expected size to increase after re-insertion, but it did not." + ); + + // Insert a static edge + + segment.insert_static_edge_internal(LocalPOS(1), 4, 6, 0, 1); + + let est_size4 = segment.est_size(); + assert_eq!( + est_size4, est_size3, + "Expected size to remain the same after static edge insertion, but it changed." + ); + + let prop_id = meta + .const_prop_meta() + .get_or_create_and_validate("a", PropType::U8) + .unwrap() + .inner(); + + segment.update_const_properties(LocalPOS(1), 4, 6, 0, [(prop_id, Prop::U8(2))]); + + let est_size5 = segment.est_size(); + assert!( + est_size5 > est_size4, + "Expected size to increase after updating properties, but it did not." + ); + + // update const properties for the other edge, hard to predict size change + // segment.update_const_properties(LocalPOS(0), 1, 2, 0, [(prop_id, Prop::U8(3))]); + + // let est_size6 = segment.est_size(); + // assert!( + // est_size6 > est_size5, + // "Expected size to increase after updating properties for the other edge, but it did not." + // ); + } +} diff --git a/db4-storage/src/segments/mod.rs b/db4-storage/src/segments/mod.rs index 3c95246f33..ba5bffd252 100644 --- a/db4-storage/src/segments/mod.rs +++ b/db4-storage/src/segments/mod.rs @@ -24,6 +24,7 @@ pub mod additions; pub mod edge_entry; pub mod node_entry; +#[derive(serde::Serialize)] pub struct SegmentContainer { segment_id: usize, items: BitVec, @@ -41,8 +42,8 @@ impl Debug for SegmentContainer { .iter() .map(|x| if *x { 1 } else { 0 }) .collect::>(); - let mut data = self.data.iter().map(|(k, v)| (k, v)).collect::>(); - data.sort_by(|a, b| a.0.cmp(&b.0)); + let mut data = self.data.iter().collect::>(); + data.sort_by(|a, b| a.0.cmp(b.0)); f.debug_struct("SegmentContainer") .field("page_id", &self.segment_id) @@ -75,9 +76,13 @@ impl SegmentContainer { #[inline] pub fn est_size(&self) -> usize { - //FIXME: this is a rough estimate and should be improved + //TODO: this is a rough estimate and should be improved let data_size = (self.data.len() as f64 * std::mem::size_of::() as f64 * 1.5) as usize; // Estimate size of data - data_size + self.t_prop_est_size() + self.c_prop_est_size() + let timestamp_size = std::mem::size_of::(); + (self.properties.additions_count * timestamp_size) + + data_size + + self.t_prop_est_size() + + self.c_prop_est_size() } pub fn get(&self, item_pos: &LocalPOS) -> Option<&T> { @@ -100,6 +105,10 @@ impl SegmentContainer { self.properties.t_len() } + /// Reserves a local row for the given item position. + /// If the item position already exists, it returns a mutable reference to the existing item. + /// Left variant indicates that the item was already present, + /// Right variant indicates that a new item was created. pub(crate) fn reserve_local_row(&mut self, item_pos: LocalPOS) -> Either<&mut T, &mut T> { let local_row = self.data.len(); self.set_item(item_pos); diff --git a/db4-storage/src/segments/node.rs b/db4-storage/src/segments/node.rs index 2655c069f1..2dc3dd6595 100644 --- a/db4-storage/src/segments/node.rs +++ b/db4-storage/src/segments/node.rs @@ -25,11 +25,12 @@ use crate::{ segments::node_entry::{MemNodeEntry, MemNodeRef}, }; -#[derive(Debug)] +#[derive(Debug, serde::Serialize)] pub struct MemNodeSegment { segment_id: usize, max_page_len: usize, layers: Vec>, + est_size: usize, } impl>> From for MemNodeSegment { @@ -41,15 +42,17 @@ impl>> From for MemNodeSegm ); let segment_id = layers[0].segment_id(); let max_page_len = layers[0].max_page_len(); + let est_size = layers.iter().map(|l| l.est_size()).sum::(); Self { segment_id, max_page_len, layers, + est_size, } } } -#[derive(Debug, Default)] +#[derive(Debug, Default, serde::Serialize)] pub struct AdjEntry { row: usize, adj: Adj, @@ -92,6 +95,28 @@ impl AsMut<[SegmentContainer]> for MemNodeSegment { } impl MemNodeSegment { + pub fn segment_id(&self) -> usize { + self.segment_id + } + + pub fn swap_out_layers(&mut self) -> Vec> { + let layers = self + .layers + .iter_mut() + .map(|head_guard| { + let mut old_head = SegmentContainer::new( + head_guard.segment_id(), + head_guard.max_page_len(), + head_guard.meta().clone(), + ); + std::mem::swap(&mut *head_guard, &mut old_head); + old_head + }) + .collect::>(); + self.est_size = 0; // Reset estimated size after swapping out layers + layers + } + pub fn get_or_create_layer(&mut self, layer_id: usize) -> &mut SegmentContainer { if layer_id >= self.layers.len() { let max_page_len = self.layers[0].max_page_len(); @@ -121,7 +146,7 @@ impl MemNodeSegment { } pub fn est_size(&self) -> usize { - self.layers.iter().map(|seg| seg.est_size()).sum::() + self.est_size } pub fn to_vid(&self, pos: LocalPOS) -> VID { @@ -139,7 +164,7 @@ impl MemNodeSegment { pub fn has_node(&self, n: LocalPOS, layer_id: usize) -> bool { self.layers .get(layer_id) - .is_some_and(|layer| layer.items().get(n.0).map_or(false, |v| *v)) + .is_some_and(|layer| layer.items().first().is_some_and(|v| *v)) } pub fn get_out_edge(&self, n: LocalPOS, dst: VID, layer_id: usize) -> Option { @@ -169,6 +194,7 @@ impl MemNodeSegment { segment_id, max_page_len, layers: vec![SegmentContainer::new(segment_id, max_page_len, meta)], + est_size: 0, } } @@ -184,23 +210,33 @@ impl MemNodeSegment { let e_id = e_id.into(); let layer_id = e_id.layer(); let layer = self.get_or_create_layer(layer_id); + let est_size = layer.est_size(); layer.set_lsn(lsn); let add_out = layer.reserve_local_row(src_pos).map_either( |row| { - row.adj.add_edge_out(dst, e_id.edge); - row.row() + let new_mem_edge = row.adj.add_edge_out(dst, e_id.edge); + (new_mem_edge, row.row()) }, |row| { row.adj.add_edge_out(dst, e_id.edge); - row.row() + (true, row.row()) }, ); + let is_new_edge = add_out.as_ref().either( + |(is_new, _)| *is_new as usize, + |(is_new, _)| *is_new as usize, + ); + let new_entry = add_out.is_right(); if let Some(t) = t { - self.update_timestamp_inner(t, add_out.either(|a| a, |a| a), e_id); + self.update_timestamp_inner(t, add_out.either(|(_, a)| a, |(_, a)| a), e_id); } + let layer_est_size = self.layers[layer_id].est_size(); + let added_size = + (layer_est_size - est_size) + (is_new_edge * std::mem::size_of::<(VID, VID)>()); + self.est_size += added_size; new_entry } @@ -218,21 +254,31 @@ impl MemNodeSegment { let dst_pos = dst_pos.into(); let layer = self.get_or_create_layer(layer_id); + let est_size = layer.est_size(); layer.set_lsn(lsn); let add_in = layer.reserve_local_row(dst_pos).map_either( |row| { - row.adj.add_edge_into(src, e_id.edge); - row.row() + let new_mem_edge = row.adj.add_edge_into(src, e_id.edge); + (new_mem_edge, row.row()) }, |row| { row.adj.add_edge_into(src, e_id.edge); - row.row() + (true, row.row()) }, ); + let is_new_edge = add_in.as_ref().either( + |(is_new, _)| *is_new as usize, + |(is_new, _)| *is_new as usize, + ); + let new_entry = add_in.is_right(); if let Some(t) = t { - self.update_timestamp_inner(t, add_in.either(|a| a, |a| a), e_id); + self.update_timestamp_inner(t, add_in.either(|(_, a)| a, |(_, a)| a), e_id); } + let layer_est_size = self.layers[layer_id].est_size(); + let added_size = + (layer_est_size - est_size) + (is_new_edge * std::mem::size_of::<(VID, VID)>()); + self.est_size += added_size; new_entry } @@ -246,10 +292,18 @@ impl MemNodeSegment { } pub fn update_timestamp(&mut self, t: T, node_pos: LocalPOS, e_id: ELID) { - let row = self.layers[e_id.layer()] - .reserve_local_row(node_pos) - .either(|a| a.row, |a| a.row); + let (row, est_size) = { + let segment_container = &mut self.layers[e_id.layer()]; + let est_size = segment_container.est_size(); + let row = segment_container + .reserve_local_row(node_pos) + .either(|a| a.row, |a| a.row); + (row, est_size) + }; self.update_timestamp_inner(t, row, e_id); + let layer_est_size = self.layers[e_id.layer()].est_size(); + let added_size = layer_est_size - est_size; + self.est_size += added_size; } pub fn add_props( @@ -260,12 +314,15 @@ impl MemNodeSegment { props: impl IntoIterator, ) -> bool { let layer = self.get_or_create_layer(layer_id); + let est_size = layer.est_size(); let row = layer.reserve_local_row(node_pos); let is_new = row.is_right(); let row = row.either(|a| a.row, |a| a.row); - let mut prop_mut_entry = self.layers[layer_id].properties_mut().get_mut_entry(row); + let mut prop_mut_entry = layer.properties_mut().get_mut_entry(row); let ts = TimeIndexEntry::new(t.t(), t.i()); prop_mut_entry.append_t_props(ts, props); + let layer_est_size = layer.est_size(); + self.est_size += layer_est_size - est_size; is_new } @@ -275,13 +332,20 @@ impl MemNodeSegment { layer_id: usize, props: impl IntoIterator, ) -> bool { - let row = self.layers[layer_id] + let segment_container = &mut self.layers[layer_id]; + let est_size = segment_container.est_size(); + + let row = segment_container .reserve_local_row(node_pos) .map_either(|a| a.row, |a| a.row); let is_new = row.is_right(); let row = row.either(|a| a, |a| a); - let mut prop_mut_entry = self.layers[layer_id].properties_mut().get_mut_entry(row); + let mut prop_mut_entry = segment_container.properties_mut().get_mut_entry(row); prop_mut_entry.append_const_props(props); + + let layer_est_size = segment_container.est_size(); + let added_size = (layer_est_size - est_size) + 8; // random estimate for constant properties + self.est_size += added_size; is_new } @@ -440,3 +504,113 @@ impl>> NodeSegmentOps for NodeSegm .map_or(0, |layer| layer.len()) } } + +#[cfg(test)] +mod test { + use std::{ops::Deref, sync::Arc}; + + use raphtory_api::core::entities::properties::{ + meta::Meta, + prop::{Prop, PropType}, + }; + use raphtory_core::entities::{EID, ELID, VID}; + use tempfile::tempdir; + + use crate::{ + LocalPOS, + api::nodes::NodeSegmentOps, + pages::{layer_counter::GraphStats, node_page::writer::NodeWriter}, + segments::node::NodeSegmentView, + }; + + #[test] + fn est_size_changes() { + let node_meta = Arc::new(Meta::default()); + let edge_meta = Arc::new(Meta::default()); + let path = tempdir().unwrap(); + let ext = (); + let segment = NodeSegmentView::new(0, 10, node_meta.clone(), edge_meta, path.path(), ext); + let stats = GraphStats::default(); + + let mut writer = NodeWriter::new(&segment, &stats, segment.head_mut()); + + let est_size1 = writer.mut_segment.est_size(); + assert_eq!(est_size1, 0); + + writer.add_outbound_edge(Some(1), LocalPOS(1), VID(3), EID(7).with_layer(0), 0); + + let est_size2 = writer.mut_segment.est_size(); + assert!( + est_size2 > est_size1, + "Estimated size should be greater than 0 after adding an edge" + ); + + writer.add_inbound_edge(Some(1), LocalPOS(2), VID(4), EID(8).with_layer(0), 0); + + let est_size3 = writer.mut_segment.est_size(); + assert!( + est_size3 > est_size2, + "Estimated size should increase after adding an inbound edge" + ); + + // no change when adding the same edge again + + writer.add_outbound_edge::(None, LocalPOS(1), VID(3), EID(7).with_layer(0), 0); + let est_size4 = writer.mut_segment.est_size(); + assert_eq!( + est_size4, est_size3, + "Estimated size should not change when adding the same edge again" + ); + + // add constant properties + + let prop_id = node_meta + .const_prop_meta() + .get_or_create_and_validate("a", PropType::U64) + .unwrap() + .inner(); + + writer.update_c_props(LocalPOS(1), 0, [(prop_id, Prop::U64(73))], 0); + + let est_size5 = writer.mut_segment.est_size(); + assert!( + est_size5 > est_size4, + "Estimated size should increase after adding constant properties" + ); + + writer.update_timestamp(17, LocalPOS(1), ELID::new(EID(0), 0), 0); + + let est_size6 = writer.mut_segment.est_size(); + assert!( + est_size6 > est_size5, + "Estimated size should increase after updating timestamp" + ); + + // add temporal properties + let prop_id = node_meta + .temporal_prop_meta() + .get_or_create_and_validate("b", PropType::F64) + .unwrap() + .inner(); + + writer.add_props(42, LocalPOS(1), 0, [(prop_id, Prop::F64(4.13))], 0); + + let est_size7 = writer.mut_segment.est_size(); + assert!( + est_size7 > est_size6, + "Estimated size should increase after adding temporal properties" + ); + + writer.add_props(72, LocalPOS(1), 0, [(prop_id, Prop::F64(5.41))], 0); + let est_size8 = writer.mut_segment.est_size(); + assert!( + est_size8 > est_size7, + "Estimated size should increase after adding another temporal property" + ); + + let actual_size = bincode::serialize(writer.mut_segment.deref()) + .unwrap() + .len(); + println!("{actual_size} vs {est_size7}"); + } +} diff --git a/raphtory-api/src/core/entities/properties/meta.rs b/raphtory-api/src/core/entities/properties/meta.rs index 8ccb92d3cd..3dffda30a7 100644 --- a/raphtory-api/src/core/entities/properties/meta.rs +++ b/raphtory-api/src/core/entities/properties/meta.rs @@ -260,7 +260,7 @@ impl PropMapper { let dtype_read = self.dtypes.read_recursive(); if let Some(old_type) = dtype_read.get(id) { let mut unified = false; - if let Ok(_) = unify_types(&dtype, old_type, &mut unified) { + if unify_types(&dtype, old_type, &mut unified).is_ok() { if !unified { // means the types were equal, no change needed return Ok(wrapped_id); diff --git a/raphtory-api/src/core/entities/properties/prop/prop_enum.rs b/raphtory-api/src/core/entities/properties/prop/prop_enum.rs index 5deff1d743..4d500a29b0 100644 --- a/raphtory-api/src/core/entities/properties/prop/prop_enum.rs +++ b/raphtory-api/src/core/entities/properties/prop/prop_enum.rs @@ -195,7 +195,7 @@ impl Prop { .reduce(|a, b| unify_types(&a?, &b?, &mut false)) .transpose() .map(|e| e.unwrap_or(PropType::Empty)) - .unwrap_or_else(|e| panic!("Cannot unify types for list {:?}: {e:?}", list)); + .unwrap_or_else(|e| panic!("Cannot unify types for list {list:?}: {e:?}")); PropType::List(Box::new(list_type)) } Prop::Map(map) => PropType::map(map.iter().map(|(k, v)| (k, v.dtype()))), diff --git a/raphtory-core/Cargo.toml b/raphtory-core/Cargo.toml index 24e1f00507..2c9f503cd3 100644 --- a/raphtory-core/Cargo.toml +++ b/raphtory-core/Cargo.toml @@ -13,7 +13,8 @@ edition.workspace = true [dependencies] raphtory-api = { path = "../raphtory-api", version = "0.15.1" } -dashmap = { workspace = true } +dashmap = { workspace = true, features = ["raw-api"] } +hashbrown = { workspace = true } either = { workspace = true } serde = { workspace = true, features = ["derive"] } rustc-hash = { workspace = true } @@ -35,4 +36,4 @@ proptest = { workspace = true } [features] arrow = ["raphtory-api/arrow"] -python = ["dep:pyo3", "raphtory-api/python"] \ No newline at end of file +python = ["dep:pyo3", "raphtory-api/python"] diff --git a/raphtory-core/src/entities/graph/logical_to_physical.rs b/raphtory-core/src/entities/graph/logical_to_physical.rs index 4f5b797edf..e9424e2aa1 100644 --- a/raphtory-core/src/entities/graph/logical_to_physical.rs +++ b/raphtory-core/src/entities/graph/logical_to_physical.rs @@ -2,15 +2,20 @@ use crate::{ entities::nodes::node_store::NodeStore, storage::{NodeSlot, UninitialisedEntry}, }; -use dashmap::mapref::entry::Entry; +use dashmap::{mapref::entry::Entry, RwLockWriteGuard, SharedValue}; use either::Either; +use hashbrown::raw::RawTable; use once_cell::sync::OnceCell; use raphtory_api::core::{ entities::{GidRef, GidType, VID}, storage::{dict_mapper::MaybeNew, FxDashMap}, }; +use rayon::prelude::*; use serde::{Deserialize, Deserializer, Serialize}; -use std::hash::Hash; +use std::{ + hash::{BuildHasher, Hash}, + panic, +}; use thiserror::Error; #[derive(Debug, Deserialize, Serialize)] @@ -41,6 +46,40 @@ impl Map { _ => None, } } + + pub fn run_with_locked) -> Result<(), E> + Send + Sync>( + &self, + work_fn: FN, + ) -> Result<(), E> { + match self { + Map::U64(map) => { + let shards = map.shards(); + shards + .par_iter() + .enumerate() + .try_for_each(|(shard_id, shard)| { + work_fn(ResolverShard::U64 { + guard: shard.write(), + map, + shard_id, + }) + }) + } + Map::Str(map) => { + let shards = map.shards(); + shards + .par_iter() + .enumerate() + .try_for_each(|(shard_id, shard)| { + work_fn(ResolverShard::Str { + guard: shard.write(), + map, + shard_id, + }) + }) + } + } + } } impl Default for Map { @@ -54,6 +93,112 @@ pub struct Mapping { map: OnceCell, } +pub enum ResolverShard<'a> { + U64 { + guard: RwLockWriteGuard<'a, RawTable<(u64, SharedValue)>>, + map: &'a FxDashMap, + shard_id: usize, + }, + Str { + guard: RwLockWriteGuard<'a, RawTable<(String, SharedValue)>>, + map: &'a FxDashMap, + shard_id: usize, + }, +} + +pub struct U64ResolverShard<'a> { + guard: RwLockWriteGuard<'a, RawTable<(u64, SharedValue)>>, + map: &'a FxDashMap, + shard_id: usize, +} + +pub struct StrResolverShard<'a> { + guard: RwLockWriteGuard<'a, RawTable<(String, SharedValue)>>, + map: &'a FxDashMap, + shard_id: usize, +} + +impl<'a> ResolverShard<'a> { + pub fn resolve_nodes_u64<'b, I: Iterator>( + &mut self, + nodes: impl Fn() -> I, + next_id: impl FnOnce() -> VID, + ) { + } + + pub fn resolve_nodes_str<'b, I: Iterator>( + &mut self, + nodes: impl Fn() -> I, + next_id: impl FnOnce() -> VID, + ) { + } + + pub fn resolve_node<'b>(&mut self, node_ref: GidRef<'b>, next_id: impl FnOnce() -> VID) -> VID { + match self { + ResolverShard::U64 { + guard, + map, + shard_id, + } => { + if let GidRef::U64(id) = node_ref { + let shard_ind = map.determine_map(&id); + let mut factory = map.hasher().clone(); + let hash = factory.hash_one(&id); + // let data = (id, SharedValue::new(value)) + + match guard.get(hash, |(k, _)| k == &id) { + Some((_, vid)) => { + // Node already exists, do nothing + *(vid.get()) + } + None => { + // Node does not exist, create it + let vid = next_id(); + + guard.insert(hash, (id, SharedValue::new(vid)), |t| { + factory.hash_one(t.0) + }); + vid + } + } + } else { + panic!("Expected GidRef::U64, got {:?}", node_ref); + } + } + ResolverShard::Str { + guard, + map, + shard_id, + } => { + if let GidRef::Str(id) = node_ref { + let shard_ind = map.determine_map(id); + let mut factory = map.hasher().clone(); + let hash = factory.hash_one(&id); + + match guard.get(hash, |(k, _)| k == &id) { + Some((_, vid)) => { + // Node already exists, do nothing + *(vid.get()) + } + None => { + // Node does not exist, create it + let vid = next_id(); + + guard.insert(hash, (id.to_owned(), SharedValue::new(vid)), |t| { + factory.hash_one(&t.0) + }); + vid + } + } + } else { + panic!("Expected GidRef::Str, got {:?}", node_ref); + } + } + }; + VID(0) + } +} + impl Mapping { pub fn len(&self) -> usize { self.map.get().map_or(0, |map| match map { @@ -62,6 +207,18 @@ impl Mapping { }) } + pub fn run_with_locked) -> Result<(), E> + Send + Sync>( + &self, + work_fn: FN, + ) -> Result<(), E> { + let inner_map = self.map.get().unwrap(); + inner_map.run_with_locked(work_fn) + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + pub fn dtype(&self) -> Option { self.map.get().map(|map| match map { Map::U64(_) => GidType::U64, @@ -170,8 +327,7 @@ impl Mapping { }); match gid { GidRef::U64(id) => { - map.as_u64() - .ok_or_else(|| InvalidNodeId::InvalidNodeIdU64(id))?; + map.as_u64().ok_or(InvalidNodeId::InvalidNodeIdU64(id))?; } GidRef::Str(id) => { map.as_str() diff --git a/raphtory-core/src/entities/nodes/node_store.rs b/raphtory-core/src/entities/nodes/node_store.rs index e32d40752f..a9b95b5810 100644 --- a/raphtory-core/src/entities/nodes/node_store.rs +++ b/raphtory-core/src/entities/nodes/node_store.rs @@ -262,7 +262,7 @@ impl NodeStore { } } - pub fn add_edge(&mut self, v_id: VID, dir: Direction, layer: usize, edge_id: EID) { + pub fn add_edge(&mut self, v_id: VID, dir: Direction, layer: usize, edge_id: EID) -> bool { if layer >= self.layers.len() { self.layers.resize_with(layer + 1, || Adj::Solo); } @@ -270,7 +270,7 @@ impl NodeStore { match dir { Direction::IN => self.layers[layer].add_edge_into(v_id, edge_id), Direction::OUT => self.layers[layer].add_edge_out(v_id, edge_id), - _ => {} + _ => false, } } diff --git a/raphtory-core/src/entities/nodes/structure/adj.rs b/raphtory-core/src/entities/nodes/structure/adj.rs index 8dba2bbdbe..b1f045e144 100644 --- a/raphtory-core/src/entities/nodes/structure/adj.rs +++ b/raphtory-core/src/entities/nodes/structure/adj.rs @@ -46,16 +46,24 @@ impl Adj { } } - pub fn add_edge_into(&mut self, v: VID, e: EID) { + pub fn add_edge_into(&mut self, v: VID, e: EID) -> bool { match self { - Adj::Solo => *self = Self::new_into(v, e), + Adj::Solo => { + *self = Self::new_into(v, e); + true + } Adj::List { into, .. } => into.push(v, e), } } - pub fn add_edge_out(&mut self, v: VID, e: EID) { + /// Adds an edge in the out direction, creating a new adjacency if necessary. + /// Returns `true` if the edge was added, `false` if it already exists. + pub fn add_edge_out(&mut self, v: VID, e: EID) -> bool { match self { - Adj::Solo => *self = Self::new_out(v, e), + Adj::Solo => { + *self = Self::new_out(v, e); + true + } Adj::List { out, .. } => out.push(v, e), } } diff --git a/raphtory-core/src/entities/nodes/structure/adjset.rs b/raphtory-core/src/entities/nodes/structure/adjset.rs index 3f56b566a2..dad7bc3d5f 100644 --- a/raphtory-core/src/entities/nodes/structure/adjset.rs +++ b/raphtory-core/src/entities/nodes/structure/adjset.rs @@ -48,26 +48,36 @@ impl + Copy + Send + Sync> Ad Self::One(v, e) } - pub fn push(&mut self, v: K, e: V) { + /// Push a new node and edge into the adjacency set. + /// + /// If the node already exists, it will not be added again. + /// Returns `true` if the node was added, `false` if it already existed + pub fn push(&mut self, v: K, e: V) -> bool { match self { AdjSet::Empty => { *self = Self::new(v, e); + true } AdjSet::One(vv, ee) => { if *vv < v { *self = Self::Small { vs: vec![*vv, v], edges: vec![*ee, e], - } + }; + true } else if *vv > v { *self = Self::Small { vs: vec![v, *vv], edges: vec![e, *ee], - } + }; + true + } else { + // already exists + false } } AdjSet::Small { vs, edges } => match vs.binary_search(&v) { - Ok(_) => {} + Ok(_) => false, Err(i) => { if vs.len() < SMALL_SET { vs.insert(i, v); @@ -78,11 +88,10 @@ impl + Copy + Send + Sync> Ad map.insert(v, e); *self = Self::Large { vs: map } } + true } }, - AdjSet::Large { vs } => { - vs.insert(v, e); - } + AdjSet::Large { vs } => vs.insert(v, e).is_none(), } } diff --git a/raphtory/Cargo.toml b/raphtory/Cargo.toml index a3362e0c8b..21e44984f0 100644 --- a/raphtory/Cargo.toml +++ b/raphtory/Cargo.toml @@ -128,6 +128,7 @@ io = [ "dep:parquet", "dep:arrow-json", "proto", + "kdam", ] # search @@ -145,7 +146,7 @@ vectors = [ "dep:heed", "dep:sysinfo", "dep:moka", - "dep:tempfile", # also used for the storage feature + "dep:tempfile", # also used for the storage feature ] # Enables generating the pyo3 python bindings @@ -181,7 +182,7 @@ arrow = [ "dep:polars-parquet", "polars-parquet?/compression", "raphtory-api/arrow", - "raphtory-core/arrow" + "raphtory-core/arrow", ] proto = [ diff --git a/raphtory/src/io/arrow/df_loaders.rs b/raphtory/src/io/arrow/df_loaders.rs index 433c4cdb0f..47b919205b 100644 --- a/raphtory/src/io/arrow/df_loaders.rs +++ b/raphtory/src/io/arrow/df_loaders.rs @@ -11,7 +11,6 @@ use crate::{ serialise::incremental::InternalCache, }; use bytemuck::checked::cast_slice_mut; -#[cfg(feature = "python")] use kdam::{Bar, BarBuilder, BarExt}; use raphtory_api::{ atomic_extra::atomic_usize_from_mut_slice, @@ -20,7 +19,10 @@ use raphtory_api::{ storage::{dict_mapper::MaybeNew, timeindex::TimeIndexEntry}, }, }; -use raphtory_core::storage::timeindex::AsTime; +use raphtory_core::{ + entities::{graph::logical_to_physical::Mapping, VID}, + storage::timeindex::AsTime, +}; use raphtory_storage::mutation::addition_ops::{InternalAdditionOps, SessionAdditionOps}; use rayon::prelude::*; use std::{ @@ -30,8 +32,8 @@ use std::{ Arc, }, }; +use storage::{api::nodes::NodeSegmentOps, resolver::mapping_resolver::MappingResolver}; -#[cfg(feature = "python")] fn build_progress_bar(des: String, num_rows: usize) -> Result { BarBuilder::default() .desc(des) @@ -233,9 +235,7 @@ pub(crate) fn load_edges_from_df< .map_err(into_graph_err) })?; - #[cfg(feature = "python")] let mut pb = build_progress_bar("Loading edges".to_string(), df_view.num_rows)?; - #[cfg(feature = "python")] let _ = pb.update(0); let mut start_idx = session .reserve_event_ids(df_view.num_rows) @@ -249,7 +249,29 @@ pub(crate) fn load_edges_from_df< let cache = graph.get_cache(); let mut write_locked_graph = graph.write_lock().map_err(into_graph_err)?; - for chunk in df_view.chunks { + // set the type of the resolver; + let resolver = Mapping::new(); + let mut chunks = df_view.chunks.peekable(); + + if let Some(chunk) = chunks.peek() { + if let Ok(chunk) = chunk { + let src_col = chunk.node_col(src_index)?; + let dst_col = chunk.node_col(dst_index)?; + src_col.validate(graph, LoadError::MissingSrcError)?; + dst_col.validate(graph, LoadError::MissingDstError)?; + let mut iter = src_col.iter(); + + if let Some(id) = iter.next() { + graph + .resolve_node(id.as_node_ref()) + .map_err(|_| LoadError::FatalError)?; // initialize the type of the resolver + } + } + } else { + return Ok(()); + } + + for chunk in chunks { let df = chunk?; let prop_cols = combine_properties(properties, &properties_indices, &df, |key, dtype| { session @@ -266,6 +288,10 @@ pub(crate) fn load_edges_from_df< .map_err(into_graph_err) }, )?; + + let src_col_shared = atomic_usize_from_mut_slice(cast_slice_mut(&mut src_col_resolved)); + let dst_col_shared = atomic_usize_from_mut_slice(cast_slice_mut(&mut dst_col_resolved)); + let layer = lift_layer_col(layer, layer_index, &df)?; let layer_col_resolved = layer.resolve(graph)?; @@ -275,8 +301,19 @@ pub(crate) fn load_edges_from_df< let dst_col = df.node_col(dst_index)?; dst_col.validate(graph, LoadError::MissingDstError)?; + let node_count = &write_locked_graph.graph().node_count; + + resolver.run_with_locked(|mut shard| { + for id in src_col.iter().chain(dst_col.iter()) { + shard.resolve_node(id, || VID(node_count.fetch_add(1, Ordering::Relaxed))); + //.map_err(|_| LoadError::FatalError)?; + } + Ok::<(), LoadError>(()) + })?; + let time_col = df.time_col(time_index)?; + let num_nodes = AtomicUsize::new(write_locked_graph.graph().internal_num_nodes()); // It's our graph, no one else can change it src_col_resolved.resize_with(df.len(), Default::default); src_col @@ -288,6 +325,9 @@ pub(crate) fn load_edges_from_df< .graph() .resolve_node(gid.as_node_ref()) .map_err(|_| LoadError::FatalError)?; + if vid.is_new() { + num_nodes.fetch_add(1, Ordering::Relaxed); + } if let Some(cache) = cache { cache.resolve_node(vid, gid); } @@ -305,6 +345,9 @@ pub(crate) fn load_edges_from_df< .graph() .resolve_node(gid.as_node_ref()) .map_err(|_| LoadError::FatalError)?; + if vid.is_new() { + num_nodes.fetch_add(1, Ordering::Relaxed); + } if let Some(cache) = cache { cache.resolve_node(vid, gid); } @@ -312,8 +355,7 @@ pub(crate) fn load_edges_from_df< Ok::<(), LoadError>(()) })?; - write_locked_graph - .resize_chunks_to_num_nodes(write_locked_graph.graph().internal_num_nodes()); + write_locked_graph.resize_chunks_to_num_nodes(num_nodes.load(Ordering::Relaxed)); // resolve all the edges eid_col_resolved.resize_with(df.len(), Default::default); @@ -324,10 +366,14 @@ pub(crate) fn load_edges_from_df< AtomicUsize::new(write_locked_graph.graph().internal_num_edges()).into(); let next_edge_id = || num_edges.fetch_add(1, Ordering::Relaxed); + let mut per_segment_edge_count = Vec::with_capacity(write_locked_graph.nodes.len()); + per_segment_edge_count.resize_with(write_locked_graph.nodes.len(), || AtomicUsize::new(0)); + write_locked_graph .nodes .par_iter_mut() - .for_each(|locked_page| { + .enumerate() + .for_each(|(page_id, locked_page)| { for (row, ((((src, src_gid), dst), time), layer)) in src_col_resolved .iter() .zip(src_col.iter()) @@ -359,29 +405,44 @@ pub(crate) fn load_edges_from_df< edge_id.with_layer(*layer), 0, ); // FIXME: when we update this to work with layers use the correct layer + per_segment_edge_count[page_id].fetch_add(1, Ordering::Relaxed); } } }); // link the destinations - write_locked_graph.nodes.par_iter_mut().for_each(|shard| { - for (row, ((((src, (dst, dst_gid)), eid), time), layer)) in src_col_resolved + write_locked_graph + .nodes + .par_iter_mut() + .enumerate() + .for_each(|(page_id, shard)| { + for (row, ((((src, (dst, dst_gid)), eid), time), layer)) in src_col_resolved + .iter() + .zip(dst_col_resolved.iter().zip(dst_col.iter())) + .zip(eid_col_resolved.iter()) + .zip(time_col.iter()) + .zip(layer_col_resolved.iter()) + .enumerate() + { + if let Some(dst_pos) = shard.resolve_pos(*dst) { + let t = TimeIndexEntry(time, start_idx + row); + let mut writer = shard.writer(); + writer.store_node_id(dst_pos, 0, dst_gid, 0); + writer.add_static_inbound_edge(dst_pos, *src, *eid, 0); + writer.add_inbound_edge(Some(t), dst_pos, *src, eid.with_layer(*layer), 0); + per_segment_edge_count[page_id].fetch_add(1, Ordering::Relaxed); + } + } + }); + + println!( + "Counts per shard: {:?}", + per_segment_edge_count .iter() - .zip(dst_col_resolved.iter().zip(dst_col.iter())) - .zip(eid_col_resolved.iter()) - .zip(time_col.iter()) - .zip(layer_col_resolved.iter()) .enumerate() - { - if let Some(dst_pos) = shard.resolve_pos(*dst) { - let t = TimeIndexEntry(time, start_idx + row); - let mut writer = shard.writer(); - writer.store_node_id(dst_pos, 0, dst_gid, 0); - writer.add_static_inbound_edge(dst_pos, *src, *eid, 0); - writer.add_inbound_edge(Some(t), dst_pos, *src, eid.with_layer(*layer), 0); - } - } - }); + .filter(|(_, count)| count.load(Ordering::Relaxed) > 0) + .collect::>() + ); write_locked_graph.resize_chunks_to_num_edges(num_edges.load(Ordering::Relaxed)); @@ -427,7 +488,6 @@ pub(crate) fn load_edges_from_df< } start_idx += df.len(); - #[cfg(feature = "python")] let _ = pb.update(df.len()); } Ok(()) diff --git a/raphtory/src/io/arrow/node_col.rs b/raphtory/src/io/arrow/node_col.rs index 512a568de1..088bf8f914 100644 --- a/raphtory/src/io/arrow/node_col.rs +++ b/raphtory/src/io/arrow/node_col.rs @@ -1,7 +1,7 @@ use crate::{errors::LoadError, io::arrow::dataframe::DFChunk, prelude::AdditionOps}; use arrow_array::{Array as ArrowArray, Int32Array, Int64Array}; use polars_arrow::{ - array::{Array, PrimitiveArray, StaticArray, Utf8Array}, + array::{Array, PrimitiveArray, StaticArray, Utf8Array, Utf8ViewArray}, datatypes::ArrowDataType, offset::Offset, }; @@ -87,6 +87,32 @@ impl NodeColOps for PrimitiveArray { } } +impl NodeColOps for Utf8ViewArray { + fn get(&self, i: usize) -> Option { + if i >= self.len() { + None + } else { + // safety: bounds checked above + unsafe { + let value = self.value_unchecked(i); + Some(GidRef::Str(value)) + } + } + } + + fn dtype(&self) -> GidType { + GidType::Str + } + + fn null_count(&self) -> usize { + Array::null_count(self) + } + + fn len(&self) -> usize { + Array::len(self) + } +} + impl NodeColOps for Utf8Array { fn get(&self, i: usize) -> Option { if i >= self.len() { @@ -308,6 +334,14 @@ impl<'a> TryFrom<&'a dyn Array> for NodeCol { .clone(); Ok(NodeCol(Box::new(col))) } + ArrowDataType::Utf8View => { + let col = value + .as_any() + .downcast_ref::() + .unwrap() + .clone(); + Ok(NodeCol(Box::new(col))) + } dtype => Err(LoadError::InvalidNodeIdType(dtype.clone())), } } diff --git a/raphtory/src/io/parquet_loaders.rs b/raphtory/src/io/parquet_loaders.rs index 3acb81265b..48ae3d0a39 100644 --- a/raphtory/src/io/parquet_loaders.rs +++ b/raphtory/src/io/parquet_loaders.rs @@ -94,24 +94,56 @@ pub fn load_edges_from_parquet< cols_to_check.push(layer_col.as_ref()); } - for path in get_parquet_file_paths(parquet_path)? { - let df_view = process_parquet_file_to_df(path.as_path(), Some(&cols_to_check))?; - df_view.check_cols_exist(&cols_to_check)?; - load_edges_from_df( - df_view, - time, - src, - dst, - properties, - constant_properties, - shared_constant_properties, - layer, - layer_col, - graph, - ) - .map_err(|e| GraphError::LoadFailure(format!("Failed to load graph {e:?}")))?; + let all_files = get_parquet_file_paths(parquet_path)? + .into_iter() + .map(|file| { + let (names, _, num_rows) = + read_parquet_file(file, (!cols_to_check.is_empty()).then_some(&cols_to_check))?; + Ok::<_, GraphError>((names, num_rows)) + }) + .collect::, _>>()?; + + let mut count_rows = 0; + let mut all_names = Vec::new(); + for (names, num_rows) in all_files { + count_rows += num_rows; + if all_names.is_empty() { + all_names = names; + } else if all_names != names { + return Err(GraphError::LoadFailure( + "Parquet files have different column names".to_string(), + )); + } } + let all_df_view = get_parquet_file_paths(parquet_path)? + .into_iter() + .flat_map(|file| { + let df_view = process_parquet_file_to_df(file.as_path(), Some(&cols_to_check)) + .expect("Failed to process Parquet file"); + df_view.chunks + }); + + let df_view = DFView { + names: all_names, + chunks: all_df_view, + num_rows: count_rows, + }; + + load_edges_from_df( + df_view, + time, + src, + dst, + properties, + constant_properties, + shared_constant_properties, + layer, + layer_col, + graph, + ) + .map_err(|e| GraphError::LoadFailure(format!("Failed to load graph {e:?}")))?; + Ok(()) } @@ -254,9 +286,9 @@ pub(crate) fn process_parquet_file_to_df( .collect(); let chunks = chunks.into_iter().map(move |result| { - result.map(|r| DFChunk { chunk: r.to_vec() }).map_err(|e| { - GraphError::LoadFailure(format!("Failed to process Parquet file: {:?}", e)) - }) + result + .map(|r| DFChunk { chunk: r.to_vec() }) + .map_err(|e| GraphError::LoadFailure(format!("Failed to process Parquet file: {e:?}"))) }); Ok(DFView { From 398d2a35febc0c5cacf93dcbdc941655d9b4d8c0 Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Mon, 21 Jul 2025 22:15:18 +0100 Subject: [PATCH 086/321] try making the resolver shards more generic --- .../src/entities/graph/logical_to_physical.rs | 165 ++++++++---------- 1 file changed, 68 insertions(+), 97 deletions(-) diff --git a/raphtory-core/src/entities/graph/logical_to_physical.rs b/raphtory-core/src/entities/graph/logical_to_physical.rs index e9424e2aa1..c6f898af54 100644 --- a/raphtory-core/src/entities/graph/logical_to_physical.rs +++ b/raphtory-core/src/entities/graph/logical_to_physical.rs @@ -58,11 +58,11 @@ impl Map { .par_iter() .enumerate() .try_for_each(|(shard_id, shard)| { - work_fn(ResolverShard::U64 { - guard: shard.write(), + work_fn(ResolverShard::U64(ResolverShardT::new( + shard.write(), map, shard_id, - }) + ))) }) } Map::Str(map) => { @@ -71,11 +71,11 @@ impl Map { .par_iter() .enumerate() .try_for_each(|(shard_id, shard)| { - work_fn(ResolverShard::Str { - guard: shard.write(), + work_fn(ResolverShard::Str(ResolverShardT::new( + shard.write(), map, shard_id, - }) + ))) }) } } @@ -94,108 +94,79 @@ pub struct Mapping { } pub enum ResolverShard<'a> { - U64 { - guard: RwLockWriteGuard<'a, RawTable<(u64, SharedValue)>>, - map: &'a FxDashMap, - shard_id: usize, - }, - Str { - guard: RwLockWriteGuard<'a, RawTable<(String, SharedValue)>>, - map: &'a FxDashMap, - shard_id: usize, - }, -} - -pub struct U64ResolverShard<'a> { - guard: RwLockWriteGuard<'a, RawTable<(u64, SharedValue)>>, - map: &'a FxDashMap, - shard_id: usize, -} - -pub struct StrResolverShard<'a> { - guard: RwLockWriteGuard<'a, RawTable<(String, SharedValue)>>, - map: &'a FxDashMap, - shard_id: usize, + U64(ResolverShardT<'a, u64>), + Str(ResolverShardT<'a, String>), } impl<'a> ResolverShard<'a> { - pub fn resolve_nodes_u64<'b, I: Iterator>( - &mut self, - nodes: impl Fn() -> I, - next_id: impl FnOnce() -> VID, - ) { + pub fn shard_id(&self) -> usize { + match self { + ResolverShard::U64(ResolverShardT { shard_id, .. }) => *shard_id, + ResolverShard::Str(ResolverShardT { shard_id, .. }) => *shard_id, + } } - pub fn resolve_nodes_str<'b, I: Iterator>( - &mut self, - nodes: impl Fn() -> I, - next_id: impl FnOnce() -> VID, - ) { + pub fn as_u64(&self) -> Option<&ResolverShardT<'a, u64>> { + if let ResolverShard::U64(shard) = self { + Some(shard) + } else { + None + } } - pub fn resolve_node<'b>(&mut self, node_ref: GidRef<'b>, next_id: impl FnOnce() -> VID) -> VID { - match self { - ResolverShard::U64 { - guard, - map, - shard_id, - } => { - if let GidRef::U64(id) = node_ref { - let shard_ind = map.determine_map(&id); - let mut factory = map.hasher().clone(); - let hash = factory.hash_one(&id); - // let data = (id, SharedValue::new(value)) + pub fn as_str(&self) -> Option<&ResolverShardT<'a, String>> { + if let ResolverShard::Str(shard) = self { + Some(shard) + } else { + None + } + } +} - match guard.get(hash, |(k, _)| k == &id) { - Some((_, vid)) => { - // Node already exists, do nothing - *(vid.get()) - } - None => { - // Node does not exist, create it - let vid = next_id(); +pub struct ResolverShardT<'a, T> { + guard: RwLockWriteGuard<'a, RawTable<(T, SharedValue)>>, + map: &'a FxDashMap, + shard_id: usize, +} - guard.insert(hash, (id, SharedValue::new(vid)), |t| { - factory.hash_one(t.0) - }); - vid - } - } - } else { - panic!("Expected GidRef::U64, got {:?}", node_ref); - } +impl<'a, T: Eq + Hash + Clone> ResolverShardT<'a, T> { + pub fn new( + guard: RwLockWriteGuard<'a, RawTable<(T, SharedValue)>>, + map: &'a FxDashMap, + shard_id: usize, + ) -> Self { + Self { + guard, + map, + shard_id, + } + } + pub fn resolve_node(&mut self, id: &T, next_id: impl FnOnce() -> VID) -> Option { + let shard_ind = self.map.determine_map(id); + if shard_ind != self.shard_id { + // This shard does not contain the id, return None + return None; + } + let factory = self.map.hasher().clone(); + let hash = factory.hash_one(id); + // let data = (id, SharedValue::new(value)) + + match self.guard.get(hash, |(k, _)| k == id) { + Some((_, vid)) => { + // Node already exists, do nothing + Some(*(vid.get())) } - ResolverShard::Str { - guard, - map, - shard_id, - } => { - if let GidRef::Str(id) = node_ref { - let shard_ind = map.determine_map(id); - let mut factory = map.hasher().clone(); - let hash = factory.hash_one(&id); - - match guard.get(hash, |(k, _)| k == &id) { - Some((_, vid)) => { - // Node already exists, do nothing - *(vid.get()) - } - None => { - // Node does not exist, create it - let vid = next_id(); - - guard.insert(hash, (id.to_owned(), SharedValue::new(vid)), |t| { - factory.hash_one(&t.0) - }); - vid - } - } - } else { - panic!("Expected GidRef::Str, got {:?}", node_ref); - } + None => { + // Node does not exist, create it + let vid = next_id(); + + self.guard + .insert(hash, (id.clone(), SharedValue::new(vid)), |t| { + factory.hash_one(&t.0) + }); + Some(vid) } - }; - VID(0) + } } } From 198a103f0b911708bf7d13a13fe7ff3d8caa7b09 Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Tue, 22 Jul 2025 11:04:03 +0100 Subject: [PATCH 087/321] can now load things into mapper in parallel --- .../src/entities/graph/logical_to_physical.rs | 18 +++--- raphtory/src/io/arrow/df_loaders.rs | 55 +++++++++++++++++-- raphtory/src/io/arrow/node_col.rs | 4 ++ 3 files changed, 64 insertions(+), 13 deletions(-) diff --git a/raphtory-core/src/entities/graph/logical_to_physical.rs b/raphtory-core/src/entities/graph/logical_to_physical.rs index c6f898af54..4e18a402a9 100644 --- a/raphtory-core/src/entities/graph/logical_to_physical.rs +++ b/raphtory-core/src/entities/graph/logical_to_physical.rs @@ -13,8 +13,8 @@ use raphtory_api::core::{ use rayon::prelude::*; use serde::{Deserialize, Deserializer, Serialize}; use std::{ + borrow::Borrow, hash::{BuildHasher, Hash}, - panic, }; use thiserror::Error; @@ -106,7 +106,7 @@ impl<'a> ResolverShard<'a> { } } - pub fn as_u64(&self) -> Option<&ResolverShardT<'a, u64>> { + pub fn as_u64(&mut self) -> Option<&mut ResolverShardT<'a, u64>> { if let ResolverShard::U64(shard) = self { Some(shard) } else { @@ -114,7 +114,7 @@ impl<'a> ResolverShard<'a> { } } - pub fn as_str(&self) -> Option<&ResolverShardT<'a, String>> { + pub fn as_str(&mut self) -> Option<&mut ResolverShardT<'a, String>> { if let ResolverShard::Str(shard) = self { Some(shard) } else { @@ -141,8 +141,12 @@ impl<'a, T: Eq + Hash + Clone> ResolverShardT<'a, T> { shard_id, } } - pub fn resolve_node(&mut self, id: &T, next_id: impl FnOnce() -> VID) -> Option { - let shard_ind = self.map.determine_map(id); + pub fn resolve_node(&mut self, id: &Q, next_id: impl FnOnce() -> VID) -> Option + where + T: Borrow, + Q: Eq + Hash + ToOwned + ?Sized, + { + let shard_ind = self.map.determine_map(id.borrow()); if shard_ind != self.shard_id { // This shard does not contain the id, return None return None; @@ -151,7 +155,7 @@ impl<'a, T: Eq + Hash + Clone> ResolverShardT<'a, T> { let hash = factory.hash_one(id); // let data = (id, SharedValue::new(value)) - match self.guard.get(hash, |(k, _)| k == id) { + match self.guard.get(hash, |(k, _)| k.borrow() == id) { Some((_, vid)) => { // Node already exists, do nothing Some(*(vid.get())) @@ -161,7 +165,7 @@ impl<'a, T: Eq + Hash + Clone> ResolverShardT<'a, T> { let vid = next_id(); self.guard - .insert(hash, (id.clone(), SharedValue::new(vid)), |t| { + .insert(hash, (id.borrow().to_owned(), SharedValue::new(vid)), |t| { factory.hash_one(&t.0) }); Some(vid) diff --git a/raphtory/src/io/arrow/df_loaders.rs b/raphtory/src/io/arrow/df_loaders.rs index 47b919205b..b59d81e17d 100644 --- a/raphtory/src/io/arrow/df_loaders.rs +++ b/raphtory/src/io/arrow/df_loaders.rs @@ -20,7 +20,7 @@ use raphtory_api::{ }, }; use raphtory_core::{ - entities::{graph::logical_to_physical::Mapping, VID}, + entities::{graph::logical_to_physical::Mapping, GidType, VID}, storage::timeindex::AsTime, }; use raphtory_storage::mutation::addition_ops::{InternalAdditionOps, SessionAdditionOps}; @@ -301,14 +301,57 @@ pub(crate) fn load_edges_from_df< let dst_col = df.node_col(dst_index)?; dst_col.validate(graph, LoadError::MissingDstError)?; + let gid_type = src_col.dtype(); + let node_count = &write_locked_graph.graph().node_count; - resolver.run_with_locked(|mut shard| { - for id in src_col.iter().chain(dst_col.iter()) { - shard.resolve_node(id, || VID(node_count.fetch_add(1, Ordering::Relaxed))); - //.map_err(|_| LoadError::FatalError)?; + resolver.run_with_locked(|mut shard| match gid_type { + GidType::Str => { + let shard = shard.as_str().unwrap(); + let src_iter = src_col.iter().map(|gid| gid.as_str().unwrap()).enumerate(); + + let dst_iter = dst_col.iter().map(|gid| gid.as_str().unwrap()).enumerate(); + + for (id, gid) in src_iter { + if let Some(vid) = + shard.resolve_node(gid, || VID(node_count.fetch_add(1, Ordering::Relaxed))) + { + src_col_shared[id].store(vid.0, Ordering::Relaxed); + } + } + + for (id, gid) in dst_iter { + if let Some(vid) = + shard.resolve_node(gid, || VID(node_count.fetch_add(1, Ordering::Relaxed))) + { + dst_col_shared[id].store(vid.0, Ordering::Relaxed); + } + } + Ok::<_, LoadError>(()) + } + GidType::U64 => { + let shard = shard.as_u64().unwrap(); + let src_iter = src_col.iter().map(|gid| gid.as_u64().unwrap()).enumerate(); + + let dst_iter = dst_col.iter().map(|gid| gid.as_u64().unwrap()).enumerate(); + + for (id, gid) in src_iter { + if let Some(vid) = + shard.resolve_node(&gid, || VID(node_count.fetch_add(1, Ordering::Relaxed))) + { + src_col_shared[id].store(vid.0, Ordering::Relaxed); + } + } + + for (id, gid) in dst_iter { + if let Some(vid) = + shard.resolve_node(&gid, || VID(node_count.fetch_add(1, Ordering::Relaxed))) + { + dst_col_shared[id].store(vid.0, Ordering::Relaxed); + } + } + Ok::<_, LoadError>(()) } - Ok::<(), LoadError>(()) })?; let time_col = df.time_col(time_index)?; diff --git a/raphtory/src/io/arrow/node_col.rs b/raphtory/src/io/arrow/node_col.rs index 088bf8f914..cf8f274d48 100644 --- a/raphtory/src/io/arrow/node_col.rs +++ b/raphtory/src/io/arrow/node_col.rs @@ -438,6 +438,10 @@ impl NodeCol { } Ok(()) } + + pub fn dtype(&self) -> GidType { + self.0.dtype() + } } pub fn lift_node_col(index: usize, df: &DFChunk) -> Result { From aa10a4a7c26e99981e60eae53a282a341f2d2adf Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Thu, 24 Jul 2025 13:50:29 +0100 Subject: [PATCH 088/321] url_encode works now --- Cargo.toml | 200 -------- db4-graph/src/lib.rs | 8 +- db4-storage/src/api/edges.rs | 2 +- db4-storage/src/api/nodes.rs | 8 +- db4-storage/src/pages/flush_thread.rs | 61 +++ db4-storage/src/pages/layer_counter.rs | 8 + db4-storage/src/pages/mod.rs | 12 +- db4-storage/src/pages/node_page/writer.rs | 1 + db4-storage/src/pages/node_store.rs | 2 +- db4-storage/src/pages/session.rs | 3 +- db4-storage/src/pages/test_utils/checkers.rs | 11 +- db4-storage/src/resolver/mapping_resolver.rs | 34 ++ db4-storage/src/resolver/mod.rs | 17 + db4-storage/src/segments/node.rs | 25 +- python/Cargo.toml | 2 +- raphtory-api/src/core/entities/mod.rs | 10 +- .../src/entities/graph/logical_to_physical.rs | 75 ++- raphtory-graphql/Cargo.toml | 1 + raphtory-graphql/src/url_encode.rs | 84 +++- raphtory-storage/src/mutation/addition_ops.rs | 2 +- raphtory/Cargo.toml | 6 +- raphtory/src/db/api/storage/storage.rs | 2 +- raphtory/src/db/graph/edge.rs | 2 +- raphtory/src/io/arrow/df_loaders.rs | 440 +++++++++++------- raphtory/src/python/graph/graph.rs | 5 + raphtory/src/serialise/parquet/mod.rs | 27 +- 26 files changed, 627 insertions(+), 421 deletions(-) delete mode 100644 Cargo.toml create mode 100644 db4-storage/src/pages/flush_thread.rs diff --git a/Cargo.toml b/Cargo.toml deleted file mode 100644 index b425877978..0000000000 --- a/Cargo.toml +++ /dev/null @@ -1,200 +0,0 @@ -[workspace] -members = [ - "raphtory", - "raphtory-cypher", - "raphtory-benchmark", - "examples/rust", - "examples/netflow", - "examples/custom-gql-apis", - "python", - "raphtory-graphql", - "raphtory-api", - "raphtory-core", - "raphtory-storage", - # "db4-common", - "db4-storage", - "db4-graph", -] -default-members = [ - "raphtory", - # "db4-common", - "db4-storage", - "db4-graph"] -resolver = "2" - -[workspace.package] -version = "0.15.1" -documentation = "https://raphtory.readthedocs.io/en/latest/" -repository = "https://github.com/Raphtory/raphtory/" -license = "GPL-3.0" -readme = "README.md" -homepage = "https://github.com/Raphtory/raphtory/" -keywords = ["graph", "temporal-graph", "temporal"] -authors = ["Pometry"] -rust-version = "1.86.0" -edition = "2021" - -# debug symbols are using a lot of resources -[profile.dev] -split-debuginfo = "unpacked" -debug = false - -[profile.release-with-debug] -inherits = "release" -debug = true - -# use this if you really need debug symbols -[profile.with-debug] -inherits = "dev" -debug = true - -# for fast one-time builds (e.g., docs/CI) -[profile.build-fast] -inherits = "dev" -debug = false -incremental = false - - -[workspace.dependencies] -#[public-storage] -pometry-storage = { version = ">=0.8.1", path = "pometry-storage" } -#[private-storage] -#pometry-storage = { path = "pometry-storage-private", package = "pometry-storage-private" } -async-graphql = { version = "7.0.16", features = ["dynamic-schema"] } -bincode = "1.3.3" -bitvec = "1.0.1" -boxcar = "0.2.8" -async-graphql-poem = "7.0.16" -dynamic-graphql = "0.10.1" -reqwest = { version = "0.12.8", default-features = false, features = [ - "rustls-tls", - "multipart", - "json", -] } -iter-enum = { version = "1.2.0", features = ["rayon"] } -serde = { version = "1.0.197", features = ["derive", "rc"] } -serde_json = "1.0.114" -pyo3 = { version = "=0.23.3", features = ["multiple-pymethods", "chrono"] } -pyo3-build-config = "=0.23.3" -pyo3-arrow = "0.6" -numpy = "0.23.0" -itertools = "0.13.0" -rand = "0.8.5" -rayon = "1.8.1" -roaring = "0.10.6" -sorted_vector_map = "0.2.0" -tokio = { version = "1.43.1", features = ["full"] } -once_cell = "1.19.0" -parking_lot = { version = "0.12.1", features = [ - "serde", - "arc_lock", - "send_guard", -] } -ordered-float = "4.2.0" -chrono = { version = "=0.4.38", features = ["serde"] } -tempfile = "3.10.0" -futures-util = "0.3.30" -thiserror = "2.0.0" -dotenv = "0.15.0" -csv = "1.3.0" -flate2 = "1.0.28" -regex = "1.10.3" -num-traits = "0.2.18" -num-integer = "0.1" -rand_distr = "0.4.3" -rustc-hash = "2.0.0" -twox-hash = "2.1.0" -lock_api = { version = "0.4.11", features = ["arc_lock", "serde"] } -dashmap = { version = "6.0.1", features = ["serde", "rayon"] } -enum_dispatch = "0.3.12" -glam = "0.29.0" -quad-rand = "0.2.1" -zip = "2.3.0" -neo4rs = "0.8.0" -bzip2 = "0.4.4" -tantivy = "0.22.0" -async-trait = "0.1.77" -async-openai = "0.26.0" -num = "0.4.1" -display-error-chain = "0.2.0" -polars-arrow = "0.42.0" -polars-parquet = "0.42.0" -polars-core = "0.42.0" -polars-io = "0.42.0" -bigdecimal = { version = "0.4.7", features = ["serde"] } -kdam = "0.6.2" -hashbrown = "0.15.1" -pretty_assertions = "1.4.0" -quickcheck = "1.0.3" -quickcheck_macros = "1.0.0" -streaming-stats = "0.2.3" -proptest = "1.4.0" -proptest-derive = "0.5.1" -criterion = "0.5.1" -crossbeam-channel = "0.5.15" -base64 = "0.22.1" -jsonwebtoken = "9.3.1" -spki = "0.7.3" -poem = { version = "3.0.1", features = ["cookie"] } -opentelemetry = "0.27.1" -opentelemetry_sdk = { version = "0.27.1", features = ["rt-tokio"] } -opentelemetry-otlp = { version = "0.27.0" } -tracing = "0.1.37" -tracing-opentelemetry = "0.28.0" -tracing-subscriber = { version = "0.3.16", features = ["std", "env-filter"] } -indoc = "2.0.5" -walkdir = "2" -config = "0.14.0" -either = "=1.11.0" -clap = { version = "4.5.21", features = ["derive", "env"] } -memmap2 = { version = "0.9.4" } -ahash = { version = "0.8.3", features = ["serde"] } -bytemuck = { version = "1.18.0", features = ["derive"] } -ouroboros = "0.18.3" -url = "2.2" -base64-compat = { package = "base64-compat", version = "1.0.0" } -prost = "0.13.1" -prost-types = "0.13.1" -prost-build = "0.13.1" -lazy_static = "1.4.0" -pest = "2.7.8" -pest_derive = "2.7.8" -minijinja = "2.2.0" -minijinja-contrib = { version = "2.2.0", features = ["datetime"] } -datafusion = { version = "43.0.0" } -arroy = "0.6.1" -heed = "0.22.0" -sysinfo = "0.35.1" -sqlparser = "0.51.0" -futures = "0.3" -arrow = { version = "=53.4.1" } -parquet = { version = "=53.4.1" } -arrow-json = { version = "=53.4.1" } -arrow-buffer = { version = "=53.4.1" } -arrow-schema = { version = "=53.4.1" } -arrow-array = { version = "=53.4.1" } -arrow-ipc = { version = "=53.4.1" } -arrow-csv = { version = "=53.4.1" } - -moka = { version = "0.12.7", features = ["sync"] } -indexmap = { version = "2.7.0", features = ["rayon"] } -fake = { version = "3.1.0", features = ["chrono"] } -strsim = { version = "0.11.1" } -uuid = { version = "1.16.0", features = ["v4"] } - -raphtory ={ version = "0.15.1", path = "./raphtory", default-features = false} -raphtory-api ={ version = "0.15.1", path = "./raphtory-api", default-features = false} -#raphtory-core ={ version = "0.15.1", path = "./raphtory-core", default-features = false} -raphtory-graphql ={ version = "0.15.1", path = "./raphtory-graphql", default-features = false} - -# Make sure that transitive dependencies stick to disk_graph 50 -[patch.crates-io] -arrow-buffer = { git = "https://github.com/apache/arrow-rs.git", tag = "53.4.1" } -arrow-schema = { git = "https://github.com/apache/arrow-rs.git", tag = "53.4.1" } -arrow-data = { git = "https://github.com/apache/arrow-rs.git", tag = "53.4.1" } -arrow-array = { git = "https://github.com/apache/arrow-rs.git", tag = "53.4.1" } -arrow-csv = { git = "https://github.com/apache/arrow-rs.git", tag = "53.4.1" } - -[workspace.dependencies.storage] -package = "db4-storage" -path = "db4-storage" \ No newline at end of file diff --git a/db4-graph/src/lib.rs b/db4-graph/src/lib.rs index 7cf0ab4009..89dca9350c 100644 --- a/db4-graph/src/lib.rs +++ b/db4-graph/src/lib.rs @@ -28,13 +28,13 @@ use tempfile::TempDir; pub mod entries; pub mod mutation; -const DEFAULT_MAX_PAGE_LEN_NODES: usize = 50_000; +const DEFAULT_MAX_PAGE_LEN_NODES: usize = 25_000; const DEFAULT_MAX_PAGE_LEN_EDGES: usize = 50_000; #[derive(Debug)] pub struct TemporalGraph { // mapping between logical and physical ids - pub logical_to_physical: GIDResolver, + pub logical_to_physical: Arc, pub node_count: AtomicUsize, storage: Arc>, pub graph_meta: Arc, @@ -98,7 +98,7 @@ impl, ES = ES>> TemporalGraph { let node_count = AtomicUsize::new(storage.nodes().num_nodes()); Self { graph_dir, - logical_to_physical: resolver, + logical_to_physical: resolver.into(), node_count, storage: Arc::new(storage), graph_meta: Arc::new(GraphMeta::default()), @@ -120,7 +120,7 @@ impl, ES = ES>> TemporalGraph { ); Self { graph_dir, - logical_to_physical: GIDResolver::new(gid_resolver_dir).unwrap(), + logical_to_physical: GIDResolver::new(gid_resolver_dir).unwrap().into(), node_count: AtomicUsize::new(0), storage: Arc::new(storage), graph_meta: Arc::new(GraphMeta::default()), diff --git a/db4-storage/src/api/edges.rs b/db4-storage/src/api/edges.rs index eb688828b9..2f9cc05dfb 100644 --- a/db4-storage/src/api/edges.rs +++ b/db4-storage/src/api/edges.rs @@ -14,7 +14,7 @@ use rayon::iter::ParallelIterator; use crate::{LocalPOS, error::DBV4Error, segments::edge::MemEdgeSegment}; -pub trait EdgeSegmentOps: Send + Sync + std::fmt::Debug { +pub trait EdgeSegmentOps: Send + Sync + std::fmt::Debug + 'static { type Extension; type Entry<'a>: EdgeEntryOps<'a> diff --git a/db4-storage/src/api/nodes.rs b/db4-storage/src/api/nodes.rs index 197cb68b82..1cecbc7726 100644 --- a/db4-storage/src/api/nodes.rs +++ b/db4-storage/src/api/nodes.rs @@ -32,7 +32,7 @@ use crate::{ utils::{Iter2, Iter3, Iter4}, }; -pub trait NodeSegmentOps: Send + Sync + std::fmt::Debug { +pub trait NodeSegmentOps: Send + Sync + std::fmt::Debug + 'static { type Extension; type Entry<'a>: NodeEntryOps<'a> @@ -46,6 +46,10 @@ pub trait NodeSegmentOps: Send + Sync + std::fmt::Debug { fn t_len(&self) -> usize; + fn event_id(&self) -> i64; + fn increment_event_id(&self, i: i64); + fn decrement_event_id(&self) -> i64; + fn load( page_id: usize, max_page_len: usize, @@ -106,6 +110,8 @@ pub trait NodeSegmentOps: Send + Sync + std::fmt::Debug { fn entry<'a>(&'a self, pos: impl Into) -> Self::Entry<'a>; fn locked(self: &Arc) -> Self::ArcLockedSegment; + + fn flush(&self); } pub trait LockedNSSegment: std::fmt::Debug + Send + Sync { diff --git a/db4-storage/src/pages/flush_thread.rs b/db4-storage/src/pages/flush_thread.rs new file mode 100644 index 0000000000..153e81b792 --- /dev/null +++ b/db4-storage/src/pages/flush_thread.rs @@ -0,0 +1,61 @@ +use std::{sync::Arc, thread::JoinHandle}; + +use itertools::Itertools; + +use crate::{ + NS, + api::{edges::EdgeSegmentOps, nodes::NodeSegmentOps}, + pages::node_store::NodeStorageInner, + persist::strategy::PersistentStrategy, +}; + +// This should be a rayon thread with a reference to Arc that will flush the data to disk periodically. +#[derive(Debug)] +pub(crate) struct FlushThread { + handler: JoinHandle<()>, +} + +impl FlushThread { + pub fn new< + NS: NodeSegmentOps + Send + Sync + 'static, + ES: EdgeSegmentOps + Send + Sync + 'static, + EXT: Clone + Default + Send + Sync + 'static, + >( + nodes: Arc>, + ) -> Self { + let handler = std::thread::spawn({ + let nodes = Arc::clone(&nodes); + move || { + // Implement the logic to periodically flush the nodes to disk. + loop { + // Flush logic here + std::thread::sleep(std::time::Duration::from_millis(100)); // Example sleep duration + + // let's do some stats, run over all the segments and decrement the event id then get the max and the min + // the event_id functions are atomic so no need to lock the segments + + let min = Iterator::min( + nodes + .segments() + .iter() + .map(|(i, ns)| (i, ns.decrement_event_id())), + ); + + if let Some((segment_id, event_id)) = min { + if event_id <= 0 { + // If the event id is 0, we can flush this segment + let segment = nodes.segments().get(segment_id).unwrap(); + segment.flush(); + segment.increment_event_id(1000); // ignore this segment for the next 1000 events + // eprintln!("Triggered flush for {segment_id}") + } + } + + // println!("Min Max event ids for segments {min_max:?}"); + } + } + }); + + Self { handler } + } +} diff --git a/db4-storage/src/pages/layer_counter.rs b/db4-storage/src/pages/layer_counter.rs index 2a7e8ed156..ff86265e76 100644 --- a/db4-storage/src/pages/layer_counter.rs +++ b/db4-storage/src/pages/layer_counter.rs @@ -7,6 +7,7 @@ pub struct GraphStats { layers: boxcar::Vec, earliest: MinCounter, latest: MaxCounter, + global_event_counter: AtomicUsize, } impl> From for GraphStats { @@ -16,6 +17,7 @@ impl> From for GraphStats { layers, earliest: MinCounter::new(), latest: MaxCounter::new(), + global_event_counter: AtomicUsize::new(0), } } } @@ -34,6 +36,7 @@ impl GraphStats { layers, earliest: MinCounter::new(), latest: MaxCounter::new(), + global_event_counter: AtomicUsize::new(0), } } @@ -59,6 +62,11 @@ impl GraphStats { self.latest.get() } + pub fn increment_global_event_counter(&self) -> usize { + self.global_event_counter + .fetch_add(1, std::sync::atomic::Ordering::Relaxed) + } + pub fn increment(&self, layer_id: usize) -> usize { let counter = self.get_or_create_layer(layer_id); counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed) diff --git a/db4-storage/src/pages/mod.rs b/db4-storage/src/pages/mod.rs index 6883a2e8f5..61e36206fb 100644 --- a/db4-storage/src/pages/mod.rs +++ b/db4-storage/src/pages/mod.rs @@ -10,7 +10,11 @@ use crate::{ LocalPOS, api::{edges::EdgeSegmentOps, nodes::NodeSegmentOps}, error::DBV4Error, - pages::{edge_store::ReadLockedEdgeStorage, node_store::ReadLockedNodeStorage}, + pages::{ + edge_store::ReadLockedEdgeStorage, flush_thread::FlushThread, + node_store::ReadLockedNodeStorage, + }, + persist::strategy::PersistentStrategy, properties::props_meta_writer::PropsMetaWriter, segments::{edge::MemEdgeSegment, node::MemNodeSegment}, }; @@ -36,6 +40,7 @@ use session::WriteSession; pub mod edge_page; pub mod edge_store; +pub mod flush_thread; pub mod layer_counter; pub mod locked; pub mod node_page; @@ -53,6 +58,7 @@ pub const NODE_TYPE_PROP_KEY: &str = "_raphtory_node_type"; #[derive(Debug)] pub struct GraphStore { nodes: Arc>, + node_flush_thread: FlushThread, edges: Arc>, event_id: AtomicUsize, _ext: EXT, @@ -72,7 +78,7 @@ pub struct ReadLockedGraphStore< impl< NS: NodeSegmentOps, ES: EdgeSegmentOps, - EXT: Clone + Send + Sync + Default, + EXT: PersistentStrategy, > GraphStore { pub fn read_locked(self: &Arc) -> ReadLockedGraphStore { @@ -145,6 +151,7 @@ impl< let t_len = edges.t_len(); Ok(Self { + node_flush_thread: FlushThread::new::<_, ES, _>(nodes.clone()), nodes, edges, event_id: AtomicUsize::new(t_len), @@ -194,6 +201,7 @@ impl< .expect("Unrecoverable! Failed to write graph meta"); Self { + node_flush_thread: FlushThread::new::<_, ES, _>(nodes.clone()), nodes, edges, event_id: AtomicUsize::new(0), diff --git a/db4-storage/src/pages/node_page/writer.rs b/db4-storage/src/pages/node_page/writer.rs index 48e6fa52f5..b824617e7c 100644 --- a/db4-storage/src/pages/node_page/writer.rs +++ b/db4-storage/src/pages/node_page/writer.rs @@ -201,6 +201,7 @@ impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> Drop for NodeWriter<'a, MP, NS> { fn drop(&mut self) { + self.page.increment_event_id(1); self.page .notify_write(self.mut_segment.deref_mut()) .expect("Failed to persist node page"); diff --git a/db4-storage/src/pages/node_store.rs b/db4-storage/src/pages/node_store.rs index 48e5ba846a..72feb6ec94 100644 --- a/db4-storage/src/pages/node_store.rs +++ b/db4-storage/src/pages/node_store.rs @@ -166,7 +166,7 @@ impl, EXT: Clone> NodeStorageInner &self.stats } - pub fn pages(&self) -> &boxcar::Vec> { + pub fn segments(&self) -> &boxcar::Vec> { &self.pages } diff --git a/db4-storage/src/pages/session.rs b/db4-storage/src/pages/session.rs index 98ddd8abe4..6d610bb04c 100644 --- a/db4-storage/src/pages/session.rs +++ b/db4-storage/src/pages/session.rs @@ -5,6 +5,7 @@ use crate::{ LocalPOS, api::{edges::EdgeSegmentOps, nodes::NodeSegmentOps}, pages::NODE_ID_PROP_KEY, + persist::strategy::PersistentStrategy, segments::{edge::MemEdgeSegment, node::MemNodeSegment}, }; use parking_lot::RwLockWriteGuard; @@ -24,7 +25,7 @@ impl< 'a, NS: NodeSegmentOps, ES: EdgeSegmentOps, - EXT: Clone + Default + Send + Sync, + EXT: PersistentStrategy, > WriteSession<'a, NS, ES, EXT> { pub fn new( diff --git a/db4-storage/src/pages/test_utils/checkers.rs b/db4-storage/src/pages/test_utils/checkers.rs index a97f6222ac..b843e66759 100644 --- a/db4-storage/src/pages/test_utils/checkers.rs +++ b/db4-storage/src/pages/test_utils/checkers.rs @@ -18,6 +18,7 @@ use crate::{ }, error::DBV4Error, pages::GraphStore, + persist::strategy::PersistentStrategy, }; use super::fixtures::{AddEdge, Fixture, NodeFixture}; @@ -25,7 +26,7 @@ use super::fixtures::{AddEdge, Fixture, NodeFixture}; pub fn check_edges_support< NS: NodeSegmentOps, ES: EdgeSegmentOps, - EXT: Clone + Default + Send + Sync + std::fmt::Debug, + EXT: PersistentStrategy, >( edges: Vec<(impl Into, impl Into, Option)>, // src, dst, optional layer_id par_load: bool, @@ -102,7 +103,7 @@ pub fn check_edges_support< fn check< NS: NodeSegmentOps, ES: EdgeSegmentOps, - EXT: Clone + Send + Sync + Default + std::fmt::Debug, + EXT: PersistentStrategy, >( stage: &str, expected_edges: &[(VID, VID, Option)], // (src, dst, layer_id) @@ -112,7 +113,7 @@ pub fn check_edges_support< let edges = graph.edges(); if !expected_edges.is_empty() { - assert!(nodes.pages().count() > 0, "{stage}"); + assert!(nodes.segments().count() > 0, "{stage}"); } // Group edges by layer_id first @@ -205,7 +206,7 @@ pub fn check_edges_support< } pub fn check_graph_with_nodes_support< - EXT: Clone + Default + Send + Sync, + EXT: PersistentStrategy, NS: NodeSegmentOps, ES: EdgeSegmentOps, >( @@ -338,7 +339,7 @@ pub fn check_graph_with_nodes_support< } pub fn check_graph_with_props_support< - EXT: Clone + Default + Send + Sync, + EXT: PersistentStrategy, NS: NodeSegmentOps, ES: EdgeSegmentOps, >( diff --git a/db4-storage/src/resolver/mapping_resolver.rs b/db4-storage/src/resolver/mapping_resolver.rs index 1542b873c8..1abf39e8d7 100644 --- a/db4-storage/src/resolver/mapping_resolver.rs +++ b/db4-storage/src/resolver/mapping_resolver.rs @@ -11,6 +11,12 @@ pub struct MappingResolver { mapping: Mapping, } +impl MappingResolver { + pub fn mapping(&self) -> &Mapping { + &self.mapping + } +} + impl GIDResolverOps for MappingResolver { fn new(_path: impl AsRef) -> Result { Ok(Self { @@ -54,4 +60,32 @@ impl GIDResolverOps for MappingResolver { fn get_u64(&self, gid: u64) -> Option { self.mapping.get_u64(gid) } + + fn bulk_set_str>( + &self, + gids: impl IntoIterator, + ) -> Result<(), GIDResolverError> { + for (gid, vid) in gids { + self.set(gid.as_ref().into(), vid)?; + } + Ok(()) + } + + fn bulk_set_u64( + &self, + gids: impl IntoIterator, + ) -> Result<(), GIDResolverError> { + for (gid, vid) in gids { + self.set(gid.into(), vid)?; + } + Ok(()) + } + + fn iter_str(&self) -> impl Iterator + '_ { + self.mapping().iter_str() + } + + fn iter_u64(&self) -> impl Iterator + '_ { + self.mapping().iter_u64() + } } diff --git a/db4-storage/src/resolver/mod.rs b/db4-storage/src/resolver/mod.rs index 6555186ea5..4140159c22 100644 --- a/db4-storage/src/resolver/mod.rs +++ b/db4-storage/src/resolver/mod.rs @@ -22,6 +22,9 @@ pub trait GIDResolverOps { where Self: Sized; fn len(&self) -> usize; + fn is_empty(&self) -> bool { + self.len() == 0 + } fn dtype(&self) -> Option; fn set(&self, gid: GidRef, vid: VID) -> Result<(), GIDResolverError>; fn get_or_init VID>( @@ -35,4 +38,18 @@ pub trait GIDResolverOps { ) -> Result<(), GIDResolverError>; fn get_str(&self, gid: &str) -> Option; fn get_u64(&self, gid: u64) -> Option; + + fn bulk_set_str>( + &self, + gids: impl IntoIterator, + ) -> Result<(), GIDResolverError>; + + fn bulk_set_u64( + &self, + gids: impl IntoIterator, + ) -> Result<(), GIDResolverError>; + + fn iter_str(&self) -> impl Iterator + '_; + + fn iter_u64(&self) -> impl Iterator + '_; } diff --git a/db4-storage/src/segments/node.rs b/db4-storage/src/segments/node.rs index 2dc3dd6595..2523115782 100644 --- a/db4-storage/src/segments/node.rs +++ b/db4-storage/src/segments/node.rs @@ -13,7 +13,10 @@ use raphtory_core::{ }; use std::{ ops::{Deref, DerefMut}, - sync::Arc, + sync::{ + Arc, + atomic::{AtomicI64, Ordering}, + }, }; use super::{HasRow, SegmentContainer}; @@ -370,6 +373,7 @@ impl MemNodeSegment { pub struct NodeSegmentView { inner: Arc>, segment_id: usize, + event_id: AtomicI64, _ext: EXT, } @@ -406,6 +410,22 @@ impl>> NodeSegmentOps for NodeSegm self.head().t_len() } + fn event_id(&self) -> i64 { + self.event_id.load(Ordering::Relaxed) + } + + fn increment_event_id(&self, i: i64) { + self.event_id.fetch_add(i, Ordering::Relaxed); + } + + fn decrement_event_id(&self) -> i64 { + self.event_id + .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |x| { + if x > 0 { Some(x - 1) } else { None } + }) + .unwrap_or_default() + } + fn load( _page_id: usize, _max_page_len: usize, @@ -433,6 +453,7 @@ impl>> NodeSegmentOps for NodeSegm .into(), segment_id: page_id, _ext, + event_id: Default::default(), } } @@ -503,6 +524,8 @@ impl>> NodeSegmentOps for NodeSegm .get_layer(layer_id) .map_or(0, |layer| layer.len()) } + + fn flush(&self) {} } #[cfg(test)] diff --git a/python/Cargo.toml b/python/Cargo.toml index a4c79e14a6..1570bddda8 100644 --- a/python/Cargo.toml +++ b/python/Cargo.toml @@ -20,7 +20,7 @@ crate-type = ["cdylib"] [dependencies] pyo3 = { workspace = true } numpy = { workspace = true } -raphtory_core = { path = "../raphtory", version = "0.15.1", features = [ +raphtory_core = { path = "../raphtory", features = [ "python", "search", "vectors", diff --git a/raphtory-api/src/core/entities/mod.rs b/raphtory-api/src/core/entities/mod.rs index 1be7a635d9..185f7fa61d 100644 --- a/raphtory-api/src/core/entities/mod.rs +++ b/raphtory-api/src/core/entities/mod.rs @@ -311,8 +311,8 @@ impl Display for GidType { impl Display for GidRef<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - GidRef::U64(v) => write!(f, "{}", v), - GidRef::Str(v) => write!(f, "{}", v), + GidRef::U64(v) => write!(f, "{v}"), + GidRef::Str(v) => write!(f, "{v}"), } } } @@ -332,6 +332,12 @@ impl<'a> From<&'a str> for GidRef<'a> { } } +impl From for GidRef<'_> { + fn from(value: u64) -> Self { + GidRef::U64(value) + } +} + impl<'a> GidRef<'a> { pub fn dtype(self) -> GidType { match self { diff --git a/raphtory-core/src/entities/graph/logical_to_physical.rs b/raphtory-core/src/entities/graph/logical_to_physical.rs index 4e18a402a9..f95931ecdb 100644 --- a/raphtory-core/src/entities/graph/logical_to_physical.rs +++ b/raphtory-core/src/entities/graph/logical_to_physical.rs @@ -141,7 +141,11 @@ impl<'a, T: Eq + Hash + Clone> ResolverShardT<'a, T> { shard_id, } } - pub fn resolve_node(&mut self, id: &Q, next_id: impl FnOnce() -> VID) -> Option + pub fn resolve_node( + &mut self, + id: &Q, + next_id: impl FnOnce(&Q) -> Either, + ) -> Option where T: Borrow, Q: Eq + Hash + ToOwned + ?Sized, @@ -153,7 +157,6 @@ impl<'a, T: Eq + Hash + Clone> ResolverShardT<'a, T> { } let factory = self.map.hasher().clone(); let hash = factory.hash_one(id); - // let data = (id, SharedValue::new(value)) match self.guard.get(hash, |(k, _)| k.borrow() == id) { Some((_, vid)) => { @@ -162,13 +165,17 @@ impl<'a, T: Eq + Hash + Clone> ResolverShardT<'a, T> { } None => { // Node does not exist, create it - let vid = next_id(); + let vid = next_id(id); - self.guard - .insert(hash, (id.borrow().to_owned(), SharedValue::new(vid)), |t| { - factory.hash_one(&t.0) - }); - Some(vid) + if let Either::Left(vid) = vid { + self.guard + .insert(hash, (id.borrow().to_owned(), SharedValue::new(vid)), |t| { + factory.hash_one(&t.0) + }); + Some(vid) + } else { + vid.right() + } } } } @@ -325,6 +332,25 @@ impl Mapping { let map = self.map.get()?; map.as_u64().and_then(|m| m.get(&gid).map(|id| *id)) } + + pub fn iter_str(&self) -> impl Iterator + '_ { + self.map + .get() + .and_then(|map| map.as_str()) + .into_iter() + .flat_map(|m| { + m.iter() + .map(|entry| (entry.key().to_owned(), *(entry.value()))) + }) + } + + pub fn iter_u64(&self) -> impl Iterator + '_ { + self.map + .get() + .and_then(|map| map.as_u64()) + .into_iter() + .flat_map(|m| m.iter().map(|entry| (*entry.key(), *(entry.value())))) + } } #[inline] @@ -392,3 +418,36 @@ impl Serialize for Mapping { } } } + +#[cfg(test)] +mod test { + use std::sync::atomic::{AtomicUsize, Ordering}; + + use super::*; + use itertools::Itertools; + use proptest::prelude::*; + + #[test] + fn test_parallel_sharded_mapping() { + let at_least_one_vec = proptest::collection::vec(any::(), 1..100); + proptest!(|(gids in at_least_one_vec)| { + let mapping = Mapping::new(); + let vid_count = AtomicUsize::new(0); + mapping.set(gids.first().map(|x|GidRef::Str(x)).unwrap(), VID(0)).unwrap(); + + let resolved_col = gids.iter().map(|_| AtomicUsize::new(0)).collect::>(); + mapping.run_with_locked(|mut shard| { + for (id, gid) in gids.iter().enumerate() { + if let Some(vid) = shard.as_str().unwrap().resolve_node(gid, |_| Either::Left(VID(vid_count.fetch_add(1, Ordering::Relaxed)))) { + resolved_col[id].store(vid.index(), Ordering::Relaxed); + } + } + Ok::<_, String>(()) + }).unwrap(); + + for (gid, expected_vid) in gids.iter().zip_eq(resolved_col.iter().map(|v| VID(v.load(Ordering::Relaxed)))) { + assert_eq!(mapping.get_str(gid).unwrap(), expected_vid); + } + }) + } +} diff --git a/raphtory-graphql/Cargo.toml b/raphtory-graphql/Cargo.toml index 175eaed7f9..11060e032e 100644 --- a/raphtory-graphql/Cargo.toml +++ b/raphtory-graphql/Cargo.toml @@ -18,6 +18,7 @@ raphtory = { path = "../raphtory", version = "0.15.1", features = [ 'search', "io", ] } +tempfile = { workspace = true } raphtory-api = { path = "../raphtory-api", version = "0.15.1" } raphtory-storage = { path = "../raphtory-storage", version = "0.15.1" } base64 = { workspace = true } diff --git a/raphtory-graphql/src/url_encode.rs b/raphtory-graphql/src/url_encode.rs index 017d3ab886..9b3cb74be4 100644 --- a/raphtory-graphql/src/url_encode.rs +++ b/raphtory-graphql/src/url_encode.rs @@ -1,9 +1,13 @@ +use std::io::{copy, Read, Write}; + use base64::{prelude::BASE64_URL_SAFE, DecodeError, Engine}; use raphtory::{ db::api::view::MaterializedGraph, errors::GraphError, - serialise::{InternalStableDecode, StableEncode}, + prelude::{ParquetDecoder, ParquetEncoder}, + serialise::StableEncode, }; +use zip::write::SimpleFileOptions; #[derive(thiserror::Error, Debug)] pub enum UrlDecodeError { @@ -21,11 +25,79 @@ pub enum UrlDecodeError { pub fn url_encode_graph>(graph: G) -> Result { let g: MaterializedGraph = graph.into(); - Ok(BASE64_URL_SAFE.encode(g.encode_to_vec())) + let temp_dir = tempfile::tempdir()?; + g.encode_parquet(temp_dir.path())?; + // now zip the entire directory, don't bother with compression + let mut bytes = Vec::new(); + let mut zip = zip::ZipWriter::new(std::io::Cursor::new(&mut bytes)); + + let mut paths = vec![temp_dir.path().to_path_buf()]; + while let Some(path) = paths.pop() { + for entry in std::fs::read_dir(&path)? { + let entry = entry?; + let path = entry.path(); + if path.is_dir() { + // If it's a directory, we should add it to the list to process later + paths.push(path); + } else { + // If it's a file, we should write it directly + let options = + SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored); + let rel_path = path.strip_prefix(temp_dir.path()).unwrap(); + zip.start_file_from_path(rel_path, options)?; + let mut file = std::fs::File::open(path)?; + copy(&mut file, &mut zip)?; + } + } + } + zip.finish()?; + Ok(BASE64_URL_SAFE.encode(bytes)) +} + +pub fn url_decode_graph>(graph: T) -> Result { + let zip_bytes = BASE64_URL_SAFE.decode(graph.as_ref()).unwrap(); + + let temp_dir = tempfile::tempdir()?; + let mut zip = zip::ZipArchive::new(std::io::Cursor::new(zip_bytes))?; + + for i in 0..zip.len() { + let mut file = zip.by_index(i)?; + let out_path = temp_dir.path().join(file.enclosed_name().unwrap()); + dbg!(&out_path); + + if let Some(path) = out_path.parent() { + std::fs::create_dir_all(path)?; + } + let mut out_file = std::fs::File::create(&out_path)?; + std::io::copy(&mut file, &mut out_file)?; + } + + MaterializedGraph::decode_parquet(temp_dir.path()) } -pub fn url_decode_graph>(graph: T) -> Result { - Ok(MaterializedGraph::decode_from_bytes( - &BASE64_URL_SAFE.decode(graph)?, - )?) +#[cfg(test)] +mod tests { + use raphtory::{db::graph::graph::assert_graph_equal, prelude::*}; + + use super::*; + + #[test] + fn test_url_encode_decode() { + let graph = Graph::new(); + graph.add_edge(1, 2, 3, [("bla", "blu")], None).unwrap(); + let edge = graph.add_edge(2, 3, 4, [("foo", 42)], Some("7")).unwrap(); + + edge.add_constant_properties([("14", 15f64)], Some("7")) + .unwrap(); + + let node = graph.add_node(17, 0, NO_PROPS, None).unwrap(); + node.add_constant_properties([("blerg", "test")]).unwrap(); + + let bytes = url_encode_graph(graph.clone()).unwrap(); + let decoded_graph = url_decode_graph(bytes).unwrap(); + + let g2 = decoded_graph.into_events().unwrap(); + + assert_graph_equal(&graph, &g2); + } } diff --git a/raphtory-storage/src/mutation/addition_ops.rs b/raphtory-storage/src/mutation/addition_ops.rs index a76b8cee17..930f252f3b 100644 --- a/raphtory-storage/src/mutation/addition_ops.rs +++ b/raphtory-storage/src/mutation/addition_ops.rs @@ -217,7 +217,7 @@ impl InternalAdditionOps for GraphStorage { } fn resolve_node(&self, id: NodeRef) -> Result, Self::Error> { - Ok(self.mutable()?.resolve_node(id)?) + self.mutable()?.resolve_node(id) } fn resolve_node_and_type( diff --git a/raphtory/Cargo.toml b/raphtory/Cargo.toml index 21e44984f0..e31425e721 100644 --- a/raphtory/Cargo.toml +++ b/raphtory/Cargo.toml @@ -15,9 +15,9 @@ homepage.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -raphtory-api = { path = "../raphtory-api", version = "0.15.1" } -raphtory-core = { path = "../raphtory-core", version = "0.15.1" } -raphtory-storage = { path = "../raphtory-storage", version = "0.15.1" } +raphtory-api.workspace = true #{ path = "../raphtory-api", version = "0.15.1" } +raphtory-core.workspace = true # = { path = "../raphtory-core", version = "0.15.1" } +raphtory-storage.workspace = true # = { path = "../raphtory-storage", version = "0.15.1" } db4-graph.workspace = true storage.workspace = true iter-enum = { workspace = true, features = ["rayon"] } diff --git a/raphtory/src/db/api/storage/storage.rs b/raphtory/src/db/api/storage/storage.rs index c696962513..70135e7798 100644 --- a/raphtory/src/db/api/storage/storage.rs +++ b/raphtory/src/db/api/storage/storage.rs @@ -397,7 +397,7 @@ impl InternalAdditionOps for Storage { fn resolve_node(&self, id: NodeRef) -> Result, Self::Error> { match id { NodeRef::Internal(id) => Ok(MaybeNew::Existing(id)), - NodeRef::External(gid) => { + NodeRef::External(_) => { let id = self.graph.resolve_node(id)?; Ok(id) diff --git a/raphtory/src/db/graph/edge.rs b/raphtory/src/db/graph/edge.rs index eb346019e9..d6d150baf3 100644 --- a/raphtory/src/db/graph/edge.rs +++ b/raphtory/src/db/graph/edge.rs @@ -36,7 +36,7 @@ use raphtory_core::entities::graph::tgraph::InvalidLayer; use raphtory_storage::{ graph::edges::edge_storage_ops::EdgeStorageOps, mutation::{ - addition_ops::{EdgeWriteLock, InternalAdditionOps, SessionAdditionOps}, + addition_ops::{EdgeWriteLock, InternalAdditionOps}, deletion_ops::InternalDeletionOps, property_addition_ops::InternalPropertyAdditionOps, }, diff --git a/raphtory/src/io/arrow/df_loaders.rs b/raphtory/src/io/arrow/df_loaders.rs index b59d81e17d..0aea94b4b0 100644 --- a/raphtory/src/io/arrow/df_loaders.rs +++ b/raphtory/src/io/arrow/df_loaders.rs @@ -11,6 +11,7 @@ use crate::{ serialise::incremental::InternalCache, }; use bytemuck::checked::cast_slice_mut; +use either::Either; use kdam::{Bar, BarBuilder, BarExt}; use raphtory_api::{ atomic_extra::atomic_usize_from_mut_slice, @@ -20,19 +21,23 @@ use raphtory_api::{ }, }; use raphtory_core::{ - entities::{graph::logical_to_physical::Mapping, GidType, VID}, + entities::{ + graph::logical_to_physical::{Mapping, ResolverShardT}, + GidRef, GidType, VID, + }, storage::timeindex::AsTime, }; use raphtory_storage::mutation::addition_ops::{InternalAdditionOps, SessionAdditionOps}; use rayon::prelude::*; use std::{ + borrow::{Borrow, Cow}, collections::HashMap, sync::{ atomic::{AtomicBool, AtomicUsize, Ordering}, Arc, }, }; -use storage::{api::nodes::NodeSegmentOps, resolver::mapping_resolver::MappingResolver}; +use storage::resolver::GIDResolverOps; fn build_progress_bar(des: String, num_rows: usize) -> Result { BarBuilder::default() @@ -246,30 +251,35 @@ pub(crate) fn load_edges_from_df< let mut eid_col_resolved: Vec = vec![]; let mut eids_exist: Vec = vec![]; // exists or needs to be created - let cache = graph.get_cache(); let mut write_locked_graph = graph.write_lock().map_err(into_graph_err)?; // set the type of the resolver; - let resolver = Mapping::new(); let mut chunks = df_view.chunks.peekable(); - - if let Some(chunk) = chunks.peek() { - if let Ok(chunk) = chunk { - let src_col = chunk.node_col(src_index)?; - let dst_col = chunk.node_col(dst_index)?; - src_col.validate(graph, LoadError::MissingSrcError)?; - dst_col.validate(graph, LoadError::MissingDstError)?; - let mut iter = src_col.iter(); - - if let Some(id) = iter.next() { - graph - .resolve_node(id.as_node_ref()) - .map_err(|_| LoadError::FatalError)?; // initialize the type of the resolver - } - } - } else { - return Ok(()); - } + // let mapping = Mapping::new(); + // let mapping = write_locked_graph.graph().logical_to_physical.clone(); + + // if let Some(chunk) = chunks.peek() { + // if let Ok(chunk) = chunk { + // let src_col = chunk.node_col(src_index)?; + // let dst_col = chunk.node_col(dst_index)?; + // src_col.validate(graph, LoadError::MissingSrcError)?; + // dst_col.validate(graph, LoadError::MissingDstError)?; + // let mut iter = src_col.iter(); + + // if let Some(id) = iter.next() { + // let vid = graph + // .resolve_node(id.as_node_ref()) + // .map_err(|_| LoadError::FatalError)?; // initialize the type of the resolver + // mapping + // .set(id, vid.inner()) + // .map_err(|_| LoadError::FatalError)?; + // } else { + // return Ok(()); + // } + // } + // } else { + // return Ok(()); + // } for chunk in chunks { let df = chunk?; @@ -289,8 +299,11 @@ pub(crate) fn load_edges_from_df< }, )?; - let src_col_shared = atomic_usize_from_mut_slice(cast_slice_mut(&mut src_col_resolved)); - let dst_col_shared = atomic_usize_from_mut_slice(cast_slice_mut(&mut dst_col_resolved)); + src_col_resolved.resize_with(df.len(), Default::default); + dst_col_resolved.resize_with(df.len(), Default::default); + + // let src_col_shared = atomic_usize_from_mut_slice(cast_slice_mut(&mut src_col_resolved)); + // let dst_col_shared = atomic_usize_from_mut_slice(cast_slice_mut(&mut dst_col_resolved)); let layer = lift_layer_col(layer, layer_index, &df)?; let layer_col_resolved = layer.resolve(graph)?; @@ -301,64 +314,38 @@ pub(crate) fn load_edges_from_df< let dst_col = df.node_col(dst_index)?; dst_col.validate(graph, LoadError::MissingDstError)?; - let gid_type = src_col.dtype(); - - let node_count = &write_locked_graph.graph().node_count; - - resolver.run_with_locked(|mut shard| match gid_type { - GidType::Str => { - let shard = shard.as_str().unwrap(); - let src_iter = src_col.iter().map(|gid| gid.as_str().unwrap()).enumerate(); - - let dst_iter = dst_col.iter().map(|gid| gid.as_str().unwrap()).enumerate(); - - for (id, gid) in src_iter { - if let Some(vid) = - shard.resolve_node(gid, || VID(node_count.fetch_add(1, Ordering::Relaxed))) - { - src_col_shared[id].store(vid.0, Ordering::Relaxed); - } - } - - for (id, gid) in dst_iter { - if let Some(vid) = - shard.resolve_node(gid, || VID(node_count.fetch_add(1, Ordering::Relaxed))) - { - dst_col_shared[id].store(vid.0, Ordering::Relaxed); - } - } - Ok::<_, LoadError>(()) - } - GidType::U64 => { - let shard = shard.as_u64().unwrap(); - let src_iter = src_col.iter().map(|gid| gid.as_u64().unwrap()).enumerate(); - - let dst_iter = dst_col.iter().map(|gid| gid.as_u64().unwrap()).enumerate(); + // let gid_type = src_col.dtype(); - for (id, gid) in src_iter { - if let Some(vid) = - shard.resolve_node(&gid, || VID(node_count.fetch_add(1, Ordering::Relaxed))) - { - src_col_shared[id].store(vid.0, Ordering::Relaxed); - } - } - - for (id, gid) in dst_iter { - if let Some(vid) = - shard.resolve_node(&gid, || VID(node_count.fetch_add(1, Ordering::Relaxed))) - { - dst_col_shared[id].store(vid.0, Ordering::Relaxed); - } - } - Ok::<_, LoadError>(()) - } - })?; + let num_nodes = AtomicUsize::new(write_locked_graph.graph().internal_num_nodes()); - let time_col = df.time_col(time_index)?; + // let fallback_resolver = write_locked_graph.graph().logical_to_physical.clone(); + + // mapping + // .mapping() + // .run_with_locked(|mut shard| match gid_type { + // GidType::Str => load_into_shard( + // src_col_shared, + // dst_col_shared, + // &src_col, + // &dst_col, + // &num_nodes, + // shard.as_str().unwrap(), + // |gid| Cow::Borrowed(gid.as_str().unwrap()), + // |id| fallback_resolver.get_str(id), + // ), + // GidType::U64 => load_into_shard( + // src_col_shared, + // dst_col_shared, + // &src_col, + // &dst_col, + // &num_nodes, + // shard.as_u64().unwrap(), + // |gid| Cow::Owned(gid.as_u64().unwrap()), + // |id| fallback_resolver.get_u64(*id), + // ), + // })?; - let num_nodes = AtomicUsize::new(write_locked_graph.graph().internal_num_nodes()); // It's our graph, no one else can change it - src_col_resolved.resize_with(df.len(), Default::default); src_col .par_iter() .zip(src_col_resolved.par_iter_mut()) @@ -371,14 +358,10 @@ pub(crate) fn load_edges_from_df< if vid.is_new() { num_nodes.fetch_add(1, Ordering::Relaxed); } - if let Some(cache) = cache { - cache.resolve_node(vid, gid); - } *resolved = vid.inner(); Ok::<(), LoadError>(()) })?; - dst_col_resolved.resize_with(df.len(), Default::default); dst_col .par_iter() .zip(dst_col_resolved.par_iter_mut()) @@ -391,13 +374,12 @@ pub(crate) fn load_edges_from_df< if vid.is_new() { num_nodes.fetch_add(1, Ordering::Relaxed); } - if let Some(cache) = cache { - cache.resolve_node(vid, gid); - } *resolved = vid.inner(); Ok::<(), LoadError>(()) })?; + let time_col = df.time_col(time_index)?; + write_locked_graph.resize_chunks_to_num_nodes(num_nodes.load(Ordering::Relaxed)); // resolve all the edges @@ -453,89 +435,145 @@ pub(crate) fn load_edges_from_df< } }); - // link the destinations - write_locked_graph - .nodes - .par_iter_mut() - .enumerate() - .for_each(|(page_id, shard)| { - for (row, ((((src, (dst, dst_gid)), eid), time), layer)) in src_col_resolved - .iter() - .zip(dst_col_resolved.iter().zip(dst_col.iter())) - .zip(eid_col_resolved.iter()) - .zip(time_col.iter()) - .zip(layer_col_resolved.iter()) - .enumerate() - { - if let Some(dst_pos) = shard.resolve_pos(*dst) { - let t = TimeIndexEntry(time, start_idx + row); - let mut writer = shard.writer(); - writer.store_node_id(dst_pos, 0, dst_gid, 0); - writer.add_static_inbound_edge(dst_pos, *src, *eid, 0); - writer.add_inbound_edge(Some(t), dst_pos, *src, eid.with_layer(*layer), 0); - per_segment_edge_count[page_id].fetch_add(1, Ordering::Relaxed); - } - } - }); - - println!( - "Counts per shard: {:?}", - per_segment_edge_count - .iter() - .enumerate() - .filter(|(_, count)| count.load(Ordering::Relaxed) > 0) - .collect::>() - ); - write_locked_graph.resize_chunks_to_num_edges(num_edges.load(Ordering::Relaxed)); - write_locked_graph - .edges - .par_iter_mut() - .try_for_each(|shard| { - let mut t_props = vec![]; - let mut c_props = vec![]; - for (idx, (((((src, dst), time), eid), layer), exists)) in src_col_resolved - .iter() - .zip(dst_col_resolved.iter()) - .zip(time_col.iter()) - .zip(eid_col_resolved.iter()) - .zip(layer_col_resolved.iter()) - .zip( - eids_exist - .iter() - .map(|exists| exists.load(Ordering::Relaxed)), - ) + rayon::scope(|sc| { + sc.spawn(|_| { + // link the destinations + write_locked_graph + .nodes + .par_iter_mut() .enumerate() - { - if let Some(eid_pos) = shard.resolve_pos(*eid) { - let t = TimeIndexEntry(time, start_idx + idx); - let mut writer = shard.writer(); + .for_each(|(page_id, shard)| { + for (row, ((((src, (dst, dst_gid)), eid), time), layer)) in src_col_resolved + .iter() + .zip(dst_col_resolved.iter().zip(dst_col.iter())) + .zip(eid_col_resolved.iter()) + .zip(time_col.iter()) + .zip(layer_col_resolved.iter()) + .enumerate() + { + if let Some(dst_pos) = shard.resolve_pos(*dst) { + let t = TimeIndexEntry(time, start_idx + row); + let mut writer = shard.writer(); + writer.store_node_id(dst_pos, 0, dst_gid, 0); + writer.add_static_inbound_edge(dst_pos, *src, *eid, 0); + writer.add_inbound_edge( + Some(t), + dst_pos, + *src, + eid.with_layer(*layer), + 0, + ); + per_segment_edge_count[page_id].fetch_add(1, Ordering::Relaxed); + } + } + }); + }); - t_props.clear(); - t_props.extend(prop_cols.iter_row(idx)); + sc.spawn(|_| { + write_locked_graph.edges.par_iter_mut().for_each(|shard| { + let mut t_props = vec![]; + let mut c_props = vec![]; + for (idx, (((((src, dst), time), eid), layer), exists)) in src_col_resolved + .iter() + .zip(dst_col_resolved.iter()) + .zip(time_col.iter()) + .zip(eid_col_resolved.iter()) + .zip(layer_col_resolved.iter()) + .zip( + eids_exist + .iter() + .map(|exists| exists.load(Ordering::Relaxed)), + ) + .enumerate() + { + if let Some(eid_pos) = shard.resolve_pos(*eid) { + let t = TimeIndexEntry(time, start_idx + idx); + let mut writer = shard.writer(); - c_props.clear(); - c_props.extend(const_prop_cols.iter_row(idx)); - c_props.extend_from_slice(&shared_constant_properties); + t_props.clear(); + t_props.extend(prop_cols.iter_row(idx)); - writer.add_static_edge(Some(eid_pos), *src, *dst, 0, Some(exists)); - writer.update_c_props(eid_pos, *src, *dst, *layer, c_props.drain(..)); - writer.add_edge(t, eid_pos, *src, *dst, t_props.drain(..), *layer, 0); + c_props.clear(); + c_props.extend(const_prop_cols.iter_row(idx)); + c_props.extend_from_slice(&shared_constant_properties); + + writer.add_static_edge(Some(eid_pos), *src, *dst, 0, Some(exists)); + writer.update_c_props(eid_pos, *src, *dst, *layer, c_props.drain(..)); + writer.add_edge(t, eid_pos, *src, *dst, t_props.drain(..), *layer, 0); + } } - } - Ok::<(), GraphError>(()) - })?; - if let Some(cache) = cache { - cache.write()?; - } + }); + }); + }); start_idx += df.len(); let _ = pb.update(df.len()); } + + // put the mapping into the fallback resolver + // let fallback_resolver = &write_locked_graph.graph().logical_to_physical; + // match fallback_resolver.dtype() { + // Some(GidType::Str) => { + // fallback_resolver + // .bulk_set_str(mapping.iter_str()) + // .map_err(|_| LoadError::FatalError)?; + // } + // Some(GidType::U64) => { + // fallback_resolver + // .bulk_set_u64(mapping.iter_u64()) + // .map_err(|_| LoadError::FatalError)?; + // } + // _ => {} + // } + Ok(()) } +fn load_into_shard( + src_col_shared: &[AtomicUsize], + dst_col_shared: &[AtomicUsize], + src_col: &super::node_col::NodeCol, + dst_col: &super::node_col::NodeCol, + node_count: &AtomicUsize, + shard: &mut ResolverShardT<'_, T>, + mut mapper_fn: impl FnMut(GidRef<'_>) -> Cow<'_, Q>, + mut fallback_fn: impl FnMut(&Q) -> Option, +) -> Result<(), LoadError> +where + T: Clone + Eq + std::hash::Hash + Borrow, + Q: Eq + std::hash::Hash + ToOwned + ?Sized, +{ + let src_iter = src_col.iter().map(&mut mapper_fn).enumerate(); + + for (id, gid) in src_iter { + if let Some(vid) = shard.resolve_node(&gid, |id| { + // fallback_fn(id).map(Either::Right).unwrap_or_else(|| { + // // If the node does not exist, create a new VID + // Either::Left(VID(node_count.fetch_add(1, Ordering::Relaxed))) + // }) + Either::Left(VID(node_count.fetch_add(1, Ordering::Relaxed))) + }) { + src_col_shared[id].store(vid.0, Ordering::Relaxed); + } + } + + let dst_iter = dst_col.iter().map(mapper_fn).enumerate(); + for (id, gid) in dst_iter { + if let Some(vid) = shard.resolve_node(&gid, |id| { + // fallback_fn(id).map(Either::Right).unwrap_or_else(|| { + // // If the node does not exist, create a new VID + // Either::Left(VID(node_count.fetch_add(1, Ordering::Relaxed))) + // }) + Either::Left(VID(node_count.fetch_add(1, Ordering::Relaxed))) + }) { + dst_col_shared[id].store(vid.0, Ordering::Relaxed); + } + } + Ok::<_, LoadError>(()) +} + pub(crate) fn load_edge_deletions_from_df< G: StaticGraphViewOps + PropertyAdditionOps + AdditionOps + DeletionOps, >( @@ -1169,33 +1207,11 @@ mod tests { } #[test] - fn test_load_edges_with_cache() { - proptest!(|(edges in build_edge_list(100, 100), chunk_size in 1usize..=100)| { - let df_view = build_df(chunk_size, &edges); - let g = Graph::new(); - let cache_file = TempDir::new().unwrap(); - g.cache(cache_file.path()).unwrap(); - let props = ["str_prop", "int_prop"]; - load_edges_from_df(df_view, "time", "src", "dst", &props, &[], None, None, None, &g).unwrap(); - let g = Graph::load_cached(cache_file.path()).unwrap(); - let g2 = Graph::new(); - for (src, dst, time, str_prop, int_prop) in edges { - g2.add_edge(time, src, dst, [("str_prop", str_prop.clone().into_prop()), ("int_prop", int_prop.into_prop())], None).unwrap(); - let edge = g.edge(src, dst).unwrap().at(time); - assert_eq!(edge.properties().get("str_prop").unwrap_str(), str_prop); - assert_eq!(edge.properties().get("int_prop").unwrap_i64(), int_prop); - } - assert_graph_equal(&g, &g2); - }) - } - - #[test] - fn load_single_edge_with_cache() { - let edges = [(0, 0, 0, "".to_string(), 0)]; - let df_view = build_df(1, &edges); + fn test_load_edges_1() { + let edges = [(0, 1, 0, "a".to_string(), 0)]; + let chunk_size = 412; + let df_view = build_df(chunk_size, &edges); let g = Graph::new(); - let cache_file = TempDir::new().unwrap(); - g.cache(cache_file.path()).unwrap(); let props = ["str_prop", "int_prop"]; load_edges_from_df( df_view, @@ -1210,7 +1226,6 @@ mod tests { &g, ) .unwrap(); - let g = Graph::load_cached(cache_file.path()).unwrap(); let g2 = Graph::new(); for (src, dst, time, str_prop, int_prop) in edges { g2.add_edge( @@ -1230,4 +1245,67 @@ mod tests { } assert_graph_equal(&g, &g2); } + + // #[test] + // fn test_load_edges_with_cache() { + // proptest!(|(edges in build_edge_list(100, 100), chunk_size in 1usize..=100)| { + // let df_view = build_df(chunk_size, &edges); + // let g = Graph::new(); + // let cache_file = TempDir::new().unwrap(); + // g.cache(cache_file.path()).unwrap(); + // let props = ["str_prop", "int_prop"]; + // load_edges_from_df(df_view, "time", "src", "dst", &props, &[], None, None, None, &g).unwrap(); + // let g = Graph::load_cached(cache_file.path()).unwrap(); + // let g2 = Graph::new(); + // for (src, dst, time, str_prop, int_prop) in edges { + // g2.add_edge(time, src, dst, [("str_prop", str_prop.clone().into_prop()), ("int_prop", int_prop.into_prop())], None).unwrap(); + // let edge = g.edge(src, dst).unwrap().at(time); + // assert_eq!(edge.properties().get("str_prop").unwrap_str(), str_prop); + // assert_eq!(edge.properties().get("int_prop").unwrap_i64(), int_prop); + // } + // assert_graph_equal(&g, &g2); + // }) + // } + + // #[test] + // fn load_single_edge_with_cache() { + // let edges = [(0, 0, 0, "".to_string(), 0)]; + // let df_view = build_df(1, &edges); + // let g = Graph::new(); + // let cache_file = TempDir::new().unwrap(); + // g.cache(cache_file.path()).unwrap(); + // let props = ["str_prop", "int_prop"]; + // load_edges_from_df( + // df_view, + // "time", + // "src", + // "dst", + // &props, + // &[], + // None, + // None, + // None, + // &g, + // ) + // .unwrap(); + // let g = Graph::load_cached(cache_file.path()).unwrap(); + // let g2 = Graph::new(); + // for (src, dst, time, str_prop, int_prop) in edges { + // g2.add_edge( + // time, + // src, + // dst, + // [ + // ("str_prop", str_prop.clone().into_prop()), + // ("int_prop", int_prop.into_prop()), + // ], + // None, + // ) + // .unwrap(); + // let edge = g.edge(src, dst).unwrap().at(time); + // assert_eq!(edge.properties().get("str_prop").unwrap_str(), str_prop); + // assert_eq!(edge.properties().get("int_prop").unwrap_i64(), int_prop); + // } + // assert_graph_equal(&g, &g2); + // } } diff --git a/raphtory/src/python/graph/graph.rs b/raphtory/src/python/graph/graph.rs index a8dc19e6ec..b7b52fe51e 100644 --- a/raphtory/src/python/graph/graph.rs +++ b/raphtory/src/python/graph/graph.rs @@ -160,6 +160,11 @@ impl PyGraph { ) } + #[staticmethod] + pub fn load(path: PathBuf) -> Graph { + Graph::load_from_path(path) + } + fn __reduce__(&self) -> (PyGraphEncoder, (Vec,)) { let state = self.graph.encode_to_vec(); (PyGraphEncoder, (state,)) diff --git a/raphtory/src/serialise/parquet/mod.rs b/raphtory/src/serialise/parquet/mod.rs index 33c385a25f..ce3d43add2 100644 --- a/raphtory/src/serialise/parquet/mod.rs +++ b/raphtory/src/serialise/parquet/mod.rs @@ -1,5 +1,8 @@ use crate::{ - db::{api::storage::storage::Storage, graph::views::deletion_graph::PersistentGraph}, + db::{ + api::{storage::storage::Storage, view::MaterializedGraph}, + graph::views::deletion_graph::PersistentGraph, + }, errors::GraphError, io::parquet_loaders::{ load_edge_deletions_from_parquet, load_edge_props_from_parquet, load_edges_from_parquet, @@ -90,6 +93,17 @@ impl ParquetEncoder for PersistentGraph { } } +impl ParquetEncoder for MaterializedGraph { + fn encode_parquet(&self, path: impl AsRef) -> Result<(), GraphError> { + match self { + MaterializedGraph::EventGraph(graph) => graph.encode_parquet(path), + MaterializedGraph::PersistentGraph(persistent_graph) => { + persistent_graph.encode_parquet(path) + } + } + } +} + fn encode_graph_storage( g: &GraphStorage, path: impl AsRef, @@ -434,6 +448,17 @@ impl ParquetDecoder for PersistentGraph { } } +impl ParquetDecoder for MaterializedGraph { + fn decode_parquet(path: impl AsRef) -> Result + where + Self: Sized, + { + let gs = decode_graph_storage(path.as_ref(), GraphType::EventGraph) + .or_else(|_| decode_graph_storage(path.as_ref(), GraphType::PersistentGraph))?; + Ok(MaterializedGraph::EventGraph(Graph::from_storage(gs))) + } +} + #[cfg(test)] mod test { use super::*; From eb4e9f38dfcbf3f29c9191f3e8c24d5c5bad54c4 Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Thu, 24 Jul 2025 16:03:15 +0100 Subject: [PATCH 089/321] Can load from python into graph --- db4-graph/src/lib.rs | 4 +- db4-storage/src/api/edges.rs | 4 +- db4-storage/src/segments/edge_entry.rs | 36 ++++++-------- db4-storage/src/segments/node_entry.rs | 48 ++++++++++++------- .../src/graph/edges/edge_storage_ops.rs | 4 +- 5 files changed, 50 insertions(+), 46 deletions(-) diff --git a/db4-graph/src/lib.rs b/db4-graph/src/lib.rs index 89dca9350c..3dde06ec75 100644 --- a/db4-graph/src/lib.rs +++ b/db4-graph/src/lib.rs @@ -92,8 +92,8 @@ impl, ES = ES>> TemporalGraph { let storage = Layer::load(graph_dir.as_ref()) .unwrap_or_else(|_| panic!("Failed to load graph from path: {graph_dir:?}")); let gid_resolver_dir = graph_dir.as_ref().join("gid_resolver"); - let resolver = GIDResolver::new(&gid_resolver_dir).unwrap_or_else(|_| { - panic!("Failed to load GID resolver from path: {gid_resolver_dir:?}") + let resolver = GIDResolver::new(&gid_resolver_dir).unwrap_or_else(|err| { + panic!("Failed to load GID resolver from path: {gid_resolver_dir:?} {err}") }); let node_count = AtomicUsize::new(storage.nodes().num_nodes()); Self { diff --git a/db4-storage/src/api/edges.rs b/db4-storage/src/api/edges.rs index 2f9cc05dfb..6b45241c2e 100644 --- a/db4-storage/src/api/edges.rs +++ b/db4-storage/src/api/edges.rs @@ -144,9 +144,9 @@ pub trait EdgeRefOps<'a>: Copy + Clone + Send + Sync { fn layer_t_prop(self, layer_id: usize, prop_id: usize) -> Self::TProps; - fn src(&self) -> VID; + fn src(&self) -> Option; - fn dst(&self) -> VID; + fn dst(&self) -> Option; fn edge_id(&self) -> EID; } diff --git a/db4-storage/src/segments/edge_entry.rs b/db4-storage/src/segments/edge_entry.rs index 22f76e33ef..b284ef2a1b 100644 --- a/db4-storage/src/segments/edge_entry.rs +++ b/db4-storage/src/segments/edge_entry.rs @@ -83,12 +83,16 @@ impl<'a> WithTimeCells<'a> for MemEdgeRef<'a> { layer_id: usize, range: Option<(TimeIndexEntry, TimeIndexEntry)>, ) -> impl Iterator + 'a { - let t_cell = MemAdditions::Props(self.es.as_ref()[layer_id].times_from_props(self.pos)); - std::iter::once( - range - .map(|(start, end)| t_cell.range(start..end)) - .unwrap_or_else(|| t_cell), - ) + self.es + .as_ref() + .get(layer_id) + .map(|layer| MemAdditions::Props(layer.times_from_props(self.pos))) + .into_iter() + .map(move |t_props| { + range + .map(|(start, end)| t_props.range(start..end)) + .unwrap_or_else(|| t_props) + }) } fn additions_tc( @@ -173,24 +177,12 @@ impl<'a> EdgeRefOps<'a> for MemEdgeRef<'a> { EdgeTProps::new_with_layer(self, layer_id, prop_id) } - fn src(&self) -> VID { - self.es.as_ref()[0] - .get(&self.pos) - .map(|entry| entry.src) - .unwrap_or_else(|| { - panic!( - "Edge must have a source vertex at position {:?} on segment_id: {}", - self.pos, - self.es.as_ref()[0].segment_id() - ) - }) + fn src(&self) -> Option { + self.es.as_ref()[0].get(&self.pos).map(|entry| entry.src) } - fn dst(&self) -> VID { - self.es.as_ref()[0] - .get(&self.pos) - .map(|entry| entry.dst) - .expect("Edge must have a destination vertex") + fn dst(&self) -> Option { + self.es.as_ref()[0].get(&self.pos).map(|entry| entry.dst) } fn edge_id(&self) -> EID { diff --git a/db4-storage/src/segments/node_entry.rs b/db4-storage/src/segments/node_entry.rs index 915d41a42b..4941007452 100644 --- a/db4-storage/src/segments/node_entry.rs +++ b/db4-storage/src/segments/node_entry.rs @@ -75,12 +75,16 @@ impl<'a> WithTimeCells<'a> for MemNodeRef<'a> { layer_id: usize, range: Option<(TimeIndexEntry, TimeIndexEntry)>, ) -> impl Iterator + 'a { - let t_cell = MemAdditions::Props(self.ns.as_ref()[layer_id].times_from_props(self.pos)); - std::iter::once( - range - .map(|(start, end)| t_cell.range(start..end)) - .unwrap_or_else(|| t_cell), - ) + self.ns + .as_ref() + .get(layer_id) + .map(|seg| MemAdditions::Props(seg.times_from_props(self.pos))) + .into_iter() + .map(move |t_cell| { + range + .map(|(start, end)| t_cell.range(start..end)) + .unwrap_or_else(|| t_cell) + }) } fn additions_tc( @@ -88,12 +92,16 @@ impl<'a> WithTimeCells<'a> for MemNodeRef<'a> { layer_id: usize, range: Option<(TimeIndexEntry, TimeIndexEntry)>, ) -> impl Iterator + 'a { - let additions = MemAdditions::Edges(self.ns.as_ref()[layer_id].additions(self.pos)); - std::iter::once( - range - .map(|(start, end)| additions.range(start..end)) - .unwrap_or_else(|| additions), - ) + self.ns + .as_ref() + .get(layer_id) + .map(|seg| MemAdditions::Edges(seg.additions(self.pos))) + .into_iter() + .map(move |t_cell| { + range + .map(|(start, end)| t_cell.range(start..end)) + .unwrap_or_else(|| t_cell) + }) } fn deletions_tc( @@ -101,12 +109,16 @@ impl<'a> WithTimeCells<'a> for MemNodeRef<'a> { layer_id: usize, range: Option<(TimeIndexEntry, TimeIndexEntry)>, ) -> impl Iterator + 'a { - let deletions = MemAdditions::Edges(self.ns.as_ref()[layer_id].deletions(self.pos)); - std::iter::once( - range - .map(|(start, end)| deletions.range(start..end)) - .unwrap_or_else(|| deletions), - ) + self.ns + .as_ref() + .get(layer_id) + .map(|seg| MemAdditions::Edges(seg.deletions(self.pos))) + .into_iter() + .map(move |t_cell| { + range + .map(|(start, end)| t_cell.range(start..end)) + .unwrap_or_else(|| t_cell) + }) } fn num_layers(&self) -> usize { diff --git a/raphtory-storage/src/graph/edges/edge_storage_ops.rs b/raphtory-storage/src/graph/edges/edge_storage_ops.rs index 3712be184f..e5bc29e3bd 100644 --- a/raphtory-storage/src/graph/edges/edge_storage_ops.rs +++ b/raphtory-storage/src/graph/edges/edge_storage_ops.rs @@ -206,11 +206,11 @@ impl<'a> EdgeStorageOps<'a> for storage::EdgeEntryRef<'a> { } fn src(self) -> VID { - EdgeRefOps::src(&self) + EdgeRefOps::src(&self).expect("EdgeRefOps::src should not return None") } fn dst(self) -> VID { - EdgeRefOps::dst(&self) + EdgeRefOps::dst(&self).expect("EdgeRefOps::dst should not return None") } fn eid(self) -> EID { From df7a395480b04a2cde2f83d7e07860be706ee2ca Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Fri, 25 Jul 2025 15:57:02 +0100 Subject: [PATCH 090/321] fix issues with interop with Raphtory --- db4-graph/src/lib.rs | 1 + db4-storage/src/pages/flush_thread.rs | 50 ++++-- db4-storage/src/pages/layer_counter.rs | 10 +- db4-storage/src/pages/mod.rs | 11 +- db4-storage/src/pages/node_page/writer.rs | 2 + db4-storage/src/segments/node.rs | 2 +- db4-storage/src/segments/node_entry.rs | 3 +- ignore_Cargo.toml | 201 ++++++++++++++++++++++ raphtory/examples/load_graph.rs | 58 +++++++ raphtory/src/io/arrow/df_loaders.rs | 3 +- 10 files changed, 306 insertions(+), 35 deletions(-) create mode 100644 ignore_Cargo.toml create mode 100644 raphtory/examples/load_graph.rs diff --git a/db4-graph/src/lib.rs b/db4-graph/src/lib.rs index 3dde06ec75..13a6c5857a 100644 --- a/db4-graph/src/lib.rs +++ b/db4-graph/src/lib.rs @@ -96,6 +96,7 @@ impl, ES = ES>> TemporalGraph { panic!("Failed to load GID resolver from path: {gid_resolver_dir:?} {err}") }); let node_count = AtomicUsize::new(storage.nodes().num_nodes()); + println!("LOADED NODE COUNT {node_count:?}"); Self { graph_dir, logical_to_physical: resolver.into(), diff --git a/db4-storage/src/pages/flush_thread.rs b/db4-storage/src/pages/flush_thread.rs index 153e81b792..f0d3da9514 100644 --- a/db4-storage/src/pages/flush_thread.rs +++ b/db4-storage/src/pages/flush_thread.rs @@ -1,4 +1,7 @@ -use std::{sync::Arc, thread::JoinHandle}; +use std::{ + sync::{Arc, atomic::AtomicBool}, + thread::JoinHandle, +}; use itertools::Itertools; @@ -12,7 +15,8 @@ use crate::{ // This should be a rayon thread with a reference to Arc that will flush the data to disk periodically. #[derive(Debug)] pub(crate) struct FlushThread { - handler: JoinHandle<()>, + stop: Arc, + handler: Option>, } impl FlushThread { @@ -23,39 +27,55 @@ impl FlushThread { >( nodes: Arc>, ) -> Self { + let stop = Arc::new(AtomicBool::new(false)); let handler = std::thread::spawn({ let nodes = Arc::clone(&nodes); + let stop = Arc::clone(&stop); move || { // Implement the logic to periodically flush the nodes to disk. loop { + if stop.load(std::sync::atomic::Ordering::Relaxed) { + break; + } // Flush logic here - std::thread::sleep(std::time::Duration::from_millis(100)); // Example sleep duration + std::thread::sleep(std::time::Duration::from_millis(50)); // Example sleep duration // let's do some stats, run over all the segments and decrement the event id then get the max and the min // the event_id functions are atomic so no need to lock the segments - let min = Iterator::min( - nodes - .segments() - .iter() - .map(|(i, ns)| (i, ns.decrement_event_id())), - ); - - if let Some((segment_id, event_id)) = min { + for (segment_id, event_id) in nodes + .segments() + .iter() + .map(|(i, ns)| (i, ns.decrement_event_id())) + { if event_id <= 0 { // If the event id is 0, we can flush this segment let segment = nodes.segments().get(segment_id).unwrap(); + // println!("Flushing from the flusher thread {segment_id}"); segment.flush(); - segment.increment_event_id(1000); // ignore this segment for the next 1000 events + segment.increment_event_id(500); // ignore this segment for the next 1000 events // eprintln!("Triggered flush for {segment_id}") } } - - // println!("Min Max event ids for segments {min_max:?}"); } } }); - Self { handler } + Self { + stop, + handler: Some(handler), + } + } +} + +impl Drop for FlushThread { + fn drop(&mut self) { + // Wait for the thread to finish + self.stop.store(true, std::sync::atomic::Ordering::Relaxed); + if let Some(handler) = self.handler.take() { + if let Err(e) = handler.join() { + eprintln!("Flush thread join error: {e:?}"); + } + } } } diff --git a/db4-storage/src/pages/layer_counter.rs b/db4-storage/src/pages/layer_counter.rs index ff86265e76..ebc51f5c39 100644 --- a/db4-storage/src/pages/layer_counter.rs +++ b/db4-storage/src/pages/layer_counter.rs @@ -7,17 +7,15 @@ pub struct GraphStats { layers: boxcar::Vec, earliest: MinCounter, latest: MaxCounter, - global_event_counter: AtomicUsize, } impl> From for GraphStats { fn from(iter: I) -> Self { - let layers = iter.into_iter().map(|_| Default::default()).collect(); + let layers = iter.into_iter().map(AtomicUsize::new).collect(); Self { layers, earliest: MinCounter::new(), latest: MaxCounter::new(), - global_event_counter: AtomicUsize::new(0), } } } @@ -36,7 +34,6 @@ impl GraphStats { layers, earliest: MinCounter::new(), latest: MaxCounter::new(), - global_event_counter: AtomicUsize::new(0), } } @@ -62,11 +59,6 @@ impl GraphStats { self.latest.get() } - pub fn increment_global_event_counter(&self) -> usize { - self.global_event_counter - .fetch_add(1, std::sync::atomic::Ordering::Relaxed) - } - pub fn increment(&self, layer_id: usize) -> usize { let counter = self.get_or_create_layer(layer_id); counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed) diff --git a/db4-storage/src/pages/mod.rs b/db4-storage/src/pages/mod.rs index 61e36206fb..e925433829 100644 --- a/db4-storage/src/pages/mod.rs +++ b/db4-storage/src/pages/mod.rs @@ -58,7 +58,7 @@ pub const NODE_TYPE_PROP_KEY: &str = "_raphtory_node_type"; #[derive(Debug)] pub struct GraphStore { nodes: Arc>, - node_flush_thread: FlushThread, + // node_flush_thread: FlushThread, edges: Arc>, event_id: AtomicUsize, _ext: EXT, @@ -151,7 +151,7 @@ impl< let t_len = edges.t_len(); Ok(Self { - node_flush_thread: FlushThread::new::<_, ES, _>(nodes.clone()), + // node_flush_thread: FlushThread::new::<_, ES, _>(nodes.clone()), nodes, edges, event_id: AtomicUsize::new(t_len), @@ -201,7 +201,7 @@ impl< .expect("Unrecoverable! Failed to write graph meta"); Self { - node_flush_thread: FlushThread::new::<_, ES, _>(nodes.clone()), + // node_flush_thread: FlushThread::new::<_, ES, _>(nodes.clone()), nodes, edges, event_id: AtomicUsize::new(0), @@ -345,7 +345,7 @@ impl< let (dst_chunk, _) = self.nodes.resolve_pos(dst); let acquire_node_writers = || { - let writer_pair = if src_chunk < dst_chunk { + if src_chunk < dst_chunk { let src_writer = self.node_writer(src_chunk); let dst_writer = self.node_writer(dst_chunk); WriterPair::Different { @@ -362,8 +362,7 @@ impl< } else { let writer = self.node_writer(src_chunk); WriterPair::Same { writer } - }; - writer_pair + } }; let node_writers = acquire_node_writers(); diff --git a/db4-storage/src/pages/node_page/writer.rs b/db4-storage/src/pages/node_page/writer.rs index b824617e7c..6a8bc7fb2d 100644 --- a/db4-storage/src/pages/node_page/writer.rs +++ b/db4-storage/src/pages/node_page/writer.rs @@ -56,6 +56,7 @@ impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> NodeWri lsn: u64, ) { let src_pos = src_pos.into(); + let dst = dst.into(); if let Some(t) = t { self.l_counter.update_time(t.t()); } @@ -102,6 +103,7 @@ impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> NodeWri lsn: u64, ) { let e_id = e_id.into(); + let src = src.into(); if let Some(t) = t { self.l_counter.update_time(t.t()); } diff --git a/db4-storage/src/segments/node.rs b/db4-storage/src/segments/node.rs index 2523115782..332ca8e3cf 100644 --- a/db4-storage/src/segments/node.rs +++ b/db4-storage/src/segments/node.rs @@ -167,7 +167,7 @@ impl MemNodeSegment { pub fn has_node(&self, n: LocalPOS, layer_id: usize) -> bool { self.layers .get(layer_id) - .is_some_and(|layer| layer.items().first().is_some_and(|v| *v)) + .is_some_and(|layer| layer.items().get(n.0).is_some_and(|x| *x)) } pub fn get_out_edge(&self, n: LocalPOS, dst: VID, layer_id: usize) -> Option { diff --git a/db4-storage/src/segments/node_entry.rs b/db4-storage/src/segments/node_entry.rs index 4941007452..4680a55bd0 100644 --- a/db4-storage/src/segments/node_entry.rs +++ b/db4-storage/src/segments/node_entry.rs @@ -142,7 +142,6 @@ impl<'a> WithTProps<'a> for MemNodeRef<'a> { self.ns.as_ref()[layer_id] .t_prop(node_pos, prop_id) .into_iter() - .map(|t_prop| t_prop.into()) } } @@ -227,6 +226,6 @@ impl<'a> NodeRefOps<'a> for MemNodeRef<'a> { .as_ref() .get(layer_id) .and_then(|seg| seg.items().get(self.pos.0)) - .map_or(false, |x| *x) + .is_some_and(|x| *x) } } diff --git a/ignore_Cargo.toml b/ignore_Cargo.toml new file mode 100644 index 0000000000..13db27b683 --- /dev/null +++ b/ignore_Cargo.toml @@ -0,0 +1,201 @@ +[workspace] +members = [ + "raphtory", + "raphtory-cypher", + "raphtory-benchmark", + "examples/rust", + "examples/netflow", + "examples/custom-gql-apis", + "python", + "raphtory-graphql", + "raphtory-api", + "raphtory-core", + "raphtory-storage", + # "db4-common", + "db4-storage", + "db4-graph", +] +default-members = [ + "raphtory", + # "db4-common", + "db4-storage", + "db4-graph", +] +resolver = "2" + +[workspace.package] +version = "0.15.1" +documentation = "https://raphtory.readthedocs.io/en/latest/" +repository = "https://github.com/Raphtory/raphtory/" +license = "GPL-3.0" +readme = "README.md" +homepage = "https://github.com/Raphtory/raphtory/" +keywords = ["graph", "temporal-graph", "temporal"] +authors = ["Pometry"] +rust-version = "1.86.0" +edition = "2021" + +# debug symbols are using a lot of resources +[profile.dev] +split-debuginfo = "unpacked" +debug = false + +[profile.release-with-debug] +inherits = "release" +debug = true + +# use this if you really need debug symbols +[profile.with-debug] +inherits = "dev" +debug = true + +# for fast one-time builds (e.g., docs/CI) +[profile.build-fast] +inherits = "dev" +debug = false +incremental = false + + +[workspace.dependencies] +#[public-storage] +pometry-storage = { version = ">=0.8.1", path = "pometry-storage" } +#[private-storage] +#pometry-storage = { path = "pometry-storage-private", package = "pometry-storage-private" } +async-graphql = { version = "7.0.16", features = ["dynamic-schema"] } +bincode = "1.3.3" +bitvec = "1.0.1" +boxcar = "0.2.8" +async-graphql-poem = "7.0.16" +dynamic-graphql = "0.10.1" +reqwest = { version = "0.12.8", default-features = false, features = [ + "rustls-tls", + "multipart", + "json", +] } +iter-enum = { version = "1.2.0", features = ["rayon"] } +serde = { version = "1.0.197", features = ["derive", "rc"] } +serde_json = "1.0.114" +pyo3 = { version = "=0.23.3", features = ["multiple-pymethods", "chrono"] } +pyo3-build-config = "=0.23.3" +pyo3-arrow = "0.6" +numpy = "0.23.0" +itertools = "0.13.0" +rand = "0.8.5" +rayon = "1.8.1" +roaring = "0.10.6" +sorted_vector_map = "0.2.0" +tokio = { version = "1.43.1", features = ["full"] } +once_cell = "1.19.0" +parking_lot = { version = "0.12.1", features = [ + "serde", + "arc_lock", + "send_guard", +] } +ordered-float = "4.2.0" +chrono = { version = "=0.4.38", features = ["serde"] } +tempfile = "3.10.0" +futures-util = "0.3.30" +thiserror = "2.0.0" +dotenv = "0.15.0" +csv = "1.3.0" +flate2 = "1.0.28" +regex = "1.10.3" +num-traits = "0.2.18" +num-integer = "0.1" +rand_distr = "0.4.3" +rustc-hash = "2.0.0" +twox-hash = "2.1.0" +lock_api = { version = "0.4.11", features = ["arc_lock", "serde"] } +dashmap = { version = "6.0.1", features = ["serde", "rayon"] } +enum_dispatch = "0.3.12" +glam = "0.29.0" +quad-rand = "0.2.1" +zip = "2.3.0" +neo4rs = "0.8.0" +bzip2 = "0.4.4" +tantivy = "0.22.0" +async-trait = "0.1.77" +async-openai = "0.26.0" +num = "0.4.1" +display-error-chain = "0.2.0" +polars-arrow = "0.42.0" +polars-parquet = "0.42.0" +polars-core = "0.42.0" +polars-io = "0.42.0" +bigdecimal = { version = "0.4.7", features = ["serde"] } +kdam = "0.6.2" +hashbrown = "0.15.1" +pretty_assertions = "1.4.0" +quickcheck = "1.0.3" +quickcheck_macros = "1.0.0" +streaming-stats = "0.2.3" +proptest = "1.4.0" +proptest-derive = "0.5.1" +criterion = "0.5.1" +crossbeam-channel = "0.5.15" +base64 = "0.22.1" +jsonwebtoken = "9.3.1" +spki = "0.7.3" +poem = { version = "3.0.1", features = ["cookie"] } +opentelemetry = "0.27.1" +opentelemetry_sdk = { version = "0.27.1", features = ["rt-tokio"] } +opentelemetry-otlp = { version = "0.27.0" } +tracing = "0.1.37" +tracing-opentelemetry = "0.28.0" +tracing-subscriber = { version = "0.3.16", features = ["std", "env-filter"] } +indoc = "2.0.5" +walkdir = "2" +config = "0.14.0" +either = "=1.11.0" +clap = { version = "4.5.21", features = ["derive", "env"] } +memmap2 = { version = "0.9.4" } +ahash = { version = "0.8.3", features = ["serde"] } +bytemuck = { version = "1.18.0", features = ["derive"] } +ouroboros = "0.18.3" +url = "2.2" +base64-compat = { package = "base64-compat", version = "1.0.0" } +prost = "0.13.1" +prost-types = "0.13.1" +prost-build = "0.13.1" +lazy_static = "1.4.0" +pest = "2.7.8" +pest_derive = "2.7.8" +minijinja = "2.2.0" +minijinja-contrib = { version = "2.2.0", features = ["datetime"] } +datafusion = { version = "43.0.0" } +arroy = "0.6.1" +heed = "0.22.0" +sysinfo = "0.35.1" +sqlparser = "0.51.0" +futures = "0.3" +arrow = { version = "=53.4.1" } +parquet = { version = "=53.4.1" } +arrow-json = { version = "=53.4.1" } +arrow-buffer = { version = "=53.4.1" } +arrow-schema = { version = "=53.4.1" } +arrow-array = { version = "=53.4.1" } +arrow-ipc = { version = "=53.4.1" } +arrow-csv = { version = "=53.4.1" } + +moka = { version = "0.12.7", features = ["sync"] } +indexmap = { version = "2.7.0", features = ["rayon"] } +fake = { version = "3.1.0", features = ["chrono"] } +strsim = { version = "0.11.1" } +uuid = { version = "1.16.0", features = ["v4"] } + +raphtory = { version = "0.15.1", path = "./raphtory", default-features = false } +raphtory-api = { version = "0.15.1", path = "./raphtory-api", default-features = false } +raphtory-core = { version = "0.15.1", path = "./raphtory-core", default-features = false } +raphtory-graphql = { version = "0.15.1", path = "./raphtory-graphql", default-features = false } + +# Make sure that transitive dependencies stick to disk_graph 50 +[patch.crates-io] +arrow-buffer = { git = "https://github.com/apache/arrow-rs.git", tag = "53.4.1" } +arrow-schema = { git = "https://github.com/apache/arrow-rs.git", tag = "53.4.1" } +arrow-data = { git = "https://github.com/apache/arrow-rs.git", tag = "53.4.1" } +arrow-array = { git = "https://github.com/apache/arrow-rs.git", tag = "53.4.1" } +arrow-csv = { git = "https://github.com/apache/arrow-rs.git", tag = "53.4.1" } + +[workspace.dependencies.storage] +package = "db4-storage" +path = "db4-storage" diff --git a/raphtory/examples/load_graph.rs b/raphtory/examples/load_graph.rs new file mode 100644 index 0000000000..7c983a589b --- /dev/null +++ b/raphtory/examples/load_graph.rs @@ -0,0 +1,58 @@ +use std::path::PathBuf; + +use raphtory::{ + io::parquet_loaders::load_edges_from_parquet, + prelude::{Graph, GraphViewOps}, +}; +use raphtory_storage::core_ops::CoreGraphOps; + +fn main() { + let graph_path = PathBuf::from("/Volumes/Work/tether/graphs/raphtory_graph"); + let layers = [ + "dai_ava_edge_list", + "usp_ava_edge_list", + "usdc_e_ava_edge_list", + ]; + let parquet_root = "/Volumes/Work/tether/avalance_table"; + + for layer in layers { + let parquet_path = format!("{parquet_root}/{layer}"); + println!("Loading layer: {layer} from {parquet_path}"); + let g = if graph_path.exists() { + Graph::load_from_path(&graph_path) + } else { + Graph::new_at_path(&graph_path) + }; + + load_edges_from_parquet( + &g, + &parquet_path, + "transaction_timestamp", + "transfer_sender_cluster_id", + "transfer_receiver_cluster_id", + &[ + "transaction_hash", + "transfer_index", + "transfer_amount_asset", + "transfer_amount_usd", + "transfer_sender_name", + "transfer_receiver_name", + "receiver_address", + "transfer_sender_category", + "transfer_receiver_category", + ], + &[], + None, + Some(layer), + None, + ) + .expect("Failed to load edges from parquet"); + + println!( + "num_edges: {} num_nodes: {}, layers: {:?}", + g.unfiltered_num_edges(), + g.unfiltered_num_nodes(), + g.unique_layers().collect::>() + ); + } +} diff --git a/raphtory/src/io/arrow/df_loaders.rs b/raphtory/src/io/arrow/df_loaders.rs index 0aea94b4b0..ce5dcff355 100644 --- a/raphtory/src/io/arrow/df_loaders.rs +++ b/raphtory/src/io/arrow/df_loaders.rs @@ -281,6 +281,7 @@ pub(crate) fn load_edges_from_df< // return Ok(()); // } + let num_nodes = AtomicUsize::new(write_locked_graph.graph().internal_num_nodes()); for chunk in chunks { let df = chunk?; let prop_cols = combine_properties(properties, &properties_indices, &df, |key, dtype| { @@ -316,8 +317,6 @@ pub(crate) fn load_edges_from_df< // let gid_type = src_col.dtype(); - let num_nodes = AtomicUsize::new(write_locked_graph.graph().internal_num_nodes()); - // let fallback_resolver = write_locked_graph.graph().logical_to_physical.clone(); // mapping From 5a8b2bd4813001dd22be449fa7acf118fde5dd59 Mon Sep 17 00:00:00 2001 From: Fabian Murariu Date: Fri, 25 Jul 2025 17:11:34 +0100 Subject: [PATCH 091/321] move est_size onto the node segment not the locked part --- db4-graph/src/lib.rs | 1 - db4-storage/src/api/nodes.rs | 3 ++ db4-storage/src/pages/flush_thread.rs | 23 ++++++++- db4-storage/src/pages/node_page/writer.rs | 16 ++++-- db4-storage/src/segments/node.rs | 61 +++++++++++------------ raphtory/examples/load_graph.rs | 18 +++++-- raphtory/src/io/arrow/df_loaders.rs | 1 + 7 files changed, 80 insertions(+), 43 deletions(-) diff --git a/db4-graph/src/lib.rs b/db4-graph/src/lib.rs index 13a6c5857a..3dde06ec75 100644 --- a/db4-graph/src/lib.rs +++ b/db4-graph/src/lib.rs @@ -96,7 +96,6 @@ impl, ES = ES>> TemporalGraph { panic!("Failed to load GID resolver from path: {gid_resolver_dir:?} {err}") }); let node_count = AtomicUsize::new(storage.nodes().num_nodes()); - println!("LOADED NODE COUNT {node_count:?}"); Self { graph_dir, logical_to_physical: resolver.into(), diff --git a/db4-storage/src/api/nodes.rs b/db4-storage/src/api/nodes.rs index 1cecbc7726..9703c07fa6 100644 --- a/db4-storage/src/api/nodes.rs +++ b/db4-storage/src/api/nodes.rs @@ -112,6 +112,9 @@ pub trait NodeSegmentOps: Send + Sync + std::fmt::Debug + 'static { fn locked(self: &Arc) -> Self::ArcLockedSegment; fn flush(&self); + + fn est_size(&self) -> usize; + fn increment_est_size(&self, size: usize) -> usize; } pub trait LockedNSSegment: std::fmt::Debug + Send + Sync { diff --git a/db4-storage/src/pages/flush_thread.rs b/db4-storage/src/pages/flush_thread.rs index f0d3da9514..fe394f91a8 100644 --- a/db4-storage/src/pages/flush_thread.rs +++ b/db4-storage/src/pages/flush_thread.rs @@ -1,4 +1,5 @@ use std::{ + collections::BinaryHeap, sync::{Arc, atomic::AtomicBool}, thread::JoinHandle, }; @@ -43,19 +44,39 @@ impl FlushThread { // let's do some stats, run over all the segments and decrement the event id then get the max and the min // the event_id functions are atomic so no need to lock the segments + let mut total_est_size = 0; + let mut smallest_event_id_segments = BinaryHeap::new(); + // top 10 segments with the smallest event id for (segment_id, event_id) in nodes .segments() .iter() .map(|(i, ns)| (i, ns.decrement_event_id())) { + let segment = nodes.segments().get(segment_id).unwrap(); if event_id <= 0 { // If the event id is 0, we can flush this segment - let segment = nodes.segments().get(segment_id).unwrap(); // println!("Flushing from the flusher thread {segment_id}"); segment.flush(); segment.increment_event_id(500); // ignore this segment for the next 1000 events // eprintln!("Triggered flush for {segment_id}") } + if smallest_event_id_segments.len() < 10 { + smallest_event_id_segments.push((event_id, segment_id)); + } else if let Some((top_event_id, _)) = + smallest_event_id_segments.peek().cloned() + { + if event_id < top_event_id { + smallest_event_id_segments.pop(); + smallest_event_id_segments.push((event_id, segment_id)); + } + } + + total_est_size += segment.est_size(); + + println!( + "TOTAL EST SIZE {}GB", + total_est_size as f64 / (1024f64 * 1024f64) + ) } } } diff --git a/db4-storage/src/pages/node_page/writer.rs b/db4-storage/src/pages/node_page/writer.rs index 6a8bc7fb2d..0a2e75b2c7 100644 --- a/db4-storage/src/pages/node_page/writer.rs +++ b/db4-storage/src/pages/node_page/writer.rs @@ -63,9 +63,10 @@ impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> NodeWri let e_id = e_id.into(); let layer_id = e_id.layer(); - let is_new_node = self + let (is_new_node, add) = self .mut_segment .add_outbound_edge(t, src_pos, dst, e_id, lsn); + self.page.increment_est_size(add); if is_new_node && !self.page.check_node(src_pos, layer_id) { self.l_counter.increment(layer_id); @@ -109,10 +110,12 @@ impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> NodeWri } let layer = e_id.layer(); let dst_pos = dst_pos.into(); - let is_new_node = self + let (is_new_node, add) = self .mut_segment .add_inbound_edge(t, dst_pos, src, e_id, lsn); + self.page.increment_est_size(add); + if is_new_node && !self.page.check_node(dst_pos, layer) { self.l_counter.increment(layer); } @@ -128,7 +131,8 @@ impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> NodeWri ) { self.mut_segment.as_mut()[layer_id].set_lsn(lsn); self.l_counter.update_time(t.t()); - let is_new_node = self.mut_segment.add_props(t, pos, layer_id, props); + let (is_new_node, add) = self.mut_segment.add_props(t, pos, layer_id, props); + self.page.increment_est_size(add); if is_new_node && !self.page.check_node(pos, layer_id) { self.l_counter.increment(layer_id); } @@ -142,7 +146,8 @@ impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> NodeWri lsn: u64, ) { self.mut_segment.as_mut()[layer_id].set_lsn(lsn); - let is_new_node = self.mut_segment.update_c_props(pos, layer_id, props); + let (is_new_node, add) = self.mut_segment.update_c_props(pos, layer_id, props); + self.page.increment_est_size(add); if is_new_node && !self.page.check_node(pos, layer_id) { self.l_counter.increment(layer_id); } @@ -152,7 +157,8 @@ impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> NodeWri let layer_id = e_id.layer(); self.l_counter.update_time(t.t()); self.mut_segment.as_mut()[layer_id].set_lsn(lsn); - self.mut_segment.update_timestamp(t, pos, e_id); + let add = self.mut_segment.update_timestamp(t, pos, e_id); + self.page.increment_est_size(add); } pub fn get_out_edge(&self, pos: LocalPOS, dst: VID, layer_id: usize) -> Option { diff --git a/db4-storage/src/segments/node.rs b/db4-storage/src/segments/node.rs index 332ca8e3cf..5e362234a5 100644 --- a/db4-storage/src/segments/node.rs +++ b/db4-storage/src/segments/node.rs @@ -15,7 +15,7 @@ use std::{ ops::{Deref, DerefMut}, sync::{ Arc, - atomic::{AtomicI64, Ordering}, + atomic::{AtomicI64, AtomicUsize, Ordering}, }, }; @@ -33,7 +33,6 @@ pub struct MemNodeSegment { segment_id: usize, max_page_len: usize, layers: Vec>, - est_size: usize, } impl>> From for MemNodeSegment { @@ -45,12 +44,10 @@ impl>> From for MemNodeSegm ); let segment_id = layers[0].segment_id(); let max_page_len = layers[0].max_page_len(); - let est_size = layers.iter().map(|l| l.est_size()).sum::(); Self { segment_id, max_page_len, layers, - est_size, } } } @@ -116,7 +113,6 @@ impl MemNodeSegment { old_head }) .collect::>(); - self.est_size = 0; // Reset estimated size after swapping out layers layers } @@ -148,10 +144,6 @@ impl MemNodeSegment { self.layers.iter().map(|seg| seg.lsn()).min().unwrap_or(0) } - pub fn est_size(&self) -> usize { - self.est_size - } - pub fn to_vid(&self, pos: LocalPOS) -> VID { pos.as_vid(self.segment_id, self.max_page_len) } @@ -197,7 +189,6 @@ impl MemNodeSegment { segment_id, max_page_len, layers: vec![SegmentContainer::new(segment_id, max_page_len, meta)], - est_size: 0, } } @@ -208,7 +199,7 @@ impl MemNodeSegment { dst: impl Into, e_id: impl Into, lsn: u64, - ) -> bool { + ) -> (bool, usize) { let dst = dst.into(); let e_id = e_id.into(); let layer_id = e_id.layer(); @@ -239,8 +230,7 @@ impl MemNodeSegment { let layer_est_size = self.layers[layer_id].est_size(); let added_size = (layer_est_size - est_size) + (is_new_edge * std::mem::size_of::<(VID, VID)>()); - self.est_size += added_size; - new_entry + (new_entry, added_size) } pub fn add_inbound_edge( @@ -250,7 +240,7 @@ impl MemNodeSegment { src: impl Into, e_id: impl Into, lsn: u64, - ) -> bool { + ) -> (bool, usize) { let src = src.into(); let e_id = e_id.into(); let layer_id = e_id.layer(); @@ -281,8 +271,7 @@ impl MemNodeSegment { let layer_est_size = self.layers[layer_id].est_size(); let added_size = (layer_est_size - est_size) + (is_new_edge * std::mem::size_of::<(VID, VID)>()); - self.est_size += added_size; - new_entry + (new_entry, added_size) } fn update_timestamp_inner(&mut self, t: T, row: usize, e_id: ELID) { @@ -294,7 +283,7 @@ impl MemNodeSegment { prop_mut_entry.addition_timestamp(ts, e_id); } - pub fn update_timestamp(&mut self, t: T, node_pos: LocalPOS, e_id: ELID) { + pub fn update_timestamp(&mut self, t: T, node_pos: LocalPOS, e_id: ELID) -> usize { let (row, est_size) = { let segment_container = &mut self.layers[e_id.layer()]; let est_size = segment_container.est_size(); @@ -306,7 +295,7 @@ impl MemNodeSegment { self.update_timestamp_inner(t, row, e_id); let layer_est_size = self.layers[e_id.layer()].est_size(); let added_size = layer_est_size - est_size; - self.est_size += added_size; + added_size } pub fn add_props( @@ -315,7 +304,7 @@ impl MemNodeSegment { node_pos: LocalPOS, layer_id: usize, props: impl IntoIterator, - ) -> bool { + ) -> (bool, usize) { let layer = self.get_or_create_layer(layer_id); let est_size = layer.est_size(); let row = layer.reserve_local_row(node_pos); @@ -325,8 +314,7 @@ impl MemNodeSegment { let ts = TimeIndexEntry::new(t.t(), t.i()); prop_mut_entry.append_t_props(ts, props); let layer_est_size = layer.est_size(); - self.est_size += layer_est_size - est_size; - is_new + (is_new, layer_est_size - est_size) } pub fn update_c_props( @@ -334,7 +322,7 @@ impl MemNodeSegment { node_pos: LocalPOS, layer_id: usize, props: impl IntoIterator, - ) -> bool { + ) -> (bool, usize) { let segment_container = &mut self.layers[layer_id]; let est_size = segment_container.est_size(); @@ -348,8 +336,7 @@ impl MemNodeSegment { let layer_est_size = segment_container.est_size(); let added_size = (layer_est_size - est_size) + 8; // random estimate for constant properties - self.est_size += added_size; - is_new + (is_new, added_size) } pub fn latest(&self) -> Option { @@ -374,6 +361,7 @@ pub struct NodeSegmentView { inner: Arc>, segment_id: usize, event_id: AtomicI64, + est_size: AtomicUsize, _ext: EXT, } @@ -454,6 +442,7 @@ impl>> NodeSegmentOps for NodeSegm segment_id: page_id, _ext, event_id: Default::default(), + est_size: AtomicUsize::new(0), } } @@ -526,6 +515,14 @@ impl>> NodeSegmentOps for NodeSegm } fn flush(&self) {} + + fn est_size(&self) -> usize { + self.est_size.load(Ordering::Relaxed) + } + + fn increment_est_size(&self, size: usize) -> usize { + self.est_size.fetch_add(size, Ordering::Relaxed) + } } #[cfg(test)] @@ -557,12 +554,12 @@ mod test { let mut writer = NodeWriter::new(&segment, &stats, segment.head_mut()); - let est_size1 = writer.mut_segment.est_size(); + let est_size1 = segment.est_size(); assert_eq!(est_size1, 0); writer.add_outbound_edge(Some(1), LocalPOS(1), VID(3), EID(7).with_layer(0), 0); - let est_size2 = writer.mut_segment.est_size(); + let est_size2 = segment.est_size(); assert!( est_size2 > est_size1, "Estimated size should be greater than 0 after adding an edge" @@ -570,7 +567,7 @@ mod test { writer.add_inbound_edge(Some(1), LocalPOS(2), VID(4), EID(8).with_layer(0), 0); - let est_size3 = writer.mut_segment.est_size(); + let est_size3 = segment.est_size(); assert!( est_size3 > est_size2, "Estimated size should increase after adding an inbound edge" @@ -579,7 +576,7 @@ mod test { // no change when adding the same edge again writer.add_outbound_edge::(None, LocalPOS(1), VID(3), EID(7).with_layer(0), 0); - let est_size4 = writer.mut_segment.est_size(); + let est_size4 = segment.est_size(); assert_eq!( est_size4, est_size3, "Estimated size should not change when adding the same edge again" @@ -595,7 +592,7 @@ mod test { writer.update_c_props(LocalPOS(1), 0, [(prop_id, Prop::U64(73))], 0); - let est_size5 = writer.mut_segment.est_size(); + let est_size5 = segment.est_size(); assert!( est_size5 > est_size4, "Estimated size should increase after adding constant properties" @@ -603,7 +600,7 @@ mod test { writer.update_timestamp(17, LocalPOS(1), ELID::new(EID(0), 0), 0); - let est_size6 = writer.mut_segment.est_size(); + let est_size6 = segment.est_size(); assert!( est_size6 > est_size5, "Estimated size should increase after updating timestamp" @@ -618,14 +615,14 @@ mod test { writer.add_props(42, LocalPOS(1), 0, [(prop_id, Prop::F64(4.13))], 0); - let est_size7 = writer.mut_segment.est_size(); + let est_size7 = segment.est_size(); assert!( est_size7 > est_size6, "Estimated size should increase after adding temporal properties" ); writer.add_props(72, LocalPOS(1), 0, [(prop_id, Prop::F64(5.41))], 0); - let est_size8 = writer.mut_segment.est_size(); + let est_size8 = segment.est_size(); assert!( est_size8 > est_size7, "Estimated size should increase after adding another temporal property" diff --git a/raphtory/examples/load_graph.rs b/raphtory/examples/load_graph.rs index 7c983a589b..68f3dfc9af 100644 --- a/raphtory/examples/load_graph.rs +++ b/raphtory/examples/load_graph.rs @@ -1,9 +1,6 @@ use std::path::PathBuf; -use raphtory::{ - io::parquet_loaders::load_edges_from_parquet, - prelude::{Graph, GraphViewOps}, -}; +use raphtory::{io::parquet_loaders::load_edges_from_parquet, prelude::*}; use raphtory_storage::core_ops::CoreGraphOps; fn main() { @@ -12,6 +9,8 @@ fn main() { "dai_ava_edge_list", "usp_ava_edge_list", "usdc_e_ava_edge_list", + "usdc_ava_edge_list", + "usdt_ava_edge_list", ]; let parquet_root = "/Volumes/Work/tether/avalance_table"; @@ -54,5 +53,16 @@ fn main() { g.unfiltered_num_nodes(), g.unique_layers().collect::>() ); + + let mut all_edges_count = 0; + let mut all_nodes_count = 0; + + for n in g.nodes() { + all_nodes_count += 1usize; + for _ in n.out_edges() { + all_edges_count += 1usize; + } + } + println!("Total edges in graph: {all_edges_count}, total nodes: {all_nodes_count}"); } } diff --git a/raphtory/src/io/arrow/df_loaders.rs b/raphtory/src/io/arrow/df_loaders.rs index ce5dcff355..43376d2000 100644 --- a/raphtory/src/io/arrow/df_loaders.rs +++ b/raphtory/src/io/arrow/df_loaders.rs @@ -975,6 +975,7 @@ mod tests { use itertools::Itertools; use polars_arrow::array::{MutableArray, MutablePrimitiveArray, MutableUtf8Array}; use proptest::proptest; + use raphtory_storage::core_ops::CoreGraphOps; use tempfile::TempDir; #[cfg(feature = "storage")] From b8a51e6cfb9a45ddc480424f3fcaac94eccdeecd Mon Sep 17 00:00:00 2001 From: Fadhil Abubaker Date: Fri, 1 Aug 2025 14:55:14 -0400 Subject: [PATCH 092/321] Enable wal logging for `add_edge` (#2178) --- db4-graph/src/lib.rs | 60 ++++- db4-storage/Cargo.toml | 1 - db4-storage/src/lib.rs | 2 + db4-storage/src/pages/edge_store.rs | 20 +- .../src/properties/props_meta_writer.rs | 99 +++++++-- db4-storage/src/wal/entries.rs | 124 ----------- db4-storage/src/wal/entry.rs | 108 +++++++++ db4-storage/src/wal/mod.rs | 209 ++++++++++++++++-- db4-storage/src/wal/no_wal.rs | 13 +- raphtory-storage/src/mutation/addition_ops.rs | 57 ++++- .../src/mutation/addition_ops_ext.rs | 42 +++- raphtory/src/db/api/mutation/addition_ops.rs | 95 +++++++- raphtory/src/db/api/storage/storage.rs | 23 +- raphtory/src/db/mod.rs | 1 + raphtory/src/db/replay/mod.rs | 116 ++++++++++ 15 files changed, 773 insertions(+), 197 deletions(-) delete mode 100644 db4-storage/src/wal/entries.rs create mode 100644 db4-storage/src/wal/entry.rs create mode 100644 raphtory/src/db/replay/mod.rs diff --git a/db4-graph/src/lib.rs b/db4-graph/src/lib.rs index 3dde06ec75..617d388503 100644 --- a/db4-graph/src/lib.rs +++ b/db4-graph/src/lib.rs @@ -1,6 +1,9 @@ use std::{ path::{Path, PathBuf}, - sync::{atomic::AtomicUsize, Arc}, + sync::{ + atomic::{self, AtomicU64, AtomicUsize}, + Arc, + }, }; use raphtory_api::core::{ @@ -21,7 +24,8 @@ use storage::{ }, persist::strategy::PersistentStrategy, resolver::GIDResolverOps, - Extension, GIDResolver, Layer, ReadLockedLayer, ES, NS, + wal::{GraphWal, TransactionID, Wal}, + Extension, GIDResolver, Layer, ReadLockedLayer, WalImpl, ES, NS, }; use tempfile::TempDir; @@ -39,6 +43,8 @@ pub struct TemporalGraph { storage: Arc>, pub graph_meta: Arc, graph_dir: GraphDir, + pub transaction_manager: Arc, + pub wal: Arc, } #[derive(Debug)] @@ -68,6 +74,40 @@ impl AsRef for GraphDir { } } +#[derive(Debug)] +pub struct TransactionManager { + last_transaction_id: AtomicU64, + wal: Arc, +} + +impl TransactionManager { + const STARTING_TRANSACTION_ID: TransactionID = 1; + + pub fn new(wal: Arc) -> Self { + Self { + last_transaction_id: AtomicU64::new(Self::STARTING_TRANSACTION_ID), + wal, + } + } + + pub fn load(self, last_transaction_id: TransactionID) { + self.last_transaction_id + .store(last_transaction_id, atomic::Ordering::SeqCst) + } + + pub fn begin_transaction(&self) -> TransactionID { + let transaction_id = self + .last_transaction_id + .fetch_add(1, atomic::Ordering::SeqCst); + self.wal.log_begin_transaction(transaction_id).unwrap(); + transaction_id + } + + pub fn end_transaction(&self, transaction_id: TransactionID) { + self.wal.log_end_transaction(transaction_id).unwrap(); + } +} + impl Default for TemporalGraph { fn default() -> Self { Self::new() @@ -78,7 +118,7 @@ impl, ES = ES>> TemporalGraph { pub fn new() -> Self { let node_meta = Meta::new(); let edge_meta = Meta::new(); - Self::new_with_meta(Default::default(), node_meta, edge_meta) + Self::new_with_meta(GraphDir::default(), node_meta, edge_meta) } pub fn new_with_path(path: impl AsRef) -> Self { @@ -91,17 +131,24 @@ impl, ES = ES>> TemporalGraph { let graph_dir: GraphDir = path.as_ref().into(); let storage = Layer::load(graph_dir.as_ref()) .unwrap_or_else(|_| panic!("Failed to load graph from path: {graph_dir:?}")); + let gid_resolver_dir = graph_dir.as_ref().join("gid_resolver"); let resolver = GIDResolver::new(&gid_resolver_dir).unwrap_or_else(|err| { panic!("Failed to load GID resolver from path: {gid_resolver_dir:?} {err}") }); + let node_count = AtomicUsize::new(storage.nodes().num_nodes()); + let wal_dir = graph_dir.as_ref().join("wal"); + let wal = Arc::new(WalImpl::new(&wal_dir).unwrap()); + Self { graph_dir, logical_to_physical: resolver.into(), node_count, storage: Arc::new(storage), graph_meta: Arc::new(GraphMeta::default()), + transaction_manager: Arc::new(TransactionManager::new(wal.clone())), + wal, } } @@ -110,6 +157,7 @@ impl, ES = ES>> TemporalGraph { node_meta.get_or_create_layer_id(Some("staticgraph")); std::fs::create_dir_all(&graph_dir) .unwrap_or_else(|_| panic!("Failed to create graph directory at {graph_dir:?}")); + let gid_resolver_dir = graph_dir.as_ref().join("gid_resolver"); let storage: Layer = Layer::new_with_meta( graph_dir.as_ref(), @@ -118,12 +166,18 @@ impl, ES = ES>> TemporalGraph { node_meta, edge_meta, ); + + let wal_dir = graph_dir.as_ref().join("wal"); + let wal = Arc::new(WalImpl::new(&wal_dir).unwrap()); + Self { graph_dir, logical_to_physical: GIDResolver::new(gid_resolver_dir).unwrap().into(), node_count: AtomicUsize::new(0), storage: Arc::new(storage), graph_meta: Arc::new(GraphMeta::default()), + transaction_manager: Arc::new(TransactionManager::new(wal.clone())), + wal, } } diff --git a/db4-storage/Cargo.toml b/db4-storage/Cargo.toml index 0852a3ea92..ea58184cf2 100644 --- a/db4-storage/Cargo.toml +++ b/db4-storage/Cargo.toml @@ -33,7 +33,6 @@ bytemuck.workspace = true rayon.workspace = true itertools.workspace = true thiserror.workspace = true -postcard.workspace = true proptest = { workspace = true, optional = true } tempfile = { workspace = true, optional = true } diff --git a/db4-storage/src/lib.rs b/db4-storage/src/lib.rs index 0c201fc797..f8675178f7 100644 --- a/db4-storage/src/lib.rs +++ b/db4-storage/src/lib.rs @@ -17,6 +17,7 @@ use crate::{ node::NodeSegmentView, node_entry::{MemNodeEntry, MemNodeRef}, }, + wal::no_wal::NoWal, }; use raphtory_api::core::entities::{EID, VID}; use segments::{edge::MemEdgeSegment, node::MemNodeSegment}; @@ -37,6 +38,7 @@ pub type NS

= NodeSegmentView

; pub type ES

= EdgeSegmentView

; pub type Layer

= GraphStore, ES

, P>; +pub type WalImpl = NoWal; pub type GIDResolver = MappingResolver; pub type ReadLockedLayer

= ReadLockedGraphStore, ES

{ self.id } - pub fn history(&self) -> BoxedLIter { + pub fn history(&self) -> BoxedLIter<'_, i64> { self.props.temporal_history_iter(self.id) } - pub fn history_rev(&self) -> BoxedLIter { + pub fn history_rev(&self) -> BoxedLIter<'_, i64> { self.props.temporal_history_iter_rev(self.id) } pub fn history_date_time(&self) -> Option>> { self.props.temporal_history_date_time(self.id) } - pub fn values(&self) -> BoxedLIter { + pub fn values(&self) -> BoxedLIter<'_, Prop> { self.props.temporal_values_iter(self.id) } diff --git a/raphtory/src/db/api/state/lazy_node_state.rs b/raphtory/src/db/api/state/lazy_node_state.rs index e195e73f6d..9ab1632ec8 100644 --- a/raphtory/src/db/api/state/lazy_node_state.rs +++ b/raphtory/src/db/api/state/lazy_node_state.rs @@ -271,7 +271,10 @@ impl<'graph, Op: NodeOp + 'graph, G: GraphViewOps<'graph>, GH: GraphViewOps<'gra fn get_by_index( &self, index: usize, - ) -> Option<(NodeView<&Self::BaseGraph, &Self::Graph>, Self::Value<'_>)> { + ) -> Option<( + NodeView<'_, &Self::BaseGraph, &Self::Graph>, + Self::Value<'_>, + )> { if self.graph().filtered() { self.iter().nth(index) } else { diff --git a/raphtory/src/db/api/state/node_state.rs b/raphtory/src/db/api/state/node_state.rs index 55e3ada4e3..c399f12913 100644 --- a/raphtory/src/db/api/state/node_state.rs +++ b/raphtory/src/db/api/state/node_state.rs @@ -444,7 +444,10 @@ impl< fn get_by_index( &self, index: usize, - ) -> Option<(NodeView<&Self::BaseGraph, &Self::Graph>, Self::Value<'_>)> { + ) -> Option<( + NodeView<'_, &Self::BaseGraph, &Self::Graph>, + Self::Value<'_>, + )> { match &self.keys { Some(node_index) => node_index.key(index).map(|n| { ( diff --git a/raphtory/src/db/api/state/node_state_ops.rs b/raphtory/src/db/api/state/node_state_ops.rs index 22e52a7ea2..cf453aad27 100644 --- a/raphtory/src/db/api/state/node_state_ops.rs +++ b/raphtory/src/db/api/state/node_state_ops.rs @@ -72,7 +72,10 @@ pub trait NodeStateOps<'graph>: fn get_by_index( &self, index: usize, - ) -> Option<(NodeView<&Self::BaseGraph, &Self::Graph>, Self::Value<'_>)>; + ) -> Option<( + NodeView<'_, &Self::BaseGraph, &Self::Graph>, + Self::Value<'_>, + )>; fn get_by_node(&self, node: N) -> Option>; @@ -183,7 +186,10 @@ pub trait NodeStateOps<'graph>: fn min_item_by std::cmp::Ordering + Sync>( &self, cmp: F, - ) -> Option<(NodeView<&Self::BaseGraph, &Self::Graph>, Self::Value<'_>)> { + ) -> Option<( + NodeView<'_, &Self::BaseGraph, &Self::Graph>, + Self::Value<'_>, + )> { self.par_iter() .min_by(|(_, v1), (_, v2)| cmp(v1.borrow(), v2.borrow())) } @@ -191,7 +197,10 @@ pub trait NodeStateOps<'graph>: fn max_item_by std::cmp::Ordering + Sync>( &self, cmp: F, - ) -> Option<(NodeView<&Self::BaseGraph, &Self::Graph>, Self::Value<'_>)> { + ) -> Option<( + NodeView<'_, &Self::BaseGraph, &Self::Graph>, + Self::Value<'_>, + )> { self.par_iter() .max_by(|(_, v1), (_, v2)| cmp(v1.borrow(), v2.borrow())) } @@ -199,7 +208,10 @@ pub trait NodeStateOps<'graph>: fn median_item_by std::cmp::Ordering + Sync>( &self, cmp: F, - ) -> Option<(NodeView<&Self::BaseGraph, &Self::Graph>, Self::Value<'_>)> { + ) -> Option<( + NodeView<'_, &Self::BaseGraph, &Self::Graph>, + Self::Value<'_>, + )> { let mut values: Vec<_> = self.par_iter().collect(); let len = values.len(); if len == 0 { diff --git a/raphtory/src/db/api/state/node_state_ord_ops.rs b/raphtory/src/db/api/state/node_state_ord_ops.rs index 24c2dab52a..c73772e5af 100644 --- a/raphtory/src/db/api/state/node_state_ord_ops.rs +++ b/raphtory/src/db/api/state/node_state_ord_ops.rs @@ -85,21 +85,36 @@ where ) -> NodeState<'graph, Self::OwnedValue, Self::BaseGraph, Self::Graph>; /// Returns a tuple of the min result with its key - fn min_item(&self) -> Option<(NodeView<&Self::BaseGraph, &Self::Graph>, Self::Value<'_>)>; + fn min_item( + &self, + ) -> Option<( + NodeView<'_, &Self::BaseGraph, &Self::Graph>, + Self::Value<'_>, + )>; fn min(&self) -> Option> { self.min_item().map(|(_, v)| v) } /// Returns a tuple of the max result with its key - fn max_item(&self) -> Option<(NodeView<&Self::BaseGraph, &Self::Graph>, Self::Value<'_>)>; + fn max_item( + &self, + ) -> Option<( + NodeView<'_, &Self::BaseGraph, &Self::Graph>, + Self::Value<'_>, + )>; fn max(&self) -> Option> { self.max_item().map(|(_, v)| v) } /// Returns a tuple of the median result with its key - fn median_item(&self) -> Option<(NodeView<&Self::BaseGraph, &Self::Graph>, Self::Value<'_>)>; + fn median_item( + &self, + ) -> Option<( + NodeView<'_, &Self::BaseGraph, &Self::Graph>, + Self::Value<'_>, + )>; fn median(&self) -> Option> { self.median_item().map(|(_, v)| v) @@ -143,21 +158,36 @@ pub trait AsOrderedNodeStateOps<'graph>: NodeStateOps<'graph> { ) -> NodeState<'graph, Self::OwnedValue, Self::BaseGraph, Self::Graph>; /// Returns a tuple of the min result with its key - fn min_item(&self) -> Option<(NodeView<&Self::BaseGraph, &Self::Graph>, Self::Value<'_>)>; + fn min_item( + &self, + ) -> Option<( + NodeView<'_, &Self::BaseGraph, &Self::Graph>, + Self::Value<'_>, + )>; fn min(&self) -> Option> { self.min_item().map(|(_, v)| v) } /// Returns a tuple of the max result with its key - fn max_item(&self) -> Option<(NodeView<&Self::BaseGraph, &Self::Graph>, Self::Value<'_>)>; + fn max_item( + &self, + ) -> Option<( + NodeView<'_, &Self::BaseGraph, &Self::Graph>, + Self::Value<'_>, + )>; fn max(&self) -> Option> { self.max_item().map(|(_, v)| v) } /// Returns a tuple of the median result with its key - fn median_item(&self) -> Option<(NodeView<&Self::BaseGraph, &Self::Graph>, Self::Value<'_>)>; + fn median_item( + &self, + ) -> Option<( + NodeView<'_, &Self::BaseGraph, &Self::Graph>, + Self::Value<'_>, + )>; fn median(&self) -> Option> { self.median_item().map(|(_, v)| v) @@ -190,15 +220,30 @@ where self.bottom_k_by(Ord::cmp, k) } - fn min_item(&self) -> Option<(NodeView<&Self::BaseGraph, &Self::Graph>, Self::Value<'_>)> { + fn min_item( + &self, + ) -> Option<( + NodeView<'_, &Self::BaseGraph, &Self::Graph>, + Self::Value<'_>, + )> { self.min_item_by(Ord::cmp) } - fn max_item(&self) -> Option<(NodeView<&Self::BaseGraph, &Self::Graph>, Self::Value<'_>)> { + fn max_item( + &self, + ) -> Option<( + NodeView<'_, &Self::BaseGraph, &Self::Graph>, + Self::Value<'_>, + )> { self.max_item_by(Ord::cmp) } - fn median_item(&self) -> Option<(NodeView<&Self::BaseGraph, &Self::Graph>, Self::Value<'_>)> { + fn median_item( + &self, + ) -> Option<( + NodeView<'_, &Self::BaseGraph, &Self::Graph>, + Self::Value<'_>, + )> { self.median_item_by(Ord::cmp) } } @@ -229,15 +274,30 @@ where self.bottom_k_by(|a, b| a.as_ord().cmp(b.as_ord()), k) } - fn min_item(&self) -> Option<(NodeView<&Self::BaseGraph, &Self::Graph>, Self::Value<'_>)> { + fn min_item( + &self, + ) -> Option<( + NodeView<'_, &Self::BaseGraph, &Self::Graph>, + Self::Value<'_>, + )> { self.min_item_by(|a, b| a.as_ord().cmp(b.as_ord())) } - fn max_item(&self) -> Option<(NodeView<&Self::BaseGraph, &Self::Graph>, Self::Value<'_>)> { + fn max_item( + &self, + ) -> Option<( + NodeView<'_, &Self::BaseGraph, &Self::Graph>, + Self::Value<'_>, + )> { self.max_item_by(|a, b| a.as_ord().cmp(b.as_ord())) } - fn median_item(&self) -> Option<(NodeView<&Self::BaseGraph, &Self::Graph>, Self::Value<'_>)> { + fn median_item( + &self, + ) -> Option<( + NodeView<'_, &Self::BaseGraph, &Self::Graph>, + Self::Value<'_>, + )> { self.median_item_by(|a, b| a.as_ord().cmp(b.as_ord())) } } diff --git a/raphtory/src/db/api/storage/graph/storage_ops/metadata.rs b/raphtory/src/db/api/storage/graph/storage_ops/metadata.rs index e5c252f5be..1d89b108a1 100644 --- a/raphtory/src/db/api/storage/graph/storage_ops/metadata.rs +++ b/raphtory/src/db/api/storage/graph/storage_ops/metadata.rs @@ -16,7 +16,7 @@ impl InternalMetadataOps for GraphStorage { self.graph_meta().get_metadata_name(id) } - fn metadata_ids(&self) -> BoxedLIter { + fn metadata_ids(&self) -> BoxedLIter<'_, usize> { Box::new(self.graph_meta().metadata_ids()) } @@ -24,7 +24,7 @@ impl InternalMetadataOps for GraphStorage { self.graph_meta().get_metadata(id) } - fn metadata_keys(&self) -> BoxedLIter { + fn metadata_keys(&self) -> BoxedLIter<'_, ArcStr> { Box::new(self.graph_meta().metadata_names().into_iter()) } } diff --git a/raphtory/src/db/api/storage/graph/storage_ops/time_props.rs b/raphtory/src/db/api/storage/graph/storage_ops/time_props.rs index a5882be452..e16f37d7c0 100644 --- a/raphtory/src/db/api/storage/graph/storage_ops/time_props.rs +++ b/raphtory/src/db/api/storage/graph/storage_ops/time_props.rs @@ -22,7 +22,7 @@ impl InternalTemporalPropertyViewOps for GraphStorage { self.graph_meta().get_temporal_dtype(id).unwrap() } - fn temporal_iter(&self, id: usize) -> BoxedLIter<(TimeIndexEntry, Prop)> { + fn temporal_iter(&self, id: usize) -> BoxedLIter<'_, (TimeIndexEntry, Prop)> { self.graph_meta() .get_temporal_prop(id) .into_iter() @@ -30,7 +30,7 @@ impl InternalTemporalPropertyViewOps for GraphStorage { .into_dyn_boxed() } - fn temporal_iter_rev(&self, id: usize) -> BoxedLIter<(TimeIndexEntry, Prop)> { + fn temporal_iter_rev(&self, id: usize) -> BoxedLIter<'_, (TimeIndexEntry, Prop)> { self.graph_meta() .get_temporal_prop(id) .into_iter() @@ -68,11 +68,11 @@ impl InternalTemporalPropertiesOps for GraphStorage { self.graph_meta().get_temporal_name(id) } - fn temporal_prop_ids(&self) -> BoxedLIter { + fn temporal_prop_ids(&self) -> BoxedLIter<'_, usize> { Box::new(self.graph_meta().temporal_ids()) } - fn temporal_prop_keys(&self) -> BoxedLIter { + fn temporal_prop_keys(&self) -> BoxedLIter<'_, ArcStr> { Box::new(self.graph_meta().temporal_names().into_iter()) } } diff --git a/raphtory/src/db/api/storage/graph/storage_ops/time_semantics.rs b/raphtory/src/db/api/storage/graph/storage_ops/time_semantics.rs index 4cf9f3c470..98c1481129 100644 --- a/raphtory/src/db/api/storage/graph/storage_ops/time_semantics.rs +++ b/raphtory/src/db/api/storage/graph/storage_ops/time_semantics.rs @@ -98,7 +98,7 @@ impl GraphTimeSemanticsOps for GraphStorage { self.graph_meta().temporal_mapper().has_id(prop_id) } - fn temporal_prop_iter(&self, prop_id: usize) -> BoxedLIter<(TimeIndexEntry, Prop)> { + fn temporal_prop_iter(&self, prop_id: usize) -> BoxedLIter<'_, (TimeIndexEntry, Prop)> { self.graph_meta() .get_temporal_prop(prop_id) .into_iter() @@ -120,7 +120,7 @@ impl GraphTimeSemanticsOps for GraphStorage { prop_id: usize, start: i64, end: i64, - ) -> BoxedLIter<(TimeIndexEntry, Prop)> { + ) -> BoxedLIter<'_, (TimeIndexEntry, Prop)> { self.graph_meta() .get_temporal_prop(prop_id) .into_iter() @@ -139,7 +139,7 @@ impl GraphTimeSemanticsOps for GraphStorage { prop_id: usize, start: i64, end: i64, - ) -> BoxedLIter<(TimeIndexEntry, Prop)> { + ) -> BoxedLIter<'_, (TimeIndexEntry, Prop)> { self.graph_meta() .get_temporal_prop(prop_id) .into_iter() diff --git a/raphtory/src/db/api/storage/storage.rs b/raphtory/src/db/api/storage/storage.rs index 9c4c3c5e96..0fd59d04b3 100644 --- a/raphtory/src/db/api/storage/storage.rs +++ b/raphtory/src/db/api/storage/storage.rs @@ -7,7 +7,6 @@ use crate::{ errors::GraphError, }; use db4_graph::{TemporalGraph, TransactionManager, WriteLockedGraph}; -use either::Either; use raphtory_api::core::{ entities::{ properties::{ @@ -38,19 +37,17 @@ use std::{ }; use storage::{Extension, WalImpl}; -#[cfg(feature = "proto")] -use crate::serialise::GraphFolder; - -use raphtory_core::entities::nodes::node_ref::AsNodeRef; -use raphtory_storage::{core_ops::CoreGraphOps, graph::nodes::node_storage_ops::NodeStorageOps}; #[cfg(feature = "search")] use { crate::{ db::api::view::IndexSpec, search::graph_index::{GraphIndex, MutableGraphIndex}, + serialise::GraphFolder, }, - once_cell::sync::OnceCell, + either::Either, parking_lot::RwLock, + raphtory_core::entities::nodes::node_ref::AsNodeRef, + raphtory_storage::{core_ops::CoreGraphOps, graph::nodes::node_storage_ops::NodeStorageOps}, std::ops::{Deref, DerefMut}, tracing::info, }; @@ -278,6 +275,15 @@ pub struct AtomicAddEdgeSession<'a> { } impl EdgeWriteLock for AtomicAddEdgeSession<'_> { + fn internal_add_static_edge( + &mut self, + src: impl Into, + dst: impl Into, + lsn: u64, + ) -> MaybeNew { + self.session.internal_add_static_edge(src, dst, lsn) + } + fn internal_add_edge( &mut self, t: TimeIndexEntry, @@ -291,15 +297,6 @@ impl EdgeWriteLock for AtomicAddEdgeSession<'_> { .internal_add_edge(t, src, dst, e_id, lsn, props) } - fn internal_add_static_edge( - &mut self, - src: impl Into, - dst: impl Into, - lsn: u64, - ) -> MaybeNew { - self.session.internal_add_static_edge(src, dst, lsn) - } - fn internal_delete_edge( &mut self, t: TimeIndexEntry, @@ -436,7 +433,7 @@ impl InternalAdditionOps for Storage { type WS<'a> = StorageWriteSession<'a>; type AtomicAddEdge<'a> = AtomicAddEdgeSession<'a>; - fn write_lock(&self) -> Result, Self::Error> { + fn write_lock(&self) -> Result, Self::Error> { Ok(self.graph.write_lock()?) } @@ -589,7 +586,7 @@ impl InternalPropertyAdditionOps for Storage { &self, vid: VID, props: &[(usize, Prop)], - ) -> Result { + ) -> Result, Self::Error> { let lock = self.graph.internal_add_node_metadata(vid, props)?; #[cfg(feature = "search")] @@ -602,7 +599,7 @@ impl InternalPropertyAdditionOps for Storage { &self, vid: VID, props: &[(usize, Prop)], - ) -> Result { + ) -> Result, Self::Error> { let lock = self.graph.internal_update_node_metadata(vid, props)?; #[cfg(feature = "search")] @@ -616,7 +613,7 @@ impl InternalPropertyAdditionOps for Storage { eid: EID, layer: usize, props: &[(usize, Prop)], - ) -> Result { + ) -> Result, Self::Error> { let lock = self.graph.internal_add_edge_metadata(eid, layer, props)?; #[cfg(feature = "search")] @@ -630,7 +627,7 @@ impl InternalPropertyAdditionOps for Storage { eid: EID, layer: usize, props: &[(usize, Prop)], - ) -> Result { + ) -> Result, Self::Error> { let lock = self .graph .internal_update_edge_metadata(eid, layer, props)?; diff --git a/raphtory/src/db/api/view/internal/materialize.rs b/raphtory/src/db/api/view/internal/materialize.rs index e0625ff4b9..cbcf4b9710 100644 --- a/raphtory/src/db/api/view/internal/materialize.rs +++ b/raphtory/src/db/api/view/internal/materialize.rs @@ -9,10 +9,7 @@ use crate::{ }, prelude::*, }; -use raphtory_api::{ - iter::{BoxedLDIter, BoxedLIter}, - GraphType, -}; +use raphtory_api::{iter::BoxedLIter, GraphType}; use raphtory_storage::{graph::graph::GraphStorage, mutation::InheritMutationOps}; use std::ops::Range; @@ -141,7 +138,7 @@ impl GraphTimeSemanticsOps for MaterializedGraph { for_all!(self, g => g.has_temporal_prop(prop_id)) } - fn temporal_prop_iter(&self, prop_id: usize) -> BoxedLIter<(TimeIndexEntry, Prop)> { + fn temporal_prop_iter(&self, prop_id: usize) -> BoxedLIter<'_, (TimeIndexEntry, Prop)> { for_all!(self, g => g.temporal_prop_iter(prop_id)) } @@ -154,7 +151,7 @@ impl GraphTimeSemanticsOps for MaterializedGraph { prop_id: usize, start: i64, end: i64, - ) -> BoxedLIter<(TimeIndexEntry, Prop)> { + ) -> BoxedLIter<'_, (TimeIndexEntry, Prop)> { for_all!(self, g => g.temporal_prop_iter_window(prop_id, start, end)) } @@ -163,7 +160,7 @@ impl GraphTimeSemanticsOps for MaterializedGraph { prop_id: usize, start: i64, end: i64, - ) -> BoxedLIter<(TimeIndexEntry, Prop)> { + ) -> BoxedLIter<'_, (TimeIndexEntry, Prop)> { for_all!(self, g => g.temporal_prop_iter_window_rev(prop_id, start, end)) } diff --git a/raphtory/src/db/api/view/internal/time_semantics/event_semantics.rs b/raphtory/src/db/api/view/internal/time_semantics/event_semantics.rs index 9bf065c41f..a9fb689ae9 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/event_semantics.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/event_semantics.rs @@ -1,4 +1,3 @@ -use std::iter::Peekable; use crate::db::api::view::internal::{ time_semantics::{ filtered_edge::FilteredEdgeStorageOps, filtered_node::FilteredNodeStorageOps, @@ -16,7 +15,7 @@ use raphtory_api::core::{ storage::timeindex::{AsTime, TimeIndexEntry, TimeIndexOps}, }; use raphtory_storage::graph::{ - edges::{edge_storage_ops::EdgeStorageOps}, + edges::edge_storage_ops::EdgeStorageOps, nodes::{node_ref::NodeStorageRef, node_storage_ops::NodeStorageOps}, }; use std::ops::Range; @@ -780,4 +779,4 @@ impl EdgeTimeSemanticsOps for EventSemantics { }; e.filtered_edge_metadata(&view, prop_id, layer_filter) } -} \ No newline at end of file +} diff --git a/raphtory/src/db/api/view/internal/time_semantics/filtered_node.rs b/raphtory/src/db/api/view/internal/time_semantics/filtered_node.rs index 052d8336be..54cf517dff 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/filtered_node.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/filtered_node.rs @@ -11,10 +11,7 @@ use raphtory_api::core::{ storage::timeindex::{TimeIndexEntry, TimeIndexOps}, Direction, }; -use raphtory_storage::{ - core_ops::CoreGraphOps, - graph::{edges::edge_storage_ops::EdgeStorageOps, nodes::node_storage_ops::NodeStorageOps}, -}; +use raphtory_storage::{core_ops::CoreGraphOps, graph::nodes::node_storage_ops::NodeStorageOps}; use std::ops::Range; use storage::gen_ts::ALL_LAYERS; diff --git a/raphtory/src/db/api/view/internal/time_semantics/mod.rs b/raphtory/src/db/api/view/internal/time_semantics/mod.rs index 7b190b80fc..9b66c9b025 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/mod.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/mod.rs @@ -59,7 +59,7 @@ pub trait GraphTimeSemanticsOps { /// A vector of tuples representing the temporal values of the property /// that fall within the specified time window, where the first element of each tuple is the timestamp /// and the second element is the property value. - fn temporal_prop_iter(&self, prop_id: usize) -> BoxedLIter<(TimeIndexEntry, Prop)>; + fn temporal_prop_iter(&self, prop_id: usize) -> BoxedLIter<'_, (TimeIndexEntry, Prop)>; /// Check if graph has temporal property with the given id in the window /// /// # Arguments @@ -87,7 +87,7 @@ pub trait GraphTimeSemanticsOps { prop_id: usize, start: i64, end: i64, - ) -> BoxedLIter<(TimeIndexEntry, Prop)>; + ) -> BoxedLIter<'_, (TimeIndexEntry, Prop)>; /// Returns all temporal values of the graph property with the given name /// that fall within the specified time window in reverse order. @@ -108,7 +108,7 @@ pub trait GraphTimeSemanticsOps { prop_id: usize, start: i64, end: i64, - ) -> BoxedLIter<(TimeIndexEntry, Prop)>; + ) -> BoxedLIter<'_, (TimeIndexEntry, Prop)>; /// Returns the value and update time for the temporal graph property at or before a given timestamp fn temporal_prop_last_at( @@ -186,7 +186,7 @@ impl GraphTimeSemanticsOps for G { } #[inline] - fn temporal_prop_iter(&self, prop_id: usize) -> BoxedLIter<(TimeIndexEntry, Prop)> { + fn temporal_prop_iter(&self, prop_id: usize) -> BoxedLIter<'_, (TimeIndexEntry, Prop)> { self.graph().temporal_prop_iter(prop_id) } @@ -201,7 +201,7 @@ impl GraphTimeSemanticsOps for G { prop_id: usize, start: i64, end: i64, - ) -> BoxedLIter<(TimeIndexEntry, Prop)> { + ) -> BoxedLIter<'_, (TimeIndexEntry, Prop)> { self.graph().temporal_prop_iter_window(prop_id, start, end) } @@ -211,7 +211,7 @@ impl GraphTimeSemanticsOps for G { prop_id: usize, start: i64, end: i64, - ) -> BoxedLIter<(TimeIndexEntry, Prop)> { + ) -> BoxedLIter<'_, (TimeIndexEntry, Prop)> { self.graph() .temporal_prop_iter_window_rev(prop_id, start, end) } diff --git a/raphtory/src/db/graph/edge.rs b/raphtory/src/db/graph/edge.rs index de56020447..ef31e2a185 100644 --- a/raphtory/src/db/graph/edge.rs +++ b/raphtory/src/db/graph/edge.rs @@ -129,7 +129,7 @@ impl EdgeView } } - pub fn deletions_hist(&self) -> BoxedLIter<(TimeIndexEntry, usize)> { + pub fn deletions_hist(&self) -> BoxedLIter<'_, (TimeIndexEntry, usize)> { let g = &self.graph; let e = self.edge; if edge_valid_layer(g, e) { @@ -454,7 +454,7 @@ impl<'graph, G: GraphViewOps<'graph>, GH: GraphViewOps<'graph>> InternalMetadata .clone() } - fn metadata_ids(&self) -> BoxedLIter { + fn metadata_ids(&self) -> BoxedLIter<'_, usize> { self.graph .edge_meta() .metadata_mapper() @@ -462,7 +462,7 @@ impl<'graph, G: GraphViewOps<'graph>, GH: GraphViewOps<'graph>> InternalMetadata .into_dyn_boxed() } - fn metadata_keys(&self) -> BoxedLIter { + fn metadata_keys(&self) -> BoxedLIter<'_, ArcStr> { self.graph .edge_meta() .metadata_mapper() @@ -543,7 +543,7 @@ impl InternalTemporal } } - fn temporal_iter(&self, id: usize) -> BoxedLIter<(TimeIndexEntry, Prop)> { + fn temporal_iter(&self, id: usize) -> BoxedLIter<'_, (TimeIndexEntry, Prop)> { if edge_valid_layer(&self.graph, self.edge) { let time_semantics = self.graph.edge_time_semantics(); let edge = self.graph.core_edge(self.edge.pid()); @@ -582,7 +582,7 @@ impl InternalTemporal } } - fn temporal_iter_rev(&self, id: usize) -> BoxedLIter<(TimeIndexEntry, Prop)> { + fn temporal_iter_rev(&self, id: usize) -> BoxedLIter<'_, (TimeIndexEntry, Prop)> { if edge_valid_layer(&self.graph, self.edge) { let time_semantics = self.graph.edge_time_semantics(); let edge = self.graph.core_edge(self.edge.pid()); @@ -679,7 +679,7 @@ impl<'graph, G: GraphViewOps<'graph>, GH: GraphViewOps<'graph>> InternalTemporal .clone() } - fn temporal_prop_ids(&self) -> BoxedLIter { + fn temporal_prop_ids(&self) -> BoxedLIter<'_, usize> { self.graph .edge_meta() .temporal_prop_mapper() @@ -687,7 +687,7 @@ impl<'graph, G: GraphViewOps<'graph>, GH: GraphViewOps<'graph>> InternalTemporal .into_dyn_boxed() } - fn temporal_prop_keys(&self) -> BoxedLIter { + fn temporal_prop_keys(&self) -> BoxedLIter<'_, ArcStr> { self.graph .edge_meta() .temporal_prop_mapper() diff --git a/raphtory/src/db/graph/node.rs b/raphtory/src/db/graph/node.rs index be9dcbef9e..eb72bfa8fa 100644 --- a/raphtory/src/db/graph/node.rs +++ b/raphtory/src/db/graph/node.rs @@ -104,7 +104,7 @@ impl<'a, G: IntoDynamic, GH: IntoDynamic> NodeView<'a, G, GH> { } impl<'graph, G: Send + Sync, GH: Send + Sync> AsNodeRef for NodeView<'graph, G, GH> { - fn as_node_ref(&self) -> NodeRef { + fn as_node_ref(&self) -> NodeRef<'_> { NodeRef::Internal(self.node) } } @@ -247,7 +247,7 @@ impl<'graph, G: GraphView, GH: GraphView> InternalTemporalPropertiesOps .clone() } - fn temporal_prop_ids(&self) -> BoxedLIter { + fn temporal_prop_ids(&self) -> BoxedLIter<'_, usize> { self.graph .node_meta() .temporal_prop_mapper() @@ -277,7 +277,7 @@ impl<'graph, G, GH: GraphViewOps<'graph>> InternalTemporalPropertyViewOps res } - fn temporal_iter(&self, id: usize) -> BoxedLIter<(TimeIndexEntry, Prop)> { + fn temporal_iter(&self, id: usize) -> BoxedLIter<'_, (TimeIndexEntry, Prop)> { let semantics = self.graph.node_time_semantics(); let node = self.graph.core_node(self.node); GenLockedIter::from(node, |node| { @@ -288,7 +288,7 @@ impl<'graph, G, GH: GraphViewOps<'graph>> InternalTemporalPropertyViewOps .into_dyn_boxed() } - fn temporal_iter_rev(&self, id: usize) -> BoxedLIter<(TimeIndexEntry, Prop)> { + fn temporal_iter_rev(&self, id: usize) -> BoxedLIter<'_, (TimeIndexEntry, Prop)> { let semantics = self.graph.node_time_semantics(); let node = self.graph.core_node(self.node); GenLockedIter::from(node, |node| { @@ -338,7 +338,7 @@ impl<'graph, G: Send + Sync, GH: CoreGraphOps> InternalMetadataOps for NodeView< .clone() } - fn metadata_ids(&self) -> BoxedLIter { + fn metadata_ids(&self) -> BoxedLIter<'_, usize> { self.graph .node_meta() .metadata_mapper() diff --git a/raphtory/src/db/graph/nodes.rs b/raphtory/src/db/graph/nodes.rs index 3c9fb96363..8062825d65 100644 --- a/raphtory/src/db/graph/nodes.rs +++ b/raphtory/src/db/graph/nodes.rs @@ -203,7 +203,7 @@ where }) } - pub fn iter(&self) -> impl Iterator> + use<'_, 'graph, G, GH> { + pub fn iter(&self) -> impl Iterator> + use<'_, 'graph, G, GH> { self.iter_refs() .map(|v| NodeView::new_one_hop_filtered(&self.base_graph, &self.graph, v)) } @@ -218,7 +218,7 @@ where pub fn par_iter( &self, - ) -> impl ParallelIterator> + use<'_, 'graph, G, GH> { + ) -> impl ParallelIterator> + use<'_, 'graph, G, GH> { self.par_iter_refs() .map(|v| NodeView::new_one_hop_filtered(&self.base_graph, &self.graph, v)) } diff --git a/raphtory/src/db/graph/views/deletion_graph.rs b/raphtory/src/db/graph/views/deletion_graph.rs index 3deb62e51d..ea0a985b86 100644 --- a/raphtory/src/db/graph/views/deletion_graph.rs +++ b/raphtory/src/db/graph/views/deletion_graph.rs @@ -194,7 +194,7 @@ impl GraphTimeSemanticsOps for PersistentGraph { self.0.has_temporal_prop(prop_id) } - fn temporal_prop_iter(&self, prop_id: usize) -> BoxedLIter<(TimeIndexEntry, Prop)> { + fn temporal_prop_iter(&self, prop_id: usize) -> BoxedLIter<'_, (TimeIndexEntry, Prop)> { self.0.temporal_prop_iter(prop_id) } @@ -210,7 +210,7 @@ impl GraphTimeSemanticsOps for PersistentGraph { prop_id: usize, start: i64, end: i64, - ) -> BoxedLIter<(TimeIndexEntry, Prop)> { + ) -> BoxedLIter<'_, (TimeIndexEntry, Prop)> { if let Some(prop) = self.graph_meta().get_temporal_prop(prop_id) { let first = persisted_prop_value_at(start, &*prop, &TimeIndex::Empty) .map(|v| (TimeIndexEntry::start(start), v)); @@ -232,7 +232,7 @@ impl GraphTimeSemanticsOps for PersistentGraph { prop_id: usize, start: i64, end: i64, - ) -> BoxedLIter<(TimeIndexEntry, Prop)> { + ) -> BoxedLIter<'_, (TimeIndexEntry, Prop)> { if let Some(prop) = self.graph_meta().get_temporal_prop(prop_id) { let first = persisted_prop_value_at(start, &*prop, &TimeIndex::Empty) .map(|v| (TimeIndexEntry::start(start), v)); @@ -2054,6 +2054,4 @@ mod test_edge_history_filter_persistent_graph { // let bool = g.is_edge_prop_update_latest_window(prop_id, edge_id, TimeIndexEntry::end(3), w.clone()); // assert!(!bool); } - - } diff --git a/raphtory/src/db/graph/views/node_subgraph.rs b/raphtory/src/db/graph/views/node_subgraph.rs index 473c634772..bea0be9a5c 100644 --- a/raphtory/src/db/graph/views/node_subgraph.rs +++ b/raphtory/src/db/graph/views/node_subgraph.rs @@ -212,7 +212,7 @@ mod subgraph_tests { use ahash::HashSet; use itertools::Itertools; use proptest::{proptest, sample::subsequence}; - use raphtory_storage::mutation::addition_ops::{InternalAdditionOps, SessionAdditionOps}; + use raphtory_storage::mutation::addition_ops::InternalAdditionOps; use std::collections::BTreeSet; #[test] diff --git a/raphtory/src/db/graph/views/window_graph.rs b/raphtory/src/db/graph/views/window_graph.rs index 414f86f17c..31865b6469 100644 --- a/raphtory/src/db/graph/views/window_graph.rs +++ b/raphtory/src/db/graph/views/window_graph.rs @@ -353,7 +353,7 @@ impl<'graph, G: GraphViewOps<'graph>> InternalTemporalPropertyViewOps for Window self.graph.temporal_value_at(id, self.end_bound()) } - fn temporal_iter(&self, id: usize) -> BoxedLIter<(TimeIndexEntry, Prop)> { + fn temporal_iter(&self, id: usize) -> BoxedLIter<'_, (TimeIndexEntry, Prop)> { if self.window_is_empty() { return iter::empty().into_dyn_boxed(); } @@ -362,7 +362,7 @@ impl<'graph, G: GraphViewOps<'graph>> InternalTemporalPropertyViewOps for Window .into_dyn_boxed() } - fn temporal_iter_rev(&self, id: usize) -> BoxedLIter<(TimeIndexEntry, Prop)> { + fn temporal_iter_rev(&self, id: usize) -> BoxedLIter<'_, (TimeIndexEntry, Prop)> { self.graph .temporal_prop_iter_window_rev(id, self.start_bound(), self.end_bound()) .into_dyn_boxed() @@ -390,7 +390,7 @@ impl<'graph, G: GraphViewOps<'graph>> InternalTemporalPropertiesOps for Windowed self.graph.get_temporal_prop_name(id) } - fn temporal_prop_ids(&self) -> BoxedLIter { + fn temporal_prop_ids(&self) -> BoxedLIter<'_, usize> { Box::new( self.graph .temporal_prop_ids() @@ -455,7 +455,7 @@ impl<'graph, G: GraphViewOps<'graph>> GraphTimeSemanticsOps for WindowedGraph .has_temporal_prop_window(prop_id, self.start_bound()..self.end_bound()) } - fn temporal_prop_iter(&self, prop_id: usize) -> BoxedLIter<(TimeIndexEntry, Prop)> { + fn temporal_prop_iter(&self, prop_id: usize) -> BoxedLIter<'_, (TimeIndexEntry, Prop)> { if self.window_is_empty() { return iter::empty().into_dyn_dboxed(); } @@ -472,7 +472,7 @@ impl<'graph, G: GraphViewOps<'graph>> GraphTimeSemanticsOps for WindowedGraph prop_id: usize, start: i64, end: i64, - ) -> BoxedLIter<(TimeIndexEntry, Prop)> { + ) -> BoxedLIter<'_, (TimeIndexEntry, Prop)> { self.graph.temporal_prop_iter_window(prop_id, start, end) } @@ -481,7 +481,7 @@ impl<'graph, G: GraphViewOps<'graph>> GraphTimeSemanticsOps for WindowedGraph prop_id: usize, start: i64, end: i64, - ) -> BoxedLIter<(TimeIndexEntry, Prop)> { + ) -> BoxedLIter<'_, (TimeIndexEntry, Prop)> { self.graph .temporal_prop_iter_window_rev(prop_id, start, end) } diff --git a/raphtory/src/db/replay/mod.rs b/raphtory/src/db/replay/mod.rs index ae8cf1b8cb..3f583b98af 100644 --- a/raphtory/src/db/replay/mod.rs +++ b/raphtory/src/db/replay/mod.rs @@ -3,7 +3,7 @@ use raphtory_api::core::{ entities::{properties::prop::Prop, EID, GID, VID}, storage::{dict_mapper::MaybeNew, timeindex::TimeIndexEntry}, }; -use raphtory_storage::mutation::addition_ops::{EdgeWriteLock, InternalAdditionOps}; +use raphtory_storage::mutation::addition_ops::InternalAdditionOps; use storage::{ api::edges::EdgeSegmentOps, error::StorageError, diff --git a/raphtory/src/io/arrow/dataframe.rs b/raphtory/src/io/arrow/dataframe.rs index f7fb71499f..40428639f1 100644 --- a/raphtory/src/io/arrow/dataframe.rs +++ b/raphtory/src/io/arrow/dataframe.rs @@ -98,7 +98,7 @@ impl TimeCol { } #[derive(Clone, Debug)] -pub(crate) struct DFChunk { +pub struct DFChunk { pub(crate) chunk: Vec>, } diff --git a/raphtory/src/io/arrow/df_loaders.rs b/raphtory/src/io/arrow/df_loaders.rs index c7cd3ba1e6..c4cd0ae0b5 100644 --- a/raphtory/src/io/arrow/df_loaders.rs +++ b/raphtory/src/io/arrow/df_loaders.rs @@ -21,10 +21,7 @@ use raphtory_api::{ }, }; use raphtory_core::{ - entities::{ - graph::logical_to_physical::{Mapping, ResolverShardT}, - GidRef, GidType, VID, - }, + entities::{graph::logical_to_physical::ResolverShardT, GidRef, VID}, storage::timeindex::AsTime, }; use raphtory_storage::mutation::addition_ops::{InternalAdditionOps, SessionAdditionOps}; @@ -37,7 +34,6 @@ use std::{ Arc, }, }; -use storage::resolver::GIDResolverOps; fn build_progress_bar(des: String, num_rows: usize) -> Result { BarBuilder::default() @@ -250,7 +246,7 @@ pub(crate) fn load_edges_from_df< let mut write_locked_graph = graph.write_lock().map_err(into_graph_err)?; // set the type of the resolver; - let mut chunks = df_view.chunks.peekable(); + let chunks = df_view.chunks.peekable(); // let mapping = Mapping::new(); // let mapping = write_locked_graph.graph().logical_to_physical.clone(); @@ -949,8 +945,6 @@ mod tests { use itertools::Itertools; use polars_arrow::array::{MutableArray, MutablePrimitiveArray, MutableUtf8Array}; use proptest::proptest; - use raphtory_storage::core_ops::CoreGraphOps; - use tempfile::TempDir; #[cfg(feature = "storage")] mod load_multi_layer { diff --git a/raphtory/src/io/arrow/layer_col.rs b/raphtory/src/io/arrow/layer_col.rs index c91b27b86e..8adb65b4d4 100644 --- a/raphtory/src/io/arrow/layer_col.rs +++ b/raphtory/src/io/arrow/layer_col.rs @@ -7,7 +7,6 @@ use iter_enum::{ DoubleEndedIterator, ExactSizeIterator, IndexedParallelIterator, Iterator, ParallelIterator, }; use polars_arrow::array::{StaticArray, Utf8Array, Utf8ViewArray}; -use raphtory_storage::mutation::addition_ops::SessionAdditionOps; use rayon::prelude::*; #[derive(Copy, Clone)] diff --git a/raphtory/src/io/arrow/node_col.rs b/raphtory/src/io/arrow/node_col.rs index cf8f274d48..99406b9b80 100644 --- a/raphtory/src/io/arrow/node_col.rs +++ b/raphtory/src/io/arrow/node_col.rs @@ -12,7 +12,7 @@ trait NodeColOps: Send + Sync { fn has_missing_values(&self) -> bool { self.null_count() != 0 } - fn get(&self, i: usize) -> Option; + fn get(&self, i: usize) -> Option>; fn dtype(&self) -> GidType; @@ -22,7 +22,7 @@ trait NodeColOps: Send + Sync { } impl NodeColOps for PrimitiveArray { - fn get(&self, i: usize) -> Option { + fn get(&self, i: usize) -> Option> { StaticArray::get(self, i).map(GidRef::U64) } @@ -39,7 +39,7 @@ impl NodeColOps for PrimitiveArray { } impl NodeColOps for PrimitiveArray { - fn get(&self, i: usize) -> Option { + fn get(&self, i: usize) -> Option> { StaticArray::get(self, i).map(|v| GidRef::U64(v as u64)) } @@ -56,7 +56,7 @@ impl NodeColOps for PrimitiveArray { } impl NodeColOps for PrimitiveArray { - fn get(&self, i: usize) -> Option { + fn get(&self, i: usize) -> Option> { StaticArray::get(self, i).map(|v| GidRef::U64(v as u64)) } @@ -72,7 +72,7 @@ impl NodeColOps for PrimitiveArray { } impl NodeColOps for PrimitiveArray { - fn get(&self, i: usize) -> Option { + fn get(&self, i: usize) -> Option> { StaticArray::get(self, i).map(|v| GidRef::U64(v as u64)) } @@ -88,7 +88,7 @@ impl NodeColOps for PrimitiveArray { } impl NodeColOps for Utf8ViewArray { - fn get(&self, i: usize) -> Option { + fn get(&self, i: usize) -> Option> { if i >= self.len() { None } else { @@ -114,7 +114,7 @@ impl NodeColOps for Utf8ViewArray { } impl NodeColOps for Utf8Array { - fn get(&self, i: usize) -> Option { + fn get(&self, i: usize) -> Option> { if i >= self.len() { None } else { @@ -143,7 +143,7 @@ impl NodeColOps for Utf8Array { } impl NodeColOps for Int32Array { - fn get(&self, i: usize) -> Option { + fn get(&self, i: usize) -> Option> { self.values().get(i).map(|v| GidRef::U64(*v as u64)) } @@ -159,7 +159,7 @@ impl NodeColOps for Int32Array { } impl NodeColOps for Int64Array { - fn get(&self, i: usize) -> Option { + fn get(&self, i: usize) -> Option> { self.values().get(i).map(|v| GidRef::U64(*v as u64)) } @@ -175,7 +175,7 @@ impl NodeColOps for Int64Array { } impl NodeColOps for arrow_array::StringArray { - fn get(&self, i: usize) -> Option { + fn get(&self, i: usize) -> Option> { if i >= ArrowArray::len(self) { None } else { @@ -199,7 +199,7 @@ impl NodeColOps for arrow_array::StringArray { } impl NodeColOps for arrow_array::LargeStringArray { - fn get(&self, i: usize) -> Option { + fn get(&self, i: usize) -> Option> { if i >= ArrowArray::len(self) { None } else { @@ -224,7 +224,7 @@ impl NodeColOps for arrow_array::LargeStringArray { } impl NodeColOps for arrow_array::StringViewArray { - fn get(&self, i: usize) -> Option { + fn get(&self, i: usize) -> Option> { if i >= ArrowArray::len(self) { None } else { @@ -248,7 +248,7 @@ impl NodeColOps for arrow_array::StringViewArray { } impl NodeColOps for arrow_array::UInt32Array { - fn get(&self, i: usize) -> Option { + fn get(&self, i: usize) -> Option> { self.values().get(i).map(|v| GidRef::U64(*v as u64)) } @@ -264,7 +264,7 @@ impl NodeColOps for arrow_array::UInt32Array { } impl NodeColOps for arrow_array::UInt64Array { - fn get(&self, i: usize) -> Option { + fn get(&self, i: usize) -> Option> { self.values().get(i).map(|v| GidRef::U64(*v)) } @@ -418,7 +418,7 @@ impl NodeCol { (0..self.0.len()).into_par_iter().map(|i| self.0.get(i)) } - pub fn iter(&self) -> impl Iterator + '_ { + pub fn iter(&self) -> impl Iterator> + '_ { (0..self.0.len()).map(|i| self.0.get(i).unwrap()) } diff --git a/raphtory/src/python/filter/mod.rs b/raphtory/src/python/filter/mod.rs index 34ec47d89d..52be7caa2b 100644 --- a/raphtory/src/python/filter/mod.rs +++ b/raphtory/src/python/filter/mod.rs @@ -18,7 +18,7 @@ pub mod filter_expr; pub mod node_filter_builders; pub mod property_filter_builders; -pub fn base_filter_module(py: Python<'_>) -> Result, PyErr> { +pub fn base_filter_module(py: Python) -> Result, PyErr> { let filter_module = PyModule::new(py, "filter")?; filter_module.add_class::()?; filter_module.add_class::()?; diff --git a/raphtory/src/python/graph/node.rs b/raphtory/src/python/graph/node.rs index 31f0a1f4d2..fdd82a0037 100644 --- a/raphtory/src/python/graph/node.rs +++ b/raphtory/src/python/graph/node.rs @@ -94,7 +94,7 @@ impl /// Converts a python node into a rust node. impl AsNodeRef for PyNode { - fn as_node_ref(&self) -> NodeRef { + fn as_node_ref(&self) -> NodeRef<'_> { self.node.as_node_ref() } } diff --git a/raphtory/src/python/graph/node_state/mod.rs b/raphtory/src/python/graph/node_state/mod.rs index f8261944f5..005db2dd5d 100644 --- a/raphtory/src/python/graph/node_state/mod.rs +++ b/raphtory/src/python/graph/node_state/mod.rs @@ -4,7 +4,7 @@ use crate::{add_classes, python::graph::node_state::group_by::PyNodeGroups}; pub use node_state::*; use pyo3::prelude::*; -pub fn base_node_state_module(py: Python<'_>) -> PyResult> { +pub fn base_node_state_module(py: Python) -> PyResult> { let m = PyModule::new(py, "node_state")?; add_classes!( &m, diff --git a/raphtory/src/python/packages/base_modules.rs b/raphtory/src/python/packages/base_modules.rs index a2d2c8b44f..643c9512ac 100644 --- a/raphtory/src/python/packages/base_modules.rs +++ b/raphtory/src/python/packages/base_modules.rs @@ -66,7 +66,7 @@ pub fn add_raphtory_classes(m: &Bound) -> PyResult<()> { Ok(()) } -pub fn base_algorithm_module(py: Python<'_>) -> Result, PyErr> { +pub fn base_algorithm_module(py: Python) -> Result, PyErr> { let algorithm_module = PyModule::new(py, "algorithms")?; add_functions!( &algorithm_module, @@ -119,7 +119,7 @@ pub fn base_algorithm_module(py: Python<'_>) -> Result, PyErr> { Ok(algorithm_module) } -pub fn base_graph_loader_module(py: Python<'_>) -> Result, PyErr> { +pub fn base_graph_loader_module(py: Python) -> Result, PyErr> { let graph_loader_module = PyModule::new(py, "graph_loader")?; add_functions!( &graph_loader_module, @@ -134,7 +134,7 @@ pub fn base_graph_loader_module(py: Python<'_>) -> Result, PyErr Ok(graph_loader_module) } -pub fn base_graph_gen_module(py: Python<'_>) -> Result, PyErr> { +pub fn base_graph_gen_module(py: Python) -> Result, PyErr> { let graph_gen_module = PyModule::new(py, "graph_gen")?; add_functions!( &graph_gen_module, @@ -144,7 +144,7 @@ pub fn base_graph_gen_module(py: Python<'_>) -> Result, PyErr> { Ok(graph_gen_module) } -pub fn base_vectors_module(py: Python<'_>) -> Result, PyErr> { +pub fn base_vectors_module(py: Python) -> Result, PyErr> { let vectors_module = PyModule::new(py, "vectors")?; vectors_module.add_class::()?; vectors_module.add_class::()?; diff --git a/raphtory/src/python/types/macros/borrowing_iterator.rs b/raphtory/src/python/types/macros/borrowing_iterator.rs index c22c3a29db..f8f85dc355 100644 --- a/raphtory/src/python/types/macros/borrowing_iterator.rs +++ b/raphtory/src/python/types/macros/borrowing_iterator.rs @@ -4,7 +4,7 @@ macro_rules! py_borrowing_iter { struct Iterator($inner_t); impl $crate::python::types::wrappers::iterators::PyIter for Iterator { - fn iter(&self) -> $crate::db::api::view::BoxedLIter> { + fn iter(&self) -> $crate::db::api::view::BoxedLIter<'_, PyResult> { // forces the type inference to return the correct lifetimes, // calling the closure directly does not work fn apply<'a, O: $crate::python::types::wrappers::iterators::IntoPyIter<'a>>( diff --git a/raphtory/src/python/types/wrappers/iterators.rs b/raphtory/src/python/types/wrappers/iterators.rs index 102dfb3b16..b0cc553e01 100644 --- a/raphtory/src/python/types/wrappers/iterators.rs +++ b/raphtory/src/python/types/wrappers/iterators.rs @@ -22,7 +22,7 @@ impl PyBorrowingIterator { } pub trait PyIter: Send + Sync + 'static { - fn iter(&self) -> BoxedLIter>; + fn iter(&self) -> BoxedLIter<'_, PyResult>; fn into_py_iter(self) -> PyBorrowingIterator where diff --git a/raphtory/src/python/utils/mod.rs b/raphtory/src/python/utils/mod.rs index 5b9a99a88a..9a2f68e05e 100644 --- a/raphtory/src/python/utils/mod.rs +++ b/raphtory/src/python/utils/mod.rs @@ -56,7 +56,7 @@ impl<'source> FromPyObject<'source> for PyNodeRef { } impl AsNodeRef for PyNodeRef { - fn as_node_ref(&self) -> NodeRef { + fn as_node_ref(&self) -> NodeRef<'_> { match self { PyNodeRef::ExternalStr(str) => NodeRef::External(GidRef::Str(str)), PyNodeRef::ExternalInt(gid) => NodeRef::External(GidRef::U64(*gid)), diff --git a/raphtory/src/search/edge_index.rs b/raphtory/src/search/edge_index.rs index 938f652bed..aee7c1f6dd 100644 --- a/raphtory/src/search/edge_index.rs +++ b/raphtory/src/search/edge_index.rs @@ -15,7 +15,7 @@ use raphtory_storage::{ core_ops::CoreGraphOps, graph::{edges::edge_storage_ops::EdgeStorageOps, graph::GraphStorage}, }; -use rayon::{iter::IntoParallelIterator, prelude::ParallelIterator}; +use rayon::prelude::ParallelIterator; use std::{ fmt::{Debug, Formatter}, path::PathBuf, diff --git a/raphtory/src/search/graph_index.rs b/raphtory/src/search/graph_index.rs index 46665aae43..c2540bbc45 100644 --- a/raphtory/src/search/graph_index.rs +++ b/raphtory/src/search/graph_index.rs @@ -10,7 +10,7 @@ use crate::{ serialise::GraphFolder, }; use parking_lot::RwLock; -use raphtory_api::core::storage::{arc_str::ArcStr, dict_mapper::MaybeNew}; +use raphtory_api::core::storage::dict_mapper::MaybeNew; use raphtory_storage::graph::graph::GraphStorage; use std::{ ffi::OsStr, @@ -368,7 +368,7 @@ impl GraphIndex { !matches!(self, GraphIndex::Empty) } - pub fn searcher(&self) -> Option { + pub fn searcher(&self) -> Option> { self.index().map(Searcher::new) } } diff --git a/raphtory/src/search/node_index.rs b/raphtory/src/search/node_index.rs index 346285de54..7f442aa90f 100644 --- a/raphtory/src/search/node_index.rs +++ b/raphtory/src/search/node_index.rs @@ -1,8 +1,5 @@ use crate::{ - core::{ - entities::VID, - storage::timeindex::{AsTime, TimeIndexEntry}, - }, + core::{entities::VID, storage::timeindex::TimeIndexEntry}, db::{api::view::IndexSpec, graph::node::NodeView}, errors::GraphError, prelude::*, @@ -13,10 +10,7 @@ use crate::{ }, }; use ahash::HashSet; -use raphtory_api::core::storage::{ - arc_str::{ArcStr, OptionAsStr}, - dict_mapper::MaybeNew, -}; +use raphtory_api::core::storage::arc_str::OptionAsStr; use raphtory_storage::graph::graph::GraphStorage; use rayon::{iter::IntoParallelIterator, prelude::ParallelIterator}; use std::{ @@ -203,7 +197,7 @@ impl NodeIndex { document.add_text(self.node_name_field, node_name.clone()); document.add_text(self.node_name_tokenized_field, node_name); if let Some(node_type) = node_type { - document.add_text(self.node_type_field, node_type.clone()); + document.add_text(self.node_type_field, node_type); document.add_text(self.node_type_tokenized_field, node_type); } document From 8d77c00b388aeb3af08fc9dd4c4f061ba3b5a3c1 Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Wed, 20 Aug 2025 12:26:03 +0200 Subject: [PATCH 126/321] fix missing layer mapping for deletions in materialize --- raphtory/src/db/api/view/graph.rs | 16 +++------- raphtory/src/db/graph/views/deletion_graph.rs | 32 +++++++++++++++---- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/raphtory/src/db/api/view/graph.rs b/raphtory/src/db/api/view/graph.rs index e736bb2cc6..2e956e9c11 100644 --- a/raphtory/src/db/api/view/graph.rs +++ b/raphtory/src/db/api/view/graph.rs @@ -396,6 +396,7 @@ impl<'graph, G: GraphView + 'graph> GraphViewOps<'graph> for G { self, self.layer_ids(), ) { + let layer = layer_map[layer]; writer.delete_edge(t, edge_pos, src, dst, layer, 0); } } @@ -469,23 +470,14 @@ impl<'graph, G: GraphView + 'graph> GraphViewOps<'graph> for G { self, self.layer_ids(), ) { + let layer = layer_map[layer]; if let Some(node_pos) = maybe_src_pos { let mut writer = shard.writer(); - writer.update_deletion_time( - t, - node_pos, - eid.with_layer_deletion(layer), - 0, - ); + writer.update_timestamp(t, node_pos, eid.with_layer_deletion(layer), 0); } if let Some(node_pos) = maybe_dst_pos { let mut writer = shard.writer(); - writer.update_deletion_time( - t, - node_pos, - eid.with_layer_deletion(layer), - 0, - ); + writer.update_timestamp(t, node_pos, eid.with_layer_deletion(layer), 0); } } } diff --git a/raphtory/src/db/graph/views/deletion_graph.rs b/raphtory/src/db/graph/views/deletion_graph.rs index ea0a985b86..51f487880f 100644 --- a/raphtory/src/db/graph/views/deletion_graph.rs +++ b/raphtory/src/db/graph/views/deletion_graph.rs @@ -426,7 +426,8 @@ mod test_deletions { use proptest::{arbitrary::any, proptest, sample::subsequence}; use raphtory_api::core::entities::GID; use raphtory_storage::mutation::addition_ops::InternalAdditionOps; - use std::ops::Range; + use rayon::ThreadPoolBuilder; + use std::ops::{Deref, Range}; #[test] fn test_nodes() { @@ -648,14 +649,33 @@ mod test_deletions { } #[test] fn materialize_window_layers_prop_test() { - proptest!(|(graph_f in build_graph_strat(10, 10, true), w in any::>(), l in subsequence(&["a", "b"], 0..=2))| { - let g = PersistentGraph(build_graph(&graph_f)); - let glw = g.valid_layers(l).window(w.start, w.end); - let gmlw = glw.materialize().unwrap(); - assert_persistent_materialize_graph_equal(&glw, &gmlw); + proptest!(|(graph_f in build_graph_strat(10, 10, true), w in any::>(), l in subsequence(&["a", "b"], 0..=2), num_threads in 1..=16usize)| { + let pool = ThreadPoolBuilder::new().num_threads(num_threads).build().unwrap(); + pool.install(|| { + let g = PersistentGraph(build_graph(&graph_f)); + let glw = g.valid_layers(l.clone()).window(w.start, w.end); + let gmlw = glw.materialize().unwrap(); + assert_persistent_materialize_graph_equal(&glw, &gmlw); + }) + }) } + #[test] + + #[test] + fn materialize_window_multilayer() { + let g = PersistentGraph::new(); + g.add_edge(1, 0, 0, NO_PROPS, None).unwrap(); + g.delete_edge(3, 0, 0, Some("a")).unwrap(); + + let w = 0..10; + let glw = g.valid_layers("a").window(w.start, w.end); + let layers = glw.edge(0, 0).unwrap().explode_layers(); + dbg!(layers); + let gmlw = glw.materialize().unwrap(); + assert_persistent_materialize_graph_equal(&glw, &gmlw); + } #[test] fn test_materialize_deleted_edge() { let g = PersistentGraph::new(); From 83fc30c1fab2552f86294bf6106291fa89bc5b75 Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Wed, 20 Aug 2025 12:32:27 +0200 Subject: [PATCH 127/321] update assert to reflect static graph --- raphtory/src/db/graph/edge.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/raphtory/src/db/graph/edge.rs b/raphtory/src/db/graph/edge.rs index ef31e2a185..17a7309176 100644 --- a/raphtory/src/db/graph/edge.rs +++ b/raphtory/src/db/graph/edge.rs @@ -868,7 +868,7 @@ mod test_edge { fn test_metadata_additions() { let g = Graph::new(); let e = g.add_edge(0, 1, 2, NO_PROPS, Some("test")).unwrap(); - assert_eq!(e.edge.layer(), Some(0)); + assert_eq!(e.edge.layer(), Some(1)); // 0 is static graph assert!(e.add_metadata([("test1", "test1")], None).is_ok()); // adds properties to layer `"test"` assert!(e.add_metadata([("test", "test")], Some("test2")).is_err()); // cannot add properties to a different layer e.add_metadata([("test", "test")], Some("test")).unwrap(); // layer is consistent From 8eef6a44bb0d90fc8ee37bf53a902740fb726838 Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Wed, 20 Aug 2025 14:28:13 +0200 Subject: [PATCH 128/321] explode_layers was ignoring layer filters for persistent semantics --- .../internal/time_semantics/filtered_edge.rs | 76 +++++++++---------- .../time_semantics/persistent_semantics.rs | 4 +- raphtory/src/db/graph/views/valid_graph.rs | 15 ++++ 3 files changed, 55 insertions(+), 40 deletions(-) diff --git a/raphtory/src/db/api/view/internal/time_semantics/filtered_edge.rs b/raphtory/src/db/api/view/internal/time_semantics/filtered_edge.rs index aef20991e1..7d9f106159 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/filtered_edge.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/filtered_edge.rs @@ -10,10 +10,7 @@ use raphtory_api::core::{ }, storage::timeindex::{TimeIndexEntry, TimeIndexOps}, }; -use raphtory_storage::graph::edges::{ - edge_storage_ops::{EdgeStorageOps}, - edges::EdgesStorage, -}; +use raphtory_storage::graph::edges::{edge_storage_ops::EdgeStorageOps, edges::EdgesStorage}; use rayon::iter::ParallelIterator; use std::{iter, marker::PhantomData, ops::Range}; use storage::{EdgeAdditions, EdgeDeletions, EdgeEntryRef}; @@ -254,6 +251,12 @@ impl<'graph, G: GraphViewOps<'graph>, P: TPropOps<'graph>> TPropOps<'graph> } pub trait FilteredEdgeStorageOps<'a> { + fn filtered_layer_ids_iter( + self, + view: G, + layer_ids: &'a LayerIds, + ) -> impl Iterator + 'a; + fn filtered_additions_iter( self, view: G, @@ -313,16 +316,22 @@ pub trait FilteredEdgeStorageOps<'a> { } impl<'a> FilteredEdgeStorageOps<'a> for EdgeEntryRef<'a> { + fn filtered_layer_ids_iter( + self, + view: G, + layer_ids: &'a LayerIds, + ) -> impl Iterator + 'a { + self.layer_ids_iter(layer_ids) + .filter(move |layer_id| view.internal_filter_edge_layer(self, *layer_id)) + } + fn filtered_additions_iter( self, view: G, layer_ids: &'a LayerIds, ) -> impl Iterator>)> { - self.layer_ids_iter(layer_ids).filter_map(move |layer| { - let view = view.clone(); - view.internal_filter_edge_layer(self, layer) - .then(move || (layer, self.filtered_additions(layer, view.clone()))) - }) + self.filtered_layer_ids_iter(view.clone(), layer_ids) + .map(move |layer_id| (layer_id, self.filtered_additions(layer_id, view.clone()))) } fn filtered_deletions_iter>( @@ -330,11 +339,8 @@ impl<'a> FilteredEdgeStorageOps<'a> for EdgeEntryRef<'a> { view: G, layer_ids: &'a LayerIds, ) -> impl Iterator>)> { - self.layer_ids_iter(layer_ids).filter_map(move |layer| { - let view = view.clone(); - view.internal_filter_edge_layer(self, layer) - .then(move || (layer, self.filtered_deletions(layer, view.clone()))) - }) + self.filtered_layer_ids_iter(view.clone(), layer_ids) + .map(move |layer| (layer, self.filtered_deletions(layer, view.clone()))) } fn filtered_updates_iter>( @@ -348,29 +354,26 @@ impl<'a> FilteredEdgeStorageOps<'a> for EdgeEntryRef<'a> { FilteredEdgeTimeIndex<'a, G, storage::EdgeDeletions<'a>>, ), > + 'a { - self.layer_ids_iter(layer_ids).filter_map(move |layer_id| { - let view = view.clone(); - view.internal_filter_edge_layer(self, layer_id) - .then(move || { - ( - layer_id, - self.filtered_additions(layer_id, view.clone()), - self.filtered_deletions(layer_id, view.clone()), - ) - }) - }) + self.filtered_layer_ids_iter(view.clone(), layer_ids) + .map(move |layer_id| { + ( + layer_id, + self.filtered_additions(layer_id, view.clone()), + self.filtered_deletions(layer_id, view.clone()), + ) + }) } fn filtered_additions>( self, layer_id: usize, view: G, - ) -> FilteredEdgeTimeIndex<'a, G, storage::EdgeAdditions<'a>> { + ) -> FilteredEdgeTimeIndex<'a, G, EdgeAdditions<'a>> { FilteredEdgeTimeIndex { eid: self.eid().with_layer(layer_id), time_index: self.additions(layer_id), view, - _marker: std::marker::PhantomData, + _marker: PhantomData, } } @@ -383,7 +386,7 @@ impl<'a> FilteredEdgeStorageOps<'a> for EdgeEntryRef<'a> { eid: self.eid().with_layer_deletion(layer_id), time_index: self.deletions(layer_id), view, - _marker: std::marker::PhantomData, + _marker: PhantomData, } } @@ -406,16 +409,13 @@ impl<'a> FilteredEdgeStorageOps<'a> for EdgeEntryRef<'a> { view: G, layer_ids: &'a LayerIds, ) -> impl Iterator)> + 'a { - self.layer_ids_iter(layer_ids).filter_map(move |layer_id| { - let view = view.clone(); - view.internal_filter_edge_layer(self, layer_id) - .then(move || { - ( - layer_id, - self.filtered_temporal_prop_layer(layer_id, prop_id, view.clone()), - ) - }) - }) + self.filtered_layer_ids_iter(view.clone(), layer_ids) + .map(move |layer_id| { + ( + layer_id, + self.filtered_temporal_prop_layer(layer_id, prop_id, view.clone()), + ) + }) } fn filtered_edge_metadata( diff --git a/raphtory/src/db/api/view/internal/time_semantics/persistent_semantics.rs b/raphtory/src/db/api/view/internal/time_semantics/persistent_semantics.rs index fcaa7f6571..40078be049 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/persistent_semantics.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/persistent_semantics.rs @@ -593,10 +593,10 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { fn edge_layers<'graph, G: GraphViewOps<'graph>>( self, e: EdgeEntryRef<'graph>, - _view: G, + view: G, layer_ids: &'graph LayerIds, ) -> impl Iterator + Send + Sync + 'graph { - e.layer_ids_iter(layer_ids) + e.filtered_layer_ids_iter(view, layer_ids) } fn edge_window_exploded<'graph, G: GraphViewOps<'graph>>( diff --git a/raphtory/src/db/graph/views/valid_graph.rs b/raphtory/src/db/graph/views/valid_graph.rs index 532b2cb8ea..b68a52d12a 100644 --- a/raphtory/src/db/graph/views/valid_graph.rs +++ b/raphtory/src/db/graph/views/valid_graph.rs @@ -127,6 +127,21 @@ mod tests { }) } + #[test] + fn test_explode_layers() { + let g = PersistentGraph::new(); + g.add_edge(0, 0, 1, NO_PROPS, Some("a")).unwrap(); + g.delete_edge(0, 0, 1, Some("b")).unwrap(); + let gv = g.valid(); + let edge = gv.edge(0, 1).unwrap(); + let exploded = edge.explode_layers(); + let layers = exploded + .layer_name() + .collect::, _>>() + .unwrap(); + assert_eq!(layers, ["a"]); + } + #[test] fn materialize_prop_test_events() { proptest!(|(graph_f in build_graph_strat(10, 10, true))| { From 8fe84bc02719aaaeaad1ac2638bcc05eca583e41 Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Wed, 20 Aug 2025 16:04:30 +0200 Subject: [PATCH 129/321] test multiple edge properties persistence --- raphtory/src/db/graph/views/deletion_graph.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/raphtory/src/db/graph/views/deletion_graph.rs b/raphtory/src/db/graph/views/deletion_graph.rs index 51f487880f..3ca57038b2 100644 --- a/raphtory/src/db/graph/views/deletion_graph.rs +++ b/raphtory/src/db/graph/views/deletion_graph.rs @@ -662,7 +662,6 @@ mod test_deletions { } #[test] - #[test] fn materialize_window_multilayer() { let g = PersistentGraph::new(); @@ -894,6 +893,21 @@ mod test_deletions { ); } + #[test] + fn test_multiple_edge_properties() { + let g = PersistentGraph::new(); + g.add_edge(0, 0, 1, [("test1", "test1")], None).unwrap(); + g.add_edge(1, 0, 1, [("test2", "test2")], None).unwrap(); + + let e = g.edge(0, 1).unwrap(); + assert_eq!(e.properties().get("test1").unwrap_str(), "test1"); + assert_eq!(e.properties().get("test2").unwrap_str(), "test2"); + + let ew = e.window(1, 10); + assert_eq!(ew.properties().get("test1").unwrap_str(), "test1"); + assert_eq!(ew.properties().get("test2").unwrap_str(), "test2"); + } + #[test] fn test_edge_history() { let g = PersistentGraph::new(); From 0ea294cb466270b606446ef781ef8a796525250e Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Wed, 20 Aug 2025 16:04:58 +0200 Subject: [PATCH 130/321] edge property value can persist even when the edge does not have a persisted event itself --- db4-storage/src/gen_t_props.rs | 6 ++-- .../time_semantics/persistent_semantics.rs | 35 ++++++++++++------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/db4-storage/src/gen_t_props.rs b/db4-storage/src/gen_t_props.rs index de4023f366..e3683cd492 100644 --- a/db4-storage/src/gen_t_props.rs +++ b/db4-storage/src/gen_t_props.rs @@ -89,10 +89,9 @@ impl<'a, Ref: WithTProps<'a> + 'a> TPropOps<'a> for GenTProps<'a, Ref> { self, w: Option>, ) -> impl Iterator + Send + Sync + 'a { - let w = w.map(|w| (w.start, w.end)); let tprops = self.tprops(self.prop_id); tprops - .map(|t_prop| t_prop.iter_inner(w.map(|(start, end)| start..end))) + .map(|t_prop| t_prop.iter_inner(w.clone())) .kmerge_by(|(a, _), (b, _)| a < b) } @@ -100,10 +99,9 @@ impl<'a, Ref: WithTProps<'a> + 'a> TPropOps<'a> for GenTProps<'a, Ref> { self, w: Option>, ) -> impl Iterator + Send + Sync + 'a { - let w = w.map(|w| (w.start, w.end)); let tprops = self .tprops(self.prop_id) - .map(move |t_cell| t_cell.iter_inner_rev(w.map(|(start, end)| start..end))); + .map(move |t_cell| t_cell.iter_inner_rev(w.clone())); tprops.kmerge_by(|(a, _), (b, _)| a > b) } diff --git a/raphtory/src/db/api/view/internal/time_semantics/persistent_semantics.rs b/raphtory/src/db/api/view/internal/time_semantics/persistent_semantics.rs index 40078be049..71a8eb2623 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/persistent_semantics.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/persistent_semantics.rs @@ -151,15 +151,30 @@ fn last_prop_value_before<'a, 'b>( fn persisted_prop_value_at<'a, 'b>( t: i64, props: impl TPropOps<'a>, + additions: impl TimeIndexOps<'b, IndexType = TimeIndexEntry>, deletions: impl TimeIndexOps<'b, IndexType = TimeIndexEntry>, -) -> Option { +) -> Option<(TimeIndexEntry, Prop)> { if props.active_t(t..t.saturating_add(1)) || deletions.active_t(t..t.saturating_add(1)) { None } else { - last_prop_value_before(TimeIndexEntry::start(t), props, deletions).map(|(_, v)| v) + persisted_secondary_index(t, additions).and_then(|index| { + last_prop_value_before(TimeIndexEntry::start(t), props, deletions) + .map(|(_, v)| (TimeIndexEntry(t, index), v)) + }) } } +fn persisted_secondary_index<'a>( + t: i64, + additions: impl TimeIndexOps<'a, IndexType = TimeIndexEntry>, +) -> Option { + additions + .range_t(t..t.saturating_add(1)) + .first() + .or_else(|| additions.range_t(i64::MIN..t).last()) + .map(|t| t.i()) +} + /// Exclude anything from the window that happens before the last deletion at the start of the window fn interior_window<'a>( w: Range, @@ -1148,11 +1163,9 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { let additions = e.filtered_additions(layer, &view); let deletions = e.filtered_deletions(layer, &view); let merged_deletions = deletions.clone().merge(additions.clone().invert()); - let persisted_ts = persisted_event(additions, deletions, w.start); - let first_prop = persisted_ts.and_then(|ts| { - persisted_prop_value_at(w.start, props.clone(), &merged_deletions) - .map(|v| (TimeIndexEntry(w.start, ts.i()), layer, v)) - }); + let first_prop = + persisted_prop_value_at(w.start, props.clone(), additions, &merged_deletions) + .map(|(ts, v)| (ts, layer, v)); first_prop.into_iter().chain( props .iter_window(interior_window(w.clone(), &merged_deletions)) @@ -1175,11 +1188,9 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { let additions = e.filtered_additions(layer, &view); let deletions = e.filtered_deletions(layer, &view); let merged_deletions = deletions.clone().merge(additions.clone().invert()); - let persisted_ts = persisted_event(additions, deletions, w.start); - let first_prop = persisted_ts.and_then(|ts| { - persisted_prop_value_at(w.start, props.clone(), &merged_deletions) - .map(|v| (TimeIndexEntry(w.start, ts.i()), layer, v)) - }); + let first_prop = + persisted_prop_value_at(w.start, props.clone(), additions, &merged_deletions) + .map(|(ts, v)| (ts, layer, v)); props .iter_inner_rev(Some(interior_window(w.clone(), &merged_deletions))) .map(move |(t, v)| (t, layer, v)) From f2e2e832098427ce8b64e04a99387b9943973fa5 Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Thu, 21 Aug 2025 12:46:24 +0200 Subject: [PATCH 131/321] handle metadata updates correctly for edges --- db4-storage/src/lib.rs | 7 +- db4-storage/src/pages/edge_page/writer.rs | 12 +- db4-storage/src/properties/mod.rs | 25 ++-- db4-storage/src/segments/edge.rs | 39 ++++-- .../entities/properties/prop/prop_enum.rs | 24 ++++ .../src/entities/properties/props.rs | 18 ++- raphtory-core/src/storage/lazy_vec.rs | 48 ++++---- raphtory-core/src/storage/mod.rs | 113 ++++++++++-------- .../src/mutation/property_addition_ops.rs | 1 + raphtory/src/db/graph/edge.rs | 35 ++++-- 10 files changed, 212 insertions(+), 110 deletions(-) diff --git a/db4-storage/src/lib.rs b/db4-storage/src/lib.rs index 28eaa110a1..905161be30 100644 --- a/db4-storage/src/lib.rs +++ b/db4-storage/src/lib.rs @@ -62,7 +62,7 @@ pub mod error { use std::{path::PathBuf, sync::Arc}; use raphtory_api::core::entities::properties::prop::PropError; - use raphtory_core::utils::time::ParseTimeError; + use raphtory_core::{entities::properties::props::MetadataError, utils::time::ParseTimeError}; #[derive(thiserror::Error, Debug)] pub enum StorageError { @@ -76,9 +76,10 @@ pub mod error { ArrowRS(#[from] arrow_schema::ArrowError), #[error("Parquet error: {0}")] Parquet(#[from] parquet::errors::ParquetError), - - #[error("Property error: {0}")] + #[error(transparent)] PropError(#[from] PropError), + #[error(transparent)] + MetadataError(#[from] MetadataError), #[error("Empty Graph: {0}")] EmptyGraphDir(PathBuf), #[error("Failed to parse time string")] diff --git a/db4-storage/src/pages/edge_page/writer.rs b/db4-storage/src/pages/edge_page/writer.rs index 1e6c0de226..f70fbf3214 100644 --- a/db4-storage/src/pages/edge_page/writer.rs +++ b/db4-storage/src/pages/edge_page/writer.rs @@ -1,7 +1,7 @@ use std::{borrow::Borrow, ops::DerefMut}; use crate::{ - LocalPOS, api::edges::EdgeSegmentOps, pages::layer_counter::GraphStats, + LocalPOS, api::edges::EdgeSegmentOps, error::StorageError, pages::layer_counter::GraphStats, segments::edge::MemEdgeSegment, }; use raphtory_api::core::entities::{VID, properties::prop::Prop}; @@ -116,6 +116,16 @@ impl<'a, MP: DerefMut + std::fmt::Debug, ES: EdgeSegmen self.page.get_edge(edge_pos, layer_id, self.writer.deref()) } + pub fn validate_c_props( + &self, + edge_pos: LocalPOS, + layer_id: usize, + props: &[(usize, Prop)], + ) -> Result<(), StorageError> { + self.writer + .check_const_properties(edge_pos, layer_id, props) + } + pub fn update_c_props>( &mut self, edge_pos: LocalPOS, diff --git a/db4-storage/src/properties/mod.rs b/db4-storage/src/properties/mod.rs index 40e0bce58e..785ead5e26 100644 --- a/db4-storage/src/properties/mod.rs +++ b/db4-storage/src/properties/mod.rs @@ -1,5 +1,4 @@ -use std::borrow::Borrow; - +use crate::error::StorageError; use bigdecimal::ToPrimitive; use polars_arrow::array::{Array, BooleanArray, PrimitiveArray, Utf8ViewArray}; use raphtory_api::core::entities::properties::{ @@ -9,10 +8,11 @@ use raphtory_api::core::entities::properties::{ use raphtory_core::{ entities::{ ELID, - properties::{tcell::TCell, tprop::TPropCell}, + properties::{props::MetadataError, tcell::TCell, tprop::TPropCell}, }, storage::{PropColumn, TColumns, timeindex::TimeIndexEntry}, }; +use std::borrow::Borrow; pub mod props_meta_writer; @@ -341,12 +341,7 @@ impl<'a> PropMutEntry<'a> { .resize_with(prop_id + 1, Default::default); } let const_props = &mut self.properties.c_properties[*prop_id]; - if let Err(err) = const_props.set(self.row, prop.clone()) { - panic!( - "Failed to set constant property {prop_id} for row {}: {err}", - self.row - ); - } + const_props.upsert(self.row, prop.clone()); } } } @@ -357,6 +352,18 @@ impl<'a> RowEntry<'a> { Some(TPropCell::new(t_cell, self.properties.t_column(prop_id))) } + pub fn metadata(self, prop_id: usize) -> Option { + self.properties.c_column(prop_id)?.get(self.row) + } + + pub fn check_metadata(self, prop_id: usize, new_val: &Prop) -> Result<(), StorageError> { + if let Some(col) = self.properties.c_column(prop_id) { + col.check(self.row, new_val) + .map_err(Into::::into)?; + } + Ok(()) + } + pub fn t_cell(self) -> &'a TCell> { self.properties .times_from_props(self.row) diff --git a/db4-storage/src/segments/edge.rs b/db4-storage/src/segments/edge.rs index 2403f8de53..96c8f3c761 100644 --- a/db4-storage/src/segments/edge.rs +++ b/db4-storage/src/segments/edge.rs @@ -7,17 +7,6 @@ use std::{ }, }; -use parking_lot::lock_api::ArcRwLockReadGuard; -use raphtory_api::core::entities::{ - VID, - properties::{meta::Meta, prop::Prop}, -}; -use raphtory_core::{ - entities::LayerIds, - storage::timeindex::{AsTime, TimeIndexEntry}, -}; -use rayon::prelude::*; - use crate::{ LocalPOS, api::edges::{EdgeSegmentOps, LockedESegment}, @@ -27,6 +16,16 @@ use crate::{ segments::edge_entry::MemEdgeRef, utils::Iter4, }; +use parking_lot::lock_api::ArcRwLockReadGuard; +use raphtory_api::core::entities::{ + VID, + properties::{meta::Meta, prop::Prop}, +}; +use raphtory_core::{ + entities::{LayerIds, properties::props::MetadataError}, + storage::timeindex::{AsTime, TimeIndexEntry}, +}; +use rayon::prelude::*; use super::{HasRow, SegmentContainer, edge_entry::MemEdgeEntry}; @@ -270,6 +269,24 @@ impl MemEdgeSegment { row.either(|a| a, |a| a) } + pub fn check_const_properties( + &self, + edge_pos: LocalPOS, + layer_id: usize, + props: &[(usize, Prop)], + ) -> Result<(), StorageError> { + if let Some(layer) = self.layers.get(layer_id) { + if let Some(edge) = layer.get(&edge_pos) { + let local_row = edge.row(); + let edge_properties = layer.properties().get_entry(local_row); + for (prop_id, prop_val) in props { + edge_properties.check_metadata(*prop_id, prop_val)?; + } + } + } + Ok(()) + } + pub fn update_const_properties>( &mut self, edge_pos: impl Into, diff --git a/raphtory-api/src/core/entities/properties/prop/prop_enum.rs b/raphtory-api/src/core/entities/properties/prop/prop_enum.rs index 4d500a29b0..e6ba93f125 100644 --- a/raphtory-api/src/core/entities/properties/prop/prop_enum.rs +++ b/raphtory-api/src/core/entities/properties/prop/prop_enum.rs @@ -431,18 +431,42 @@ impl From> for Prop { } } +impl From for Prop { + fn from(value: NaiveDateTime) -> Self { + Prop::NDTime(value) + } +} + impl From for Prop { fn from(b: bool) -> Self { Prop::Bool(b) } } +impl From>> for Prop { + fn from(value: Arc>) -> Self { + Prop::List(value) + } +} + +#[cfg(feature = "arrow")] +impl From for Prop { + fn from(value: PropArray) -> Self { + Prop::Array(value) + } +} impl From> for Prop { fn from(value: HashMap) -> Self { Prop::Map(Arc::new(value.into_iter().collect())) } } +impl From>> for Prop { + fn from(value: Arc>) -> Self { + Prop::Map(value) + } +} + impl From> for Prop { fn from(value: FxHashMap) -> Self { Prop::Map(Arc::new(value)) diff --git a/raphtory-core/src/entities/properties/props.rs b/raphtory-core/src/entities/properties/props.rs index d99d30521d..652320fce4 100644 --- a/raphtory-core/src/entities/properties/props.rs +++ b/raphtory-core/src/entities/properties/props.rs @@ -1,6 +1,6 @@ use crate::{ entities::properties::tprop::{IllegalPropType, TProp}, - storage::lazy_vec::IllegalSet, + storage::{lazy_vec::IllegalSet, TPropColumnError}, }; use raphtory_api::core::entities::properties::prop::Prop; use std::fmt::Debug; @@ -18,6 +18,9 @@ pub enum TPropError { pub enum MetadataError { #[error("Attempted to change value of metadata, old: {old}, new: {new}")] IllegalUpdate { old: Prop, new: Prop }, + + #[error(transparent)] + IllegalPropType(#[from] IllegalPropType), } impl From>> for MetadataError { @@ -28,6 +31,19 @@ impl From>> for MetadataError { } } +impl From for MetadataError { + fn from(value: TPropColumnError) -> Self { + match value { + TPropColumnError::IllegalSet(inner) => { + let old = inner.previous_value; + let new = inner.new_value; + MetadataError::IllegalUpdate { old, new } + } + TPropColumnError::IllegalType(inner) => MetadataError::IllegalPropType(inner), + } + } +} + #[cfg(test)] mod test { use super::*; diff --git a/raphtory-core/src/storage/lazy_vec.rs b/raphtory-core/src/storage/lazy_vec.rs index 11aa949a5f..5d6c8f4935 100644 --- a/raphtory-core/src/storage/lazy_vec.rs +++ b/raphtory-core/src/storage/lazy_vec.rs @@ -168,48 +168,54 @@ where A: PartialEq + Default + Debug + Sync + Send + Clone, { // fails if there is already a value set for the given id to a different value - pub fn set(&mut self, id: usize, value: A) -> Result<(), IllegalSet> { + + pub fn upsert(&mut self, id: usize, value: A) { match self { LazyVec::Empty => { *self = Self::from(id, value); - Ok(()) } + LazyVec::LazyVec1(_, tuples) => { + tuples.upsert(id, Some(value)); + self.swap_lazy_types(); + } + LazyVec::LazyVecN(_, vector) => { + vector.upsert(id, Some(value)); + } + } + } + + /// checks if there is already a different value for a given id + pub fn check(&self, id: usize, value: &A) -> Result<(), IllegalSet> { + match self { + LazyVec::Empty => {} LazyVec::LazyVec1(_, tuples) => { if let Some(only_value) = tuples.get(id) { - if only_value != &value { - return Err(IllegalSet::new(id, only_value.clone(), value)); + if only_value != value { + return Err(IllegalSet::new(id, only_value.clone(), value.clone())); } - } else { - tuples.upsert(id, Some(value)); - - self.swap_lazy_types(); } - Ok(()) } LazyVec::LazyVecN(_, vector) => { if let Some(only_value) = vector.get(id) { - if only_value != &value { - return Err(IllegalSet::new(id, only_value.clone(), value)); + if only_value != value { + return Err(IllegalSet::new(id, only_value.clone(), value.clone())); } - } else { - vector.upsert(id, Some(value)); } - Ok(()) } } + Ok(()) } pub fn update(&mut self, id: usize, updater: F) -> Result where F: FnOnce(&mut A) -> Result, - E: From>, { let b = match self.get_mut(id) { Some(value) => updater(value)?, None => { let mut value = A::default(); let b = updater(&mut value)?; - self.set(id, value)?; + self.upsert(id, value); b } }; @@ -404,9 +410,9 @@ mod lazy_vec_tests { fn normal_operation() { let mut vec = LazyVec::::Empty; - vec.set(5, 55).unwrap(); - vec.set(1, 11).unwrap(); - vec.set(8, 88).unwrap(); + vec.upsert(5, 55); + vec.upsert(1, 11); + vec.upsert(8, 88); assert_eq!(vec.get(5), Some(&55)); assert_eq!(vec.get(1), Some(&11)); assert_eq!(vec.get(0), Some(&0)); @@ -436,9 +442,9 @@ mod lazy_vec_tests { } #[test] - fn set_fails_if_present() { + fn check_fails_if_present() { let mut vec = LazyVec::from(5, 55); - let result = vec.set(5, 555); + let result = vec.check(5, &555); assert_eq!(result, Err(IllegalSet::new(5, 55, 555))) } } diff --git a/raphtory-core/src/storage/mod.rs b/raphtory-core/src/storage/mod.rs index b914e48df5..3c5e7b159a 100644 --- a/raphtory-core/src/storage/mod.rs +++ b/raphtory-core/src/storage/mod.rs @@ -117,40 +117,21 @@ pub enum PropColumn { #[derive(Error, Debug)] pub enum TPropColumnError { #[error(transparent)] - IllegalSetBool(#[from] IllegalSet), + IllegalSet(IllegalSet), #[error(transparent)] - IllegalSetU8(#[from] IllegalSet), - #[error(transparent)] - IllegalSetU16(#[from] IllegalSet), - #[error(transparent)] - IllegalSetU32(#[from] IllegalSet), - #[error(transparent)] - IllegalSetU64(#[from] IllegalSet), - #[error(transparent)] - IllegalSetI32(#[from] IllegalSet), - #[error(transparent)] - IllegalSetI64(#[from] IllegalSet), - #[error(transparent)] - IllegalSetF32(#[from] IllegalSet), - #[error(transparent)] - IllegalSetF64(#[from] IllegalSet), - #[error(transparent)] - IllegalSetStr(#[from] IllegalSet), - #[cfg(feature = "arrow")] - #[error(transparent)] - IllegalSetArray(#[from] IllegalSet), - #[error(transparent)] - IllegalSetList(#[from] IllegalSet>>), - #[error(transparent)] - IllegalSetMap(#[from] IllegalSet>>), - #[error(transparent)] - IllegalSetNDTime(#[from] IllegalSet), - #[error(transparent)] - IllegalSetDTime(#[from] IllegalSet>), - #[error(transparent)] - Decimal(#[from] IllegalSet), - #[error(transparent)] - IllegalPropType(#[from] IllegalPropType), + IllegalType(#[from] IllegalPropType), +} + +impl + Debug> From> for TPropColumnError { + fn from(value: IllegalSet) -> Self { + let previous_value = value.previous_value.into(); + let new_value = value.new_value.into(); + TPropColumnError::IllegalSet(IllegalSet { + index: value.index, + previous_value, + new_value, + }) + } } impl Default for PropColumn { @@ -162,7 +143,7 @@ impl Default for PropColumn { impl PropColumn { pub(crate) fn new(idx: usize, prop: Prop) -> Self { let mut col = PropColumn::default(); - col.set(idx, prop).unwrap(); + col.upsert(idx, prop).unwrap(); col } @@ -195,26 +176,56 @@ impl PropColumn { } } - pub fn set(&mut self, index: usize, prop: Prop) -> Result<(), TPropColumnError> { + pub fn upsert(&mut self, index: usize, prop: Prop) -> Result<(), TPropColumnError> { self.init_empty_col(&prop); match (self, prop) { - (PropColumn::Bool(col), Prop::Bool(v)) => col.set(index, v)?, - (PropColumn::I64(col), Prop::I64(v)) => col.set(index, v)?, - (PropColumn::U32(col), Prop::U32(v)) => col.set(index, v)?, - (PropColumn::U64(col), Prop::U64(v)) => col.set(index, v)?, - (PropColumn::F32(col), Prop::F32(v)) => col.set(index, v)?, - (PropColumn::F64(col), Prop::F64(v)) => col.set(index, v)?, - (PropColumn::Str(col), Prop::Str(v)) => col.set(index, v)?, + (PropColumn::Bool(col), Prop::Bool(v)) => col.upsert(index, v), + (PropColumn::I64(col), Prop::I64(v)) => col.upsert(index, v), + (PropColumn::U32(col), Prop::U32(v)) => col.upsert(index, v), + (PropColumn::U64(col), Prop::U64(v)) => col.upsert(index, v), + (PropColumn::F32(col), Prop::F32(v)) => col.upsert(index, v), + (PropColumn::F64(col), Prop::F64(v)) => col.upsert(index, v), + (PropColumn::Str(col), Prop::Str(v)) => col.upsert(index, v), + #[cfg(feature = "arrow")] + (PropColumn::Array(col), Prop::Array(v)) => col.upsert(index, v), + (PropColumn::U8(col), Prop::U8(v)) => col.upsert(index, v), + (PropColumn::U16(col), Prop::U16(v)) => col.upsert(index, v), + (PropColumn::I32(col), Prop::I32(v)) => col.upsert(index, v), + (PropColumn::List(col), Prop::List(v)) => col.upsert(index, v), + (PropColumn::Map(col), Prop::Map(v)) => col.upsert(index, v), + (PropColumn::NDTime(col), Prop::NDTime(v)) => col.upsert(index, v), + (PropColumn::DTime(col), Prop::DTime(v)) => col.upsert(index, v), + (PropColumn::Decimal(col), Prop::Decimal(v)) => col.upsert(index, v), + (col, prop) => { + Err(IllegalPropType { + expected: col.dtype(), + actual: prop.dtype(), + })?; + } + } + Ok(()) + } + + pub fn check(&self, index: usize, prop: &Prop) -> Result<(), TPropColumnError> { + match (self, prop) { + (PropColumn::Empty(_), _) => {} + (PropColumn::Bool(col), Prop::Bool(v)) => col.check(index, v)?, + (PropColumn::I64(col), Prop::I64(v)) => col.check(index, v)?, + (PropColumn::U32(col), Prop::U32(v)) => col.check(index, v)?, + (PropColumn::U64(col), Prop::U64(v)) => col.check(index, v)?, + (PropColumn::F32(col), Prop::F32(v)) => col.check(index, v)?, + (PropColumn::F64(col), Prop::F64(v)) => col.check(index, v)?, + (PropColumn::Str(col), Prop::Str(v)) => col.check(index, v)?, #[cfg(feature = "arrow")] - (PropColumn::Array(col), Prop::Array(v)) => col.set(index, v)?, - (PropColumn::U8(col), Prop::U8(v)) => col.set(index, v)?, - (PropColumn::U16(col), Prop::U16(v)) => col.set(index, v)?, - (PropColumn::I32(col), Prop::I32(v)) => col.set(index, v)?, - (PropColumn::List(col), Prop::List(v)) => col.set(index, v)?, - (PropColumn::Map(col), Prop::Map(v)) => col.set(index, v)?, - (PropColumn::NDTime(col), Prop::NDTime(v)) => col.set(index, v)?, - (PropColumn::DTime(col), Prop::DTime(v)) => col.set(index, v)?, - (PropColumn::Decimal(col), Prop::Decimal(v)) => col.set(index, v)?, + (PropColumn::Array(col), Prop::Array(v)) => col.check(index, v)?, + (PropColumn::U8(col), Prop::U8(v)) => col.check(index, v)?, + (PropColumn::U16(col), Prop::U16(v)) => col.check(index, v)?, + (PropColumn::I32(col), Prop::I32(v)) => col.check(index, v)?, + (PropColumn::List(col), Prop::List(v)) => col.check(index, v)?, + (PropColumn::Map(col), Prop::Map(v)) => col.check(index, v)?, + (PropColumn::NDTime(col), Prop::NDTime(v)) => col.check(index, v)?, + (PropColumn::DTime(col), Prop::DTime(v)) => col.check(index, v)?, + (PropColumn::Decimal(col), Prop::Decimal(v)) => col.check(index, v)?, (col, prop) => { Err(IllegalPropType { expected: col.dtype(), diff --git a/raphtory-storage/src/mutation/property_addition_ops.rs b/raphtory-storage/src/mutation/property_addition_ops.rs index 101958a514..cdc2903d54 100644 --- a/raphtory-storage/src/mutation/property_addition_ops.rs +++ b/raphtory-storage/src/mutation/property_addition_ops.rs @@ -105,6 +105,7 @@ impl InternalPropertyAdditionOps for db4_graph::TemporalGraph { let (src, dst) = writer.get_edge(layer, edge_pos).unwrap_or_else(|| { panic!("Edge with EID {eid:?} not found in layer {layer}"); }); + writer.validate_c_props(edge_pos, layer, props)?; writer.update_c_props(edge_pos, src, dst, layer, props); Ok(writer) } diff --git a/raphtory/src/db/graph/edge.rs b/raphtory/src/db/graph/edge.rs index 17a7309176..1873515fb7 100644 --- a/raphtory/src/db/graph/edge.rs +++ b/raphtory/src/db/graph/edge.rs @@ -334,6 +334,26 @@ impl EdgeView { Ok(layer_id) } + fn resolve_and_check_layer_for_metadata( + &self, + layer: Option<&str>, + ) -> Result { + let layer_id = self.resolve_layer(layer, false)?; + if self + .graph + .core_edge(self.edge.pid()) + .has_layer(&LayerIds::One(layer_id)) + { + Ok(layer_id) + } else { + Err(GraphError::InvalidEdgeLayer { + layer: layer.unwrap_or("_default").to_string(), + src: self.src().name(), + dst: self.dst().name(), + }) + } + } + /// Add metadata for the edge /// /// # Arguments @@ -349,18 +369,7 @@ impl EdgeView { properties: impl IntoIterator, layer: Option<&str>, ) -> Result<(), GraphError> { - let input_layer_id = self.resolve_layer(layer, false)?; - if !self - .graph - .core_edge(self.edge.pid()) - .has_layer(&LayerIds::One(input_layer_id)) - { - return Err(GraphError::InvalidEdgeLayer { - layer: layer.unwrap_or("_default").to_string(), - src: self.src().name(), - dst: self.dst().name(), - }); - } + let input_layer_id = self.resolve_and_check_layer_for_metadata(layer)?; let properties = self.graph.core_graph().validate_props( true, self.graph.edge_meta(), @@ -378,7 +387,7 @@ impl EdgeView { props: impl IntoIterator, layer: Option<&str>, ) -> Result<(), GraphError> { - let input_layer_id = self.resolve_layer(layer, false).map_err(into_graph_err)?; + let input_layer_id = self.resolve_and_check_layer_for_metadata(layer)?; let properties = self.graph.core_graph().validate_props( true, From bc3f776719216a8dcf5769b3f0f2f576eef8728e Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Thu, 21 Aug 2025 14:49:41 +0200 Subject: [PATCH 132/321] fix handling of node metadata updates --- db4-storage/src/pages/edge_page/writer.rs | 4 +- db4-storage/src/pages/node_page/writer.rs | 11 ++++- db4-storage/src/properties/mod.rs | 15 +++---- db4-storage/src/segments/edge.rs | 13 ++---- db4-storage/src/segments/mod.rs | 18 +++++++- db4-storage/src/segments/node.rs | 12 ++++++ .../src/mutation/property_addition_ops.rs | 42 ++++++++++--------- raphtory/src/db/api/storage/storage.rs | 32 ++++++++++---- raphtory/src/db/graph/edge.rs | 4 +- raphtory/src/db/graph/node.rs | 4 +- 10 files changed, 100 insertions(+), 55 deletions(-) diff --git a/db4-storage/src/pages/edge_page/writer.rs b/db4-storage/src/pages/edge_page/writer.rs index f70fbf3214..1a314f1b85 100644 --- a/db4-storage/src/pages/edge_page/writer.rs +++ b/db4-storage/src/pages/edge_page/writer.rs @@ -126,13 +126,13 @@ impl<'a, MP: DerefMut + std::fmt::Debug, ES: EdgeSegmen .check_const_properties(edge_pos, layer_id, props) } - pub fn update_c_props>( + pub fn update_c_props( &mut self, edge_pos: LocalPOS, src: impl Into, dst: impl Into, layer_id: usize, - props: impl IntoIterator, + props: impl IntoIterator, ) { let existing_edge = self .page diff --git a/db4-storage/src/pages/node_page/writer.rs b/db4-storage/src/pages/node_page/writer.rs index 0c9ac3ec83..ce926b1bb8 100644 --- a/db4-storage/src/pages/node_page/writer.rs +++ b/db4-storage/src/pages/node_page/writer.rs @@ -1,5 +1,5 @@ use crate::{ - LocalPOS, api::nodes::NodeSegmentOps, pages::layer_counter::GraphStats, + LocalPOS, api::nodes::NodeSegmentOps, error::StorageError, pages::layer_counter::GraphStats, segments::node::MemNodeSegment, }; use raphtory_api::core::entities::{ @@ -144,6 +144,15 @@ impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> NodeWri } } + pub fn check_metadata( + &self, + pos: LocalPOS, + layer_id: usize, + props: &[(usize, Prop)], + ) -> Result<(), StorageError> { + self.mut_segment.check_metadata(pos, layer_id, props) + } + pub fn update_c_props( &mut self, pos: LocalPOS, diff --git a/db4-storage/src/properties/mod.rs b/db4-storage/src/properties/mod.rs index 785ead5e26..c56aadfdd4 100644 --- a/db4-storage/src/properties/mod.rs +++ b/db4-storage/src/properties/mod.rs @@ -329,19 +329,16 @@ impl<'a> PropMutEntry<'a> { self.properties.update_earliest_latest(t); } - pub(crate) fn append_const_props>( - &mut self, - props: impl IntoIterator, - ) { - for prop in props { - let (prop_id, prop) = prop.borrow(); - if self.properties.c_properties.len() <= *prop_id { + pub(crate) fn append_const_props(&mut self, props: impl IntoIterator) { + for (prop_id, prop) in props { + if self.properties.c_properties.len() <= prop_id { self.properties .c_properties .resize_with(prop_id + 1, Default::default); } - let const_props = &mut self.properties.c_properties[*prop_id]; - const_props.upsert(self.row, prop.clone()); + let const_props = &mut self.properties.c_properties[prop_id]; + // property types should have been validated before! + const_props.upsert(self.row, prop.clone()).unwrap(); } } } diff --git a/db4-storage/src/segments/edge.rs b/db4-storage/src/segments/edge.rs index 96c8f3c761..9254d24fdf 100644 --- a/db4-storage/src/segments/edge.rs +++ b/db4-storage/src/segments/edge.rs @@ -276,24 +276,18 @@ impl MemEdgeSegment { props: &[(usize, Prop)], ) -> Result<(), StorageError> { if let Some(layer) = self.layers.get(layer_id) { - if let Some(edge) = layer.get(&edge_pos) { - let local_row = edge.row(); - let edge_properties = layer.properties().get_entry(local_row); - for (prop_id, prop_val) in props { - edge_properties.check_metadata(*prop_id, prop_val)?; - } - } + layer.check_metadata(edge_pos, props)?; } Ok(()) } - pub fn update_const_properties>( + pub fn update_const_properties( &mut self, edge_pos: impl Into, src: impl Into, dst: impl Into, layer_id: usize, - props: impl IntoIterator, + props: impl IntoIterator, ) { let edge_pos = edge_pos.into(); let src = src.into(); @@ -302,7 +296,6 @@ impl MemEdgeSegment { // Ensure we have enough layers self.ensure_layer(layer_id); let est_size = self.layers[layer_id].est_size(); - let local_row = self.reserve_local_row(edge_pos, src, dst, layer_id); let mut prop_entry: PropMutEntry<'_> = self.layers[layer_id] .properties_mut() diff --git a/db4-storage/src/segments/mod.rs b/db4-storage/src/segments/mod.rs index 3b2445389f..8960d7e49d 100644 --- a/db4-storage/src/segments/mod.rs +++ b/db4-storage/src/segments/mod.rs @@ -1,5 +1,6 @@ use std::{collections::hash_map::Entry, fmt::Debug, sync::Arc}; +use crate::{LocalPOS, error::StorageError}; use bitvec::{order::Msb0, vec::BitVec}; use either::Either; use raphtory_api::core::entities::properties::{meta::Meta, prop::Prop}; @@ -13,8 +14,6 @@ use raphtory_core::{ use rayon::prelude::*; use rustc_hash::FxHashMap; -use crate::LocalPOS; - use super::properties::{Properties, RowEntry}; pub mod edge; @@ -142,6 +141,21 @@ impl SegmentContainer { &mut self.properties } + pub fn check_metadata( + &self, + local_pos: LocalPOS, + props: &[(usize, Prop)], + ) -> Result<(), StorageError> { + if let Some(item) = self.get(&local_pos) { + let local_row = item.row(); + let edge_properties = self.properties().get_entry(local_row); + for (prop_id, prop_val) in props { + edge_properties.check_metadata(*prop_id, prop_val)?; + } + } + Ok(()) + } + pub fn meta(&self) -> &Arc { &self.meta } diff --git a/db4-storage/src/segments/node.rs b/db4-storage/src/segments/node.rs index 9320feac5e..33545bc395 100644 --- a/db4-storage/src/segments/node.rs +++ b/db4-storage/src/segments/node.rs @@ -317,6 +317,18 @@ impl MemNodeSegment { (is_new, layer_est_size - est_size) } + pub fn check_metadata( + &self, + node_pos: LocalPOS, + layer_id: usize, + props: &[(usize, Prop)], + ) -> Result<(), StorageError> { + if let Some(layer) = self.layers.get(layer_id) { + layer.check_metadata(node_pos, props)?; + } + Ok(()) + } + pub fn update_c_props( &mut self, node_pos: LocalPOS, diff --git a/raphtory-storage/src/mutation/property_addition_ops.rs b/raphtory-storage/src/mutation/property_addition_ops.rs index cdc2903d54..a10b04eac8 100644 --- a/raphtory-storage/src/mutation/property_addition_ops.rs +++ b/raphtory-storage/src/mutation/property_addition_ops.rs @@ -23,24 +23,24 @@ pub trait InternalPropertyAdditionOps { fn internal_add_node_metadata( &self, vid: VID, - props: &[(usize, Prop)], + props: Vec<(usize, Prop)>, ) -> Result, Self::Error>; fn internal_update_node_metadata( &self, vid: VID, - props: &[(usize, Prop)], + props: Vec<(usize, Prop)>, ) -> Result, Self::Error>; fn internal_add_edge_metadata( &self, eid: EID, layer: usize, - props: &[(usize, Prop)], + props: Vec<(usize, Prop)>, ) -> Result, Self::Error>; fn internal_update_edge_metadata( &self, eid: EID, layer: usize, - props: &[(usize, Prop)], + props: Vec<(usize, Prop)>, ) -> Result, Self::Error>; } @@ -78,34 +78,38 @@ impl InternalPropertyAdditionOps for db4_graph::TemporalGraph { fn internal_add_node_metadata( &self, vid: VID, - props: &[(usize, Prop)], + props: Vec<(usize, Prop)>, ) -> Result, Self::Error> { let (segment_id, node_pos) = self.storage().nodes().resolve_pos(vid); let mut writer = self.storage().nodes().writer(segment_id); - writer.update_c_props(node_pos, 0, props.iter().cloned(), 0); + writer.check_metadata(node_pos, 0, &props)?; + writer.update_c_props(node_pos, 0, props, 0); Ok(writer) } fn internal_update_node_metadata( &self, vid: VID, - props: &[(usize, Prop)], + props: Vec<(usize, Prop)>, ) -> Result, Self::Error> { - todo!() + let (segment_id, node_pos) = self.storage().nodes().resolve_pos(vid); + let mut writer = self.storage().nodes().writer(segment_id); + writer.update_c_props(node_pos, 0, props, 0); + Ok(writer) } fn internal_add_edge_metadata( &self, eid: EID, layer: usize, - props: &[(usize, Prop)], + props: Vec<(usize, Prop)>, ) -> Result, Self::Error> { let (_, edge_pos) = self.storage().edges().resolve_pos(eid); let mut writer = self.storage().edge_writer(eid); let (src, dst) = writer.get_edge(layer, edge_pos).unwrap_or_else(|| { panic!("Edge with EID {eid:?} not found in layer {layer}"); }); - writer.validate_c_props(edge_pos, layer, props)?; + writer.validate_c_props(edge_pos, layer, &props)?; writer.update_c_props(edge_pos, src, dst, layer, props); Ok(writer) } @@ -114,7 +118,7 @@ impl InternalPropertyAdditionOps for db4_graph::TemporalGraph { &self, eid: EID, layer: usize, - props: &[(usize, Prop)], + props: Vec<(usize, Prop)>, ) -> Result, Self::Error> { let (_, edge_pos) = self.storage().edges().resolve_pos(eid); let mut writer = self.storage().edge_writer(eid); @@ -148,7 +152,7 @@ impl InternalPropertyAdditionOps for GraphStorage { fn internal_add_node_metadata( &self, vid: VID, - props: &[(usize, Prop)], + props: Vec<(usize, Prop)>, ) -> Result, Self::Error> { self.mutable()?.internal_add_node_metadata(vid, props) } @@ -156,7 +160,7 @@ impl InternalPropertyAdditionOps for GraphStorage { fn internal_update_node_metadata( &self, vid: VID, - props: &[(usize, Prop)], + props: Vec<(usize, Prop)>, ) -> Result, Self::Error> { self.mutable()?.internal_update_node_metadata(vid, props) } @@ -165,7 +169,7 @@ impl InternalPropertyAdditionOps for GraphStorage { &self, eid: EID, layer: usize, - props: &[(usize, Prop)], + props: Vec<(usize, Prop)>, ) -> Result, Self::Error> { self.mutable()? .internal_add_edge_metadata(eid, layer, props) @@ -175,7 +179,7 @@ impl InternalPropertyAdditionOps for GraphStorage { &self, eid: EID, layer: usize, - props: &[(usize, Prop)], + props: Vec<(usize, Prop)>, ) -> Result, Self::Error> { self.mutable()? .internal_update_edge_metadata(eid, layer, props) @@ -213,7 +217,7 @@ where fn internal_add_node_metadata( &self, vid: VID, - props: &[(usize, Prop)], + props: Vec<(usize, Prop)>, ) -> Result, Self::Error> { self.base().internal_add_node_metadata(vid, props) } @@ -222,7 +226,7 @@ where fn internal_update_node_metadata( &self, vid: VID, - props: &[(usize, Prop)], + props: Vec<(usize, Prop)>, ) -> Result, Self::Error> { self.base().internal_update_node_metadata(vid, props) } @@ -232,7 +236,7 @@ where &self, eid: EID, layer: usize, - props: &[(usize, Prop)], + props: Vec<(usize, Prop)>, ) -> Result, Self::Error> { self.base().internal_add_edge_metadata(eid, layer, props) } @@ -242,7 +246,7 @@ where &self, eid: EID, layer: usize, - props: &[(usize, Prop)], + props: Vec<(usize, Prop)>, ) -> Result, Self::Error> { self.base().internal_update_edge_metadata(eid, layer, props) } diff --git a/raphtory/src/db/api/storage/storage.rs b/raphtory/src/db/api/storage/storage.rs index 0fd59d04b3..70afba42ae 100644 --- a/raphtory/src/db/api/storage/storage.rs +++ b/raphtory/src/db/api/storage/storage.rs @@ -585,12 +585,15 @@ impl InternalPropertyAdditionOps for Storage { fn internal_add_node_metadata( &self, vid: VID, - props: &[(usize, Prop)], + props: Vec<(usize, Prop)>, ) -> Result, Self::Error> { + #[cfg(feature = "search")] + let props_for_index = props.clone(); + let lock = self.graph.internal_add_node_metadata(vid, props)?; #[cfg(feature = "search")] - self.if_index_mut(|index| index.add_node_metadata(vid, props))?; + self.if_index_mut(|index| index.add_node_metadata(vid, &props_for_index))?; Ok(lock) } @@ -598,12 +601,15 @@ impl InternalPropertyAdditionOps for Storage { fn internal_update_node_metadata( &self, vid: VID, - props: &[(usize, Prop)], + props: Vec<(usize, Prop)>, ) -> Result, Self::Error> { + #[cfg(feature = "search")] + let props_for_index = props.clone(); + let lock = self.graph.internal_update_node_metadata(vid, props)?; #[cfg(feature = "search")] - self.if_index_mut(|index| index.update_node_metadata(vid, props))?; + self.if_index_mut(|index| index.update_node_metadata(vid, &props_for_index))?; Ok(lock) } @@ -612,12 +618,17 @@ impl InternalPropertyAdditionOps for Storage { &self, eid: EID, layer: usize, - props: &[(usize, Prop)], + props: Vec<(usize, Prop)>, ) -> Result, Self::Error> { + // FIXME: this whole thing is not great + + #[cfg(feature = "search")] + let props_for_index = props.clone(); + let lock = self.graph.internal_add_edge_metadata(eid, layer, props)?; #[cfg(feature = "search")] - self.if_index_mut(|index| index.add_edge_metadata(eid, layer, props))?; + self.if_index_mut(|index| index.add_edge_metadata(eid, layer, &props_for_index))?; Ok(lock) } @@ -626,14 +637,19 @@ impl InternalPropertyAdditionOps for Storage { &self, eid: EID, layer: usize, - props: &[(usize, Prop)], + props: Vec<(usize, Prop)>, ) -> Result, Self::Error> { + // FIXME: this whole thing is not great + + #[cfg(feature = "search")] + let props_for_index = props.clone(); + let lock = self .graph .internal_update_edge_metadata(eid, layer, props)?; #[cfg(feature = "search")] - self.if_index_mut(|index| index.update_edge_metadata(eid, layer, props))?; + self.if_index_mut(|index| index.update_edge_metadata(eid, layer, &props_for_index))?; Ok(lock) } diff --git a/raphtory/src/db/graph/edge.rs b/raphtory/src/db/graph/edge.rs index 1873515fb7..5c91faf0f8 100644 --- a/raphtory/src/db/graph/edge.rs +++ b/raphtory/src/db/graph/edge.rs @@ -377,7 +377,7 @@ impl EdgeView { )?; self.graph - .internal_add_edge_metadata(self.edge.pid(), input_layer_id, &properties) + .internal_add_edge_metadata(self.edge.pid(), input_layer_id, properties) .map_err(into_graph_err)?; Ok(()) } @@ -396,7 +396,7 @@ impl EdgeView { )?; self.graph - .internal_update_edge_metadata(self.edge.pid(), input_layer_id, &properties) + .internal_update_edge_metadata(self.edge.pid(), input_layer_id, properties) .map_err(into_graph_err)?; Ok(()) } diff --git a/raphtory/src/db/graph/node.rs b/raphtory/src/db/graph/node.rs index eb72bfa8fa..6bf937e6b7 100644 --- a/raphtory/src/db/graph/node.rs +++ b/raphtory/src/db/graph/node.rs @@ -425,7 +425,7 @@ impl NodeView<'static props.into_iter().map(|(n, p)| (n, p.into())), )?; self.graph - .internal_add_node_metadata(self.node, &properties) + .internal_add_node_metadata(self.node, properties) .map_err(into_graph_err)?; Ok(()) } @@ -447,7 +447,7 @@ impl NodeView<'static .inner()) })?; self.graph - .internal_update_node_metadata(self.node, &properties) + .internal_update_node_metadata(self.node, properties) .map_err(into_graph_err)?; Ok(()) } From bbdf23c556e611074c78b36fa12fa1e872792c66 Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Thu, 21 Aug 2025 18:44:25 +0200 Subject: [PATCH 133/321] ignore string dedupe test for now --- raphtory/src/db/graph/node.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/raphtory/src/db/graph/node.rs b/raphtory/src/db/graph/node.rs index 6bf937e6b7..1b45a23217 100644 --- a/raphtory/src/db/graph/node.rs +++ b/raphtory/src/db/graph/node.rs @@ -570,6 +570,7 @@ mod node_test { } #[test] + #[ignore] // likely we don't want to handle it globally like this anymore, maybe we should introduce an explicit categorical property type? fn test_string_deduplication() { let g = Graph::new(); let v1 = g From 85562871b1d679866610eaf22377d2ef577b3621 Mon Sep 17 00:00:00 2001 From: Fadhil Abubaker Date: Fri, 22 Aug 2025 05:32:20 -0400 Subject: [PATCH 134/321] Switch node/edge resolution to be sequential when loading from dataframes (#2240) * Formatting * Rename NODE_ID -> NODE_ID_COL * Switch node resolution to sequential * Check in schema.graphql * Switch edge resolution to sequential --- .../test_graph_nodes_property_filter.py | 2 +- raphtory/src/io/arrow/df_loaders.rs | 117 ++++++++++-------- raphtory/src/io/arrow/layer_col.rs | 17 +++ raphtory/src/serialise/parquet/mod.rs | 10 +- raphtory/src/serialise/parquet/model.rs | 6 +- raphtory/src/serialise/parquet/nodes.rs | 10 +- 6 files changed, 96 insertions(+), 66 deletions(-) diff --git a/python/tests/test_base_install/test_graphql/test_filters/test_graph_nodes_property_filter.py b/python/tests/test_base_install/test_graphql/test_filters/test_graph_nodes_property_filter.py index f8b86b09d7..54b0d9dee2 100644 --- a/python/tests/test_base_install/test_graphql/test_filters/test_graph_nodes_property_filter.py +++ b/python/tests/test_base_install/test_graphql/test_filters/test_graph_nodes_property_filter.py @@ -807,7 +807,7 @@ def test_graph_node_not_property_filter(graph): graph(path: "g") { nodeFilter ( filter: { - not: + not: { property: { name: "prop5" diff --git a/raphtory/src/io/arrow/df_loaders.rs b/raphtory/src/io/arrow/df_loaders.rs index c4cd0ae0b5..12b8beaa36 100644 --- a/raphtory/src/io/arrow/df_loaders.rs +++ b/raphtory/src/io/arrow/df_loaders.rs @@ -101,10 +101,8 @@ pub(crate) fn load_nodes_from_df< let mut node_type_col_resolved = vec![]; let mut write_locked_graph = graph.write_lock().map_err(into_graph_err)?; + let mut start_id = session.reserve_event_ids(df_view.num_rows).map_err(into_graph_err)?; - let mut start_id = session - .reserve_event_ids(df_view.num_rows) - .map_err(into_graph_err)?; for chunk in df_view.chunks { let df = chunk?; let prop_cols = combine_properties(properties, &properties_indices, &df, |key, dtype| { @@ -125,23 +123,23 @@ pub(crate) fn load_nodes_from_df< node_col_resolved.resize_with(df.len(), Default::default); node_type_col_resolved.resize_with(df.len(), Default::default); - node_col - .par_iter() - .zip(node_col_resolved.par_iter_mut()) - .zip(node_type_col.par_iter()) - .zip(node_type_col_resolved.par_iter_mut()) - .try_for_each(|(((gid, resolved), node_type), node_type_resolved)| { - let gid = gid.ok_or(LoadError::FatalError)?; - - let (vid, res_node_type) = write_locked_graph - .graph() - .resolve_node_and_type(gid.as_node_ref(), node_type) - .map_err(|_| LoadError::FatalError)?; - *resolved = vid; - *node_type_resolved = res_node_type; - - Ok::<(), LoadError>(()) - })?; + // TODO: Using parallel iterators results in a 5x speedup, but + // needs to be implemented such that node VID order is preserved. + // See: https://github.com/Pometry/pometry-storage/issues/81 + for (((gid, resolved), node_type), node_type_resolved) in node_col + .iter() + .zip(node_col_resolved.iter_mut()) + .zip(node_type_col.iter()) + .zip(node_type_col_resolved.iter_mut()) + { + let (vid, res_node_type) = write_locked_graph + .graph() + .resolve_node_and_type(gid.as_node_ref(), node_type) + .map_err(|_| LoadError::FatalError)?; + + *resolved = vid; + *node_type_resolved = res_node_type; + } let node_stats = write_locked_graph.node_stats().clone(); let update_time = |time: TimeIndexEntry| { @@ -274,6 +272,7 @@ pub(crate) fn load_edges_from_df< // } let num_nodes = AtomicUsize::new(write_locked_graph.graph().internal_num_nodes()); + for chunk in chunks { let df = chunk?; let prop_cols = combine_properties(properties, &properties_indices, &df, |key, dtype| { @@ -368,7 +367,6 @@ pub(crate) fn load_edges_from_df< write_locked_graph.resize_chunks_to_num_nodes(num_nodes.load(Ordering::Relaxed)); - // resolve all the edges eid_col_resolved.resize_with(df.len(), Default::default); eids_exist.resize_with(df.len(), Default::default); let eid_col_shared = atomic_usize_from_mut_slice(cast_slice_mut(&mut eid_col_resolved)); @@ -380,9 +378,10 @@ pub(crate) fn load_edges_from_df< let mut per_segment_edge_count = Vec::with_capacity(write_locked_graph.nodes.len()); per_segment_edge_count.resize_with(write_locked_graph.nodes.len(), || AtomicUsize::new(0)); + // Generate all edge_ids + add outbound edges write_locked_graph .nodes - .par_iter_mut() + .iter_mut() // TODO: change to par_iter_mut but preserve edge_id order .enumerate() .for_each(|(page_id, locked_page)| { for (row, ((((src, src_gid), dst), time), layer)) in src_col_resolved @@ -397,12 +396,14 @@ pub(crate) fn load_edges_from_df< let t = TimeIndexEntry(time, start_idx + row); let mut writer = locked_page.writer(); writer.store_node_id(src_pos, 0, src_gid, 0); + let edge_id = if let Some(edge_id) = writer.get_out_edge(src_pos, *dst, 0) { eid_col_shared[row].store(edge_id.0, Ordering::Relaxed); eids_exist[row].store(true, Ordering::Relaxed); edge_id } else { let edge_id = EID(next_edge_id()); + writer.add_static_outbound_edge(src_pos, *dst, edge_id, 0); eid_col_shared[row].store(edge_id.0, Ordering::Relaxed); eids_exist[row].store(false, Ordering::Relaxed); @@ -424,8 +425,8 @@ pub(crate) fn load_edges_from_df< write_locked_graph.resize_chunks_to_num_edges(num_edges.load(Ordering::Relaxed)); rayon::scope(|sc| { + // Add inbound edges sc.spawn(|_| { - // link the destinations write_locked_graph .nodes .par_iter_mut() @@ -442,6 +443,7 @@ pub(crate) fn load_edges_from_df< if let Some(dst_pos) = shard.resolve_pos(*dst) { let t = TimeIndexEntry(time, start_idx + row); let mut writer = shard.writer(); + writer.store_node_id(dst_pos, 0, dst_gid, 0); writer.add_static_inbound_edge(dst_pos, *src, *eid, 0); writer.add_inbound_edge( @@ -451,45 +453,51 @@ pub(crate) fn load_edges_from_df< eid.with_layer(*layer), 0, ); + per_segment_edge_count[page_id].fetch_add(1, Ordering::Relaxed); } } }); }); + // Add temporal & constant properties to edges sc.spawn(|_| { - write_locked_graph.edges.par_iter_mut().for_each(|shard| { - let mut t_props = vec![]; - let mut c_props = vec![]; - for (idx, (((((src, dst), time), eid), layer), exists)) in src_col_resolved - .iter() - .zip(dst_col_resolved.iter()) - .zip(time_col.iter()) - .zip(eid_col_resolved.iter()) - .zip(layer_col_resolved.iter()) - .zip( - eids_exist - .iter() - .map(|exists| exists.load(Ordering::Relaxed)), - ) - .enumerate() - { - if let Some(eid_pos) = shard.resolve_pos(*eid) { - let t = TimeIndexEntry(time, start_idx + idx); - let mut writer = shard.writer(); - - t_props.clear(); - t_props.extend(prop_cols.iter_row(idx)); - - c_props.clear(); - c_props.extend(metadata_cols.iter_row(idx)); - c_props.extend_from_slice(&shared_metadata); - - writer.add_static_edge(Some(eid_pos), *src, *dst, 0, Some(exists)); - writer.update_c_props(eid_pos, *src, *dst, *layer, c_props.drain(..)); - writer.add_edge(t, eid_pos, *src, *dst, t_props.drain(..), *layer, 0); + write_locked_graph + .edges + .par_iter_mut() + .for_each(|shard| { + let mut t_props = vec![]; + let mut c_props = vec![]; + + for (idx, (((((src, dst), time), eid), layer), exists)) in src_col_resolved + .iter() + .zip(dst_col_resolved.iter()) + .zip(time_col.iter()) + .zip(eid_col_resolved.iter()) + .zip(layer_col_resolved.iter()) + .zip( + eids_exist + .iter() + .map(|exists| exists.load(Ordering::Relaxed)), + ) + .enumerate() + { + if let Some(eid_pos) = shard.resolve_pos(*eid) { + let t = TimeIndexEntry(time, start_idx + idx); + let mut writer = shard.writer(); + + t_props.clear(); + t_props.extend(prop_cols.iter_row(idx)); + + c_props.clear(); + c_props.extend(metadata_cols.iter_row(idx)); + c_props.extend_from_slice(&shared_metadata); + + writer.add_static_edge(Some(eid_pos), *src, *dst, 0, Some(exists)); + writer.update_c_props(eid_pos, *src, *dst, *layer, c_props.drain(..)); + writer.add_edge(t, eid_pos, *src, *dst, t_props.drain(..), *layer, 0); + } } - } }); }); }); @@ -702,6 +710,7 @@ pub(crate) fn load_node_props_from_df< writer.update_c_props(mut_node, 0, c_props.drain(..), 0); }; } + Ok::<_, GraphError>(()) })?; diff --git a/raphtory/src/io/arrow/layer_col.rs b/raphtory/src/io/arrow/layer_col.rs index 8adb65b4d4..8a0541d738 100644 --- a/raphtory/src/io/arrow/layer_col.rs +++ b/raphtory/src/io/arrow/layer_col.rs @@ -46,6 +46,23 @@ impl<'a> LayerCol<'a> { } } + pub fn iter(self) -> impl Iterator> { + match self { + LayerCol::Name { name, len } => { + LayerColVariants::Name((0..len).map(move |_| name)) + } + LayerCol::Utf8 { col } => { + LayerColVariants::Utf8((0..col.len()).map(|i| col.get(i))) + } + LayerCol::LargeUtf8 { col } => { + LayerColVariants::LargeUtf8((0..col.len()).map(|i| col.get(i))) + } + LayerCol::Utf8View { col } => { + LayerColVariants::Utf8View((0..col.len()).map(|i| col.get(i))) + } + } + } + pub fn resolve( self, graph: &(impl AdditionOps + Send + Sync), diff --git a/raphtory/src/serialise/parquet/mod.rs b/raphtory/src/serialise/parquet/mod.rs index 29f32e8bc3..7187faea94 100644 --- a/raphtory/src/serialise/parquet/mod.rs +++ b/raphtory/src/serialise/parquet/mod.rs @@ -58,7 +58,7 @@ pub trait ParquetDecoder { Self: Sized; } -const NODE_ID: &str = "rap_node_id"; +const NODE_ID_COL: &str = "rap_node_id"; const TYPE_COL: &str = "rap_node_type"; const TIME_COL: &str = "rap_time"; const SRC_COL: &str = "rap_src"; @@ -327,7 +327,7 @@ fn decode_graph_storage( let t_node_path = path.as_ref().join(NODES_T_PATH); if std::fs::exists(&t_node_path)? { - let exclude = vec![NODE_ID, TIME_COL, TYPE_COL]; + let exclude = vec![NODE_ID_COL, TIME_COL, TYPE_COL]; let (t_prop_columns, _) = collect_prop_columns(&t_node_path, &exclude)?; let t_prop_columns = t_prop_columns .iter() @@ -338,7 +338,7 @@ fn decode_graph_storage( &g, &t_node_path, TIME_COL, - NODE_ID, + NODE_ID_COL, None, Some(TYPE_COL), &t_prop_columns, @@ -349,7 +349,7 @@ fn decode_graph_storage( let c_node_path = path.as_ref().join(NODES_C_PATH); if std::fs::exists(&c_node_path)? { - let exclude = vec![NODE_ID, TYPE_COL]; + let exclude = vec![NODE_ID_COL, TYPE_COL]; let (c_prop_columns, _) = collect_prop_columns(&c_node_path, &exclude)?; let c_prop_columns = c_prop_columns .iter() @@ -359,7 +359,7 @@ fn decode_graph_storage( load_node_props_from_parquet( &g, &c_node_path, - NODE_ID, + NODE_ID_COL, None, Some(TYPE_COL), &c_prop_columns, diff --git a/raphtory/src/serialise/parquet/model.rs b/raphtory/src/serialise/parquet/model.rs index b3fa8b15e4..dcba780f93 100644 --- a/raphtory/src/serialise/parquet/model.rs +++ b/raphtory/src/serialise/parquet/model.rs @@ -1,4 +1,4 @@ -use super::{Prop, DST_COL, LAYER_COL, NODE_ID, SRC_COL, TIME_COL, TYPE_COL}; +use super::{Prop, DST_COL, LAYER_COL, NODE_ID_COL, SRC_COL, TIME_COL, TYPE_COL}; use crate::{ db::{ api::view::StaticGraphViewOps, @@ -167,7 +167,7 @@ impl<'a> Serialize for ParquetTNode<'a> { { let mut state = serializer.serialize_map(None)?; - state.serialize_entry(NODE_ID, &ParquetGID(self.node.id()))?; + state.serialize_entry(NODE_ID_COL, &ParquetGID(self.node.id()))?; state.serialize_entry(TIME_COL, &self.t.0)?; state.serialize_entry(TYPE_COL, &self.node.node_type())?; @@ -190,7 +190,7 @@ impl<'a> Serialize for ParquetCNode<'a> { { let mut state = serializer.serialize_map(None)?; - state.serialize_entry(NODE_ID, &ParquetGID(self.node.id()))?; + state.serialize_entry(NODE_ID_COL, &ParquetGID(self.node.id()))?; state.serialize_entry(TYPE_COL, &self.node.node_type())?; for (name, prop) in self.node.metadata().iter_filtered() { diff --git a/raphtory/src/serialise/parquet/nodes.rs b/raphtory/src/serialise/parquet/nodes.rs index 5131541745..7650fc5d3c 100644 --- a/raphtory/src/serialise/parquet/nodes.rs +++ b/raphtory/src/serialise/parquet/nodes.rs @@ -4,7 +4,7 @@ use crate::{ errors::GraphError, serialise::parquet::{ model::{ParquetCNode, ParquetTNode}, - run_encode, NODES_C_PATH, NODES_T_PATH, NODE_ID, TIME_COL, TYPE_COL, + run_encode, NODES_C_PATH, NODES_T_PATH, NODE_ID_COL, TIME_COL, TYPE_COL, }, }; use arrow_schema::{DataType, Field}; @@ -25,7 +25,7 @@ pub(crate) fn encode_nodes_tprop( NODES_T_PATH, |id_type| { vec![ - Field::new(NODE_ID, id_type.clone(), false), + Field::new(NODE_ID_COL, id_type.clone(), false), Field::new(TIME_COL, DataType::Int64, false), Field::new(TYPE_COL, DataType::Utf8, true), ] @@ -78,7 +78,7 @@ pub(crate) fn encode_nodes_cprop( NODES_C_PATH, |id_type| { vec![ - Field::new(NODE_ID, id_type.clone(), false), + Field::new(NODE_ID_COL, id_type.clone(), false), Field::new(TYPE_COL, DataType::Utf8, true), ] }, @@ -93,13 +93,17 @@ pub(crate) fn encode_nodes_cprop( .chunks(row_group_size) .into_iter() .map(|chunk| chunk.collect_vec()) + + // scope for the decoder { decoder.serialize(&node_rows)?; + if let Some(rb) = decoder.flush()? { writer.write(&rb)?; writer.flush()?; } } + Ok(()) }, ) From df48d8c3fa2d75cbaae2e5059ba29c352f2f93bb Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Fri, 22 Aug 2025 12:02:36 +0200 Subject: [PATCH 135/321] remove all the old dis storage code --- db4-storage/src/pages/edge_page/writer.rs | 3 +- db4-storage/src/properties/mod.rs | 1 - db4-storage/src/segments/edge.rs | 18 +- raphtory-api/Cargo.toml | 2 +- .../entities/properties/prop/prop_type.rs | 2 +- raphtory-benchmark/Cargo.toml | 5 - raphtory-benchmark/benches/arrow_algobench.rs | 181 ----- raphtory-core/src/storage/lazy_vec.rs | 2 +- raphtory-graphql/src/data.rs | 81 -- raphtory-graphql/src/graph.rs | 26 +- raphtory-graphql/src/lib.rs | 76 -- raphtory-graphql/src/model/mod.rs | 7 - raphtory-graphql/src/url_encode.rs | 4 +- raphtory-storage/src/core_ops.rs | 2 - raphtory-storage/src/disk/graph_impl/mod.rs | 4 - .../src/disk/graph_impl/prop_conversion.rs | 212 ------ raphtory-storage/src/disk/mod.rs | 704 ------------------ .../src/disk/storage_interface/edge.rs | 121 --- .../src/disk/storage_interface/edges.rs | 89 --- .../src/disk/storage_interface/edges_ref.rs | 68 -- .../src/disk/storage_interface/mod.rs | 6 - .../src/disk/storage_interface/node.rs | 354 --------- .../src/disk/storage_interface/nodes.rs | 23 - .../src/disk/storage_interface/nodes_ref.rs | 33 - .../src/graph/edges/edge_entry.rs | 7 - .../src/graph/edges/edge_storage_ops.rs | 22 +- raphtory-storage/src/graph/graph.rs | 72 +- .../src/graph/nodes/node_entry.rs | 4 - raphtory-storage/src/graph/nodes/node_ref.rs | 9 - raphtory-storage/src/graph/nodes/nodes.rs | 3 - raphtory-storage/src/graph/nodes/nodes_ref.rs | 11 - .../src/graph/variants/storage_variants2.rs | 41 +- .../src/graph/variants/storage_variants3.rs | 24 +- raphtory-storage/src/layer_ops.rs | 4 - raphtory-storage/src/lib.rs | 2 - .../algorithms/dynamics/temporal/epidemics.rs | 34 - .../storage/graph/storage_ops/disk_storage.rs | 230 ------ .../db/api/storage/graph/storage_ops/mod.rs | 2 - raphtory/src/db/graph/assertions.rs | 31 +- raphtory/src/db/graph/views/deletion_graph.rs | 3 +- raphtory/src/db/graph/views/window_graph.rs | 89 --- raphtory/src/errors.rs | 6 - raphtory/src/io/arrow/df_loaders.rs | 168 ----- raphtory/src/io/parquet_loaders.rs | 47 +- raphtory/src/lib.rs | 20 - raphtory/src/python/graph/graph.rs | 12 - .../src/python/graph/graph_with_deletions.rs | 5 - raphtory/src/python/graph/mod.rs | 2 - raphtory/src/python/packages/algorithms.rs | 11 - raphtory/src/python/packages/base_modules.rs | 7 - raphtory/src/serialise/mod.rs | 18 +- 51 files changed, 35 insertions(+), 2873 deletions(-) delete mode 100644 raphtory-benchmark/benches/arrow_algobench.rs delete mode 100644 raphtory-storage/src/disk/graph_impl/mod.rs delete mode 100644 raphtory-storage/src/disk/graph_impl/prop_conversion.rs delete mode 100644 raphtory-storage/src/disk/mod.rs delete mode 100644 raphtory-storage/src/disk/storage_interface/edge.rs delete mode 100644 raphtory-storage/src/disk/storage_interface/edges.rs delete mode 100644 raphtory-storage/src/disk/storage_interface/edges_ref.rs delete mode 100644 raphtory-storage/src/disk/storage_interface/mod.rs delete mode 100644 raphtory-storage/src/disk/storage_interface/node.rs delete mode 100644 raphtory-storage/src/disk/storage_interface/nodes.rs delete mode 100644 raphtory-storage/src/disk/storage_interface/nodes_ref.rs diff --git a/db4-storage/src/pages/edge_page/writer.rs b/db4-storage/src/pages/edge_page/writer.rs index 1a314f1b85..0e1f47df18 100644 --- a/db4-storage/src/pages/edge_page/writer.rs +++ b/db4-storage/src/pages/edge_page/writer.rs @@ -1,11 +1,10 @@ -use std::{borrow::Borrow, ops::DerefMut}; - use crate::{ LocalPOS, api::edges::EdgeSegmentOps, error::StorageError, pages::layer_counter::GraphStats, segments::edge::MemEdgeSegment, }; use raphtory_api::core::entities::{VID, properties::prop::Prop}; use raphtory_core::storage::timeindex::AsTime; +use std::ops::DerefMut; pub struct EdgeWriter< 'a, diff --git a/db4-storage/src/properties/mod.rs b/db4-storage/src/properties/mod.rs index c56aadfdd4..27576d9eb0 100644 --- a/db4-storage/src/properties/mod.rs +++ b/db4-storage/src/properties/mod.rs @@ -12,7 +12,6 @@ use raphtory_core::{ }, storage::{PropColumn, TColumns, timeindex::TimeIndexEntry}, }; -use std::borrow::Borrow; pub mod props_meta_writer; diff --git a/db4-storage/src/segments/edge.rs b/db4-storage/src/segments/edge.rs index 9254d24fdf..20eef5b4c8 100644 --- a/db4-storage/src/segments/edge.rs +++ b/db4-storage/src/segments/edge.rs @@ -1,12 +1,3 @@ -use std::{ - borrow::Borrow, - ops::{Deref, DerefMut}, - sync::{ - Arc, - atomic::{self, AtomicUsize}, - }, -}; - use crate::{ LocalPOS, api::edges::{EdgeSegmentOps, LockedESegment}, @@ -22,10 +13,17 @@ use raphtory_api::core::entities::{ properties::{meta::Meta, prop::Prop}, }; use raphtory_core::{ - entities::{LayerIds, properties::props::MetadataError}, + entities::LayerIds, storage::timeindex::{AsTime, TimeIndexEntry}, }; use rayon::prelude::*; +use std::{ + ops::{Deref, DerefMut}, + sync::{ + Arc, + atomic::{self, AtomicUsize}, + }, +}; use super::{HasRow, SegmentContainer, edge_entry::MemEdgeEntry}; diff --git a/raphtory-api/Cargo.toml b/raphtory-api/Cargo.toml index 2e68561651..01cbf20dbe 100644 --- a/raphtory-api/Cargo.toml +++ b/raphtory-api/Cargo.toml @@ -55,7 +55,7 @@ python = [ "dep:pyo3", "dep:pyo3-arrow", "dep:display-error-chain" ] -storage = [ +polars = [ "dep:polars-arrow", ] diff --git a/raphtory-api/src/core/entities/properties/prop/prop_type.rs b/raphtory-api/src/core/entities/properties/prop/prop_type.rs index 9b78c08de2..defe6d0765 100644 --- a/raphtory-api/src/core/entities/properties/prop/prop_type.rs +++ b/raphtory-api/src/core/entities/properties/prop/prop_type.rs @@ -155,7 +155,7 @@ impl PropType { } } -#[cfg(feature = "storage")] +#[cfg(feature = "polars")] mod storage { use crate::core::entities::properties::prop::PropType; use polars_arrow::datatypes::ArrowDataType as PolarsDataType; diff --git a/raphtory-benchmark/Cargo.toml b/raphtory-benchmark/Cargo.toml index aaca452940..cd5260cb87 100644 --- a/raphtory-benchmark/Cargo.toml +++ b/raphtory-benchmark/Cargo.toml @@ -66,11 +66,6 @@ harness = false name = "proto_decode" harness = false -[[bench]] -name = "arrow_algobench" -harness = false -required-features = ["storage"] - [[bench]] name = "search_bench" harness = false diff --git a/raphtory-benchmark/benches/arrow_algobench.rs b/raphtory-benchmark/benches/arrow_algobench.rs deleted file mode 100644 index c50db642bf..0000000000 --- a/raphtory-benchmark/benches/arrow_algobench.rs +++ /dev/null @@ -1,181 +0,0 @@ -use criterion::{criterion_group, criterion_main}; - -#[cfg(feature = "storage")] -pub mod arrow_bench { - use criterion::{black_box, BenchmarkId, Criterion, SamplingMode}; - use raphtory::{ - algorithms::{ - centrality::pagerank::unweighted_page_rank, - components::weakly_connected_components, - metrics::clustering_coefficient::{ - global_clustering_coefficient::global_clustering_coefficient, - local_clustering_coefficient::local_clustering_coefficient, - }, - motifs::local_triangle_count::local_triangle_count, - }, - graphgen::random_attachment::random_attachment, - prelude::*, - }; - use raphtory_benchmark::common::bench; - use rayon::prelude::*; - use tempfile::TempDir; - - pub fn local_triangle_count_analysis(c: &mut Criterion) { - let mut group = c.benchmark_group("local_triangle_count"); - group.sample_size(10); - bench(&mut group, "local_triangle_count", None, |b| { - let g = raphtory::graph_loader::lotr_graph::lotr_graph(); - let test_dir = TempDir::new().unwrap(); - let g = g.persist_as_disk_graph(test_dir.path()).unwrap(); - let windowed_graph = g.window(i64::MIN, i64::MAX); - - b.iter(|| { - let node_ids = windowed_graph.nodes().collect(); - - node_ids.into_par_iter().for_each(|v| { - local_triangle_count(&windowed_graph, v).unwrap(); - }); - }) - }); - - group.finish(); - } - - pub fn local_clustering_coefficient_analysis(c: &mut Criterion) { - let mut group = c.benchmark_group("local_clustering_coefficient"); - - bench(&mut group, "local_clustering_coefficient", None, |b| { - let g: Graph = Graph::new(); - - let vs = vec![ - (1, 2, 1), - (1, 3, 2), - (1, 4, 3), - (3, 1, 4), - (3, 4, 5), - (3, 5, 6), - (4, 5, 7), - (5, 6, 8), - (5, 8, 9), - (7, 5, 10), - (8, 5, 11), - (1, 9, 12), - (9, 1, 13), - (6, 3, 14), - (4, 8, 15), - (8, 3, 16), - (5, 10, 17), - (10, 5, 18), - (10, 8, 19), - (1, 11, 20), - (11, 1, 21), - (9, 11, 22), - (11, 9, 23), - ]; - - for (src, dst, t) in &vs { - g.add_edge(*t, *src, *dst, NO_PROPS, None).unwrap(); - } - - let test_dir = TempDir::new().unwrap(); - let g = g.persist_as_disk_graph(test_dir.path()).unwrap(); - - let windowed_graph = g.window(0, 5); - b.iter(|| local_clustering_coefficient(&windowed_graph, 1)) - }); - - group.finish(); - } - - pub fn graphgen_large_clustering_coeff(c: &mut Criterion) { - let mut group = c.benchmark_group("graphgen_large_clustering_coeff"); - // generate graph - let graph = Graph::new(); - let seed: [u8; 32] = [1; 32]; - random_attachment(&graph, 500000, 4, Some(seed)); - - let test_dir = TempDir::new().unwrap(); - let graph = graph.persist_as_disk_graph(test_dir.path()).unwrap(); - - group.sampling_mode(SamplingMode::Flat); - group.measurement_time(std::time::Duration::from_secs(60)); - group.sample_size(10); - group.bench_with_input( - BenchmarkId::new("graphgen_large_clustering_coeff", &graph), - &graph, - |b, graph| { - b.iter(|| { - let result = global_clustering_coefficient(graph); - black_box(result); - }); - }, - ); - group.finish() - } - - pub fn graphgen_large_pagerank(c: &mut Criterion) { - let mut group = c.benchmark_group("graphgen_large_pagerank"); - // generate graph - let graph = Graph::new(); - let seed: [u8; 32] = [1; 32]; - random_attachment(&graph, 500000, 4, Some(seed)); - - let test_dir = TempDir::new().unwrap(); - let graph = graph.persist_as_disk_graph(test_dir.path()).unwrap(); - group.sampling_mode(SamplingMode::Flat); - group.measurement_time(std::time::Duration::from_secs(20)); - group.sample_size(10); - group.bench_with_input( - BenchmarkId::new("graphgen_large_pagerank", &graph), - &graph, - |b, graph| { - b.iter(|| { - let result = unweighted_page_rank(graph, Some(100), None, None, true, None); - black_box(result); - }); - }, - ); - group.finish() - } - - pub fn graphgen_large_concomp(c: &mut Criterion) { - let mut group = c.benchmark_group("graphgen_large_concomp"); - // generate graph - let graph = Graph::new(); - let seed: [u8; 32] = [1; 32]; - random_attachment(&graph, 500000, 4, Some(seed)); - let test_dir = TempDir::new().unwrap(); - let graph = graph.persist_as_disk_graph(test_dir.path()).unwrap(); - - group.sampling_mode(SamplingMode::Flat); - group.measurement_time(std::time::Duration::from_secs(60)); - group.sample_size(10); - group.bench_with_input( - BenchmarkId::new("graphgen_large_concomp", &graph), - &graph, - |b, graph| { - b.iter(|| { - let result = weakly_connected_components(graph); - black_box(result); - }); - }, - ); - group.finish() - } -} - -#[cfg(feature = "storage")] -pub use arrow_bench::*; - -#[cfg(feature = "storage")] -criterion_group!( - benches, - local_triangle_count_analysis, - local_clustering_coefficient_analysis, - graphgen_large_clustering_coeff, - graphgen_large_pagerank, - graphgen_large_concomp, -); - -#[cfg(feature = "storage")] -criterion_main!(benches); diff --git a/raphtory-core/src/storage/lazy_vec.rs b/raphtory-core/src/storage/lazy_vec.rs index 5d6c8f4935..0673dd2772 100644 --- a/raphtory-core/src/storage/lazy_vec.rs +++ b/raphtory-core/src/storage/lazy_vec.rs @@ -443,7 +443,7 @@ mod lazy_vec_tests { #[test] fn check_fails_if_present() { - let mut vec = LazyVec::from(5, 55); + let vec = LazyVec::from(5, 55); let result = vec.check(5, &555); assert_eq!(result, Err(IllegalSet::new(5, 55, 555))) } diff --git a/raphtory-graphql/src/data.rs b/raphtory-graphql/src/data.rs index dbcaa30c6a..a7ad0c542d 100644 --- a/raphtory-graphql/src/data.rs +++ b/raphtory-graphql/src/data.rs @@ -234,26 +234,6 @@ pub(crate) mod data_tests { use std::{collections::HashMap, fs, fs::File, io, path::Path, time::Duration}; use tokio::time::sleep; - #[cfg(feature = "storage")] - use raphtory_storage::{core_ops::CoreGraphOps, graph::graph::GraphStorage}; - - #[cfg(feature = "storage")] - fn copy_dir_recursive(source_dir: &Path, target_dir: &Path) -> Result<(), GraphError> { - fs::create_dir_all(target_dir)?; - for entry in fs::read_dir(source_dir)? { - let entry = entry?; - let entry_path = entry.path(); - let target_path = target_dir.join(entry.file_name()); - - if entry_path.is_dir() { - copy_dir_recursive(&entry_path, &target_path)?; - } else { - fs::copy(&entry_path, &target_path)?; - } - } - Ok(()) - } - // This function creates files that mimic disk graph for tests fn create_ipc_files_in_dir(dir_path: &Path) -> io::Result<()> { if !dir_path.exists() { @@ -283,65 +263,13 @@ pub(crate) mod data_tests { for (name, graph) in graphs.into_iter() { let data = Data::new(work_dir, &AppConfig::default()); let folder = ValidGraphFolder::try_from(data.work_dir, name)?; - - #[cfg(feature = "storage")] - if let GraphStorage::Disk(dg) = graph.core_graph() { - let disk_graph_path = dg.graph_dir(); - copy_dir_recursive(disk_graph_path, &folder.get_graph_path())?; - File::create(folder.get_meta_path())?; - } else { - graph.encode(folder)?; - } - - #[cfg(not(feature = "storage"))] graph.encode(folder)?; } Ok(()) } - #[tokio::test] - #[cfg(feature = "storage")] - async fn test_get_disk_graph_from_path() { - let tmp_graph_dir = tempfile::tempdir().unwrap(); - - let graph = Graph::new(); - graph - .add_edge(0, 1, 2, [("name", "test_e1")], None) - .unwrap(); - graph - .add_edge(0, 1, 3, [("name", "test_e2")], None) - .unwrap(); - - let base_path = tmp_graph_dir.path().to_owned(); - let graph_path = base_path.join("test_dg"); - fs::create_dir(&graph_path).unwrap(); - File::create(graph_path.join(".raph")).unwrap(); - let _ = DiskGraphStorage::from_graph(&graph, &graph_path.join("graph")).unwrap(); - - let data = Data::new(&base_path, &Default::default()); - let res = data.get_graph("test_dg").await.unwrap().0; - assert_eq!(res.graph.into_events().unwrap().count_edges(), 2); - - // Dir path doesn't exists - let res = data.get_graph("test_dg1").await; - assert!(res.is_err()); - if let Err(err) = res { - assert!(err.to_string().contains("Graph not found")); - } - - // Dir path exists but is not a disk graph path - // let tmp_graph_dir = tempfile::tempdir().unwrap(); - // let res = read_graph_from_path(base_path, ""); - let res = data.get_graph("").await; - assert!(res.is_err()); - if let Err(err) = res { - assert!(err.to_string().contains("Graph not found")); - } - } - #[tokio::test] async fn test_save_graphs_to_work_dir() { - let tmp_graph_dir = tempfile::tempdir().unwrap(); let tmp_work_dir = tempfile::tempdir().unwrap(); let graph = Graph::new(); @@ -353,21 +281,12 @@ pub(crate) mod data_tests { .add_edge(0, 1, 3, [("name", "test_e2")], None) .unwrap(); - #[cfg(feature = "storage")] - let graph2: MaterializedGraph = graph - .persist_as_disk_graph(tmp_graph_dir.path()) - .unwrap() - .into(); - let graph: MaterializedGraph = graph.into(); let mut graphs = HashMap::new(); graphs.insert("test_g".to_string(), graph); - #[cfg(feature = "storage")] - graphs.insert("test_dg".to_string(), graph2); - save_graphs_to_work_dir(tmp_work_dir.path(), &graphs).unwrap(); let data = Data::new(tmp_work_dir.path(), &Default::default()); diff --git a/raphtory-graphql/src/graph.rs b/raphtory-graphql/src/graph.rs index 8a0eb31e12..4468eb419c 100644 --- a/raphtory-graphql/src/graph.rs +++ b/raphtory-graphql/src/graph.rs @@ -22,9 +22,6 @@ use raphtory_storage::{ mutation::InheritMutationOps, }; -#[cfg(feature = "storage")] -use {raphtory::prelude::IntoGraph, raphtory_storage::disk::DiskGraphStorage}; - #[derive(Clone)] pub struct GraphWithVectors { pub graph: MaterializedGraph, @@ -71,8 +68,6 @@ impl GraphWithVectors { pub(crate) fn write_updates(&self) -> Result<(), GraphError> { match self.graph.core_graph() { GraphStorage::Mem(_) | GraphStorage::Unlocked(_) => self.graph.write_updates(), - #[cfg(feature = "storage")] - GraphStorage::Disk(_) => Ok(()), } } @@ -81,12 +76,7 @@ impl GraphWithVectors { cache: Option, create_index: bool, ) -> Result { - let graph_path = &folder.get_graph_path(); - let graph = if graph_path.is_dir() { - get_disk_graph_from_path(folder)? - } else { - MaterializedGraph::load_cached(folder.clone())? - }; + let graph = MaterializedGraph::load_cached(folder.clone())?; let vectors = cache.and_then(|cache| { VectorisedGraph::read_from_path(&folder.get_vectors_path(), graph.clone(), cache).ok() }); @@ -103,20 +93,6 @@ impl GraphWithVectors { } } -#[cfg(feature = "storage")] -fn get_disk_graph_from_path(path: &ExistingGraphFolder) -> Result { - let disk_graph = DiskGraphStorage::load_from_dir(&path.get_graph_path()) - .map_err(|e| GraphError::LoadFailure(e.to_string()))?; - let graph: MaterializedGraph = disk_graph.into_graph().into(); // TODO: We currently have no way to identify disk graphs as MaterializedGraphs - println!("Disk Graph loaded = {}", path.get_original_path().display()); - Ok(graph) -} - -#[cfg(not(feature = "storage"))] -fn get_disk_graph_from_path(path: &ExistingGraphFolder) -> Result { - Err(GraphError::GraphNotFound(path.to_error_path())) -} - impl Base for GraphWithVectors { type Base = MaterializedGraph; #[inline] diff --git a/raphtory-graphql/src/lib.rs b/raphtory-graphql/src/lib.rs index 55c99df880..de34686b0c 100644 --- a/raphtory-graphql/src/lib.rs +++ b/raphtory-graphql/src/lib.rs @@ -1394,82 +1394,6 @@ mod graphql_test { ); } - #[cfg(feature = "storage")] - #[tokio::test] - async fn test_disk_graph() { - let graph = Graph::new(); - graph.add_metadata([("name", "graph")]).unwrap(); - graph.add_node(1, 1, NO_PROPS, Some("a")).unwrap(); - graph.add_node(1, 2, NO_PROPS, Some("b")).unwrap(); - graph.add_node(1, 3, NO_PROPS, Some("b")).unwrap(); - graph.add_node(1, 4, NO_PROPS, Some("a")).unwrap(); - graph.add_node(1, 5, NO_PROPS, Some("c")).unwrap(); - graph.add_node(1, 6, NO_PROPS, Some("e")).unwrap(); - graph.add_edge(22, 1, 2, NO_PROPS, Some("a")).unwrap(); - graph.add_edge(22, 3, 2, NO_PROPS, Some("a")).unwrap(); - graph.add_edge(22, 2, 4, NO_PROPS, Some("a")).unwrap(); - graph.add_edge(22, 4, 5, NO_PROPS, Some("a")).unwrap(); - graph.add_edge(22, 4, 5, NO_PROPS, Some("a")).unwrap(); - graph.add_edge(22, 5, 6, NO_PROPS, Some("a")).unwrap(); - graph.add_edge(22, 3, 6, NO_PROPS, Some("a")).unwrap(); - - let tmp_work_dir = tempdir().unwrap(); - let tmp_work_dir = tmp_work_dir.path(); - - let disk_graph_path = tmp_work_dir.join("graph"); - fs::create_dir(&disk_graph_path).unwrap(); - fs::File::create(disk_graph_path.join(".raph")).unwrap(); - let _ = DiskGraphStorage::from_graph(&graph, disk_graph_path.join("graph")).unwrap(); - - let data = Data::new(&tmp_work_dir, &AppConfig::default()); - let schema = App::create_schema().data(data).finish().unwrap(); - - let req = r#" - { - graph(path: "graph") { - nodes { - list { - name - } - } - } - } - "#; - - let req = Request::new(req); - let res = schema.execute(req).await; - let data = res.data.into_json().unwrap(); - assert_eq!( - data, - json!({ - "graph": { - "nodes": { - "list": [ - { - "name": "1" - }, - { - "name": "2" - }, - { - "name": "3" - }, - { - "name": "4" - }, - { - "name": "5" - }, - { - "name": "6" - } - ] - } - } - }), - ); - } - #[tokio::test] async fn test_query_namespace() { let graph = Graph::new(); diff --git a/raphtory-graphql/src/model/mod.rs b/raphtory-graphql/src/model/mod.rs index 4201cf8e5d..5b31a1e48c 100644 --- a/raphtory-graphql/src/model/mod.rs +++ b/raphtory-graphql/src/model/mod.rs @@ -25,8 +25,6 @@ use raphtory::{ serialise::InternalStableDecode, version, }; -#[cfg(feature = "storage")] -use raphtory_storage::{core_ops::CoreGraphOps, graph::graph::GraphStorage}; use std::{ error::Error, fmt::{Display, Formatter}, @@ -209,11 +207,6 @@ impl Mut { // for the templates or if it needs to be vectorised at all let data = ctx.data_unchecked::(); let graph = data.get_graph(path).await?.0.graph; - - #[cfg(feature = "storage")] - if let GraphStorage::Disk(_) = graph.core_graph() { - return Err(GqlGraphError::ImmutableDiskGraph.into()); - } data.insert_graph(new_path, graph).await?; Ok(true) diff --git a/raphtory-graphql/src/url_encode.rs b/raphtory-graphql/src/url_encode.rs index f45fe890ce..4400fc9a68 100644 --- a/raphtory-graphql/src/url_encode.rs +++ b/raphtory-graphql/src/url_encode.rs @@ -1,12 +1,10 @@ -use std::io::{copy, Read, Write}; - use base64::{prelude::BASE64_URL_SAFE, DecodeError, Engine}; use raphtory::{ db::api::view::MaterializedGraph, errors::GraphError, prelude::{ParquetDecoder, ParquetEncoder}, - serialise::StableEncode, }; +use std::io::copy; use zip::write::SimpleFileOptions; #[derive(thiserror::Error, Debug)] diff --git a/raphtory-storage/src/core_ops.rs b/raphtory-storage/src/core_ops.rs index 3bab456c23..dc054168fc 100644 --- a/raphtory-storage/src/core_ops.rs +++ b/raphtory-storage/src/core_ops.rs @@ -39,8 +39,6 @@ pub trait CoreGraphOps: Send + Sync { // GraphStorage::Mem(LockedGraph { graph, .. }) | GraphStorage::Unlocked(graph) => { // graph.storage.num_shards() // } - // #[cfg(feature = "storage")] - // GraphStorage::Disk(_) => 1, // } // } diff --git a/raphtory-storage/src/disk/graph_impl/mod.rs b/raphtory-storage/src/disk/graph_impl/mod.rs deleted file mode 100644 index 72682ff61d..0000000000 --- a/raphtory-storage/src/disk/graph_impl/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -use pometry_storage::edge::Edge; -pub mod prop_conversion; - -pub type DiskEdge<'a> = Edge<'a>; diff --git a/raphtory-storage/src/disk/graph_impl/prop_conversion.rs b/raphtory-storage/src/disk/graph_impl/prop_conversion.rs deleted file mode 100644 index 9cae66aab8..0000000000 --- a/raphtory-storage/src/disk/graph_impl/prop_conversion.rs +++ /dev/null @@ -1,212 +0,0 @@ -use crate::{core_ops::CoreGraphOps, graph::nodes::node_storage_ops::NodeStorageOps}; -use itertools::Itertools; -use num_traits::ToPrimitive; -use polars_arrow::{ - array::{Array, BooleanArray, PrimitiveArray, Utf8Array}, - datatypes::{ArrowDataType, ArrowSchema, Field}, -}; -use pometry_storage::{ - properties::{node_ts, NodePropsBuilder, Properties}, - RAError, -}; -use raphtory_api::core::entities::{ - properties::{ - meta::PropMapper, - prop::{Prop, PropType, PropUnwrap}, - tprop::TPropOps, - }, - VID, -}; -use raphtory_core::utils::iter::GenLockedIter; -use std::path::Path; - -pub fn make_node_properties_from_graph( - graph: &G, - graph_dir: impl AsRef, -) -> Result, RAError> { - let graph_dir = graph_dir.as_ref(); - let n = graph.unfiltered_num_nodes(); - - let temporal_mapper = graph.node_meta().temporal_prop_mapper(); - let metadata_mapper = graph.node_meta().metadata_mapper(); - - let gs = graph.core_graph(); - - let temporal_prop_keys = temporal_mapper - .get_keys() - .iter() - .map(|s| s.to_string()) - .collect(); - - let metadata_keys = metadata_mapper - .get_keys() - .iter() - .map(|s| s.to_string()) - .collect(); - - let builder = NodePropsBuilder::new(n, graph_dir) - .with_timestamps(|vid| { - let node = gs.core_node(vid); - node.as_ref().temp_prop_rows().map(|(ts, _)| ts).collect() - }) - .with_metadata(metadata_keys, |prop_id, prop_key| { - let prop_type = metadata_mapper.get_dtype(prop_id).unwrap(); - let col = arrow_array_from_props( - (0..n).map(|vid| { - let node = gs.core_node(VID(vid)); - node.prop(prop_id) - }), - prop_type, - ); - col.map(|col| { - let dtype = col.data_type().clone(); - (Field::new(prop_key, dtype, true), col) - }) - }) - .with_properties(temporal_prop_keys, |prop_id, prop_key, ts, offsets| { - let prop_type = temporal_mapper.get_dtype(prop_id).unwrap(); - let col = arrow_array_from_props( - (0..n).flat_map(|vid| { - let ts = node_ts(VID(vid), offsets, ts); - let node = gs.core_node(VID(vid)); - let iter = - GenLockedIter::from(node, |node| Box::new(node.tprop(prop_id).iter())); - iter.merge_join_by(ts, |(t2, _), &t1| t2.cmp(t1)) - .map(|result| match result { - itertools::EitherOrBoth::Both((_, t_prop), _) => Some(t_prop), - _ => None, - }) - }), - prop_type, - ); - col.map(|col| { - let dtype = col.data_type().clone(); - (Field::new(prop_key, dtype, true), col) - }) - }); - - let props = builder.build()?; - Ok(props) -} - -/// Map iterator of prop values to array (returns None if all the props are None) -pub fn arrow_array_from_props( - props: impl Iterator>, - prop_type: PropType, -) -> Option> { - match prop_type { - PropType::Str => { - let array: Utf8Array = props.map(|prop| prop.into_str()).collect(); - (array.null_count() != array.len()).then_some(array.boxed()) - } - PropType::U8 => { - let array: PrimitiveArray = props.map(|prop| prop.into_u8()).collect(); - (array.null_count() != array.len()).then_some(array.boxed()) - } - PropType::U16 => { - let array: PrimitiveArray = props.map(|prop| prop.into_u16()).collect(); - (array.null_count() != array.len()).then_some(array.boxed()) - } - PropType::I32 => { - let array: PrimitiveArray = props.map(|prop| prop.into_i32()).collect(); - (array.null_count() != array.len()).then_some(array.boxed()) - } - PropType::I64 => { - let array: PrimitiveArray = props.map(|prop| prop.into_i64()).collect(); - (array.null_count() != array.len()).then_some(array.boxed()) - } - PropType::U32 => { - let array: PrimitiveArray = props.map(|prop| prop.into_u32()).collect(); - (array.null_count() != array.len()).then_some(array.boxed()) - } - PropType::U64 => { - let array: PrimitiveArray = props.map(|prop| prop.into_u64()).collect(); - (array.null_count() != array.len()).then_some(array.boxed()) - } - PropType::F32 => { - let array: PrimitiveArray = props.map(|prop| prop.into_f32()).collect(); - (array.null_count() != array.len()).then_some(array.boxed()) - } - PropType::F64 => { - let array: PrimitiveArray = props.map(|prop| prop.into_f64()).collect(); - (array.null_count() != array.len()).then_some(array.boxed()) - } - PropType::Bool => { - let array: BooleanArray = props.map(|prop| prop.into_bool()).collect(); - (array.null_count() != array.len()).then_some(array.boxed()) - } - PropType::Decimal { scale } => { - let array: PrimitiveArray = props - .map(|prop| { - prop.into_decimal().and_then(|d| { - let (int, _) = d.as_bigint_and_exponent(); - int.to_i128() - }) - }) - .collect(); - (array.null_count() != array.len()) - .then_some(array.to(ArrowDataType::Decimal(38, scale as usize)).boxed()) - } - PropType::Empty - | PropType::List(_) - | PropType::Map(_) - | PropType::NDTime - | PropType::Array(_) - | PropType::DTime => panic!("{prop_type:?} not supported as disk_graph property"), - } -} - -pub fn schema_from_prop_meta(prop_map: &PropMapper) -> ArrowSchema { - let time_field = Field::new("time", ArrowDataType::Int64, false); - let mut schema = vec![time_field]; - - for (id, key) in prop_map.get_keys().iter().enumerate() { - match prop_map.get_dtype(id).unwrap() { - PropType::Str => { - schema.push(Field::new(key, ArrowDataType::LargeUtf8, true)); - } - PropType::U8 => { - schema.push(Field::new(key, ArrowDataType::UInt8, true)); - } - PropType::U16 => { - schema.push(Field::new(key, ArrowDataType::UInt16, true)); - } - PropType::I32 => { - schema.push(Field::new(key, ArrowDataType::Int32, true)); - } - PropType::I64 => { - schema.push(Field::new(key, ArrowDataType::Int64, true)); - } - PropType::U32 => { - schema.push(Field::new(key, ArrowDataType::UInt32, true)); - } - PropType::U64 => { - schema.push(Field::new(key, ArrowDataType::UInt64, true)); - } - PropType::F32 => { - schema.push(Field::new(key, ArrowDataType::Float32, true)); - } - PropType::F64 => { - schema.push(Field::new(key, ArrowDataType::Float64, true)); - } - PropType::Bool => { - schema.push(Field::new(key, ArrowDataType::Boolean, true)); - } - PropType::Decimal { scale } => { - schema.push(Field::new( - key, - ArrowDataType::Decimal(38, scale as usize), - true, - )); - } - prop_type @ (PropType::Empty - | PropType::List(_) - | PropType::Map(_) - | PropType::NDTime - | PropType::Array(_) - | PropType::DTime) => panic!("{:?} not supported as disk_graph property", prop_type), - } - } - - ArrowSchema::from(schema) -} diff --git a/raphtory-storage/src/disk/mod.rs b/raphtory-storage/src/disk/mod.rs deleted file mode 100644 index c4dec412ff..0000000000 --- a/raphtory-storage/src/disk/mod.rs +++ /dev/null @@ -1,704 +0,0 @@ -use crate::{ - core_ops::CoreGraphOps, disk::graph_impl::prop_conversion::make_node_properties_from_graph, -}; -use polars_arrow::{ - array::{Array, PrimitiveArray, StructArray}, - datatypes::{ArrowDataType as DataType, Field}, -}; -use pometry_storage::{ - graph::TemporalGraph, graph_fragment::TempColGraphFragment, interop::GraphLike, - load::ExternalEdgeList, merge::merge_graph::merge_graphs, RAError, -}; -use raphtory_api::core::{ - entities::{properties::meta::Meta, Layer, LayerIds}, - storage::timeindex::AsTime, -}; -use raphtory_core::entities::{graph::tgraph::InvalidLayer, properties::graph_meta::GraphMeta}; -use rayon::iter::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator}; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use std::{ - fmt::{Display, Formatter}, - path::{Path, PathBuf}, - sync::Arc, -}; - -pub mod graph_impl; -pub mod storage_interface; - -pub type Time = i64; - -pub mod prelude { - pub use pometry_storage::chunked_array::array_ops::*; -} - -pub use pometry_storage as disk_storage; - -#[derive(Debug)] -pub struct ParquetLayerCols<'a> { - pub parquet_dir: &'a str, - pub layer: &'a str, - pub src_col: &'a str, - pub dst_col: &'a str, - pub time_col: &'a str, - pub exclude_edge_props: Vec<&'a str>, -} - -#[derive(Clone, Debug)] -pub struct DiskGraphStorage { - pub inner: Arc, - graph_props: Arc, -} - -impl Serialize for DiskGraphStorage { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let path = self.graph_dir(); - path.serialize(serializer) - } -} - -impl<'de> Deserialize<'de> for DiskGraphStorage { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let path = PathBuf::deserialize(deserializer)?; - let graph_result = DiskGraphStorage::load_from_dir(&path).map_err(|err| { - serde::de::Error::custom(format!("Failed to load Diskgraph: {:?}", err)) - })?; - Ok(graph_result) - } -} - -impl Display for DiskGraphStorage { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!( - f, - "Diskgraph(num_nodes={}, num_temporal_edges={}", - self.inner.num_nodes(), - self.inner.count_temporal_edges() - ) - } -} - -impl AsRef for DiskGraphStorage { - fn as_ref(&self) -> &TemporalGraph { - &self.inner - } -} - -impl DiskGraphStorage { - pub fn inner(&self) -> &Arc { - &self.inner - } - - pub fn graph_dir(&self) -> &Path { - self.inner.graph_dir() - } - - pub fn valid_layer_ids_from_names(&self, key: Layer) -> LayerIds { - match key { - Layer::All => LayerIds::All, - Layer::Default => LayerIds::One(0), - Layer::One(name) => self - .inner - .find_layer_id(&name) - .map(LayerIds::One) - .unwrap_or(LayerIds::None), - Layer::None => LayerIds::None, - Layer::Multiple(names) => { - let mut new_layers = names - .iter() - .filter_map(|name| self.inner.find_layer_id(name)) - .collect::>(); - - let num_layers = self.inner.num_layers(); - let num_new_layers = new_layers.len(); - if num_new_layers == 0 { - LayerIds::None - } else if num_new_layers == 1 { - LayerIds::One(new_layers[0]) - } else if num_new_layers == num_layers { - LayerIds::All - } else { - new_layers.sort_unstable(); - new_layers.dedup(); - LayerIds::Multiple(new_layers.into()) - } - } - } - } - - pub fn layer_ids_from_names(&self, key: Layer) -> Result { - match key { - Layer::All => Ok(LayerIds::All), - Layer::Default => Ok(LayerIds::One(0)), - Layer::One(name) => { - let id = self - .inner - .find_layer_id(&name) - .ok_or_else(|| InvalidLayer::new(name, self.inner.get_valid_layers()))?; - Ok(LayerIds::One(id)) - } - Layer::None => Ok(LayerIds::None), - Layer::Multiple(names) => { - let mut new_layers = names - .iter() - .map(|name| { - self.inner.find_layer_id(name).ok_or_else(|| { - InvalidLayer::new(name.clone(), self.inner.get_valid_layers()) - }) - }) - .collect::, _>>()?; - - let num_layers = self.inner.num_layers(); - let num_new_layers = new_layers.len(); - if num_new_layers == 0 { - Ok(LayerIds::None) - } else if num_new_layers == 1 { - Ok(LayerIds::One(new_layers[0])) - } else if num_new_layers == num_layers { - Ok(LayerIds::All) - } else { - new_layers.sort_unstable(); - new_layers.dedup(); - Ok(LayerIds::Multiple(new_layers.into())) - } - } - } - } - - pub fn make_simple_graph( - graph_dir: impl AsRef, - edges: &[(u64, u64, i64, f64)], - chunk_size: usize, - t_props_chunk_size: usize, - ) -> DiskGraphStorage { - // unzip into 4 vectors - let (src, (dst, (time, weight))): (Vec<_>, (Vec<_>, (Vec<_>, Vec<_>))) = edges - .iter() - .map(|(a, b, c, d)| (*a, (*b, (*c, *d)))) - .unzip(); - - let edge_lists = vec![StructArray::new( - DataType::Struct(vec![ - Field::new("src", DataType::UInt64, false), - Field::new("dst", DataType::UInt64, false), - Field::new("time", DataType::Int64, false), - Field::new("weight", DataType::Float64, false), - ]), - vec![ - PrimitiveArray::from_vec(src).boxed(), - PrimitiveArray::from_vec(dst).boxed(), - PrimitiveArray::from_vec(time).boxed(), - PrimitiveArray::from_vec(weight).boxed(), - ], - None, - )]; - DiskGraphStorage::load_from_edge_lists( - &edge_lists, - chunk_size, - t_props_chunk_size, - graph_dir.as_ref(), - 2, - 0, - 1, - ) - .expect("failed to create graph") - } - - /// Merge this graph with another `DiskGraph`. Note that both graphs should have nodes that are - /// sorted by their global ids or the resulting graph will be nonsense! - pub fn merge_by_sorted_gids( - &self, - other: &DiskGraphStorage, - new_graph_dir: impl AsRef, - ) -> Result { - let graph_dir = new_graph_dir.as_ref(); - let inner = merge_graphs(graph_dir, &self.inner, &other.inner)?; - Ok(DiskGraphStorage::new(inner)) - } - - pub fn new(inner_graph: TemporalGraph) -> Self { - let graph_meta = GraphMeta::new(); - - Self { - inner: Arc::new(inner_graph), - graph_props: Arc::new(graph_meta), - } - } - - pub fn from_graph + CoreGraphOps>( - graph: &G, - graph_dir: impl AsRef, - ) -> Result { - let inner_graph = TemporalGraph::from_graph(graph, graph_dir.as_ref(), || { - make_node_properties_from_graph(graph, graph_dir.as_ref()) - })?; - let mut storage = Self::new(inner_graph); - storage.graph_props = Arc::new(graph.graph_meta().deep_clone()); - Ok(storage) - } - - pub fn load_from_edge_lists( - edge_list: &[StructArray], - chunk_size: usize, - t_props_chunk_size: usize, - graph_dir: impl AsRef + Sync, - time_col_idx: usize, - src_col_idx: usize, - dst_col_idx: usize, - ) -> Result { - let inner = TemporalGraph::from_sorted_edge_list( - graph_dir, - src_col_idx, - dst_col_idx, - time_col_idx, - chunk_size, - t_props_chunk_size, - edge_list, - )?; - Ok(Self::new(inner)) - } - - pub fn load_from_dir(graph_dir: impl AsRef) -> Result { - let inner = TemporalGraph::new(graph_dir)?; - Ok(Self::new(inner)) - } - - pub fn load_from_parquets>( - graph_dir: P, - layer_parquet_cols: Vec, - node_properties: Option

, - chunk_size: usize, - t_props_chunk_size: usize, - num_threads: usize, - node_type_col: Option<&str>, - node_id_col: Option<&str>, - ) -> Result { - let edge_lists: Vec> = layer_parquet_cols - .into_iter() - .map( - |ParquetLayerCols { - parquet_dir, - layer, - src_col, - dst_col, - time_col, - exclude_edge_props, - }| { - ExternalEdgeList::new( - layer, - parquet_dir.as_ref(), - src_col, - dst_col, - time_col, - exclude_edge_props, - ) - .expect("Failed to load events") - }, - ) - .collect::>(); - - let t_graph = TemporalGraph::from_parquets( - num_threads, - chunk_size, - t_props_chunk_size, - graph_dir.as_ref(), - edge_lists, - &[], - node_properties.as_ref().map(|p| p.as_ref()), - node_type_col, - node_id_col, - )?; - Ok(Self::new(t_graph)) - } - - pub fn load_node_types_from_arrays( - &mut self, - arrays: impl IntoIterator, RAError>>, - chunk_size: usize, - ) -> Result<(), RAError> { - let inner = Arc::make_mut(&mut self.inner); - inner.load_node_types_from_chunks(arrays, chunk_size)?; - Ok(()) - } - - pub fn filtered_layers_par<'a>( - &'a self, - layer_ids: LayerIds, - ) -> impl ParallelIterator + 'a { - self.inner - .layers() - .par_iter() - .enumerate() - .filter(move |(l_id, _)| layer_ids.contains(l_id)) - .map(|(_, layer)| layer) - } - - pub fn filtered_layers_iter<'a>( - &'a self, - layer_ids: LayerIds, - ) -> impl Iterator + 'a { - self.inner - .layers() - .iter() - .enumerate() - .filter(move |(l_id, _)| layer_ids.contains(l_id)) - .map(|(_, layer)| layer) - } - - pub fn node_meta(&self) -> &Meta { - self.inner.node_meta() - } - - pub fn edge_meta(&self) -> &Meta { - self.inner.edge_meta() - } - - pub fn graph_meta(&self) -> &GraphMeta { - &self.graph_props - } -} - -#[cfg(test)] -mod test { - use itertools::Itertools; - use polars_arrow::{ - array::{PrimitiveArray, StructArray}, - datatypes::{ArrowDataType, ArrowSchema, Field}, - }; - use pometry_storage::{graph::TemporalGraph, RAError}; - use proptest::{prelude::*, sample::size_range}; - use raphtory_api::core::entities::{EID, VID}; - use std::path::Path; - use tempfile::TempDir; - - fn edges_sanity_node_list(edges: &[(u64, u64, i64)]) -> Vec { - edges - .iter() - .map(|(s, _, _)| *s) - .chain(edges.iter().map(|(_, d, _)| *d)) - .sorted() - .dedup() - .collect() - } - - pub fn edges_sanity_check_build_graph>( - test_dir: P, - edges: &[(u64, u64, i64)], - input_chunk_size: u64, - chunk_size: usize, - t_props_chunk_size: usize, - ) -> Result { - let chunks = edges - .iter() - .map(|(src, _, _)| *src) - .chunks(input_chunk_size as usize); - let srcs = chunks - .into_iter() - .map(|chunk| PrimitiveArray::from_vec(chunk.collect())); - let chunks = edges - .iter() - .map(|(_, dst, _)| *dst) - .chunks(input_chunk_size as usize); - let dsts = chunks - .into_iter() - .map(|chunk| PrimitiveArray::from_vec(chunk.collect())); - let chunks = edges - .iter() - .map(|(_, _, times)| *times) - .chunks(input_chunk_size as usize); - let times = chunks - .into_iter() - .map(|chunk| PrimitiveArray::from_vec(chunk.collect())); - - let schema = ArrowSchema::from(vec![ - Field::new("srcs", ArrowDataType::UInt64, false), - Field::new("dsts", ArrowDataType::UInt64, false), - Field::new("time", ArrowDataType::Int64, false), - ]); - - let triples = srcs - .zip(dsts) - .zip(times) - .map(move |((a, b), c)| { - StructArray::new( - ArrowDataType::Struct(schema.fields.clone()), - vec![a.boxed(), b.boxed(), c.boxed()], - None, - ) - }) - .collect::>(); - - TemporalGraph::from_sorted_edge_list( - test_dir.as_ref(), - 0, - 1, - 2, - chunk_size, - t_props_chunk_size, - &triples, - ) - } - - pub fn check_graph_sanity(edges: &[(u64, u64, i64)], nodes: &[u64], graph: &TemporalGraph) { - let actual_num_verts = nodes.len(); - let g_num_verts = graph.num_nodes(); - assert_eq!(actual_num_verts, g_num_verts); - assert!(graph - .edges_iter() - .map(|edge| (edge.src_id(), edge.dst_id())) - .all(|(VID(src), VID(dst))| src < g_num_verts && dst < g_num_verts)); - - for v in 0..g_num_verts { - let v = VID(v); - assert!(graph - .node(v, 0) - .out_neighbours() - .tuple_windows() - .all(|(v1, v2)| v1 <= v2)); - assert!(graph - .node(v, 0) - .in_neighbours() - .tuple_windows() - .all(|(v1, v2)| v1 <= v2)); - } - - let exploded_edges: Vec<_> = graph - .exploded_edges() - .map(|(src, dst, time)| (nodes[src.0], nodes[dst.0], time)) - .collect(); - assert_eq!(exploded_edges, edges); - - let mut expected_inbounds = edges - .iter() - .map(|(src, dst, _)| (*dst, *src)) - .into_group_map(); - for v in expected_inbounds.values_mut() { - v.sort(); - v.dedup(); - } - - // check incoming edges - for (v_id, g_id) in nodes.iter().enumerate() { - let expected_inbound = match expected_inbounds.get(g_id) { - None => &vec![], - Some(res) => res, - }; - - let actual_inbound = graph - .node(VID(v_id), 0) - .in_neighbours() - .map(|v| nodes[v.0]) - .collect::>(); - - assert_eq!(&actual_inbound, expected_inbound); - } - - let unique_edges = edges.iter().map(|(src, dst, _)| (*src, *dst)).dedup(); - - for (e_id, (src, dst)) in unique_edges.enumerate() { - let edge = graph.edge(EID(e_id)); - let VID(src_id) = edge.src_id(); - let VID(dst_id) = edge.dst_id(); - - assert_eq!(nodes[src_id], src); - assert_eq!(nodes[dst_id], dst); - } - - let mut expected_node_additions = edges - .iter() - .flat_map(|(src, dst, t)| [(*src, *t), (*dst, *t)]) - .into_group_map(); - for v in expected_node_additions.values_mut() { - v.sort(); - v.dedup(); - } - - for (v_id, node) in nodes.iter().enumerate() { - let expected = expected_node_additions.get(node).unwrap(); - let node = graph.node(VID(v_id), 0); - let actual = node.timestamps().into_iter_t().collect::>(); - assert_eq!(&actual, expected); - } - } - - fn edges_sanity_check_inner( - edges: Vec<(u64, u64, i64)>, - input_chunk_size: u64, - chunk_size: usize, - t_props_chunk_size: usize, - ) { - let test_dir = TempDir::new().unwrap(); - let nodes = edges_sanity_node_list(&edges); - match edges_sanity_check_build_graph( - test_dir.path(), - &edges, - input_chunk_size, - chunk_size, - t_props_chunk_size, - ) { - Ok(graph) => { - // check graph is sane - check_graph_sanity(&edges, &nodes, &graph); - - // check that reloading from graph dir works - let reloaded_graph = TemporalGraph::new(&test_dir).unwrap(); - check_graph_sanity(&edges, &nodes, &reloaded_graph) - } - Err(RAError::NoEdgeLists | RAError::EmptyChunk) => assert!(edges.is_empty()), - Err(error) => panic!("{}", error.to_string()), - }; - } - - proptest! { - #[test] - fn edges_sanity_check( - edges in any_with::)>>(size_range(1..=100).lift()).prop_map(|v| { - let mut v: Vec<(u64, u64, i64)> = v.into_iter().flat_map(|(src, dst, times)| { - let src = src as u64; - let dst = dst as u64; - times.into_iter().map(move |t| (src, dst, t))}).collect(); - v.sort(); - v}), - input_chunk_size in 1..1024u64, - chunk_size in 1..1024usize, - t_props_chunk_size in 1..128usize - ) { - edges_sanity_check_inner(edges, input_chunk_size, chunk_size, t_props_chunk_size); - } - } - - #[test] - fn edge_sanity_fail1() { - let edges = vec![(0, 17, 0), (1, 0, -1), (17, 0, 0)]; - edges_sanity_check_inner(edges, 4, 4, 4) - } - - #[test] - fn edge_sanity_bad() { - let edges = vec![ - (0, 85, -8744527736816607775), - (0, 85, -8533859256444633783), - (0, 85, -7949123054744509169), - (0, 85, -7208573652910411733), - (0, 85, -7004677070223473589), - (0, 85, -6486844751834401685), - (0, 85, -6420653301843451067), - (0, 85, -6151481582745013767), - (0, 85, -5577061971106014565), - (0, 85, -5484794766797320810), - ]; - edges_sanity_check_inner(edges, 3, 5, 12) - } - - #[test] - fn edge_sanity_more_bad() { - let edges = vec![ - (1, 3, -8622734205120758463), - (2, 0, -8064563587743129892), - (2, 0, 0), - (2, 0, 66718116), - (2, 0, 733950369757766878), - (2, 0, 2044789983495278802), - (2, 0, 2403967656666566197), - (2, 4, -9199293364914546702), - (2, 4, -9104424882442202562), - (2, 4, -8942117006530427874), - (2, 4, -8805351871358148900), - (2, 4, -8237347600058197888), - ]; - edges_sanity_check_inner(edges, 3, 5, 6) - } - - #[test] - fn edges_sanity_chunk_1() { - edges_sanity_check_inner(vec![(876787706323152993, 0, 0)], 1, 1, 1) - } - - #[test] - fn edges_sanity_chunk_2() { - edges_sanity_check_inner(vec![(4, 3, 2), (4, 5, 0)], 2, 2, 2) - } - - #[test] - fn one_edge_bounds_chunk_remainder() { - let edges = vec![(0u64, 1, 0)]; - edges_sanity_check_inner(edges, 1, 3, 3); - } - - #[test] - fn same_edge_twice() { - let edges = vec![(0, 1, 0), (0, 1, 1)]; - edges_sanity_check_inner(edges, 2, 3, 3); - } - - #[test] - fn node_additions_bounds_to_arrays() { - let edges = vec![(0, 0, -2), (0, 0, -1), (0, 0, 0), (0, 0, 1), (0, 0, 2)]; - let len = edges.len(); - edges_sanity_check_inner(edges, len as u64, 2, 2); - } - - #[test] - fn large_failing_edge_sanity_repeated() { - let edges = vec![ - (0, 0, 0), - (0, 1, 0), - (0, 2, 0), - (0, 3, 0), - (0, 4, 0), - (0, 5, 0), - (0, 6, -30), - (4, 7, -83), - (4, 7, -77), - (6, 8, -68), - (6, 8, -65), - (9, 10, 46), - (9, 10, 46), - (9, 10, 51), - (9, 10, 54), - (9, 10, 59), - (9, 10, 59), - (9, 10, 59), - (9, 10, 65), - (9, 11, -75), - ]; - let input_chunk_size = 411; - let edge_chunk_size = 5; - let edge_max_list_size = 7; - - edges_sanity_check_inner(edges, input_chunk_size, edge_chunk_size, edge_max_list_size); - } - - #[test] - fn edge_sanity_chunk_broken_incoming() { - let edges = vec![ - (0, 0, 0), - (0, 0, 0), - (0, 0, 66), - (0, 1, 0), - (2, 0, 0), - (3, 4, 0), - (4, 0, 0), - (4, 4, 0), - (4, 4, 0), - (4, 4, 0), - (4, 4, 0), - (5, 0, 0), - (6, 7, 7274856480798084567), - (8, 3, -7707029126214574305), - ]; - - edges_sanity_check_inner(edges, 853, 122, 98) - } - - #[test] - fn edge_sanity_chunk_broken_something() { - let edges = vec![(0, 3, 0), (1, 2, 0), (3, 2, 0)]; - edges_sanity_check_inner(edges, 1, 1, 1) - } -} diff --git a/raphtory-storage/src/disk/storage_interface/edge.rs b/raphtory-storage/src/disk/storage_interface/edge.rs deleted file mode 100644 index 399947370e..0000000000 --- a/raphtory-storage/src/disk/storage_interface/edge.rs +++ /dev/null @@ -1,121 +0,0 @@ -use crate::graph::edges::edge_storage_ops::{EdgeStorageOps, TimeIndexRef}; -use pometry_storage::{edge::Edge, tprops::DiskTProp}; -use raphtory_api::core::{ - entities::{ - properties::{prop::Prop, tprop::TPropOps}, - LayerIds, LayerVariants, EID, VID, - }, - storage::timeindex::{TimeIndexEntry, TimeIndexOps}, -}; -use raphtory_core::storage::timeindex::TimeIndex; -use rayon::prelude::*; -use std::{iter, ops::Range}; - -impl<'a> EdgeStorageOps<'a> for Edge<'a> { - fn added(self, layer_ids: &LayerIds, w: Range) -> bool { - self.has_layer(layer_ids) && { - match layer_ids { - LayerIds::None => false, - LayerIds::All => self - .additions_iter(layer_ids) - .any(|(_, t_index)| t_index.active_t(w.clone())), - LayerIds::One(l_id) => self.get_additions::(*l_id).active_t(w), - LayerIds::Multiple(layers) => layers - .iter() - .any(|l_id| self.added(&LayerIds::One(l_id), w.clone())), - } - } - } - - fn has_layer(self, layer_ids: &LayerIds) -> bool { - match layer_ids { - LayerIds::None => false, - LayerIds::All => true, - LayerIds::One(id) => self.has_layer_inner(*id), - LayerIds::Multiple(ids) => ids.iter().any(|id| self.has_layer_inner(id)), - } - } - - fn src(self) -> VID { - self.src_id() - } - - fn dst(self) -> VID { - self.dst_id() - } - - fn eid(self) -> EID { - self.pid() - } - - fn layer_ids_iter(self, layer_ids: &'a LayerIds) -> impl Iterator + 'a { - match layer_ids { - LayerIds::None => LayerVariants::None(std::iter::empty()), - LayerIds::All => LayerVariants::All( - (0..self.internal_num_layers()).filter(move |&l| self.has_layer_inner(l)), - ), - LayerIds::One(id) => { - LayerVariants::One(self.has_layer_inner(*id).then_some(*id).into_iter()) - } - LayerIds::Multiple(ids) => { - LayerVariants::Multiple(ids.into_iter().filter(move |&id| self.has_layer_inner(id))) - } - } - } - - fn layer_ids_par_iter(self, layer_ids: &LayerIds) -> impl ParallelIterator + 'a { - match layer_ids { - LayerIds::None => LayerVariants::None(rayon::iter::empty()), - LayerIds::All => LayerVariants::All( - (0..self.internal_num_layers()) - .into_par_iter() - .filter(move |&l| self.has_layer_inner(l)), - ), - LayerIds::One(id) => { - LayerVariants::One(self.has_layer_inner(*id).then_some(*id).into_par_iter()) - } - LayerIds::Multiple(ids) => { - LayerVariants::Multiple(ids.par_iter().filter(move |&id| self.has_layer_inner(id))) - } - } - } - - fn deletions_iter( - self, - _layer_ids: &'a LayerIds, - ) -> impl Iterator)> + 'a { - Box::new(iter::empty()) - } - - fn deletions_par_iter( - self, - _layer_ids: &LayerIds, - ) -> impl ParallelIterator)> + 'a { - rayon::iter::empty() - } - - fn additions(self, layer_id: usize) -> TimeIndexRef<'a> { - TimeIndexRef::External(self.get_additions::(layer_id)) - } - - fn deletions(self, _layer_id: usize) -> TimeIndexRef<'a> { - TimeIndexRef::Ref(&TimeIndex::Empty) - } - - fn temporal_prop_layer(self, layer_id: usize, prop_id: usize) -> impl TPropOps<'a> + 'a { - self.graph() - .localize_edge_prop_id(layer_id, prop_id) - .map(|prop_id| { - self.graph() - .layer(layer_id) - .edges_storage() - .prop(self.eid(), prop_id) - }) - .unwrap_or(DiskTProp::empty()) - } - - fn metadata_layer(self, _layer_id: usize, _prop_id: usize) -> Option { - // TODO: metadata edge properties not implemented in diskgraph yet - None - } -} diff --git a/raphtory-storage/src/disk/storage_interface/edges.rs b/raphtory-storage/src/disk/storage_interface/edges.rs deleted file mode 100644 index 42dbf08675..0000000000 --- a/raphtory-storage/src/disk/storage_interface/edges.rs +++ /dev/null @@ -1,89 +0,0 @@ -use crate::disk::{ - graph_impl::DiskEdge, storage_interface::edges_ref::DiskEdgesRef, DiskGraphStorage, -}; -use itertools::Itertools; -use raphtory_api::{ - core::entities::{edges::edge_ref::EdgeRef, LayerIds, LayerVariants, EID}, - iter::IntoDynBoxed, -}; -use raphtory_core::utils::iter::GenLockedIter; -use rayon::iter::{IntoParallelIterator, ParallelIterator}; -use std::{iter, sync::Arc}; - -#[derive(Clone, Debug)] -pub struct DiskEdges { - graph: Arc, -} - -impl DiskEdges { - pub(crate) fn new(graph: &DiskGraphStorage) -> Self { - Self { - graph: Arc::new(graph.clone()), - } - } - - pub fn as_ref(&self) -> DiskEdgesRef { - DiskEdgesRef { - graph: &self.graph.inner, - } - } - - pub fn into_iter_refs(self, layer_ids: LayerIds) -> impl Iterator { - match layer_ids { - LayerIds::None => LayerVariants::None(iter::empty()), - LayerIds::All => LayerVariants::All(GenLockedIter::from(self.graph, |graph| { - graph - .inner - .all_edge_ids() - .map(|(eid, src, dst)| EdgeRef::new_outgoing(eid, src, dst)) - .into_dyn_boxed() - })), - LayerIds::One(layer_id) => { - LayerVariants::One(GenLockedIter::from(self.graph, move |graph| { - graph - .inner - .layer_edge_ids(layer_id) - .map(|(eid, src, dst)| EdgeRef::new_outgoing(eid, src, dst)) - .into_dyn_boxed() - })) - } - LayerIds::Multiple(ids) => LayerVariants::Multiple( - ids.into_iter() - .map(move |layer_id| { - GenLockedIter::from(self.graph.clone(), move |graph| { - graph.inner.layer_edge_ids(layer_id).into_dyn_boxed() - }) - }) - .kmerge_by(|(eid1, _, _), (eid2, _, _)| eid1 < eid2) - .dedup() - .map(move |(eid, src, dst)| EdgeRef::new_outgoing(eid, src, dst)), - ), - } - } - - pub fn into_par_iter_refs(self, layer_ids: LayerIds) -> impl ParallelIterator { - match layer_ids { - LayerIds::None => LayerVariants::None(rayon::iter::empty()), - LayerIds::One(layer_id) => { - LayerVariants::One(self.graph.inner.all_edge_ids_par(layer_id)) - } - LayerIds::All => { - LayerVariants::All((0..self.graph.inner.num_edges()).into_par_iter().map(EID)) - } - LayerIds::Multiple(ids) => LayerVariants::Multiple( - (0..self.graph.inner.num_edges()) - .into_par_iter() - .map(EID) - .filter(move |e| { - ids.clone() - .into_iter() - .any(|layer_id| self.graph.inner.edge(*e).has_layer_inner(layer_id)) - }), - ), - } - } - - pub fn get(&self, eid: EID) -> DiskEdge { - self.graph.inner.edge(eid) - } -} diff --git a/raphtory-storage/src/disk/storage_interface/edges_ref.rs b/raphtory-storage/src/disk/storage_interface/edges_ref.rs deleted file mode 100644 index c638bebb6c..0000000000 --- a/raphtory-storage/src/disk/storage_interface/edges_ref.rs +++ /dev/null @@ -1,68 +0,0 @@ -use crate::{disk::graph_impl::DiskEdge, graph::edges::edge_storage_ops::EdgeStorageOps}; -use pometry_storage::graph::TemporalGraph; -use raphtory_api::core::entities::{LayerIds, LayerVariants, EID}; -use rayon::prelude::*; -use std::iter; - -#[derive(Copy, Clone, Debug)] -pub struct DiskEdgesRef<'a> { - pub(super) graph: &'a TemporalGraph, -} - -impl<'a> DiskEdgesRef<'a> { - pub(crate) fn new(storage: &'a TemporalGraph) -> Self { - Self { graph: storage } - } - - pub fn edge(self, eid: EID) -> DiskEdge<'a> { - self.graph.edge(eid) - } - - pub fn iter(self, layers: &LayerIds) -> impl Iterator> + use<'a, '_> { - match layers { - LayerIds::None => LayerVariants::None(iter::empty()), - LayerIds::All => LayerVariants::All(self.graph.edges_iter()), - LayerIds::One(layer_id) => LayerVariants::One(self.graph.edges_layer_iter(*layer_id)), - layer_ids => LayerVariants::Multiple( - self.graph - .edges_iter() - .filter(move |e| e.has_layer(layer_ids)), - ), - } - } - - pub fn par_iter( - self, - layers: &LayerIds, - ) -> impl ParallelIterator> + use<'a, '_> { - match layers { - LayerIds::None => LayerVariants::None(rayon::iter::empty()), - LayerIds::All => LayerVariants::All(self.graph.edges_par_iter()), - LayerIds::One(layer_id) => { - LayerVariants::One(self.graph.edges_layer_par_iter(*layer_id)) - } - layer_ids => LayerVariants::Multiple( - self.graph - .edges_par_iter() - .filter(move |e| e.has_layer(layer_ids)), - ), - } - } - - pub fn count(self, layers: &LayerIds) -> usize { - match layers { - LayerIds::None => 0, - LayerIds::All => self.graph.num_edges(), - LayerIds::One(id) => self.graph.layer(*id).num_edges(), - layer_ids => self - .graph - .edges_par_iter() - .filter(move |e| e.has_layer(layer_ids)) - .count(), - } - } - - pub fn len(&self) -> usize { - self.count(&LayerIds::All) - } -} diff --git a/raphtory-storage/src/disk/storage_interface/mod.rs b/raphtory-storage/src/disk/storage_interface/mod.rs deleted file mode 100644 index 27f130f009..0000000000 --- a/raphtory-storage/src/disk/storage_interface/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub mod edge; -pub mod edges; -pub mod edges_ref; -pub mod node; -pub mod nodes; -pub mod nodes_ref; diff --git a/raphtory-storage/src/disk/storage_interface/node.rs b/raphtory-storage/src/disk/storage_interface/node.rs deleted file mode 100644 index 15826e6300..0000000000 --- a/raphtory-storage/src/disk/storage_interface/node.rs +++ /dev/null @@ -1,354 +0,0 @@ -use crate::graph::nodes::{ - node_additions::NodeAdditions, - node_storage_ops::NodeStorageOps, - row::{DiskRow, Row}, -}; -use itertools::Itertools; -use polars_arrow::datatypes::ArrowDataType; -use pometry_storage::{ - graph::TemporalGraph, timestamps::LayerAdditions, tprops::DiskTProp, GidRef, -}; -use raphtory_api::{ - core::{ - entities::{ - edges::edge_ref::EdgeRef, - properties::{prop::Prop, tprop::TPropOps}, - LayerIds, LayerVariants, VID, - }, - storage::timeindex::{TimeIndexEntry, TimeIndexOps}, - Direction, DirectionVariants, - }, - iter::BoxedLIter, -}; -use std::{borrow::Cow, iter, ops::Range}; - -#[derive(Copy, Clone, Debug)] -pub struct DiskNode<'a> { - graph: &'a TemporalGraph, - pub(super) vid: VID, -} - -impl<'a> DiskNode<'a> { - pub fn into_rows(self) -> impl Iterator)> { - self.graph - .node_properties() - .temporal_props() - .iter() - .enumerate() - .flat_map(move |(layer, props)| { - let ts = props.timestamps::(self.vid); - ts.into_iter().zip(0..ts.len()).map(move |(t, row)| { - let row = DiskRow::new(self.graph, ts, row, layer); - (t, Row::Disk(row)) - }) - }) - } - - pub fn into_rows_window( - self, - window: Range, - ) -> impl Iterator)> { - self.graph - .node_properties() - .temporal_props() - .iter() - .enumerate() - .flat_map(move |(layer, props)| { - let ts = props.timestamps::(self.vid); - let ts = ts.range(window.clone()); - ts.iter().enumerate().map(move |(row, t)| { - let row = DiskRow::new(self.graph, ts, row, layer); - (t, Row::Disk(row)) - }) - }) - } - - pub fn last_before_row(self, t: TimeIndexEntry) -> Vec<(usize, Prop)> { - self.graph - .prop_mapping() - .nodes() - .iter() - .enumerate() - .filter_map(|(prop_id, &location)| { - let (layer, local_prop_id) = location?; - let layer = self.graph().node_properties().temporal_props().get(layer)?; - let t_prop = layer.prop::(self.vid, local_prop_id); - t_prop.last_before(t).map(|(_, p)| (prop_id, p)) - }) - .collect() - } - - pub fn node_metadata_ids(self) -> BoxedLIter<'a, usize> { - match &self.graph.node_properties().metadata { - None => Box::new(std::iter::empty()), - Some(props) => { - Box::new((0..props.num_props()).filter(move |id| props.has_prop(self.vid, *id))) - } - } - } - - pub fn temporal_node_prop_ids(self) -> impl Iterator + 'a { - self.graph - .prop_mapping() - .nodes() - .iter() - .enumerate() - .filter(|(_, exists)| exists.is_some()) - .map(|(id, _)| id) - } - - pub(crate) fn new(graph: &'a TemporalGraph, vid: VID) -> Self { - Self { graph, vid } - } - - pub fn out_edges(self, layers: &LayerIds) -> impl Iterator + 'a { - match layers { - LayerIds::None => LayerVariants::None(iter::empty()), - LayerIds::All => LayerVariants::All( - self.graph - .layers() - .iter() - .enumerate() - .map(|(layer_id, layer)| { - layer - .nodes_storage() - .out_adj_list(self.vid) - .map(move |(eid, dst)| { - EdgeRef::new_outgoing(eid, self.vid, dst).at_layer(layer_id) - }) - }) - .kmerge_by(|e1, e2| e1.remote() <= e2.remote()), - ), - LayerIds::One(layer_id) => { - let layer_id = *layer_id; - LayerVariants::One( - self.graph.layers()[layer_id] - .nodes_storage() - .out_adj_list(self.vid) - .map(move |(eid, dst)| { - EdgeRef::new_outgoing(eid, self.vid, dst).at_layer(layer_id) - }), - ) - } - LayerIds::Multiple(ids) => LayerVariants::Multiple( - ids.into_iter() - .map(|layer_id| { - self.graph.layers()[layer_id] - .nodes_storage() - .out_adj_list(self.vid) - .map(move |(eid, dst)| { - EdgeRef::new_outgoing(eid, self.vid, dst).at_layer(layer_id) - }) - }) - .kmerge_by(|e1, e2| e1.remote() <= e2.remote()), - ), - } - } - - pub fn in_edges(self, layers: &LayerIds) -> impl Iterator + 'a { - match layers { - LayerIds::None => LayerVariants::None(iter::empty()), - LayerIds::All => LayerVariants::All( - self.graph - .layers() - .iter() - .enumerate() - .map(|(layer_id, layer)| { - layer - .nodes_storage() - .in_adj_list(self.vid) - .map(move |(eid, src)| { - EdgeRef::new_incoming(eid, src, self.vid).at_layer(layer_id) - }) - }) - .kmerge_by(|e1, e2| e1.remote() <= e2.remote()), - ), - LayerIds::One(layer_id) => { - let layer_id = *layer_id; - LayerVariants::One( - self.graph.layers()[layer_id] - .nodes_storage() - .in_adj_list(self.vid) - .map(move |(eid, src)| { - EdgeRef::new_incoming(eid, src, self.vid).at_layer(layer_id) - }), - ) - } - LayerIds::Multiple(ids) => LayerVariants::Multiple( - ids.into_iter() - .map(|layer_id| { - self.graph.layers()[layer_id] - .nodes_storage() - .in_adj_list(self.vid) - .map(move |(eid, src)| { - EdgeRef::new_incoming(eid, src, self.vid).at_layer(layer_id) - }) - }) - .kmerge_by(|e1, e2| e1.remote() <= e2.remote()), - ), - } - } - - pub fn edges(self, layers: &LayerIds) -> impl Iterator + 'a { - self.in_edges(layers) - .merge_by(self.out_edges(layers), |e1, e2| e1.remote() <= e2.remote()) - } - - pub fn additions_for_layers(self, layer_ids: LayerIds) -> NodeAdditions<'a> { - NodeAdditions::Col(LayerAdditions::new(self.graph, self.vid, layer_ids, None)) - } - - pub fn graph(&self) -> &TemporalGraph { - self.graph - } -} - -impl<'a> NodeStorageOps<'a> for DiskNode<'a> { - fn degree(self, layers: &LayerIds, dir: Direction) -> usize { - let single_layer = match &layers { - LayerIds::None => return 0, - LayerIds::All => match self.graph.layers().len() { - 0 => return 0, - 1 => Some(&self.graph.layers()[0]), - _ => None, - }, - LayerIds::One(id) => Some(&self.graph.layers()[*id]), - LayerIds::Multiple(ids) => match ids.len() { - 0 => return 0, - 1 => Some(&self.graph.layers()[ids.get_id_by_index(0).unwrap()]), - _ => None, - }, - }; - match dir { - Direction::OUT => match single_layer { - None => self - .out_edges(layers) - .dedup_by(|e1, e2| e1.remote() == e2.remote()) - .count(), - Some(layer) => layer.nodes_storage().out_degree(self.vid), - }, - Direction::IN => match single_layer { - None => self - .in_edges(layers) - .dedup_by(|e1, e2| e1.remote() == e2.remote()) - .count(), - Some(layer) => layer.nodes_storage().in_degree(self.vid), - }, - Direction::BOTH => match single_layer { - None => self - .edges(layers) - .dedup_by(|e1, e2| e1.remote() == e2.remote()) - .count(), - Some(layer) => layer - .nodes_storage() - .in_neighbours_iter(self.vid) - .merge(layer.nodes_storage().out_neighbours_iter(self.vid)) - .dedup() - .count(), - }, - } - } - - fn additions(self) -> NodeAdditions<'a> { - self.additions_for_layers(LayerIds::All) - } - - fn tprop(self, prop_id: usize) -> impl TPropOps<'a> { - self.graph - .prop_mapping() - .localise_node_prop_id(prop_id) - .and_then(|(layer, local_prop_id)| { - self.graph - .node_properties() - .temporal_props() - .get(layer) - .map(|t_props| t_props.prop(self.vid, local_prop_id)) - }) - .unwrap_or(DiskTProp::empty()) - } - - fn tprops(self) -> impl Iterator)> { - self.graph - .node_properties() - .temporal_props() - .iter() - .flat_map(move |t_props| t_props.props(self.vid)) - .enumerate() - } - - fn prop(self, prop_id: usize) -> Option { - let cprops = self.graph.node_properties().metadata.as_ref()?; - let prop_type = cprops.prop_dtype(prop_id); - match prop_type.data_type { - ArrowDataType::Int32 => cprops.prop_native::(self.vid, prop_id).map(Prop::I32), - ArrowDataType::Int64 => cprops.prop_native::(self.vid, prop_id).map(Prop::I64), - ArrowDataType::UInt32 => cprops.prop_native::(self.vid, prop_id).map(Prop::U32), - ArrowDataType::UInt64 => cprops.prop_native::(self.vid, prop_id).map(Prop::U64), - ArrowDataType::Float32 => cprops.prop_native::(self.vid, prop_id).map(Prop::F32), - ArrowDataType::Float64 => cprops.prop_native::(self.vid, prop_id).map(Prop::F64), - ArrowDataType::Utf8 | ArrowDataType::LargeUtf8 | ArrowDataType::Utf8View => { - cprops.prop_str(self.vid, prop_id).map(Prop::str) - } - // Add cases for other types, including special handling for complex types - _ => None, // Placeholder for unhandled types - } - } - - fn edges_iter( - self, - layers: &LayerIds, - dir: Direction, - ) -> impl Iterator + Send + 'a { - match dir { - Direction::OUT => DirectionVariants::Out(self.out_edges(layers)), - Direction::IN => DirectionVariants::In(self.in_edges(layers)), - Direction::BOTH => DirectionVariants::Both(self.edges(layers)), - } - .map(|e| e.unexplode()) - .dedup_by(|l, r| l.pid() == r.pid()) - } - - fn node_type_id(self) -> usize { - self.graph.node_type_id(self.vid) - } - - fn vid(self) -> VID { - self.vid - } - - fn id(self) -> GidRef<'a> { - self.graph.node_gid(self.vid).unwrap() - } - - fn name(self) -> Option> { - match self.graph.node_gid(self.vid).unwrap() { - GidRef::U64(_) => None, - GidRef::Str(v) => Some(Cow::from(v)), - } - } - - fn find_edge(self, dst: VID, layer_ids: &LayerIds) -> Option { - match layer_ids { - LayerIds::None => None, - LayerIds::All => self - .graph - .find_edge(self.vid, dst) - .map(|e| EdgeRef::new_outgoing(e.pid(), self.vid, dst)), - LayerIds::One(id) => { - let eid = self.graph.layers()[*id] - .nodes_storage() - .find_edge(self.vid, dst)?; - Some(EdgeRef::new_outgoing(eid, self.vid, dst)) - } - LayerIds::Multiple(ids) => ids - .iter() - .filter_map(|layer_id| { - self.graph.layers()[layer_id] - .nodes_storage() - .find_edge(self.vid, dst) - .map(|eid| EdgeRef::new_outgoing(eid, self.vid, dst)) - }) - .next(), - } - } -} diff --git a/raphtory-storage/src/disk/storage_interface/nodes.rs b/raphtory-storage/src/disk/storage_interface/nodes.rs deleted file mode 100644 index 23daaf050f..0000000000 --- a/raphtory-storage/src/disk/storage_interface/nodes.rs +++ /dev/null @@ -1,23 +0,0 @@ -use crate::disk::storage_interface::{node::DiskNode, nodes_ref::DiskNodesRef}; -use pometry_storage::graph::TemporalGraph; -use raphtory_api::core::entities::VID; -use std::sync::Arc; - -#[derive(Clone, Debug)] -pub struct DiskNodesOwned { - graph: Arc, -} - -impl DiskNodesOwned { - pub(crate) fn new(graph: Arc) -> Self { - Self { graph } - } - - pub fn node(&self, vid: VID) -> DiskNode { - DiskNode::new(&self.graph, vid) - } - - pub fn as_ref(&self) -> DiskNodesRef { - DiskNodesRef::new(&self.graph) - } -} diff --git a/raphtory-storage/src/disk/storage_interface/nodes_ref.rs b/raphtory-storage/src/disk/storage_interface/nodes_ref.rs deleted file mode 100644 index bd5ba75b7c..0000000000 --- a/raphtory-storage/src/disk/storage_interface/nodes_ref.rs +++ /dev/null @@ -1,33 +0,0 @@ -use crate::disk::storage_interface::node::DiskNode; -use pometry_storage::graph::TemporalGraph; -use raphtory_api::core::entities::VID; -use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator}; - -#[derive(Copy, Clone, Debug)] -pub struct DiskNodesRef<'a> { - graph: &'a TemporalGraph, -} - -impl<'a> DiskNodesRef<'a> { - pub(crate) fn new(graph: &'a TemporalGraph) -> Self { - Self { graph } - } - - pub fn len(&self) -> usize { - self.graph.num_nodes() - } - - pub fn node(self, vid: VID) -> DiskNode<'a> { - DiskNode::new(self.graph, vid) - } - - pub fn par_iter(self) -> impl IndexedParallelIterator> { - (0..self.graph.num_nodes()) - .into_par_iter() - .map(move |vid| self.node(VID(vid))) - } - - pub fn iter(self) -> impl Iterator> { - (0..self.graph.num_nodes()).map(move |vid| self.node(VID(vid))) - } -} diff --git a/raphtory-storage/src/graph/edges/edge_entry.rs b/raphtory-storage/src/graph/edges/edge_entry.rs index f750458626..67a5598a4a 100644 --- a/raphtory-storage/src/graph/edges/edge_entry.rs +++ b/raphtory-storage/src/graph/edges/edge_entry.rs @@ -5,15 +5,10 @@ use raphtory_api::core::entities::properties::{prop::Prop, tprop::TPropOps}; use raphtory_core::entities::{LayerIds, EID, VID}; use storage::{api::edges::EdgeEntryOps, EdgeEntry, EdgeEntryRef}; -#[cfg(feature = "storage")] -use crate::disk::graph_impl::DiskEdge; - #[derive(Debug)] pub enum EdgeStorageEntry<'a> { Mem(EdgeEntryRef<'a>), Unlocked(EdgeEntry<'a>), - #[cfg(feature = "storage")] - Disk(DiskEdge<'a>), } impl<'a> EdgeStorageEntry<'a> { @@ -22,8 +17,6 @@ impl<'a> EdgeStorageEntry<'a> { match self { EdgeStorageEntry::Mem(edge) => *edge, EdgeStorageEntry::Unlocked(edge) => edge.as_ref(), - #[cfg(feature = "storage")] - EdgeStorageEntry::Disk(edge) => EdgeEntryRef::Disk(*edge), } } } diff --git a/raphtory-storage/src/graph/edges/edge_storage_ops.rs b/raphtory-storage/src/graph/edges/edge_storage_ops.rs index 1b6da45353..00860f8b8b 100644 --- a/raphtory-storage/src/graph/edges/edge_storage_ops.rs +++ b/raphtory-storage/src/graph/edges/edge_storage_ops.rs @@ -1,6 +1,4 @@ use iter_enum::{DoubleEndedIterator, ExactSizeIterator, FusedIterator, Iterator}; -#[cfg(feature = "storage")] -use pometry_storage::timestamps::TimeStamps; use raphtory_api::core::{ entities::{ edges::edge_ref::{Dir, EdgeRef}, @@ -17,16 +15,12 @@ use storage::api::edges::EdgeRefOps; pub enum TimeIndexRef<'a> { Ref(&'a TimeIndex), Range(TimeIndexWindow<'a, TimeIndexEntry, TimeIndex>), - #[cfg(feature = "storage")] - External(TimeStamps<'a, TimeIndexEntry>), } #[derive(Iterator, DoubleEndedIterator, ExactSizeIterator, FusedIterator, Debug, Clone)] -pub enum TimeIndexRefVariants { +pub enum TimeIndexRefVariants { Ref(Ref), Range(Range), - #[cfg(feature = "storage")] - External(External), } impl<'a> TimeIndexOps<'a> for TimeIndexRef<'a> { @@ -38,8 +32,6 @@ impl<'a> TimeIndexOps<'a> for TimeIndexRef<'a> { match self { TimeIndexRef::Ref(t) => t.active(w), TimeIndexRef::Range(t) => t.active(w), - #[cfg(feature = "storage")] - TimeIndexRef::External(t) => t.active(w), } } @@ -47,8 +39,6 @@ impl<'a> TimeIndexOps<'a> for TimeIndexRef<'a> { match self { TimeIndexRef::Ref(t) => TimeIndexRef::Range(t.range(w)), TimeIndexRef::Range(t) => TimeIndexRef::Range(t.range(w)), - #[cfg(feature = "storage")] - TimeIndexRef::External(t) => TimeIndexRef::External(t.range(w)), } } @@ -56,8 +46,6 @@ impl<'a> TimeIndexOps<'a> for TimeIndexRef<'a> { match self { TimeIndexRef::Ref(t) => t.first(), TimeIndexRef::Range(t) => t.first(), - #[cfg(feature = "storage")] - TimeIndexRef::External(t) => t.first(), } } @@ -65,8 +53,6 @@ impl<'a> TimeIndexOps<'a> for TimeIndexRef<'a> { match self { TimeIndexRef::Ref(t) => t.last(), TimeIndexRef::Range(t) => t.last(), - #[cfg(feature = "storage")] - TimeIndexRef::External(t) => t.last(), } } @@ -74,8 +60,6 @@ impl<'a> TimeIndexOps<'a> for TimeIndexRef<'a> { match self { TimeIndexRef::Ref(t) => TimeIndexRefVariants::Ref(t.iter()), TimeIndexRef::Range(t) => TimeIndexRefVariants::Range(t.iter()), - #[cfg(feature = "storage")] - TimeIndexRef::External(t) => TimeIndexRefVariants::External(t.iter()), } } @@ -83,8 +67,6 @@ impl<'a> TimeIndexOps<'a> for TimeIndexRef<'a> { match self { TimeIndexRef::Ref(t) => TimeIndexRefVariants::Ref(t.iter_rev()), TimeIndexRef::Range(t) => TimeIndexRefVariants::Range(t.iter_rev()), - #[cfg(feature = "storage")] - TimeIndexRef::External(t) => TimeIndexRefVariants::External(t.iter_rev()), } } @@ -92,8 +74,6 @@ impl<'a> TimeIndexOps<'a> for TimeIndexRef<'a> { match self { TimeIndexRef::Ref(ts) => ts.len(), TimeIndexRef::Range(ts) => ts.len(), - #[cfg(feature = "storage")] - TimeIndexRef::External(t) => t.len(), } } } diff --git a/raphtory-storage/src/graph/graph.rs b/raphtory-storage/src/graph/graph.rs index 99e371134c..7218025e8e 100644 --- a/raphtory-storage/src/graph/graph.rs +++ b/raphtory-storage/src/graph/graph.rs @@ -2,10 +2,13 @@ use super::{ edges::{edge_entry::EdgeStorageEntry, unlocked::UnlockedEdges}, nodes::node_entry::NodeStorageEntry, }; -use crate::graph::{ - edges::edges::{EdgesStorage, EdgesStorageRef}, - locked::LockedGraph, - nodes::{nodes::NodesStorage, nodes_ref::NodesStorageEntry}, +use crate::{ + graph::{ + edges::edges::{EdgesStorage, EdgesStorageRef}, + locked::LockedGraph, + nodes::{nodes::NodesStorage, nodes_ref::NodesStorageEntry}, + }, + mutation::MutationError, }; use db4_graph::TemporalGraph; use raphtory_api::core::entities::{properties::meta::Meta, LayerIds, LayerVariants, EID, VID}; @@ -13,31 +16,16 @@ use raphtory_core::entities::{nodes::node_ref::NodeRef, properties::graph_meta:: use std::{fmt::Debug, iter, sync::Arc}; use thiserror::Error; -#[cfg(feature = "storage")] -use crate::disk::{ - storage_interface::{ - edges::DiskEdges, edges_ref::DiskEdgesRef, node::DiskNode, nodes::DiskNodesOwned, - nodes_ref::DiskNodesRef, - }, - DiskGraphStorage, -}; -use crate::mutation::MutationError; - #[derive(Clone, Debug)] pub enum GraphStorage { Mem(LockedGraph), Unlocked(Arc), - #[cfg(feature = "storage")] - Disk(Arc), } #[derive(Error, Debug)] pub enum Immutable { #[error("The graph is locked and cannot be mutated")] ReadLockedImmutable, - #[cfg(feature = "storage")] - #[error("DiskGraph cannot be mutated")] - DiskGraphImmutable, } impl From for GraphStorage { @@ -75,13 +63,6 @@ impl GraphStorage { graph: other_graph, .. }) | GraphStorage::Unlocked(other_graph) => Arc::ptr_eq(this_graph, other_graph), - #[cfg(feature = "storage")] - _ => false, - }, - #[cfg(feature = "storage")] - GraphStorage::Disk(this_graph) => match other { - GraphStorage::Disk(other_graph) => Arc::ptr_eq(this_graph, other_graph), - _ => false, }, } } @@ -90,8 +71,6 @@ impl GraphStorage { match self { GraphStorage::Mem(_) => Err(Immutable::ReadLockedImmutable)?, GraphStorage::Unlocked(graph) => Ok(graph), - #[cfg(feature = "storage")] - GraphStorage::Disk(_) => Err(Immutable::DiskGraphImmutable)?, } } @@ -100,8 +79,6 @@ impl GraphStorage { match self { GraphStorage::Mem(_) => true, GraphStorage::Unlocked(_) => false, - #[cfg(feature = "storage")] - GraphStorage::Disk(_) => true, } } @@ -123,10 +100,6 @@ impl GraphStorage { GraphStorage::Unlocked(storage) => { NodesStorageEntry::Unlocked(storage.storage().nodes().locked()) } - #[cfg(feature = "storage")] - GraphStorage::Disk(storage) => { - NodesStorageEntry::Disk(DiskNodesRef::new(&storage.inner)) - } } } @@ -137,11 +110,6 @@ impl GraphStorage { node_ref => match self { GraphStorage::Mem(locked) => locked.graph.resolve_node_ref(node_ref), GraphStorage::Unlocked(unlocked) => unlocked.resolve_node_ref(node_ref), - #[cfg(feature = "storage")] - GraphStorage::Disk(storage) => match v { - NodeRef::External(id) => storage.inner.find_node(id), - _ => unreachable!("VID is handled above!"), - }, }, } } @@ -151,8 +119,6 @@ impl GraphStorage { match self { GraphStorage::Mem(storage) => storage.graph.internal_num_nodes(), GraphStorage::Unlocked(storage) => storage.internal_num_nodes(), - #[cfg(feature = "storage")] - GraphStorage::Disk(storage) => storage.inner.num_nodes(), } } @@ -161,8 +127,6 @@ impl GraphStorage { match self { GraphStorage::Mem(storage) => storage.graph.internal_num_edges(), GraphStorage::Unlocked(storage) => storage.internal_num_edges(), - #[cfg(feature = "storage")] - GraphStorage::Disk(storage) => storage.inner.count_edges(), } } @@ -171,8 +135,6 @@ impl GraphStorage { match self { GraphStorage::Mem(storage) => storage.graph.num_layers(), GraphStorage::Unlocked(storage) => storage.num_layers(), - #[cfg(feature = "storage")] - GraphStorage::Disk(storage) => storage.inner.layers().len(), } } @@ -183,10 +145,6 @@ impl GraphStorage { GraphStorage::Unlocked(storage) => { NodesStorage::new(storage.read_locked().nodes.clone()) } - #[cfg(feature = "storage")] - GraphStorage::Disk(storage) => { - NodesStorage::Disk(DiskNodesOwned::new(storage.inner.clone())) - } } } @@ -197,10 +155,6 @@ impl GraphStorage { GraphStorage::Unlocked(storage) => { NodeStorageEntry::Unlocked(storage.storage().nodes().node(vid)) } - #[cfg(feature = "storage")] - GraphStorage::Disk(storage) => { - NodeStorageEntry::Disk(DiskNode::new(&storage.inner, vid)) - } } } @@ -211,8 +165,6 @@ impl GraphStorage { GraphStorage::Unlocked(storage) => { EdgesStorageRef::Unlocked(UnlockedEdges(storage.storage())) } - #[cfg(feature = "storage")] - GraphStorage::Disk(storage) => EdgesStorageRef::Disk(DiskEdgesRef::new(&storage.inner)), } } @@ -223,8 +175,6 @@ impl GraphStorage { GraphStorage::Unlocked(storage) => { EdgesStorage::new(storage.storage().edges().locked().into()) } - #[cfg(feature = "storage")] - GraphStorage::Disk(storage) => EdgesStorage::Disk(DiskEdges::new(storage)), } } @@ -235,8 +185,6 @@ impl GraphStorage { GraphStorage::Unlocked(storage) => { EdgeStorageEntry::Unlocked(storage.storage().edges().edge(eid)) } - #[cfg(feature = "storage")] - GraphStorage::Disk(storage) => EdgeStorageEntry::Disk(storage.inner.edge(eid)), } } @@ -257,8 +205,6 @@ impl GraphStorage { match self { GraphStorage::Mem(storage) => storage.graph.node_meta(), GraphStorage::Unlocked(storage) => storage.node_meta(), - #[cfg(feature = "storage")] - GraphStorage::Disk(storage) => storage.node_meta(), } } @@ -266,8 +212,6 @@ impl GraphStorage { match self { GraphStorage::Mem(storage) => storage.graph.edge_meta(), GraphStorage::Unlocked(storage) => storage.edge_meta(), - #[cfg(feature = "storage")] - GraphStorage::Disk(storage) => storage.edge_meta(), } } @@ -275,8 +219,6 @@ impl GraphStorage { match self { GraphStorage::Mem(storage) => storage.graph.graph_meta(), GraphStorage::Unlocked(storage) => storage.graph_meta(), - #[cfg(feature = "storage")] - GraphStorage::Disk(storage) => storage.graph_meta(), } } } diff --git a/raphtory-storage/src/graph/nodes/node_entry.rs b/raphtory-storage/src/graph/nodes/node_entry.rs index 80860d890e..6a1a0179f7 100644 --- a/raphtory-storage/src/graph/nodes/node_entry.rs +++ b/raphtory-storage/src/graph/nodes/node_entry.rs @@ -66,8 +66,6 @@ impl<'b> NodeStorageEntry<'b> { // NodeStorageEntry::Unlocked(entry) => Box::new(GenLockedIter::from(entry, |e| { // Box::new(e.as_ref().node().const_prop_ids()) // })), - // #[cfg(feature = "storage")] - // NodeStorageEntry::Disk(node) => Box::new(node.constant_node_prop_ids()), // } // } @@ -77,8 +75,6 @@ impl<'b> NodeStorageEntry<'b> { // NodeStorageEntry::Unlocked(entry) => Box::new(GenLockedIter::from(entry, |e| { // Box::new(e.as_ref().temporal_prop_ids()) // })), - // #[cfg(feature = "storage")] - // NodeStorageEntry::Disk(node) => Box::new(node.temporal_node_prop_ids()), // } // } } diff --git a/raphtory-storage/src/graph/nodes/node_ref.rs b/raphtory-storage/src/graph/nodes/node_ref.rs index 4c03a8486d..9ce0c2bb96 100644 --- a/raphtory-storage/src/graph/nodes/node_ref.rs +++ b/raphtory-storage/src/graph/nodes/node_ref.rs @@ -1,8 +1,5 @@ use storage::NodeEntryRef; -#[cfg(feature = "storage")] -use crate::disk::storage_interface::node::DiskNode; - pub type NodeStorageRef<'a> = NodeEntryRef<'a>; // impl<'a> NodeStorageRef<'a> { @@ -12,8 +9,6 @@ pub type NodeStorageRef<'a> = NodeEntryRef<'a>; // // .into_rows() // // .map(|(t, row)| (t, Row::Mem(row))) // // .into_dyn_boxed(), -// // #[cfg(feature = "storage")] -// // NodeStorageRef::Disk(disk_node) => disk_node.into_rows().into_dyn_boxed(), // // } // //TODO: // std::iter::empty() @@ -28,8 +23,6 @@ pub type NodeStorageRef<'a> = NodeEntryRef<'a>; // // .into_rows_window(window) // // .map(|(t, row)| (t, Row::Mem(row))) // // .into_dyn_boxed(), -// // #[cfg(feature = "storage")] -// // NodeStorageRef::Disk(disk_node) => disk_node.into_rows_window(window).into_dyn_boxed(), // // } // std::iter::empty() // } @@ -37,8 +30,6 @@ pub type NodeStorageRef<'a> = NodeEntryRef<'a>; // pub fn last_before_row(self, t: TimeIndexEntry) -> Vec<(usize, Prop)> { // // match self { // // NodeStorageRef::Mem(node_entry) => node_entry.last_before_row(t), -// // #[cfg(feature = "storage")] -// // NodeStorageRef::Disk(disk_node) => disk_node.last_before_row(t), // // } // todo!() // } diff --git a/raphtory-storage/src/graph/nodes/nodes.rs b/raphtory-storage/src/graph/nodes/nodes.rs index a6c56ef02c..173edacd4b 100644 --- a/raphtory-storage/src/graph/nodes/nodes.rs +++ b/raphtory-storage/src/graph/nodes/nodes.rs @@ -5,9 +5,6 @@ use crate::graph::nodes::nodes_ref::NodesStorageEntry; use raphtory_api::core::entities::VID; use storage::{Extension, ReadLockedNodes}; -#[cfg(feature = "storage")] -use crate::disk::storage_interface::nodes::DiskNodesOwned; - #[repr(transparent)] pub struct NodesStorage { storage: Arc>, diff --git a/raphtory-storage/src/graph/nodes/nodes_ref.rs b/raphtory-storage/src/graph/nodes/nodes_ref.rs index 6a0b13b7a8..1aec0c1d8d 100644 --- a/raphtory-storage/src/graph/nodes/nodes_ref.rs +++ b/raphtory-storage/src/graph/nodes/nodes_ref.rs @@ -4,15 +4,10 @@ use raphtory_api::core::entities::VID; use rayon::iter::ParallelIterator; use storage::{Extension, ReadLockedNodes}; -#[cfg(feature = "storage")] -use crate::disk::storage_interface::nodes_ref::DiskNodesRef; - #[derive(Debug)] pub enum NodesStorageEntry<'a> { Mem(&'a ReadLockedNodes), Unlocked(ReadLockedNodes), - #[cfg(feature = "storage")] - Disk(DiskNodesRef<'a>), } macro_rules! for_all_variants { @@ -20,8 +15,6 @@ macro_rules! for_all_variants { match $value { NodesStorageEntry::Mem($pattern) => StorageVariants3::Mem($result), NodesStorageEntry::Unlocked($pattern) => StorageVariants3::Unlocked($result), - #[cfg(feature = "storage")] - NodesStorageEntry::Disk($pattern) => StorageVariants3::Disk($result), } }; } @@ -31,8 +24,6 @@ impl<'a> NodesStorageEntry<'a> { match self { NodesStorageEntry::Mem(store) => store.node_ref(vid), NodesStorageEntry::Unlocked(store) => store.node_ref(vid), - #[cfg(feature = "storage")] - NodesStorageEntry::Disk(store) => NodeStorageRef::Disk(store.node(vid)), } } @@ -40,8 +31,6 @@ impl<'a> NodesStorageEntry<'a> { match self { NodesStorageEntry::Mem(store) => store.len(), NodesStorageEntry::Unlocked(store) => store.len(), - #[cfg(feature = "storage")] - NodesStorageEntry::Disk(store) => store.len(), } } diff --git a/raphtory-storage/src/graph/variants/storage_variants2.rs b/raphtory-storage/src/graph/variants/storage_variants2.rs index a736cb0426..56df6fbf1e 100644 --- a/raphtory-storage/src/graph/variants/storage_variants2.rs +++ b/raphtory-storage/src/graph/variants/storage_variants2.rs @@ -20,20 +20,10 @@ use std::ops::Range; IndexedParallelIterator, ParallelExtend, )] -pub enum StorageVariants2 { +pub enum StorageVariants2 { Mem(Mem), - #[cfg(feature = "storage")] - Disk(Disk), } -#[cfg(feature = "storage")] -macro_rules! SelfType { - ($Mem:ident, $Disk:ident) => { - StorageVariants2<$Mem, $Disk> - }; -} - -#[cfg(not(feature = "storage"))] macro_rules! SelfType { ($Mem:ident, $Disk:ident) => { StorageVariants2<$Mem> @@ -44,23 +34,10 @@ macro_rules! for_all { ($value:expr, $pattern:pat => $result:expr) => { match $value { StorageVariants2::Mem($pattern) => $result, - #[cfg(feature = "storage")] - StorageVariants2::Disk($pattern) => $result, - } - }; -} - -#[cfg(feature = "storage")] -macro_rules! for_all_iter { - ($value:expr, $pattern:pat => $result:expr) => { - match $value { - StorageVariants2::Mem($pattern) => StorageVariants2::Mem($result), - StorageVariants2::Disk($pattern) => StorageVariants2::Disk($result), } }; } -#[cfg(not(feature = "storage"))] macro_rules! for_all_iter { ($value:expr, $pattern:pat => $result:expr) => { match $value { @@ -69,9 +46,7 @@ macro_rules! for_all_iter { }; } -impl<'a, Mem: TPropOps<'a> + 'a, #[cfg(feature = "storage")] Disk: TPropOps<'a> + 'a> TPropOps<'a> - for SelfType!(Mem, Disk) -{ +impl<'a, Mem: TPropOps<'a> + 'a> TPropOps<'a> for SelfType!(Mem, Disk) { fn last_before(&self, t: TimeIndexEntry) -> Option<(TimeIndexEntry, Prop)> { for_all!(self, props => props.last_before(t)) } @@ -95,20 +70,10 @@ impl<'a, Mem: TPropOps<'a> + 'a, #[cfg(feature = "storage")] Disk: TPropOps<'a> } } -impl< - 'a, - Mem: TimeIndexOps<'a>, - #[cfg(feature = "storage")] Disk: TimeIndexOps<'a, IndexType = Mem::IndexType>, - > TimeIndexOps<'a> for SelfType!(Mem, Disk) -{ +impl<'a, Mem: TimeIndexOps<'a>> TimeIndexOps<'a> for SelfType!(Mem, Disk) { type IndexType = Mem::IndexType; - - #[cfg(not(feature = "storage"))] type RangeType = Mem::RangeType; - #[cfg(feature = "storage")] - type RangeType = StorageVariants2; - fn active(&self, w: Range) -> bool { for_all!(self, props => props.active(w)) } diff --git a/raphtory-storage/src/graph/variants/storage_variants3.rs b/raphtory-storage/src/graph/variants/storage_variants3.rs index 2ac2d41e20..dcb32d8b7c 100644 --- a/raphtory-storage/src/graph/variants/storage_variants3.rs +++ b/raphtory-storage/src/graph/variants/storage_variants3.rs @@ -19,21 +19,11 @@ use std::ops::Range; ParallelIterator, IndexedParallelIterator, )] -pub enum StorageVariants3 { +pub enum StorageVariants3 { Mem(Mem), Unlocked(Unlocked), - #[cfg(feature = "storage")] - Disk(Disk), } -#[cfg(feature = "storage")] -macro_rules! SelfType { - ($Mem:ident, $Unlocked:ident, $Disk:ident) => { - StorageVariants3<$Mem, $Unlocked, $Disk> - }; -} - -#[cfg(not(feature = "storage"))] macro_rules! SelfType { ($Mem:ident, $Unlocked:ident, $Disk:ident) => { StorageVariants3<$Mem, $Unlocked> @@ -45,8 +35,6 @@ macro_rules! for_all { match $value { StorageVariants3::Mem($pattern) => $result, StorageVariants3::Unlocked($pattern) => $result, - #[cfg(feature = "storage")] - StorageVariants3::Disk($pattern) => $result, } }; } @@ -56,18 +44,12 @@ macro_rules! for_all_iter { match $value { StorageVariants3::Mem($pattern) => StorageVariants3::Mem($result), StorageVariants3::Unlocked($pattern) => StorageVariants3::Unlocked($result), - #[cfg(feature = "storage")] - StorageVariants3::Disk($pattern) => StorageVariants3::Disk($result), } }; } -impl< - 'a, - Mem: TPropOps<'a> + 'a, - Unlocked: TPropOps<'a> + 'a, - #[cfg(feature = "storage")] Disk: TPropOps<'a> + 'a, - > TPropOps<'a> for SelfType!(Mem, Unlocked, Disk) +impl<'a, Mem: TPropOps<'a> + 'a, Unlocked: TPropOps<'a> + 'a> TPropOps<'a> + for SelfType!(Mem, Unlocked, Disk) { fn last_before(&self, t: TimeIndexEntry) -> Option<(TimeIndexEntry, Prop)> { for_all!(self, props => props.last_before(t)) diff --git a/raphtory-storage/src/layer_ops.rs b/raphtory-storage/src/layer_ops.rs index 6d190a58b7..8780f053ff 100644 --- a/raphtory-storage/src/layer_ops.rs +++ b/raphtory-storage/src/layer_ops.rs @@ -19,8 +19,6 @@ pub trait InternalLayerOps: CoreGraphOps { GraphStorage::Mem(LockedGraph { graph, .. }) | GraphStorage::Unlocked(graph) => { graph.layer_ids(key) } - #[cfg(feature = "storage")] - GraphStorage::Disk(graph) => graph.layer_ids_from_names(key), }?; Ok(layer_ids.intersect(self.layer_ids())) } @@ -31,8 +29,6 @@ pub trait InternalLayerOps: CoreGraphOps { GraphStorage::Unlocked(graph) | GraphStorage::Mem(LockedGraph { graph, .. }) => { graph.valid_layer_ids(key) } - #[cfg(feature = "storage")] - GraphStorage::Disk(graph) => graph.valid_layer_ids_from_names(key), }; layer_ids.intersect(self.layer_ids()) } diff --git a/raphtory-storage/src/lib.rs b/raphtory-storage/src/lib.rs index 98b6be3a3b..8fba0f0625 100644 --- a/raphtory-storage/src/lib.rs +++ b/raphtory-storage/src/lib.rs @@ -1,6 +1,4 @@ pub mod core_ops; -#[cfg(feature = "storage")] -pub mod disk; pub mod graph; pub mod layer_ops; pub mod mutation; diff --git a/raphtory/src/algorithms/dynamics/temporal/epidemics.rs b/raphtory/src/algorithms/dynamics/temporal/epidemics.rs index 4ce237cdd1..ea8c1fc2e7 100644 --- a/raphtory/src/algorithms/dynamics/temporal/epidemics.rs +++ b/raphtory/src/algorithms/dynamics/temporal/epidemics.rs @@ -263,8 +263,6 @@ mod test { use raphtory_api::core::utils::logging::global_info_logger; use rayon::prelude::*; use stats::{mean, stddev}; - #[cfg(feature = "storage")] - use tempfile::TempDir; use tracing::info; fn correct_res(x: f64) -> f64 { @@ -386,36 +384,4 @@ mod test { inner_test(event_rate, recovery_rate, p); } - - #[cfg(feature = "storage")] - #[test] - fn compare_disk_with_in_mem() { - let event_rate = 0.00000001; - let recovery_rate = 0.000000001; - let p = 0.3; - - let mut rng = SmallRng::seed_from_u64(0); - let g = generate_graph(1000, event_rate, &mut rng); - let test_dir = TempDir::new().unwrap(); - let disk_graph = g.persist_as_disk_graph(test_dir.path()).unwrap(); - let mut rng = SmallRng::seed_from_u64(0); - let res_arrow = temporal_SEIR( - &disk_graph, - Some(recovery_rate), - None, - p, - 0, - Number(1), - &mut rng, - ) - .unwrap(); - - let mut rng = SmallRng::seed_from_u64(0); - let res_mem = - temporal_SEIR(&g, Some(recovery_rate), None, p, 0, Number(1), &mut rng).unwrap(); - - assert!(res_mem - .iter() - .all(|(key, val)| res_arrow.get_by_node(key.id()).unwrap() == val)); - } } diff --git a/raphtory/src/db/api/storage/graph/storage_ops/disk_storage.rs b/raphtory/src/db/api/storage/graph/storage_ops/disk_storage.rs index a52aed50fa..9e79633557 100644 --- a/raphtory/src/db/api/storage/graph/storage_ops/disk_storage.rs +++ b/raphtory/src/db/api/storage/graph/storage_ops/disk_storage.rs @@ -1008,233 +1008,3 @@ mod test { assert_eq!(actual, expected); } } - -#[cfg(feature = "storage")] -#[cfg(test)] -mod storage_tests { - use std::collections::BTreeSet; - - use itertools::Itertools; - use proptest::prelude::*; - use tempfile::TempDir; - - use crate::{ - db::{ - api::storage::graph::storage_ops::disk_storage::IntoGraph, - graph::graph::assert_graph_equal, - }, - prelude::{AdditionOps, Graph, GraphViewOps, NodeViewOps, NO_PROPS, *}, - }; - use raphtory_api::core::storage::arc_str::OptionAsStr; - use raphtory_core::entities::nodes::node_ref::AsNodeRef; - use raphtory_storage::{disk::DiskGraphStorage, mutation::addition_ops::InternalAdditionOps}; - - #[test] - fn test_merge() { - let g1 = Graph::new(); - g1.add_node(0, 0, [("node_prop", 0f64)], Some("1")).unwrap(); - g1.add_node(0, 1, NO_PROPS, None).unwrap(); - g1.add_node(0, 2, [("node_prop", 2f64)], Some("2")).unwrap(); - g1.add_edge(1, 0, 1, [("test", 1i32)], None).unwrap(); - g1.add_edge(2, 0, 1, [("test", 2i32)], Some("1")).unwrap(); - g1.add_edge(2, 1, 2, [("test2", "test")], None).unwrap(); - g1.node(1) - .unwrap() - .add_metadata([("const_str", "test")]) - .unwrap(); - g1.node(0) - .unwrap() - .add_updates(3, [("test", "test")]) - .unwrap(); - - let g2 = Graph::new(); - g2.add_node(1, 0, [("node_prop", 1f64)], None).unwrap(); - g2.add_node(0, 1, NO_PROPS, None).unwrap(); - g2.add_node(3, 2, [("node_prop", 3f64)], Some("3")).unwrap(); - g2.add_edge(1, 0, 1, [("test", 2i32)], None).unwrap(); - g2.add_edge(3, 0, 1, [("test", 3i32)], Some("2")).unwrap(); - g2.add_edge(2, 1, 2, [("test2", "test")], None).unwrap(); - g2.node(1) - .unwrap() - .add_metadata([("const_str2", "test2")]) - .unwrap(); - g2.node(0) - .unwrap() - .add_updates(3, [("test", "test")]) - .unwrap(); - let g1_dir = TempDir::new().unwrap(); - let g2_dir = TempDir::new().unwrap(); - let gm_dir = TempDir::new().unwrap(); - - let g1_a = DiskGraphStorage::from_graph(&g1, g1_dir.path()).unwrap(); - let g2_a = DiskGraphStorage::from_graph(&g2, g2_dir.path()).unwrap(); - - let gm = g1_a - .merge_by_sorted_gids(&g2_a, &gm_dir) - .unwrap() - .into_graph(); - - let n0 = gm.node(0).unwrap(); - assert_eq!( - n0.properties() - .temporal() - .get("node_prop") - .unwrap() - .iter() - .collect_vec(), - [(0, Prop::F64(0.)), (1, Prop::F64(1.))] - ); - assert_eq!( - n0.properties() - .temporal() - .get("test") - .unwrap() - .iter() - .collect_vec(), - [(3, Prop::str("test")), (3, Prop::str("test"))] - ); - assert_eq!(n0.node_type().as_str(), Some("1")); - let n1 = gm.node(1).unwrap(); - assert_eq!(n1.metadata().get("const_str"), Some(Prop::str("test"))); - assert_eq!(n1.metadata().get("const_str2").unwrap_str(), "test2"); - assert!(n1 - .properties() - .temporal() - .values() - .all(|prop| prop.values().next().is_none())); - let n2 = gm.node(2).unwrap(); - assert_eq!(n2.node_type().as_str(), Some("3")); // right has priority - - assert_eq!( - gm.default_layer() - .edges() - .id() - .filter_map(|(a, b)| a.as_u64().zip(b.as_u64())) - .collect::>(), - [(0, 1), (1, 2)] - ); - assert_eq!( - gm.valid_layers("1") - .edges() - .id() - .filter_map(|(a, b)| a.as_u64().zip(b.as_u64())) - .collect::>(), - [(0, 1)] - ); - assert_eq!( - gm.valid_layers("2") - .edges() - .id() - .filter_map(|(a, b)| a.as_u64().zip(b.as_u64())) - .collect::>(), - [(0, 1)] - ); - } - - fn add_edges(g: &Graph, edges: &[(i64, u64, u64)]) { - let nodes: BTreeSet<_> = edges - .iter() - .flat_map(|(_, src, dst)| [*src, *dst]) - .collect(); - for n in nodes { - g.resolve_node(n.as_node_ref()).unwrap(); - } - for (t, src, dst) in edges { - g.add_edge(*t, *src, *dst, NO_PROPS, None).unwrap(); - } - } - - fn inner_merge_test(left_edges: &[(i64, u64, u64)], right_edges: &[(i64, u64, u64)]) { - let left_g = Graph::new(); - add_edges(&left_g, left_edges); - let right_g = Graph::new(); - add_edges(&right_g, right_edges); - let merged_g_expected = Graph::new(); - add_edges(&merged_g_expected, left_edges); - add_edges(&merged_g_expected, right_edges); - - let left_dir = TempDir::new().unwrap(); - let right_dir = TempDir::new().unwrap(); - let merged_dir = TempDir::new().unwrap(); - - let left_g_disk = DiskGraphStorage::from_graph(&left_g, left_dir.path()).unwrap(); - let right_g_disk = DiskGraphStorage::from_graph(&right_g, right_dir.path()).unwrap(); - - let merged_g_disk = left_g_disk - .merge_by_sorted_gids(&right_g_disk, &merged_dir) - .unwrap(); - assert_graph_equal(&merged_g_disk.into_graph(), &merged_g_expected) - } - - #[test] - fn test_merge_proptest() { - proptest!(|(left_edges in prop::collection::vec((0i64..10, 0u64..10, 0u64..10), 0..=100), right_edges in prop::collection::vec((0i64..10, 0u64..10, 0u64..10), 0..=100))| { - inner_merge_test(&left_edges, &right_edges) - }) - } - - #[test] - fn test_one_empty_graph_non_zero_time() { - inner_merge_test(&[], &[(1, 0, 0)]) - } - #[test] - fn test_empty_graphs() { - inner_merge_test(&[], &[]) - } - - #[test] - fn test_one_empty_graph() { - inner_merge_test(&[], &[(0, 0, 0)]) - } - - #[test] - fn inbounds_not_merging() { - inner_merge_test(&[], &[(0, 0, 0), (0, 0, 1), (0, 0, 2)]) - } - - #[test] - fn inbounds_not_merging_take2() { - inner_merge_test( - &[(0, 0, 2)], - &[ - (0, 1, 0), - (0, 0, 0), - (0, 0, 0), - (0, 0, 0), - (0, 0, 0), - (0, 0, 0), - (0, 0, 0), - ], - ) - } - - #[test] - fn offsets_panic_overflow() { - inner_merge_test( - &[ - (0, 0, 4), - (0, 0, 4), - (0, 0, 0), - (0, 0, 4), - (0, 1, 2), - (0, 3, 4), - ], - &[(0, 0, 5), (0, 2, 0)], - ) - } - - #[test] - fn inbounds_not_merging_take3() { - inner_merge_test( - &[ - (0, 0, 4), - (0, 0, 4), - (0, 0, 0), - (0, 0, 4), - (0, 1, 2), - (0, 3, 4), - ], - &[(0, 0, 3), (0, 0, 4), (0, 2, 2), (0, 0, 5), (0, 0, 6)], - ) - } -} diff --git a/raphtory/src/db/api/storage/graph/storage_ops/mod.rs b/raphtory/src/db/api/storage/graph/storage_ops/mod.rs index f9aa55f052..ff86ad3374 100644 --- a/raphtory/src/db/api/storage/graph/storage_ops/mod.rs +++ b/raphtory/src/db/api/storage/graph/storage_ops/mod.rs @@ -1,8 +1,6 @@ use crate::db::api::{storage::storage::Storage, view::internal::InternalStorageOps}; use raphtory_storage::graph::graph::GraphStorage; -#[cfg(feature = "storage")] -pub(crate) mod disk_storage; pub mod edge_filter; pub mod list_ops; pub mod materialize; diff --git a/raphtory/src/db/graph/assertions.rs b/raphtory/src/db/graph/assertions.rs index 54b75c6380..63ba2a3d88 100644 --- a/raphtory/src/db/graph/assertions.rs +++ b/raphtory/src/db/graph/assertions.rs @@ -14,11 +14,6 @@ use crate::{ #[cfg(feature = "search")] use crate::prelude::IndexMutationOps; use raphtory_api::core::Direction; -#[cfg(feature = "storage")] -use { - crate::db::api::storage::graph::storage_ops::disk_storage::IntoGraph, - raphtory_storage::disk::DiskGraphStorage, tempfile::TempDir, -}; #[cfg(feature = "search")] pub use crate::db::api::view::SearchableGraphOps; @@ -274,30 +269,8 @@ fn assert_results( let result = apply.apply(graph); assert_eq!(expected, result); } - TestGraphVariants::EventDiskGraph => { - #[cfg(feature = "storage")] - { - let tmp = TempDir::new().unwrap(); - let graph = graph.persist_as_disk_graph(tmp.path()).unwrap(); - pre_transform(&graph); - let graph = transform.apply(graph); - let result = apply.apply(graph); - assert_eq!(expected, result); - } - } - TestGraphVariants::PersistentDiskGraph => { - #[cfg(feature = "storage")] - { - let tmp = TempDir::new().unwrap(); - let graph = DiskGraphStorage::from_graph(&graph, &tmp).unwrap(); - let graph = graph.into_graph(); - pre_transform(&graph); - let graph = graph.persistent_graph(); - let graph = transform.apply(graph); - let result = apply.apply(graph); - assert_eq!(expected, result); - } - } + TestGraphVariants::EventDiskGraph => {} + TestGraphVariants::PersistentDiskGraph => {} } } } diff --git a/raphtory/src/db/graph/views/deletion_graph.rs b/raphtory/src/db/graph/views/deletion_graph.rs index 3ca57038b2..0c89c63e67 100644 --- a/raphtory/src/db/graph/views/deletion_graph.rs +++ b/raphtory/src/db/graph/views/deletion_graph.rs @@ -427,7 +427,7 @@ mod test_deletions { use raphtory_api::core::entities::GID; use raphtory_storage::mutation::addition_ops::InternalAdditionOps; use rayon::ThreadPoolBuilder; - use std::ops::{Deref, Range}; + use std::ops::Range; #[test] fn test_nodes() { @@ -661,7 +661,6 @@ mod test_deletions { }) } - #[test] #[test] fn materialize_window_multilayer() { let g = PersistentGraph::new(); diff --git a/raphtory/src/db/graph/views/window_graph.rs b/raphtory/src/db/graph/views/window_graph.rs index 31865b6469..d4fbeaa71d 100644 --- a/raphtory/src/db/graph/views/window_graph.rs +++ b/raphtory/src/db/graph/views/window_graph.rs @@ -614,8 +614,6 @@ mod views_test { use rand::{prelude::*, Rng}; use raphtory_api::core::{entities::GID, utils::logging::global_info_logger}; use rayon::prelude::*; - #[cfg(feature = "storage")] - use tempfile::TempDir; use tracing::{error, info}; #[test] @@ -767,55 +765,6 @@ mod views_test { }); } - // FIXME: Issue #46 - // #[quickcheck] - // fn windowed_disk_graph_has_node(mut vs: Vec<(i64, u64)>) -> TestResult { - // global_info_logger(); - // if vs.is_empty() { - // return TestResult::discard(); - // } - // - // vs.sort_by_key(|v| v.1); // Sorted by node - // vs.dedup_by_key(|v| v.1); // Have each node only once to avoid headaches - // vs.sort_by_key(|v| v.0); // Sorted by time - // - // let rand_start_index = thread_rng().gen_range(0..vs.len()); - // let rand_end_index = thread_rng().gen_range(rand_start_index..vs.len()); - // - // let g = Graph::new(); - // for (t, v) in &vs { - // g.add_node(*t, *v, NO_PROPS, None) - // .map_err(|err| error!("{:?}", err)) - // .ok(); - // } - // let test_dir = TempDir::new().unwrap(); - #[cfg(feature = "storage")] - // let g = g.persist_as_disk_graph(test_dir.path()).unwrap(); - // - // let start = vs.get(rand_start_index).expect("start index in range").0; - // let end = vs.get(rand_end_index).expect("end index in range").0; - // - // let wg = g.window(start, end); - // - // let rand_test_index: usize = thread_rng().gen_range(0..vs.len()); - // - // let (i, v) = vs.get(rand_test_index).expect("test index in range"); - // if (start..end).contains(i) { - // if wg.has_node(*v) { - // TestResult::passed() - // } else { - // TestResult::error(format!( - // "Node {:?} was not in window {:?}", - // (i, v), - // start..end - // )) - // } - // } else if !wg.has_node(*v) { - // TestResult::passed() - // } else { - // TestResult::error(format!("Node {:?} was in window {:?}", (i, v), start..end)) - // } - // } #[test] fn windowed_graph_has_edge() { proptest!(|(mut edges: Vec<(i64, (u64, u64))>)| { @@ -850,44 +799,6 @@ mod views_test { }); } - #[cfg(feature = "storage")] - #[test] - fn windowed_disk_graph_has_edge() { - proptest!(|(mut edges: Vec<(i64, (u64, u64))>)| { - prop_assume!(!edges.is_empty()); - - edges.sort_by_key(|e| e.1); // Sorted by edge - edges.dedup_by_key(|e| e.1); // Have each edge only once to avoid headaches - edges.sort_by_key(|e| e.0); // Sorted by time - - let rand_start_index = thread_rng().gen_range(0..edges.len()); - let rand_end_index = thread_rng().gen_range(rand_start_index..edges.len()); - - let g = Graph::new(); - - for (t, e) in &edges { - g.add_edge(*t, e.0, e.1, NO_PROPS, None).unwrap(); - } - - let test_dir = TempDir::new().unwrap(); - let disk_graph = g.persist_as_disk_graph(test_dir.path()).unwrap(); - - let start = edges.get(rand_start_index).expect("start index in range").0; - let end = edges.get(rand_end_index).expect("end index in range").0; - - let wg = disk_graph.window(start, end); - - let rand_test_index: usize = thread_rng().gen_range(0..edges.len()); - - let (i, e) = edges.get(rand_test_index).expect("test index in range"); - if (start..end).contains(i) { - prop_assert!(wg.has_edge(e.0, e.1), "Edge {:?} was not in window {:?}", (i, e), start..end); - } else { - prop_assert!(!wg.has_edge(e.0, e.1), "Edge {:?} was in window {:?}", (i, e), start..end); - } - }); - } - #[test] fn windowed_graph_edge_count() { proptest!(|(mut edges: Vec<(i64, (u64, u64))>, window: Range)| { diff --git a/raphtory/src/errors.rs b/raphtory/src/errors.rs index 4e590fd530..0c5a0de099 100644 --- a/raphtory/src/errors.rs +++ b/raphtory/src/errors.rs @@ -26,8 +26,6 @@ use tracing::error; #[cfg(feature = "io")] use parquet::errors::ParquetError; -#[cfg(feature = "storage")] -use pometry_storage::RAError; #[cfg(feature = "arrow")] use { polars_arrow::{datatypes::ArrowDataType, legacy::error}, @@ -270,10 +268,6 @@ pub enum GraphError { )] ColumnDoesNotExist(String), - #[cfg(feature = "storage")] - #[error("Raphtory Arrow Error: {0}")] - DiskGraphError(#[from] RAError), - #[cfg(feature = "search")] #[error("Index operation failed: {source}")] IndexError { diff --git a/raphtory/src/io/arrow/df_loaders.rs b/raphtory/src/io/arrow/df_loaders.rs index 12b8beaa36..48338f8dcc 100644 --- a/raphtory/src/io/arrow/df_loaders.rs +++ b/raphtory/src/io/arrow/df_loaders.rs @@ -955,174 +955,6 @@ mod tests { use polars_arrow::array::{MutableArray, MutablePrimitiveArray, MutableUtf8Array}; use proptest::proptest; - #[cfg(feature = "storage")] - mod load_multi_layer { - use std::{ - fs::File, - path::{Path, PathBuf}, - }; - - use crate::{ - db::graph::graph::assert_graph_equal, io::parquet_loaders::load_edges_from_parquet, - prelude::Graph, test_utils::build_edge_list, - }; - use polars_arrow::{ - array::{PrimitiveArray, Utf8Array}, - types::NativeType, - }; - use polars_core::{frame::DataFrame, prelude::*}; - use polars_io::prelude::{ParquetCompression, ParquetWriter}; - use pometry_storage::{graph::TemporalGraph, load::ExternalEdgeList}; - use prop::sample::SizeRange; - use proptest::prelude::*; - use raphtory_storage::{disk::DiskGraphStorage, graph::graph::GraphStorage}; - use tempfile::TempDir; - - fn build_edge_list_df( - len: usize, - num_nodes: impl Strategy, - num_layers: impl Into, - ) -> impl Strategy> { - let layer = num_nodes - .prop_flat_map(move |num_nodes| { - build_edge_list(len, num_nodes) - .prop_filter("no empty edge lists", |el| !el.is_empty()) - }) - .prop_map(move |mut rows| { - rows.sort_by_key(|(src, dst, time, _, _)| (*src, *dst, *time)); - new_df_from_rows(&rows) - }); - proptest::collection::vec(layer, num_layers) - } - - fn new_df_from_rows(rows: &[(u64, u64, i64, String, i64)]) -> DataFrame { - let src = native_series("src", rows.iter().map(|(src, _, _, _, _)| *src)); - let dst = native_series("dst", rows.iter().map(|(_, dst, _, _, _)| *dst)); - let time = native_series("time", rows.iter().map(|(_, _, time, _, _)| *time)); - let int_prop = native_series( - "int_prop", - rows.iter().map(|(_, _, _, _, int_prop)| *int_prop), - ); - - let str_prop = Series::from_arrow( - "str_prop", - Utf8Array::::from_iter( - rows.iter() - .map(|(_, _, _, str_prop, _)| Some(str_prop.clone())), - ) - .boxed(), - ) - .unwrap(); - - DataFrame::new(vec![src, dst, time, str_prop, int_prop]).unwrap() - } - - fn native_series(name: &str, is: impl IntoIterator) -> Series { - let is = PrimitiveArray::from_vec(is.into_iter().collect()); - Series::from_arrow(name, is.boxed()).unwrap() - } - - fn check_layers_from_df(input: Vec, num_threads: usize) { - let root_dir = TempDir::new().unwrap(); - let graph_dir = TempDir::new().unwrap(); - let layers = input - .into_iter() - .enumerate() - .map(|(i, df)| (i.to_string(), df)) - .collect::>(); - let edge_lists = write_layers(&layers, root_dir.path()); - - let expected = Graph::new(); - for edge_list in &edge_lists { - load_edges_from_parquet( - &expected, - &edge_list.path, - "time", - "src", - "dst", - &["int_prop", "str_prop"], - &[], - None, - Some(edge_list.layer), - None, - ) - .unwrap(); - } - - let g = TemporalGraph::from_parquets( - num_threads, - 13, - 23, - graph_dir.path(), - edge_lists, - &[], - None, - None, - None, - ) - .unwrap(); - let actual = - Graph::from_internal_graph(GraphStorage::Disk(DiskGraphStorage::new(g).into())); - - assert_graph_equal(&expected, &actual); - - let g = TemporalGraph::new(graph_dir.path()).unwrap(); - - for edge in g.edges_iter() { - assert!(g.find_edge(edge.src_id(), edge.dst_id()).is_some()); - } - - let actual = - Graph::from_internal_graph(GraphStorage::Disk(DiskGraphStorage::new(g).into())); - assert_graph_equal(&expected, &actual); - } - - #[test] - fn load_from_multiple_layers() { - proptest!(|(input in build_edge_list_df(50, 1u64..23, 1..10, ), num_threads in 1usize..2)| { - check_layers_from_df(input, num_threads) - }); - } - - #[test] - fn single_layer_single_edge() { - let df = new_df_from_rows(&[(0, 0, 1, "".to_owned(), 2)]); - check_layers_from_df(vec![df], 1) - } - - fn write_layers<'a>( - layers: &'a [(String, DataFrame)], - root_dir: &Path, - ) -> Vec> { - let mut paths = vec![]; - for (name, df) in layers.iter() { - let layer_dir = root_dir.join(name); - std::fs::create_dir_all(&layer_dir).unwrap(); - let layer_path = layer_dir.join("edges.parquet"); - - paths.push( - ExternalEdgeList::new( - name, - layer_path.to_path_buf(), - "src", - "dst", - "time", - vec![], - ) - .unwrap(), - ); - - let file = File::create(layer_path).unwrap(); - let mut df = df.clone(); - ParquetWriter::new(file) - .with_compression(ParquetCompression::Snappy) - .finish(&mut df) - .unwrap(); - } - paths - } - } - fn build_df( chunk_size: usize, edges: &[(u64, u64, i64, String, i64)], diff --git a/raphtory/src/io/parquet_loaders.rs b/raphtory/src/io/parquet_loaders.rs index f1580d9d0e..0671964097 100644 --- a/raphtory/src/io/parquet_loaders.rs +++ b/raphtory/src/io/parquet_loaders.rs @@ -1,8 +1,8 @@ use crate::{ db::api::view::StaticGraphViewOps, - errors::InvalidPathReason::PathDoesNotExist, + errors::{GraphError, InvalidPathReason::PathDoesNotExist}, io::arrow::{dataframe::*, df_loaders::*}, - prelude::DeletionOps, + prelude::{AdditionOps, DeletionOps, PropertyAdditionOps}, serialise::incremental::InternalCache, }; use itertools::Itertools; @@ -11,6 +11,7 @@ use polars_parquet::{ read, read::{read_metadata, FileMetaData, FileReader}, }; +use raphtory_api::core::entities::properties::prop::Prop; use std::{ collections::HashMap, fs, @@ -18,19 +19,6 @@ use std::{ path::{Path, PathBuf}, }; -use crate::{ - errors::GraphError, - prelude::{AdditionOps, PropertyAdditionOps}, -}; -#[cfg(feature = "storage")] -use polars_arrow::{ - array::StructArray, - datatypes::{ArrowDataType, Field}, -}; -#[cfg(feature = "storage")] -use pometry_storage::RAError; -use raphtory_api::core::entities::properties::prop::Prop; - pub fn load_nodes_from_parquet< G: StaticGraphViewOps + PropertyAdditionOps + AdditionOps + InternalCache + std::fmt::Debug, >( @@ -349,35 +337,6 @@ pub fn get_parquet_file_paths(parquet_path: &Path) -> Result, Graph Ok(parquet_files) } -#[cfg(feature = "storage")] -pub fn read_struct_arrays( - path: &Path, - col_names: Option<&[&str]>, -) -> Result>, GraphError> { - let readers = get_parquet_file_paths(path)? - .into_iter() - .map(|path| { - read_parquet_file(path, col_names).map(|(col_names, reader, _)| (col_names, reader)) - }) - .collect::, _>>()?; - - let chunks = readers.into_iter().flat_map(|(field_names, iter)| { - iter.map(move |cols| { - cols.map(|col| { - let values = col.into_arrays(); - let fields = values - .iter() - .zip(field_names.iter()) - .map(|(arr, field_name)| Field::new(field_name, arr.data_type().clone(), true)) - .collect::>(); - StructArray::new(ArrowDataType::Struct(fields), values, None) - }) - .map_err(RAError::Arrow) - }) - }); - Ok(chunks) -} - #[cfg(test)] mod test { use super::*; diff --git a/raphtory/src/lib.rs b/raphtory/src/lib.rs index a98cfb34c9..1c42c01c35 100644 --- a/raphtory/src/lib.rs +++ b/raphtory/src/lib.rs @@ -142,12 +142,6 @@ pub mod prelude { }, }; - #[cfg(feature = "storage")] - pub use { - crate::db::api::storage::graph::storage_ops::disk_storage::IntoGraph, - raphtory_storage::disk::{DiskGraphStorage, ParquetLayerCols}, - }; - #[cfg(feature = "proto")] pub use crate::serialise::{ parquet::{ParquetDecoder, ParquetEncoder}, @@ -158,9 +152,6 @@ pub mod prelude { pub use crate::db::api::{mutation::IndexMutationOps, view::SearchableGraphOps}; } -#[cfg(feature = "storage")] -pub use polars_arrow as arrow2; - pub use raphtory_api::{atomic_extra, core::utils::logging}; #[cfg(test)] @@ -178,8 +169,6 @@ mod test_utils { mutation::addition_ops::{InternalAdditionOps, SessionAdditionOps}, }; use std::{collections::HashMap, sync::Arc}; - #[cfg(feature = "storage")] - use tempfile::TempDir; pub(crate) fn test_graph(graph: &Graph, test: impl FnOnce(&Graph)) { test(graph) @@ -189,18 +178,9 @@ mod test_utils { macro_rules! test_storage { ($graph:expr, $test:expr) => { $crate::test_utils::test_graph($graph, $test); - #[cfg(feature = "storage")] - $crate::test_utils::test_disk_graph($graph, $test); }; } - #[cfg(feature = "storage")] - pub(crate) fn test_disk_graph(graph: &Graph, test: impl FnOnce(&Graph)) { - let test_dir = TempDir::new().unwrap(); - let disk_graph = graph.persist_as_disk_graph(test_dir.path()).unwrap(); - test(&disk_graph) - } - pub(crate) fn build_edge_list( len: usize, num_nodes: u64, diff --git a/raphtory/src/python/graph/graph.rs b/raphtory/src/python/graph/graph.rs index 2ef8ae0ce0..4f5507fa5d 100644 --- a/raphtory/src/python/graph/graph.rs +++ b/raphtory/src/python/graph/graph.rs @@ -170,18 +170,6 @@ impl PyGraph { (PyGraphEncoder, (state,)) } - /// Persist graph on disk - /// - /// Arguments: - /// graph_dir (str | PathLike): the folder where the graph will be persisted - /// - /// Returns: - /// Graph: a view of the persisted graph - #[cfg(feature = "storage")] - pub fn to_disk_graph(&self, graph_dir: PathBuf) -> Result { - self.graph.persist_as_disk_graph(graph_dir) - } - /// Persist graph to parquet files /// /// Arguments: diff --git a/raphtory/src/python/graph/graph_with_deletions.rs b/raphtory/src/python/graph/graph_with_deletions.rs index 077190805d..c7568a743e 100644 --- a/raphtory/src/python/graph/graph_with_deletions.rs +++ b/raphtory/src/python/graph/graph_with_deletions.rs @@ -107,11 +107,6 @@ impl PyPersistentGraph { ) } - #[cfg(feature = "storage")] - pub fn to_disk_graph(&self, graph_dir: PathBuf) -> Result { - self.graph.persist_as_disk_graph(graph_dir) - } - fn __reduce__(&self) -> (PyGraphEncoder, (Vec,)) { let state = self.graph.encode_to_vec(); (PyGraphEncoder, (state,)) diff --git a/raphtory/src/python/graph/mod.rs b/raphtory/src/python/graph/mod.rs index bcd8ddc9b9..8c8d0ede75 100644 --- a/raphtory/src/python/graph/mod.rs +++ b/raphtory/src/python/graph/mod.rs @@ -1,5 +1,3 @@ -#[cfg(feature = "storage")] -pub mod disk_graph; pub mod edge; pub mod graph; pub mod graph_with_deletions; diff --git a/raphtory/src/python/packages/algorithms.rs b/raphtory/src/python/packages/algorithms.rs index 96d8a72333..e7661936fa 100644 --- a/raphtory/src/python/packages/algorithms.rs +++ b/raphtory/src/python/packages/algorithms.rs @@ -1,7 +1,5 @@ #![allow(non_snake_case)] -#[cfg(feature = "storage")] -use crate::python::graph::disk_graph::PyDiskGraph; use crate::{ algorithms::{ bipartite::max_weight_matching::{max_weight_matching as mwm, Matching}, @@ -71,8 +69,6 @@ use crate::{ utils::{PyNodeRef, PyTime}, }, }; -#[cfg(feature = "storage")] -use pometry_storage::algorithms::connected_components::connected_components as connected_components_rs; use pyo3::{prelude::*, types::PyList}; use rand::{prelude::StdRng, SeedableRng}; use raphtory_api::core::Direction; @@ -159,13 +155,6 @@ pub fn strongly_connected_components( components::strongly_connected_components(&graph.graph) } -#[cfg(feature = "storage")] -#[pyfunction] -#[pyo3(signature = (graph))] -pub fn connected_components(graph: &PyDiskGraph) -> Vec { - connected_components_rs(graph.0.as_ref()) -} - /// In components -- Finding the "in-component" of a node in a directed graph involves identifying all nodes that can be reached following only incoming edges. /// /// Arguments: diff --git a/raphtory/src/python/packages/base_modules.rs b/raphtory/src/python/packages/base_modules.rs index 643c9512ac..a3ff2116cb 100644 --- a/raphtory/src/python/packages/base_modules.rs +++ b/raphtory/src/python/packages/base_modules.rs @@ -1,7 +1,4 @@ //ALGORITHMS - -#[cfg(feature = "storage")] -use crate::python::graph::disk_graph::PyDiskGraph; use crate::{ add_classes, add_functions, python::{ @@ -61,8 +58,6 @@ pub fn add_raphtory_classes(m: &Bound) -> PyResult<()> { m.add_function(wrap_pyfunction!(version, m)?)?; - #[cfg(feature = "storage")] - add_classes!(m, PyDiskGraph); Ok(()) } @@ -114,8 +109,6 @@ pub fn base_algorithm_module(py: Python) -> Result, PyErr> { ); add_classes!(&algorithm_module, PyMatching, PyInfected); - #[cfg(feature = "storage")] - add_functions!(&algorithm_module, connected_components); Ok(algorithm_module) } diff --git a/raphtory/src/serialise/mod.rs b/raphtory/src/serialise/mod.rs index cca9e03f3f..764b34a3cb 100644 --- a/raphtory/src/serialise/mod.rs +++ b/raphtory/src/serialise/mod.rs @@ -19,8 +19,6 @@ use crate::{ serialise::metadata::GraphMetadata, }; pub use proto::Graph as ProtoGraph; -#[cfg(feature = "storage")] -use raphtory_storage::disk::DiskGraphStorage; pub use serialise::{CacheOps, InternalStableDecode, StableDecode, StableEncode}; use std::{ fs::{self, File, OpenOptions}, @@ -148,21 +146,7 @@ impl GraphFolder { info!( "Metadata file does not exist or is invalid. Attempting to recreate..." ); - let graph: MaterializedGraph = if self.is_disk_graph() { - #[cfg(not(feature = "storage"))] - return Err(GraphError::DiskGraphNotFound); - #[cfg(feature = "storage")] - { - use crate::prelude::IntoGraph; - - MaterializedGraph::from( - DiskGraphStorage::load_from_dir(self.get_graph_path())? - .into_graph(), - ) - } - } else { - MaterializedGraph::decode(self)? - }; + let graph: MaterializedGraph = MaterializedGraph::decode(self)?; self.write_metadata(&graph)?; Ok(self.try_read_metadata()?) } From 4e53d02059a6c8cee1ce6fded146658a6d53957d Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Fri, 22 Aug 2025 15:48:20 +0200 Subject: [PATCH 136/321] fixes for metadata handling --- db4-storage/src/pages/node_page/writer.rs | 4 ++-- db4-storage/src/pages/test_utils/checkers.rs | 13 ++++++----- db4-storage/src/segments/node_entry.rs | 10 +++++++-- .../src/core/entities/properties/meta.rs | 22 +++++-------------- raphtory-api/src/core/storage/dict_mapper.rs | 12 ++++------ 5 files changed, 26 insertions(+), 35 deletions(-) diff --git a/db4-storage/src/pages/node_page/writer.rs b/db4-storage/src/pages/node_page/writer.rs index ce926b1bb8..0a948cce1e 100644 --- a/db4-storage/src/pages/node_page/writer.rs +++ b/db4-storage/src/pages/node_page/writer.rs @@ -135,9 +135,9 @@ impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> NodeWri props: impl IntoIterator, lsn: u64, ) { - self.mut_segment.as_mut()[layer_id].set_lsn(lsn); self.l_counter.update_time(t.t()); let (is_new_node, add) = self.mut_segment.add_props(t, pos, layer_id, props); + self.mut_segment.as_mut()[layer_id].set_lsn(lsn); self.page.increment_est_size(add); if is_new_node && !self.page.check_node(pos, layer_id) { self.l_counter.increment(layer_id); @@ -160,8 +160,8 @@ impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> NodeWri props: impl IntoIterator, lsn: u64, ) { - self.mut_segment.as_mut()[layer_id].set_lsn(lsn); let (is_new_node, add) = self.mut_segment.update_c_props(pos, layer_id, props); + self.mut_segment.as_mut()[layer_id].set_lsn(lsn); self.page.increment_est_size(add); if is_new_node && !self.page.check_node(pos, layer_id) { self.l_counter.increment(layer_id); diff --git a/db4-storage/src/pages/test_utils/checkers.rs b/db4-storage/src/pages/test_utils/checkers.rs index 7a57112513..aafc0c67f0 100644 --- a/db4-storage/src/pages/test_utils/checkers.rs +++ b/db4-storage/src/pages/test_utils/checkers.rs @@ -221,15 +221,16 @@ pub fn check_graph_with_nodes_support< let graph_dir = tempfile::tempdir().unwrap(); let graph = make_graph(graph_dir.path()); + let layer_id = graph.edge_meta().get_default_layer_id().unwrap(); for (node, t, t_props) in temp_props { - let err = graph.add_node_props(*t, *node, 0, t_props.clone()); + let err = graph.add_node_props(*t, *node, layer_id, t_props.clone()); assert!(err.is_ok(), "Failed to add node: {err:?}"); } for (node, const_props) in const_props { - let err = graph.update_node_const_props(*node, 0, const_props.clone()); + let err = graph.update_node_const_props(*node, layer_id, const_props.clone()); assert!(err.is_ok(), "Failed to add node: {err:?}"); } @@ -249,9 +250,9 @@ pub fn check_graph_with_nodes_support< let ne = graph.nodes().node(node); let node_entry = ne.as_ref(); let actual: Vec<_> = node_entry - .edge_additions(0) + .edge_additions(layer_id) .iter_t() - .merge(node_entry.node_additions(0).iter_t()) + .merge(node_entry.node_additions(layer_id).iter_t()) .collect(); assert_eq!( actual, ts_expected, @@ -279,7 +280,7 @@ pub fn check_graph_with_nodes_support< .metadata_mapper() .get_id(name) .unwrap_or_else(|| panic!("Failed to get prop id for {name}")); - let actual_props = node_entry.c_prop(0, prop_id); + let actual_props = node_entry.c_prop(layer_id, prop_id); if !const_props.is_empty() { let actual_prop = actual_props @@ -318,7 +319,7 @@ pub fn check_graph_with_nodes_support< let ne = graph.nodes().node(node); let node_entry = ne.as_ref(); let actual_props = node_entry - .temporal_prop_layer(0, prop_id) + .temporal_prop_layer(layer_id, prop_id) .iter_t() .collect::>(); diff --git a/db4-storage/src/segments/node_entry.rs b/db4-storage/src/segments/node_entry.rs index 4680a55bd0..93db87abf2 100644 --- a/db4-storage/src/segments/node_entry.rs +++ b/db4-storage/src/segments/node_entry.rs @@ -175,11 +175,17 @@ impl<'a> NodeRefOps<'a> for MemNodeRef<'a> { } fn c_prop(self, layer_id: usize, prop_id: usize) -> Option { - self.ns.as_ref()[layer_id].c_prop(self.pos, prop_id) + self.ns + .as_ref() + .get(layer_id) + .and_then(|layer| layer.c_prop(self.pos, prop_id)) } fn c_prop_str(self, layer_id: usize, prop_id: usize) -> Option<&'a str> { - self.ns.as_ref()[layer_id].c_prop_str(self.pos, prop_id) + self.ns + .as_ref() + .get(layer_id) + .and_then(|layer| layer.c_prop_str(self.pos, prop_id)) } fn node_additions>>(self, layer_id: L) -> Self::Additions { diff --git a/raphtory-api/src/core/entities/properties/meta.rs b/raphtory-api/src/core/entities/properties/meta.rs index 63b7ebfe4e..5196fecd9f 100644 --- a/raphtory-api/src/core/entities/properties/meta.rs +++ b/raphtory-api/src/core/entities/properties/meta.rs @@ -25,7 +25,7 @@ pub const NODE_TYPE_PROP_KEY: &str = "_raphtory_node_type"; pub const NODE_TYPE_IDX: usize = 1; pub const STATIC_GRAPH_LAYER: &str = "_static_graph"; -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Default)] pub struct Meta { temporal_prop_mapper: PropMapper, metadata_mapper: PropMapper, @@ -33,23 +33,11 @@ pub struct Meta { node_type_mapper: DictMapper, } -impl Default for Meta { - fn default() -> Self { - Meta { - temporal_prop_mapper: Default::default(), - metadata_mapper: Default::default(), - layer_mapper: DictMapper::new_layer_mapper(), - node_type_mapper: Default::default(), - } - } -} - impl Meta { - pub fn layer_iter(&self) -> impl Iterator + use<'_> { - self.layer_mapper.ids().map(move |id| { - let name = self.layer_mapper.get_name(id); - (id, name) - }) + pub fn all_layer_iter(&self) -> impl Iterator + use<'_> { + self.layer_mapper + .all_ids() + .zip(self.layer_mapper.all_keys()) } pub fn set_metadata_mapper(&mut self, meta: PropMapper) { diff --git a/raphtory-api/src/core/storage/dict_mapper.rs b/raphtory-api/src/core/storage/dict_mapper.rs index c1bf571c23..28fa987884 100644 --- a/raphtory-api/src/core/storage/dict_mapper.rs +++ b/raphtory-api/src/core/storage/dict_mapper.rs @@ -258,14 +258,6 @@ impl DictMapper { map_entry.insert_entry(id); } - pub fn set_reverse_id(&self, id: usize, name: impl Into) { - let mut keys = self.reverse_map.write(); - if keys.len() <= id { - keys.resize(id + 1, Default::default()) - } - keys[id] = name.into(); - } - pub fn has_id(&self, id: usize) -> bool { let guard = self.reverse_map.read(); guard.get(id).is_some() @@ -310,6 +302,10 @@ impl DictMapper { pub fn num_fields(&self) -> usize { self.map.read().len() } + + pub fn num_private_fields(&self) -> usize { + self.num_private_fields + } } #[cfg(test)] From 05806a1d1a725b15d5aafa8c629063d1ab989c26 Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Fri, 22 Aug 2025 18:36:25 +0200 Subject: [PATCH 137/321] fix node properties panics --- db4-storage/src/segments/node.rs | 2 +- db4-storage/src/segments/node_entry.rs | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/db4-storage/src/segments/node.rs b/db4-storage/src/segments/node.rs index 33545bc395..c4a125379c 100644 --- a/db4-storage/src/segments/node.rs +++ b/db4-storage/src/segments/node.rs @@ -335,7 +335,7 @@ impl MemNodeSegment { layer_id: usize, props: impl IntoIterator, ) -> (bool, usize) { - let segment_container = &mut self.layers[layer_id]; + let segment_container = self.get_or_create_layer(layer_id); let est_size = segment_container.est_size(); let row = segment_container diff --git a/db4-storage/src/segments/node_entry.rs b/db4-storage/src/segments/node_entry.rs index 93db87abf2..9fef4ea13d 100644 --- a/db4-storage/src/segments/node_entry.rs +++ b/db4-storage/src/segments/node_entry.rs @@ -139,8 +139,10 @@ impl<'a> WithTProps<'a> for MemNodeRef<'a> { prop_id: usize, ) -> impl Iterator + 'a { let node_pos = self.pos; - self.ns.as_ref()[layer_id] - .t_prop(node_pos, prop_id) + self.ns + .as_ref() + .get(layer_id) + .and_then(|layer| layer.t_prop(node_pos, prop_id)) .into_iter() } } From 679293e3c9b50394bdc8995999758d960e85298a Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Mon, 25 Aug 2025 12:29:23 +0200 Subject: [PATCH 138/321] remove useless closure --- db4-storage/src/pages/mod.rs | 36 ++++++++++++++++-------------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/db4-storage/src/pages/mod.rs b/db4-storage/src/pages/mod.rs index 8468ab4b21..332a9d2ca9 100644 --- a/db4-storage/src/pages/mod.rs +++ b/db4-storage/src/pages/mod.rs @@ -331,29 +331,25 @@ impl< let (src_chunk, _) = self.nodes.resolve_pos(src); let (dst_chunk, _) = self.nodes.resolve_pos(dst); - let acquire_node_writers = || { - if src_chunk < dst_chunk { - let src_writer = self.node_writer(src_chunk); - let dst_writer = self.node_writer(dst_chunk); - WriterPair::Different { - src_writer, - dst_writer, - } - } else if src_chunk > dst_chunk { - let dst_writer = self.node_writer(dst_chunk); - let src_writer = self.node_writer(src_chunk); - WriterPair::Different { - src_writer, - dst_writer, - } - } else { - let writer = self.node_writer(src_chunk); - WriterPair::Same { writer } + let node_writers = if src_chunk < dst_chunk { + let src_writer = self.node_writer(src_chunk); + let dst_writer = self.node_writer(dst_chunk); + WriterPair::Different { + src_writer, + dst_writer, } + } else if src_chunk > dst_chunk { + let dst_writer = self.node_writer(dst_chunk); + let src_writer = self.node_writer(src_chunk); + WriterPair::Different { + src_writer, + dst_writer, + } + } else { + let writer = self.node_writer(src_chunk); + WriterPair::Same { writer } }; - let node_writers = acquire_node_writers(); - let edge_writer = e_id.map(|e_id| self.edge_writer(e_id)); WriteSession::new(node_writers, edge_writer, self) From dcc60e1e299b538e83e50f39a084d740ec9d628c Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Tue, 26 Aug 2025 11:24:47 +0200 Subject: [PATCH 139/321] cannot sort unstable here or it might break on duplicate timestamps --- db4-storage/src/pages/test_utils/checkers.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/db4-storage/src/pages/test_utils/checkers.rs b/db4-storage/src/pages/test_utils/checkers.rs index aafc0c67f0..e6742f904e 100644 --- a/db4-storage/src/pages/test_utils/checkers.rs +++ b/db4-storage/src/pages/test_utils/checkers.rs @@ -396,7 +396,7 @@ pub fn check_graph_with_props_support< } edge_groups.iter_mut().for_each(|(_, props)| { - props.sort_unstable_by_key(|(t, _)| *t); + props.sort_by_key(|(t, _)| *t); }); // Group node additions and their timestamps @@ -418,7 +418,7 @@ pub fn check_graph_with_props_support< } node_groups.iter_mut().for_each(|(_, ts)| { - ts.sort_unstable(); + ts.sort(); }); for ((src, dst, prop_name), props) in edge_groups { From 1667c19be387e7b25226fe72a413f40a7431d9af Mon Sep 17 00:00:00 2001 From: Fadhil Abubaker Date: Wed, 27 Aug 2025 05:00:52 -0400 Subject: [PATCH 140/321] Switch node resolution to be sequential in `load_node_props_from_df` (#2246) * Return EventGraph/PersistentGraph correctly in decode_parquet * Remove par_iter from load_node_props_from_df --- .../test_graphql/test_apply_views.py | 28 +++++++++---------- raphtory-graphql/schema.graphql | 2 +- raphtory/src/io/arrow/df_loaders.rs | 15 ++++++---- raphtory/src/serialise/parquet/mod.rs | 12 ++++++-- 4 files changed, 33 insertions(+), 24 deletions(-) diff --git a/python/tests/test_base_install/test_graphql/test_apply_views.py b/python/tests/test_base_install/test_graphql/test_apply_views.py index 92cb4a2dc1..c24a36c253 100644 --- a/python/tests/test_base_install/test_graphql/test_apply_views.py +++ b/python/tests/test_base_install/test_graphql/test_apply_views.py @@ -1489,11 +1489,11 @@ def test_apply_view_node_filter(): name: "where" operator: EQUAL value: {str: "Berlin"} - + } } } - + ]) { nodes { list { @@ -1521,11 +1521,11 @@ def test_apply_view_edge_filter(): name: "where" operator: EQUAL value: {str: "fishbowl"} - + } } } - + ]) { edges { list { @@ -1756,7 +1756,7 @@ def test_apply_view_neighbours_latest(): query = """ { graph(path: "g") { - node(name: "1") { + node(name: "1") { neighbours { applyViews(views: [{latest: true}]) { list { @@ -1764,7 +1764,7 @@ def test_apply_view_neighbours_latest(): history } } - + } } } @@ -1801,7 +1801,7 @@ def test_apply_view_neighbours_layer(): history} } } - + } } }""" @@ -1833,7 +1833,7 @@ def test_apply_view_neighbours_exclude_layer(): history} } } - + } } }""" @@ -1863,7 +1863,7 @@ def test_apply_view_neighbours_layers(): history } } - } + } } } }""" @@ -1899,7 +1899,7 @@ def test_apply_view_neighbours_exclude_layers(): history } } - } + } } } }""" @@ -2101,7 +2101,7 @@ def test_apply_view_in_neighbours_shrink_start(): node(name: "7") { inNeighbours { applyViews(views: [{shrinkStart: 1735948800000}]) { - list { + list { name history } @@ -2135,7 +2135,7 @@ def test_apply_view_in_neighbours_shrink_end(): node(name: "2") { inNeighbours { applyViews(views: [{shrinkEnd: 1735862400000}]) { - list { + list { name history } @@ -2169,7 +2169,7 @@ def test_apply_view_in_neighbours_at(): node(name: "2") { inNeighbours { applyViews(views: [{at: 1735862400000}]) { - list { + list { name history } @@ -2295,7 +2295,7 @@ def test_valid_graph(): id latestTime } - } + } } } }""" diff --git a/raphtory-graphql/schema.graphql b/raphtory-graphql/schema.graphql index caeeda51c0..61d2c3a62b 100644 --- a/raphtory-graphql/schema.graphql +++ b/raphtory-graphql/schema.graphql @@ -344,8 +344,8 @@ type Graph { } type GraphAlgorithmPlugin { - pagerank(iterCount: Int!, threads: Int, tol: Float): [PagerankOutput!]! shortest_path(source: String!, targets: [String!]!, direction: String): [ShortestPathOutput!]! + pagerank(iterCount: Int!, threads: Int, tol: Float): [PagerankOutput!]! } type GraphSchema { diff --git a/raphtory/src/io/arrow/df_loaders.rs b/raphtory/src/io/arrow/df_loaders.rs index 48338f8dcc..d894d0251d 100644 --- a/raphtory/src/io/arrow/df_loaders.rs +++ b/raphtory/src/io/arrow/df_loaders.rs @@ -340,9 +340,11 @@ pub(crate) fn load_edges_from_df< .graph() .resolve_node(gid.as_node_ref()) .map_err(|_| LoadError::FatalError)?; + if vid.is_new() { num_nodes.fetch_add(1, Ordering::Relaxed); } + *resolved = vid.inner(); Ok::<(), LoadError>(()) })?; @@ -356,9 +358,11 @@ pub(crate) fn load_edges_from_df< .graph() .resolve_node(gid.as_node_ref()) .map_err(|_| LoadError::FatalError)?; + if vid.is_new() { num_nodes.fetch_add(1, Ordering::Relaxed); } + *resolved = vid.inner(); Ok::<(), LoadError>(()) })?; @@ -670,12 +674,11 @@ pub(crate) fn load_node_props_from_df< node_type_col_resolved.resize_with(df.len(), Default::default); node_col - .par_iter() - .zip(node_col_resolved.par_iter_mut()) - .zip(node_type_col.par_iter()) - .zip(node_type_col_resolved.par_iter_mut()) + .iter() + .zip(node_col_resolved.iter_mut()) + .zip(node_type_col.iter()) + .zip(node_type_col_resolved.iter_mut()) .try_for_each(|(((gid, resolved), node_type), node_type_resolved)| { - let gid = gid.ok_or(LoadError::FatalError)?; let (vid, res_node_type) = write_locked_graph .graph() .resolve_node_and_type(gid.as_node_ref(), node_type) @@ -690,7 +693,7 @@ pub(crate) fn load_node_props_from_df< write_locked_graph .nodes - .par_iter_mut() + .iter_mut() .try_for_each(|shard| { let mut c_props = vec![]; diff --git a/raphtory/src/serialise/parquet/mod.rs b/raphtory/src/serialise/parquet/mod.rs index 7187faea94..9353994611 100644 --- a/raphtory/src/serialise/parquet/mod.rs +++ b/raphtory/src/serialise/parquet/mod.rs @@ -450,9 +450,15 @@ impl ParquetDecoder for MaterializedGraph { where Self: Sized, { - let gs = decode_graph_storage(path.as_ref(), GraphType::EventGraph) - .or_else(|_| decode_graph_storage(path.as_ref(), GraphType::PersistentGraph))?; - Ok(MaterializedGraph::EventGraph(Graph::from_storage(gs))) + // Try to decode as EventGraph first + match decode_graph_storage(path.as_ref(), GraphType::EventGraph) { + Ok(gs) => Ok(MaterializedGraph::EventGraph(Graph::from_storage(gs))), + Err(_) => { + // If that fails, try PersistentGraph + let gs = decode_graph_storage(path.as_ref(), GraphType::PersistentGraph)?; + Ok(MaterializedGraph::PersistentGraph(PersistentGraph(gs))) + } + } } } From 42e2029e1def9a0dc4ec193a794ff1229e40c4c3 Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Mon, 1 Sep 2025 13:51:45 +0200 Subject: [PATCH 141/321] start making paths optional --- db4-graph/src/lib.rs | 96 ++++++++++++++++++----------- db4-storage/src/api/edges.rs | 13 ++-- db4-storage/src/api/nodes.rs | 15 +++-- db4-storage/src/lib.rs | 7 ++- db4-storage/src/pages/edge_store.rs | 28 ++++++--- db4-storage/src/pages/mod.rs | 30 ++++----- db4-storage/src/pages/node_store.rs | 14 ++--- db4-storage/src/segments/edge.rs | 6 +- db4-storage/src/segments/node.rs | 20 +++--- db4-storage/src/wal/mod.rs | 7 +-- db4-storage/src/wal/no_wal.rs | 14 +---- 11 files changed, 138 insertions(+), 112 deletions(-) diff --git a/db4-graph/src/lib.rs b/db4-graph/src/lib.rs index 254b11fcb9..a739ed3a83 100644 --- a/db4-graph/src/lib.rs +++ b/db4-graph/src/lib.rs @@ -1,4 +1,5 @@ use std::{ + io, path::{Path, PathBuf}, sync::{ atomic::{self, AtomicU64, AtomicUsize}, @@ -18,6 +19,7 @@ use raphtory_core::{ storage::timeindex::TimeIndexEntry, }; use storage::{ + error::StorageError, pages::{ layer_counter::GraphStats, locked::{edges::WriteLockedEdgePages, nodes::WriteLockedNodePages}, @@ -42,7 +44,7 @@ pub struct TemporalGraph { pub node_count: AtomicUsize, storage: Arc>, pub graph_meta: Arc, - graph_dir: GraphDir, + graph_dir: Option, pub transaction_manager: Arc, pub wal: Arc, } @@ -53,24 +55,38 @@ pub enum GraphDir { Path(PathBuf), } -impl<'a> From<&'a Path> for GraphDir { - fn from(path: &'a Path) -> Self { - GraphDir::Path(path.to_path_buf()) +impl GraphDir { + pub fn path(&self) -> &Path { + match self { + GraphDir::Temp(dir) => dir.path(), + GraphDir::Path(path) => path, + } + } + pub fn gid_resolver_dir(&self) -> PathBuf { + self.path().join("gid_resolver") } -} -impl Default for GraphDir { - fn default() -> Self { - GraphDir::Temp(tempfile::tempdir().expect("Failed to create temporary directory")) + pub fn wal_dir(&self) -> PathBuf { + self.path().join("wal") + } + + pub fn create_dir(&self) -> Result<(), io::Error> { + if let GraphDir::Path(path) = self { + std::fs::create_dir_all(path)?; + } + Ok(()) } } impl AsRef for GraphDir { fn as_ref(&self) -> &Path { - match self { - GraphDir::Temp(temp_dir) => temp_dir.path(), - GraphDir::Path(path) => path, - } + self.path() + } +} + +impl<'a> From<&'a Path> for GraphDir { + fn from(path: &'a Path) -> Self { + GraphDir::Path(path.to_path_buf()) } } @@ -118,45 +134,51 @@ impl, ES = ES>> TemporalGraph { pub fn new() -> Self { let node_meta = Meta::new_for_nodes(); let edge_meta = Meta::new_for_edges(); - Self::new_with_meta(GraphDir::default(), node_meta, edge_meta) + Self::new_with_meta(None, node_meta, edge_meta) } pub fn new_with_path(path: impl AsRef) -> Self { let node_meta = Meta::new_for_nodes(); let edge_meta = Meta::new_for_edges(); - Self::new_with_meta(path.as_ref().into(), node_meta, edge_meta) + Self::new_with_meta(Some(path.as_ref().into()), node_meta, edge_meta) } - pub fn load_from_path(path: impl AsRef) -> Self { - let graph_dir: GraphDir = path.as_ref().into(); - let storage = Layer::load(graph_dir.as_ref()) - .unwrap_or_else(|_| panic!("Failed to load graph from path: {graph_dir:?}")); - - let gid_resolver_dir = graph_dir.as_ref().join("gid_resolver"); - let resolver = GIDResolver::new(&gid_resolver_dir).unwrap_or_else(|err| { - panic!("Failed to load GID resolver from path: {gid_resolver_dir:?} {err}") - }); + pub fn load_from_path(path: impl AsRef) -> Result { + let path = path.as_ref(); + let storage = Layer::load(path)?; + let gid_resolver_dir = path.join("gid_resolver"); + let resolver = GIDResolver::new_with_path(&gid_resolver_dir)?; let node_count = AtomicUsize::new(storage.nodes().num_nodes()); - let wal_dir = graph_dir.as_ref().join("wal"); - let wal = Arc::new(WalImpl::new(&wal_dir).unwrap()); + let wal_dir = path.join("wal"); + let wal = Arc::new(WalImpl::new(Some(wal_dir))?); - Self { - graph_dir, + Ok(Self { + graph_dir: path.into(), logical_to_physical: resolver.into(), node_count, storage: Arc::new(storage), graph_meta: Arc::new(GraphMeta::default()), transaction_manager: Arc::new(TransactionManager::new(wal.clone())), wal, - } + }) } - pub fn new_with_meta(graph_dir: GraphDir, node_meta: Meta, edge_meta: Meta) -> Self { - std::fs::create_dir_all(&graph_dir) - .unwrap_or_else(|_| panic!("Failed to create graph directory at {graph_dir:?}")); + pub fn new_with_meta( + graph_dir: Option, + node_meta: Meta, + edge_meta: Meta, + ) -> Result { + if let Some(dir) = graph_dir.as_ref() { + std::fs::create_dir_all(dir)? + } + let gid_resolver_dir = graph_dir.as_ref().map(|dir| dir.gid_resolver_dir()); + let logical_to_physical = match gid_resolver_dir { + Some(gid_resolver_dir) => GIDResolver::new_with_path(gid_resolver_dir)?, + None => GIDResolver::new()?, + } + .into(); - let gid_resolver_dir = graph_dir.as_ref().join("gid_resolver"); let storage: Layer = Layer::new_with_meta( graph_dir.as_ref(), DEFAULT_MAX_PAGE_LEN_NODES, @@ -165,18 +187,18 @@ impl, ES = ES>> TemporalGraph { edge_meta, ); - let wal_dir = graph_dir.as_ref().join("wal"); - let wal = Arc::new(WalImpl::new(&wal_dir).unwrap()); + let wal_dir = graph_dir.as_ref().map(|dir| dir.wal_dir()); + let wal = Arc::new(WalImpl::new(wal_dir)?); - Self { + Ok(Self { graph_dir, - logical_to_physical: GIDResolver::new(gid_resolver_dir).unwrap().into(), + logical_to_physical, node_count: AtomicUsize::new(0), storage: Arc::new(storage), graph_meta: Arc::new(GraphMeta::default()), transaction_manager: Arc::new(TransactionManager::new(wal.clone())), wal, - } + }) } pub fn read_event_counter(&self) -> usize { diff --git a/db4-storage/src/api/edges.rs b/db4-storage/src/api/edges.rs index 4d4a98da01..055af78719 100644 --- a/db4-storage/src/api/edges.rs +++ b/db4-storage/src/api/edges.rs @@ -1,9 +1,3 @@ -use std::{ - ops::{Deref, DerefMut}, - path::Path, - sync::Arc, -}; - use parking_lot::{RwLockReadGuard, RwLockWriteGuard, lock_api::ArcRwLockReadGuard}; use raphtory_api::core::entities::properties::{meta::Meta, prop::Prop, tprop::TPropOps}; use raphtory_core::{ @@ -11,6 +5,11 @@ use raphtory_core::{ storage::timeindex::{TimeIndexEntry, TimeIndexOps}, }; use rayon::iter::ParallelIterator; +use std::{ + ops::{Deref, DerefMut}, + path::{Path, PathBuf}, + sync::Arc, +}; use crate::{LocalPOS, error::StorageError, segments::edge::MemEdgeSegment}; @@ -44,7 +43,7 @@ pub trait EdgeSegmentOps: Send + Sync + std::fmt::Debug + 'static { page_id: usize, max_page_len: usize, meta: Arc, - path: impl AsRef, + path: Option, ext: Self::Extension, ) -> Self; diff --git a/db4-storage/src/api/nodes.rs b/db4-storage/src/api/nodes.rs index f066ba5bdf..89ff27d83e 100644 --- a/db4-storage/src/api/nodes.rs +++ b/db4-storage/src/api/nodes.rs @@ -1,10 +1,3 @@ -use std::{ - borrow::Cow, - ops::{Deref, DerefMut, Range}, - path::Path, - sync::Arc, -}; - use itertools::Itertools; use parking_lot::{RwLockReadGuard, RwLockWriteGuard, lock_api::ArcRwLockReadGuard}; use raphtory_api::{ @@ -23,6 +16,12 @@ use raphtory_core::{ storage::timeindex::{TimeIndexEntry, TimeIndexOps}, utils::iter::GenLockedIter, }; +use std::{ + borrow::Cow, + ops::{Deref, DerefMut, Range}, + path::{Path, PathBuf}, + sync::Arc, +}; use crate::{ LocalPOS, @@ -65,7 +64,7 @@ pub trait NodeSegmentOps: Send + Sync + std::fmt::Debug + 'static { max_page_len: usize, node_meta: Arc, edge_meta: Arc, - path: impl AsRef, + path: Option, ext: Self::Extension, ) -> Self; diff --git a/db4-storage/src/lib.rs b/db4-storage/src/lib.rs index 905161be30..4bb698cf31 100644 --- a/db4-storage/src/lib.rs +++ b/db4-storage/src/lib.rs @@ -62,7 +62,10 @@ pub mod error { use std::{path::PathBuf, sync::Arc}; use raphtory_api::core::entities::properties::prop::PropError; - use raphtory_core::{entities::properties::props::MetadataError, utils::time::ParseTimeError}; + use raphtory_core::{ + entities::{graph::logical_to_physical::InvalidNodeId, properties::props::MetadataError}, + utils::time::ParseTimeError, + }; #[derive(thiserror::Error, Debug)] pub enum StorageError { @@ -91,6 +94,8 @@ pub mod error { // MutationError(#[from] MutationError), #[error("Unnamed Failure: {0}")] GenericFailure(String), + #[error(transparent)] + InvalidNodeId(#[from] InvalidNodeId), } } diff --git a/db4-storage/src/pages/edge_store.rs b/db4-storage/src/pages/edge_store.rs index f5f0297327..e259e41c82 100644 --- a/db4-storage/src/pages/edge_store.rs +++ b/db4-storage/src/pages/edge_store.rs @@ -30,7 +30,7 @@ pub struct EdgeStorageInner { segments: boxcar::Vec>, layer_counter: Arc, free_pages: Box<[RwLock; N]>, - edges_path: PathBuf, + edges_path: Option, max_page_len: usize, prop_meta: Arc, ext: EXT, @@ -118,7 +118,7 @@ impl, EXT: Clone + Send + Sync> EdgeStorageI } pub fn new_with_meta( - edges_path: impl AsRef, + edges_path: Option, max_page_len: usize, edge_meta: Arc, ext: EXT, @@ -128,14 +128,14 @@ impl, EXT: Clone + Send + Sync> EdgeStorageI segments: boxcar::Vec::new(), layer_counter: GraphStats::new().into(), free_pages: free_pages.try_into().unwrap(), - edges_path: edges_path.as_ref().to_path_buf(), + edges_path, max_page_len, prop_meta: edge_meta, ext, } } - pub fn new(edges_path: impl AsRef, max_page_len: usize, ext: EXT) -> Self { + pub fn new(edges_path: Option, max_page_len: usize, ext: EXT) -> Self { Self::new_with_meta(edges_path, max_page_len, Meta::new_for_edges().into(), ext) } @@ -143,8 +143,8 @@ impl, EXT: Clone + Send + Sync> EdgeStorageI &self.segments } - pub fn edges_path(&self) -> &Path { - &self.edges_path + pub fn edges_path(&self) -> Option<&Path> { + self.edges_path.as_ref().map(|path| path.as_path()) } pub fn earliest(&self) -> Option { @@ -178,7 +178,11 @@ impl, EXT: Clone + Send + Sync> EdgeStorageI let meta = Arc::new(Meta::new_for_edges()); if !edges_path.exists() { - return Ok(Self::new(edges_path, max_page_len, ext.clone())); + return Ok(Self::new( + Some(edges_path.to_path_buf()), + max_page_len, + ext.clone(), + )); } let mut pages = std::fs::read_dir(edges_path)? .filter(|entry| { @@ -209,7 +213,13 @@ impl, EXT: Clone + Send + Sync> EdgeStorageI let pages: boxcar::Vec> = (0..=max_page) .map(|page_id| { let np = pages.remove(&page_id).unwrap_or_else(|| { - ES::new(page_id, max_page_len, meta.clone(), edges_path, ext.clone()) + ES::new( + page_id, + max_page_len, + meta.clone(), + Some(edges_path.to_path_buf()), + ext.clone(), + ) }); Arc::new(np) }) @@ -260,7 +270,7 @@ impl, EXT: Clone + Send + Sync> EdgeStorageI Ok(Self { segments: pages, - edges_path: edges_path.to_path_buf(), + edges_path: Some(edges_path.to_path_buf()), max_page_len, layer_counter: GraphStats::from(layer_counts).into(), free_pages: free_pages.try_into().unwrap(), diff --git a/db4-storage/src/pages/mod.rs b/db4-storage/src/pages/mod.rs index 332a9d2ca9..d799447f2a 100644 --- a/db4-storage/src/pages/mod.rs +++ b/db4-storage/src/pages/mod.rs @@ -149,14 +149,14 @@ impl< } pub fn new_with_meta( - graph_dir: impl AsRef, + graph_dir: Option<&Path>, max_page_len_nodes: usize, max_page_len_edges: usize, node_meta: Meta, edge_meta: Meta, ) -> Self { - let nodes_path = graph_dir.as_ref().join("nodes"); - let edges_path = graph_dir.as_ref().join("edges"); + let nodes_path = graph_dir.map(|graph_dir| graph_dir.join("nodes")); + let edges_path = graph_dir.map(|graph_dir| graph_dir.join("edges")); let ext = EXT::default(); let node_meta = Arc::new(node_meta); @@ -176,13 +176,15 @@ impl< ext.clone(), )); - let graph_meta = GraphMeta { - max_page_len_nodes, - max_page_len_edges, - }; + if let Some(graph_dir) = graph_dir { + let graph_meta = GraphMeta { + max_page_len_nodes, + max_page_len_edges, + }; - write_graph_meta(&graph_dir, graph_meta) - .expect("Unrecoverable! Failed to write graph meta"); + write_graph_meta(&graph_dir, graph_meta) + .expect("Unrecoverable! Failed to write graph meta"); + } Self { // node_flush_thread: FlushThread::new::<_, ES, _>(nodes.clone()), @@ -194,7 +196,7 @@ impl< } pub fn new( - graph_dir: impl AsRef, + graph_dir: Option<&Path>, max_page_len_nodes: usize, max_page_len_edges: usize, ) -> Self { @@ -435,7 +437,7 @@ mod test { .collect(); check_edges_support(edges, par_load, false, |graph_dir| { - Layer::::new(graph_dir, chunk_size, chunk_size) + Layer::::new(Some(graph_dir), chunk_size, chunk_size) }) } @@ -445,7 +447,7 @@ mod test { par_load: bool, ) { check_edges_support(edges, par_load, false, |graph_dir| { - Layer::::new(graph_dir, chunk_size, chunk_size) + Layer::::new(Some(graph_dir), chunk_size, chunk_size) }) } @@ -517,7 +519,7 @@ mod test { #[test] fn test_add_one_edge_get_num_nodes() { let graph_dir = tempfile::tempdir().unwrap(); - let g = Layer::::new(graph_dir.path(), 32, 32); + let g = Layer::::new(Some(graph_dir.path()), 32, 32); g.add_edge(4, 7, 3).unwrap(); assert_eq!(g.nodes().num_nodes(), 2); } @@ -525,7 +527,7 @@ mod test { #[test] fn test_node_additions_1() { let graph_dir = tempfile::tempdir().unwrap(); - let g = GraphStore::new(graph_dir.path(), 32, 32); + let g = GraphStore::new(Some(graph_dir.path()), 32, 32); g.add_edge(4, 7, 3).unwrap(); let check = |g: &Layer<()>| { diff --git a/db4-storage/src/pages/node_store.rs b/db4-storage/src/pages/node_store.rs index 3d1009ad90..8d5b13b1a2 100644 --- a/db4-storage/src/pages/node_store.rs +++ b/db4-storage/src/pages/node_store.rs @@ -25,7 +25,7 @@ use std::{ pub struct NodeStorageInner { pages: boxcar::Vec>, stats: Arc, - nodes_path: PathBuf, + nodes_path: Option, max_page_len: usize, node_meta: Arc, edge_meta: Arc, @@ -89,7 +89,7 @@ impl, EXT: Clone> NodeStorageInner } pub fn new_with_meta( - nodes_path: impl AsRef, + nodes_path: Option, max_page_len: usize, node_meta: Arc, edge_meta: Arc, @@ -98,7 +98,7 @@ impl, EXT: Clone> NodeStorageInner Self { pages: boxcar::Vec::new(), stats: GraphStats::new().into(), - nodes_path: nodes_path.as_ref().to_path_buf(), + nodes_path, max_page_len, node_meta, edge_meta, @@ -170,8 +170,8 @@ impl, EXT: Clone> NodeStorageInner &self.pages } - pub fn nodes_path(&self) -> &Path { - &self.nodes_path + pub fn nodes_path(&self) -> Option<&Path> { + self.nodes_path.as_ref().map(|path| path.as_path()) } pub fn load( @@ -224,7 +224,7 @@ impl, EXT: Clone> NodeStorageInner max_page_len, node_meta.clone(), edge_meta.clone(), - nodes_path, + Some(nodes_path.to_path_buf()), ext.clone(), ) }); @@ -255,7 +255,7 @@ impl, EXT: Clone> NodeStorageInner Ok(Self { pages, - nodes_path: nodes_path.to_path_buf(), + nodes_path: Some(nodes_path.to_path_buf()), max_page_len, stats: GraphStats::from(layer_counts).into(), node_meta, diff --git a/db4-storage/src/segments/edge.rs b/db4-storage/src/segments/edge.rs index 20eef5b4c8..84efbdc253 100644 --- a/db4-storage/src/segments/edge.rs +++ b/db4-storage/src/segments/edge.rs @@ -1,3 +1,4 @@ +use super::{HasRow, SegmentContainer, edge_entry::MemEdgeEntry}; use crate::{ LocalPOS, api::edges::{EdgeSegmentOps, LockedESegment}, @@ -19,14 +20,13 @@ use raphtory_core::{ use rayon::prelude::*; use std::{ ops::{Deref, DerefMut}, + path::PathBuf, sync::{ Arc, atomic::{self, AtomicUsize}, }, }; -use super::{HasRow, SegmentContainer, edge_entry::MemEdgeEntry}; - #[derive(Debug, Default)] pub struct MemPageEntry { pub src: VID, @@ -443,7 +443,7 @@ impl>> EdgeSegmentOps for EdgeSegm page_id: usize, max_page_len: usize, meta: Arc, - _path: impl AsRef, + _path: Option, _ext: Self::Extension, ) -> Self { Self { diff --git a/db4-storage/src/segments/node.rs b/db4-storage/src/segments/node.rs index c4a125379c..f0516290b2 100644 --- a/db4-storage/src/segments/node.rs +++ b/db4-storage/src/segments/node.rs @@ -1,3 +1,11 @@ +use super::{HasRow, SegmentContainer}; +use crate::{ + LocalPOS, + api::nodes::{LockedNSSegment, NodeSegmentOps}, + error::StorageError, + persist::strategy::PersistentStrategy, + segments::node_entry::{MemNodeEntry, MemNodeRef}, +}; use either::Either; use parking_lot::lock_api::ArcRwLockReadGuard; use raphtory_api::core::{ @@ -13,21 +21,13 @@ use raphtory_core::{ }; use std::{ ops::{Deref, DerefMut}, + path::PathBuf, sync::{ Arc, atomic::{AtomicI64, AtomicUsize, Ordering}, }, }; -use super::{HasRow, SegmentContainer}; -use crate::{ - LocalPOS, - api::nodes::{LockedNSSegment, NodeSegmentOps}, - error::StorageError, - persist::strategy::PersistentStrategy, - segments::node_entry::{MemNodeEntry, MemNodeRef}, -}; - #[derive(Debug, serde::Serialize)] pub struct MemNodeSegment { segment_id: usize, @@ -455,7 +455,7 @@ impl>> NodeSegmentOps for NodeSegm max_page_len: usize, meta: Arc, _edge_meta: Arc, - _path: impl AsRef, + _path: Option, _ext: Self::Extension, ) -> Self { Self { diff --git a/db4-storage/src/wal/mod.rs b/db4-storage/src/wal/mod.rs index a0752a5316..7538781b16 100644 --- a/db4-storage/src/wal/mod.rs +++ b/db4-storage/src/wal/mod.rs @@ -4,7 +4,7 @@ use raphtory_core::{ entities::{EID, GID, VID}, storage::timeindex::TimeIndexEntry, }; -use std::path::Path; +use std::path::{Path, PathBuf}; pub mod entry; pub mod no_wal; @@ -20,13 +20,10 @@ pub struct WalRecord { /// Core Wal methods. pub trait Wal { - fn new(dir: impl AsRef) -> Result + fn new(dir: Option) -> Result where Self: Sized; - /// Returns the directory the WAL is stored in. - fn dir(&self) -> &Path; - /// Appends data to the WAL and returns the assigned LSN. fn append(&self, data: &[u8]) -> Result; diff --git a/db4-storage/src/wal/no_wal.rs b/db4-storage/src/wal/no_wal.rs index 560c372b11..e94aea1eed 100644 --- a/db4-storage/src/wal/no_wal.rs +++ b/db4-storage/src/wal/no_wal.rs @@ -8,19 +8,11 @@ use crate::{ /// NoWAL is a no-op WAL implementation that discards all writes. /// Used for in-memory only graphs. #[derive(Debug)] -pub struct NoWal { - dir: PathBuf, -} +pub struct NoWal; impl Wal for NoWal { - fn new(dir: impl AsRef) -> Result { - Ok(Self { - dir: dir.as_ref().to_path_buf(), - }) - } - - fn dir(&self) -> &Path { - &self.dir + fn new(_dir: Option) -> Result { + Ok(Self) } fn append(&self, _data: &[u8]) -> Result { From 11ef53d851b7d4274fbd975fcfc0b21b52bf034b Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Mon, 1 Sep 2025 13:51:57 +0200 Subject: [PATCH 142/321] tidy up resolver errors --- db4-storage/src/resolver/mapping_resolver.rs | 24 ++++++++++-------- db4-storage/src/resolver/mod.rs | 26 +++++++------------- raphtory-storage/src/mutation/mod.rs | 10 ++++---- raphtory/src/lib.rs | 17 ++++++------- 4 files changed, 36 insertions(+), 41 deletions(-) diff --git a/db4-storage/src/resolver/mapping_resolver.rs b/db4-storage/src/resolver/mapping_resolver.rs index 1abf39e8d7..8317880a35 100644 --- a/db4-storage/src/resolver/mapping_resolver.rs +++ b/db4-storage/src/resolver/mapping_resolver.rs @@ -1,4 +1,4 @@ -use crate::resolver::{GIDResolverError, GIDResolverOps}; +use crate::resolver::{GIDResolverOps, StorageError}; use raphtory_api::core::{ entities::{GidRef, GidType, VID}, storage::dict_mapper::MaybeNew, @@ -18,12 +18,19 @@ impl MappingResolver { } impl GIDResolverOps for MappingResolver { - fn new(_path: impl AsRef) -> Result { + fn new() -> Result + where + Self: Sized, + { Ok(Self { mapping: Mapping::new(), }) } + fn new_with_path(_path: impl AsRef) -> Result { + Self::new() + } + fn len(&self) -> usize { self.mapping.len() } @@ -32,7 +39,7 @@ impl GIDResolverOps for MappingResolver { self.mapping.dtype() } - fn set(&self, gid: GidRef, vid: VID) -> Result<(), GIDResolverError> { + fn set(&self, gid: GidRef, vid: VID) -> Result<(), StorageError> { self.mapping.set(gid, vid)?; Ok(()) } @@ -41,7 +48,7 @@ impl GIDResolverOps for MappingResolver { &self, gid: GidRef, next_id: NFN, - ) -> Result, GIDResolverError> { + ) -> Result, StorageError> { let result = self.mapping.get_or_init(gid, next_id)?; Ok(result) } @@ -49,7 +56,7 @@ impl GIDResolverOps for MappingResolver { fn validate_gids<'a>( &self, gids: impl IntoIterator>, - ) -> Result<(), GIDResolverError> { + ) -> Result<(), StorageError> { Ok(self.mapping.validate_gids(gids)?) } @@ -64,17 +71,14 @@ impl GIDResolverOps for MappingResolver { fn bulk_set_str>( &self, gids: impl IntoIterator, - ) -> Result<(), GIDResolverError> { + ) -> Result<(), StorageError> { for (gid, vid) in gids { self.set(gid.as_ref().into(), vid)?; } Ok(()) } - fn bulk_set_u64( - &self, - gids: impl IntoIterator, - ) -> Result<(), GIDResolverError> { + fn bulk_set_u64(&self, gids: impl IntoIterator) -> Result<(), StorageError> { for (gid, vid) in gids { self.set(gid.into(), vid)?; } diff --git a/db4-storage/src/resolver/mod.rs b/db4-storage/src/resolver/mod.rs index 8389f2adb8..198c73a775 100644 --- a/db4-storage/src/resolver/mod.rs +++ b/db4-storage/src/resolver/mod.rs @@ -9,16 +9,11 @@ use raphtory_core::entities::graph::logical_to_physical::InvalidNodeId; pub mod mapping_resolver; -#[derive(thiserror::Error, Debug)] -pub enum GIDResolverError { - #[error(transparent)] - StorageError(#[from] StorageError), - #[error(transparent)] - InvalidNodeId(#[from] InvalidNodeId), -} - pub trait GIDResolverOps { - fn new(path: impl AsRef) -> Result + fn new() -> Result + where + Self: Sized; + fn new_with_path(path: impl AsRef) -> Result where Self: Sized; fn len(&self) -> usize; @@ -26,28 +21,25 @@ pub trait GIDResolverOps { self.len() == 0 } fn dtype(&self) -> Option; - fn set(&self, gid: GidRef, vid: VID) -> Result<(), GIDResolverError>; + fn set(&self, gid: GidRef, vid: VID) -> Result<(), StorageError>; fn get_or_init VID>( &self, gid: GidRef, next_id: NFN, - ) -> Result, GIDResolverError>; + ) -> Result, StorageError>; fn validate_gids<'a>( &self, gids: impl IntoIterator>, - ) -> Result<(), GIDResolverError>; + ) -> Result<(), StorageError>; fn get_str(&self, gid: &str) -> Option; fn get_u64(&self, gid: u64) -> Option; fn bulk_set_str>( &self, gids: impl IntoIterator, - ) -> Result<(), GIDResolverError>; + ) -> Result<(), StorageError>; - fn bulk_set_u64( - &self, - gids: impl IntoIterator, - ) -> Result<(), GIDResolverError>; + fn bulk_set_u64(&self, gids: impl IntoIterator) -> Result<(), StorageError>; fn iter_str(&self) -> impl Iterator + '_; diff --git a/raphtory-storage/src/mutation/mod.rs b/raphtory-storage/src/mutation/mod.rs index c470ce8257..eb5dce4691 100644 --- a/raphtory-storage/src/mutation/mod.rs +++ b/raphtory-storage/src/mutation/mod.rs @@ -22,7 +22,7 @@ use std::sync::Arc; use storage::{ error::StorageError, pages::{edge_page::writer::EdgeWriter, node_page::writer::NodeWriter}, - resolver::GIDResolverError, + resolver::StorageError, segments::{edge::MemEdgeSegment, node::MemNodeSegment}, Extension, ES, NS, }; @@ -66,11 +66,11 @@ pub enum MutationError { StorageError(#[from] StorageError), } -impl From for MutationError { - fn from(error: GIDResolverError) -> Self { +impl From for MutationError { + fn from(error: StorageError) -> Self { match error { - GIDResolverError::StorageError(e) => MutationError::StorageError(e), - GIDResolverError::InvalidNodeId(e) => MutationError::InvalidNodeId(e), + StorageError::StorageError(e) => MutationError::StorageError(e), + StorageError::InvalidNodeId(e) => MutationError::InvalidNodeId(e), } } } diff --git a/raphtory/src/lib.rs b/raphtory/src/lib.rs index 1c42c01c35..f2cd3730c9 100644 --- a/raphtory/src/lib.rs +++ b/raphtory/src/lib.rs @@ -298,15 +298,14 @@ mod test_utils { // PropType::Decimal { scale }, decimal breaks the tests because of polars-parquet ]); - // leaf.prop_recursive(3, 10, 10, |inner| { - // let dict = proptest::collection::hash_map(r"\w{1,10}", inner.clone(), 1..10) - // .prop_map(PropType::map); - // let list = inner - // .clone() - // .prop_map(|p_type| PropType::List(Box::new(p_type))); - // prop_oneof![inner, list, dict] - // }) - leaf + leaf.prop_recursive(3, 10, 10, |inner| { + let dict = proptest::collection::hash_map(r"\w{1,10}", inner.clone(), 1..10) + .prop_map(PropType::map); + let list = inner + .clone() + .prop_map(|p_type| PropType::List(Box::new(p_type))); + prop_oneof![inner, list, dict] + }) } #[derive(Debug, Clone)] From 019617ef5f18b868a1f583ac67d02c4d5ddeae4e Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Tue, 2 Sep 2025 14:36:45 +0200 Subject: [PATCH 143/321] make paths optional --- db4-graph/src/lib.rs | 14 +++++++------- raphtory-storage/src/mutation/mod.rs | 10 ---------- raphtory/src/db/api/storage/storage.rs | 5 +++-- raphtory/src/db/api/view/graph.rs | 2 +- 4 files changed, 11 insertions(+), 20 deletions(-) diff --git a/db4-graph/src/lib.rs b/db4-graph/src/lib.rs index a739ed3a83..22a1613472 100644 --- a/db4-graph/src/lib.rs +++ b/db4-graph/src/lib.rs @@ -126,18 +126,18 @@ impl TransactionManager { impl Default for TemporalGraph { fn default() -> Self { - Self::new() + Self::new().unwrap() } } impl, ES = ES>> TemporalGraph { - pub fn new() -> Self { + pub fn new() -> Result { let node_meta = Meta::new_for_nodes(); let edge_meta = Meta::new_for_edges(); Self::new_with_meta(None, node_meta, edge_meta) } - pub fn new_with_path(path: impl AsRef) -> Self { + pub fn new_with_path(path: impl AsRef) -> Result { let node_meta = Meta::new_for_nodes(); let edge_meta = Meta::new_for_edges(); Self::new_with_meta(Some(path.as_ref().into()), node_meta, edge_meta) @@ -154,7 +154,7 @@ impl, ES = ES>> TemporalGraph { let wal = Arc::new(WalImpl::new(Some(wal_dir))?); Ok(Self { - graph_dir: path.into(), + graph_dir: Some(path.into()), logical_to_physical: resolver.into(), node_count, storage: Arc::new(storage), @@ -180,7 +180,7 @@ impl, ES = ES>> TemporalGraph { .into(); let storage: Layer = Layer::new_with_meta( - graph_dir.as_ref(), + graph_dir.as_ref().map(|p| p.path()), DEFAULT_MAX_PAGE_LEN_NODES, DEFAULT_MAX_PAGE_LEN_EDGES, node_meta, @@ -255,8 +255,8 @@ impl, ES = ES>> TemporalGraph { self.storage().node_meta() } - pub fn graph_dir(&self) -> &Path { - self.graph_dir.as_ref() + pub fn graph_dir(&self) -> Option<&Path> { + self.graph_dir.as_ref().map(|p| p.path()) } #[inline] diff --git a/raphtory-storage/src/mutation/mod.rs b/raphtory-storage/src/mutation/mod.rs index eb5dce4691..8dd4cf8157 100644 --- a/raphtory-storage/src/mutation/mod.rs +++ b/raphtory-storage/src/mutation/mod.rs @@ -22,7 +22,6 @@ use std::sync::Arc; use storage::{ error::StorageError, pages::{edge_page::writer::EdgeWriter, node_page::writer::NodeWriter}, - resolver::StorageError, segments::{edge::MemEdgeSegment, node::MemNodeSegment}, Extension, ES, NS, }; @@ -66,15 +65,6 @@ pub enum MutationError { StorageError(#[from] StorageError), } -impl From for MutationError { - fn from(error: StorageError) -> Self { - match error { - StorageError::StorageError(e) => MutationError::StorageError(e), - StorageError::InvalidNodeId(e) => MutationError::InvalidNodeId(e), - } - } -} - pub trait InheritMutationOps: Base {} impl InheritAdditionOps for G {} diff --git a/raphtory/src/db/api/storage/storage.rs b/raphtory/src/db/api/storage/storage.rs index 70afba42ae..c4f0839c3e 100644 --- a/raphtory/src/db/api/storage/storage.rs +++ b/raphtory/src/db/api/storage/storage.rs @@ -37,6 +37,7 @@ use std::{ }; use storage::{Extension, WalImpl}; +use crate::prelude::Graph; #[cfg(feature = "search")] use { crate::{ @@ -91,14 +92,14 @@ impl Storage { pub(crate) fn new_at_path(path: impl AsRef) -> Self { Self { - graph: GraphStorage::Unlocked(Arc::new(TemporalGraph::new_with_path(path))), + graph: GraphStorage::Unlocked(Arc::new(TemporalGraph::new_with_path(path).unwrap())), #[cfg(feature = "search")] index: RwLock::new(GraphIndex::Empty), } } pub(crate) fn load_from(path: impl AsRef) -> Self { - let graph = GraphStorage::Unlocked(Arc::new(TemporalGraph::load_from_path(path))); + let graph = GraphStorage::Unlocked(Arc::new(TemporalGraph::load_from_path(path).unwrap())); Self { graph, #[cfg(feature = "search")] diff --git a/raphtory/src/db/api/view/graph.rs b/raphtory/src/db/api/view/graph.rs index 2e956e9c11..a4b7f973ca 100644 --- a/raphtory/src/db/api/view/graph.rs +++ b/raphtory/src/db/api/view/graph.rs @@ -230,7 +230,7 @@ impl<'graph, G: GraphView + 'graph> GraphViewOps<'graph> for G { edge_meta.set_metadata_mapper(self.edge_meta().metadata_mapper().deep_clone()); edge_meta.set_temporal_prop_meta(self.edge_meta().temporal_prop_mapper().deep_clone()); - let mut g = TemporalGraph::new_with_meta(Default::default(), node_meta, edge_meta); + let mut g = TemporalGraph::new_with_meta(Default::default(), node_meta, edge_meta).unwrap(); // Copy all graph properties g.graph_meta = self.graph_meta().deep_clone().into(); From 4930c445efa611f947a4b5eea8093f0902b3d3ad Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Thu, 4 Sep 2025 13:15:44 +0200 Subject: [PATCH 144/321] fix layer_id in tests --- db4-storage/src/pages/mod.rs | 6 +++--- db4-storage/src/pages/test_utils/checkers.rs | 2 +- db4-storage/src/segments/node.rs | 9 ++++++++- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/db4-storage/src/pages/mod.rs b/db4-storage/src/pages/mod.rs index d799447f2a..c446209c25 100644 --- a/db4-storage/src/pages/mod.rs +++ b/db4-storage/src/pages/mod.rs @@ -569,7 +569,7 @@ mod test { #[test] fn node_temporal_props() { let graph_dir = tempfile::tempdir().unwrap(); - let g = Layer::::new(graph_dir.path(), 32, 32); + let g = Layer::::new(Some(graph_dir.path()), 32, 32); g.add_node_props::(1, 0, 0, vec![]) .expect("Failed to add node props"); g.add_node_props::(2, 0, 0, vec![]) @@ -1375,13 +1375,13 @@ mod test { fn check_graph_with_nodes(node_page_len: usize, edge_page_len: usize, fixture: &NodeFixture) { check_graph_with_nodes_support(fixture, false, |path| { - Layer::<()>::new(path, node_page_len, edge_page_len) + Layer::<()>::new(Some(path), node_page_len, edge_page_len) }); } fn check_graph_with_props(node_page_len: usize, edge_page_len: usize, fixture: &Fixture) { check_graph_with_props_support(fixture, false, |path| { - Layer::<()>::new(path, node_page_len, edge_page_len) + Layer::<()>::new(Some(path), node_page_len, edge_page_len) }); } } diff --git a/db4-storage/src/pages/test_utils/checkers.rs b/db4-storage/src/pages/test_utils/checkers.rs index e6742f904e..0441270149 100644 --- a/db4-storage/src/pages/test_utils/checkers.rs +++ b/db4-storage/src/pages/test_utils/checkers.rs @@ -221,7 +221,7 @@ pub fn check_graph_with_nodes_support< let graph_dir = tempfile::tempdir().unwrap(); let graph = make_graph(graph_dir.path()); - let layer_id = graph.edge_meta().get_default_layer_id().unwrap(); + let layer_id = 0; for (node, t, t_props) in temp_props { let err = graph.add_node_props(*t, *node, layer_id, t_props.clone()); diff --git a/db4-storage/src/segments/node.rs b/db4-storage/src/segments/node.rs index f0516290b2..c311008c92 100644 --- a/db4-storage/src/segments/node.rs +++ b/db4-storage/src/segments/node.rs @@ -571,7 +571,14 @@ mod test { let edge_meta = Arc::new(Meta::default()); let path = tempdir().unwrap(); let ext = (); - let segment = NodeSegmentView::new(0, 10, node_meta.clone(), edge_meta, path.path(), ext); + let segment = NodeSegmentView::new( + 0, + 10, + node_meta.clone(), + edge_meta, + Some(path.path().to_path_buf()), + ext, + ); let stats = GraphStats::default(); let mut writer = NodeWriter::new(&segment, &stats, segment.head_mut()); From eba75d461b8c4970f51a34ad41876be07f541c48 Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Thu, 4 Sep 2025 13:19:48 +0200 Subject: [PATCH 145/321] at least some debug symbols --- Cargo.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 2a7eebc4a8..cbea69d6df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,8 @@ edition = "2021" # debug symbols are using a lot of resources [profile.dev] split-debuginfo = "unpacked" -debug = false +opt-level = 1 +debug = 1 [profile.release-with-debug] inherits = "release" From 7a92275258850977472b76788fb814fe09498648 Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Thu, 4 Sep 2025 13:20:02 +0200 Subject: [PATCH 146/321] vec of bool instead of bitvec --- db4-storage/src/segments/edge.rs | 16 ++++++++++++++-- db4-storage/src/segments/mod.rs | 28 ++++++++++++++++------------ 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/db4-storage/src/segments/edge.rs b/db4-storage/src/segments/edge.rs index 84efbdc253..f3863edd9e 100644 --- a/db4-storage/src/segments/edge.rs +++ b/db4-storage/src/segments/edge.rs @@ -348,7 +348,13 @@ impl ArcLockedSegmentView { .layers .get(layer_id) .into_iter() - .flat_map(|layer| layer.items().iter_ones()) + .flat_map(|layer| { + layer + .items() + .iter() + .enumerate() + .filter_map(|(index, check)| check.then_some(index)) + }) .map(move |pos| MemEdgeRef::new(LocalPOS(pos), &self.inner)) } @@ -360,7 +366,13 @@ impl ArcLockedSegmentView { .layers .get(layer_id) .into_par_iter() - .flat_map(|layer| layer.items().iter_ones().par_bridge()) + .flat_map(|layer| { + layer + .items() + .par_iter() + .enumerate() + .filter_map(|(index, check)| check.then_some(index)) + }) .map(move |pos| MemEdgeRef::new(LocalPOS(pos), &self.inner)) } } diff --git a/db4-storage/src/segments/mod.rs b/db4-storage/src/segments/mod.rs index 8960d7e49d..1ca9945379 100644 --- a/db4-storage/src/segments/mod.rs +++ b/db4-storage/src/segments/mod.rs @@ -26,7 +26,7 @@ pub mod node_entry; #[derive(serde::Serialize)] pub struct SegmentContainer { segment_id: usize, - items: BitVec, + items: Vec, data: FxHashMap, max_page_len: usize, properties: Properties, @@ -64,7 +64,7 @@ impl SegmentContainer { assert!(max_page_len > 0, "max_page_len must be greater than 0"); Self { segment_id, - items: BitVec::repeat(false, max_page_len), + items: vec![false; max_page_len], data: Default::default(), max_page_len, properties: Default::default(), @@ -89,7 +89,7 @@ impl SegmentContainer { } pub fn set_item(&mut self, item_pos: LocalPOS) { - self.items.set(item_pos.0, true); + self.items[item_pos.0] = true; } pub fn max_page_len(&self) -> usize { @@ -160,7 +160,7 @@ impl SegmentContainer { &self.meta } - pub fn items(&self) -> &BitVec { + pub fn items(&self) -> &Vec { &self.items } @@ -188,14 +188,18 @@ impl SegmentContainer { } pub fn row_entries(&self) -> impl Iterator)> { - self.items.iter_ones().filter_map(move |l_pos| { - let entry = self.data.get(&LocalPOS(l_pos))?; - Some(( - LocalPOS(l_pos), - entry, - self.properties().get_entry(entry.row()), - )) - }) + self.items + .iter() + .enumerate() + .filter_map(|(index, check)| check.then_some(index)) + .filter_map(move |l_pos| { + let entry = self.data.get(&LocalPOS(l_pos))?; + Some(( + LocalPOS(l_pos), + entry, + self.properties().get_entry(entry.row()), + )) + }) } pub fn all_entries( From 8c59288c7323c5f7bcdc5e4dbd5f8859ee1a235e Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Thu, 4 Sep 2025 16:37:53 +0200 Subject: [PATCH 147/321] optimised has_item --- db4-storage/src/segments/mod.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/db4-storage/src/segments/mod.rs b/db4-storage/src/segments/mod.rs index 1ca9945379..b81e56f5be 100644 --- a/db4-storage/src/segments/mod.rs +++ b/db4-storage/src/segments/mod.rs @@ -88,6 +88,10 @@ impl SegmentContainer { self.data.get(item_pos) } + pub fn has_item(&self, item_pos: LocalPOS) -> bool { + self.items[item_pos.0] + } + pub fn set_item(&mut self, item_pos: LocalPOS) { self.items[item_pos.0] = true; } From 4c0fbce16c3b283d79cf41bf789411eed203d8b2 Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Tue, 9 Sep 2025 09:10:05 +0200 Subject: [PATCH 148/321] replace Vec with RoaringBitmap and limit pagesize to u32 --- Cargo.lock | 1 + db4-graph/src/lib.rs | 4 +- db4-storage/Cargo.toml | 2 +- db4-storage/src/api/edges.rs | 16 +- db4-storage/src/api/nodes.rs | 8 +- db4-storage/src/lib.rs | 24 ++- db4-storage/src/pages/edge_page/writer.rs | 16 +- db4-storage/src/pages/edge_store.rs | 17 +- db4-storage/src/pages/layer_counter.rs | 3 +- db4-storage/src/pages/locked/edges.rs | 4 +- db4-storage/src/pages/locked/nodes.rs | 4 +- db4-storage/src/pages/mod.rs | 53 ++--- db4-storage/src/pages/node_store.rs | 12 +- db4-storage/src/pages/test_utils/checkers.rs | 2 +- db4-storage/src/resolver/mod.rs | 4 +- db4-storage/src/segments/edge.rs | 100 ++++----- db4-storage/src/segments/edge_entry.rs | 3 +- db4-storage/src/segments/mod.rs | 214 ++++++++++++++----- db4-storage/src/segments/node.rs | 14 +- db4-storage/src/segments/node_entry.rs | 3 +- 20 files changed, 286 insertions(+), 218 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9452f824c3..400c09699c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1960,6 +1960,7 @@ dependencies = [ "raphtory-api", "raphtory-core", "rayon", + "roaring", "rustc-hash 2.1.1", "serde", "serde_json", diff --git a/db4-graph/src/lib.rs b/db4-graph/src/lib.rs index 22a1613472..5258eb1ab0 100644 --- a/db4-graph/src/lib.rs +++ b/db4-graph/src/lib.rs @@ -34,8 +34,8 @@ use tempfile::TempDir; pub mod entries; pub mod mutation; -const DEFAULT_MAX_PAGE_LEN_NODES: usize = 25_000; -const DEFAULT_MAX_PAGE_LEN_EDGES: usize = 50_000; +const DEFAULT_MAX_PAGE_LEN_NODES: u32 = 25_000; +const DEFAULT_MAX_PAGE_LEN_EDGES: u32 = 50_000; #[derive(Debug)] pub struct TemporalGraph { diff --git a/db4-storage/Cargo.toml b/db4-storage/Cargo.toml index ea58184cf2..70dcf6b1ac 100644 --- a/db4-storage/Cargo.toml +++ b/db4-storage/Cargo.toml @@ -33,7 +33,7 @@ bytemuck.workspace = true rayon.workspace = true itertools.workspace = true thiserror.workspace = true - +roaring.workspace = true proptest = { workspace = true, optional = true } tempfile = { workspace = true, optional = true } iter-enum = { workspace = true, features = ["rayon"] } diff --git a/db4-storage/src/api/edges.rs b/db4-storage/src/api/edges.rs index 055af78719..cb9edcded3 100644 --- a/db4-storage/src/api/edges.rs +++ b/db4-storage/src/api/edges.rs @@ -27,11 +27,11 @@ pub trait EdgeSegmentOps: Send + Sync + std::fmt::Debug + 'static { fn t_len(&self) -> usize; fn num_layers(&self) -> usize; - fn layer_count(&self, layer_id: usize) -> usize; + fn layer_count(&self, layer_id: usize) -> u32; fn load( page_id: usize, - max_page_len: usize, + max_page_len: u32, meta: Arc, path: impl AsRef, ext: Self::Extension, @@ -41,7 +41,7 @@ pub trait EdgeSegmentOps: Send + Sync + std::fmt::Debug + 'static { fn new( page_id: usize, - max_page_len: usize, + max_page_len: u32, meta: Arc, path: Option, ext: Self::Extension, @@ -49,7 +49,7 @@ pub trait EdgeSegmentOps: Send + Sync + std::fmt::Debug + 'static { fn segment_id(&self) -> usize; - fn num_edges(&self) -> usize; + fn num_edges(&self) -> u32; fn head(&self) -> RwLockReadGuard<'_, MemEdgeSegment>; @@ -64,7 +64,7 @@ pub trait EdgeSegmentOps: Send + Sync + std::fmt::Debug + 'static { head_lock: impl DerefMut, ) -> Result<(), StorageError>; - fn increment_num_edges(&self) -> usize; + fn increment_num_edges(&self) -> u32; fn contains_edge( &self, @@ -80,11 +80,11 @@ pub trait EdgeSegmentOps: Send + Sync + std::fmt::Debug + 'static { locked_head: impl Deref, ) -> Option<(VID, VID)>; - fn entry<'a, LP: Into>(&'a self, edge_pos: LP) -> Self::Entry<'a>; + fn entry<'a>(&'a self, edge_pos: LocalPOS) -> Self::Entry<'a>; - fn layer_entry<'a, LP: Into>( + fn layer_entry<'a>( &'a self, - edge_pos: LP, + edge_pos: LocalPOS, layer_id: usize, locked_head: Option>, ) -> Option>; diff --git a/db4-storage/src/api/nodes.rs b/db4-storage/src/api/nodes.rs index 89ff27d83e..7d2edea966 100644 --- a/db4-storage/src/api/nodes.rs +++ b/db4-storage/src/api/nodes.rs @@ -51,7 +51,7 @@ pub trait NodeSegmentOps: Send + Sync + std::fmt::Debug + 'static { fn load( page_id: usize, - max_page_len: usize, + max_page_len: u32, node_meta: Arc, edge_meta: Arc, path: impl AsRef, @@ -61,7 +61,7 @@ pub trait NodeSegmentOps: Send + Sync + std::fmt::Debug + 'static { Self: Sized; fn new( page_id: usize, - max_page_len: usize, + max_page_len: u32, node_meta: Arc, edge_meta: Arc, path: Option, @@ -75,13 +75,13 @@ pub trait NodeSegmentOps: Send + Sync + std::fmt::Debug + 'static { fn head_mut(&self) -> RwLockWriteGuard<'_, MemNodeSegment>; - fn num_nodes(&self) -> usize { + fn num_nodes(&self) -> u32 { self.layer_count(0) } fn num_layers(&self) -> usize; - fn layer_count(&self, layer_id: usize) -> usize; + fn layer_count(&self, layer_id: usize) -> u32; fn notify_write( &self, diff --git a/db4-storage/src/lib.rs b/db4-storage/src/lib.rs index 4bb698cf31..461b0b5cba 100644 --- a/db4-storage/src/lib.rs +++ b/db4-storage/src/lib.rs @@ -101,21 +101,27 @@ pub mod error { #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize)] #[repr(transparent)] -pub struct LocalPOS(pub usize); +pub struct LocalPOS(pub u32); + + +impl From for LocalPOS { + fn from(value: usize) -> Self { + assert!(value <= u32::MAX as usize); + LocalPOS(value as u32) + } +} impl LocalPOS { - pub fn as_vid(self, page_id: usize, max_page_len: usize) -> VID { - VID(page_id * max_page_len + self.0) + pub fn as_vid(self, page_id: usize, max_page_len: u32) -> VID { + VID(page_id * (max_page_len as usize) + (self.0 as usize)) } - pub fn as_eid(self, page_id: usize, max_page_len: usize) -> EID { - EID(page_id * max_page_len + self.0) + pub fn as_eid(self, page_id: usize, max_page_len: u32) -> EID { + EID(page_id * (max_page_len as usize) + (self.0 as usize)) } -} -impl From for LocalPOS { - fn from(pos: usize) -> Self { - Self(pos) + pub fn as_index(self) -> usize { + self.0 as usize } } diff --git a/db4-storage/src/pages/edge_page/writer.rs b/db4-storage/src/pages/edge_page/writer.rs index 0e1f47df18..b68a30fc63 100644 --- a/db4-storage/src/pages/edge_page/writer.rs +++ b/db4-storage/src/pages/edge_page/writer.rs @@ -37,8 +37,8 @@ impl<'a, MP: DerefMut + std::fmt::Debug, ES: EdgeSegmen &mut self, t: T, edge_pos: LocalPOS, - src: impl Into, - dst: impl Into, + src: VID, + dst: VID, props: impl IntoIterator, layer_id: usize, lsn: u64, @@ -59,8 +59,8 @@ impl<'a, MP: DerefMut + std::fmt::Debug, ES: EdgeSegmen &mut self, t: T, edge_pos: LocalPOS, - src: impl Into, - dst: impl Into, + src: VID, + dst: VID, layer_id: usize, lsn: u64, ) { @@ -70,10 +70,6 @@ impl<'a, MP: DerefMut + std::fmt::Debug, ES: EdgeSegmen if !existing_edge { self.increment_layer_num_edges(layer_id); } - - let src = src.into(); - let dst = dst.into(); - self.graph_stats.update_time(t.t()); self.writer .delete_edge_internal(t, edge_pos, src, dst, layer_id, lsn); @@ -128,8 +124,8 @@ impl<'a, MP: DerefMut + std::fmt::Debug, ES: EdgeSegmen pub fn update_c_props( &mut self, edge_pos: LocalPOS, - src: impl Into, - dst: impl Into, + src: VID, + dst: VID, layer_id: usize, props: impl IntoIterator, ) { diff --git a/db4-storage/src/pages/edge_store.rs b/db4-storage/src/pages/edge_store.rs index e259e41c82..e557910d53 100644 --- a/db4-storage/src/pages/edge_store.rs +++ b/db4-storage/src/pages/edge_store.rs @@ -31,7 +31,7 @@ pub struct EdgeStorageInner { layer_counter: Arc, free_pages: Box<[RwLock; N]>, edges_path: Option, - max_page_len: usize, + max_page_len: u32, prop_meta: Arc, ext: EXT, } @@ -119,7 +119,7 @@ impl, EXT: Clone + Send + Sync> EdgeStorageI pub fn new_with_meta( edges_path: Option, - max_page_len: usize, + max_page_len: u32, edge_meta: Arc, ext: EXT, ) -> Self { @@ -135,7 +135,7 @@ impl, EXT: Clone + Send + Sync> EdgeStorageI } } - pub fn new(edges_path: Option, max_page_len: usize, ext: EXT) -> Self { + pub fn new(edges_path: Option, max_page_len: u32, ext: EXT) -> Self { Self::new_with_meta(edges_path, max_page_len, Meta::new_for_edges().into(), ext) } @@ -171,7 +171,7 @@ impl, EXT: Clone + Send + Sync> EdgeStorageI pub fn load( edges_path: impl AsRef, - max_page_len: usize, + max_page_len: u32, ext: EXT, ) -> Result { let edges_path = edges_path.as_ref(); @@ -260,7 +260,7 @@ impl, EXT: Clone + Send + Sync> EdgeStorageI for (_, page) in pages.iter() { for layer_id in 0..page.num_layers() { - let count = page.layer_count(layer_id); + let count = page.layer_count(layer_id) as usize; if layer_counts.len() <= layer_id { layer_counts.resize(layer_id + 1, 0); } @@ -344,7 +344,7 @@ impl, EXT: Clone + Send + Sync> EdgeStorageI } } - pub fn max_page_len(&self) -> usize { + pub fn max_page_len(&self) -> u32 { self.max_page_len } @@ -421,7 +421,7 @@ impl, EXT: Clone + Send + Sync> EdgeStorageI ) -> EdgeWriter<'a, RwLockWriteGuard<'a, MemEdgeSegment>, ES> { // optimistic first try to get a free page 3 times let num_edges = self.num_edges(); - let slot_idx = num_edges % N; + let slot_idx = num_edges as usize % N; let maybe_free_page = self.free_pages[slot_idx..] .iter() .cycle() @@ -463,6 +463,7 @@ impl, EXT: Clone + Send + Sync> EdgeStorageI .flat_map(move |page| { (0..page.num_edges()) .into_par_iter() + .map(LocalPOS) .filter_map(move |local_edge| { page.layer_entry(local_edge, layer, Some(page.head())) }) @@ -474,7 +475,7 @@ impl, EXT: Clone + Send + Sync> EdgeStorageI .filter_map(move |page_id| self.segments.get(page_id)) .flat_map(move |page| { (0..page.num_edges()).filter_map(move |local_edge| { - page.layer_entry(local_edge, layer, Some(page.head())) + page.layer_entry(LocalPOS(local_edge), layer, Some(page.head())) }) }) } diff --git a/db4-storage/src/pages/layer_counter.rs b/db4-storage/src/pages/layer_counter.rs index ebc51f5c39..5ba06fdc1e 100644 --- a/db4-storage/src/pages/layer_counter.rs +++ b/db4-storage/src/pages/layer_counter.rs @@ -1,6 +1,5 @@ -use std::sync::atomic::AtomicUsize; - use raphtory_core::entities::graph::timer::{MaxCounter, MinCounter, TimeCounterTrait}; +use std::sync::atomic::AtomicUsize; #[derive(Debug)] pub struct GraphStats { diff --git a/db4-storage/src/pages/locked/edges.rs b/db4-storage/src/pages/locked/edges.rs index 126aaabd78..67702248a5 100644 --- a/db4-storage/src/pages/locked/edges.rs +++ b/db4-storage/src/pages/locked/edges.rs @@ -13,7 +13,7 @@ use rayon::prelude::*; #[derive(Debug)] pub struct LockedEdgePage<'a, ES> { page_id: usize, - max_page_len: usize, + max_page_len: u32, page: &'a ES, num_edges: &'a GraphStats, lock: RwLockWriteGuard<'a, MemEdgeSegment>, @@ -22,7 +22,7 @@ pub struct LockedEdgePage<'a, ES> { impl<'a, EXT, ES: EdgeSegmentOps> LockedEdgePage<'a, ES> { pub fn new( page_id: usize, - max_page_len: usize, + max_page_len: u32, page: &'a ES, num_edges: &'a GraphStats, lock: RwLockWriteGuard<'a, MemEdgeSegment>, diff --git a/db4-storage/src/pages/locked/nodes.rs b/db4-storage/src/pages/locked/nodes.rs index dc6a020e26..7213daa2e0 100644 --- a/db4-storage/src/pages/locked/nodes.rs +++ b/db4-storage/src/pages/locked/nodes.rs @@ -11,7 +11,7 @@ use std::ops::DerefMut; pub struct LockedNodePage<'a, NS> { page_id: usize, - max_page_len: usize, + max_page_len: u32, layer_counter: &'a GraphStats, page: &'a NS, lock: RwLockWriteGuard<'a, MemNodeSegment>, @@ -21,7 +21,7 @@ impl<'a, EXT, NS: NodeSegmentOps> LockedNodePage<'a, NS> { pub fn new( page_id: usize, layer_counter: &'a GraphStats, - max_page_len: usize, + max_page_len: u32, page: &'a NS, lock: RwLockWriteGuard<'a, MemNodeSegment>, ) -> Self { diff --git a/db4-storage/src/pages/mod.rs b/db4-storage/src/pages/mod.rs index c446209c25..27fb62d90b 100644 --- a/db4-storage/src/pages/mod.rs +++ b/db4-storage/src/pages/mod.rs @@ -150,8 +150,8 @@ impl< pub fn new_with_meta( graph_dir: Option<&Path>, - max_page_len_nodes: usize, - max_page_len_edges: usize, + max_page_len_nodes: u32, + max_page_len_edges: u32, node_meta: Meta, edge_meta: Meta, ) -> Self { @@ -195,11 +195,7 @@ impl< } } - pub fn new( - graph_dir: Option<&Path>, - max_page_len_nodes: usize, - max_page_len_edges: usize, - ) -> Self { + pub fn new(graph_dir: Option<&Path>, max_page_len_nodes: u32, max_page_len_edges: u32) -> Self { Self::new_with_meta( graph_dir, max_page_len_nodes, @@ -395,15 +391,16 @@ fn read_graph_meta(graph_dir: impl AsRef) -> Result>(i: I, max_page_len: usize) -> (usize, LocalPOS) { - let chunk = i.into() / max_page_len; - let pos = i.into() % max_page_len; - (chunk, pos.into()) +pub fn resolve_pos>(i: I, max_page_len: u32) -> (usize, LocalPOS) { + let i = i.into(); + let chunk = i / max_page_len as usize; + let pos = i % max_page_len as usize; + (chunk, LocalPOS(pos as u32)) } #[cfg(test)] @@ -424,11 +421,7 @@ mod test { use raphtory_api::core::entities::properties::prop::Prop; use raphtory_core::{entities::VID, storage::timeindex::TimeIndexOps}; - fn check_edges( - edges: Vec<(impl Into, impl Into)>, - chunk_size: usize, - par_load: bool, - ) { + fn check_edges(edges: Vec<(impl Into, impl Into)>, chunk_size: u32, par_load: bool) { // Set optional layer_id to None let layer_id = None; let edges = edges @@ -443,7 +436,7 @@ mod test { fn check_edges_with_layers( edges: Vec<(impl Into, impl Into, Option)>, // src, dst, layer_id - chunk_size: usize, + chunk_size: u32, par_load: bool, ) { check_edges_support(edges, par_load, false, |graph_dir| { @@ -454,7 +447,7 @@ mod test { #[test] fn test_storage() { let edges_strat = edges_strat(10); - proptest!(|(edges in edges_strat, chunk_size in 1usize .. 100)|{ + proptest!(|(edges in edges_strat, chunk_size in 1u32 .. 100)|{ check_edges(edges, chunk_size, false); }); } @@ -462,7 +455,7 @@ mod test { #[test] fn test_storage_par() { let edges_strat = edges_strat(15); - proptest!(|(edges in edges_strat, chunk_size in 1usize..100)|{ + proptest!(|(edges in edges_strat, chunk_size in 1u32..100)|{ check_edges(edges, chunk_size, true); }); } @@ -470,7 +463,7 @@ mod test { #[test] fn test_storage_par_1024_x2() { let edges_strat = edges_strat(50); - proptest!(|(edges in edges_strat, chunk_size in 1usize..100)|{ + proptest!(|(edges in edges_strat, chunk_size in 1u32..100)|{ check_edges(edges, chunk_size, true); }); } @@ -478,7 +471,7 @@ mod test { #[test] fn test_storage_par_1024() { let edges_strat = edges_strat(50); - proptest!(|(edges in edges_strat, chunk_size in 2usize..100)|{ + proptest!(|(edges in edges_strat, chunk_size in 2u32..100)|{ check_edges(edges, chunk_size, false); }); } @@ -505,7 +498,7 @@ mod test { fn test_storage_with_layers() { let edges_strat = edges_strat_with_layers(10); - proptest!(|(edges in edges_strat, chunk_size in 1usize .. 100)|{ + proptest!(|(edges in edges_strat, chunk_size in 1u32 .. 100)|{ check_edges_with_layers(edges, chunk_size, false); }); } @@ -597,7 +590,7 @@ mod test { #[test] fn add_one_edge_with_props() { let edges = make_edges(1, 1); - proptest!(|(edges in edges, node_page_len in 1usize..100, edge_page_len in 1usize .. 100)|{ + proptest!(|(edges in edges, node_page_len in 1u32..100, edge_page_len in 1u32 .. 100)|{ check_graph_with_props(node_page_len, edge_page_len, &edges); }); } @@ -660,7 +653,7 @@ mod test { #[test] fn add_one_node_with_props() { let nodes = make_nodes(1); - proptest!(|(nodes in nodes, node_page_len in 1usize..100, edge_page_len in 1usize .. 100)|{ + proptest!(|(nodes in nodes, node_page_len in 1u32..100, edge_page_len in 1u32 .. 100)|{ check_graph_with_nodes(node_page_len, edge_page_len, &nodes); }); } @@ -668,7 +661,7 @@ mod test { #[test] fn add_multiple_node_with_props() { let nodes = make_nodes(20); - proptest!(|(nodes in nodes, node_page_len in 1usize..100, edge_page_len in 1usize .. 100)|{ + proptest!(|(nodes in nodes, node_page_len in 1u32..100, edge_page_len in 1u32 .. 100)|{ check_graph_with_nodes(node_page_len, edge_page_len, &nodes); }); } @@ -868,7 +861,7 @@ mod test { #[test] fn add_multiple_edges_with_props() { let edges = make_edges(20, 20); - proptest!(|(edges in edges, node_page_len in 1usize..100, edge_page_len in 1usize .. 100)|{ + proptest!(|(edges in edges, node_page_len in 1u32..100, edge_page_len in 1u32 .. 100)|{ check_graph_with_props(node_page_len, edge_page_len, &edges); }); } @@ -1373,13 +1366,13 @@ mod test { check_graph_with_props(10, 10, &edges.into()); } - fn check_graph_with_nodes(node_page_len: usize, edge_page_len: usize, fixture: &NodeFixture) { + fn check_graph_with_nodes(node_page_len: u32, edge_page_len: u32, fixture: &NodeFixture) { check_graph_with_nodes_support(fixture, false, |path| { Layer::<()>::new(Some(path), node_page_len, edge_page_len) }); } - fn check_graph_with_props(node_page_len: usize, edge_page_len: usize, fixture: &Fixture) { + fn check_graph_with_props(node_page_len: u32, edge_page_len: u32, fixture: &Fixture) { check_graph_with_props_support(fixture, false, |path| { Layer::<()>::new(Some(path), node_page_len, edge_page_len) }); diff --git a/db4-storage/src/pages/node_store.rs b/db4-storage/src/pages/node_store.rs index 8d5b13b1a2..208aa37d0c 100644 --- a/db4-storage/src/pages/node_store.rs +++ b/db4-storage/src/pages/node_store.rs @@ -26,7 +26,7 @@ pub struct NodeStorageInner { pages: boxcar::Vec>, stats: Arc, nodes_path: Option, - max_page_len: usize, + max_page_len: u32, node_meta: Arc, edge_meta: Arc, ext: EXT, @@ -49,7 +49,7 @@ impl, EXT: Send + Sync + Clone> ReadLockedNo } pub fn len(&self) -> usize { - self.storage.num_nodes() + self.storage.num_nodes() as usize } pub fn iter( @@ -90,7 +90,7 @@ impl, EXT: Clone> NodeStorageInner pub fn new_with_meta( nodes_path: Option, - max_page_len: usize, + max_page_len: u32, node_meta: Arc, edge_meta: Arc, ext: EXT, @@ -176,7 +176,7 @@ impl, EXT: Clone> NodeStorageInner pub fn load( nodes_path: impl AsRef, - max_page_len: usize, + max_page_len: u32, edge_meta: Arc, ext: EXT, ) -> Result { @@ -245,7 +245,7 @@ impl, EXT: Clone> NodeStorageInner for (_, page) in pages.iter() { for layer_id in 0..page.num_layers() { - let count = page.layer_count(layer_id); + let count = page.layer_count(layer_id) as usize; if layer_counts.len() <= layer_id { layer_counts.resize(layer_id + 1, 0); } @@ -327,7 +327,7 @@ impl, EXT: Clone> NodeStorageInner } } - pub fn max_page_len(&self) -> usize { + pub fn max_page_len(&self) -> u32 { self.max_page_len } } diff --git a/db4-storage/src/pages/test_utils/checkers.rs b/db4-storage/src/pages/test_utils/checkers.rs index 0441270149..184045564a 100644 --- a/db4-storage/src/pages/test_utils/checkers.rs +++ b/db4-storage/src/pages/test_utils/checkers.rs @@ -95,7 +95,7 @@ pub fn check_edges_support< .expect("Failed to add edge"); } - let actual_num_nodes = graph.nodes().num_nodes(); + let actual_num_nodes = graph.nodes().num_nodes() as usize; assert_eq!(actual_num_nodes, nodes.len()); edges.sort_unstable(); diff --git a/db4-storage/src/resolver/mod.rs b/db4-storage/src/resolver/mod.rs index 198c73a775..90201efe85 100644 --- a/db4-storage/src/resolver/mod.rs +++ b/db4-storage/src/resolver/mod.rs @@ -1,11 +1,9 @@ -use std::path::Path; - use crate::error::StorageError; use raphtory_api::core::{ entities::{GidRef, GidType, VID}, storage::dict_mapper::MaybeNew, }; -use raphtory_core::entities::graph::logical_to_physical::InvalidNodeId; +use std::path::Path; pub mod mapping_resolver; diff --git a/db4-storage/src/segments/edge.rs b/db4-storage/src/segments/edge.rs index f3863edd9e..24fb0058ae 100644 --- a/db4-storage/src/segments/edge.rs +++ b/db4-storage/src/segments/edge.rs @@ -23,7 +23,7 @@ use std::{ path::PathBuf, sync::{ Arc, - atomic::{self, AtomicUsize}, + atomic::{self, AtomicU32}, }, }; @@ -75,7 +75,7 @@ impl AsMut<[SegmentContainer]> for MemEdgeSegment { } impl MemEdgeSegment { - pub fn new(segment_id: usize, max_page_len: usize, meta: Arc) -> Self { + pub fn new(segment_id: usize, max_page_len: u32, meta: Arc) -> Self { Self { layers: vec![SegmentContainer::new(segment_id, max_page_len, meta)], est_size: 0, @@ -128,12 +128,11 @@ impl MemEdgeSegment { self.layers.iter().map(|seg| seg.lsn()).min().unwrap_or(0) } - pub fn max_page_len(&self) -> usize { + pub fn max_page_len(&self) -> u32 { self.layers[0].max_page_len() } - pub fn get_edge(&self, edge_pos: impl Into, layer_id: usize) -> Option<(VID, VID)> { - let edge_pos = edge_pos.into(); + pub fn get_edge(&self, edge_pos: LocalPOS, layer_id: usize) -> Option<(VID, VID)> { self.layers .get(layer_id)? .get(&edge_pos) @@ -143,17 +142,13 @@ impl MemEdgeSegment { pub fn insert_edge_internal( &mut self, t: T, - edge_pos: impl Into, - src: impl Into, - dst: impl Into, + edge_pos: LocalPOS, + src: VID, + dst: VID, layer_id: usize, props: impl IntoIterator, lsn: u64, ) { - let edge_pos = edge_pos.into(); - let src = src.into(); - let dst = dst.into(); - // Ensure we have enough layers self.ensure_layer(layer_id); let est_size = self.layers[layer_id].est_size(); @@ -173,15 +168,12 @@ impl MemEdgeSegment { pub fn delete_edge_internal( &mut self, t: T, - edge_pos: impl Into, - src: impl Into, - dst: impl Into, + edge_pos: LocalPOS, + src: VID, + dst: VID, layer_id: usize, lsn: u64, ) { - let edge_pos = edge_pos.into(); - let src = src.into(); - let dst = dst.into(); let t = TimeIndexEntry::new(t.t(), t.i()); // Ensure we have enough layers @@ -281,16 +273,12 @@ impl MemEdgeSegment { pub fn update_const_properties( &mut self, - edge_pos: impl Into, - src: impl Into, - dst: impl Into, + edge_pos: LocalPOS, + src: VID, + dst: VID, layer_id: usize, props: impl IntoIterator, ) { - let edge_pos = edge_pos.into(); - let src = src.into(); - let dst = dst.into(); - // Ensure we have enough layers self.ensure_layer(layer_id); let est_size = self.layers[layer_id].est_size(); @@ -307,9 +295,8 @@ impl MemEdgeSegment { pub fn contains_edge(&self, edge_pos: LocalPOS, layer_id: usize) -> bool { self.layers .get(layer_id) - .and_then(|layer| layer.items().get::(edge_pos.0)) - .map(|b| *b) - .unwrap_or_default() + .filter(|layer| layer.has_item(edge_pos)) + .is_some() } pub fn latest(&self) -> Option { @@ -330,7 +317,7 @@ impl MemEdgeSegment { pub struct EdgeSegmentView { segment: Arc>, segment_id: usize, - num_edges: AtomicUsize, + num_edges: AtomicU32, _ext: EXT, } @@ -348,14 +335,8 @@ impl ArcLockedSegmentView { .layers .get(layer_id) .into_iter() - .flat_map(|layer| { - layer - .items() - .iter() - .enumerate() - .filter_map(|(index, check)| check.then_some(index)) - }) - .map(move |pos| MemEdgeRef::new(LocalPOS(pos), &self.inner)) + .flat_map(|layer| layer.filled_positions()) + .map(move |pos| MemEdgeRef::new(pos, &self.inner)) } fn edge_par_iter_layer<'a>( @@ -366,14 +347,8 @@ impl ArcLockedSegmentView { .layers .get(layer_id) .into_par_iter() - .flat_map(|layer| { - layer - .items() - .par_iter() - .enumerate() - .filter_map(|(index, check)| check.then_some(index)) - }) - .map(move |pos| MemEdgeRef::new(LocalPOS(pos), &self.inner)) + .flat_map(|layer| layer.filled_positions_par()) + .map(move |pos| MemEdgeRef::new(pos, &self.inner)) } } @@ -440,7 +415,7 @@ impl>> EdgeSegmentOps for EdgeSegm fn load( _page_id: usize, - _max_page_len: usize, + _max_page_len: u32, _meta: Arc, _path: impl AsRef, _ext: Self::Extension, @@ -453,7 +428,7 @@ impl>> EdgeSegmentOps for EdgeSegm fn new( page_id: usize, - max_page_len: usize, + max_page_len: u32, meta: Arc, _path: Option, _ext: Self::Extension, @@ -462,7 +437,7 @@ impl>> EdgeSegmentOps for EdgeSegm segment: parking_lot::RwLock::new(MemEdgeSegment::new(page_id, max_page_len, meta)) .into(), segment_id: page_id, - num_edges: AtomicUsize::new(0), + num_edges: AtomicU32::new(0), _ext, } } @@ -471,7 +446,7 @@ impl>> EdgeSegmentOps for EdgeSegm self.segment_id } - fn num_edges(&self) -> usize { + fn num_edges(&self) -> u32 { self.num_edges.load(atomic::Ordering::Relaxed) } @@ -498,7 +473,7 @@ impl>> EdgeSegmentOps for EdgeSegm Ok(()) } - fn increment_num_edges(&self) -> usize { + fn increment_num_edges(&self) -> u32 { self.num_edges.fetch_add(1, atomic::Ordering::Relaxed) } @@ -520,22 +495,23 @@ impl>> EdgeSegmentOps for EdgeSegm locked_head.get_edge(edge_pos, layer_id) } - fn entry<'a, LP: Into>(&'a self, edge_pos: LP) -> Self::Entry<'a> { + fn entry<'a>(&'a self, edge_pos: LocalPOS) -> Self::Entry<'a> { let edge_pos = edge_pos.into(); MemEdgeEntry::new(edge_pos, self.head()) } - fn layer_entry<'a, LP: Into>( + fn layer_entry<'a>( &'a self, - edge_pos: LP, + edge_pos: LocalPOS, layer_id: usize, locked_head: Option>, ) -> Option> { let edge_pos = edge_pos.into(); locked_head.and_then(|locked_head| { let layer = locked_head.as_ref().get(layer_id)?; - let has_edge = layer.items().get(edge_pos.0).is_some_and(|item| *item); - has_edge.then(|| MemEdgeEntry::new(edge_pos, locked_head)) + layer + .has_item(edge_pos) + .then(|| MemEdgeEntry::new(edge_pos, locked_head)) }) } @@ -549,10 +525,10 @@ impl>> EdgeSegmentOps for EdgeSegm self.head().layers.len() } - fn layer_count(&self, layer_id: usize) -> usize { + fn layer_count(&self, layer_id: usize) -> u32 { self.head() .get_layer(layer_id) - .map_or(0, |layer| layer.len()) + .map_or(0, |layer| layer.len() as u32) } } @@ -573,8 +549,8 @@ mod test { segment.insert_edge_internal( TimeIndexEntry::new(1, 0), LocalPOS(0), - 1, - 2, + VID(1), + VID(2), 0, vec![(0, Prop::from("test"))], 1, @@ -597,8 +573,8 @@ mod test { segment.insert_edge_internal( TimeIndexEntry::new(3, 0), LocalPOS(1), - 4, - 6, + VID(4), + VID(6), 0, vec![(0, Prop::from("test2"))], 1, @@ -626,7 +602,7 @@ mod test { .unwrap() .inner(); - segment.update_const_properties(LocalPOS(1), 4, 6, 0, [(prop_id, Prop::U8(2))]); + segment.update_const_properties(LocalPOS(1), VID(4), VID(6), 0, [(prop_id, Prop::U8(2))]); let est_size5 = segment.est_size(); assert!( diff --git a/db4-storage/src/segments/edge_entry.rs b/db4-storage/src/segments/edge_entry.rs index b284ef2a1b..4a4e17f4ea 100644 --- a/db4-storage/src/segments/edge_entry.rs +++ b/db4-storage/src/segments/edge_entry.rs @@ -69,8 +69,7 @@ impl<'a> MemEdgeRef<'a> { self.es .as_ref() .get(layer_id) - .and_then(|entry| entry.items().get(self.pos.0)) - .is_some_and(|item| *item) + .is_some_and(|layer| layer.has_item(self.pos)) }) } } diff --git a/db4-storage/src/segments/mod.rs b/db4-storage/src/segments/mod.rs index b81e56f5be..b2dfd3f8f9 100644 --- a/db4-storage/src/segments/mod.rs +++ b/db4-storage/src/segments/mod.rs @@ -1,7 +1,5 @@ -use std::{collections::hash_map::Entry, fmt::Debug, sync::Arc}; - +use super::properties::{Properties, RowEntry}; use crate::{LocalPOS, error::StorageError}; -use bitvec::{order::Msb0, vec::BitVec}; use either::Either; use raphtory_api::core::entities::properties::{meta::Meta, prop::Prop}; use raphtory_core::{ @@ -11,10 +9,13 @@ use raphtory_core::{ }, storage::timeindex::TimeIndexEntry, }; -use rayon::prelude::*; +use rayon::{ + iter::plumbing::{Consumer, Producer, ProducerCallback, UnindexedConsumer, bridge}, + prelude::*, +}; +use roaring::{RoaringBitmap, bitmap::Iter}; use rustc_hash::FxHashMap; - -use super::properties::{Properties, RowEntry}; +use std::{collections::hash_map::Entry, fmt::Debug, ops::Range, sync::Arc}; pub mod edge; pub mod node; @@ -23,24 +24,120 @@ pub mod additions; pub mod edge_entry; pub mod node_entry; -#[derive(serde::Serialize)] pub struct SegmentContainer { segment_id: usize, - items: Vec, + items: RoaringBitmap, data: FxHashMap, - max_page_len: usize, + max_page_len: u32, properties: Properties, meta: Arc, lsn: u64, } +pub struct FullRangeIter<'a, T> { + range: Range, + iter: Iter<'a>, + head: Option, + container: &'a SegmentContainer, +} + +impl<'a, T: HasRow> Iterator for FullRangeIter<'a, T> { + type Item = (LocalPOS, Option<(&'a T, RowEntry<'a>)>); + + fn next(&mut self) -> Option { + let next_item = self.range.next()?; + if self.head.is_none() { + self.head = self.iter.next(); + } + let l_pos = LocalPOS(next_item); + let data = if self + .head + .as_ref() + .filter(|&&head| head == next_item) + .is_some() + { + let entry = self.container.data.get(&l_pos).unwrap(); + Some((entry, self.container.properties().get_entry(entry.row()))) + } else { + None + }; + Some((LocalPOS(next_item), data)) + } + + fn size_hint(&self) -> (usize, Option) { + self.range.size_hint() + } +} + +struct ItemProducer<'a> { + items: &'a RoaringBitmap, + range: Range, +} + +impl<'a> Producer for ItemProducer<'a> { + type Item = u32; + type IntoIter = Iter<'a>; + + fn into_iter(self) -> Self::IntoIter { + let start = self.items.select(self.range.start).unwrap_or(u32::MAX); + let end = self.items.select(self.range.end).unwrap_or(u32::MAX); + self.items.range(start..end) + } + + fn split_at(self, index: usize) -> (Self, Self) { + let left_range = self.range.start..(self.range.start + index as u32); + let right_range = index as u32..self.range.end; + ( + ItemProducer { + items: self.items, + range: left_range, + }, + ItemProducer { + items: self.items, + range: right_range, + }, + ) + } +} + +pub struct ParItemIter<'a> { + items: &'a RoaringBitmap, +} + +impl<'a> ParallelIterator for ParItemIter<'a> { + type Item = u32; + + fn drive_unindexed(self, consumer: C) -> C::Result + where + C: UnindexedConsumer, + { + bridge(self, consumer) + } +} + +impl<'a> IndexedParallelIterator for ParItemIter<'a> { + fn len(&self) -> usize { + self.items.len() as usize + } + + fn drive>(self, consumer: C) -> C::Result { + bridge(self, consumer) + } + + fn with_producer>(self, callback: CB) -> CB::Output { + let producer = ItemProducer { + items: self.items, + range: 0..self.items.len() as u32, + }; + callback.callback(producer) + } +} + +impl<'a, T: HasRow> ExactSizeIterator for FullRangeIter<'a, T> {} + impl Debug for SegmentContainer { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let items = self - .items - .iter() - .map(|x| if *x { 1 } else { 0 }) - .collect::>(); + let items = self.items.iter().collect::>(); let mut data = self.data.iter().collect::>(); data.sort_by(|a, b| a.0.cmp(b.0)); @@ -60,11 +157,11 @@ pub trait HasRow: Default + Send + Sync { } impl SegmentContainer { - pub fn new(segment_id: usize, max_page_len: usize, meta: Arc) -> Self { + pub fn new(segment_id: usize, max_page_len: u32, meta: Arc) -> Self { assert!(max_page_len > 0, "max_page_len must be greater than 0"); Self { segment_id, - items: vec![false; max_page_len], + items: RoaringBitmap::new(), data: Default::default(), max_page_len, properties: Default::default(), @@ -85,23 +182,27 @@ impl SegmentContainer { } pub fn get(&self, item_pos: &LocalPOS) -> Option<&T> { - self.data.get(item_pos) + if self.items.contains(item_pos.0) { + self.data.get(item_pos) + } else { + None + } } pub fn has_item(&self, item_pos: LocalPOS) -> bool { - self.items[item_pos.0] + self.items.contains(item_pos.0) } pub fn set_item(&mut self, item_pos: LocalPOS) { - self.items[item_pos.0] = true; + self.items.insert(item_pos.0); } - pub fn max_page_len(&self) -> usize { + pub fn max_page_len(&self) -> u32 { self.max_page_len } pub fn is_full(&self) -> bool { - self.data.len() == self.max_page_len + self.data.len() == self.max_page_len() as usize } pub fn t_len(&self) -> usize { @@ -134,7 +235,7 @@ impl SegmentContainer { } pub(crate) fn c_prop_est_size(&self) -> usize { - self.meta.const_est_row_size() * self.len() + self.meta.const_est_row_size() * self.len() as usize } pub fn properties(&self) -> &Properties { @@ -164,10 +265,21 @@ impl SegmentContainer { &self.meta } - pub fn items(&self) -> &Vec { + pub fn items(&self) -> &RoaringBitmap { &self.items } + pub fn filled_positions(&self) -> impl Iterator { + self.items.iter().map(LocalPOS) + } + + pub fn filled_positions_par(&self) -> impl ParallelIterator { + ParItemIter { + items: self.items(), + } + .map(LocalPOS) + } + #[inline(always)] pub fn segment_id(&self) -> usize { self.segment_id @@ -183,8 +295,8 @@ impl SegmentContainer { self.lsn = lsn; } - pub fn len(&self) -> usize { - self.data.len() + pub fn len(&self) -> u32 { + self.data.len() as u32 } pub fn is_empty(&self) -> bool { @@ -192,45 +304,33 @@ impl SegmentContainer { } pub fn row_entries(&self) -> impl Iterator)> { - self.items - .iter() - .enumerate() - .filter_map(|(index, check)| check.then_some(index)) - .filter_map(move |l_pos| { - let entry = self.data.get(&LocalPOS(l_pos))?; - Some(( - LocalPOS(l_pos), - entry, - self.properties().get_entry(entry.row()), - )) - }) + self.items.iter().map(LocalPOS).filter_map(move |l_pos| { + let entry = self.data.get(&l_pos)?; + Some((l_pos, entry, self.properties().get_entry(entry.row()))) + }) } - pub fn all_entries( - &self, - ) -> impl ExactSizeIterator)>)> { - self.items.iter().enumerate().map(move |(l_pos, exists)| { - let l_pos = LocalPOS(l_pos); - let entry = (*exists).then(|| { - let entry = self.data.get(&l_pos).unwrap(); - (entry, self.properties().get_entry(entry.row())) - }); - (l_pos, entry) - }) + pub fn all_entries(&self) -> FullRangeIter<'_, T> { + FullRangeIter { + range: 0..self.max_page_len, + iter: self.items.iter(), + head: None, + container: self, + } } pub fn all_entries_par( &self, ) -> impl ParallelIterator)>)> + '_ { - (0..self.items.len()).into_par_iter().map(move |l_pos| { - let exists = unsafe { self.items.get_unchecked(l_pos) }; - let l_pos = LocalPOS(l_pos); - let entry = (*exists).then(|| { - let entry = self.data.get(&l_pos).unwrap(); - (entry, self.properties().get_entry(entry.row())) - }); - (l_pos, entry) - }) + (0..self.max_page_len) + .into_par_iter() + .map(LocalPOS) + .map(move |l_pos| { + let entry = self + .get(&l_pos) + .map(|entry| (entry, self.properties().get_entry(entry.row()))); + (l_pos, entry) + }) } pub fn earliest(&self) -> Option { diff --git a/db4-storage/src/segments/node.rs b/db4-storage/src/segments/node.rs index c311008c92..10e46dd4ab 100644 --- a/db4-storage/src/segments/node.rs +++ b/db4-storage/src/segments/node.rs @@ -28,10 +28,10 @@ use std::{ }, }; -#[derive(Debug, serde::Serialize)] +#[derive(Debug)] pub struct MemNodeSegment { segment_id: usize, - max_page_len: usize, + max_page_len: u32, layers: Vec>, } @@ -159,7 +159,7 @@ impl MemNodeSegment { pub fn has_node(&self, n: LocalPOS, layer_id: usize) -> bool { self.layers .get(layer_id) - .is_some_and(|layer| layer.items().get(n.0).is_some_and(|x| *x)) + .is_some_and(|layer| layer.has_item(n)) } pub fn get_out_edge(&self, n: LocalPOS, dst: VID, layer_id: usize) -> Option { @@ -184,7 +184,7 @@ impl MemNodeSegment { .flat_map(|adj| adj.inb_iter()) } - pub fn new(segment_id: usize, max_page_len: usize, meta: Arc) -> Self { + pub fn new(segment_id: usize, max_page_len: u32, meta: Arc) -> Self { Self { segment_id, max_page_len, @@ -438,7 +438,7 @@ impl>> NodeSegmentOps for NodeSegm fn load( _page_id: usize, - _max_page_len: usize, + _max_page_len: u32, _node_meta: Arc, _edge_meta: Arc, _path: impl AsRef, @@ -452,7 +452,7 @@ impl>> NodeSegmentOps for NodeSegm fn new( page_id: usize, - max_page_len: usize, + max_page_len: u32, meta: Arc, _edge_meta: Arc, _path: Option, @@ -530,7 +530,7 @@ impl>> NodeSegmentOps for NodeSegm self.head().layers.len() } - fn layer_count(&self, layer_id: usize) -> usize { + fn layer_count(&self, layer_id: usize) -> u32 { self.head() .get_layer(layer_id) .map_or(0, |layer| layer.len()) diff --git a/db4-storage/src/segments/node_entry.rs b/db4-storage/src/segments/node_entry.rs index 9fef4ea13d..17def105dc 100644 --- a/db4-storage/src/segments/node_entry.rs +++ b/db4-storage/src/segments/node_entry.rs @@ -233,7 +233,6 @@ impl<'a> NodeRefOps<'a> for MemNodeRef<'a> { self.ns .as_ref() .get(layer_id) - .and_then(|seg| seg.items().get(self.pos.0)) - .is_some_and(|x| *x) + .is_some_and(|layer| layer.has_item(self.pos)) } } From aff546f38b3fa9ca4ab9e218324ab5aa62c8827a Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Tue, 9 Sep 2025 10:03:34 +0200 Subject: [PATCH 149/321] fix the full-range iter --- db4-storage/src/segments/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/db4-storage/src/segments/mod.rs b/db4-storage/src/segments/mod.rs index b2dfd3f8f9..8ac5c381ee 100644 --- a/db4-storage/src/segments/mod.rs +++ b/db4-storage/src/segments/mod.rs @@ -57,6 +57,7 @@ impl<'a, T: HasRow> Iterator for FullRangeIter<'a, T> { .is_some() { let entry = self.container.data.get(&l_pos).unwrap(); + self.head = None; Some((entry, self.container.properties().get_entry(entry.row()))) } else { None From 5291a617ac281251d20c31214599e5155401621c Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Tue, 9 Sep 2025 13:00:42 +0200 Subject: [PATCH 150/321] fix tests --- db4-storage/src/segments/edge.rs | 2 +- db4-storage/src/segments/node.rs | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/db4-storage/src/segments/edge.rs b/db4-storage/src/segments/edge.rs index 24fb0058ae..0c47c43727 100644 --- a/db4-storage/src/segments/edge.rs +++ b/db4-storage/src/segments/edge.rs @@ -560,7 +560,7 @@ mod test { assert!(est_size1 > 0); - segment.delete_edge_internal(TimeIndexEntry::new(2, 3), LocalPOS(0), 5, 3, 0, 0); + segment.delete_edge_internal(TimeIndexEntry::new(2, 3), LocalPOS(0), VID(5), VID(3), 0, 0); let est_size2 = segment.est_size(); diff --git a/db4-storage/src/segments/node.rs b/db4-storage/src/segments/node.rs index 10e46dd4ab..eab60f2a99 100644 --- a/db4-storage/src/segments/node.rs +++ b/db4-storage/src/segments/node.rs @@ -656,10 +656,5 @@ mod test { est_size8 > est_size7, "Estimated size should increase after adding another temporal property" ); - - let actual_size = bincode::serialize(writer.mut_segment.deref()) - .unwrap() - .len(); - println!("{actual_size} vs {est_size7}"); } } From 6587994dbefc2feb488d67f753d299282add1528 Mon Sep 17 00:00:00 2001 From: Fabian Murariu <2404621+fabianmurariu@users.noreply.github.com> Date: Tue, 9 Sep 2025 16:18:54 +0100 Subject: [PATCH 151/321] Fix debug symbols (#2271) * trying to cheat on iterators and debug symbols * fix macro for iterators and debug symbols * fix macro for iterators and debug symbols 2 * fix macro for iterators and debug symbols 2 * fix macro for iterators and debug symbols 4 * fix macro for iterators and debug symbols 5 * make box_on_debug a procmacro * the box_on_debug macro works but it generates Boxes without Send + Sync * some progress with the boxed debug symbols # Conflicts: # db4-storage/src/resolver/mod.rs * working debug symbols bypass * symbols are back * removed some useless tests * include all values debug could take --- Cargo.lock | 11 + Cargo.toml | 3 +- db4-storage/Cargo.toml | 1 + db4-storage/build.rs | 9 + db4-storage/src/api/nodes.rs | 3 + db4-storage/src/gen_t_props.rs | 9 +- db4-storage/src/lib.rs | 1 - db4-storage/src/segments/additions.rs | 5 + db4-storage/src/segments/edge.rs | 4 +- raphtory-api-macros/Cargo.toml | 20 ++ raphtory-api-macros/build.rs | 11 + raphtory-api-macros/src/lib.rs | 217 ++++++++++++++++++ raphtory-api-macros/tests/integration_test.rs | 74 ++++++ .../tests/macro_expansion_test.rs | 26 +++ raphtory-storage/Cargo.toml | 1 + raphtory-storage/build.rs | 10 + raphtory-storage/src/graph/edges/unlocked.rs | 7 +- raphtory/build.rs | 9 + .../time_semantics/base_time_semantics.rs | 4 +- .../internal/time_semantics/time_semantics.rs | 2 +- .../time_semantics/time_semantics_ops.rs | 3 +- .../time_semantics/window_time_semantics.rs | 2 +- raphtory/src/db/graph/views/cached_view.rs | 2 +- .../views/filter/edge_and_filtered_graph.rs | 2 +- .../views/filter/edge_field_filtered_graph.rs | 3 +- .../views/filter/edge_or_filtered_graph.rs | 2 +- .../filter/edge_property_filtered_graph.rs | 2 +- .../src/db/graph/views/filter/model/mod.rs | 2 +- .../views/filter/model/property_filter.rs | 2 +- raphtory/src/io/arrow/df_loaders.rs | 116 +++++----- raphtory/src/io/arrow/layer_col.rs | 8 +- raphtory/src/serialise/parquet/nodes.rs | 1 - 32 files changed, 483 insertions(+), 89 deletions(-) create mode 100644 db4-storage/build.rs create mode 100644 raphtory-api-macros/Cargo.toml create mode 100644 raphtory-api-macros/build.rs create mode 100644 raphtory-api-macros/src/lib.rs create mode 100644 raphtory-api-macros/tests/integration_test.rs create mode 100644 raphtory-api-macros/tests/macro_expansion_test.rs create mode 100644 raphtory-storage/build.rs diff --git a/Cargo.lock b/Cargo.lock index 400c09699c..1da76be99c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1958,6 +1958,7 @@ dependencies = [ "polars-arrow", "proptest", "raphtory-api", + "raphtory-api-macros", "raphtory-core", "rayon", "roaring", @@ -5175,6 +5176,15 @@ dependencies = [ "twox-hash 2.1.0", ] +[[package]] +name = "raphtory-api-macros" +version = "0.16.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "raphtory-benchmark" version = "0.16.0" @@ -5325,6 +5335,7 @@ dependencies = [ "polars-arrow", "proptest", "raphtory-api", + "raphtory-api-macros", "raphtory-core", "rayon", "serde", diff --git a/Cargo.toml b/Cargo.toml index cbea69d6df..8f01ca1839 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ members = [ "raphtory-graphql", "raphtory-api", "raphtory-core", - "raphtory-storage", + "raphtory-storage", "raphtory-api-macros", ] default-members = ["raphtory"] resolver = "2" @@ -53,6 +53,7 @@ incremental = false db4-graph = { version = "0.16.0", path = "db4-graph", default-features = false } raphtory = { version = "0.16.0", path = "raphtory", default-features = false } raphtory-api = { version = "0.16.0", path = "raphtory-api", default-features = false } +raphtory-api-macros = { version = "0.16.0", path = "raphtory-api-macros", default-features = false } raphtory-core = { version = "0.16.0", path = "raphtory-core", default-features = false } raphtory-graphql = { version = "0.16.0", path = "raphtory-graphql", default-features = false } raphtory-storage = { version = "0.16.0", path = "raphtory-storage", default-features = false } diff --git a/db4-storage/Cargo.toml b/db4-storage/Cargo.toml index 70dcf6b1ac..7148917eb3 100644 --- a/db4-storage/Cargo.toml +++ b/db4-storage/Cargo.toml @@ -12,6 +12,7 @@ edition = "2024" [dependencies] raphtory-api.workspace = true +raphtory-api-macros.workspace = true raphtory-core = { workspace = true } # db4-common = {path = "../db4-common"} diff --git a/db4-storage/build.rs b/db4-storage/build.rs new file mode 100644 index 0000000000..7acbc3f99d --- /dev/null +++ b/db4-storage/build.rs @@ -0,0 +1,9 @@ +use std::io::Result; + +fn main() -> Result<()> { + println!("cargo::rustc-check-cfg=cfg(has_debug_symbols)"); + if let Ok("true" | "1" | "2") = std::env::var("DEBUG").as_deref() { + println!("cargo::rustc-cfg=has_debug_symbols"); + } + Ok(()) +} diff --git a/db4-storage/src/api/nodes.rs b/db4-storage/src/api/nodes.rs index 7d2edea966..7297ebc727 100644 --- a/db4-storage/src/api/nodes.rs +++ b/db4-storage/src/api/nodes.rs @@ -11,6 +11,7 @@ use raphtory_api::{ }, iter::IntoDynBoxed, }; +use raphtory_api_macros::box_on_debug_lifetime; use raphtory_core::{ entities::{EID, GidRef, LayerIds, VID, edges::edge_ref::EdgeRef}, storage::timeindex::{TimeIndexEntry, TimeIndexOps}, @@ -171,6 +172,7 @@ pub trait NodeRefOps<'a>: Copy + Clone + Send + Sync + 'a { fn vid(&self) -> VID; + #[box_on_debug_lifetime] fn edges_dir( self, layer_id: usize, @@ -202,6 +204,7 @@ pub trait NodeRefOps<'a>: Copy + Clone + Send + Sync + 'a { } } + #[box_on_debug_lifetime] fn edges_iter<'b>( self, layers_ids: &'b LayerIds, diff --git a/db4-storage/src/gen_t_props.rs b/db4-storage/src/gen_t_props.rs index e3683cd492..0592637bf4 100644 --- a/db4-storage/src/gen_t_props.rs +++ b/db4-storage/src/gen_t_props.rs @@ -3,6 +3,7 @@ use std::{borrow::Borrow, ops::Range}; use either::Either; use itertools::Itertools; use raphtory_api::core::entities::properties::{prop::Prop, tprop::TPropOps}; +use raphtory_api_macros::box_on_debug_lifetime; use raphtory_core::{entities::LayerIds, storage::timeindex::TimeIndexEntry}; use crate::utils::Iter4; @@ -18,13 +19,14 @@ where self, layer_id: usize, prop_id: usize, - ) -> impl Iterator + 'a; + ) -> impl Iterator + Send + Sync + 'a; + #[box_on_debug_lifetime] fn into_t_props_layers( self, layers: impl Borrow, prop_id: usize, - ) -> impl Iterator + 'a { + ) -> impl Iterator + Send + Sync + 'a { match layers.borrow() { LayerIds::None => Iter4::I(std::iter::empty()), LayerIds::One(layer_id) => Iter4::J(self.into_t_props(*layer_id, prop_id)), @@ -68,7 +70,8 @@ impl<'a, Ref> GenTProps<'a, Ref> { } impl<'a, Ref: WithTProps<'a>> GenTProps<'a, Ref> { - fn tprops(self, prop_id: usize) -> impl Iterator + 'a { + #[box_on_debug_lifetime] + fn tprops(self, prop_id: usize) -> impl Iterator + Send + Sync + 'a { match self.layer_id { Either::Left(layer_ids) => { Either::Left(self.node.into_t_props_layers(layer_ids, prop_id)) diff --git a/db4-storage/src/lib.rs b/db4-storage/src/lib.rs index 461b0b5cba..f457fd9f42 100644 --- a/db4-storage/src/lib.rs +++ b/db4-storage/src/lib.rs @@ -103,7 +103,6 @@ pub mod error { #[repr(transparent)] pub struct LocalPOS(pub u32); - impl From for LocalPOS { fn from(value: usize) -> Self { assert!(value <= u32::MAX as usize); diff --git a/db4-storage/src/segments/additions.rs b/db4-storage/src/segments/additions.rs index e28029e806..cdd178ca97 100644 --- a/db4-storage/src/segments/additions.rs +++ b/db4-storage/src/segments/additions.rs @@ -1,5 +1,6 @@ use std::ops::Range; +use raphtory_api_macros::box_on_debug_lifetime; use raphtory_core::{ entities::{ELID, properties::tcell::TCell}, storage::timeindex::{TimeIndexEntry, TimeIndexOps, TimeIndexWindow}, @@ -28,6 +29,7 @@ impl<'a> From<&'a TCell>> for MemAdditions<'a> { } impl<'a> EdgeEventOps<'a> for MemAdditions<'a> { + #[box_on_debug_lifetime] fn edge_events(self) -> impl Iterator + Send + Sync + 'a { match self { MemAdditions::Edges(edges) => Iter4::I(edges.iter().map(|(k, v)| (*k, *v))), @@ -41,6 +43,7 @@ impl<'a> EdgeEventOps<'a> for MemAdditions<'a> { } } + #[box_on_debug_lifetime] fn edge_events_rev(self) -> impl Iterator + Send + Sync + 'a { match self { MemAdditions::Edges(edges) => Iter4::I(edges.iter().map(|(k, v)| (*k, *v)).rev()), @@ -78,6 +81,7 @@ impl<'a> TimeIndexOps<'a> for MemAdditions<'a> { } } + #[box_on_debug_lifetime] fn iter(self) -> impl Iterator + Send + Sync + 'a { match self { MemAdditions::Props(props) => Iter4::I(props.iter().map(|(k, _)| *k)), @@ -87,6 +91,7 @@ impl<'a> TimeIndexOps<'a> for MemAdditions<'a> { } } + #[box_on_debug_lifetime] fn iter_rev(self) -> impl Iterator + Send + Sync + 'a { match self { MemAdditions::Props(props) => Iter4::I(props.iter_rev()), diff --git a/db4-storage/src/segments/edge.rs b/db4-storage/src/segments/edge.rs index 0c47c43727..20520b1783 100644 --- a/db4-storage/src/segments/edge.rs +++ b/db4-storage/src/segments/edge.rs @@ -13,6 +13,7 @@ use raphtory_api::core::entities::{ VID, properties::{meta::Meta, prop::Prop}, }; +use raphtory_api_macros::box_on_debug_lifetime; use raphtory_core::{ entities::LayerIds, storage::timeindex::{AsTime, TimeIndexEntry}, @@ -363,6 +364,7 @@ impl LockedESegment for ArcLockedSegmentView { MemEdgeRef::new(edge_pos, &self.inner) } + #[box_on_debug_lifetime] fn edge_iter<'a, 'b: 'a>( &'a self, layer_ids: &'b LayerIds, @@ -381,7 +383,7 @@ impl LockedESegment for ArcLockedSegmentView { fn edge_par_iter<'a, 'b: 'a>( &'a self, layer_ids: &'b LayerIds, - ) -> impl ParallelIterator> + Send + Sync + 'a { + ) -> impl ParallelIterator> + 'a { match layer_ids { LayerIds::None => Iter4::I(rayon::iter::empty()), LayerIds::All => Iter4::J(self.edge_par_iter_layer(0)), diff --git a/raphtory-api-macros/Cargo.toml b/raphtory-api-macros/Cargo.toml new file mode 100644 index 0000000000..1d2df24aa0 --- /dev/null +++ b/raphtory-api-macros/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "raphtory-api-macros" +version.workspace = true +documentation.workspace = true +repository.workspace = true +license.workspace = true +readme.workspace = true +homepage.workspace = true +keywords.workspace = true +authors.workspace = true +rust-version.workspace = true +edition.workspace = true + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0" +quote = "1.0" +syn = { version = "2.0", features = ["full"] } diff --git a/raphtory-api-macros/build.rs b/raphtory-api-macros/build.rs new file mode 100644 index 0000000000..33154a7c92 --- /dev/null +++ b/raphtory-api-macros/build.rs @@ -0,0 +1,11 @@ +use std::io::Result; +fn main() -> Result<()> { + println!("cargo::rustc-check-cfg=cfg(has_debug_symbols)"); + + if let Ok(profile) = std::env::var("PROFILE") { + if profile.contains("debug") { + println!("cargo::rustc-cfg=has_debug_symbols"); + } + } + Ok(()) +} diff --git a/raphtory-api-macros/src/lib.rs b/raphtory-api-macros/src/lib.rs new file mode 100644 index 0000000000..aaa289882f --- /dev/null +++ b/raphtory-api-macros/src/lib.rs @@ -0,0 +1,217 @@ +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::{quote, ToTokens}; +use syn::{parse_macro_input, Error, ItemFn, Path, Result, ReturnType, Type, TypeParamBound}; + +/// A specialized procedural macro for functions with complex lifetime parameters. +/// This macro handles functions that have explicit lifetime parameters and complex bounds. +/// +/// # Usage +/// +/// Simply annotate your iterator-returning function with `#[box_on_debug_lifetime]`: +/// +/// ## Method with complex lifetime bounds: +/// ```rust +/// use raphtory_api_macros::box_on_debug_lifetime; +/// +/// struct Graph; +/// struct LayerIds; +/// struct EntryRef<'a>(&'a str); +/// +/// impl Graph { +/// #[box_on_debug_lifetime] +/// fn edge_iter<'a, 'b: 'a>( +/// &'a self, +/// layer_ids: &'b LayerIds, +/// ) -> impl Iterator> + Send + Sync + 'a { +/// std::iter::once(EntryRef("test")) +/// } +/// } +/// +/// // Test the method works +/// let graph = Graph; +/// let layer_ids = LayerIds; +/// let entries: Vec = graph.edge_iter(&layer_ids).collect(); +/// assert_eq!(entries.len(), 1); +/// assert_eq!(entries[0].0, "test"); +/// ``` +/// +/// ## Function consuming self with lifetime parameter: +/// ```rust +/// use raphtory_api_macros::box_on_debug_lifetime; +/// +/// struct EdgeStorage; +/// struct LayerIds; +/// struct EdgeStorageEntry<'a>(&'a str); +/// +/// impl EdgeStorage { +/// #[box_on_debug_lifetime] +/// pub fn iter<'a>(self, layer_ids: &'a LayerIds) -> impl Iterator> + 'a { +/// std::iter::once(EdgeStorageEntry("test")) +/// } +/// } +/// +/// // Test the function works +/// let storage = EdgeStorage; +/// let layer_ids = LayerIds; +/// let entries: Vec = storage.iter(&layer_ids).collect(); +/// assert_eq!(entries.len(), 1); +/// assert_eq!(entries[0].0, "test"); +/// ``` +/// +/// ## Function with where clause: +/// ```rust +/// use raphtory_api_macros::box_on_debug_lifetime; +/// +/// struct Data { +/// items: Vec, +/// } +/// +/// impl Data +/// where +/// T: Clone + Send + Sync, +/// { +/// #[box_on_debug_lifetime] +/// pub fn iter_cloned<'a>(&'a self) -> impl Iterator + 'a +/// where +/// T: Clone, +/// { +/// self.items.iter().cloned() +/// } +/// } +/// +/// // Test the function works +/// let data = Data { items: vec![1, 2, 3, 4, 5] }; +/// let cloned: Vec = data.iter_cloned().collect(); +/// assert_eq!(cloned, vec![1, 2, 3, 4, 5]); +/// ``` +/// +#[proc_macro_attribute] +pub fn box_on_debug_lifetime(_attr: TokenStream, item: TokenStream) -> TokenStream { + let input_fn = parse_macro_input!(item as ItemFn); + + match generate_box_on_debug_lifetime_impl(&input_fn) { + Ok(output) => output.into(), + Err(err) => err.to_compile_error().into(), + } +} + +fn generate_box_on_debug_lifetime_impl(input_fn: &ItemFn) -> Result { + let attrs = &input_fn.attrs; + let vis = &input_fn.vis; + let sig = &input_fn.sig; + let block = &input_fn.block; + let fn_name = &sig.ident; + + // Parse the return type to extract iterator information + let (item_type, bounds) = parse_iterator_return_type(&sig.output)?; + + // For lifetime version, we preserve all bounds including lifetimes + let debug_return_type = generate_boxed_return_type_with_lifetimes(&item_type, &bounds); + + // Generate the release version (original) + let release_return_type = &sig.output; + + let generics = &sig.generics; + let inputs = &sig.inputs; + let where_clause = &sig.generics.where_clause; + + Ok(quote! { + #[cfg(has_debug_symbols)] + #(#attrs)* + #vis fn #fn_name #generics(#inputs) #debug_return_type #where_clause { + let iter = #block; + Box::new(iter) + } + + #[cfg(not(has_debug_symbols))] + #(#attrs)* + #vis fn #fn_name #generics(#inputs) #release_return_type #where_clause { + #block + } + }) +} + +fn parse_iterator_return_type( + return_type: &ReturnType, +) -> Result<(TokenStream2, Vec)> { + match return_type { + ReturnType::Type(_, ty) => { + if let Type::ImplTrait(impl_trait) = ty.as_ref() { + let mut item_type = None; + let mut bounds = Vec::new(); + + for bound in &impl_trait.bounds { + match bound { + TypeParamBound::Trait(trait_bound) => { + let path = &trait_bound.path; + + // Check if this is an Iterator trait + if is_iterator_trait(path) { + // Extract the Item type from Iterator + if let Some(seg) = path.segments.last() { + if let syn::PathArguments::AngleBracketed(args) = &seg.arguments + { + for arg in &args.args { + if let syn::GenericArgument::AssocType(binding) = arg { + if binding.ident == "Item" { + item_type = Some(binding.ty.to_token_stream()); + } + } + } + } + } + } else { + // This is another bound like Send, Sync, or lifetime + bounds.push(bound.to_token_stream()); + } + } + TypeParamBound::Lifetime(_) => { + bounds.push(bound.to_token_stream()); + } + _ => { + // Handle any other bounds (e.g. Verbatim) + bounds.push(bound.to_token_stream()); + } + } + } + + if let Some(item) = item_type { + Ok((item, bounds)) + } else { + Err(Error::new_spanned( + return_type, + "Expected Iterator in return type", + )) + } + } else { + Err(Error::new_spanned( + return_type, + "Expected impl Iterator<...> return type", + )) + } + } + _ => Err(Error::new_spanned( + return_type, + "Expected -> impl Iterator<...> return type", + )), + } +} + +fn is_iterator_trait(path: &Path) -> bool { + path.segments + .last() + .map(|seg| seg.ident == "Iterator") + .unwrap_or(false) +} + +fn generate_boxed_return_type_with_lifetimes( + item_type: &TokenStream2, + bounds: &[TokenStream2], +) -> TokenStream2 { + if bounds.is_empty() { + quote! { -> Box> } + } else { + quote! { -> Box + #(#bounds)+*> } + } +} diff --git a/raphtory-api-macros/tests/integration_test.rs b/raphtory-api-macros/tests/integration_test.rs new file mode 100644 index 0000000000..3aaa79cb7c --- /dev/null +++ b/raphtory-api-macros/tests/integration_test.rs @@ -0,0 +1,74 @@ +use raphtory_api_macros::box_on_debug_lifetime; + +struct LayerIds; +struct Direction; +struct EdgeRef; + +struct TestStruct; + +impl TestStruct { + #[box_on_debug_lifetime] + fn edge_iter<'a, 'b: 'a>( + &'a self, + _layer_ids: &'b LayerIds, + ) -> impl Iterator + Send + Sync + 'a { + // Simplified version of your complex matching logic + std::iter::empty() + } +} + +trait TestTrait<'a> { + type EntryRef; + + fn edges_iter<'b>( + self, + layers_ids: &'b LayerIds, + dir: Direction, + ) -> impl Iterator + Send + Sync + 'a + where + Self: Sized; +} + +impl<'a> TestTrait<'a> for &'a TestStruct { + type EntryRef = EdgeRef; + + #[box_on_debug_lifetime] + fn edges_iter<'b>( + self, + _layers_ids: &'b LayerIds, + _dir: Direction, + ) -> impl Iterator + Send + Sync + 'a + where + Self: Sized, + { + std::iter::empty() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn can_send_and_sync(_t: &T) {} + + #[test] + fn test_edge_iter() { + let test_struct = TestStruct; + let layer_ids = LayerIds; + let iter = test_struct.edge_iter(&layer_ids); + can_send_and_sync(&iter); + let collected: Vec = iter.collect(); + assert_eq!(collected.len(), 0); + } + + #[test] + fn test_edges_iter() { + let test_struct = TestStruct; + let layer_ids = LayerIds; + let direction = Direction; + let iter = (&test_struct).edges_iter(&layer_ids, direction); + can_send_and_sync(&iter); + let collected: Vec = iter.collect(); + assert_eq!(collected.len(), 0); + } +} diff --git a/raphtory-api-macros/tests/macro_expansion_test.rs b/raphtory-api-macros/tests/macro_expansion_test.rs new file mode 100644 index 0000000000..e981dc0f71 --- /dev/null +++ b/raphtory-api-macros/tests/macro_expansion_test.rs @@ -0,0 +1,26 @@ +use raphtory_api_macros::box_on_debug_lifetime; + +struct TestItem; + +#[box_on_debug_lifetime] +fn test_function<'a>() -> impl Iterator + Send + Sync + 'a { + std::iter::empty() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_debug_vs_release_types() { + let iter = test_function(); + let _collected: Vec = iter.collect(); + } + + #[test] + #[cfg(debug_assertions)] + fn test_debug_build_returns_box() { + let iter = test_function(); + let _boxed: Box + Send + Sync> = iter; + } +} diff --git a/raphtory-storage/Cargo.toml b/raphtory-storage/Cargo.toml index 028f74528d..ac789e7340 100644 --- a/raphtory-storage/Cargo.toml +++ b/raphtory-storage/Cargo.toml @@ -14,6 +14,7 @@ edition.workspace = true [dependencies] raphtory-api = { path = "../raphtory-api", version = "0.16.0" } +raphtory-api-macros = { path = "../raphtory-api-macros", version = "0.16.0" } raphtory-core = { path = "../raphtory-core", version = "0.16.0" } storage.workspace = true db4-graph.workspace = true diff --git a/raphtory-storage/build.rs b/raphtory-storage/build.rs new file mode 100644 index 0000000000..2500803898 --- /dev/null +++ b/raphtory-storage/build.rs @@ -0,0 +1,10 @@ +use std::io::Result; + +fn main() -> Result<()> { + println!("cargo::rustc-check-cfg=cfg(has_debug_symbols)"); + if let Ok("true" | "1" | "2") = std::env::var("DEBUG").as_deref() { + println!("cargo::rustc-cfg=has_debug_symbols"); + } + + Ok(()) +} diff --git a/raphtory-storage/src/graph/edges/unlocked.rs b/raphtory-storage/src/graph/edges/unlocked.rs index 087d6125e0..5889565da9 100644 --- a/raphtory-storage/src/graph/edges/unlocked.rs +++ b/raphtory-storage/src/graph/edges/unlocked.rs @@ -1,3 +1,4 @@ +use raphtory_api_macros::box_on_debug_lifetime; use raphtory_core::entities::{LayerIds, EID}; use rayon::prelude::*; use storage::{pages::edge_store::EdgeStorageInner, utils::Iter4, Extension, Layer}; @@ -23,7 +24,11 @@ impl<'a> UnlockedEdges<'a> { .map(EdgeStorageEntry::Unlocked) } - pub fn iter(self, layer_ids: &'a LayerIds) -> impl Iterator> + 'a { + #[box_on_debug_lifetime] + pub fn iter( + self, + layer_ids: &'a LayerIds, + ) -> impl Iterator> + Send + Sync + 'a { match layer_ids { LayerIds::None => Iter4::I(std::iter::empty()), LayerIds::All => Iter4::J(self.iter_layer(0)), diff --git a/raphtory/build.rs b/raphtory/build.rs index be1eda9fde..f424e39b19 100644 --- a/raphtory/build.rs +++ b/raphtory/build.rs @@ -3,10 +3,19 @@ use std::io::Result; fn main() -> Result<()> { prost_build::compile_protos(&["src/serialise/graph.proto"], &["src/serialise"])?; println!("cargo::rerun-if-changed=src/serialise/graph.proto"); + + println!("cargo::rustc-check-cfg=cfg(has_debug_symbols)"); + if let Ok("true" | "1" | "2") = std::env::var("DEBUG").as_deref() { + println!("cargo::rustc-cfg=has_debug_symbols"); + } Ok(()) } #[cfg(not(feature = "proto"))] fn main() -> Result<()> { + println!("cargo::rustc-check-cfg=cfg(has_debug_symbols)"); + if let Ok("true" | "1" | "2") = std::env::var("DEBUG").as_deref() { + println!("cargo::rustc-cfg=has_debug_symbols"); + } Ok(()) } diff --git a/raphtory/src/db/api/view/internal/time_semantics/base_time_semantics.rs b/raphtory/src/db/api/view/internal/time_semantics/base_time_semantics.rs index b2c044b496..e539323334 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/base_time_semantics.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/base_time_semantics.rs @@ -10,7 +10,7 @@ use raphtory_api::core::{ entities::{properties::prop::Prop, LayerIds, ELID}, storage::timeindex::TimeIndexEntry, }; -use raphtory_storage::graph::{ nodes::node_ref::NodeStorageRef}; +use raphtory_storage::graph::nodes::node_ref::NodeStorageRef; use std::ops::Range; use storage::EdgeEntryRef; @@ -287,8 +287,6 @@ impl EdgeTimeSemanticsOps for BaseTimeSemantics { for_all_iter!(self, semantics => semantics.edge_history_window(edge, view, layer_ids, w)) } - - #[inline] fn edge_exploded_count<'graph, G: GraphView + 'graph>( &self, diff --git a/raphtory/src/db/api/view/internal/time_semantics/time_semantics.rs b/raphtory/src/db/api/view/internal/time_semantics/time_semantics.rs index 2812e24e06..3cb7e2b89f 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/time_semantics.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/time_semantics.rs @@ -11,7 +11,7 @@ use raphtory_api::core::{ entities::{properties::prop::Prop, LayerIds, ELID}, storage::timeindex::TimeIndexEntry, }; -use raphtory_storage::graph::{nodes::node_ref::NodeStorageRef}; +use raphtory_storage::graph::nodes::node_ref::NodeStorageRef; use std::ops::Range; use storage::EdgeEntryRef; diff --git a/raphtory/src/db/api/view/internal/time_semantics/time_semantics_ops.rs b/raphtory/src/db/api/view/internal/time_semantics/time_semantics_ops.rs index e11a5d0c57..aada2ff877 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/time_semantics_ops.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/time_semantics_ops.rs @@ -3,7 +3,7 @@ use raphtory_api::core::{ entities::{properties::prop::Prop, LayerIds, ELID}, storage::timeindex::TimeIndexEntry, }; -use raphtory_storage::graph::{nodes::node_ref::NodeStorageRef}; +use raphtory_storage::graph::nodes::node_ref::NodeStorageRef; use std::ops::Range; use storage::EdgeEntryRef; @@ -191,7 +191,6 @@ pub trait EdgeTimeSemanticsOps { w: Range, ) -> impl Iterator + Send + Sync + 'graph; - /// The number of exploded edge events for the `edge` fn edge_exploded_count<'graph, G: GraphView + 'graph>( &self, diff --git a/raphtory/src/db/api/view/internal/time_semantics/window_time_semantics.rs b/raphtory/src/db/api/view/internal/time_semantics/window_time_semantics.rs index 8d7ee75930..b46575fadd 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/window_time_semantics.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/window_time_semantics.rs @@ -8,7 +8,7 @@ use raphtory_api::core::{ entities::{properties::prop::Prop, LayerIds, ELID}, storage::timeindex::TimeIndexEntry, }; -use raphtory_storage::graph::{nodes::node_ref::NodeStorageRef}; +use raphtory_storage::graph::nodes::node_ref::NodeStorageRef; use std::ops::Range; use storage::EdgeEntryRef; diff --git a/raphtory/src/db/graph/views/cached_view.rs b/raphtory/src/db/graph/views/cached_view.rs index 54ce914f7a..3d408dc8ca 100644 --- a/raphtory/src/db/graph/views/cached_view.rs +++ b/raphtory/src/db/graph/views/cached_view.rs @@ -22,7 +22,7 @@ use raphtory_api::{ use raphtory_storage::{ core_ops::CoreGraphOps, graph::{ - edges::{edge_storage_ops::EdgeStorageOps}, + edges::edge_storage_ops::EdgeStorageOps, nodes::{node_ref::NodeStorageRef, node_storage_ops::NodeStorageOps}, }, }; diff --git a/raphtory/src/db/graph/views/filter/edge_and_filtered_graph.rs b/raphtory/src/db/graph/views/filter/edge_and_filtered_graph.rs index e8802ce1d1..6b17ac0176 100644 --- a/raphtory/src/db/graph/views/filter/edge_and_filtered_graph.rs +++ b/raphtory/src/db/graph/views/filter/edge_and_filtered_graph.rs @@ -24,7 +24,7 @@ use raphtory_api::{ }, inherit::Base, }; -use raphtory_storage::{core_ops::InheritCoreGraphOps}; +use raphtory_storage::core_ops::InheritCoreGraphOps; use std::ops::Range; use storage::EdgeEntryRef; diff --git a/raphtory/src/db/graph/views/filter/edge_field_filtered_graph.rs b/raphtory/src/db/graph/views/filter/edge_field_filtered_graph.rs index b91f352d98..60896e9558 100644 --- a/raphtory/src/db/graph/views/filter/edge_field_filtered_graph.rs +++ b/raphtory/src/db/graph/views/filter/edge_field_filtered_graph.rs @@ -1,4 +1,3 @@ -use raphtory_storage::graph::edges::edge_ref::EdgeEntryRef; use crate::{ db::{ api::{ @@ -16,7 +15,7 @@ use crate::{ prelude::GraphViewOps, }; use raphtory_api::{core::entities::LayerIds, inherit::Base}; -use raphtory_storage::{core_ops::InheritCoreGraphOps}; +use raphtory_storage::{core_ops::InheritCoreGraphOps, graph::edges::edge_ref::EdgeEntryRef}; #[derive(Debug, Clone)] pub struct EdgeFieldFilteredGraph { diff --git a/raphtory/src/db/graph/views/filter/edge_or_filtered_graph.rs b/raphtory/src/db/graph/views/filter/edge_or_filtered_graph.rs index 2c16d1e100..30defcfb3b 100644 --- a/raphtory/src/db/graph/views/filter/edge_or_filtered_graph.rs +++ b/raphtory/src/db/graph/views/filter/edge_or_filtered_graph.rs @@ -24,7 +24,7 @@ use raphtory_api::{ }, inherit::Base, }; -use raphtory_storage::{core_ops::InheritCoreGraphOps}; +use raphtory_storage::core_ops::InheritCoreGraphOps; use std::ops::Range; use storage::EdgeEntryRef; diff --git a/raphtory/src/db/graph/views/filter/edge_property_filtered_graph.rs b/raphtory/src/db/graph/views/filter/edge_property_filtered_graph.rs index 5ea0d120cd..5d16f32669 100644 --- a/raphtory/src/db/graph/views/filter/edge_property_filtered_graph.rs +++ b/raphtory/src/db/graph/views/filter/edge_property_filtered_graph.rs @@ -16,7 +16,7 @@ use crate::{ prelude::{GraphViewOps, LayerOps}, }; use raphtory_api::inherit::Base; -use raphtory_storage::{core_ops::InheritCoreGraphOps}; +use raphtory_storage::core_ops::InheritCoreGraphOps; use storage::EdgeEntryRef; #[derive(Debug, Clone)] diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index aa9afe6807..c00a104ee0 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -11,7 +11,7 @@ use crate::{ prelude::{GraphViewOps, NodeViewOps}, }; use raphtory_api::core::entities::properties::prop::Prop; -use raphtory_storage::graph::edges::{edge_storage_ops::EdgeStorageOps}; +use raphtory_storage::graph::edges::edge_storage_ops::EdgeStorageOps; use std::{collections::HashSet, fmt, fmt::Display, ops::Deref, sync::Arc}; use storage::EdgeEntryRef; diff --git a/raphtory/src/db/graph/views/filter/model/property_filter.rs b/raphtory/src/db/graph/views/filter/model/property_filter.rs index 06db4747ad..4641c9d10a 100644 --- a/raphtory/src/db/graph/views/filter/model/property_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/property_filter.rs @@ -23,7 +23,7 @@ use raphtory_api::core::{ storage::{arc_str::ArcStr, timeindex::TimeIndexEntry}, }; use raphtory_storage::graph::{ - edges::{edge_storage_ops::EdgeStorageOps}, + edges::edge_storage_ops::EdgeStorageOps, nodes::{node_ref::NodeStorageRef, node_storage_ops::NodeStorageOps}, }; use std::{collections::HashSet, fmt, fmt::Display, sync::Arc}; diff --git a/raphtory/src/io/arrow/df_loaders.rs b/raphtory/src/io/arrow/df_loaders.rs index d894d0251d..d346378726 100644 --- a/raphtory/src/io/arrow/df_loaders.rs +++ b/raphtory/src/io/arrow/df_loaders.rs @@ -101,7 +101,9 @@ pub(crate) fn load_nodes_from_df< let mut node_type_col_resolved = vec![]; let mut write_locked_graph = graph.write_lock().map_err(into_graph_err)?; - let mut start_id = session.reserve_event_ids(df_view.num_rows).map_err(into_graph_err)?; + let mut start_id = session + .reserve_event_ids(df_view.num_rows) + .map_err(into_graph_err)?; for chunk in df_view.chunks { let df = chunk?; @@ -466,42 +468,39 @@ pub(crate) fn load_edges_from_df< // Add temporal & constant properties to edges sc.spawn(|_| { - write_locked_graph - .edges - .par_iter_mut() - .for_each(|shard| { - let mut t_props = vec![]; - let mut c_props = vec![]; - - for (idx, (((((src, dst), time), eid), layer), exists)) in src_col_resolved - .iter() - .zip(dst_col_resolved.iter()) - .zip(time_col.iter()) - .zip(eid_col_resolved.iter()) - .zip(layer_col_resolved.iter()) - .zip( - eids_exist - .iter() - .map(|exists| exists.load(Ordering::Relaxed)), - ) - .enumerate() - { - if let Some(eid_pos) = shard.resolve_pos(*eid) { - let t = TimeIndexEntry(time, start_idx + idx); - let mut writer = shard.writer(); - - t_props.clear(); - t_props.extend(prop_cols.iter_row(idx)); - - c_props.clear(); - c_props.extend(metadata_cols.iter_row(idx)); - c_props.extend_from_slice(&shared_metadata); - - writer.add_static_edge(Some(eid_pos), *src, *dst, 0, Some(exists)); - writer.update_c_props(eid_pos, *src, *dst, *layer, c_props.drain(..)); - writer.add_edge(t, eid_pos, *src, *dst, t_props.drain(..), *layer, 0); - } + write_locked_graph.edges.par_iter_mut().for_each(|shard| { + let mut t_props = vec![]; + let mut c_props = vec![]; + + for (idx, (((((src, dst), time), eid), layer), exists)) in src_col_resolved + .iter() + .zip(dst_col_resolved.iter()) + .zip(time_col.iter()) + .zip(eid_col_resolved.iter()) + .zip(layer_col_resolved.iter()) + .zip( + eids_exist + .iter() + .map(|exists| exists.load(Ordering::Relaxed)), + ) + .enumerate() + { + if let Some(eid_pos) = shard.resolve_pos(*eid) { + let t = TimeIndexEntry(time, start_idx + idx); + let mut writer = shard.writer(); + + t_props.clear(); + t_props.extend(prop_cols.iter_row(idx)); + + c_props.clear(); + c_props.extend(metadata_cols.iter_row(idx)); + c_props.extend_from_slice(&shared_metadata); + + writer.add_static_edge(Some(eid_pos), *src, *dst, 0, Some(exists)); + writer.update_c_props(eid_pos, *src, *dst, *layer, c_props.drain(..)); + writer.add_edge(t, eid_pos, *src, *dst, t_props.drain(..), *layer, 0); } + } }); }); }); @@ -691,31 +690,28 @@ pub(crate) fn load_node_props_from_df< write_locked_graph .resize_chunks_to_num_nodes(write_locked_graph.graph().internal_num_nodes()); - write_locked_graph - .nodes - .iter_mut() - .try_for_each(|shard| { - let mut c_props = vec![]; - - for (idx, ((vid, node_type), gid)) in node_col_resolved - .iter() - .zip(node_type_col_resolved.iter()) - .zip(node_col.iter()) - .enumerate() - { - if let Some(mut_node) = shard.resolve_pos(*vid) { - let mut writer = shard.writer(); - writer.store_node_id_and_node_type(mut_node, 0, gid, *node_type, 0); - - c_props.clear(); - c_props.extend(metadata_cols.iter_row(idx)); - c_props.extend_from_slice(&shared_metadata); - writer.update_c_props(mut_node, 0, c_props.drain(..), 0); - }; - } + write_locked_graph.nodes.iter_mut().try_for_each(|shard| { + let mut c_props = vec![]; + + for (idx, ((vid, node_type), gid)) in node_col_resolved + .iter() + .zip(node_type_col_resolved.iter()) + .zip(node_col.iter()) + .enumerate() + { + if let Some(mut_node) = shard.resolve_pos(*vid) { + let mut writer = shard.writer(); + writer.store_node_id_and_node_type(mut_node, 0, gid, *node_type, 0); + + c_props.clear(); + c_props.extend(metadata_cols.iter_row(idx)); + c_props.extend_from_slice(&shared_metadata); + writer.update_c_props(mut_node, 0, c_props.drain(..), 0); + }; + } - Ok::<_, GraphError>(()) - })?; + Ok::<_, GraphError>(()) + })?; #[cfg(feature = "python")] let _ = pb.update(df.len()); diff --git a/raphtory/src/io/arrow/layer_col.rs b/raphtory/src/io/arrow/layer_col.rs index 8a0541d738..18e8c3d850 100644 --- a/raphtory/src/io/arrow/layer_col.rs +++ b/raphtory/src/io/arrow/layer_col.rs @@ -48,12 +48,8 @@ impl<'a> LayerCol<'a> { pub fn iter(self) -> impl Iterator> { match self { - LayerCol::Name { name, len } => { - LayerColVariants::Name((0..len).map(move |_| name)) - } - LayerCol::Utf8 { col } => { - LayerColVariants::Utf8((0..col.len()).map(|i| col.get(i))) - } + LayerCol::Name { name, len } => LayerColVariants::Name((0..len).map(move |_| name)), + LayerCol::Utf8 { col } => LayerColVariants::Utf8((0..col.len()).map(|i| col.get(i))), LayerCol::LargeUtf8 { col } => { LayerColVariants::LargeUtf8((0..col.len()).map(|i| col.get(i))) } diff --git a/raphtory/src/serialise/parquet/nodes.rs b/raphtory/src/serialise/parquet/nodes.rs index 7650fc5d3c..f129c21581 100644 --- a/raphtory/src/serialise/parquet/nodes.rs +++ b/raphtory/src/serialise/parquet/nodes.rs @@ -93,7 +93,6 @@ pub(crate) fn encode_nodes_cprop( .chunks(row_group_size) .into_iter() .map(|chunk| chunk.collect_vec()) - // scope for the decoder { decoder.serialize(&node_rows)?; From be2348c48a9040456c47d4323e290432c487799d Mon Sep 17 00:00:00 2001 From: Fadhil Abubaker Date: Wed, 10 Sep 2025 17:13:48 -0400 Subject: [PATCH 152/321] Use correct ranges in RoaringBitMap ItemProducer --- db4-storage/src/segments/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/db4-storage/src/segments/mod.rs b/db4-storage/src/segments/mod.rs index 8ac5c381ee..ef3b02719a 100644 --- a/db4-storage/src/segments/mod.rs +++ b/db4-storage/src/segments/mod.rs @@ -80,14 +80,14 @@ impl<'a> Producer for ItemProducer<'a> { type IntoIter = Iter<'a>; fn into_iter(self) -> Self::IntoIter { - let start = self.items.select(self.range.start).unwrap_or(u32::MAX); + let start = self.items.select(self.range.start).unwrap_or(u32::MIN); let end = self.items.select(self.range.end).unwrap_or(u32::MAX); self.items.range(start..end) } fn split_at(self, index: usize) -> (Self, Self) { let left_range = self.range.start..(self.range.start + index as u32); - let right_range = index as u32..self.range.end; + let right_range = (self.range.start + index as u32)..self.range.end; ( ItemProducer { items: self.items, From e656067fcaff5399f8c9c6ea0197be0ee42c346c Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Wed, 10 Sep 2025 12:40:35 +0200 Subject: [PATCH 153/321] some more send and sync --- db4-storage/src/gen_ts.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/db4-storage/src/gen_ts.rs b/db4-storage/src/gen_ts.rs index 8d39d78def..ffe09c31bf 100644 --- a/db4-storage/src/gen_ts.rs +++ b/db4-storage/src/gen_ts.rs @@ -17,7 +17,7 @@ pub enum LayerIter<'a> { pub static ALL_LAYERS: LayerIter<'static> = LayerIter::LRef(&LayerIds::All); impl<'a> LayerIter<'a> { - pub fn into_iter(self, num_layers: usize) -> impl Iterator + 'a { + pub fn into_iter(self, num_layers: usize) -> impl Iterator + Send + Sync + 'a { match self { LayerIter::One(id) => Iter2::I1(std::iter::once(id)), LayerIter::LRef(layers) => Iter2::I2(layers.iter(num_layers)), @@ -72,19 +72,19 @@ where self, layer_id: usize, range: Option<(TimeIndexEntry, TimeIndexEntry)>, - ) -> impl Iterator + 'a; + ) -> impl Iterator + Send + Sync + 'a; fn additions_tc( self, layer_id: usize, range: Option<(TimeIndexEntry, TimeIndexEntry)>, - ) -> impl Iterator + 'a; + ) -> impl Iterator + Send + Sync + 'a; fn deletions_tc( self, layer_id: usize, range: Option<(TimeIndexEntry, TimeIndexEntry)>, - ) -> impl Iterator + 'a; + ) -> impl Iterator + Send + Sync + 'a; fn num_layers(&self) -> usize; } @@ -297,7 +297,7 @@ where pub fn edge_events(self) -> impl Iterator + Send + Sync + 'a { self.layer_id .into_iter(self.node.num_layers()) - .flat_map(|layer_id| { + .flat_map(move |layer_id| { self.node .additions_tc(layer_id, self.range) .map(|t_cell| t_cell.edge_events()) @@ -320,7 +320,7 @@ where } impl<'a, Ref: WithTimeCells<'a> + 'a> GenericTimeOps<'a, Ref> { - pub fn time_cells(self) -> impl Iterator + 'a { + pub fn time_cells(self) -> impl Iterator + Send + Sync + 'a { let range = self.range; self.layer_id .into_iter(self.node.num_layers()) From 130c94f66362f6ea5ffb28119a1296f852ad3868 Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Thu, 11 Sep 2025 14:52:44 +0200 Subject: [PATCH 154/321] switch to vector based indexing in SegmentContainer --- db4-storage/src/segments/edge.rs | 21 +- db4-storage/src/segments/edge_entry.rs | 6 +- db4-storage/src/segments/mod.rs | 336 +++++++++++++------------ db4-storage/src/segments/node.rs | 80 ++---- 4 files changed, 204 insertions(+), 239 deletions(-) diff --git a/db4-storage/src/segments/edge.rs b/db4-storage/src/segments/edge.rs index 20520b1783..92bc49066d 100644 --- a/db4-storage/src/segments/edge.rs +++ b/db4-storage/src/segments/edge.rs @@ -136,7 +136,7 @@ impl MemEdgeSegment { pub fn get_edge(&self, edge_pos: LocalPOS, layer_id: usize) -> Option<(VID, VID)> { self.layers .get(layer_id)? - .get(&edge_pos) + .get(edge_pos) .map(|entry| (entry.src, entry.dst)) } @@ -243,21 +243,10 @@ impl MemEdgeSegment { // Ensure we have enough layers self.ensure_layer(layer_id); - let row = self.layers[layer_id] - .reserve_local_row(edge_pos) - .map_either( - |row| { - row.src = src; - row.dst = dst; - row.row() - }, - |row| { - row.src = src; - row.dst = dst; - row.row() - }, - ); - row.either(|a| a, |a| a) + let row = self.layers[layer_id].reserve_local_row(edge_pos).inner(); + row.src = src; + row.dst = dst; + row.row } pub fn check_const_properties( diff --git a/db4-storage/src/segments/edge_entry.rs b/db4-storage/src/segments/edge_entry.rs index 4a4e17f4ea..a67a0e0dba 100644 --- a/db4-storage/src/segments/edge_entry.rs +++ b/db4-storage/src/segments/edge_entry.rs @@ -156,7 +156,7 @@ impl<'a> EdgeRefOps<'a> for MemEdgeRef<'a> { self.es .as_ref() .get(layer_id)? //.get(layer_id)? - .get(&self.pos) + .get(self.pos) .map(|entry| (entry.src, entry.dst)) } @@ -177,11 +177,11 @@ impl<'a> EdgeRefOps<'a> for MemEdgeRef<'a> { } fn src(&self) -> Option { - self.es.as_ref()[0].get(&self.pos).map(|entry| entry.src) + self.es.as_ref()[0].get(self.pos).map(|entry| entry.src) } fn dst(&self) -> Option { - self.es.as_ref()[0].get(&self.pos).map(|entry| entry.dst) + self.es.as_ref()[0].get(self.pos).map(|entry| entry.dst) } fn edge_id(&self) -> EID { diff --git a/db4-storage/src/segments/mod.rs b/db4-storage/src/segments/mod.rs index ef3b02719a..949f706469 100644 --- a/db4-storage/src/segments/mod.rs +++ b/db4-storage/src/segments/mod.rs @@ -1,7 +1,11 @@ use super::properties::{Properties, RowEntry}; use crate::{LocalPOS, error::StorageError}; use either::Either; -use raphtory_api::core::entities::properties::{meta::Meta, prop::Prop}; +use polars_arrow::pushable::Pushable; +use raphtory_api::core::{ + entities::properties::{meta::Meta, prop::Prop}, + storage::dict_mapper::MaybeNew, +}; use raphtory_core::{ entities::{ ELID, @@ -15,7 +19,12 @@ use rayon::{ }; use roaring::{RoaringBitmap, bitmap::Iter}; use rustc_hash::FxHashMap; -use std::{collections::hash_map::Entry, fmt::Debug, ops::Range, sync::Arc}; +use std::{ + collections::hash_map::Entry, + fmt::{Debug, Formatter, Pointer}, + ops::Range, + sync::Arc, +}; pub mod edge; pub mod node; @@ -24,135 +33,147 @@ pub mod additions; pub mod edge_entry; pub mod node_entry; -pub struct SegmentContainer { - segment_id: usize, - items: RoaringBitmap, - data: FxHashMap, - max_page_len: u32, - properties: Properties, - meta: Arc, - lsn: u64, +pub type PageIndexT = u32; + +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +struct PageIndexEntry(PageIndexT); + +impl Default for PageIndexEntry { + fn default() -> Self { + PageIndexEntry(PageIndexT::MAX) + } } -pub struct FullRangeIter<'a, T> { - range: Range, - iter: Iter<'a>, - head: Option, - container: &'a SegmentContainer, +impl PageIndexEntry { + fn index(self) -> Option { + (self.0 != PageIndexT::MAX).then_some(self.0 as usize) + } + + fn is_filled(self) -> bool { + self.0 != PageIndexT::MAX + } } -impl<'a, T: HasRow> Iterator for FullRangeIter<'a, T> { - type Item = (LocalPOS, Option<(&'a T, RowEntry<'a>)>); +#[derive(Default)] +struct PageIndex(Vec); + +impl PageIndex { + fn get(&self, pos: LocalPOS) -> Option { + self.0.get(pos.as_index()).and_then(|index| index.index()) + } - fn next(&mut self) -> Option { - let next_item = self.range.next()?; - if self.head.is_none() { - self.head = self.iter.next(); + fn set(&mut self, pos: LocalPOS, index: PageIndexEntry) { + let pos_index = pos.as_index(); + if pos_index >= self.0.len() { + self.0.resize(pos_index + 1, PageIndexEntry::default()); } - let l_pos = LocalPOS(next_item); - let data = if self - .head - .as_ref() - .filter(|&&head| head == next_item) - .is_some() - { - let entry = self.container.data.get(&l_pos).unwrap(); - self.head = None; - Some((entry, self.container.properties().get_entry(entry.row()))) - } else { - None - }; - Some((LocalPOS(next_item), data)) - } - - fn size_hint(&self) -> (usize, Option) { - self.range.size_hint() + self.0[pos_index] = index; + } + + fn iter(&self) -> impl ExactSizeIterator> { + self.0.iter().map(|i| i.index()) + } + + fn filled_positions(&self) -> impl Iterator { + self.0 + .iter() + .enumerate() + .filter_map(|(i, p)| p.is_filled().then_some(LocalPOS::from(i))) + } + + fn par_iter(&self) -> impl IndexedParallelIterator> { + self.0.par_iter().map(|i| i.index()) + } + + fn len(&self) -> usize { + self.0.len() } } -struct ItemProducer<'a> { - items: &'a RoaringBitmap, - range: Range, +#[derive(Default)] +struct SparseVec { + index: PageIndex, + data: Vec<(LocalPOS, T)>, } -impl<'a> Producer for ItemProducer<'a> { - type Item = u32; - type IntoIter = Iter<'a>; - - fn into_iter(self) -> Self::IntoIter { - let start = self.items.select(self.range.start).unwrap_or(u32::MIN); - let end = self.items.select(self.range.end).unwrap_or(u32::MAX); - self.items.range(start..end) - } - - fn split_at(self, index: usize) -> (Self, Self) { - let left_range = self.range.start..(self.range.start + index as u32); - let right_range = (self.range.start + index as u32)..self.range.end; - ( - ItemProducer { - items: self.items, - range: left_range, - }, - ItemProducer { - items: self.items, - range: right_range, - }, - ) +impl Debug for SparseVec { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_list().entries(self.iter_filled()).finish() } } -pub struct ParItemIter<'a> { - items: &'a RoaringBitmap, -} +impl SparseVec { + fn get(&self, pos: LocalPOS) -> Option<&T> { + self.index + .get(pos) + .and_then(|i| self.data.get(i).map(|(_, x)| x)) + } -impl<'a> ParallelIterator for ParItemIter<'a> { - type Item = u32; + fn get_mut(&mut self, pos: LocalPOS) -> Option<&mut T> { + self.index + .get(pos) + .and_then(|i| self.data.get_mut(i).map(|(_, x)| x)) + } - fn drive_unindexed(self, consumer: C) -> C::Result - where - C: UnindexedConsumer, - { - bridge(self, consumer) + fn is_filled(&self, pos: LocalPOS) -> bool { + self.index.get(pos).is_some() } -} -impl<'a> IndexedParallelIterator for ParItemIter<'a> { - fn len(&self) -> usize { - self.items.len() as usize + /// Iterator over filled positions. + /// + /// Note that this returns items in insertion order! + fn iter_filled(&self) -> impl Iterator { + self.data.iter().map(|(i, x)| (*i, x)) } - fn drive>(self, consumer: C) -> C::Result { - bridge(self, consumer) + fn iter_all(&self) -> impl ExactSizeIterator> { + self.index.iter().map(|i| i.map(|i| &self.data[i].1)) } - fn with_producer>(self, callback: CB) -> CB::Output { - let producer = ItemProducer { - items: self.items, - range: 0..self.items.len() as u32, - }; - callback.callback(producer) + fn num_filled(&self) -> usize { + self.data.len() } } -impl<'a, T: HasRow> ExactSizeIterator for FullRangeIter<'a, T> {} - -impl Debug for SegmentContainer { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let items = self.items.iter().collect::>(); - let mut data = self.data.iter().collect::>(); - data.sort_by(|a, b| a.0.cmp(b.0)); +impl SparseVec { + /// Parallel iterator over filled positions. + /// + /// Note that this returns items in insertion order! + fn par_iter_filled(&self) -> impl IndexedParallelIterator { + self.data.par_iter().map(|(i, x)| (*i, x)) + } + fn par_iter_all(&self) -> impl IndexedParallelIterator> { + self.index.par_iter().map(|i| i.map(|i| &self.data[i].1)) + } +} - f.debug_struct("SegmentContainer") - .field("page_id", &self.segment_id) - .field("items", &items as &dyn Debug) - .field("data", &data) - .field("max_page_len", &self.max_page_len) - .field("properties", &self.properties) - .finish() +impl SparseVec { + fn get_or_new(&mut self, pos: LocalPOS) -> MaybeNew<&mut T> { + match self.index.get(pos) { + None => { + let next_index = self.data.len(); + self.data.push((pos, T::default())); + let new_entry = &mut self.data[next_index].1; + *new_entry.row_mut() = next_index; + self.index.set(pos, PageIndexEntry(next_index as u32)); + MaybeNew::New(new_entry) + } + Some(i) => MaybeNew::Existing(&mut self.data[i].1), + } } } -pub trait HasRow: Default + Send + Sync { +#[derive(Debug)] +pub struct SegmentContainer { + segment_id: usize, + data: SparseVec, + max_page_len: u32, + properties: Properties, + meta: Arc, + lsn: u64, +} + +pub trait HasRow: Default + Send + Sync + Sized { fn row(&self) -> usize; fn row_mut(&mut self) -> &mut usize; } @@ -162,7 +183,6 @@ impl SegmentContainer { assert!(max_page_len > 0, "max_page_len must be greater than 0"); Self { segment_id, - items: RoaringBitmap::new(), data: Default::default(), max_page_len, properties: Default::default(), @@ -174,7 +194,8 @@ impl SegmentContainer { #[inline] pub fn est_size(&self) -> usize { //TODO: this is a rough estimate and should be improved - let data_size = (self.data.len() as f64 * std::mem::size_of::() as f64 * 1.5) as usize; // Estimate size of data + let data_size = + (self.data.num_filled() as f64 * std::mem::size_of::() as f64 * 1.5) as usize; // Estimate size of data let timestamp_size = std::mem::size_of::(); (self.properties.additions_count * timestamp_size) + data_size @@ -182,20 +203,12 @@ impl SegmentContainer { + self.c_prop_est_size() } - pub fn get(&self, item_pos: &LocalPOS) -> Option<&T> { - if self.items.contains(item_pos.0) { - self.data.get(item_pos) - } else { - None - } + pub fn get(&self, item_pos: LocalPOS) -> Option<&T> { + self.data.get(item_pos) } pub fn has_item(&self, item_pos: LocalPOS) -> bool { - self.items.contains(item_pos.0) - } - - pub fn set_item(&mut self, item_pos: LocalPOS) { - self.items.insert(item_pos.0); + self.data.is_filled(item_pos) } pub fn max_page_len(&self) -> u32 { @@ -203,7 +216,7 @@ impl SegmentContainer { } pub fn is_full(&self) -> bool { - self.data.len() == self.max_page_len() as usize + self.data.num_filled() == self.max_page_len() as usize } pub fn t_len(&self) -> usize { @@ -214,17 +227,8 @@ impl SegmentContainer { /// If the item position already exists, it returns a mutable reference to the existing item. /// Left variant indicates that the item was already present, /// Right variant indicates that a new item was created. - pub(crate) fn reserve_local_row(&mut self, item_pos: LocalPOS) -> Either<&mut T, &mut T> { - let local_row = self.data.len(); - self.set_item(item_pos); - match self.data.entry(item_pos) { - Entry::Occupied(occupied_entry) => Either::Left(occupied_entry.into_mut()), - Entry::Vacant(vacant_entry) => { - let vacant_entry = vacant_entry.insert(T::default()); - *vacant_entry.row_mut() = local_row; - Either::Right(vacant_entry) - } - } + pub(crate) fn reserve_local_row(&mut self, item_pos: LocalPOS) -> MaybeNew<&mut T> { + self.data.get_or_new(item_pos) } #[inline] @@ -252,7 +256,7 @@ impl SegmentContainer { local_pos: LocalPOS, props: &[(usize, Prop)], ) -> Result<(), StorageError> { - if let Some(item) = self.get(&local_pos) { + if let Some(item) = self.get(local_pos) { let local_row = item.row(); let edge_properties = self.properties().get_entry(local_row); for (prop_id, prop_val) in props { @@ -266,19 +270,12 @@ impl SegmentContainer { &self.meta } - pub fn items(&self) -> &RoaringBitmap { - &self.items - } - pub fn filled_positions(&self) -> impl Iterator { - self.items.iter().map(LocalPOS) + self.data.index.filled_positions() } pub fn filled_positions_par(&self) -> impl ParallelIterator { - ParItemIter { - items: self.items(), - } - .map(LocalPOS) + self.data.par_iter_filled().map(|(i, _)| i) } #[inline(always)] @@ -297,41 +294,48 @@ impl SegmentContainer { } pub fn len(&self) -> u32 { - self.data.len() as u32 + self.data.data.len() as u32 } pub fn is_empty(&self) -> bool { - self.data.is_empty() + self.data.data.is_empty() } + /// returns items in insertion order! pub fn row_entries(&self) -> impl Iterator)> { - self.items.iter().map(LocalPOS).filter_map(move |l_pos| { - let entry = self.data.get(&l_pos)?; - Some((l_pos, entry, self.properties().get_entry(entry.row()))) + self.data + .iter_filled() + .map(|(l_pos, entry)| (l_pos, entry, self.properties().get_entry(entry.row()))) + } + + /// return filled entries ordered by index + pub fn row_entries_ordered(&self) -> impl Iterator)> { + self.all_entries().filter_map(|(pos, entry)| { + let (v, row) = entry?; + Some((pos, v, row)) }) } - pub fn all_entries(&self) -> FullRangeIter<'_, T> { - FullRangeIter { - range: 0..self.max_page_len, - iter: self.items.iter(), - head: None, - container: self, - } + pub fn all_entries( + &self, + ) -> impl ExactSizeIterator)>)> { + self.data.iter_all().enumerate().map(|(i, v)| { + ( + LocalPOS::from(i), + v.map(|v| (v, self.properties().get_entry(v.row()))), + ) + }) } pub fn all_entries_par( &self, ) -> impl ParallelIterator)>)> + '_ { - (0..self.max_page_len) - .into_par_iter() - .map(LocalPOS) - .map(move |l_pos| { - let entry = self - .get(&l_pos) - .map(|entry| (entry, self.properties().get_entry(entry.row()))); - (l_pos, entry) - }) + self.data.par_iter_all().enumerate().map(|(i, v)| { + ( + LocalPOS::from(i), + v.map(|entry| (entry, self.properties().get_entry(entry.row()))), + ) + }) } pub fn earliest(&self) -> Option { @@ -343,7 +347,7 @@ impl SegmentContainer { } pub fn temporal_index(&self) -> Vec { - self.row_entries() + self.row_entries_ordered() .flat_map(|(_, mp, _)| { let row = mp.row(); self.properties() @@ -357,7 +361,7 @@ impl SegmentContainer { pub fn t_prop(&self, item_id: impl Into, prop_id: usize) -> Option> { let item_id = item_id.into(); - self.data.get(&item_id).and_then(|entry| { + self.data.get(item_id).and_then(|entry| { let prop_entry = self.properties.get_entry(entry.row()); prop_entry.prop(prop_id) }) @@ -366,7 +370,7 @@ impl SegmentContainer { pub fn t_prop_rows(&self, item_id: impl Into) -> &TCell> { let item_id = item_id.into(); self.data - .get(&item_id) + .get(item_id) .map(|entry| { let prop_entry = self.properties.get_entry(entry.row()); prop_entry.t_cell() @@ -376,7 +380,7 @@ impl SegmentContainer { pub fn c_prop(&self, item_id: impl Into, prop_id: usize) -> Option { let item_id = item_id.into(); - self.data.get(&item_id).and_then(|entry| { + self.data.get(item_id).and_then(|entry| { let prop_entry = self.properties.c_column(prop_id)?; prop_entry.get(entry.row()) }) @@ -384,7 +388,7 @@ impl SegmentContainer { pub fn c_prop_str(&self, item_id: impl Into, prop_id: usize) -> Option<&str> { let item_id = item_id.into(); - self.data.get(&item_id).and_then(|entry| { + self.data.get(item_id).and_then(|entry| { let prop_entry = self.properties.c_column(prop_id)?; prop_entry .get_ref(entry.row()) @@ -394,21 +398,21 @@ impl SegmentContainer { pub fn additions(&self, item_pos: LocalPOS) -> &TCell { self.data - .get(&item_pos) + .get(item_pos) .and_then(|entry| self.properties.additions(entry.row())) .unwrap_or(&TCell::Empty) } pub fn deletions(&self, item_pos: LocalPOS) -> &TCell { self.data - .get(&item_pos) + .get(item_pos) .and_then(|entry| self.properties.deletions(entry.row())) .unwrap_or(&TCell::Empty) } pub fn times_from_props(&self, item_pos: LocalPOS) -> &TCell> { self.data - .get(&item_pos) + .get(item_pos) .and_then(|entry| self.properties.times_from_props(entry.row())) .unwrap_or(&TCell::Empty) } diff --git a/db4-storage/src/segments/node.rs b/db4-storage/src/segments/node.rs index eab60f2a99..9c79236902 100644 --- a/db4-storage/src/segments/node.rs +++ b/db4-storage/src/segments/node.rs @@ -152,7 +152,7 @@ impl MemNodeSegment { fn get_adj(&self, n: LocalPOS, layer_id: usize) -> Option<&Adj> { self.layers .get(layer_id)? - .get(&n) + .get(n) .map(|AdjEntry { adj, .. }| adj) } @@ -207,29 +207,17 @@ impl MemNodeSegment { let est_size = layer.est_size(); layer.set_lsn(lsn); - let add_out = layer.reserve_local_row(src_pos).map_either( - |row| { - let new_mem_edge = row.adj.add_edge_out(dst, e_id.edge); - (new_mem_edge, row.row()) - }, - |row| { - row.adj.add_edge_out(dst, e_id.edge); - (true, row.row()) - }, - ); - - let is_new_edge = add_out.as_ref().either( - |(is_new, _)| *is_new as usize, - |(is_new, _)| *is_new as usize, - ); - - let new_entry = add_out.is_right(); + let mut add_out = layer.reserve_local_row(src_pos); + let new_entry = add_out.is_new(); + let add_out = add_out.inner(); + let is_new_edge = add_out.adj.add_edge_out(dst, e_id.edge); + let row = add_out.row; if let Some(t) = t { - self.update_timestamp_inner(t, add_out.either(|(_, a)| a, |(_, a)| a), e_id); + self.update_timestamp_inner(t, row, e_id); } let layer_est_size = self.layers[layer_id].est_size(); - let added_size = - (layer_est_size - est_size) + (is_new_edge * std::mem::size_of::<(VID, VID)>()); + let added_size = (layer_est_size - est_size) + + (is_new_edge as usize * std::mem::size_of::<(VID, VID)>()); (new_entry, added_size) } @@ -249,28 +237,19 @@ impl MemNodeSegment { let layer = self.get_or_create_layer(layer_id); let est_size = layer.est_size(); layer.set_lsn(lsn); - let add_in = layer.reserve_local_row(dst_pos).map_either( - |row| { - let new_mem_edge = row.adj.add_edge_into(src, e_id.edge); - (new_mem_edge, row.row()) - }, - |row| { - row.adj.add_edge_into(src, e_id.edge); - (true, row.row()) - }, - ); - let is_new_edge = add_in.as_ref().either( - |(is_new, _)| *is_new as usize, - |(is_new, _)| *is_new as usize, - ); - let new_entry = add_in.is_right(); + let add_in = layer.reserve_local_row(dst_pos); + let new_entry = add_in.is_new(); + let add_in = add_in.inner(); + let is_new_edge = add_in.adj.add_edge_into(src, e_id.edge); + let row = add_in.row; + if let Some(t) = t { - self.update_timestamp_inner(t, add_in.either(|(_, a)| a, |(_, a)| a), e_id); + self.update_timestamp_inner(t, row, e_id); } let layer_est_size = self.layers[layer_id].est_size(); - let added_size = - (layer_est_size - est_size) + (is_new_edge * std::mem::size_of::<(VID, VID)>()); + let added_size = (layer_est_size - est_size) + + (is_new_edge as usize * std::mem::size_of::<(VID, VID)>()); (new_entry, added_size) } @@ -284,14 +263,9 @@ impl MemNodeSegment { } pub fn update_timestamp(&mut self, t: T, node_pos: LocalPOS, e_id: ELID) -> usize { - let (row, est_size) = { - let segment_container = &mut self.layers[e_id.layer()]; - let est_size = segment_container.est_size(); - let row = segment_container - .reserve_local_row(node_pos) - .either(|a| a.row, |a| a.row); - (row, est_size) - }; + let segment_container = &mut self.layers[e_id.layer()]; + let est_size = segment_container.est_size(); + let row = segment_container.reserve_local_row(node_pos).inner().row(); self.update_timestamp_inner(t, row, e_id); let layer_est_size = self.layers[e_id.layer()].est_size(); let added_size = layer_est_size - est_size; @@ -308,8 +282,8 @@ impl MemNodeSegment { let layer = self.get_or_create_layer(layer_id); let est_size = layer.est_size(); let row = layer.reserve_local_row(node_pos); - let is_new = row.is_right(); - let row = row.either(|a| a.row, |a| a.row); + let is_new = row.is_new(); + let row = row.inner().row; let mut prop_mut_entry = layer.properties_mut().get_mut_entry(row); let ts = TimeIndexEntry::new(t.t(), t.i()); prop_mut_entry.append_t_props(ts, props); @@ -338,11 +312,9 @@ impl MemNodeSegment { let segment_container = self.get_or_create_layer(layer_id); let est_size = segment_container.est_size(); - let row = segment_container - .reserve_local_row(node_pos) - .map_either(|a| a.row, |a| a.row); - let is_new = row.is_right(); - let row = row.either(|a| a, |a| a); + let row = segment_container.reserve_local_row(node_pos).map(|a| a.row); + let is_new = row.is_new(); + let row = row.inner(); let mut prop_mut_entry = segment_container.properties_mut().get_mut_entry(row); prop_mut_entry.append_const_props(props); From d208e8285950e8996b51e51f4929952821e93778 Mon Sep 17 00:00:00 2001 From: ljeub-pometry <97447091+ljeub-pometry@users.noreply.github.com> Date: Wed, 17 Sep 2025 11:35:59 +0200 Subject: [PATCH 155/321] No polars in parquet loaders (#2282) * Fix/top k (#2228) * add linear top_k implementation * update top_k to use new linear top_k implementation * cleanup * chore: apply tidy-public auto-fixes --------- Co-authored-by: github-actions[bot] * Fix/fastrp (#2229) * update fastrp to properly average embeddings instead of just summing * fix import issue * add helper function to test pairwise distances * cleanup and update python test * chore: apply tidy-public auto-fixes --------- Co-authored-by: github-actions[bot] * fix docker ci (#2227) * sort ci action and create new Dockerfile for python * add missing dockerfile * remove env from docker ci action * fix python dockerfile * chore: apply tidy-public auto-fixes * add action input to be able to build the python image * add permissions * read base input * fix typo * change default branch to master * chore: apply tidy-public auto-fixes * fix using wrong username on merge step * change manual docker release to also build python * fix digest name conflicts * change choice to boolean for action input * fix digest name using old variable * sort versioning and add nightly action * add permissions to nightly action * add permissions to manual docker release action * read dry-run from the docker action * add dry_run to docker action --------- Co-authored-by: github-actions[bot] * graphql bench on CI and vector bench (#2198) * rewrite the vector bench script * try disabling available memory setting * try arroy append api * add graphql benchmarks * chore: apply tidy-public auto-fixes * add parallel workflow for graphql bench * chore: apply tidy-public auto-fixes * setup python * setup k6 * chore: apply tidy-public auto-fixes * get output file out of the results folder * add pnpm-workspace.yaml * fix ci error * chore: apply tidy-public auto-fixes * fix ci for good this time? * test gh pages branch * chore: apply tidy-public auto-fixes * test different dir for graphql * reduce target * add -100 for testing * chore: apply tidy-public auto-fixes * add TODO * chore: apply tidy-public auto-fixes * cleanup PR * re-enable base benches * fix flaky bench * remove -100 * remove cell from results.ipynb * sort parent workflow * add missing bench.ts * chore: apply tidy-public auto-fixes * some final bits * print json output on ci to make sure numbers are valid * fix ci * set iteration target to 6400 --------- Co-authored-by: github-actions[bot] * James/graphql docstrings (#2210) * init * docstrings * docstrings for edges * docstrings for edges * regen schema and docs * run formatting * chore: apply tidy-public auto-fixes * more docstrings * backticks are not used by docs parser so remove * more docstrings * chore: apply tidy-public auto-fixes * more docstrings * chore: apply tidy-public auto-fixes * more docstrings * more docstrings * update schema and format * chore: apply tidy-public auto-fixes * more docstrings * chore: apply tidy-public auto-fixes * more docstrings * chore: apply tidy-public auto-fixes * more docstrings * chore: apply tidy-public auto-fixes * more docstrings * more docstrings * cleanup * more docstrings * chore: apply tidy-public auto-fixes * testcase for inputs * cleanup * chore: apply tidy-public auto-fixes * more docstrings * cleanup * chore: apply tidy-public auto-fixes * more docstrings * cleanup * chore: apply tidy-public auto-fixes * fix page docstrings * cleanup * remove latin * chore: apply tidy-public auto-fixes * initial fixes * fmt * chore: apply tidy-public auto-fixes --------- Co-authored-by: github-actions[bot] Co-authored-by: Ben Steer * Release v0.16.1 (#2236) chore: Release Co-authored-by: Pometry-Team * Fix explode layers for filtered persistent graph (#2241) * explode layers for valid graph is broken * explode_layers was ignoring layer filters for persistent semantics * chore: apply tidy-public auto-fixes --------- Co-authored-by: github-actions[bot] * James/graphql docstrings fixes (#2239) * init * docstrings * docstrings for edges * docstrings for edges * regen schema and docs * run formatting * chore: apply tidy-public auto-fixes * more docstrings * backticks are not used by docs parser so remove * more docstrings * chore: apply tidy-public auto-fixes * more docstrings * chore: apply tidy-public auto-fixes * more docstrings * more docstrings * update schema and format * chore: apply tidy-public auto-fixes * more docstrings * chore: apply tidy-public auto-fixes * more docstrings * chore: apply tidy-public auto-fixes * more docstrings * chore: apply tidy-public auto-fixes * more docstrings * more docstrings * cleanup * more docstrings * chore: apply tidy-public auto-fixes * testcase for inputs * cleanup * chore: apply tidy-public auto-fixes * more docstrings * cleanup * chore: apply tidy-public auto-fixes * more docstrings * cleanup * chore: apply tidy-public auto-fixes * fix page docstrings * cleanup * remove latin * chore: apply tidy-public auto-fixes * initial fixes * fmt * chore: apply tidy-public auto-fixes * fix double spacing * review fixes * specify layers for has_edge * tidy * chore: apply tidy-public auto-fixes --------- Co-authored-by: github-actions[bot] Co-authored-by: Ben Steer Co-authored-by: Ben Steer * James/graphql-userguide-16-x (#2233) * update ui image * mutation and views * persistent and event distinction * clean up running steps and add cli * subtitle * proper hierarchy * props and metadata examples * Add troubleshooting * add missing cli parameter * chore: apply tidy-public auto-fixes * add default save location to troubleshooting * markdown formatting * chore: apply tidy-public auto-fixes * chore: apply tidy-public auto-fixes * Clarify docker basics * Clarify docker basics --------- Co-authored-by: github-actions[bot] Co-authored-by: Ben Steer * fix nightly release action (#2244) * fix nightly release action * fix versions missing * chore: apply tidy-public auto-fixes --------- Co-authored-by: github-actions[bot] * add docker retag action (#2245) * add docker retag action * add permissions * chore: apply tidy-public auto-fixes --------- Co-authored-by: github-actions[bot] * update Slack invite link (#2252) * test strings in df loaders * Increase sleep time on graphql bench (#2278) Update Makefile * remove polars from parquet and df loading * Bump tracing-subscriber from 0.3.19 to 0.3.20 in the cargo group across 1 directory (#2251) * Bump tracing-subscriber in the cargo group across 1 directory Bumps the cargo group with 1 update in the / directory: [tracing-subscriber](https://github.com/tokio-rs/tracing). Updates `tracing-subscriber` from 0.3.19 to 0.3.20 - [Release notes](https://github.com/tokio-rs/tracing/releases) - [Commits](https://github.com/tokio-rs/tracing/compare/tracing-subscriber-0.3.19...tracing-subscriber-0.3.20) --- updated-dependencies: - dependency-name: tracing-subscriber dependency-version: 0.3.20 dependency-type: direct:production dependency-group: cargo ... Signed-off-by: dependabot[bot] * chore: apply tidy-public auto-fixes --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] Co-authored-by: Ben Steer * community detection (#2276) Covers changes for ticket #2268 and adds an introduction to community detection with a tutorial using our existing algorithms and UI. This should apear first in the search results. * initial introduction * add karate example * add karate example * add assets and tests * fix tests * fix tests * fix tests * chore: apply tidy-public auto-fixes * review: simplify node type assignment * chore: apply tidy-public auto-fixes * review: swap to csv file --------- Co-authored-by: github-actions[bot] * Use raphtory from python dir (#2275) * Use raphtory from python dir * comment to match * chore: apply tidy-public auto-fixes --------- Co-authored-by: github-actions[bot] * add batch_size argument for the low-level functions --------- Signed-off-by: dependabot[bot] Co-authored-by: wyatt-joyner-pometry Co-authored-by: github-actions[bot] Co-authored-by: Pedro Rico Pinazo Co-authored-by: James Baross Co-authored-by: Ben Steer Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Ben Steer Co-authored-by: edsherrington Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .dockerignore | 5 +- .github/workflows/_release_docker.yml | 163 +- .github/workflows/bench-graphql.yml | 88 + .github/workflows/benchmark.yml | 15 +- .github/workflows/manual_release_docker.yml | 28 +- .github/workflows/manual_retag_docker.yml | 40 + .github/workflows/nightly_release.yml | 53 + .github/workflows/release_auto.yml | 42 +- .github/workflows/release_bump_versions.yml | 1 - .github/workflows/test.yml | 4 + .github/workflows/test_during_pr.yml | 6 +- .github/workflows/test_python_workflow.yml | 4 +- .gitignore | 1 + Cargo.lock | 389 +-- Cargo.toml | 26 +- Dockerfile | 9 +- README.md | 2 +- db4-storage/src/pages/mod.rs | 2 +- docker/.dockerignore | 8 - docker/base/Dockerfile | 29 - docker/dockerfile | 27 - docker/server.py | 75 - .../images/raphtory_com_detection_ui.png | Bin 0 -> 585029 bytes .../images/raphtory_ui_playground_docs.png | Bin 0 -> 577774 bytes docs/data/karate.csv | 79 + docs/data/karate.gml | 530 +++ docs/reference/graphql/graphql_API.md | 3010 +++++++++++++---- .../algorithms/4_view-algorithms.md | 4 +- .../algorithms/5_community_detection.md | 123 + docs/user-guide/export/2_dataframes.md | 2 +- docs/user-guide/getting-started/1_intro.md | 3 + docs/user-guide/getting-started/3_cli.md | 1 + docs/user-guide/graphql/1_intro.md | 2 +- docs/user-guide/graphql/2_run-server.md | 33 +- docs/user-guide/graphql/3_writing-queries.md | 226 +- docs/user-guide/graphql/4_running-ui.md | 17 - docs/user-guide/ingestion/2_direct-updates.md | 2 +- docs/user-guide/installation.md | 31 +- docs/user-guide/troubleshooting.md | 11 + docs/user-guide/views/1_intro.md | 4 +- docs/user-guide/views/2_time.md | 9 +- examples/netflow/Cargo.toml | 4 +- examples/netflow/src/lib.rs | 12 +- examples/netflow/src/netflow_one_path_node.rs | 4 +- graphql-bench/.gitignore | 21 + graphql-bench/Makefile | 108 + graphql-bench/data/apache/master/.raph | 1 + graphql-bench/data/apache/master/graph.tar.xz | Bin 0 -> 12263628 bytes graphql-bench/package.json | 29 + graphql-bench/pnpm-lock.yaml | 2880 ++++++++++++++++ graphql-bench/pnpm-workspace.yaml | 2 + graphql-bench/process-k6-output.py | 42 + graphql-bench/results.ipynb | 566 ++++ graphql-bench/server.py | 4 + graphql-bench/src/bench.ts | 302 ++ graphql-bench/tsconfig.json | 15 + graphql-bench/tsup.config.ts | 7 + mkdocs.yml | 5 +- python.Dockerfile | 23 + python/Cargo.toml | 5 +- python/python/raphtory/__init__.pyi | 5 +- python/python/raphtory/graphql/__init__.pyi | 402 +-- python/src/lib.rs | 2 +- .../test_graphdb/test_algorithms.py | 175 +- .../test_loaders/test_load_from_pandas.py | 8 +- raphtory-benchmark/Cargo.toml | 6 +- raphtory-benchmark/bin/vectorise.rs | 35 +- raphtory-benchmark/src/common/vectors.rs | 12 +- raphtory-core/Cargo.toml | 4 +- raphtory-graphql/Cargo.toml | 6 +- raphtory-graphql/schema.graphql | 1612 ++++++++- .../src/model/graph/collection.rs | 11 +- raphtory-graphql/src/model/graph/document.rs | 7 + raphtory-graphql/src/model/graph/edge.rs | 71 + raphtory-graphql/src/model/graph/edges.rs | 46 +- raphtory-graphql/src/model/graph/filtering.rs | 144 + raphtory-graphql/src/model/graph/graph.rs | 60 +- raphtory-graphql/src/model/graph/index.rs | 13 + .../src/model/graph/meta_graph.rs | 14 + raphtory-graphql/src/model/graph/mod.rs | 4 + .../src/model/graph/mutable_graph.rs | 83 +- .../src/model/graph/namespaced_item.rs | 2 + raphtory-graphql/src/model/graph/node.rs | 53 +- raphtory-graphql/src/model/graph/nodes.rs | 32 +- .../src/model/graph/path_from_node.rs | 32 +- raphtory-graphql/src/model/graph/property.rs | 23 + .../src/model/graph/vector_selection.rs | 17 + .../src/model/graph/vectorised_graph.rs | 6 + raphtory-graphql/src/model/graph/windowset.rs | 55 +- raphtory-graphql/src/model/mod.rs | 55 +- .../src/model/plugins/algorithms.rs | 4 + .../src/model/schema/edge_schema.rs | 2 +- raphtory-graphql/src/model/sorting.rs | 11 + .../src/observability/open_telemetry.rs | 2 +- raphtory-graphql/src/python/client/mod.rs | 44 +- .../src/python/client/raphtory_client.rs | 80 +- .../src/python/client/remote_edge.rs | 26 +- .../src/python/client/remote_graph.rs | 74 +- .../src/python/client/remote_node.rs | 18 +- raphtory-graphql/src/python/mod.rs | 10 +- .../src/python/server/running_server.rs | 4 +- raphtory-graphql/src/python/server/server.rs | 54 +- raphtory-graphql/src/server.rs | 14 +- raphtory-storage/Cargo.toml | 6 +- raphtory/Cargo.toml | 51 +- raphtory/examples/load_graph.rs | 1 + raphtory/src/algorithms/embeddings/fast_rp.rs | 233 +- raphtory/src/db/api/state/node_state_ops.rs | 4 +- .../src/db/api/state/node_state_ord_ops.rs | 72 +- raphtory/src/errors.rs | 25 +- raphtory/src/io/arrow/dataframe.rs | 58 +- raphtory/src/io/arrow/df_loaders.rs | 280 +- raphtory/src/io/arrow/layer_col.rs | 54 +- raphtory/src/io/arrow/mod.rs | 39 +- raphtory/src/io/arrow/node_col.rs | 194 +- raphtory/src/io/arrow/prop_handler.rs | 604 ++-- raphtory/src/io/parquet_loaders.rs | 117 +- raphtory/src/lib.rs | 17 + raphtory/src/python/graph/graph.rs | 4 + .../src/python/graph/graph_with_deletions.rs | 5 + raphtory/src/python/graph/io/mod.rs | 3 - .../src/python/graph/io/pandas_loaders.rs | 30 +- raphtory/src/serialise/parquet/mod.rs | 18 +- raphtory/src/vectors/db.rs | 6 +- 124 files changed, 11311 insertions(+), 2982 deletions(-) create mode 100644 .github/workflows/bench-graphql.yml create mode 100644 .github/workflows/manual_retag_docker.yml create mode 100644 .github/workflows/nightly_release.yml delete mode 100644 docker/.dockerignore delete mode 100644 docker/base/Dockerfile delete mode 100644 docker/dockerfile delete mode 100644 docker/server.py create mode 100644 docs/assets/images/raphtory_com_detection_ui.png create mode 100644 docs/assets/images/raphtory_ui_playground_docs.png create mode 100644 docs/data/karate.csv create mode 100644 docs/data/karate.gml create mode 100644 docs/user-guide/algorithms/5_community_detection.md delete mode 100644 docs/user-guide/graphql/4_running-ui.md create mode 100644 docs/user-guide/troubleshooting.md create mode 100644 graphql-bench/.gitignore create mode 100644 graphql-bench/Makefile create mode 100644 graphql-bench/data/apache/master/.raph create mode 100644 graphql-bench/data/apache/master/graph.tar.xz create mode 100644 graphql-bench/package.json create mode 100644 graphql-bench/pnpm-lock.yaml create mode 100644 graphql-bench/pnpm-workspace.yaml create mode 100644 graphql-bench/process-k6-output.py create mode 100644 graphql-bench/results.ipynb create mode 100644 graphql-bench/server.py create mode 100644 graphql-bench/src/bench.ts create mode 100644 graphql-bench/tsconfig.json create mode 100644 graphql-bench/tsup.config.ts create mode 100644 python.Dockerfile diff --git a/.dockerignore b/.dockerignore index bafe553c20..d1f11364e9 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,4 +5,7 @@ target/ .env .idea/ .vscode/ -Dockerfile* +graphql-bench/ +Dockerfile +python.Dockerfile +__pycache__ diff --git a/.github/workflows/_release_docker.yml b/.github/workflows/_release_docker.yml index 61eb9ed97d..b8d2537dc8 100644 --- a/.github/workflows/_release_docker.yml +++ b/.github/workflows/_release_docker.yml @@ -1,84 +1,133 @@ name: _Release 5 - Publish Docker Images to Docker Hub on: workflow_call: - workflow_dispatch: + inputs: + base: + type: 'string' + dry_run: + type: boolean + default: false + tag: + type: string + required: true + python: + type: boolean + description: Wether to build the python version of the image + default: false + permissions: contents: read - packages: write + +env: + REGISTRY_IMAGE: pometry/raphtory jobs: - publish-docker: - name: Build and Publish Docker Images + build: runs-on: ubuntu-latest - strategy: matrix: - platform: [amd64, arm64] - + platform: + - linux/amd64 + - linux/arm64 steps: - - name: Check out the code - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.base }} + + - name: Read rust version + run: echo "RUST_VERSION=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].rust_version')" >> $GITHUB_ENV - - name: Extract Package and Rust Versions - id: version_extraction + - name: Write dockerfile path run: | - PACKAGE_VERSION=$(grep -m 1 '^version' Cargo.toml | sed 's/version = "\(.*\)"/\1/') - RUST_VERSION=$(grep -m 1 '^rust-version' Cargo.toml | sed 's/rust-version = "\(.*\)"/\1/') - echo "PACKAGE_VERSION=$PACKAGE_VERSION" >> $GITHUB_ENV - echo "RUST_VERSION=$RUST_VERSION" >> $GITHUB_ENV - shell: bash + if [ "${{ inputs.python }}" = "true" ]; then + echo "DOCKERFILE_PATH=python.Dockerfile" >> $GITHUB_ENV + else + echo "DOCKERFILE_PATH=Dockerfile" >> $GITHUB_ENV + fi - - name: Deactivate Private Storage + - name: Prepare run: | - chmod +x ./scripts/deactivate_private_storage.py - ./scripts/deactivate_private_storage.py + platform=${{ matrix.platform }} + echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY_IMAGE }} + tags: ${{ inputs.tag }} - - name: Log in to Docker Hub - uses: docker/login-action@v2 + - name: Login to Docker Hub + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Set up QEMU for multi-platform builds - uses: docker/setup-qemu-action@v2 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - # Build and push the base image with Rust version tag for each platform - - name: Build and push raphtory_base Docker image - uses: docker/build-push-action@v4 + - name: Build and push by digest + id: build + uses: docker/build-push-action@v6 with: context: . - file: docker/base/Dockerfile - platforms: linux/amd64,linux/arm64 - push: true - tags: | - ${{ secrets.DOCKERHUB_USERNAME }}/raphtory_base:${{ env.RUST_VERSION }}-${{ matrix.platform }} - - # Build and push Python Docker image with package version and platform-specific tags - - name: Build and push Python Docker image - uses: docker/build-push-action@v4 + build-args: RUST_VERSION=${{ env.RUST_VERSION }} + file: ${{ env.DOCKERFILE_PATH }} + platforms: ${{ matrix.platform }} + labels: ${{ steps.meta.outputs.labels }} + outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true + + - name: Export digest + run: | + mkdir -p /tmp/digests + digest="${{ steps.build.outputs.digest }}" + touch "/tmp/digests/${digest#sha256:}" + + - name: Upload digest + uses: actions/upload-artifact@v4 with: - context: . - file: docker/dockerfile - platforms: linux/amd64,linux/arm64 - push: true - build-args: | - BASE_IMAGE=${{ secrets.DOCKERHUB_USERNAME }}/raphtory_base:${{ env.RUST_VERSION }}-${{ matrix.platform }} - tags: | - ${{ secrets.DOCKERHUB_USERNAME }}/raphtory:${{ env.PACKAGE_VERSION }}-python-${{ matrix.platform }} - - # Build and push Rust Docker image with package version and platform-specific tags - - name: Build and push Rust Docker image - uses: docker/build-push-action@v4 + name: digests-${{ inputs.python }}-${{ env.PLATFORM_PAIR }} + path: /tmp/digests/* + if-no-files-found: error + retention-days: 1 + + merge: + runs-on: ubuntu-latest + needs: + - build + steps: + - name: Download digests + uses: actions/download-artifact@v4 with: - context: . - file: Dockerfile - platforms: linux/amd64,linux/arm64 - push: true - build-args: | - BASE_IMAGE=${{ secrets.DOCKERHUB_USERNAME }}/raphtory_base:${{ env.RUST_VERSION }}-${{ matrix.platform }} - tags: | - ${{ secrets.DOCKERHUB_USERNAME }}/raphtory:${{ env.PACKAGE_VERSION }}-rust-${{ matrix.platform }} \ No newline at end of file + path: /tmp/digests + pattern: digests-${{ inputs.python }}-* + merge-multiple: true + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY_IMAGE }} + tags: ${{ inputs.tag }} + + - name: Create manifest list and push + if: ${{ !inputs.dry_run }} + working-directory: /tmp/digests + run: | + docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *) + + - name: Inspect image + if: ${{ !inputs.dry_run }} + run: | + docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }} diff --git a/.github/workflows/bench-graphql.yml b/.github/workflows/bench-graphql.yml new file mode 100644 index 0000000000..1af46203dd --- /dev/null +++ b/.github/workflows/bench-graphql.yml @@ -0,0 +1,88 @@ +name: Rust Benchmarks +permissions: + # submit PR comment + pull-requests: write + # deployments permission to deploy GitHub pages website + deployments: write + # contents permission to update benchmark contents in gh-pages branch + contents: write +on: + workflow_call: + inputs: + skip_tests: + type: boolean + default: false + required: false + +jobs: + benchmark: + if: ${{ !inputs.skip_tests }} + name: GraphQL Benchmark + runs-on: '${{ matrix.os }}' + strategy: + matrix: + include: + - os: ubuntu-latest + steps: + - uses: actions/checkout@v3 + name: Checkout + - uses: ./.github/actions/setup_rust + name: Setup Rust + - name: Install Protoc + uses: arduino/setup-protoc@v3 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + - name: Cargo cache + uses: Swatinem/rust-cache@v2 + with: + cache-all-crates: true + - name: Setup Python ${{ matrix.python }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python }} + cache: 'pip' + - name: Install maturin + run: pip install maturin==1.8.3 + - name: Build raphtory + run: make install-python + - name: Set up pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + - uses: grafana/setup-k6-action@v1 + with: + k6-version: '1.0.0' + - name: Run GraphQL benchmarks + run: cd graphql-bench && make bench-local + - name: Restore metadata file + run: git restore graphql-bench/data/apache/master/.raph # otherwise github-action-benchmark fails to create the commit + - name: Print bench results + run: cat graphql-bench/output.json + - name: Store benchmark results from master branch + if: github.ref == 'refs/heads/master' + uses: benchmark-action/github-action-benchmark@v1 + with: + name: GraphQL Benchmark + tool: 'customBiggerIsBetter' + output-file-path: graphql-bench/output.json + github-token: ${{ secrets.GITHUB_TOKEN }} + alert-threshold: '200%' + comment-on-alert: true + summary-always: true + fail-on-alert: false + auto-push: true + benchmark-data-dir-path: dev/bench-graphql + - name: Compare benchmark results to master + if: github.ref != 'refs/heads/master' + uses: benchmark-action/github-action-benchmark@v1 + with: + name: GraphQL Benchmark + tool: 'customBiggerIsBetter' + output-file-path: graphql-bench/output.json + github-token: ${{ secrets.GITHUB_TOKEN }} + alert-threshold: '200%' + comment-on-alert: true + summary-always: true + fail-on-alert: false + save-data-file: false + benchmark-data-dir-path: dev/bench-graphql diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 2e26e72d5e..01421a1dfa 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -47,9 +47,8 @@ jobs: set -o pipefail cargo bench --bench base --bench algobench -p raphtory-benchmark -- --output-format=bencher | tee benchmark-result.txt - name: Delete cargo.lock if it exists - run: | - rm -f Cargo.lock - - name: Store benchmark result if repo is master + run: rm -f Cargo.lock + - name: Store benchmark results from master branch if: github.ref == 'refs/heads/master' uses: benchmark-action/github-action-benchmark@v1 with: @@ -57,23 +56,20 @@ jobs: tool: 'cargo' output-file-path: benchmark-result.txt github-token: ${{ secrets.GITHUB_TOKEN }} - auto-push: true - # Show alert with commit comment on detecting possible performance regression alert-threshold: '200%' comment-on-alert: true summary-always: true fail-on-alert: false - - name: Compare benchmark results if repo is not master + auto-push: true + - name: Compare benchmark results to master if: github.ref != 'refs/heads/master' uses: benchmark-action/github-action-benchmark@v1 with: name: Rust Benchmark tool: 'cargo' output-file-path: benchmark-result.txt - auto-push: false - # Show alert with commit comment on detecting possible performance regression - alert-threshold: '200%' github-token: ${{ secrets.GITHUB_TOKEN }} + alert-threshold: '200%' comment-on-alert: true summary-always: true fail-on-alert: false @@ -81,4 +77,3 @@ jobs: # TODO # ON PR DO NOT UPLOAD # IF MASTER THEN UPLOAD - diff --git a/.github/workflows/manual_release_docker.yml b/.github/workflows/manual_release_docker.yml index abdd1dfe84..aa23972fd5 100644 --- a/.github/workflows/manual_release_docker.yml +++ b/.github/workflows/manual_release_docker.yml @@ -1,20 +1,40 @@ name: (Manual) Release Docker Hub + +permissions: + contents: read + on: workflow_dispatch: inputs: base: description: 'Name of branch to open PR against' type: 'string' - default: 'main' + default: 'master' + tag: + type: string + description: The tag to use for the docker image (Note that the python version will have the '-python' suffix) + required: true dry_run: description: 'DRY RUN: If true, will not publish the release to PyPI/crates/Docker Hub but will release to GitHub' type: boolean default: false jobs: - call-release-docker-workflow: - name: _Release 5 - Publish Docker Images to Docker Hub + release-rust-docker: + name: Release rust docker image uses: ./.github/workflows/_release_docker.yml + secrets: inherit with: - version: ${{ inputs.base }} + base: ${{ inputs.base }} + dry_run: ${{ inputs.dry_run == true }} + tag: ${{ inputs.tag }} + python: false + release-python-docker: + name: Release python docker image + uses: ./.github/workflows/_release_docker.yml secrets: inherit + with: + base: ${{ inputs.base }} + dry_run: ${{ inputs.dry_run == true }} + tag: ${{ inputs.tag }}-python + python: true diff --git a/.github/workflows/manual_retag_docker.yml b/.github/workflows/manual_retag_docker.yml new file mode 100644 index 0000000000..c632e0a7d9 --- /dev/null +++ b/.github/workflows/manual_retag_docker.yml @@ -0,0 +1,40 @@ +name: Retag Docker Image + +permissions: + contents: read + +on: + workflow_dispatch: + inputs: + source_tag: + description: "The existing image tag to retag" + required: true + new_tag: + description: "The new tag to apply" + required: true + +env: + REGISTRY_IMAGE: pometry/raphtory + +jobs: + retag: + runs-on: ubuntu-latest + steps: + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Retag multi-platform image + run: | + docker buildx imagetools create \ + -t ${{ env.REGISTRY_IMAGE }}:${{ github.event.inputs.new_tag }} \ + ${{ env.REGISTRY_IMAGE }}:${{ github.event.inputs.source_tag }} + + - name: Inspect image + run: | + docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ github.event.inputs.new_tag }} diff --git a/.github/workflows/nightly_release.yml b/.github/workflows/nightly_release.yml new file mode 100644 index 0000000000..0fa4c874ef --- /dev/null +++ b/.github/workflows/nightly_release.yml @@ -0,0 +1,53 @@ +name: Nightly release (docker only) + +permissions: + contents: read + +on: + schedule: + # Run every day at 02:00 UTC (including weekends) + - cron: '0 2 * * *' + +jobs: + check_date: + runs-on: ubuntu-latest + name: Check latest commit + outputs: + should_run: ${{ steps.should_run.outputs.should_run }} + steps: + - uses: actions/checkout@v4 + - id: should_run + continue-on-error: true + name: Check latest commit is less than a day + run: test -z $(git rev-list --after="24 hours" ${{ github.sha }}) && echo "::set-output name=should_run::false" + get_date: + runs-on: ubuntu-latest + outputs: + date: ${{ steps.date.outputs.date }} + steps: + - id: date + run: echo "date=$(date +%Y-%m-%d)" >> $GITHUB_OUTPUT + release-rust-docker: + name: Release rust docker image + needs: [check_date, get_date] + if: ${{ needs.check_date.outputs.should_run != 'false' }} + uses: ./.github/workflows/_release_docker.yml + secrets: inherit + with: + base: ${{ inputs.base }} + tag: | + nightly-${{ needs.get_date.outputs.date }} + nightly + python: false + release-python-docker: + name: Release python docker image + needs: [check_date, get_date] + if: ${{ needs.check_date.outputs.should_run != 'false' }} + uses: ./.github/workflows/_release_docker.yml + secrets: inherit + with: + base: ${{ inputs.base }} + tag: | + nightly-${{ needs.get_date.outputs.date }}-python + nightly-python + python: true diff --git a/.github/workflows/release_auto.yml b/.github/workflows/release_auto.yml index 232f792812..507f5ec62d 100644 --- a/.github/workflows/release_auto.yml +++ b/.github/workflows/release_auto.yml @@ -7,11 +7,21 @@ on: type: 'string' default: 'master' dry_run: - description: 'DRY RUN: If true will not publish the release to pypi/crates but will release to github' + description: 'DRY RUN: If true will not publish the release to pypi/crates/dockerhub but will release to github' type: boolean default: false jobs: + read-raphtory-version: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.version }} + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.base }} + - id: version + run: echo "version=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version')" >> $GITHUB_OUTPUT call-release-rust-workflow: name: _Release 2 - Publish Rust package to crates.io uses: ./.github/workflows/_release_rust.yml @@ -32,9 +42,27 @@ jobs: with: base: ${{ inputs.base }} secrets: inherit - # call-release-docker-workflow: - # name: _Release 5 - Publish Docker Images to Docker Hub - # uses: ./.github/workflows/_release_docker.yml - # with: - # version: ${{ inputs.base }} - # secrets: inherit + call-release-docker-workflow-rust: + name: Release rust docker image + needs: read-raphtory-version + uses: ./.github/workflows/_release_docker.yml + with: + base: ${{ inputs.base }} + dry_run: ${{ inputs.dry_run == true }} + tag: | + ${{ needs.read-raphtory-version.outputs.version }} + latest + python: false + secrets: inherit + call-release-docker-workflow-python: + name: Release python docker image + needs: read-raphtory-version + uses: ./.github/workflows/_release_docker.yml + with: + base: ${{ inputs.base }} + dry_run: ${{ inputs.dry_run == true }} + tag: | + ${{ needs.read-raphtory-version.outputs.version }}-python + latest-python + python: true + secrets: inherit diff --git a/.github/workflows/release_bump_versions.yml b/.github/workflows/release_bump_versions.yml index ed457b6a7e..ced34d8c19 100644 --- a/.github/workflows/release_bump_versions.yml +++ b/.github/workflows/release_bump_versions.yml @@ -82,4 +82,3 @@ jobs: - [x] Publish to Github as release - Auto-generated by [create-pull-request] triggered by release action [1] [1]: https://github.com/peter-evans/create-pull-request - diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 81f8d073f9..75996aac6b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -37,6 +37,10 @@ jobs: name: Run benchmarks uses: ./.github/workflows/benchmark.yml secrets: inherit + call-graphql-bench-workflow-in-local-repo: + name: Run benchmarks + uses: ./.github/workflows/bench-graphql.yml + secrets: inherit # call-code-coverage: # name: Code Coverage # uses: ./.github/workflows/code_coverage.yml diff --git a/.github/workflows/test_during_pr.yml b/.github/workflows/test_during_pr.yml index 00da2c89cd..51bf93b6f3 100644 --- a/.github/workflows/test_during_pr.yml +++ b/.github/workflows/test_during_pr.yml @@ -41,8 +41,12 @@ jobs: uses: ./.github/workflows/benchmark.yml secrets: inherit needs: rust-format-check + call-graphql-bench-workflow-in-local-repo: + name: Run benchmarks + uses: ./.github/workflows/bench-graphql.yml + secrets: inherit + needs: rust-format-check # call-code-coverage: # name: Code Coverage # uses: ./.github/workflows/code_coverage.yml # needs: rust-format-check - diff --git a/.github/workflows/test_python_workflow.yml b/.github/workflows/test_python_workflow.yml index 3b0d630628..567cf5acc7 100644 --- a/.github/workflows/test_python_workflow.yml +++ b/.github/workflows/test_python_workflow.yml @@ -89,8 +89,8 @@ jobs: - name: Run stubsgen if: matrix.os == 'ubuntu-latest' && matrix.python == '3.13' run: | - echo "Installing Raphtory from cache..." - pip install raphtory + echo "Installing Raphtory from ./python" + pip install -e ./python echo "Installing stubsgen" python -m pip install -e stub_gen cd python/scripts && python gen-stubs.py diff --git a/.gitignore b/.gitignore index 396a7f85b1..0a0a442ff0 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ comparison-benchmark/python/data/* .DS_Store .python-version .ipynb_checkpoints +.virtual_documents _autosummary examples/docker/lotr/lotr examples/python/enron/emails.csv diff --git a/Cargo.lock b/Cargo.lock index 1da76be99c..d934581ec6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -181,12 +181,6 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" -[[package]] -name = "array-init-cursor" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed51fe0f224d1d4ea768be38c51f9f831dee9d05c163c11fba0b8c44387b1fc3" - [[package]] name = "arraydeque" version = "0.5.1" @@ -449,7 +443,7 @@ dependencies = [ "memchr", "num", "regex", - "regex-syntax 0.8.5", + "regex-syntax", ] [[package]] @@ -462,7 +456,7 @@ dependencies = [ "byteorder", "enum-iterator", "heed", - "memmap2 0.9.5", + "memmap2", "nohash", "ordered-float 4.6.0", "page_size", @@ -486,7 +480,7 @@ version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06575e6a9673580f52661c92107baabffbf41e2141373441cbcdc47cb733003c" dependencies = [ - "brotli 7.0.0", + "brotli", "bzip2 0.5.2", "flate2", "futures-core", @@ -679,12 +673,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "atoi_simd" -version = "0.15.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae037714f313c1353189ead58ef9eec30a8e8dc101b2622d461418fd59e28a9" - [[package]] name = "atomic-waker" version = "1.1.2" @@ -906,17 +894,6 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36f64beae40a84da1b4b26ff2761a5b895c12adc41dc25aaee1c4f2bbfe97a6e" -[[package]] -name = "brotli" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - [[package]] name = "brotli" version = "7.0.0" @@ -1190,7 +1167,6 @@ version = "7.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a65ebfec4fb190b6f90e944a817d60499ee0744e582530e2c9900a22e591d9a" dependencies = [ - "crossterm", "unicode-segmentation", "unicode-width", ] @@ -1402,28 +1378,6 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" -[[package]] -name = "crossterm" -version = "0.28.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" -dependencies = [ - "bitflags 2.9.0", - "crossterm_winapi", - "parking_lot", - "rustix 0.38.44", - "winapi", -] - -[[package]] -name = "crossterm_winapi" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" -dependencies = [ - "winapi", -] - [[package]] name = "crunchy" version = "0.2.3" @@ -1809,7 +1763,7 @@ dependencies = [ "itertools 0.13.0", "log", "paste", - "regex-syntax 0.8.5", + "regex-syntax", ] [[package]] @@ -1925,7 +1879,7 @@ dependencies = [ [[package]] name = "db4-graph" -version = "0.16.0" +version = "0.16.1" dependencies = [ "boxcar", "db4-storage", @@ -1938,7 +1892,7 @@ dependencies = [ [[package]] name = "db4-storage" -version = "0.16.0" +version = "0.16.1" dependencies = [ "arrow", "arrow-array", @@ -2289,18 +2243,6 @@ dependencies = [ "rand 0.8.5", ] -[[package]] -name = "fallible-streaming-iterator" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" - -[[package]] -name = "fast-float" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95765f67b4b18863968b4a1bd5bb576f732b29a4a28c7cd84c09fa3e2875f33c" - [[package]] name = "fast_chemail" version = "0.9.6" @@ -2737,15 +2679,6 @@ dependencies = [ "digest", ] -[[package]] -name = "home" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" -dependencies = [ - "windows-sys 0.59.0", -] - [[package]] name = "htmlescape" version = "0.3.1" @@ -3296,9 +3229,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.172" +version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "libm" @@ -3380,25 +3313,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" -[[package]] -name = "lz4" -version = "1.28.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a20b523e860d03443e98350ceaac5e71c6ba89aea7d960769ec3ce37f4de5af4" -dependencies = [ - "lz4-sys", -] - -[[package]] -name = "lz4-sys" -version = "1.11.1+lz4-1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bd8c0d6c6ed0cd30b3652886bb8711dc4bb01d637a68105a3d5158039b418e6" -dependencies = [ - "cc", - "libc", -] - [[package]] name = "lz4_flex" version = "0.11.3" @@ -3431,11 +3345,11 @@ dependencies = [ [[package]] name = "matchers" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ - "regex-automata 0.1.10", + "regex-automata", ] [[package]] @@ -3480,15 +3394,6 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" -[[package]] -name = "memmap2" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f49388d20533534cd19360ad3d6a7dadc885944aa802ba3995040c5ec11288c6" -dependencies = [ - "libc", -] - [[package]] name = "memmap2" version = "0.9.5" @@ -3614,28 +3519,6 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" -[[package]] -name = "multiversion" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4851161a11d3ad0bf9402d90ffc3967bf231768bfd7aeb61755ad06dbf1a142" -dependencies = [ - "multiversion-macros", - "target-features", -] - -[[package]] -name = "multiversion-macros" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79a74ddee9e0c27d2578323c13905793e91622148f138ba29738f9dddb835e90" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", - "target-features", -] - [[package]] name = "murmurhash32" version = "0.3.1" @@ -3734,12 +3617,11 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.46.0" +version = "0.50.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" dependencies = [ - "overload", - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -4039,12 +3921,6 @@ dependencies = [ "syn 2.0.101", ] -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "ownedbytes" version = "0.7.0" @@ -4108,7 +3984,7 @@ dependencies = [ "arrow-schema", "arrow-select 53.4.1 (registry+https://github.com/rust-lang/crates.io-index)", "base64 0.22.1", - "brotli 7.0.0", + "brotli", "bytes", "chrono", "flate2", @@ -4129,12 +4005,6 @@ dependencies = [ "zstd-sys", ] -[[package]] -name = "parquet-format-safe" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1131c54b167dd4e4799ce762e1ab01549ebb94d5bdd13e6ec1b467491c378e1f" - [[package]] name = "parse-zoneinfo" version = "0.3.1" @@ -4337,15 +4207,6 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" -[[package]] -name = "planus" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1691dd09e82f428ce8d6310bd6d5da2557c82ff17694d2a32cad7242aea89f" -dependencies = [ - "array-init-cursor", -] - [[package]] name = "plotters" version = "0.3.7" @@ -4429,157 +4290,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32d19c6db79cb6a3c55af3b5a3976276edaab64cbf7f69b392617c2af30d7742" dependencies = [ "ahash", - "atoi_simd", "bytemuck", "chrono", "dyn-clone", "either", "ethnum", - "fast-float", "getrandom 0.2.16", "hashbrown 0.14.5", - "itoa", - "multiversion", "num-traits", "parking_lot", - "polars-arrow-format", "polars-error", "polars-utils", - "ryu", "simdutf8", "streaming-iterator", - "strength_reduce", "version_check", ] -[[package]] -name = "polars-arrow-format" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b0ef2474af9396b19025b189d96e992311e6a47f90c53cd998b36c4c64b84c" -dependencies = [ - "planus", - "serde", -] - -[[package]] -name = "polars-compute" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30194a5ff325f61d6fcb62dc215c9210f308fc4fc85a493ef777dbcd938cba24" -dependencies = [ - "bytemuck", - "either", - "num-traits", - "polars-arrow", - "polars-error", - "polars-utils", - "strength_reduce", - "version_check", -] - -[[package]] -name = "polars-core" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ba2a3b736d55b92a12889672d0197dc25ad321ab23eba4168a3b6316a6b6349" -dependencies = [ - "ahash", - "bitflags 2.9.0", - "bytemuck", - "comfy-table", - "either", - "hashbrown 0.14.5", - "indexmap 2.9.0", - "num-traits", - "once_cell", - "polars-arrow", - "polars-compute", - "polars-error", - "polars-row", - "polars-utils", - "rayon", - "smartstring", - "thiserror 1.0.69", - "version_check", - "xxhash-rust", -] - [[package]] name = "polars-error" version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07101d1803ca2046cdb3a8adb1523ddcc879229860f0ac56a853034269dec1e1" dependencies = [ - "polars-arrow-format", "simdutf8", "thiserror 1.0.69", ] -[[package]] -name = "polars-io" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a48ddf416ae185336c3d7880d2e05b7e55686e3e0da1014e5e7325eff9c7d722" -dependencies = [ - "ahash", - "bytes", - "flate2", - "glob", - "home", - "memchr", - "memmap2 0.7.1", - "num-traits", - "once_cell", - "percent-encoding", - "polars-arrow", - "polars-core", - "polars-error", - "polars-parquet", - "polars-utils", - "rayon", - "regex", - "smartstring", - "zstd", -] - -[[package]] -name = "polars-parquet" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2993265079ffa07dd16277189444424f8d787b00b01c6f6e001f58bab543ce" -dependencies = [ - "ahash", - "base64 0.22.1", - "brotli 6.0.0", - "bytemuck", - "ethnum", - "flate2", - "lz4", - "num-traits", - "parquet-format-safe", - "polars-arrow", - "polars-compute", - "polars-error", - "polars-utils", - "simdutf8", - "snap", - "streaming-decompression", - "zstd", -] - -[[package]] -name = "polars-row" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e11f43f48466c4b1caa6dc61c381dc10c2d67b87fcb74bc996e21c4f7b0a311" -dependencies = [ - "bytemuck", - "polars-arrow", - "polars-error", - "polars-utils", -] - [[package]] name = "polars-utils" version = "0.42.0" @@ -4591,7 +4327,6 @@ dependencies = [ "bytes", "hashbrown 0.14.5", "indexmap 2.9.0", - "memmap2 0.7.1", "num-traits", "once_cell", "polars-error", @@ -4706,7 +4441,7 @@ dependencies = [ "rand 0.8.5", "rand_chacha 0.3.1", "rand_xorshift", - "regex-syntax 0.8.5", + "regex-syntax", "rusty-fork", "tempfile", "unarray", @@ -5057,10 +4792,12 @@ dependencies = [ [[package]] name = "raphtory" -version = "0.16.0" +version = "0.16.1" dependencies = [ "ahash", "arrow-array", + "arrow-buffer", + "arrow-cast 53.4.1 (registry+https://github.com/rust-lang/crates.io-index)", "arrow-json", "arrow-schema", "arroy", @@ -5088,7 +4825,7 @@ dependencies = [ "iter-enum", "itertools 0.13.0", "kdam", - "memmap2 0.9.5", + "memmap2", "minijinja", "minijinja-contrib", "moka", @@ -5102,10 +4839,6 @@ dependencies = [ "ouroboros", "parking_lot", "parquet", - "polars-arrow", - "polars-core", - "polars-io", - "polars-parquet", "pretty_assertions", "proptest", "proptest-derive", @@ -5142,7 +4875,7 @@ dependencies = [ [[package]] name = "raphtory-api" -version = "0.16.0" +version = "0.16.1" dependencies = [ "arrow-array", "arrow-ipc", @@ -5178,7 +4911,7 @@ dependencies = [ [[package]] name = "raphtory-api-macros" -version = "0.16.0" +version = "0.16.1" dependencies = [ "proc-macro2", "quote", @@ -5187,7 +4920,7 @@ dependencies = [ [[package]] name = "raphtory-benchmark" -version = "0.16.0" +version = "0.16.1" dependencies = [ "chrono", "criterion", @@ -5208,7 +4941,7 @@ dependencies = [ [[package]] name = "raphtory-core" -version = "0.16.0" +version = "0.16.1" dependencies = [ "bigdecimal", "chrono", @@ -5233,7 +4966,7 @@ dependencies = [ [[package]] name = "raphtory-cypher" -version = "0.16.0" +version = "0.16.1" dependencies = [ "arrow", "arrow-array", @@ -5263,7 +4996,7 @@ dependencies = [ [[package]] name = "raphtory-graphql" -version = "0.16.0" +version = "0.16.1" dependencies = [ "ahash", "arrow-array", @@ -5311,7 +5044,7 @@ dependencies = [ [[package]] name = "raphtory-pymodule" -version = "0.16.0" +version = "0.16.1" dependencies = [ "numpy", "pyo3", @@ -5322,7 +5055,7 @@ dependencies = [ [[package]] name = "raphtory-storage" -version = "0.16.0" +version = "0.16.1" dependencies = [ "bigdecimal", "db4-graph", @@ -5366,7 +5099,7 @@ dependencies = [ [[package]] name = "raphtory_netflow" -version = "0.16.0" +version = "0.16.1" dependencies = [ "pyo3", "pyo3-build-config", @@ -5426,17 +5159,8 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", + "regex-automata", + "regex-syntax", ] [[package]] @@ -5447,15 +5171,9 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax", ] -[[package]] -name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - [[package]] name = "regex-syntax" version = "0.8.5" @@ -5579,7 +5297,7 @@ dependencies = [ [[package]] name = "rust-examples" -version = "0.16.0" +version = "0.16.1" dependencies = [ "chrono", "itertools 0.13.0", @@ -6121,15 +5839,6 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7beae5182595e9a8b683fa98c4317f956c9a2dec3b9716990d20023cc60c766" -[[package]] -name = "streaming-decompression" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf6cc3b19bfb128a8ad11026086e31d3ce9ad23f8ea37354b31383a187c44cf3" -dependencies = [ - "fallible-streaming-iterator", -] - [[package]] name = "streaming-iterator" version = "0.1.9" @@ -6145,12 +5854,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "strength_reduce" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" - [[package]] name = "strsim" version = "0.11.1" @@ -6238,9 +5941,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.35.1" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79251336d17c72d9762b8b54be4befe38d2db56fbbc0241396d70f173c39d47a" +checksum = "07cec4dc2d2e357ca1e610cfb07de2fa7a10fc3e9fe89f72545f3d244ea87753" dependencies = [ "libc", "memchr", @@ -6281,7 +5984,7 @@ dependencies = [ "lru", "lz4_flex", "measure_time", - "memmap2 0.9.5", + "memmap2", "num_cpus", "once_cell", "oneshot", @@ -6352,7 +6055,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d60769b80ad7953d8a7b2c70cdfe722bbcdcac6bccc8ac934c40c034d866fc18" dependencies = [ "byteorder", - "regex-syntax 0.8.5", + "regex-syntax", "utf8-ranges", ] @@ -6403,12 +6106,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" -[[package]] -name = "target-features" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1bbb9f3c5c463a01705937a24fdabc5047929ac764b2d5b9cf681c1f5041ed5" - [[package]] name = "target-lexicon" version = "0.12.16" @@ -6831,14 +6528,14 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" dependencies = [ "matchers", "nu-ansi-term", "once_cell", - "regex", + "regex-automata", "sharded-slab", "smallvec", "thread_local", @@ -7576,12 +7273,6 @@ dependencies = [ "tap", ] -[[package]] -name = "xxhash-rust" -version = "0.8.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" - [[package]] name = "xz2" version = "0.1.7" diff --git a/Cargo.toml b/Cargo.toml index 8f01ca1839..6d5eceb6ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ default-members = ["raphtory"] resolver = "2" [workspace.package] -version = "0.16.0" +version = "0.16.1" documentation = "https://raphtory.readthedocs.io/en/latest/" repository = "https://github.com/Raphtory/raphtory/" license = "GPL-3.0" @@ -50,13 +50,13 @@ incremental = false [workspace.dependencies] -db4-graph = { version = "0.16.0", path = "db4-graph", default-features = false } -raphtory = { version = "0.16.0", path = "raphtory", default-features = false } -raphtory-api = { version = "0.16.0", path = "raphtory-api", default-features = false } -raphtory-api-macros = { version = "0.16.0", path = "raphtory-api-macros", default-features = false } -raphtory-core = { version = "0.16.0", path = "raphtory-core", default-features = false } -raphtory-graphql = { version = "0.16.0", path = "raphtory-graphql", default-features = false } -raphtory-storage = { version = "0.16.0", path = "raphtory-storage", default-features = false } +db4-graph = { version = "0.16.1", path = "db4-graph", default-features = false } +raphtory = { version = "0.16.1", path = "raphtory", default-features = false } +raphtory-api = { version = "0.16.1", path = "raphtory-api", default-features = false } +raphtory-api-macros = { version = "0.16.1", path = "raphtory-api-macros", default-features = false } +raphtory-core = { version = "0.16.1", path = "raphtory-core", default-features = false } +raphtory-graphql = { version = "0.16.1", path = "raphtory-graphql", default-features = false } +raphtory-storage = { version = "0.16.1", path = "raphtory-storage", default-features = false } async-graphql = { version = "7.0.16", features = ["dynamic-schema"] } bincode = "1.3.3" async-graphql-poem = "7.0.16" @@ -103,7 +103,6 @@ rustc-hash = "2.0.0" twox-hash = "2.1.0" lock_api = { version = "0.4.11", features = ["arc_lock", "serde"] } dashmap = { version = "6.0.1", features = ["serde", "rayon"] } -enum_dispatch = "0.3.12" glam = "0.29.0" quad-rand = "0.2.1" zip = "2.3.0" @@ -115,14 +114,10 @@ async-openai = "0.26.0" num = "0.4.1" display-error-chain = "0.2.0" polars-arrow = "0.42.0" -polars-parquet = "0.42.0" -polars-core = "0.42.0" -polars-io = "0.42.0" bigdecimal = { version = "0.4.7", features = ["serde"] } kdam = "0.6.2" hashbrown = { version = "0.14.5", features = ["raw"] } pretty_assertions = "1.4.0" -quickcheck = "1.0.3" quickcheck_macros = "1.0.0" streaming-stats = "0.2.3" proptest = "1.4.0" @@ -138,7 +133,7 @@ opentelemetry_sdk = { version = "0.27.1", features = ["rt-tokio"] } opentelemetry-otlp = { version = "0.27.0" } tracing = "0.1.37" tracing-opentelemetry = "0.28.0" -tracing-subscriber = { version = "0.3.16", features = ["std", "env-filter"] } +tracing-subscriber = { version = "0.3.20", features = ["std", "env-filter"] } indoc = "2.0.5" walkdir = "2" config = "0.14.0" @@ -161,7 +156,6 @@ minijinja-contrib = { version = "2.2.0", features = ["datetime"] } datafusion = { version = "43.0.0" } arroy = "0.6.1" heed = "0.22.0" -sysinfo = "0.35.1" sqlparser = "0.51.0" futures = "0.3" arrow = { version = "=53.4.1" } @@ -170,6 +164,7 @@ arrow-json = { version = "=53.4.1" } arrow-buffer = { version = "=53.4.1" } arrow-schema = { version = "=53.4.1" } arrow-array = { version = "=53.4.1" } +arrow-cast = { version = "=53.4.1" } arrow-ipc = { version = "=53.4.1" } moka = { version = "0.12.7", features = ["future"] } indexmap = { version = "2.7.0", features = ["rayon"] } @@ -177,6 +172,7 @@ fake = { version = "3.1.0", features = ["chrono"] } strsim = { version = "0.11.1" } uuid = { version = "1.16.0", features = ["v4"] } bitvec = "1.0.1" +sysinfo = "0.37.0" # Make sure that transitive dependencies stick to disk_graph 50 [patch.crates-io] diff --git a/Dockerfile b/Dockerfile index cdca7b954d..0f4bdcf374 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,6 @@ -FROM rust:1.86.0 AS chef +ARG RUST_VERSION=1.86.0 + +FROM rust:${RUST_VERSION} AS chef RUN cargo install cargo-chef --version 0.1.67 WORKDIR /app @@ -9,8 +11,7 @@ RUN sed -i '/members = \[/,/\]/c\members = ["raphtory", "raphtory-graphql"]' Car RUN cargo chef prepare --recipe-path recipe.json FROM chef AS builder -RUN apt-get update -RUN apt-get install -y protobuf-compiler +RUN apt-get update && apt-get install -y protobuf-compiler COPY --from=planner /app/recipe.json recipe.json RUN cargo chef cook --release --recipe-path recipe.json COPY . . @@ -18,6 +19,6 @@ RUN cargo build --release -p raphtory-graphql FROM debian:bookworm-slim COPY --from=builder /app/target/release/raphtory-graphql /raphtory-graphql -WORKDIR /app +WORKDIR /var/lib/raphtory ENTRYPOINT ["/raphtory-graphql"] diff --git a/README.md b/README.md index 66c00d6735..356eb358ef 100644 --- a/README.md +++ b/README.md @@ -230,7 +230,7 @@ Join the growing community of open-source enthusiasts using Raphtory to power th - Follow [![Slack](https://img.shields.io/twitter/follow/raphtory?label=@raphtory)](https://twitter.com/raphtory) for the latest Raphtory news and development -- Join our [![Slack](https://join.slack.com/t/raphtory/shared_invite/zt-38j5i1bib-9BovBVoRTJB71APB_VzHyg) to chat with us and get answers to your questions! +- Join our [Slack](https://join.slack.com/t/raphtory/shared_invite/zt-38j5i1bib-9BovBVoRTJB71APB_VzHyg) to chat with us and get answers to your questions! ### Contributors diff --git a/db4-storage/src/pages/mod.rs b/db4-storage/src/pages/mod.rs index 27fb62d90b..5b0376043b 100644 --- a/db4-storage/src/pages/mod.rs +++ b/db4-storage/src/pages/mod.rs @@ -39,7 +39,7 @@ pub mod locked; pub mod node_page; pub mod node_store; pub mod session; -#[cfg(feature = "test-utils")] +#[cfg(any(test, feature = "test-utils"))] pub mod test_utils; // graph // (node/edges) // segment // layer_ids (0, 1, 2, ...) // actual graphy bits diff --git a/docker/.dockerignore b/docker/.dockerignore deleted file mode 100644 index bafe553c20..0000000000 --- a/docker/.dockerignore +++ /dev/null @@ -1,8 +0,0 @@ -pometry-storage-private -.dockerignore -target/ -.git/ -.env -.idea/ -.vscode/ -Dockerfile* diff --git a/docker/base/Dockerfile b/docker/base/Dockerfile deleted file mode 100644 index 750c82c00b..0000000000 --- a/docker/base/Dockerfile +++ /dev/null @@ -1,29 +0,0 @@ -FROM python:3.12.4-slim - -# Install packages & python base -RUN apt-get update && \ - apt-get install -y --no-install-recommends \ - python3 \ - protobuf-compiler \ - curl \ - g++ \ - git \ - libssl-dev \ - patchelf && \ - apt-get clean && \ - rm -rf /var/lib/apt/lists/* - -# Create a virtual environment -RUN python3 -m venv /opt/venv -ENV VIRTUAL_ENV=/opt/venv -ENV PATH="$VIRTUAL_ENV/bin:$PATH" - -# Install Rustup -RUN curl --proto '=https' --tlsv1.3 https://sh.rustup.rs -sSf | sh -s -- -y -ENV PATH="/root/.cargo/bin:${PATH}" -RUN rustup toolchain install 1.83.0 && rustup default 1.83.0 -RUN cargo install --locked maturin - -WORKDIR /home/raphtory_server - -ENTRYPOINT [ "/bin/sh"] diff --git a/docker/dockerfile b/docker/dockerfile deleted file mode 100644 index 4d40664cc2..0000000000 --- a/docker/dockerfile +++ /dev/null @@ -1,27 +0,0 @@ -#THIS SHOULD BE RUN FROM THE MAIN RAPHTORY DIR VIA MAKE -# Stage 1: Build -ARG BASE_IMAGE -FROM ${BASE_IMAGE} AS build - -WORKDIR /home/raphtory_server - -# Install custom raphtory and then delete the files -COPY . /home/raphtory_server/raphtory -RUN cd raphtory && rm -rf target && rm -rf pometry-storage-private -RUN cd raphtory/python && maturin build -r -RUN cd raphtory && pip install $(ls target/wheels/*.whl | head -n 1) -RUN rm -rf raphtory - -RUN pip install python-dotenv - -# # Stage 2: Final -FROM python:3.12.4-slim - -# Copy the virtual environment from the build stage -COPY --from=build /opt/venv /opt/venv -ENV VIRTUAL_ENV=/opt/venv -ENV PATH="$VIRTUAL_ENV/bin:$PATH" - -COPY docker/server.py /home/raphtory_server/server.py - -ENTRYPOINT ["python", "/home/raphtory_server/server.py"] diff --git a/docker/server.py b/docker/server.py deleted file mode 100644 index 303d3230fd..0000000000 --- a/docker/server.py +++ /dev/null @@ -1,75 +0,0 @@ -from raphtory import graphql -from dotenv import load_dotenv -import argparse - -# Load the .env file -load_dotenv() - -parser = argparse.ArgumentParser(description="For passing the working_dir") -parser.add_argument( - "--working-dir", - type=str, - default="graphs", - help="Path for the working directory of the raphtory server, defaults to 'graphs/'", -) -parser.add_argument( - "--port", - type=int, - default=1736, - help="Graphql server port, defaults to 1736", -) -parser.add_argument( - "--log-level", - type=str, - default="info", - help="Log level for the server, defaults to info", -) -parser.add_argument( - "--tracing", - type=bool, - default=False, - help="If tracing should be enabled or not, defaults to False", -) -parser.add_argument( - "--otlp-agent-host", - type=str, - default="localhost", - help="The address of the open telemetry collector, defaults to localhost", -) -parser.add_argument( - "--otlp-agent-port", - type=str, - default="4317", - help="The port of the open telemetry collector, default to 4317", -) -parser.add_argument( - "--otlp-tracing-service-name", - type=str, - default="Raphtory", - help="The name this service will be known by for open telemetry, default to Raphtory", -) -parser.add_argument( - "--cache-capacity", - type=int, - default=30, - help="The maximum amount of graphs to keep in memory at any given time, defaults to 30", -) -parser.add_argument( - "--cache-tti-seconds", - type=int, - default=900, - help="The amount of time a graph will be kept in memory before being dropped, defaults to 900 seconds", -) -args = parser.parse_args() - -server = graphql.GraphServer( - work_dir=args.working_dir, - tracing=args.tracing, - log_level=args.log_level, - otlp_agent_host=args.otlp_agent_host, - otlp_agent_port=args.otlp_agent_port, - otlp_tracing_service_name=args.otlp_tracing_service_name, - cache_capacity=args.cache_capacity, - cache_tti_seconds=args.cache_tti_seconds, -) -server.run(port=args.port) diff --git a/docs/assets/images/raphtory_com_detection_ui.png b/docs/assets/images/raphtory_com_detection_ui.png new file mode 100644 index 0000000000000000000000000000000000000000..63897e07362bde4dda17459c8d37b2194ffde7ef GIT binary patch literal 585029 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

TR&qz3Tb>rpd9hCkQYXZ; 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

yxs>fm{D;G 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¬|^rF3^_F1~pt5Jy0M^;yxksp?Z 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Πh-uIzZ)0wt49ti5#R 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 GF8v<`=&h6h literal 0 HcmV?d00001 diff --git a/docs/assets/images/raphtory_ui_playground_docs.png b/docs/assets/images/raphtory_ui_playground_docs.png new file mode 100644 index 0000000000000000000000000000000000000000..c424619e28d5e91907102d6d4e89da058057bc98 GIT binary patch literal 577774 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≧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 z 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℘=E{J(6=4##R3ID}XM6dp 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 DO8(OH literal 0 HcmV?d00001 diff --git a/docs/data/karate.csv b/docs/data/karate.csv new file mode 100644 index 0000000000..ad73ce9503 --- /dev/null +++ b/docs/data/karate.csv @@ -0,0 +1,79 @@ +source,target,time +1,2,2023-01-01 +1,3,2023-01-01 +1,4,2023-01-01 +1,5,2023-01-01 +1,6,2023-01-01 +1,7,2023-01-01 +1,8,2023-01-01 +1,9,2023-01-01 +1,11,2023-01-01 +1,12,2023-01-01 +1,13,2023-01-01 +1,14,2023-01-01 +1,18,2023-01-01 +1,20,2023-01-01 +1,22,2023-01-01 +1,32,2023-01-01 +2,3,2023-01-01 +2,4,2023-01-01 +2,8,2023-01-01 +2,14,2023-01-01 +2,18,2023-01-01 +2,20,2023-01-01 +2,22,2023-01-01 +2,31,2023-01-01 +3,4,2023-01-01 +3,8,2023-01-01 +3,9,2023-01-01 +3,10,2023-01-01 +3,14,2023-01-01 +3,28,2023-01-01 +3,29,2023-01-01 +3,33,2023-01-01 +4,8,2023-01-01 +4,13,2023-01-01 +4,14,2023-01-01 +5,7,2023-01-01 +5,11,2023-01-01 +6,7,2023-01-01 +6,11,2023-01-01 +6,17,2023-01-01 +7,17,2023-01-01 +9,31,2023-01-01 +9,33,2023-01-01 +9,34,2023-01-01 +10,34,2023-01-01 +14,34,2023-01-01 +15,33,2023-01-01 +15,34,2023-01-01 +16,33,2023-01-01 +16,34,2023-01-01 +19,33,2023-01-01 +19,34,2023-01-01 +20,34,2023-01-01 +21,33,2023-01-01 +21,34,2023-01-01 +23,33,2023-01-01 +23,34,2023-01-01 +24,26,2023-01-01 +24,28,2023-01-01 +24,30,2023-01-01 +24,33,2023-01-01 +24,34,2023-01-01 +25,26,2023-01-01 +25,28,2023-01-01 +25,32,2023-01-01 +26,32,2023-01-01 +27,30,2023-01-01 +27,34,2023-01-01 +28,34,2023-01-01 +29,32,2023-01-01 +29,34,2023-01-01 +30,33,2023-01-01 +30,34,2023-01-01 +31,33,2023-01-01 +31,34,2023-01-01 +32,33,2023-01-01 +32,34,2023-01-01 +33,34,2023-01-01 diff --git a/docs/data/karate.gml b/docs/data/karate.gml new file mode 100644 index 0000000000..ecafe9c5a5 --- /dev/null +++ b/docs/data/karate.gml @@ -0,0 +1,530 @@ +Creator "Mark Newman on Fri Jul 21 12:39:27 2006" +graph +[ + node + [ + id 1 + ] + node + [ + id 2 + ] + node + [ + id 3 + ] + node + [ + id 4 + ] + node + [ + id 5 + ] + node + [ + id 6 + ] + node + [ + id 7 + ] + node + [ + id 8 + ] + node + [ + id 9 + ] + node + [ + id 10 + ] + node + [ + id 11 + ] + node + [ + id 12 + ] + node + [ + id 13 + ] + node + [ + id 14 + ] + node + [ + id 15 + ] + node + [ + id 16 + ] + node + [ + id 17 + ] + node + [ + id 18 + ] + node + [ + id 19 + ] + node + [ + id 20 + ] + node + [ + id 21 + ] + node + [ + id 22 + ] + node + [ + id 23 + ] + node + [ + id 24 + ] + node + [ + id 25 + ] + node + [ + id 26 + ] + node + [ + id 27 + ] + node + [ + id 28 + ] + node + [ + id 29 + ] + node + [ + id 30 + ] + node + [ + id 31 + ] + node + [ + id 32 + ] + node + [ + id 33 + ] + node + [ + id 34 + ] + edge + [ + source 2 + target 1 + ] + edge + [ + source 3 + target 1 + ] + edge + [ + source 3 + target 2 + ] + edge + [ + source 4 + target 1 + ] + edge + [ + source 4 + target 2 + ] + edge + [ + source 4 + target 3 + ] + edge + [ + source 5 + target 1 + ] + edge + [ + source 6 + target 1 + ] + edge + [ + source 7 + target 1 + ] + edge + [ + source 7 + target 5 + ] + edge + [ + source 7 + target 6 + ] + edge + [ + source 8 + target 1 + ] + edge + [ + source 8 + target 2 + ] + edge + [ + source 8 + target 3 + ] + edge + [ + source 8 + target 4 + ] + edge + [ + source 9 + target 1 + ] + edge + [ + source 9 + target 3 + ] + edge + [ + source 10 + target 3 + ] + edge + [ + source 11 + target 1 + ] + edge + [ + source 11 + target 5 + ] + edge + [ + source 11 + target 6 + ] + edge + [ + source 12 + target 1 + ] + edge + [ + source 13 + target 1 + ] + edge + [ + source 13 + target 4 + ] + edge + [ + source 14 + target 1 + ] + edge + [ + source 14 + target 2 + ] + edge + [ + source 14 + target 3 + ] + edge + [ + source 14 + target 4 + ] + edge + [ + source 17 + target 6 + ] + edge + [ + source 17 + target 7 + ] + edge + [ + source 18 + target 1 + ] + edge + [ + source 18 + target 2 + ] + edge + [ + source 20 + target 1 + ] + edge + [ + source 20 + target 2 + ] + edge + [ + source 22 + target 1 + ] + edge + [ + source 22 + target 2 + ] + edge + [ + source 26 + target 24 + ] + edge + [ + source 26 + target 25 + ] + edge + [ + source 28 + target 3 + ] + edge + [ + source 28 + target 24 + ] + edge + [ + source 28 + target 25 + ] + edge + [ + source 29 + target 3 + ] + edge + [ + source 30 + target 24 + ] + edge + [ + source 30 + target 27 + ] + edge + [ + source 31 + target 2 + ] + edge + [ + source 31 + target 9 + ] + edge + [ + source 32 + target 1 + ] + edge + [ + source 32 + target 25 + ] + edge + [ + source 32 + target 26 + ] + edge + [ + source 32 + target 29 + ] + edge + [ + source 33 + target 3 + ] + edge + [ + source 33 + target 9 + ] + edge + [ + source 33 + target 15 + ] + edge + [ + source 33 + target 16 + ] + edge + [ + source 33 + target 19 + ] + edge + [ + source 33 + target 21 + ] + edge + [ + source 33 + target 23 + ] + edge + [ + source 33 + target 24 + ] + edge + [ + source 33 + target 30 + ] + edge + [ + source 33 + target 31 + ] + edge + [ + source 33 + target 32 + ] + edge + [ + source 34 + target 9 + ] + edge + [ + source 34 + target 10 + ] + edge + [ + source 34 + target 14 + ] + edge + [ + source 34 + target 15 + ] + edge + [ + source 34 + target 16 + ] + edge + [ + source 34 + target 19 + ] + edge + [ + source 34 + target 20 + ] + edge + [ + source 34 + target 21 + ] + edge + [ + source 34 + target 23 + ] + edge + [ + source 34 + target 24 + ] + edge + [ + source 34 + target 27 + ] + edge + [ + source 34 + target 28 + ] + edge + [ + source 34 + target 29 + ] + edge + [ + source 34 + target 30 + ] + edge + [ + source 34 + target 31 + ] + edge + [ + source 34 + target 32 + ] + edge + [ + source 34 + target 33 + ] +] diff --git a/docs/reference/graphql/graphql_API.md b/docs/reference/graphql/graphql_API.md index 9dc848aa04..995283236d 100644 --- a/docs/reference/graphql/graphql_API.md +++ b/docs/reference/graphql/graphql_API.md @@ -22,7 +22,11 @@ hide: hello String! - + + +Hello world demo + + graph @@ -41,7 +45,13 @@ Returns a graph updateGraph MutableGraph! - + + +Update graph query, has side effects to update graph state + +Returns:: GqlMutableGraph + + path @@ -51,7 +61,13 @@ Returns a graph vectorisedGraph VectorisedGraph - + + +Create vectorised graph in the format used for queries + +Returns:: GqlVectorisedGraph + + path @@ -61,12 +77,24 @@ Returns a graph namespaces CollectionOfNamespace! - + + +Returns all namespaces using recursive search + +Returns:: List of namespaces on root + + namespace Namespace! - + + +Returns a specific namespace at a given path + +Returns:: Namespace or error if no namespace found + + path @@ -76,17 +104,33 @@ Returns a graph root Namespace! - + + +Returns root namespace + +Returns:: Root namespace + + plugins QueryPlugin! - + + +Returns a plugin. + + receiveGraph String! - + + +Encodes graph and returns as string + +Returns:: Base64 url safe encoded string + + path @@ -115,12 +159,20 @@ Returns a graph plugins MutationPlugin! - + + +Returns a collection of mutation plugins. + + deleteGraph Boolean! - + + +Delete graph from a path on the server. + + path @@ -130,7 +182,11 @@ Returns a graph newGraph Boolean! - + + +Creates a new graph. + + path @@ -145,7 +201,14 @@ Returns a graph moveGraph Boolean! - + + +Move graph from a path path on the server to a new_path on the server. + +If namespace is not provided, it will be set to the current working directory. +This applies to both the graph namespace and new graph namespace. + + path @@ -160,7 +223,14 @@ Returns a graph copyGraph Boolean! - + + +Copy graph from a path path on the server to a new_path on the server. + +If namespace is not provided, it will be set to the current working directory. +This applies to both the graph namespace and new graph namespace. + + path @@ -177,7 +247,7 @@ Returns a graph String! -Use GQL multipart upload to send new graphs to server +Upload a graph file from a path on the client using GQL multipart uploading. Returns:: name of the new graph @@ -204,7 +274,7 @@ name of the new graph String! -Send graph bincode as base64 encoded string +Send graph bincode as base64 encoded string. Returns:: path of the new graph @@ -231,7 +301,7 @@ path of the new graph String! -Create a subgraph out of some existing graph in the server +Returns a subgraph given a set of nodes from an existing graph in the server. Returns:: name of the new graph @@ -261,7 +331,11 @@ name of the new graph createIndex Boolean! - + + +(Experimental) Creates search index. + + path @@ -285,6 +359,8 @@ name of the new graph ### CollectionOfMetaGraph +Collection of items + @@ -298,20 +374,20 @@ name of the new graph - + @@ -334,13 +410,19 @@ will be returned. - +
list [MetaGraph!]! + +Returns a list of collection objects. + +
page [MetaGraph!]! -Fetch one "page" of items, optionally offset by a specified amount. +Fetch one page with a number of items up to a specified limit, optionally offset by a specified amount. The page_index sets the number of pages to skip (defaults to 0). -* `limit` - The size of the page (number of items to fetch). -* `offset` - The number of items to skip (defaults to 0). -* `page_index` - The number of pages (of size `limit`) to skip (defaults to 0). - -e.g. if page(5, 2, 1) is called, a page with 5 items, offset by 11 items (2 pages of 5 + 1), +For example, if page(5, 2, 1) is called, a page with 5 items, offset by 11 items (2 pages of 5 + 1), will be returned.
count Int! + +Returns a count of collection objects. + +
### CollectionOfNamespace +Collection of items + @@ -354,20 +436,20 @@ will be returned. - + @@ -390,13 +472,19 @@ will be returned. - +
list [Namespace!]! + +Returns a list of collection objects. + +
page [Namespace!]! -Fetch one "page" of items, optionally offset by a specified amount. +Fetch one page with a number of items up to a specified limit, optionally offset by a specified amount. The page_index sets the number of pages to skip (defaults to 0). -* `limit` - The size of the page (number of items to fetch). -* `offset` - The number of items to skip (defaults to 0). -* `page_index` - The number of pages (of size `limit`) to skip (defaults to 0). - -e.g. if page(5, 2, 1) is called, a page with 5 items, offset by 11 items (2 pages of 5 + 1), +For example, if page(5, 2, 1) is called, a page with 5 items, offset by 11 items (2 pages of 5 + 1), will be returned.
count Int! + +Returns a count of collection objects. + +
### CollectionOfNamespacedItem +Collection of items + @@ -410,20 +498,20 @@ will be returned. - + @@ -446,13 +534,19 @@ will be returned. - +
list [NamespacedItem!]! + +Returns a list of collection objects. + +
page [NamespacedItem!]! -Fetch one "page" of items, optionally offset by a specified amount. +Fetch one page with a number of items up to a specified limit, optionally offset by a specified amount. The page_index sets the number of pages to skip (defaults to 0). -* `limit` - The size of the page (number of items to fetch). -* `offset` - The number of items to skip (defaults to 0). -* `page_index` - The number of pages (of size `limit`) to skip (defaults to 0). - -e.g. if page(5, 2, 1) is called, a page with 5 items, offset by 11 items (2 pages of 5 + 1), +For example, if page(5, 2, 1) is called, a page with 5 items, offset by 11 items (2 pages of 5 + 1), will be returned.
count Int! + +Returns a count of collection objects. + +
### Edge +Raphtory graph edge. + @@ -466,12 +560,22 @@ will be returned. - + - + @@ -481,7 +585,13 @@ will be returned. - + @@ -491,7 +601,13 @@ will be returned. - + @@ -501,7 +617,13 @@ will be returned. - + @@ -511,7 +633,13 @@ will be returned. - + @@ -526,7 +654,13 @@ will be returned. - + @@ -536,7 +670,13 @@ will be returned. - + @@ -551,7 +691,11 @@ will be returned. - + @@ -561,12 +705,22 @@ will be returned. - + - + @@ -576,12 +730,22 @@ will be returned. - + - + @@ -591,7 +755,11 @@ will be returned. - + @@ -601,7 +769,11 @@ will be returned. - + @@ -616,7 +788,11 @@ will be returned. - + @@ -626,7 +802,11 @@ will be returned. - + @@ -636,7 +816,11 @@ will be returned. - + @@ -646,7 +830,11 @@ will be returned. - + @@ -656,7 +844,11 @@ will be returned. - + @@ -666,97 +858,183 @@ will be returned. - + - + - + - + - + - + - - + + - + - + - + - + - + - + - + - + - + - + - + - +
defaultLayer Edge! + +Return a view of Edge containing only the default edge layer. + +
layers Edge! + +Returns a view of Edge containing all layers in the list of names. + +Errors if any of the layers do not exist. + +
names
excludeLayers Edge! + +Returns a view of Edge containing all layers except the excluded list of names. + +Errors if any of the layers do not exist. + +
names
layer Edge! + +Returns a view of Edge containing the specified layer. + +Errors if any of the layers do not exist. + +
name
excludeLayer Edge! + +Returns a view of Edge containing all layers except the excluded layer specified. + +Errors if any of the layers do not exist. + +
name
rolling EdgeWindowSet! + +Creates a WindowSet with the given window duration and optional step using a rolling window. + +A rolling window is a window that moves forward by step size at each iteration. + +
window
expanding EdgeWindowSet! + +Creates a WindowSet with the given step size using an expanding window. + +An expanding window is a window that grows by step size at each iteration. + +
step
window Edge! + +Creates a view of the Edge including all events between the specified start (inclusive) and end (exclusive). + +For persistent graphs, any edge which exists at any point during the window will be included. You may want to restrict this to only edges that are present at the end of the window using the is_valid function. + +
start
at Edge! + +Creates a view of the Edge including all events at a specified time. + +
time
latest Edge! + +Returns a view of the edge at the latest time of the graph. + +
snapshotAt Edge! + +Creates a view of the Edge including all events that are valid at time. + +This is equivalent to before(time + 1) for Graph and at(time) for PersistentGraph. + +
time
snapshotLatest Edge! + +Creates a view of the Edge including all events that are valid at the latest time. + +This is equivalent to a no-op for Graph and latest() for PersistentGraph. + +
before Edge! + +Creates a view of the Edge including all events before a specified end (exclusive). + +
time
after Edge! + +Creates a view of the Edge including all events after a specified start (exclusive). + +
time
shrinkWindow Edge! + +Shrinks both the start and end of the window. + +
start
shrinkStart Edge! + +Set the start of the window. + +
start
shrinkEnd Edge! + +Set the end of the window. + +
end
applyViews Edge! + +Takes a specified selection of views and applies them in given order. + +
views
earliestTime Int + +Returns the earliest time of an edge. + +
firstUpdate
latestTime Int + +Returns the latest time of an edge. + +
lastUpdate
time Int! + +Returns the time of an exploded edge. Errors on an unexploded edge. + +
start Int + +Returns the start time for rolling and expanding windows for this edge. Returns none if no window is applied. + +
end Int + +Returns the end time of the window. Returns none if no window is applied. + +
src Node! + +Returns the source node of the edge. + +
dst Node! + +Returns the destination node of the edge. + +
nbr Node! + +Returns the node at the other end of the edge (same as dst() for out-edges and src() for in-edges). + +
id [String!]!
+ +Returns the id of the edge. + +
properties Properties! + +Returns a view of the properties of the edge. + +
metadata Metadata! + +Returns the metadata of an edge. + +
layerNames [String!]! + +Returns the names of the layers that have this edge as a member. + +
layerName String! + +Returns the layer name of an exploded edge, errors on an edge. + +
explode Edges! + +Returns an edge object for each update within the original edge. + +
explodeLayers Edges! + +Returns an edge object for each layer within the original edge. + +Each new edge object contains only updates from the respective layers. + +
history [Int!]! + +Returns a list of timestamps of when an edge is added or change to an edge is made. + +
deletions [Int!]! + +Returns a list of timestamps of when an edge is deleted. + +
isValid Boolean! + +Checks if the edge is currently valid and exists at the current time. + +Returns: boolean + +
isActive Boolean! + +Checks if the edge is currently active and has at least one update within the current period. + +Returns: boolean + +
isDeleted Boolean! + +Checks if the edge is deleted at the current time. + +Returns: boolean + +
isSelfLoop Boolean! + +Returns true if the edge source and destination nodes are the same. + +Returns: boolean + +
@@ -803,7 +1081,11 @@ Returns the list of property schemas for edges connecting these types of nodes metadata [PropertySchema!]! - + + +Returns the list of metadata schemas for edges connecting these types of nodes + + @@ -830,13 +1112,10 @@ Returns the list of property schemas for edges connecting these types of nodes [Edge!]! -Fetch one "page" of items, optionally offset by a specified amount. - -* `limit` - The size of the page (number of items to fetch). -* `offset` - The number of items to skip (defaults to 0). -* `page_index` - The number of pages (of size `limit`) to skip (defaults to 0). +Fetch one page with a number of items up to a specified limit, optionally offset by a specified amount. +The page_index sets the number of pages to skip (defaults to 0). -e.g. if page(5, 2, 1) is called, a page with 5 items, offset by 11 items (2 pages of 5 + 1), +For example, if page(5, 2, 1) is called, a page with 5 items, offset by 11 items (2 pages of 5 + 1), will be returned. @@ -879,12 +1158,20 @@ will be returned. defaultLayer Edges! - + + +Returns a collection containing only edges in the default edge layer. + + layers Edges! - + + +Returns a collection containing only edges belonging to the listed layers. + + names @@ -894,7 +1181,11 @@ will be returned. excludeLayers Edges! - + + +Returns a collection containing edges belonging to all layers except the excluded list of layers. + + names @@ -904,7 +1195,11 @@ will be returned. layer Edges! - + + +Returns a collection containing edges belonging to the specified layer. + + name @@ -914,7 +1209,11 @@ will be returned. excludeLayer Edges! - + + +Returns a collection containing edges belonging to all layers except the excluded layer specified. + + name @@ -924,7 +1223,13 @@ will be returned. rolling EdgesWindowSet! - + + +Creates a WindowSet with the given window duration and optional step using a rolling window. A rolling window is a window that moves forward by step size at each iteration. + +Returns a collection of collections. This means that item in the window set is a collection of edges. + + window @@ -939,7 +1244,13 @@ will be returned. expanding EdgesWindowSet! - + + +Creates a WindowSet with the given step size using an expanding window. An expanding window is a window that grows by step size at each iteration. + +Returns a collection of collections. This means that item in the window set is a collection of edges. + + step @@ -949,7 +1260,11 @@ will be returned. window Edges! - + + +Creates a view of the Edge including all events between the specified start (inclusive) and end (exclusive). + + start @@ -964,7 +1279,11 @@ will be returned. at Edges! - + + +Creates a view of the Edge including all events at a specified time. + + time @@ -979,7 +1298,11 @@ will be returned. snapshotAt Edges! - + + +Creates a view of the Edge including all events that are valid at time. This is equivalent to before(time + 1) for Graph and at(time) for PersistentGraph. + + time @@ -989,12 +1312,20 @@ will be returned. snapshotLatest Edges! - + + +Creates a view of the Edge including all events that are valid at the latest time. This is equivalent to a no-op for Graph and latest() for PersistentGraph. + + before Edges! - + + +Creates a view of the Edge including all events before a specified end (exclusive). + + time @@ -1004,7 +1335,11 @@ will be returned. after Edges! - + + +Creates a view of the Edge including all events after a specified start (exclusive). + + time @@ -1014,7 +1349,11 @@ will be returned. shrinkWindow Edges! - + + +Shrinks both the start and end of the window. + + start @@ -1029,7 +1368,11 @@ will be returned. shrinkStart Edges! - + + +Set the start of the window. + + start @@ -1039,7 +1382,11 @@ will be returned. shrinkEnd Edges! - + + +Set the end of the window. + + end @@ -1049,7 +1396,11 @@ will be returned. applyViews Edges! - + + +Takes a specified selection of views and applies them in order given. + + views @@ -1059,17 +1410,31 @@ will be returned. explode Edges! - + + +Returns an edge object for each update within the original edge. + + explodeLayers Edges! - + + +Returns an edge object for each layer within the original edge. + +Each new edge object contains only updates from the respective layers. + + sorted Edges! - + + +Specify a sort order from: source, destination, property, time. You can also reverse the ordering. + + sortBys @@ -1079,30 +1444,39 @@ will be returned. start Int - + + +Returns the start time of the window or none if there is no window. + + end Int - + + +Returns the end time of the window or none if there is no window. + + count Int! - + + +Returns the number of edges. + + page [Edge!]! -Fetch one "page" of items, optionally offset by a specified amount. +Fetch one page with a number of items up to a specified limit, optionally offset by a specified amount. +The page_index sets the number of pages to skip (defaults to 0). -* `limit` - The size of the page (number of items to fetch). -* `offset` - The number of items to skip (defaults to 0). -* `page_index` - The number of pages (of size `limit`) to skip (defaults to 0). - -e.g. if page(5, 2, 1) is called, a page with 5 items, offset by 11 items (2 pages of 5 + 1), +For example, if page(5, 2, 1) is called, a page with 5 items, offset by 11 items (2 pages of 5 + 1), will be returned. @@ -1125,7 +1499,11 @@ will be returned. list [Edge!]! - + + +Returns a list of all objects in the current selection of the collection. You should filter filter the collection first then call list. + + @@ -1152,13 +1530,10 @@ will be returned. [Edges!]! -Fetch one "page" of items, optionally offset by a specified amount. +Fetch one page with a number of items up to a specified limit, optionally offset by a specified amount. +The page_index sets the number of pages to skip (defaults to 0). -* `limit` - The size of the page (number of items to fetch). -* `offset` - The number of items to skip (defaults to 0). -* `page_index` - The number of pages (of size `limit`) to skip (defaults to 0). - -e.g. if page(5, 2, 1) is called, a page with 5 items, offset by 11 items (2 pages of 5 + 1), +For example, if page(5, 2, 1) is called, a page with 5 items, offset by 11 items (2 pages of 5 + 1), will be returned. @@ -1188,6 +1563,8 @@ will be returned. ### GqlDocument +Document in a vector graph + @@ -1201,17 +1578,29 @@ will be returned. - + - + - + @@ -1236,22 +1625,38 @@ will be returned. - + - + - + - +
entity DocumentEntity! + +Entity associated with document. + +
content String! + +Content of the document. + +
embedding [Float!]! + +Similarity score with a specified query + +
score
nodeMetadata [String!]! + +Returns node metadata. + +
nodeProperties [String!]! + +Returns node properties. + +
edgeMetadata [String!]! + +Returns edge metadata. + +
edgeProperties [String!]! + +Returns edge properties. + +
@@ -1271,32 +1676,56 @@ will be returned. nodes [Node!]! - + + +Returns a list of nodes in the current selection. + + edges [Edge!]! - + + +Returns a list of edges in the current selection. + + getDocuments [GqlDocument!]! - + + +Returns a list of documents in the current selection. + + addNodes GqlVectorSelection! - - - -nodes + + +Adds all the documents associated with the specified nodes to the current selection. + +Documents added by this call are assumed to have a score of 0. + + + + +nodes [String!]! addEdges GqlVectorSelection! - + + +Adds all the documents associated with the specified edges to the current selection. + +Documents added by this call are assumed to have a score of 0. + + edges @@ -1306,7 +1735,13 @@ will be returned. expand GqlVectorSelection! - + + +Add all the documents a specified number of hops away to the selection. + +Two documents A and B are considered to be 1 hop away of each other if they are on the same entity or if they are on the same node and edge pair. + + hops @@ -1321,7 +1756,11 @@ will be returned. expandEntitiesBySimilarity GqlVectorSelection! - + + +Adds documents, from the set of one hop neighbours to the current selection, to the selection based on their similarity score with the specified query. This function loops so that the set of one hop neighbours expands on each loop and number of documents added is determined by the specified limit. + + query @@ -1341,7 +1780,11 @@ will be returned. expandNodesBySimilarity GqlVectorSelection! - + + +Add the adjacent nodes with higher score for query to the selection up to a specified limit. This function loops like expand_entities_by_similarity but is restricted to nodes. + + query @@ -1361,7 +1804,11 @@ will be returned. expandEdgesBySimilarity GqlVectorSelection! - + + +Add the adjacent edges with higher score for query to the selection up to a specified limit. This function loops like expand_entities_by_similarity but is restricted to edges. + + query @@ -1396,17 +1843,29 @@ will be returned. uniqueLayers [String!]! - + + +Returns the names of all layers in the graphview. + + defaultLayer Graph! - + + +Returns a view containing only the default layer. + + layers Graph! - + + +Returns a view containing all the specified layers. + + names @@ -1416,7 +1875,11 @@ will be returned. excludeLayers Graph! - + + +Returns a view containing all layers except the specified excluded layers. + + names @@ -1426,7 +1889,11 @@ will be returned. layer Graph! - + + +Returns a view containing the layer specified. + + name @@ -1436,7 +1903,11 @@ will be returned. excludeLayer Graph! - + + +Returns a view containing all layers except the specified excluded layer. + + name @@ -1446,7 +1917,11 @@ will be returned. subgraph Graph! - + + +Returns a subgraph of a specified set of nodes which contains only the edges that connect nodes of the subgraph to each other. + + nodes @@ -1456,12 +1931,20 @@ will be returned. valid Graph! - + + +Returns a view of the graph that only includes valid edges. + + subgraphNodeTypes Graph! - + + +Returns a subgraph filtered by the specified node types. + + nodeTypes @@ -1471,7 +1954,11 @@ will be returned. excludeNodes Graph! - + + +Returns a subgraph containing all nodes except the specified excluded nodes. + + nodes @@ -1481,7 +1968,11 @@ will be returned. rolling GraphWindowSet! - + + +Creates a rolling window with the specified window size and an optional step. + + window @@ -1496,7 +1987,11 @@ will be returned. expanding GraphWindowSet! - + + +Creates a expanding window with the specified step size. + + step @@ -1508,7 +2003,7 @@ will be returned. Graph! -Return a graph containing only the activity between `start` and `end` measured as milliseconds from epoch +Return a graph containing only the activity between start and end, by default raphtory stores times in milliseconds from the unix epoch. @@ -1525,7 +2020,11 @@ Return a graph containing only the activity between `start` and `end` measured a at Graph! - + + +Creates a view including all events at a specified time. + + time @@ -1535,12 +2034,20 @@ Return a graph containing only the activity between `start` and `end` measured a latest Graph! - + + +Creates a view including all events at the latest time. + + snapshotAt Graph! - + + +Create a view including all events that are valid at the specified time. + + time @@ -1550,12 +2057,20 @@ Return a graph containing only the activity between `start` and `end` measured a snapshotLatest Graph! - + + +Create a view including all events that are valid at the latest time. + + before Graph! - + + +Create a view including all events before a specified end (exclusive). + + time @@ -1565,7 +2080,11 @@ Return a graph containing only the activity between `start` and `end` measured a after Graph! - + + +Create a view including all events after a specified start (exclusive). + + time @@ -1575,7 +2094,11 @@ Return a graph containing only the activity between `start` and `end` measured a shrinkWindow Graph! - + + +Shrink both the start and end of the window. + + start @@ -1590,7 +2113,11 @@ Return a graph containing only the activity between `start` and `end` measured a shrinkStart Graph! - + + +Set the start of the window to the larger of the specified value or current start. + + start @@ -1600,7 +2127,11 @@ Return a graph containing only the activity between `start` and `end` measured a shrinkEnd Graph! - + + +Set the end of the window to the smaller of the specified value or current end. + + end @@ -1610,42 +2141,74 @@ Return a graph containing only the activity between `start` and `end` measured a created Int! - + + +Returns the timestamp for the creation of the graph. + + lastOpened Int! - + + +Returns the graph's last opened timestamp according to system time. + + lastUpdated Int! - + + +Returns the graph's last updated timestamp. + + earliestTime Int - + + +Returns the timestamp of the earliest activity in the graph. + + latestTime Int - + + +Returns the timestamp of the latest activity in the graph. + + start Int - + + +Returns the start time of the window. Errors if there is no window. + + end Int - + + +Returns the end time of the window. Errors if there is no window. + + earliestEdgeTime Int - + + +Returns the earliest time that any edge in this graph is valid. + + includeNegative @@ -1655,7 +2218,11 @@ Return a graph containing only the activity between `start` and `end` measured a latestEdgeTime Int - + + +/// Returns the latest time that any edge in this graph is valid. + + includeNegative @@ -1665,22 +2232,40 @@ Return a graph containing only the activity between `start` and `end` measured a countEdges Int! - + + +Returns the number of edges in the graph. + + countTemporalEdges Int! - + + +Returns the number of temporal edges in the graph. + + countNodes Int! - + + +Returns the number of nodes in the graph. + +Optionally takes a list of node ids to return a subset. + + hasNode Boolean! - + + +Returns true if the graph contains the specified node. + + name @@ -1690,7 +2275,11 @@ Return a graph containing only the activity between `start` and `end` measured a hasEdge Boolean! - + + +Returns true if the graph contains the specified edge. Edges are specified by providing a source and destination node id. You can restrict the search to a specified layer. + + src @@ -1710,7 +2299,11 @@ Return a graph containing only the activity between `start` and `end` measured a node Node - + + +Gets the node with the specified id. + + name @@ -1722,7 +2315,7 @@ Return a graph containing only the activity between `start` and `end` measured a Nodes! -query (optionally a subset of) the nodes in the graph +Gets (optionally a subset of) the nodes in the graph. @@ -1734,7 +2327,11 @@ query (optionally a subset of) the nodes in the graph edge Edge - + + +Gets the edge with the specified source and destination nodes. + + src @@ -1749,37 +2346,65 @@ query (optionally a subset of) the nodes in the graph edges Edges! - + + +Gets the edges in the graph. + + properties Properties! - + + +Returns the properties of the graph. + + metadata Metadata! - + + +Returns the metadata of the graph. + + name String! - + + +Returns the graph name. + + path String! - + + +Returns path of graph. + + namespace String! - + + +Returns namespace of graph. + + schema GraphSchema! - + + +Returns the graph schema. + + algorithms @@ -1833,12 +2458,22 @@ Export all nodes and edges from this graph view to another existing graph getIndexSpec GqlIndexSpec! - + + +(Experimental) Get index specification. + + searchNodes [Node!]! - + + +(Experimental) Searches for nodes which match the given filter expression. + +Uses Tantivy's exact search. + + filter @@ -1858,7 +2493,13 @@ Export all nodes and edges from this graph view to another existing graph searchEdges [Edge!]! - + + +(Experimental) Searches the index for edges which match the given filter expression. + +Uses Tantivy's exact search. + + filter @@ -1878,7 +2519,12 @@ Export all nodes and edges from this graph view to another existing graph applyViews Graph! - + + +Returns the specified graph view or if none is specified returns the default view. +This allows you to specify multiple operations together. + + views @@ -1901,43 +2547,43 @@ Export all nodes and edges from this graph view to another existing graph -shortest_path -[ShortestPathOutput!]! +pagerank +[PagerankOutput!]! -source -String! +iterCount +Int! -targets -[String!]! +threads +Int -direction -String +tol +Float -pagerank -[PagerankOutput!]! +shortest_path +[ShortestPathOutput!]! -iterCount -Int! +source +String! -threads -Int +targets +[String!]! -tol -Float +direction +String @@ -1983,20 +2629,21 @@ Export all nodes and edges from this graph view to another existing graph count Int! - + + +Returns the number of items. + + page [Graph!]! -Fetch one "page" of items, optionally offset by a specified amount. - -* `limit` - The size of the page (number of items to fetch). -* `offset` - The number of items to skip (defaults to 0). -* `page_index` - The number of pages (of size `limit`) to skip (defaults to 0). +Fetch one page with a number of items up to a specified limit, optionally offset by a specified amount. +The page_index sets the number of pages to skip (defaults to 0). -e.g. if page(5, 2, 1) is called, a page with 5 items, offset by 11 items (2 pages of 5 + 1), +For example, if page(5, 2, 1) is called, a page with 5 items, offset by 11 items (2 pages of 5 + 1), will be returned. @@ -2072,42 +2719,74 @@ Returns the list of edge schemas for this edge layer name String - + + +Returns the graph name. + + path String! - + + +Returns path of graph. + + created Int! - + + +Returns the timestamp for the creation of the graph. + + lastOpened Int! - + + +Returns the graph's last opened timestamp according to system time. + + lastUpdated Int! - + + +Returns the graph's last updated timestamp. + + nodeCount Int! - + + +Returns the number of nodes in the graph. + + edgeCount Int! - + + +Returns the number of edges in the graph. + + metadata [Property!]! - + + +Returns the metadata of the graph. + + @@ -2127,7 +2806,11 @@ Returns the list of edge schemas for this edge layer get Property - + + +Get metadata value matching the specified key. + + key @@ -2137,7 +2820,11 @@ Returns the list of edge schemas for this edge layer contains Boolean! - + + +/// Check if the key is in the metadata. + + key @@ -2147,12 +2834,20 @@ Returns the list of edge schemas for this edge layer keys [String!]! - + + +Return all metadata keys. + + values [Property!]! - + + +/// Return all metadata values. + + keys @@ -2179,7 +2874,7 @@ Returns the list of edge schemas for this edge layer Boolean! -Use to check if adding the edge was successful +Use to check if adding the edge was successful. @@ -2188,7 +2883,7 @@ Use to check if adding the edge was successful Edge! -Get the non-mutable edge for querying +Get the non-mutable edge for querying. @@ -2197,7 +2892,7 @@ Get the non-mutable edge for querying MutableNode! -Get the mutable source node of the edge +Get the mutable source node of the edge. @@ -2206,7 +2901,7 @@ Get the mutable source node of the edge MutableNode! -Get the mutable destination node of the edge +Get the mutable destination node of the edge. @@ -2215,7 +2910,7 @@ Get the mutable destination node of the edge Boolean! -Mark the edge as deleted at time `time` +Mark the edge as deleted at time time. @@ -2234,9 +2929,9 @@ Mark the edge as deleted at time `time` Boolean! -Add metadata to the edge (errors if the value already exists) +Add metadata to the edge (errors if the value already exists). -If this is called after `add_edge`, the layer is inherited from the `add_edge` and does not +If this is called after add_edge, the layer is inherited from the add_edge and does not need to be specified again. @@ -2256,9 +2951,9 @@ need to be specified again. Boolean! -Update metadata of the edge (existing values are overwritten) +Update metadata of the edge (existing values are overwritten). -If this is called after `add_edge`, the layer is inherited from the `add_edge` and does not +If this is called after add_edge, the layer is inherited from the add_edge and does not need to be specified again. @@ -2278,9 +2973,9 @@ need to be specified again. Boolean! -Add temporal property updates to the edge +Add temporal property updates to the edge. -If this is called after `add_edge`, the layer is inherited from the `add_edge` and does not +If this is called after add_edge, the layer is inherited from the add_edge and does not need to be specified again. @@ -2320,7 +3015,7 @@ need to be specified again. Graph! -Get the non-mutable graph +Get the non-mutable graph. @@ -2329,7 +3024,7 @@ Get the non-mutable graph MutableNode -Get mutable existing node +Get mutable existing node. @@ -2343,7 +3038,7 @@ Get mutable existing node MutableNode! -Add a new node or add updates to an existing node +Add a new node or add updates to an existing node. @@ -2372,7 +3067,7 @@ Add a new node or add updates to an existing node MutableNode! -Create a new node or fail if it already exists +Create a new node or fail if it already exists. @@ -2401,7 +3096,7 @@ Create a new node or fail if it already exists Boolean! -Add a batch of nodes +Add a batch of nodes. @@ -2415,7 +3110,7 @@ Add a batch of nodes MutableEdge -Get a mutable existing edge +Get a mutable existing edge. @@ -2434,7 +3129,7 @@ Get a mutable existing edge MutableEdge! -Add a new edge or add updates to an existing edge +Add a new edge or add updates to an existing edge. @@ -2468,7 +3163,7 @@ Add a new edge or add updates to an existing edge Boolean! -Add a batch of edges +Add a batch of edges. @@ -2482,7 +3177,7 @@ Add a batch of edges MutableEdge! -Mark an edge as deleted (creates the edge if it did not exist) +Mark an edge as deleted (creates the edge if it did not exist). @@ -2511,7 +3206,7 @@ Mark an edge as deleted (creates the edge if it did not exist) Boolean! -Add temporal properties to graph +Add temporal properties to graph. @@ -2530,7 +3225,7 @@ Add temporal properties to graph Boolean! -Add metadata to graph (errors if the property already exists) +Add metadata to graph (errors if the property already exists). @@ -2544,7 +3239,7 @@ Add metadata to graph (errors if the property already exists) Boolean! -Update metadata of the graph (overwrites existing values) +Update metadata of the graph (overwrites existing values). @@ -2573,7 +3268,7 @@ Update metadata of the graph (overwrites existing values) Boolean! -Use to check if adding the node was successful +Use to check if adding the node was successful. @@ -2582,7 +3277,7 @@ Use to check if adding the node was successful Node! -Get the non-mutable `Node` +Get the non-mutable Node. @@ -2591,7 +3286,7 @@ Get the non-mutable `Node` Boolean! -Add metadata to the node (errors if the property already exists) +Add metadata to the node (errors if the property already exists). @@ -2605,7 +3300,7 @@ Add metadata to the node (errors if the property already exists) Boolean! -Set the node type (errors if the node already has a non-default type) +Set the node type (errors if the node already has a non-default type). @@ -2619,7 +3314,7 @@ Set the node type (errors if the node already has a non-default type) Boolean! -Update metadata of the node (overwrites existing property values) +Update metadata of the node (overwrites existing property values). @@ -2633,7 +3328,7 @@ Update metadata of the node (overwrites existing property values) Boolean! -Add temporal property updates to the node +Add temporal property updates to the node. @@ -2712,6 +3407,8 @@ Add temporal property updates to the node ### Node +Raphtory graph node. + @@ -2725,22 +3422,38 @@ Add temporal property updates to the node - + - + - + - + @@ -2750,7 +3463,11 @@ Add temporal property updates to the node - + @@ -2760,7 +3477,11 @@ Add temporal property updates to the node - + @@ -2770,7 +3491,11 @@ Add temporal property updates to the node - + @@ -2780,7 +3505,13 @@ Add temporal property updates to the node - + @@ -2795,7 +3526,11 @@ Add temporal property updates to the node - + @@ -2805,7 +3540,11 @@ Add temporal property updates to the node - + @@ -2820,7 +3559,11 @@ Add temporal property updates to the node - + @@ -2830,12 +3573,20 @@ Add temporal property updates to the node - + - + @@ -2845,12 +3596,20 @@ Add temporal property updates to the node - + - + @@ -2860,7 +3619,11 @@ Add temporal property updates to the node - + @@ -2870,7 +3633,11 @@ Add temporal property updates to the node - + @@ -2885,7 +3652,11 @@ Add temporal property updates to the node - + @@ -2895,7 +3666,11 @@ Add temporal property updates to the node - + @@ -2915,69 +3690,117 @@ Add temporal property updates to the node - + - + - + - + - + - + - + - - - - + + + + - + - + - + - + @@ -2986,7 +3809,7 @@ Returns the number of edges connected to this node @@ -2995,7 +3818,7 @@ Returns the number edges with this node as the source @@ -3012,32 +3835,56 @@ Returns the number edges with this node as the destination - + - + - + - + - + - + @@ -3108,13 +3955,10 @@ Returns the list of property schemas for this node @@ -3157,12 +4001,20 @@ will be returned. - + - + @@ -3172,7 +4024,11 @@ will be returned. - + @@ -3182,7 +4038,11 @@ will be returned. - + @@ -3192,7 +4052,11 @@ will be returned. - + @@ -3202,7 +4066,11 @@ will be returned. - + @@ -3217,7 +4085,11 @@ will be returned. - + @@ -3227,7 +4099,11 @@ will be returned. - + @@ -3242,7 +4118,11 @@ will be returned. - + @@ -3252,12 +4132,20 @@ will be returned. - + - + @@ -3267,12 +4155,20 @@ will be returned. - + - + @@ -3282,7 +4178,11 @@ will be returned. - + @@ -3292,7 +4192,11 @@ will be returned. - + @@ -3307,7 +4211,11 @@ will be returned. - + @@ -3317,7 +4225,11 @@ will be returned. - + @@ -3327,7 +4239,11 @@ will be returned. - + @@ -3337,7 +4253,11 @@ will be returned. - + @@ -3367,12 +4287,20 @@ will be returned. - + - + @@ -3384,13 +4312,10 @@ will be returned. @@ -3418,7 +4343,11 @@ will be returned. - +
id String! + +Returns the unique id of the node. + +
name String! + +Returns the name of the node. + +
defaultLayer Node! + +Return a view of the node containing only the default layer. + +
layers Node! + +Return a view of node containing all layers specified. + +
names
excludeLayers Node! + +Returns a collection containing nodes belonging to all layers except the excluded list of layers. + +
names
layer Node! + +Returns a collection containing nodes belonging to the specified layer. + +
name
excludeLayer Node! + +Returns a collection containing nodes belonging to all layers except the excluded layer. + +
name
rolling NodeWindowSet! + +Creates a WindowSet with the specified window size and optional step using a rolling window. + +Returns a collection of collections. This means that item in the window set is a collection of nodes. + +
window
expanding NodeWindowSet! + +Creates a WindowSet with the specified step size using an expanding window. + +
step
window Node! + +Create a view of the node including all events between the specified start (inclusive) and end (exclusive). + +
start
at Node! + +Create a view of the node including all events at a specified time. + +
time
latest Node! + +Create a view of the node including all events at the latest time. + +
snapshotAt Node! + +Create a view of the node including all events that are valid at the specified time. + +
time
snapshotLatest Node! + +Create a view of the node including all events that are valid at the latest time. + +
before Node! + +Create a view of the node including all events before specified end time (exclusive). + +
time
after Node! + +Create a view of the node including all events after the specified start time (exclusive). + +
time
shrinkWindow Node! + +Shrink a Window to a specified start and end time, if these are earlier and later than the current start and end respectively. + +
start
shrinkStart Node! + +Set the start of the window to the larger of a specified start time and self.start(). + +
start
shrinkEnd Node! + +Set the end of the window to the smaller of a specified end and self.end(). + +
end
earliestTime Int + +Returns the earliest time that the node exists. + +
firstUpdate Int + +Returns the time of the first update made to the node. + +
latestTime Int + +Returns the latest time that the node exists. + +
lastUpdate Int + +Returns the time of the last update made to the node. + +
start Int + +Gets the start time for the window. Errors if there is no window. + +
end Int + +Gets the end time for the window. Errors if there is no window. + +
history [Int!]! + +Returns the history of a node, including node additions and changes made to node. + +
edgeHistoryCount Int!
isActive + +Get the number of edge events for this node. + +
isActive Boolean! + +Check if the node is active and it's history is not empty. + +
nodeType String + +Returns the type of node. + +
properties Properties! + +Returns the properties of the node. + +
metadata Metadata! + +Returns the metadata of the node. + +
degree Int! -Returns the number of edges connected to this node +Returns the number of unique counter parties for this node.
Int! -Returns the number edges with this node as the source +Returns the number edges with this node as the source.
Int! -Returns the number edges with this node as the destination +Returns the number edges with this node as the destination.
edges Edges! + +Returns all connected edges. + +
outEdges Edges! + +Returns outgoing edges. + +
inEdges Edges! + +Returns incoming edges. + +
neighbours PathFromNode! + +Returns neighbouring nodes. + +
inNeighbours PathFromNode! + +Returns the number of neighbours that have at least one in-going edge to this node. + +
outNeighbours PathFromNode! + +Returns the number of neighbours that have at least one out-going edge from this node. + +
nodeFilter[Node!]! -Fetch one "page" of items, optionally offset by a specified amount. - -* `limit` - The size of the page (number of items to fetch). -* `offset` - The number of items to skip (defaults to 0). -* `page_index` - The number of pages (of size `limit`) to skip (defaults to 0). +Fetch one page with a number of items up to a specified limit, optionally offset by a specified amount. +The page_index sets the number of pages to skip (defaults to 0). -e.g. if page(5, 2, 1) is called, a page with 5 items, offset by 11 items (2 pages of 5 + 1), +For example, if page(5, 2, 1) is called, a page with 5 items, offset by 11 items (2 pages of 5 + 1), will be returned.
defaultLayer Nodes! + +Return a view of the nodes containing only the default edge layer. + +
layers Nodes! + +Return a view of the nodes containing all layers specified. + +
names
excludeLayers Nodes! + +Return a view of the nodes containing all layers except those specified. + +
names
layer Nodes! + +Return a view of the nodes containing the specified layer. + +
name
excludeLayer Nodes! + +Return a view of the nodes containing all layers except those specified. + +
name
rolling NodesWindowSet! + +Creates a WindowSet with the specified window size and optional step using a rolling window. + +
window
expanding NodesWindowSet! + +Creates a WindowSet with the specified step size using an expanding window. + +
step
window Nodes! + +Create a view of the node including all events between the specified start (inclusive) and end (exclusive). + +
start
at Nodes! + +Create a view of the nodes including all events at a specified time. + +
time
latest Nodes! + +Create a view of the nodes including all events at the latest time. + +
snapshotAt Nodes! + +Create a view of the nodes including all events that are valid at the specified time. + +
time
snapshotLatest Nodes! + +Create a view of the nodes including all events that are valid at the latest time. + +
before Nodes! + +Create a view of the nodes including all events before specified end time (exclusive). + +
time
after Nodes! + +Create a view of the nodes including all events after the specified start time (exclusive). + +
time
shrinkWindow Nodes! + +Shrink both the start and end of the window. + +
start
shrinkStart Nodes! + +Set the start of the window to the larger of a specified start time and self.start(). + +
start
shrinkEnd Nodes! + +Set the end of the window to the smaller of a specified end and self.end(). + +
end
typeFilter Nodes! + +Filter nodes by node type. + +
nodeTypes
nodeFilter Nodes! + +Returns a view of the node types. + +
filter
start Int + +Returns the start time of the window. Errors if there is no window. + +
end Int + +Returns the end time of the window. Errors if there is no window. + +
count[Node!]! -Fetch one "page" of items, optionally offset by a specified amount. - -* `limit` - The size of the page (number of items to fetch). -* `offset` - The number of items to skip (defaults to 0). -* `page_index` - The number of pages (of size `limit`) to skip (defaults to 0). +Fetch one page with a number of items up to a specified limit, optionally offset by a specified amount. +The page_index sets the number of pages to skip (defaults to 0). -e.g. if page(5, 2, 1) is called, a page with 5 items, offset by 11 items (2 pages of 5 + 1), +For example, if page(5, 2, 1) is called, a page with 5 items, offset by 11 items (2 pages of 5 + 1), will be returned.
ids [String!]! + +Returns a view of the node ids. + +
@@ -3445,13 +4374,10 @@ will be returned. [Nodes!]! -Fetch one "page" of items, optionally offset by a specified amount. - -* `limit` - The size of the page (number of items to fetch). -* `offset` - The number of items to skip (defaults to 0). -* `page_index` - The number of pages (of size `limit`) to skip (defaults to 0). +Fetch one page with a number of items up to a specified limit, optionally offset by a specified amount. +The page_index sets the number of pages to skip (defaults to 0). -e.g. if page(5, 2, 1) is called, a page with 5 items, offset by 11 items (2 pages of 5 + 1), +For example, if page(5, 2, 1) is called, a page with 5 items, offset by 11 items (2 pages of 5 + 1), will be returned. @@ -3481,6 +4407,8 @@ will be returned. ### PagerankOutput +PageRank score. + @@ -3519,7 +4447,11 @@ will be returned. - + @@ -3529,7 +4461,11 @@ will be returned. - + @@ -3539,7 +4475,11 @@ will be returned. - + @@ -3549,7 +4489,11 @@ will be returned. - + @@ -3559,7 +4503,11 @@ will be returned. - + @@ -3574,7 +4522,11 @@ will be returned. - + @@ -3584,7 +4536,11 @@ will be returned. - + @@ -3599,7 +4555,11 @@ will be returned. - + @@ -3609,12 +4569,20 @@ will be returned. - + - + @@ -3624,12 +4592,20 @@ will be returned. - + - + @@ -3639,7 +4615,11 @@ will be returned. - + @@ -3649,7 +4629,11 @@ will be returned. - + @@ -3664,7 +4648,11 @@ will be returned. - + @@ -3674,7 +4662,11 @@ will be returned. - + @@ -3684,7 +4676,11 @@ will be returned. - + @@ -3694,12 +4690,20 @@ will be returned. - + - + @@ -3711,13 +4715,10 @@ will be returned. @@ -3745,12 +4746,20 @@ will be returned. - + - + @@ -3782,13 +4791,10 @@ will be returned. @@ -3831,7 +4837,11 @@ will be returned. - + @@ -3841,7 +4851,11 @@ will be returned. - + @@ -3851,12 +4865,20 @@ will be returned. - + - + @@ -4021,7 +5043,11 @@ will be returned. - + @@ -4031,7 +5057,11 @@ will be returned. - + @@ -4041,12 +5071,20 @@ will be returned. - + - + @@ -4071,7 +5109,11 @@ will be returned. - + @@ -4081,7 +5123,11 @@ will be returned. - + @@ -4131,12 +5177,20 @@ will be returned. - + - + @@ -4156,7 +5210,11 @@ will be returned. - + @@ -4176,7 +5234,11 @@ will be returned. - + @@ -4212,22 +5274,38 @@ will be returned. - + - + - + - + @@ -4251,42 +5329,74 @@ will be returned. - + - + - + - + - + - + - + - +
layers PathFromNode! + +Returns a view of PathFromNode containing the specified layer, errors if the layer does not exist. + +
names
excludeLayers PathFromNode! + +Return a view of PathFromNode containing all layers except the specified excluded layers, errors if any of the layers do not exist. + +
names
layer PathFromNode! + +Return a view of PathFromNode containing the layer specified layer, errors if the layer does not exist. + +
name
excludeLayer PathFromNode! + +Return a view of PathFromNode containing all layers except the specified excluded layers, errors if any of the layers do not exist. + +
name
rolling PathFromNodeWindowSet! + +Creates a WindowSet with the given window size and optional step using a rolling window. + +
window
expanding PathFromNodeWindowSet! + +Creates a WindowSet with the given step size using an expanding window. + +
step
window PathFromNode! + +Create a view of the PathFromNode including all events between a specified start (inclusive) and end (exclusive). + +
start
at PathFromNode! + +Create a view of the PathFromNode including all events at time. + +
time
snapshotLatest PathFromNode! + +Create a view of the PathFromNode including all events that are valid at the latest time. + +
snapshotAt PathFromNode! + +Create a view of the PathFromNode including all events that are valid at the specified time. + +
time
latest PathFromNode! + +Create a view of the PathFromNode including all events at the latest time. + +
before PathFromNode! + +Create a view of the PathFromNode including all events before the specified end (exclusive). + +
time
after PathFromNode! + +Create a view of the PathFromNode including all events after the specified start (exclusive). + +
time
shrinkWindow PathFromNode! + +Shrink both the start and end of the window. + +
start
shrinkStart PathFromNode! + +Set the start of the window to the larger of the specified start and self.start(). + +
start
shrinkEnd PathFromNode! + +Set the end of the window to the smaller of the specified end and self.end(). + +
end
typeFilter PathFromNode! + +Filter nodes by type. + +
nodeTypes
start Int + +Returns the earliest time that this PathFromNode is valid or None if the PathFromNode is valid for all times. + +
end Int + +Returns the latest time that this PathFromNode is valid or None if the PathFromNode is valid for all times. + +
count[Node!]! -Fetch one "page" of items, optionally offset by a specified amount. +Fetch one page with a number of items up to a specified limit, optionally offset by a specified amount. +The page_index sets the number of pages to skip (defaults to 0). -* `limit` - The size of the page (number of items to fetch). -* `offset` - The number of items to skip (defaults to 0). -* `page_index` - The number of pages (of size `limit`) to skip (defaults to 0). - -e.g. if page(5, 2, 1) is called, a page with 5 items, offset by 11 items (2 pages of 5 + 1), +For example, if page(5, 2, 1) is called, a page with 5 items, offset by 11 items (2 pages of 5 + 1), will be returned.
ids [String!]! + +Returns the node ids. + +
applyViews PathFromNode! + +Takes a specified selection of views and applies them in given order. + +
views[PathFromNode!]! -Fetch one "page" of items, optionally offset by a specified amount. - -* `limit` - The size of the page (number of items to fetch). -* `offset` - The number of items to skip (defaults to 0). -* `page_index` - The number of pages (of size `limit`) to skip (defaults to 0). +Fetch one page with a number of items up to a specified limit, optionally offset by a specified amount. +The page_index sets the number of pages to skip (defaults to 0). -e.g. if page(5, 2, 1) is called, a page with 5 items, offset by 11 items (2 pages of 5 + 1), +For example, if page(5, 2, 1) is called, a page with 5 items, offset by 11 items (2 pages of 5 + 1), will be returned.
get Property + +Get property value matching the specified key. + +
key
contains Boolean! + +Check if the key is in the properties. + +
key
keys [String!]! + +Return all property keys. + +
values [Property!]! + +Return all property values. + +
keys
get TemporalProperty + +Get property value matching the specified key. + +
key
contains Boolean! + +Check if the key is in the properties. + +
key
keys [String!]! + +Return all property keys. + +
values [TemporalProperty!]! + +Return all property values. + +
keys
key String! + +Key of a property. + +
history
values [String!]! + +Return the values of the properties. + +
at
emptySelection GqlVectorSelection! + +Returns an empty selection of documents. + +
entitiesBySimilarity GqlVectorSelection! + +Search the top scoring entities according to a specified query returning no more than a specified limit of entities. + +
query
nodesBySimilarity GqlVectorSelection! + +Search the top scoring nodes according to a specified query returning no more than a specified limit of nodes. + +
query
edgesBySimilarity GqlVectorSelection! + +Search the top scoring edges according to a specified query returning no more than a specified limit of edges. + +
query
src String! + +Source node. + +
dst String! + +Destination node. + +
layer String + +Layer. + +
metadata [PropertyInput!] + +Metadata. + +
updates
src NodeFieldFilter + +Source node. + +
dst NodeFieldFilter + +Destination node. + +
property PropertyFilterExpr + +Property. + +
metadata MetadataFilterExpr + +Metadata. + +
temporalProperty TemporalPropertyFilterExpr + +Temporal property. + +
and [EdgeFilter!] + +AND operator. + +
or [EdgeFilter!] + +OR operator. + +
not EdgeFilter + +NOT operator. + +
@@ -4305,27 +5415,47 @@ will be returned. reverse Boolean - + + +Reverse order + + src Boolean - + + +Source node + + dst Boolean - + + +Destination + + time SortByTime - + + +Time + + property String - + + +Property + + @@ -4344,77 +5474,137 @@ will be returned. defaultLayer Boolean - + + +Contains only the default layer. + + latest Boolean - + + +Latest time. + + snapshotLatest Boolean - + + +Snapshot at latest time. + + snapshotAt Int - + + +Snapshot at specified time. + + layers [String!] - + + +List of included layers. + + excludeLayers [String!] - + + +List of excluded layers. + + layer String - + + +Single included layer. + + excludeLayer String - + + +Single excluded layer. + + window Window - + + +Window between a start and end time. + + at Int - + + +View at a specified time. + + before Int - + + +View before a specified time (end exclusive). + + after Int - + + +View after a specified time (start exclusive). + + shrinkWindow Window - + + +Shrink a Window to a specified start and end time. + + shrinkStart Int - + + +Set the window start to a specified time. + + shrinkEnd Int - + + +Set the window end to a specified time. + + @@ -4433,77 +5623,137 @@ will be returned. defaultLayer Boolean - - - -latest + + +Contains only the default layer. + + + + +latest Boolean - + + +Latest time. + + snapshotLatest Boolean - + + +Snapshot at latest time. + + snapshotAt Int - + + +Snapshot at specified time. + + layers [String!] - + + +List of included layers. + + excludeLayers [String!] - + + +List of excluded layers. + + layer String - + + +Single included layer. + + excludeLayer String - + + +Single excluded layer. + + window Window - + + +Window between a start and end time. + + at Int - + + +View at a specified time. + + before Int - + + +View before a specified time (end exclusive). + + after Int - + + +View after a specified time (start exclusive). + + shrinkWindow Window - + + +Shrink a Window to a specified start and end time. + + shrinkStart Int - + + +Set the window start to a specified time. + + shrinkEnd Int - + + +Set the window end to a specified time. + + @@ -4522,107 +5772,191 @@ will be returned. defaultLayer Boolean - + + +Contains only the default layer. + + layers [String!] - + + +List of included layers. + + excludeLayers [String!] - + + +List of excluded layers. + + layer String - + + +Single included layer. + + excludeLayer String - + + +Single excluded layer. + + subgraph [String!] - + + +Subgraph nodes. + + subgraphNodeTypes [String!] - + + +Subgraph node types. + + excludeNodes [String!] - + + +List of excluded nodes. + + valid Boolean - + + +Valid state. + + window Window - + + +Window between a start and end time. + + at Int - + + +View at a specified time. + + latest Boolean - + + +View at the latest time. + + snapshotAt Int - + + +Snapshot at specified time. + + snapshotLatest Boolean - + + +Snapshot at latest time. + + before Int - + + +View before a specified time (end exclusive). + + after Int - + + +View after a specified time (start exclusive). + + shrinkWindow Window - + + +Shrink a Window to a specified start and end time. + + shrinkStart Int - + + +Set the window start to a specified time. + + shrinkEnd Int - + + +Set the window end to a specified time. + + nodeFilter NodeFilter - + + +Node filter. + + edgeFilter EdgeFilter - + + +Edge filter. + + @@ -4641,12 +5975,20 @@ will be returned. nodeProps PropsInput! - + + +Node properties. + + edgeProps PropsInput! - + + +Edge properties. + + @@ -4665,12 +6007,20 @@ will be returned. src String! - + + +Source node. + + dst String! - + + +Destination node. + + @@ -4689,17 +6039,29 @@ will be returned. name String! - + + +Node metadata to compare against. + + operator Operator! - + + +Operator. + + value Value - + + +Value. + + @@ -4718,22 +6080,38 @@ will be returned. name String! - + + +Name. + + nodeType String - + + +Node type. + + metadata [PropertyInput!] - + + +Metadata. + + updates [TemporalPropertyInput!] - + + +Updates. + + @@ -4752,17 +6130,29 @@ will be returned. field NodeField! - + + +Node component to compare against. + + operator Operator! - + + +Operator filter. + + value Value! - + + +Value filter. + + @@ -4781,37 +6171,65 @@ will be returned. node NodeFieldFilter - - - -property + + +Node filter. + + + + +property PropertyFilterExpr - + + +Property filter. + + metadata MetadataFilterExpr - + + +Metadata filter. + + temporalProperty TemporalPropertyFilterExpr - + + +Temporal property filter. + + and [NodeFilter!] - + + +AND operator. + + or [NodeFilter!] - + + +OR operator. + + not NodeFilter - + + +NOT operator. + + @@ -4830,22 +6248,38 @@ will be returned. reverse Boolean - + + +Reverse order + + id Boolean - + + +Unique Id + + time SortByTime - + + +Time + + property String - + + +Property + + @@ -4864,82 +6298,146 @@ will be returned. defaultLayer Boolean - + + +Contains only the default layer. + + latest Boolean - + + +View at the latest time. + + snapshotLatest Boolean - + + +Snapshot at latest time. + + snapshotAt Int - + + +Snapshot at specified time. + + layers [String!] - + + +List of included layers. + + excludeLayers [String!] - + + +List of excluded layers. + + layer String - + + +Single included layer. + + excludeLayer String - + + +Single excluded layer. + + window Window - + + +Window between a start and end time. + + at Int - + + +View at a specified time. + + before Int - + + +View before a specified time (end exclusive). + + after Int - + + +View after a specified time (start exclusive). + + shrinkWindow Window - + + +Shrink a Window to a specified start and end time. + + shrinkStart Int - + + +Set the window start to a specified time. + + shrinkEnd Int - + + +Set the window end to a specified time. + + nodeFilter NodeFilter - + + +Node filter. + + @@ -4958,87 +6456,155 @@ will be returned. defaultLayer Boolean - + + +Contains only the default layer. + + latest Boolean - + + +View at the latest time. + + snapshotLatest Boolean - + + +Snapshot at latest time. + + layers [String!] - + + +List of included layers. + + excludeLayers [String!] - + + +List of excluded layers. + + layer String - + + +Single included layer. + + excludeLayer String - + + +Single excluded layer. + + window Window - + + +Window between a start and end time. + + at Int - + + +View at a specified time. + + snapshotAt Int - + + +Snapshot at specified time. + + before Int - + + +View before a specified time (end exclusive). + + after Int - + + +View after a specified time (start exclusive). + + shrinkWindow Window - + + +Shrink a Window to a specified start and end time. + + shrinkStart Int - + + +Set the window start to a specified time. + + shrinkEnd Int - + + +Set the window end to a specified time. + + nodeFilter NodeFilter - + + +Node filter. + + typeFilter [String!] - + + +List of types. + + @@ -5057,12 +6623,20 @@ will be returned. key String! - + + +Key. + + value Value! - + + +Value. + + @@ -5081,72 +6655,128 @@ will be returned. latest Boolean - + + +Latest time. + + snapshotLatest Boolean - + + +Latest snapshot. + + snapshotAt Int - + + +Time. + + layers [String!] - + + +List of layers. + + excludeLayers [String!] - + + +List of excluded layers. + + layer String - + + +Single layer. + + excludeLayer String - + + +Single layer to exclude. + + window Window - + + +Window between a start and end time. + + at Int - + + +View at a specified time. + + before Int - + + +View before a specified time (end exclusive). + + after Int - + + +View after a specified time (start exclusive). + + shrinkWindow Window - + + +Shrink a Window to a specified start and end time. + + shrinkStart Int - + + +Set the window start to a specified time. + + shrinkEnd Int - + + +Set the window end to a specified time. + + @@ -5165,17 +6795,29 @@ will be returned. name String! - + + +Node property to compare against. + + operator Operator! - + + +Operator. + + value Value - + + +Value. + + @@ -5194,12 +6836,20 @@ will be returned. key String! - + + +Key. + + value Value! - + + +Value. + + @@ -5218,12 +6868,20 @@ will be returned. all AllPropertySpec - + + +All properties and metadata. + + some SomePropertySpec - + + +Some properties and metadata. + + @@ -5242,12 +6900,20 @@ will be returned. metadata [String!]! - + + +List of metadata. + + properties [String!]! - + + +List of properties. + + @@ -5266,22 +6932,38 @@ will be returned. name String! - + + +Name. + + temporal TemporalType! - + + +Type of temporal property. Choose from: any, latest. + + operator Operator! - + + +Operator. + + value Value - + + +Value. + + @@ -5300,12 +6982,20 @@ will be returned. time Int! - + + +Time. + + properties [PropertyInput!] - + + +Properties. + + @@ -5324,37 +7014,65 @@ will be returned. u64 Int - + + +64 bit unsigned integer. + + i64 Int - + + +64 bit signed integer. + + f64 Float - + + +64 bit float. + + str String - + + +String. + + bool Boolean - + + +Boolean. + + list [Value!] - + + +List. + + object [ObjectEntry!] - + + +Object. + + @@ -5373,12 +7091,20 @@ will be returned. start Int! - + + +Start time. + + end Int! - + + +End time. + + @@ -5397,12 +7123,22 @@ will be returned. duration String - + + +Duration of window period. + +Choose from: + + epoch Int - + + +Time. + + @@ -5421,15 +7157,27 @@ will be returned. ALL - + + +All properties and metadata. + + ALL_METADATA - + + +All metadata. + + ALL_PROPERTIES - + + +All properties. + + @@ -5446,11 +7194,19 @@ will be returned. PERSISTENT - + + +Persistent. + + EVENT - + + +Event. + + @@ -5467,11 +7223,19 @@ will be returned. NODE_NAME - + + +Node name. + + NODE_TYPE - + + +Node type. + + @@ -5488,51 +7252,99 @@ will be returned. EQUAL - + + +Equality operator. + + NOT_EQUAL - + + +Inequality operator. + + GREATER_THAN_OR_EQUAL - + + +Greater Than Or Equal operator. + + LESS_THAN_OR_EQUAL - + + +Less Than Or Equal operator. + + GREATER_THAN - + + +Greater Than operator. + + LESS_THAN - + + +Less Than operator. + + IS_NONE - + + +Is None operator. + + IS_SOME - + + +Is Some operator. + + IS_IN - + + +Is In operator. + + IS_NOT_IN - + + +Is Not In operator. + + CONTAINS - + + +Contains operator. + + NOT_CONTAINS - + + +Not Contains operator. + + @@ -5549,11 +7361,19 @@ will be returned. LATEST - + + +Latest time + + EARLIEST - + + +Earliest time + + @@ -5570,11 +7390,19 @@ will be returned. ANY - + + +Any. + + LATEST - + + +Latest. + + @@ -5606,6 +7434,8 @@ The `String` scalar type represents textual data, represented as UTF-8 character ### DocumentEntity +Entity associated with document. + @@ -5616,11 +7446,19 @@ The `String` scalar type represents textual data, represented as UTF-8 character - + - +
Node + +Raphtory graph node. + +
Edge + +Raphtory graph edge. + +
diff --git a/docs/user-guide/algorithms/4_view-algorithms.md b/docs/user-guide/algorithms/4_view-algorithms.md index 84bfc43e56..40ae2bbff6 100644 --- a/docs/user-guide/algorithms/4_view-algorithms.md +++ b/docs/user-guide/algorithms/4_view-algorithms.md @@ -1,6 +1,6 @@ -# Running algorithms on graph views +# Running algorithms on GraphViews -Both `graphwide` and `node centric` algorithms can be run on `graph views`. This allows us to see how results change over time, run algorithms on subsets of the layers, or remove specific nodes from the graph to see the impact this has. +Both `graphwide` and `node centric` algorithms can be run on `GraphViews`. This allows us to see how results change over time, run algorithms on subsets of the layers, or remove specific nodes from the graph to see the impact this has. To demonstrate this, the following example shows how you could track Gandalf's importance over the course of the story using rolling windows and the `PageRank` algorithm. diff --git a/docs/user-guide/algorithms/5_community_detection.md b/docs/user-guide/algorithms/5_community_detection.md new file mode 100644 index 0000000000..56d52bbcbc --- /dev/null +++ b/docs/user-guide/algorithms/5_community_detection.md @@ -0,0 +1,123 @@ +# Community detection + +One important feature of graphs is the degree of clustering and presence of community structures. Groups of nodes that are densely connected amongst members of the group but have comparatively few connections with the rest of the graph can be considered distinct communities. + +Identifying clusters can be informative in social, biological and technological networks. For example, identifying clusters in web clients accessing a site can help optimise performance using a CDN or spotting changes in the communities amongst a baboon pack over time might inform theories about group dynamics. Raphtory provides a variety of algorithms to analyse community structures in your graphs. + +## Exploring Zachary's karate club network + +As an example, we use a data set from the paper "An Information Flow Model for Conflict and Fission in Small Groups" by Wayne W. Zachary which captures social links between the 34 members of the club. + +### Ingest data + +First set up imports and ingest the data using NetworkX and Pandas to handle the `karate.gml` file. + +/// tab | :fontawesome-brands-python: Python +```python +from raphtory import Graph +import pandas as pd +import networkx as nx +from raphtory import graphql +from raphtory import algorithms as rp +import matplotlib.pyplot as plt + +# Load the CSV file using NetworkX +edges_df = pd.read_csv("../data/karate.csv") +edges_df["time"] = pd.to_datetime(edges_df["time"]) + +print(edges_df.head()) +``` +/// + +You should see the dummy timestamps have been added in the head output. + +!!! Output + + ```output + source target time + 0 1 2 2023-01-01 + 1 1 3 2023-01-01 + 2 1 4 2023-01-01 + 3 1 5 2023-01-01 + 4 1 6 2023-01-01 + ``` + +### Create the graph + +The dataframe can then be used to create a Raphtory graph: + +/// tab | :fontawesome-brands-python: Python +```{.python continuation} +G = Graph() + +G.load_edges_from_pandas( + df=edges_df, + src="source", + dst="target", + time="time", +) +``` +/// + +### Analyse the clustering of club members + +Raphtory provides multiple algorithms to perform community detection, including the following: + +- [Louvain][raphtory.algorithms.louvain] - a commonly used and well understood modularity based algorithm. +- [Label propagation][raphtory.algorithms.label_propagation] - a more efficient cluster detection algorithm when used at scale. + +Here we use the [Louvain][raphtory.algorithms.louvain] algorithm to identify distinct clusters of nodes. + +/// tab | :fontawesome-brands-python: Python +```{.python continuation} +clustering = rp.louvain(G) + +# Extract unique cluster values +unique_clusters = {cluster for node, cluster in clustering.items()} +print("Number of unique clusters:", len(unique_clusters)) +``` +/// + +!!! Output + + ```output + Number of unique clusters: 4 + ``` + +```{.python continuation hide} +assert len(unique_clusters) == 4 +``` + +The algorithm identifies four clusters of nodes which could be interpreted as four social groups amongst the students. + +### Explore the data + +You can explore the results of our cluster detection algorithm in greater detail using the Raphtory UI. + +To do this assign a type to nodes of each cluster and start the Raphtory server. Each unique node type will be assigned a colour in the **Graph canvas** so that you can distinguish them visually. + +/// tab | :fontawesome-brands-python: Python +```{.python continuation} + +# Check value of cluster for each node and add to corresponding cluster list +labels = {0:'Cobra Kai',1:'Miyagi-Do',2:'Polarslaget',3:'Redentores'} +for node, cluster in clustering.items(): + G.node(node).set_node_type(labels[cluster]) + +# Start a Raphtory server and send the karate graph +server = graphql.GraphServer(".idea/my-test/graphs") +client = server.start().get_client() +client.send_graph("cluster-graph", G, overwrite=True) + +``` +/// + +```{.python continuation hide} +assert len(G.get_all_node_types()) == 4 +``` + +You should see that there are four distinct communities, for each node you can see it's node type in the **Node Statistics** panel of the **Selected** menu and by visual inspection verify that each node is connected mostly to it's own group. + +You may also spot other features that could be investigated further, for example the nodes with the highest degree are members of the 'Miyagi-Do' cluster. + +![UI Search page](../../assets/images/raphtory_com_detection_ui.png) \ No newline at end of file diff --git a/docs/user-guide/export/2_dataframes.md b/docs/user-guide/export/2_dataframes.md index 3ebacbde64..fa70d78379 100644 --- a/docs/user-guide/export/2_dataframes.md +++ b/docs/user-guide/export/2_dataframes.md @@ -124,7 +124,7 @@ export the property history for each edge, split by edge layer. This is because explode the edges and view each update individually (which will then ignore the `include_property_history` flag). In the below example we first create a subgraph of the monkey interactions, selecting `ANGELE` and `FELIPE` as the -monkeys we are interested in. This isn't a required step, but helps to demonstrate the export of graph views. +monkeys we are interested in. This isn't a required step, but helps to demonstrate the export of GraphViews. Then we call `to_df()` on the subgraph edges, setting no flags. In the output you can see the property history for each interaction type (layer) between `ANGELE` and `FELIPE`. diff --git a/docs/user-guide/getting-started/1_intro.md b/docs/user-guide/getting-started/1_intro.md index 66680ebc47..a08719a588 100644 --- a/docs/user-guide/getting-started/1_intro.md +++ b/docs/user-guide/getting-started/1_intro.md @@ -113,6 +113,9 @@ client.send_graph("OBS-graph", g, overwrite=True) This will start the UI locally on the default port `1736`, you should see **Search** page by default. +!!! Note + You can also start a standalone server using the Raphtory CLI tool or Docker image. + ![UI Search page](../../assets/images/raphtory_ui_search_empty.png) You can use the **Query Builder** to select the graph you created and identify which baboons attacked each other in the last month. diff --git a/docs/user-guide/getting-started/3_cli.md b/docs/user-guide/getting-started/3_cli.md index f96e50aba9..b7600c5522 100644 --- a/docs/user-guide/getting-started/3_cli.md +++ b/docs/user-guide/getting-started/3_cli.md @@ -23,6 +23,7 @@ raphtory server --port 1736 | Command | Parameter(s) | Description | |-----------------------------|---------------------------|---------------------------------------------------------------------| | -h, --help | | Show the help message and exit | +| --work-dir | WORK_DIR | Working directory | | --cache-capacity | CACHE_CAPACITY | Cache capacity | | --cache-tti-seconds | CACHE_TTI_SECONDS | Cache time-to-idle in seconds | | --log-level | LOG_LEVEL | Log level | diff --git a/docs/user-guide/graphql/1_intro.md b/docs/user-guide/graphql/1_intro.md index f8c750b61f..24aa4a85e8 100644 --- a/docs/user-guide/graphql/1_intro.md +++ b/docs/user-guide/graphql/1_intro.md @@ -2,6 +2,6 @@ [GraphQL](https://graphql.org/) is a query language for your API, and a server-side runtime for executing queries using a type system you define for your data. Using GraphQL can help you can reduce over-fetching and under-fetching of data compared to other REST APIs. -It is possible to query Raphtory graphs in GraphQL. The GraphQL server provides an IDE available at `localhost:1736/playground` where you can write GraphQL queries. Alternatively, you can write all your GraphQL queries in Python and easily update, send and receive Raphtory graphs from the GraphQL server. +It is possible to query and modify Raphtory graphs in GraphQL. The GraphQL server provides an IDE available at `localhost:1736/playground` where you can write GraphQL queries. Alternatively, you can write all your GraphQL queries in Python and easily update, send and receive Raphtory graphs from the GraphQL server. This section will show you how to start a GraphQL server and run your own queries on your data. diff --git a/docs/user-guide/graphql/2_run-server.md b/docs/user-guide/graphql/2_run-server.md index a4e700e2ea..dca048d36d 100644 --- a/docs/user-guide/graphql/2_run-server.md +++ b/docs/user-guide/graphql/2_run-server.md @@ -23,38 +23,25 @@ g.save_to_file(working_dir + "your_graph") ``` /// -## Starting a server with .run() +## Starting a server -To run the GraphQL server with `.run()`, create a python file `run_server.py` with the following code: +You can start the raphtory GraphQL in multiple ways depending on your usecase. -/// tab | :fontawesome-brands-python: Python -```{.python notest} -from raphtory import graphql - -import argparse -parser = argparse.ArgumentParser(description="For passing the working_dir") -parser.add_argument( - "--working_dir", - type=str, - help="path for the working directory of the raphtory server", -) -args = parser.parse_args() +### Using the CLI -server = graphql.GraphServer(args.working_dir) +You can use the [Raphtory CLI](../getting-started/3_cli.md) with the `server` command by running: -server.run() +```sh +raphtory server --port 1736 ``` -/// -To run the server: +This option is the simplist and provides the most configuration options. -```bash -python run_server.py --working_dir ../your_working_dir -``` +### Start a server in Python -## Starting a server with .start() +If you have a [`GraphServer`][raphtory.graphql.GraphServer] object you can use either the [`.run()`][raphtory.graphql.GraphServer.run] or [`.start()`][raphtory.graphql.GraphServer.start] functions to start a GraphQL sever and Raphtory UI. -It is also possible to start the server in Python with `.start()`. Below is an example of how to start the server and send a Raphtory graph to the server, where `new_graph` is your Raphtory graph object. +Below is an example of how to start the server and send a Raphtory graph to the server, where `new_graph` is your Raphtory graph object. /// tab | :fontawesome-brands-python: Python ```{.python notest} diff --git a/docs/user-guide/graphql/3_writing-queries.md b/docs/user-guide/graphql/3_writing-queries.md index c1ef906cb4..809c477197 100644 --- a/docs/user-guide/graphql/3_writing-queries.md +++ b/docs/user-guide/graphql/3_writing-queries.md @@ -1,12 +1,20 @@ -# Writing Raphtory queries in GraphQL +# Making GraphQL requests + +The GraphQL API largely follows the same patterns as the Python API but has a few key differences. + +In GraphQL, there are two different types of requests: a query to search through your data or a mutation of your data. Only the top-level fields in mutation operations are allowed to cause side effects. To accommodate this, in the Raphtory API you can make queries to graphs or metagraphs but must make changes using a mutable graph, node or edge. + +This division means that the distinction between Graphs and GraphViews is less important in GraphQL and all non-mutable Graph endpoints are GraphViews while MutableGraphs are used for mutation operations. This is also true for Nodes and Edges and their respective views. Graphs can be further distinguished as either `PERSISTENT` or `EVENT` types. + +## Graphical playground When you start a GraphQL server, you can find your GraphQL UI in the browser at `localhost:1736/playground` or an alternative port if you specified one. -The schema for the queries can be found on the right hand side in a pull out toggle. +An annotated schema is available from the documentation tab in the left hand menu of the playground. -![alt text](schema.png) +![alt text](../../assets/images/raphtory_ui_playground_docs.png) -## Example Queries in GraphQL +## Query a graph Here are some example queries to get you started: @@ -26,7 +34,7 @@ query { ``` /// -## List of all the edges, with specific node properties +### List of all the edges, with specific node properties To find nodes with `age`: @@ -59,7 +67,7 @@ query { ``` /// -This will return something like this: +This will return something like: !!! Output @@ -120,9 +128,163 @@ g.node("Ben").properties.get("age") ``` /// -## Querying GraphQL in Python +### Examine the metadata of a node + +Metadata does not change over the lifetime of an object. You can request it with a query like the following: + + +/// tab | ![GraphQL](https://img.icons8.com/ios-filled/15/graphql.png) GraphQL +``` +{ + graph(path: "traffic_graph") { + nodes { + list { + name + metadata { + values { + key + value + } + } + } + } + } +} +``` +/// + +Which will return something like: + +!!! Output + ```json + { + "data": { + "graph": { + "nodes": { + "list": [ + { + "name": "ServerA", + "metadata": { + "values": [ + { + "key": "datasource", + "value": "network_traffic_edges.csv" + }, + { + "key": "server_name", + "value": "Alpha" + }, + { + "key": "hardware_type", + "value": "Blade Server" + } + ] + } + }, + { + "name": "ServerB", + "metadata": { + "values": [ + { + "key": "datasource", + "value": "network_traffic_edges.csv" + }, + { + "key": "server_name", + "value": "Beta" + }, + { + "key": "hardware_type", + "value": "Rack Server" + } + ] + } + } + ] + } + } + } + } + ``` + +### Examine the properties of a node + +Properties can change over time so it is often useful to make a query for a specific time or window. + +/// tab | ![GraphQL](https://img.icons8.com/ios-filled/15/graphql.png) GraphQL +``` +{ + graph(path: "traffic_graph") { + at(time: 1693555500000) { + nodes { + list { + name + properties { + values { + key + value + } + } + } + } + } + } +} +``` +/// + +Which will return something like: -It is possible to send GraphQL queries in Python without the in-browser IDE. This can be useful if you want to update your Raphtory graph in Python. This example shows you how to do this with the Raphtory client: +!!! Output + ```json + { + "data": { + "graph": { + "at": { + "nodes": { + "list": [ + { + "name": "ServerA", + "properties": { + "values": [] + } + }, + { + "name": "ServerB", + "properties": { + "values": [ + { + "key": "OS_version", + "value": "Red Hat 8.1" + }, + { + "key": "primary_function", + "value": "Web Server" + }, + { + "key": "uptime_days", + "value": 45 + } + ] + } + }, + { + "name": "ServerC", + "properties": { + "values": [] + } + } + ] + } + } + } + } + } + ``` + +### Querying GraphQL in Python + +You can also send GraphQL queries in Python directl using the [`.query()`][raphtory.graphql.RaphtoryClient.query] function on a `RaphtoryClient`. The following example shows you how to do this: /// tab | :fontawesome-brands-python: Python ```{.python notest} @@ -144,40 +306,16 @@ Pass your graph object string into the `client.query()` method to execute the Gr {'graph': {'created': 1729075008085, 'lastOpened': 1729075036222, 'lastUpdated': 1729075008085}} ``` +## Mutation requests -## Mutation Queries - -In GraphQL, you can write two different types of queries - a query to search through your data or a query that mutates your data. +You can also mutate your graph. This can be done both in the GraphQL IDE and in Python. -The examples in the previous section are all queries used to search through your data. However in our API, you can also mutate your graph. This can be done both in the GraphQL IDE and in Python. - -The schema in the GraphQL IDE shows how you can mutate the graph within the IDE: - -``` -type MutRoot { - plugins: MutationPlugin! - deleteGraph(path: String!): Boolean! - newGraph(path: String!, graphType: GqlGraphType!): Boolean! - moveGraph(path: String!, newPath: String!): Boolean! - copyGraph(path: String!, newPath: String!): Boolean! - - # Use GQL multipart upload to send new graphs to server - # - # Returns:: - # name of the new graph - uploadGraph(path: String!, graph: Upload!, overwrite: Boolean!): String! - - # Send graph bincode as base64 encoded string - # - # Returns:: - # path of the new graph - sendGraph(path: String!, graph: String!, overwrite: Boolean!): String! -} -``` +From GraphQL these operations are available from the [Mutation root](../../../reference/graphql/graphql_API/#mutation-mutroot) which operates on mutable objects by specified by a path. -There are additional methods to mutate the graph exclusive to Python such as sending, receiving and updating a graph, these will all be explained below. +!!! note + Some methods to mutate the graph are exclusive to Python. -## Sending a graph +### Sending a graph You can send a graph to the server and overwrite an existing graph if needed. @@ -223,7 +361,7 @@ This should return: } ``` -## Receiving graphs +### Receiving graphs You can retrieve graphs from a "path" on the server which returns a Python Raphtory graph object. @@ -234,7 +372,7 @@ g.edge("sally", "tony") ``` /// -## Creating a new graph +### Creating a new graph This is an example of how to create a new graph in the server. @@ -272,7 +410,7 @@ The returning result to confirm that a new graph has been created: } ``` -## Moving a graph +### Moving a graph It is possible to move a graph to a new path on the server. @@ -307,7 +445,7 @@ The returning GraphQL result to confirm that the graph has been moved: } ``` -## Copying a graph +### Copying a graph It is possible to copy a graph to a new path on the server. @@ -342,7 +480,7 @@ The returning GraphQL result to confirm that the graph has been copied: } ``` -## Deleting a graph +### Deleting a graph It is possible to delete a graph on the server. @@ -377,7 +515,7 @@ The returning GraphQL result to confirm that the graph has been deleted: } ``` -## Updating the graph +### Updating the graph It is possible to update the graph using the `remote_graph()` method. diff --git a/docs/user-guide/graphql/4_running-ui.md b/docs/user-guide/graphql/4_running-ui.md deleted file mode 100644 index fe434cf082..0000000000 --- a/docs/user-guide/graphql/4_running-ui.md +++ /dev/null @@ -1,17 +0,0 @@ -# Running the UI - -Raphtory allows you to easily set up a sophisticated UI to examine and analyze your data. - -To run the UI, you will first need to have the [GraphQL server](../../user-guide/graphql/2_run-server.md) running. Once the server is running, the UI will be available on the same port. - -## Search page - -The search page of the UI is used to search and filter your data. For example, you can narrow your search by date, graph name and even node properties. You can view a node's direct connections as well as its activity log and history. By double clicking on one of the results, you can navigate to the graph page where you will see a graphical representation of your data. - -![alt text](search_page.png) - -## Graph page - -The graph page of the UI is used to explore your data and graph in an interactive way. Here you will be able to see all your nodes and edges in a clear format, with the ability to delete, expand and so much more. - -![alt text](graph_page.png) diff --git a/docs/user-guide/ingestion/2_direct-updates.md b/docs/user-guide/ingestion/2_direct-updates.md index cd8698a62c..d0a628bcb5 100644 --- a/docs/user-guide/ingestion/2_direct-updates.md +++ b/docs/user-guide/ingestion/2_direct-updates.md @@ -325,7 +325,7 @@ separately or merged with other layers as required. You can see this in the example below where we add five updates between `Person 1` and `Person 2` across the layers `Friends`, `Co Workers` and `Family`. When we query the history of the `weight` property on the edge we initially get -all of the values back. However, by applying the [`layers()` graph view](../views/3_layer.md) we can return only updates +all of the values back. However, by applying the [`layers()` GraphView](../views/3_layer.md) we can return only updates from `Co Workers` and `Family`. /// tab | :fontawesome-brands-python: Python diff --git a/docs/user-guide/installation.md b/docs/user-guide/installation.md index 629bde1606..09b32f7efa 100644 --- a/docs/user-guide/installation.md +++ b/docs/user-guide/installation.md @@ -22,7 +22,6 @@ raphtory = { version = "x"} To use the library import it into your project: - /// tab | :fontawesome-brands-python: Python ``` python import raphtory as rp @@ -34,3 +33,33 @@ import raphtory as rp use raphtory::prelude::*; ``` /// + +## Docker image + +Both the Python and Rust packages are available as official Docker images from the [Pometry Docker Hub](https://hub.docker.com/r/pometry/raphtory) page. + +To download these using the docker CLI run: + +/// tab | :fontawesome-brands-python: Python +``` bash +docker pull pometry/raphtory:latest-python +``` +/// + +/// tab | :fontawesome-brands-rust: Rust +``` shell +docker pull pometry/raphtory +``` +/// + +Running either container will start a Raphtory server by default, if this is all you need then the Rust image is sufficient. + +However, the Python image contains the Raphtory Python package and all the required dependencies. You should use this image if you want to develop using the Python APIs in a containerised environment. + +You can run a Raphtory container with the following Docker command: + +```docker +docker run --rm -p 1736:1736 -v "$(pwd):/home/raphtory_server" pometry/raphtory:latest-python +``` + +For more information about running and configuring containers see the [Docker documentation](https://docs.docker.com/). diff --git a/docs/user-guide/troubleshooting.md b/docs/user-guide/troubleshooting.md new file mode 100644 index 0000000000..f689971fc2 --- /dev/null +++ b/docs/user-guide/troubleshooting.md @@ -0,0 +1,11 @@ +# Troubleshooting + +This page covers common errors and misconfigurations in Raphtory. + +## Specifying time measurements + +Internally all times in Raphtory are represented as milliseconds using unix epochs. When ingesting data you will need to convert your raw data into the appropriate format. Similarly queries made using the API should use timestamps relative to the unix epoch in milliseconds. + +## Docker graph storage + +When saving a graph to disk in the official Docker container, the default location is `/home/raphtory_server`. This is where the Raphtory server will look for graphs unless you specify an alternative working directory. When saving a file or sending a graph to the server you can always specify a custom path. diff --git a/docs/user-guide/views/1_intro.md b/docs/user-guide/views/1_intro.md index d61dfe2906..16f0ac8989 100644 --- a/docs/user-guide/views/1_intro.md +++ b/docs/user-guide/views/1_intro.md @@ -1,8 +1,8 @@ # Introduction and dataset -Many operations are executed on the whole graph, including the full history. In this section we will look at applying `Graph Views` which provide a way to look at a subset of this data without having to re-ingest it. +Many operations are executed on the whole graph, including the full history. In this section we will look at applying `GraphViews` which provide a way to look at a subset of this data without having to re-ingest it. -Raphtory can maintain hundreds of thousands of `Graph Views` in parallel and allows chaining view functions together to create as specific a filter as is required for your use case. A unified API means that all functions that can be called on a graph, node or edge can also be applied to this subset. +Raphtory can maintain hundreds of thousands of `GraphViews` in parallel and allows chaining view functions together to create as specific a filter as is required for your use case. A unified API means that all functions that can be called on a graph, node or edge can also be applied to this subset. !!! info diff --git a/docs/user-guide/views/2_time.md b/docs/user-guide/views/2_time.md index da464e3e95..4489289f30 100644 --- a/docs/user-guide/views/2_time.md +++ b/docs/user-guide/views/2_time.md @@ -1,7 +1,7 @@ # Querying the graph over time Raphtory provides six functions: `before()`, `at()`, `after()`, `window()`, `expand()` and `rolling()` for traveling through time and viewing a graph as it was at a specific point, or between two points (applying a time window). -All of these functions can be called on a `graph`, `node`, or `edge`, returning an equivalent `Graph View`, `Node View` or `Edge View` which have all the same functions as its unfiltered counterpart. This means that if you write a function which takes a Raphtory entity, regardless of which filters have been applied. +All of these functions can be called on a `graph`, `node`, or `edge`, returning an equivalent `GraphView`, `NodeView` or `EdgeView` which have all the same functions as its unfiltered counterpart. This means that if you write a function which takes a Raphtory entity, regardless of which filters have been applied. ## Before, At and After @@ -129,15 +129,16 @@ assert str(f"Window start: {w.start_date_time}, First update: {w.earliest_date_t ``` ## Traversing the graph with views + There are important differences when applying views depending on which object you call them on because of how filters propagate as you traverse the graph. - -As a general rule, when you call any function which returns another entity, on a `Graph View`, `Node View` or `Edge View`, the view's filters will be passed onto the entities it returns. For example, if you call `before()` on a graph and then call `node()`, this will return a `Node View` filtered to the time passed to the graph. + +As a general rule, when you call any function which returns another entity, on a `GraphView`, `NodeView` or `EdgeView`, the view's filters will be passed onto the entities it returns. For example, if you call `before()` on a graph and then call `node()`, this will return a `NodeView` filtered to the time passed to the graph. However, if this was always the case it would be limiting if you later wanted to explore outside of these bounds. To allow for both global bounds and moving bounds, if a filter is applied onto the graph, all entities extracted always have this filter applied. However, if a filter is applied to either a `node` or an `edge`, once you have traversed to a new neighbouring `node` this filter is removed. -As an example of this, below we look at LOME's one hop neighbours before the 20th of June and their neighbours (LOME's two hop neighbours) after the 25th of June. +As an example of this, below we look at LOME's one hop neighbours before the 20th of June and their neighbours (LOME's two hop neighbours) after the 25th of June. First we show calling `before()` on the `graph`. This works for the one hop neighbours, but when `after()` is applied the graph is empty as there is no overlap in dates between the two filters. Next we show calling `before()` on the `node` instead. In this case, once the neighbours have been reached the original filter is removed which allows `after()` to work as desired. diff --git a/examples/netflow/Cargo.toml b/examples/netflow/Cargo.toml index b3843ecb2d..e84a720bc2 100644 --- a/examples/netflow/Cargo.toml +++ b/examples/netflow/Cargo.toml @@ -6,8 +6,8 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -raphtory_core = { path = "../../raphtory", version = "0.16.0", features = ["python", "search", "vectors"], package = "raphtory" } -raphtory-graphql = { path = "../../raphtory-graphql", version = "0.16.0",features = ["python"] } +raphtory = { workspace = true, features = ["python", "search", "vectors"] } +raphtory-graphql = { workspace = true, features = ["python"] } pyo3 = { workspace = true } [lib] diff --git a/examples/netflow/src/lib.rs b/examples/netflow/src/lib.rs index 322a65af36..58d96bc0eb 100644 --- a/examples/netflow/src/lib.rs +++ b/examples/netflow/src/lib.rs @@ -1,12 +1,14 @@ mod netflow_one_path_node; use crate::netflow_one_path_node::netflow_one_path_node; -use ::raphtory_core::python::packages::base_modules::{ - add_raphtory_classes, base_algorithm_module, base_graph_gen_module, base_graph_loader_module, - base_vectors_module, -}; use pyo3::prelude::*; -use raphtory_core::db::api::view::DynamicGraph; +use raphtory::{ + db::api::view::DynamicGraph, + python::packages::base_modules::{ + add_raphtory_classes, base_algorithm_module, base_graph_gen_module, + base_graph_loader_module, base_vectors_module, + }, +}; use raphtory_graphql::python::pymodule::base_graphql_module; #[pyfunction(name = "netflow_one_path_node")] diff --git a/examples/netflow/src/netflow_one_path_node.rs b/examples/netflow/src/netflow_one_path_node.rs index ff2febf35a..57171e98e1 100644 --- a/examples/netflow/src/netflow_one_path_node.rs +++ b/examples/netflow/src/netflow_one_path_node.rs @@ -1,4 +1,4 @@ -use raphtory_core::{ +use raphtory::{ core::state::{ accumulator_id::accumulators::sum, compute_state::{ComputeState, ComputeStateVec}, @@ -135,7 +135,7 @@ pub fn netflow_one_path_node( #[cfg(test)] mod one_path_test { use super::*; - use raphtory_core::{ + use raphtory::{ db::{api::mutation::AdditionOps, graph::graph::Graph}, prelude::{Prop, NO_PROPS}, }; diff --git a/graphql-bench/.gitignore b/graphql-bench/.gitignore new file mode 100644 index 0000000000..074e8e7140 --- /dev/null +++ b/graphql-bench/.gitignore @@ -0,0 +1,21 @@ +report.html +summary.json +src/__generated +breakpoints.csv +breakpoints +key.json +output.json +.terraform +results +reports +terraform.tfstate +terraform.tfstate.backup +.virtual_documents +data/apache +node_modules +dist +output.csv.gz +output.json + +!data/apache/master/graph.tar.xz +!data/apache/master/.raph diff --git a/graphql-bench/Makefile b/graphql-bench/Makefile new file mode 100644 index 0000000000..4d9277e3d4 --- /dev/null +++ b/graphql-bench/Makefile @@ -0,0 +1,108 @@ +RAPHTORY_COMMIT := $(shell git rev-parse HEAD) +CURRENT_TIME := $(shell date +"%Y-%m-%dT%H-%M-%S") + +K6_IP=$(shell terraform output k6_ip | jq -r '.') +RAPHTORY_IP=$(shell terraform output raphtory_ip | jq -r '.') + +data/apache/master/graph: + @echo "Unzipping apache master graph" + @cd data/apache/master && tar -Jxf graph.tar.xz -C . + +bench-local: data/apache/master/graph + pnpm install --frozen-lockfile + pnpm build + pnpm concurrently --raw --kill-others --names 'raphtory,bench' 'python server.py' 'sleep 10 && k6 run --out csv=output.csv.gz dist/bench.js' || : + python process-k6-output.py + +# call this like: make bench-e2-standard-16 +bench-%: + TF_VAR_machine_type=$* make bench + +bench: deploy push-graphs build-raphtory + make run-raphtory > /tmp/raphtory-server-logs & + make run-bench + make destroy + +project: + gcloud config set project $(PROJECT) + +auth: project + gcloud auth login + gcloud auth configure-docker europe-west2-docker.pkg.dev + gcloud artifacts locations list + +deploy: + # export $$(grep -v '^#' .env | xargs) && terraform apply -var="commit=$$(git rev-parse HEAD)" -var="ssh_user=$$USER" + terraform apply -auto-approve -var="ssh_user=$$USER" + +# FIXME: RAPHTORY_IP points to the server for graphql benchmarks, not the server for the vector tests +# TODO: move somewhere else +bench-vectors: + cd vectors && terraform apply -auto-approve -var="ssh_user=$$USER" + ssh -o 'StrictHostKeyChecking no' $(RAPHTORY_IP) -t 'sudo apt-get update && sudo apt-get install -y git build-essential protobuf-compiler' + ssh -o 'StrictHostKeyChecking no' $(RAPHTORY_IP) -t 'git clone https://github.com/Pometry/Raphtory.git' || true + ssh -o 'StrictHostKeyChecking no' $(RAPHTORY_IP) -t 'cd Raphtory && git checkout $(RAPHTORY_COMMIT)' + ssh -o 'StrictHostKeyChecking no' $(RAPHTORY_IP) -t 'curl https://sh.rustup.rs -sSf | sh' + ssh -o 'StrictHostKeyChecking no' $(RAPHTORY_IP) -t '. "$$HOME/.cargo/env" && cargo run --release -p raphtory-benchmark --bin vectorise' + cd vectors && terraform destroy -auto-approve -var="ssh_user=$$USER" + +destroy: + # export $$(grep -v '^#' .env | xargs) && terraform destroy -var="commit=$$(git rev-parse HEAD)" -var="ssh_user=$$USER" + terraform destroy -auto-approve -var="ssh_user=$$USER" + +push-graphs: + cd ../applications/apache/server/data/graphs && tar -czvf /tmp/graphs.tar.gz . + scp -o 'StrictHostKeyChecking no' -r /tmp/graphs.tar.gz $(RAPHTORY_IP):/tmp/graphs.tar.gz + ssh -o 'StrictHostKeyChecking no' $(RAPHTORY_IP) -t 'mkdir /tmp/graphs && tar -xzvf /tmp/graphs.tar.gz -C /tmp/graphs' + +build-raphtory: + ssh -o 'StrictHostKeyChecking no' $(RAPHTORY_IP) -t 'sudo apt-get update && sudo apt-get install -y git docker.io' + ssh -o 'StrictHostKeyChecking no' $(RAPHTORY_IP) -t 'git clone https://github.com/Pometry/Raphtory.git' || true + ssh -o 'StrictHostKeyChecking no' $(RAPHTORY_IP) -t 'cd Raphtory && git checkout $(RAPHTORY_COMMIT)' + ssh -o 'StrictHostKeyChecking no' $(RAPHTORY_IP) -t 'cd Raphtory && sudo docker build -t raphtory .' + # ssh -o 'StrictHostKeyChecking no' $(RAPHTORY_IP) -t 'cd Raphtory && sudo docker run --rm -v /tmp/graphs:/app/graphs -p 80:1736 raphtory' + +run-raphtory: + ssh -o 'StrictHostKeyChecking no' $(RAPHTORY_IP) -t 'cd Raphtory && sudo docker run --rm -v /tmp/graphs:/app/graphs -p 80:1736 raphtory' + +raphtroy-logs: + tail -f /tmp/raphtory-server-logs + +run-bench: + mkdir -p results + mkdir -p reports + pnpm build + ssh -o 'StrictHostKeyChecking no' $(K6_IP) -t 'rm -fr /tmp/bench' + scp -o 'StrictHostKeyChecking no' -r ./dist $(K6_IP):/tmp/bench + ssh -o 'StrictHostKeyChecking no' $(K6_IP) -t 'RAPHTORY_URL=http://$(RAPHTORY_IP) K6_WEB_DASHBOARD=true K6_WEB_DASHBOARD_EXPORT=/tmp/bench/report.html k6 run --out csv=/tmp/bench/output.gz /tmp/bench/test.js' || true + scp -o 'StrictHostKeyChecking no' $(K6_IP):/tmp/bench/report.html reports/$(CURRENT_TIME).html + scp -o 'StrictHostKeyChecking no' $(K6_IP):/tmp/bench/output.gz results/$(CURRENT_TIME).gz + # TODO: give name to file based on type of machine, raphtory commit and date + +ssh-k6: + ssh -o 'StrictHostKeyChecking no' $(K6_IP) + +ssh-raphtory: + ssh -o 'StrictHostKeyChecking no' $(RAPHTORY_IP) + +archive: + # wget https://raw.githubusercontent.com/Kentzo/git-archive-all/refs/heads/master/git_archive_all.py -O /tmp/git-archive-all.py + # chmod +x /tmp/git-archive-all.py + # /tmp/git-archive-all.py -C ../applications/apache/custom_raphtory/raphtory raphtory.tar.gz + # # tar -xzf raphtory.tar.gz -C /tmp/raphtory + cd ../applications/apache/custom_raphtory/raphtory/pometry-storage-private && git archive --format=tar HEAD | gzip > /tmp/pometry-storage-private.tar.gz + +push-storage: + cd ../applications/apache/custom_raphtory/raphtory && make build-python + python build_disk_graphs.py + cd ../applications/apache/custom_raphtory/raphtory/pometry-storage-private && git archive --format=tar HEAD | gzip > /tmp/pometry-storage-private.tar.gz + scp -o 'StrictHostKeyChecking no' -r /tmp/pometry-storage-private.tar.gz $(RAPHTORY_IP):/tmp/pometry-storage-private.tar.gz + scp -o 'StrictHostKeyChecking no' -r storage.Dockerfile $(RAPHTORY_IP):/tmp/storage.Dockerfile + scp -o 'StrictHostKeyChecking no' -r /tmp/graphs/* $(RAPHTORY_IP):/tmp/graphs/ + # TODO: remove pometry-storage-private from .dockerignore + # TODO: move storage.Dockerfile to /Raphtory + # TODO: move graphs + # ssh -o 'StrictHostKeyChecking no' $(RAPHTORY_IP) -t 'tar -xzvf /tmp/pometry-storage-private.tar.gz -C /Raphtory/pometry-storage-private' + # ssh -o 'StrictHostKeyChecking no' $(RAPHTORY_IP) -t 'make activate-storage' + # ssh -o 'StrictHostKeyChecking no' $(RAPHTORY_IP) -t 'docker build -t storage . -f storage.Dockerfile' + # ssh -o 'StrictHostKeyChecking no' $(RAPHTORY_IP) -t 'docker run -v /tmp/graphs:/app/graphs -p 80:1736 storage' diff --git a/graphql-bench/data/apache/master/.raph b/graphql-bench/data/apache/master/.raph new file mode 100644 index 0000000000..e4507f5f4a --- /dev/null +++ b/graphql-bench/data/apache/master/.raph @@ -0,0 +1 @@ +{"node_count":179933,"edge_count":44045,"metadata":[["hidden",{"Bool":true}]]} \ No newline at end of file diff --git a/graphql-bench/data/apache/master/graph.tar.xz b/graphql-bench/data/apache/master/graph.tar.xz new file mode 100644 index 0000000000000000000000000000000000000000..8cd640967cafece14275cd05b899ecf9792e8068 GIT binary patch literal 12263628 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%0A1UVyT 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

T18(R}lR6)1^m&Bx_v7cx*Abc4*OCbo2{+q9M@! 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%0AFR36>LCz1g zw`{>5?9s9gpp-HqC$7FW7XVNDvetjw%B^!h zhp3&=hRK4s7Q;;9i@Z6=(&Jd60$XMrpJH-J&KVC;+jp7yT$-m@9 z>vu~~Dl*Vd()>DTyB0}wl~E2kOJo%N;m&@6lZotINO?B8DbH@1=9?<6rEoYjTbAK||9l@D0Pq zGAbB{hQT%z_Rs|7eB*kPYzg3)tv|>f-4OTG2COY!tcL9csU$*03D$8WlXjnivMYI6 z|9Qrvi&RbvFSQTkPqsi}vn8%^uNOT;&Ds!wAgydmJ4y`{2Hq_&81PF)HrOOM_C|me zTY0OkAxlXo1paSKs4|d7Fgd>(Pm=|u%<@Q`Z=+wjjcSA}+Dds{>ggp%-H>kl9jeq4 zSnn4=3EGhXxLzpm~-v_L3-q7f{`OfC-8;9$7#Ep*J26BozLz$J~66 zLmvF}z%!BSgu#z|p)Koj8HxeHk1X$S$Se-Z)@rU5cC|Pux6w#|_i}3ZS(g2K<_18R z4GH>iCGMZPM8mN`?9h%@@b%eXZWB7^T|sXDyuc~9>l#rfV5edY1*u;@@TLm%ZC{J( zXcR3b7BFWSBP&Y^`U7qDMg3Ts@ICllgrp)q61NM&xGN&~0f(?Si4v#`o3X>TSCV-6 ztEr1dn3rd;w;{=3WtexlP5pd?ajW@W_Mh%dHFdV#mVWQeH;DvCFuv;>ZcFytgl}_V z&S#brSpg!Ck(lueGS=l}n}uSGqB71q9CRcobA@Iv`=gB)c=o*U8RbPmgny!PiPQ6P z-v4QBv+07i&<^!9ge7J1ZED=FLc!Xl+Qy-a_^^2`(foB{(Ye2$PLd$fO{5VFv*jAO ztc7Lb^)okFUyCM(zNm#=@=cpESwVAiP>6aba50G{l`7=lS)A1>tYg22T9If~4DvHI+Y3dUvRd*F? z{fl-dpyY@?d~Smzikf+AcZb+R;0*7vCZ{o3%260O@`a1^&Vje+iowP$vX@r7m7DS> zmAjPi?HM#pohurRF@v?aPB#9Q#VG<J(N(8Cb}m{9DKNuf%bE(fL{1G% zgj1s4a{Ji0JMccH-DgKqGt zLBk3Yf4sQ-UT0g;oq4lFUX9q>n_W`YrW_La3Wr_W1&w8UT&st_q@h*OPm?$nN_7SUA#2wl35;By8y zIFY7Uo@lo0W&KS?jz~eF(?0^!PhH6%zrPc0XH}oC+eYv)a__+1++ZpK-e%M zNikMY@m(OsOk6?x7=%Rh7A$2z#Tlrj5hO6=jPmSdiTUrp$1+>4P_6}z(S*K+N7FSt zh>|~V=Zscl7qb5pcR*(3p7kLOnkyvP@}AN93X#*>mnwY>5?rv#A+!^=P3;5ZNBgm& zK711?o3Jf5Z%d2T`J?9wBiG^HQ+JJDORyAu_7XzX8C6OstVdij;1%=oXQyMLIg?2` zfeqs$z_H3B8qaSYgXH>*n9FaKm;V8!0!WgHl=Y4`Yh14H6IT97%*^ll>g+wl+SYnM z&8O*~1?}~6IQFE94p_Q{l3{mudE}i+@0j58P<1*QzC6(8O(7I+D+HDK=VO-t;LdT( zX_c&LNAe9ECqlKmKNK`{osw!%Q@JJBka-U}v%u)rt#;OW!xDqji3tA|I71&i`3|*v zCkwhe6v zlU34ad}->5Eri8>?(LhhluO`si!GXe0j&eXbeNsuV)l8X&)MAPN64kV>qVB#-x=(x zBY&N*8wfUQS9w5*o_hhm7(?b02S+MdD;EkASteoZ#6@nX-g=n%ur0!|ICL1X_vWgr zAVsuNYVP)DCTN}Y3csyvWYXqIQ!W1Px|rEm#Dto63vD?;408$G&|k!WzMVZo?_58Nfg zOt9`M0anUawy2a$(q9M98a-wU_|(xzYfU%mw7*2ot*lw`(VU~Ts-5}APOFipxjwF$ zm5&^&$Tbnz8|uqVp@qaUtCj{<#5%=fj7U@$2<&6Py7GzGlVl z3zkR{a+w0Gp$nF;z0|PR%Y1vSZv*t_>0;@&v%HJVN_b@8kSsJHbypA5ar#ZN8w;C! zk)ZzKL3Gs7GaI&Ao&hK=1un2VTQ`7qSYDk@82^%Ty=gt0a9x^EuV1`H`34l;1`-W0 zYIUHW@sZrXW!ZBtfsqX0h@(4BDLU9Z*k&6IhiF+(I#V(X`ly^?3N|D zP#;XCt89ix2FyY!&@Rsg(gSiL>@A?5%+dH8Xs;8KO~9QuJ0Bbj1VW96J7*c)@~hR% zyau)*?dqA>0Vfk3bc1M=t73pfJeVjy)UWL{-FeGM@bUJLO4+8SJnPPit3Fe} zvP99y@Vh|ViD_>ri(=4j1qJ2J1M*9fcf;K( zC__RYlQx_uFGPSYESVdM-O#?J0k?G`$t}ll^vLt;WkuP}&JaHpCijVj3AfVuLWXn( zSwmy01f$t!axx!9Eg`>QFJ6=?#I9iGi${{#QS?Na@cDZTHq88dLGW z>QvY~I3lr1PYK;Ke;Qn2c~`y*hUu7iRKG6xM)_nTKjMbY8V_jTd5ET>#x`hRdcs5& z%Uf8%rroD2gaxUl2*nYSpx`PpFz@PEte7RVO@-69h!S!rfVm>l zobEMG2&7RV$EgZjTug|1&GM2|K0H5Mc4GE|9U zzb>%%=Dy$^DW$IFi;v*8E7Fl&^!?ek11us6qf;WRX;A~zJik~2=Q2UQPEN!QR`S5# za@f_@oa*OWJp@r?2fP#@$#F>U!|K;+lYNjX6;Hlm4KF4|R#(QaPz|H{8Y(J)Gqt=A zJr>>n3gTAFw*Pht0$WHku>EEunf=XNfLLBa0~I<#rsO|`j+N~xCAKv}8z?0%qKa=- zEF(Kqc9kfo?!^0ll0NzeP8XDzzB_Bn9nsGMqf#E#1r}mEyq2eWLli-+(%wc^Tsa(s zc4w>=OMxtc`g>)VJGVW67LjR{wL?fO52mQ`@xIB1gh)_?3#@g^N$p7q<%NQ-@N&f+ z8pz2tz}czXLR|{MZ86=cDZGNUKVO6oO0uusSYYyeRA8 zHI=!ptvD@f?rJ{`rTRNB@OABz(_0-TE}Y}GRt+YRzO)Bg+I8vq1j}sqN*)US{)G|J z4dmID2N_e!-qjsh+{UwAsC^^#>*_#S38Tc2pGm&22~qgN&cyk&2K6cd@6YJ{ekOSb zmWTOc;LZTU9~W3_W`Z9V;51>(J#@3o9@HyBxy~Ed4eZRo3TcdlOXV++cob&%AHrqY z1bFv`t%?-4Y$0|3*|fORr`K4oDXdF{g&Mu|>)#9KKukIcDuy!&y<9)9}2k#5_( z54EF2p3#COC)tcp);Xkv#!W?R2f?j_NM$y3zXMHQ9~$}R`#Hzx8~wAj*ElXc+l5%u zP^&&t`471!d7vX3E$N;lu11#xAJDg6JfqIGMT~T+Ny|~$`xP4tlN2&-O^1c|9_~{Q zMM6KnKOHpUR3t6Q373+vg#X4Q+6c9|r7uIi03JfYO-IOv%sk$uNxFJod;t45?hroe z^gnFVb5}(v%stW7>X4SspWSr&Qv3w{y3c?xtawglZ?s}>wT+{9y9Vp%WW5kJ#x95l zbt*ovpIh_1a!Og zOzTdkip?eyRSR-}H&)-W!%u<2g^0xj^=$05DVcliJq~YgabYQNV+N*U7*;yOlPaQn zFT1}jvJAAZ+6c+t--iy11jH*M0skSgXW{`z9sZK^06zii>oQ>gPmr^e#^Ct38?C@x z^9!KEw*|J{vl3cFHC!%{rOQ3?cGWDyTEOfxe~TKvH=Ang3pJ13wnwZXdI8z+v47FO zlw0?DywF&LE`>U%IFj20?TzLd0lQqf4bHL-< zxa1Z&Vkjq#1M1wID2}W23YOwaLrM?l$Vfex<>|;BF&M}NzBL7tbdr!cSe*||oJ`&Q zEfSdA_zfvnL*a2^o_By@AK!BOZ67n$U)$ZpRh+o**i!Cw*PnL7*dshoo!C!Jc&gEM zK6-WP%AprR1l#rYRRIcz1!2S>K`mQ}qdP_FqZmzmG7ksXclnh0Ssjyp#E-gOmI7gw z-5KhK>`F9iS}UrC^o=Miq#Z1#7tpd}w3F|aRK6wA4a}pJ9n0>eY-C^0T$Joy!l=Rv z-#+_H1s)ji7}{Xg=;1=+%y{|lYehu(u)aTK@em=)Y*^J`Sc{hG-%`tf$#cGJ8Ppl% zsXI)G%>blC<_RQuLu9T~3>uSsBF>x86>118gy!CZ2e_1;uO#K*Hdfc=*A(Y3e`+f& z<)qFu-Q_~-IlSjO7mp(XS*3o`GIg$kTr2S#PQIfw z7HRM&m0yoCz()YueGfDm+aOX%9ct%*FWVJ%qO<^LapIO!GiK1G9zdu^@_JC-b(9Hq z3NLhtwB=tD9Rjum?>VU-(n^&lHSC7NO#PCxoPcHn4(!2`a8ZJ~S`fZ?oS%8~RGuQz z3(DQoLA-I$|CFpAZD6=bOZAEj4v4jKg2flykB>T*a-H(307?&yViqa@`WLNTqU9t; zw9GX32jW4_DIn#1n~{O+w@ZCSzIDkHH=NSDkC=q03tOuaw}NERADd~)ZLv1pP%SN~ zNN2i2g%$GtM8-MnElu31T}J@1{_#oS4}o!NZlq!T(6}^rlI;Bjt6jhb4!0$5yAl1m z_Uk(T>Yu_qn^Z1DlOu#ytJkOl4N0N_&3w&;luD;f2R>HeLA23W1DH6<;e5K|mz`T`S3s-| z+z*>DP~{_FCX6ti+!jTD7;M*k0|96~PkKYwegn|A!^Awf!H5Z0Fi`;;w~2``vuaQ075{$zcCORBYROfxKCoz%M+Zdqhk2v5V=` z#*is>!VB$w#82|3(4K_TTYyI#Vu>xx$UvybSQ#R_d9B92;8ZRPTKE&^89TFpd3N^* zOy6?iVHk~zUQzBwGD(sEDqmbSk}z#L(Qhh}R@b$n66yc!V|G6=4b3!TC(xf z2|e$Bg9S$vytxrrGr<{%^9m(um>Z|js)P>XwiMu%QKWx zmMD14-)f3un4I9cPz=c>93@3NQVA`E>d{9a&z}k zKeGtyCpB&470~<|!1dGEg{nL%3YBy+Q&W&~oewXgW^Z+%F;8G;CIE)v*r@mGI*#A@ zgvQ*6_uL_s{(S*{<^No97K-+G_69gV{6XHATzQzz04aHxH{oDI4mDSu%&LFT6K!V0 z&oYyz8Htw1_$iKQ`|F?{3v6^N*Jr)QOILai@!M@(ylVyfB|{jXFMTnmt3Uz4-WDbj zxmYY5MATwDNRB6e{Cg{X4ZxB@ATa4Q{T{o=m94loq{P;^Mq^)e~DhGAP9n- z*4Lt}0JMPd-*b{yjoT&QC>B!)p59krSQ&1y>Jq)%BJorgiK2GUouLr;icnkI@l4_B zer<_EYyvI>H5w9F9y6B>Nrrl*T6z_KACx^AvNO&}fzJme1-=9zYWrlTo)Nol!JSlM zF$~dIMe1nYIXQ@=y7GY}jYSw-{n@1~JzvO?Bf)jDt4)~LQ4?}oAa_3D_?)_TxvhhT z5~7FuMN<1t)EoOi;dE5`FY~hJ#m81P8YU<|!%^Ic#+PNERi@<^2N+v?%B082!f~|C zF!|S3!6=)BctF?(x>rV#H{0h6gdP66hWt6$KrMszmIqWo7-UGGOprXs038nWpCz1z zH^GzTp&nheJEWQ#PtS(`*udbfICm48*B6E)gWD&$FZ=;*LoA%e3=Xyp_+QkbTkYIx zWlM$hT0`Q=??+gjz%Z1KzE1BI^ln{h0yC#kd9O!cGutnJ{nJPksglZOv<s6tm}injJ4U-*P9~ksEH7o)(stqHIK+VQs-w2 zlc$62k58;mo2nSy|%?1Y*=Q|4we zO9_=Y#|QB&jJo;3@Ha_u%Q2fqz2pA^eL&kNHeTMF>`<<4F+cATFonGt=e*PZsaQ$7 z^1(GzIJx_QWMx~&$TvCJv9b_USU-XtOjb=L3YN>Q+5`~&H#uHj;}KA-!8-vv{ciNk z9-9QGW|skTCxQ_l06iQ3)(^F9-^<3R;{1f971FKLc}uV9YsJ*a)CU7F^~IAlczZt6 zZlq_RQT2bk#c+Rdby~12urf}Xwd#?5<%GR=>F65Y!VLWFVC+Na{h%9u(NqQ6_u=#|^8cn6K$Iy3`G+TvO;o53$Ca_bq%n zl7Q6?0BAG%V-t-I1A3O>IQ|F_>}uGXqH|#iY?8r3kzq8ZwvG40cN0jUi3ld>Qn=Q`n=WlTC}n4&qQ-D0#a@3p61+X0Ij2 zJpriX;9oEUiG#We0!+3N}x;IwIRM8R#z4zqH`rNJ6}Of zj}Pf4yrQd1;RUXV3%D*5tN3jQ{n0^yD{^EG3>aFr-Ne*!1ra{U#fQHkAE`9hg>jh& zEGdcJ-AKTWYfHDwC|mf^h(lX%X1>N`;AUPz_z7;}!U%W5*)) zIR1Rbxdg)JBT*V(f(9UP)p-2Zidw9tzOGolP=$72*63ygzB_Q-F_nxsiPoXO3woIw z7HyV2vWmws%5^m$JrI$k>F(8e+z^&jlkr&E{tHO=uThI>DVpq3;X3kJ4a?e6Y~Wob zKmAEC?7T(GhoS$w8xyeqQD-O(fv!a`wN3vu(DV)G^2vNmj)j+NzZ#r&qebtKjnoE8 zX3Fmr3^y9T%G}yLiVPj*l0_^1frcX-k(&5ZgAW8y1?u;_`WOP+(5o7SO-mr;!VHo% zBeBiISIQJ`!=Sdgoub^wXm zs0Aa@JUdY1j6^PdSSl{N`d~%VIq=>~KL86CO_xajC3?zF9j6BXYBYNkk~C3BqU7aH zvFAF$6fNx+Mglz&gnD`pltKG3T~NKHNM{(0rH{Pa+7eSqG{&F8K_O8P;KE0m8S~-|J9izgw-D! zAJ`tbBx%o=AMBe#(mk<21m#?MDwzmlP@o?@t@<3}A2@@mh}uRBPOuq(3%pe1nmFeC zv^o87{7$4MOA|2~aBwgRE~0}A>UvKOqXwsbl-qOGrL1!KthI!tA^qnc=IOPLc|+(` zvWb2qcrGevi!3if{Pbuy!xMV_Ung34V{`+XbB7cLV9oatTmK9tPD^ZSz=p&FF1K4W z9g_O)U8+rYUR=u_$xY-nA=8Gh)NuKT>>E#B8`P4mz5x>0-}Vm(shd<3&w}jVBt&3q zNJtgq^2dg7D)(-u;O%%Cl=Ivt^T|^r8*?qI6{1pJj?s*{ycWVH;jFnxgWs)<_HOB@ zvPz$e*9N*{Fe%vVjH#RGKSKFbPdsyEh@=jyz4b-sGvnzm7$zrYT|)+#_E0vCb#7F@ zdCXV-zri;mfD0jB2$0DUT60)Gn~@aND21R<&ikdH3|*HSdz{_BDHuVGmaQjo+=84; zTv;+xfu;3Ym`l44=Rq$(C}VOK36kjfL~-+yu^Z!!tq%m6d+}rYFrZml@CXrm?~%BH z41tGj=%nF8pW^v=TiPm)lzE`@cK2T$G<$(r!`*!N@jk^^Don%>8BabcWaUxH8y#wVony~ zX8Ocz$!*kI-DUta(0YM1N7`^b-x`K-bBo9zpvKv3F{W^SW%ipT2K>E@yU3iZARz=R z7kB5P2B#|BM~bUp@Mpab7)EAwQ&IAG$|``D^uN8%kr!k{Tq72{0)L<5y^&FY=&?lj zx`ch@X|zNJF+Ul`IgNJRj;&%_7YwcN!xqR*ypEG`?BE~tZ)gT65Xd8nBM{Ig1fTdu zkgh2|g$hdt2?H5tG3LL-`S_}eH%Q*iq^>*KJF^FHpF}CrNz||xXMkynY0x~R&~g|# zzR&sNfn>+~M5}RyK{~Z|l6~;)r3s!riy@xdul$&xbWG|}VderLIpzhd&*b?eM;!HH z-+4+<4GghK0n^+v#Xy)-ohE+1=I|PR+19u$ZBL&aox;IKE6H2NM^ccLjh; z{gg@ZOH(`7-r%y}Mw-m=RB(k@f50J}iJh1-=xiK;HFu;}!uoH*RZ2%{kI3paHJcw^u7hUR*WHrQ(M_X!#SD1c}J@ZJBC_%(9@3 z)TlLv7sw6y%@|NEkEhW5pf}vOQftj7dF&3oG*^JtovSwLm`LnAyn*R zEosvS;!V?BfiM=cfU?29K^|Drw161==w3nt75Whd-3UiWiAAsze6%M7(K|{#5J)LJXwE?x=+4THPcwc0y zfjZA8CUKZzn%t+;v{S+2*FjG@1b=bkP2N9oiR24HQzDJ_{ti%wq2Z$yL!7M}$3v6> zQLBsiq`NQO#+_YK&=?KIu<*m5gmgFJftJ5c| z%~;-}U1sBebX}a$UVq9@z5r&Esi$~Hz@DPM=tDPdqAb5Irb5^z&HG$)Z$}?d`R7t| zf9`;CQ}&17loK4)m@scHd#vJxqzcrZFDLzjJ`K9q!jmo@v8}TuKnqTqC!!0PnQIQ- z1F$O7_T0=$4UT-t0g0Jc@~4=>5f@+ekJ)Wfimd0da?H0j-%Jn`7ZE5sR8J@=D}TU| z+wYr2w&98eIDdjG^3-P)#afe%$|b0|RR(8|u@ni1%F}IaXPJwob^W|zIcZ|8iRyN* zHo*)LT=WPR^c0-NlQd3)snjiF({mMOm-o*v`z2GS_GjYEE&h~H+!W=sYcO=%qV~ge}Ucpm}gcU1vH=rn; z9-_K7sDP)u&v&-{`qXC`lmHF*oj5*q%|yWi83DczAsR1}QDnxvy>B$a5fFG(tL**<+ET zb3p;Q^sRA20T0B^!6Qy2Wcb9`gz|JwV$@*?GQ?-xd^9@eq^87|&@;U}UQrO&@R4Dv z@M~G4zALrIuVzJSHUGCV!XW<*`}vV|K~b!0p-?>}W4`hbPdukr)d*IuYGAvbD%{gY z_x~7kr3R4PO+ur72=?wBNQ`*!NIW$m@~bXJOFH?~2R$)u(%dSjycQVk-!8#i&OL+! z5l-vI1EyS*cM2llGTP)rVM1--)a|`$kJ3=%Sk)zPpgFKV;ent(-xI!$4;?llS;<}S zpcQP3qnS0T^EvHeeVBg)xb=q+7^Tyz9j%+4-UFke9T@&*GL`7fs+X9tXEKIx6LQ3# z3g??}2e|~4aNN}#!8xF>Wq3c_qCzjyGp@_Hfu-3VJWM=EnY}qT$Y|g!L!Ng4D<75p zTy$8~-l#$RYOaafeAP0E^Ti4dubQZ_QlPkO<4FqwG#|18H%`i{7Nlej$Sf9-2HSe| z6$sUrI;HJhQjGnb#$A+pR2G7Rkte!VL!(xJYS^Z^N^|jP>bp6RMHuFuPy)FX%8NpF zs*OCEiIvo+S!fm0If|d-po0SfuR$`N&A8r1Wd%StL2{Br-(`G6%4)PFZdwVFG`n4} z4Rv5~cXb!pzvRzAuY;6+z;@P{q?v7ONY6Rvhq+hWcE<6;zbXp4=?+A`i(6#u%td`L z=Q)+(ocK-YopM*E2Aww=f_cG2q`I_Ud(a&Hdnd9*0Q85(aD1B;+;r*8&JlGrhxvKh zR{JC`dO-d*QzTn#M?$SNB?>7hU|yPHj^b_%Ad%8W4%oRenvo*S|MXOoD9LRc#s|^X z?RsLil^)WNrZu&%HJ&Zb#oLyjpO}7;M{q3$dOeimsC9T`4Nrcm5vpMq;keS|*X()i zFAJeh8%y_KylMK#qyC_-F~?QZNXEuZ!5&I|mTO6$VLV_u zJW<0f7S`v!;ul}IL4M6v-zE0p&xs02{aZC1)Kivx;&s=OsU7y5C%U(B#DV2l?V~ zUb?6|L@u_?Xg}FZ(--v5y3s(7rn%4g(Y%M9M!9HhQJeJJ)0%8u2OLj~!GLVs+Nkk; z13Y6wwH&HEPLZQyJ}KzQa=P#C7NGscGr1unOm<18hF_6ke6v ziX-aoyXOrVeeIv;3+2K*HS68#39Y#{&#wwKVXPiLtE|&nSQybcv+T{`Ualm762ont zvVnxM_`5a{wp|Z2q1v%uZqt~fy_Pl;R@#z20~L6VqoarOHSeMU z&@e4FN$SS)n^9JgYuOP>x!IZRX-4{+?L;9Ed|@#tDAz@wx>uPLUNS+yu4qY;2CD)$XFRv4HEM+zMwz1abC{Yv?o>b1+F7?*7N6}c;^@>)O58$E} zy>9+nbM~hawEk70e8MJ4*0w^{C60oJ(RgRt%G?MJDfwH_H_{UojZKN?%*;q&JMl5Vy7PIm)(G_vO6 zwfeHe09_XWJRroJ6I26bz<`+g8bE6nP2%Pib2m`U+0-2-)k9!*SnNY3rUfLX9mt)r zCBP=`*FgdTgca0%a^c3Nb-%?Dl&Z-t5go@ye70g%6zNvHa(k+_Y7rT0GbV5;_ET7` zafgXHG-Jet@W5i=@aG*AylxCxy-D^FiPLWcQ-JrS9|l0>4*61hiFwe}s%=iV&=ltW zZ@&MsTSn)q?rbKd;M#!^3PHPN8qicBNtn=#I-PR#{zNz%SWrDM^!|Q)c?@9lnNgHM zswF--LS`$(lLI~2CR;~3TU58rB-c|}U+yut+di$V9oJom#i-*Ytv>BIQnoJlaX!nP zh;knqDeu@bZF=-l+Q)v5tLxF!SEi2=mM_C#*sKSyhn=t6#m9Or__CD+Kzs~$vt2fs z6G3B1!il$WE^s+p@8jRpjIH$c@ zH<+`J0C@(38Z}Zxuy+B>(!bE??L(4QBJ!{eO;S4~hG{9~_HsSKJ_X^1?o*1s0C`F` zKvpjljW1zJeQBPT@4rgQM(2^;zViaon5;312D`^4BoPsRICB{BSWz3NT|@EaE=9m( z)LC+}eo-4fQ-e;4@uKds`vcwqBEp~`3$Eo*k3nre(hMFY($#^;{6ZL+7Z-z{2L%Uu zug8A=dRh$ec5h^I8+JH3Q%25#;d&xWKo6&KYVDJ|btPVdB*NO=#FMRB{Gx3^`OCAF znL*05X`@{|{YTuJyOGt~7e~F09?exg*UJ^GC4c3HEFVc9>y< z6U@tilxOFq%4SvOcro6qF*LmQ+=cE=rZb$`3e!Ttn7&X8_DtcIg(we6=j7jV<^I>q zVS~LJye+vPaO+u?h&5=N#E-a&pX$=nZ{<;p^uH!mqQdPvIxDz_-sAQI&{!mwv_02Q zs7LS#k;ntCjW_XHrwER($?_n?Kb4k28oQB|t%tod3%gFrr9ouDiQQaIb-5)6o5&h& zo%|AVA1z+comcV^wx}0-zw`>e%AHT1p+gpcru-l1UCYEQ$WrnCH10HFMStfprld>c ztW^j2z|yURTDr}R_o-}s}#E`r4Tu1H){_8ZNwNr zyB}h?u*ntW@}kMet)l|Y-J`swwiC^!|J)r^J8uzp_ko7dLAdDDhd#u-4-?+E_jNaQ zwXRiWtbnr={@hN0K>pyMX4Eva-KO6-WpZ(=vbZ7o5_G|tnuYv!3N-oJnjh;jw`J_% z1~h0yvSS!j9Sbu<8_?F==UgrW+zyVfxBCS~ZO*rEw)p=@4I^a}WwXmco)wjI!UZkB z61#0!dZqr2!99b;0>$=2dlLqjRH+xc9$Q#nRVEapwSb4B;UrSk0H5M)yzjE$RbxEB zx9QgvjEyEly+KU$@%Z_3!?!C2HN>WCVN@TCA|w<$9&uXH+_bqYH^^Bpu5I<zG`$K8TQ;w> zEOMDohFi}Uq-2DX&x3H7s9^sd#d%({uNYMb&4$~8rm;x}?V)Tj*X$Ye_0}1Wvl5zw zUDd@HX^!l*Y#PDLZbd~aCMHAU4L;$kq_z4FBU~)uctQM)XZ0L|oeyd=aR!3f8CM?| zL=Mv0ulFzz@(<;&;pNwO5r;fh+<3mB%IZPIXVob8A|*sFB9uN#Ygr#`3Q@1tJ;AX| zo?;}}T;eMElJGfVZFnx(rUb!=RRCFbM={}>tI|%V(fqcVkDwmZ_wi0w%2JMG?J}ZZ zMOf>R#{Mo0Hv?P8J&W#OvI&Tb>=O}3SolPJC+g@`s~jUx&y+od~Ea?*4%SBBca!sW@j7N)53il zm@?8Nplb)Ada>$52o6b_(KSl8HQPR?BO9MJ%BMU}O94|YW>66F-w5w|I{-#}Zs^mc zm4>w9^yk5`+%(Hy!GR*J4W_NSQuF<~kYLrBTCA|-NXxoh{Z`jkU+K>tjLj>3!n)_$ zuA^7%g8a49wu5Wh(adw1k0#C3x#LtG6x8>Zq_5~fL<3(NqjU%*#Dm>gqwcp*!26bR zPlq~vnypJO8#;ngZ^ht4ker%E_0f@xomda#p=ly0f6Y@#Yx7Vl^zAD$7N14tk=DRz zpuR-EexpmUQ@n=-8?}=()lStXwkRr(KjbUxmS?YgfWz>EdwIc2Y83W?7=O73L#^VC zt*>Mm3WV7MBgEv2CWl@~Ta&$7VO7Y=YNMgyFDYQI)U|P(i4ZiN0{mkNh}<3B1>WzC zF18{0iB9%+*IKWf<*vHuP|*s8x4)fKCYDNM@kAoxlU#Wf$cvif{??Qd>^}v^s_+;k z%M%y2Qh3c(NRIH8j7e|A61zt)+O?uy&nbArAew_SEmmY_1guy)h#^6>;+)7NI9*b^ z_Rw0w0PKqqUO$X5W^Z_lg&?^-4dTE=wVCtZUXd4G}Knkx#^VX&s427fr`>#8L zYYJg=C{p{x`t%Z08xvBbI|332q>)HAoAY zw$XkVqDJ>ahnYxx)-|twMwE77S|QKfYTznTwc_8QN?raWCIeL9c7JMue^sh}KyS{V z!KvAVb01o_+lPRPkyY`^>%moJrr-Q3uvb|=K(;9+{EnHMmT`55dakU44hNPhW8G~E zEZ7(-IwE?TpcQ8^sj$5JMO+y>Pcc6ZB=crpIK~3%0>${p(oRc&9C+DlZ@BDW6CJDg z#i8=vDPEMBh4BEPCzKZ?eN_>#+KZJDWuhc9R>nMvOc-bq&=9Z0sJg zb}_6yuw~I;iIt1r3mg3S^|0jigsbFfQLC16(PA=#a-&ZB{Gf0y8CznPRhE$(*9{>P z2ReVwV@4ISWO@?P$nF4*IIansndy5KO%vgX4TREIW9n2HyFD10i5y+r%z^s@H(UZ2 zCI0*ix+O3%YDgpE6kxf>I99nLn#dFX=53vsfTUYmN?X)KA)6}RCGpQls-bs*9%a?L zRZbi*kUIvFx=hBe#Dm_!#yLMs>iw0OPmcZ_C@l->q%P7Sfy>(D-cJs6sC1S?cLGGK zBS9GbGq7iAx{iDn5e%K<>8V`IK~Ff_sk1M+4a%OC3vPqEcBw0eQbOhHx}IA7Km-jt zS!axMG|6j}C8&m(s$KT*95I@jRuaGq7>?>zQa_^3t;ET?p$gPHQZS4&uw?x@OkD5- zge>~VM0p(Uo9y>NGJx^ni3$$ucDYsPsCna$;^rzdYq`A7Ssce>tdLZv-L+PT6Us)I zd@u@OaWtJTpusy>AW2Cq-wRDNA8QTpEZr)sW9e}93jTvIXEJDB)I10t9^kkzb=^JQ z0UuPki=jqEy%M7r3fOo~!0l62ztcZ&g1Gif_D~)S1(i@B`sCfzTa(DxvBBIzh7qXq zlBt?4qMOLFvEs0|2SGlEaub{n0zxJ(hnd2jz~t0y?Ns?#7XCZeZ0ks(P5)EC$2Km` zv!Bok)@ds_I%_fskmD8~BtGPt-ZpMOL^&$`gm2PIz)#x;dM~A?H4a{34kK75&B=0^ zL0YM(4)0~lIp}k}v!#=sGLgEo@xN7+pG)UM7C(^z<+o*`( ztFdr1ydb>@3){Sk7NiPUZb1b^W+1wHwj4F*Sp#D9=d4rC8_oXF-#PUPt{pmBu zQXsJ_EdM2H>46$morB#ZFghKT%5O~Cmxd6#( zq2*sKql6-&T-C;J`%YCk^qjWgS%iv@YSP4QMZ3<-&l+EE;~2LmL{x>*w;VKQ^A7Mz z%+anV1tta1zQKHW9g>Y9kqf<&(P1BgRyXzNin=%y`x+sVpV3gzGcJcytj()u^kyVS zf_i3W^#GCyTS<{R04l0G}cwa zHb=wYm)NX-%RRWHAnQkgs_OjBUwx2xXuR9%-U42U_zw}~&?9%MwP)`SWKlJR=_>G7Ya0MPcAQXaM$M|1w7CPXo zo9+w_z7x}Pk{7q=ps6T-F(5jzjF+nKpek|sbaS6;FtK4w&$zNoz~-u~0FT!$zRSQ8 zZu81bo_V2hWPi>Fb2k42PP%pT6Xgrra6Fsadv$%F5I{&lQkKI&OA914PaUfPP zr6%gsNx#Q^!#2I0tvFfri=EQzlOr{%3jN#%h-tTGOUA>Hj6K5`y$~gvab&LmLlqZa z2JS;cOp%EN5yW(zU1fuEU$qA8a(U2wd&s{IY)Cq{pBH{;ttUi4LG`L_ntM2ZkXyR| z@JEL-3s)xjuF|t;n}Pnzb!aj}8eo5{yad{uok5Xe`=!-5Z}wKPyZ6`QW2Q-6ABPe? z@weiYAWURE-D^`OVbSoq_YY*I>Eias7YhhlI`-9dHD-K{kjEP-1eDrMp_R(XyaDID zF=C!NcR^$}1;p4GZUF|5R8x7O#(X8q%>ip^cBx%5g!mz@gE%2hS;z4a4Tr?mOYLEy zH(exx6bC&~JD*ZS*Q|x@0RQITmZp@R{bMd6_V0tASb>V!bK$wSfLwuAtZ@k8h6ne) zL2na^!nf_nni`9*v(4O`i+}xy3|_ro&tF3Eskbw9K|cBGyn6hd(wtjv*hR_#CZ)!e zHFe04`0kT#bLUPP8?yL&labROvd`zYBB}5yh(=CVD$Mu=GszKj4Nr zmNbUQMWk=5Iz(cx{cMP#F_hX8VbqKb(k1FE73s?@dc}##i0Egcphu54>t2$TA~kXN z?HaHqtVnuxce2hg7Rn-Euot>L9J9A&Hl`j%){-{cV@~DlhKx-Yxef@H(yl5cd)id6 zG%WZH`m!aJ`n~$^buD$wccdmNK9)R}5JOp3&)f65`e{vo=+<1N_tOvNcv$l{*OtT$ z;JoTE^U-RVlPn<}V6|sk+v__Gc2j!Kjyq2coBZ)aSLavUfDM;)@L1DaJ`ZXr5wO6q zU2*?l>%#oGI)4CedPK_-cAh6oC^5cUR+r%n%nLqW{M z?6FLdT;tK^cL}urw(upIW`matoD-WRW6|vo3+Zl}c_#j3&z!BkfDdy&$NEW|RzMts zNy<)rps=!GW=bmi;*g$aCR;CR77x3`r*AiIS`gX)Rb5r+<05+>iX|n8j{m?f_u%yCPrZVNwPd1XtB074 zG)#xYkR)7JsfvuC=gREsd)!8lJ0lbq%6$v;(6*It&YOO6q74R6tCFubiEC;7CYNdSErmH6`kVVjl^7XdJ{9q+w! zO;tn=XS0uB+0dJm8Rgd?`BcH$s^+nbhT2TP3Z3qf`SdoT!9;dH?C36M2GQqJB2U!x z%c_9SH9S8C9rBw$Iuj#4av4Q5_ihY`H37u%jNjj|PQfG6ieFyFft$FL#uV=-INTCr%X-%EM~hE+*6+BK@;N>`QD3N&7C7$hari`f zRzdg4JYMO9vMr0DsDt`WX=~tNBtRsS`R`U|$e#j>%a3(lRFlYkL{tBfpWBS8r|e*i zFNSsx7L+o+oF6%hfGmxa)S9tN18~aer_;EJoZXY3g$=r>4Cl#YOCh4>!^(wPt!w^1 zHZiUvmho^G=Jqe7UKW>o2=VkOJb?OROhf z$iqzILD8(z&(rlyR@{(Mw&670SR3he9C*T+u~dD5T$ksnehrM%z|m!Pd2pf^ciM=k zk+(dO;x})^CaJU=vhMoarsGaH>zS+P518qGmiw>H$Z+_w$UnVT+`X+A3G$CJbC^B4 zdi++x&SK_hR_@O)OQuN10u(@Keg2Tr0p8ucx+GKFJ9wMjBld8c1y65&7w%@mq69j6 zZF>A1l#1$2$G0{5J9C>~FY-gf1`MzPiZtPqR$GVeBF3jv|7gnT%^*4@ri=!^0Fl-9 zr$|tT=c3gpn(Cz+X+KkBNQ|v$Dw?PO1`n0?y z!s_5!O2?u!59#p!>uWQh>6}lLc8I;$bwrNdI&F!p$?em^E0DcQL@!WdpCgy4sF)6) z%zQ%o5E;!^`2;GMx$p921?vqI0YS96m%8$GT;-X7MQFo6FV@vd{j!Iz)wj*N+t}j# z;>dzl|2H?%*#?W~qP?~r9hK+oVtV0@5-U!lvQognm!OJh=mP^W1QKQe&c2u>bfZ6{ z23}gWB7<|E$>#+yy9|_jD9DRTjdVU4GQiiPxMZwMf-S&i!ZAE8XZ2r}@hT45zdguL zY=y#yIKJc!k~iuC^UGM(y=DUo;to$fL&e>-I2D+UMB!BY0z~$jBPB&MV!#g89)N+{ zyb<5x`vjU#Yqoh!W+kj>Byx1}AP1Hwedo7`x2tb4z^cA(%7#@oXssn!!D-BvuyV^{ zMl%|fK&FLJ@7=Q1`4upsDJ{Y0A;v7>MQyjnn_kax#}kXo%+3nZwR}aaKX~&1RB1aw z|L5Ek>oSrzAWNt7%~L+`fj8OpRaUC1?%G*sqAr(+cJ5cal4v7ZSLBXC7%O5rXfVB6 zKAeAoZ1+zh3|g@`HEhAzwvO*NSi@9h&@SB{7@yy;u_&-au_K079^9Rgy3zm8)Y^M8 zSPF!5o&iG+mzHsO3kr(n2vzz5w%DaV>bdvTqqnWy_m)B093;)Y_yp`T1$-mGq=z$a!$of=w~Wt-e~q1sH)pP`(Oo z<8txh+Gl9+{`r(5Z}>C9LZNyOlK=BTF}~Mxp>1+3#GdVFk0LWQ!2Q2ImbE3e652NB2Zmeu6jroYJ| zxL-h+7ui~!T#~Ru0;*Q^(W!bB^jG!DR7XhZHYT`x?(gOU_~%E<`0Kg3_F)ceMW^Mr z;z^@xg*byzdh|HD>Wa;;2x~!>Zaz)yU>eVl}QApO)| zf1Ck36Vwo%^Cs+rZE9Dol9$JMeCC&KJ?disW6|G>_FC$xW|@4K>jEnaMcH|z79|%h zD*B!El&;Lu1b^9JQ*aU-K<{c59lwimMA_ z?)y4%VPBZY>u``qwVqo9^Y*1`t?qMwnZN9d0A?cDby3)m@eW_(3r`SaVhVxS(7P5^ z;Z9!?-Aryi%!Cl&4>2hpg^~|XY;iDb0F#kFL&q#8WIo}LSQTN9ADsX+Y$)Eb72%Ij zDj3F>QV4;TxEHc2{~gqldbc^TiF&kaSUGoJ!uLqQK!^dI;tn@M0KG?!BbKpr5Jpp? zIPQUf+_U|g=vK+LAhqUY)v}`5ZyRE4B{}Z{9VSN$Etng=XKeHiO)VzbX9N@fl9TmS zI-hv_4tO5;mQP~Cq1)4L3Q8Uty5G0R1y<5IhoJk_vn?sjUciM{Zz~z1=kwAPF8e|O z7B#|&zA&4YF)ee0Re@~;h_pewRgsjM^RPbs8kC3EF)4`EJ7ZigTsr|+#-Zni_li}Q zU)mO$>TySYU`7dhD&=?racIGUAQ3i|cU^zj%y@N*7)#R7Ip^OFdtKE4aeYC)E z?Kxuox^98;p3lk1JVU2WqRpbvPpEX$WJT{ARER#K zB=<4h3_WV(=_L<1e=DU-&SV?oKN^trl!CjjjXXjUCUsTdiii(OnN5l;({TZ989O=T z$8kqM&Q)I`Z%4ZSrUS~WwmH=_VRUqpBpOqON}Af8-7?r2&3;HgNLFzZc9j&^+tt^z z!1V0FiT&|t!4^SAUmaWIuSvMI4IN+08n3Zm4T(8|hBsE>wNd&P;T|r`D~-j zy2pWa_6G-DNLSE%N~YI$Ehmy0SQAod2}VA#>>9XwNhkLbryp#8hzu7sNr6?2ovG

k>05%RKhIs>>#0{%d{{G?yfuOM0l(TvtZxRWq(8{0SpEkbChhAah(HWg9Z1~ zI##r=@P*9$>_!C6gyi}Rj8ZBvvs(X|u;-wxhH@UXzcqM@{x&z+KKMdcjM!kQwo_xs z@h&@HizKu%pLeg6E@y=E?KSM!=T!5UR`T2Oi(5dj;2X~HK)!Ot`R!-?u(K-*H;Q*#HG_s04 zZC6r_jfJI-ps0aVld2x=0!#mQ_1M5I?LTn*rdn-^P_7N(hksT5T{Y<)C%0=15{;0*m zCC)SK)8A5cSn6eRx&#gDgz|-lpH<_PBn1_(D6oV2-8$*me;A(*2(52jetblQc*r|G4Xyn#@ zqjZP3@c1Q~`_P0dD1Q>Bl~O#J?H#v9$VdM^u(~I&@V=j{HF?lNRK!$y9}`u`<8&8c zRd9qZfTzh0Q-2IVFpRle&eNjhW=nCn(B(K5;c}M^8}$ZxgK*o#{nCb7j)*F4h{y;| z7ju#+5#u+ZHSQohc`vbNRkMbT3FVS@@~Vx5=#tR3QoABuy9$Ue_KGBNXcv6;#Rn3K z(k(cJh!~TK#j0!p@uHl8YDLI$UWAko?T4x~kmNuhq3v3eE&uPb5J}`6$~*kPg(=Lh z%8is6#P-&yhaUZY+-5c^JP+CZ;xkD)( zRm`f+4qB+hIV@ESx&}?xP2yS8Uk0*5Vg%eMr#P`y^_=*}_kW}6fzcdihltiS6gD{G zq6S|jbW(N2PJQR=w_TjwMxi?hZ<&Cs2@*o&^EU@>y8tD6OUnHj?kCQ|F@j-Oso=M{ zCVL%-gwTC&sg%uKDcV#cBlk6L*pA(2?b0SRZx9X{(=@ga*pvP(L;t3M>isR7I&$}? z!`;khVf$3`=*G&9UJAsc<-)&Rct=oNE3m!C8s@_tASjdh3`=CrZc!(^-*>mpqy~S@ z^J8$q4-9i{r|CDf-ScnHVX0x?G^ylauR+djH~0|XDNYMd@MzOVhGwu^qsw$kKteIn zw@i$ps7^q=?|E~XeKyicM3HD^y=>}LnrDK#WO{kK_WsbyH#$yyENOEe@5cDou7B+c ztbP_56EhZEt_4W*cOls9wdVW6T`S1#2csc@wmL+{;>FzfALd&6Dx}Q-Pe8E0Ma~`L zQ~_U`nf}A96!JEnYQqA^9>j;MD{adm!qB4=1mO{3?~)MH z+nV7+B$@V@sk*y}@vudRnR9)+WW3>y(ULq;s~n;1x9a{{4AbWNgGXojJrOyy&+7i) zj3tU1E$gp7m_#%T&qOG;^QK~xD5yaA!F4%4x0Zvt)_*u)SuV%yerzKuVCOdsj0 z^5}54ZNav{GGjG+e_)Lqk?@UdbhVl)a>Pz-XT zuz{Sc0`^)e2DJ698*VwogActm0jiQ?JNYPl39!>rPZ@W88sE~! zTk~Fe70wT2hnYCxqPAIJVKflj{YABGt|)8e4;qe>_MHcU9&xIn=4LmB?SPLVpIe#* zd8M;DA zW0S57?)smPVQU-jl!N~8ad&m9m=n@GLkEh7Y*NFtYY%c_?8Mp+2uKx+X^=yqyKbs- zx*_0>Z4x$cN_%4u`JXwB*J-m9KL^chcSX>+U{_1bWVZo5I5S>oO-k2XEW5N8&RV@b zmb|e%aD6ZyfjVa6*Lq?Q+K>nGN~=kq26#RhdLZE`<9Okciu+Rkw~7_N zNm6xyixVB`0t_IvlYzCuFbJiJ&q8)peNqm`SQ;}E!2XiK$A=UEchux1naT9m{;IMi z1xO=Qvd4Pesq=La`(r5^G_Ua-SO~zjaNorpa~74OHVFNZG;^T@AR9bb*#^F_827Rl zO{$1i&CobPkk9^x49+xQpC{hvpuG`Y!3)?tiOl&`8elvgNi4#z>}z#{@{H}7kvK;> zdWae(2pbhIL`oD&z%bhlksMC`=z{|v%JReRtRdu02<1}gENDCAmjL0Up7gP*piYef z#RtkR15eITQAmwZHz_(&$Tmm*YlRPX>(5mR?(#QW@-IjivYa++6QLW~et@3<&-Lt6dB-_mTu1B+Y{RgEJ@;R7UdqKbMXa|guf89ZsGIXzYhir)U)B6W_ ze9X_8dEmMcts%V5PX|{)tb3Qy->p1h6`)bLN<`D`Y(S<8H~7dseuOd-8*d^x1zpuS zgU(|-B`BH#LJLSmyANq^xl*^>1yN+^-~A6W0c z;CrQf>T^H~50N*<#^F`I(xt?`Kx(ABqw#fHh4uBjX%~dVW|vP*?WyM2s#pJ<9yT6? z6o(fL0gjgV+}h*6u}w585(P}|lExsN7}az$!uK*vey&*)SCq`Y@S+m9l$YBozU|zR ziQPcC^!Wcs4l=zof~_)CY9r@u&%vgYEwG)%zlSR?z%cBYYV)TPboDUFAar`RKGLbJP!Jb!hfiAgIc;KSdU6FR~hKzBC*LRb$dlaN5Qc(6@yuByx}d^V1b!drK{Pgo48O(4JscIBj5oIDeBM`;ym%GZ5v< z$CQB4{>5Zj2Pn)GIC2LN!@!2Q+}JkI7(p#@j~oIWN+{!HSB`oFO8=7L$3UA+aIw{W z8I^T#owJ1xp?UOkb^(i{U;wOi#Vm$Pxlx-{1VdoMk2KE$V7A0UX#$R-DeF*i)PaYS z`HSJx?Rq_zql0%nd?v86^$Un8tb-DBd5usDBcv&Ze6I~Ad(^+`OlhDT64JY4R+*3e zg@Q5FP(Lz7G`j6qyn1M=dZiMGt<576A>+c0cl6_5Qdca~@y_wyKO3UVqgGZZ>ipbC zl+oMPj;yt{1yx}*Kn-blRzns6WZP;BqTUo29Zb$w{7^*9jq*RfIbLUw-q~=^0S^38 zW^%Z!&7wvntfu7W>KF`6%UL5SGr%&-@@@;x2;g-1#0Yizm;SxV-%XfUcg=00KloV$ z4n`jB9aB+UhsK@;;8JVp!AHr}qKsC1#C6lcg`njbvGr(Yq{O*)GKPyX03p>r39vwr zXVaNokXFvO(AM9ySWc=lo7*?6ySpi54p}(4-+wA*@C+Z$ALZk%ZFrg+GgQa3r)(J|Z6lM|l_@w*0ZlL1ptVj4U&AL}C`mH`fRJe658B&kDAC{o*n}uoi}@3^*R2R^2FRBYKh*E(8*t zzQUNYI`*8asvF{pfeQv%E|a-)bh>g!Ak?LfeC+abHJEy1!w*lzdE^Hb(8F60uVGDj z`=~nQm3ONnhVc%^qY{%2pez-dd40`pYYecTZF2&4QT^Sxb0X){3X3b+8#7UOd-oht z9A0M-_3|Xd8Q zR9*#K34`=qwWqpsZoyheZ1XBNsPFV(XQu9-8RSit)mfDeq9N&GE4}UU$dEy_(o!vvZ2hf-K(+3pA8@ zav>1FY?A$X40H3p?W>%+4^&@YxKY3CiPJCF7T_3N}-U*;mQmNwFH zbX|qr#xqZX-4Fd67IFXYf1%-}+#Dd(oy}A@0PD{hJ zFx)KWV*j>FbkCvXLrRT_0pQ3UM~}hgQ)YEC9~_sJHklfy(*QMc3)hEqHg!~rEJJHa z0rYlb&q6YaU$PDU(qCzW78XlKWYXggsw1D(JUJRn%|rGY8nVFw6lOBkc~u_agL3UP zHlva$V#y=>Xb}7FgS+#SoHN%*ebdtR2iqsvW)DLegNcj_!oem}Ps*K^sq3|ZXF12% zs?%|)K4XGainTIL6b_k0A!^edTj`ub`?Igb)U=dMp-;yCVAkN=RO(q}&|p;gg)`4m zgO>z@oZm=of=Q7G{ffjK#Z5{Q{v$gg-AIa$tw9?lgHkDRF*URX4LL$&nVT`4*isP) z$Gq=1Xec>xVyBCHJiFAf;exOtF>Vmk3#;4^S;kmgczI* zE6$x;s-b~qKRq90K@CWhtOyTqm4IbI8aa!ctA2>?06hc9?nUNUg3w+x3&Tpk;ws~t zGi*Zu0Xb@Y=sm^`!1CJtKWPw=GOuV@3(_elwcYcpQj)8Nvi$9MYLibMu~2jT66CrS z4g&4)wcWp+E5PE$5@AC7Yqk}K`KCnC%94IevCx!sS>;qsOTTLtTS555?@!$klN#Gq z653wZPK?d=>cSTt%ZYyaHjFK#qa8`t&hS=o@7iV_19?VJJ@GRVOke4cOO~BN;o?u7 z+UogS+l)k>+s@RHoz-Nr{S!;g19Y(k13zq*QLz?)ViPN3V6Tr{r0Sv_>|!bhT*G>8 zxlkY6E}_?FR1~VBHyvWsfeM>jyc~Wo76JsUc+rAHSY{x0s#L}DlX$Vl?tpj2X0~N|&xn!RY88l3PycB1qf!{x`R|CTlZ?7pHYXz3n5!k72ZUBl>!& zd3j4yTSmxYyxEkjlIi#0%`o*s8{O1k^aPnI~vShRQ+Zg?&PYwu3ZQpRGL@9{aWgs2sC9l_vp3inxS%8 zxneicojMe#jwz0ZT_rH*BPA_=*eE2h@HfMP;&lbK=9L{-sc8R7uKRBVqCXp*z1XZp?6geL?8}~YhYFwR70RVZ zjm(9-BP>gT<;sZ!hp)N@D7tEt3~HcxIcNS3?*b0JMD`D6>v4BZDF}W|+$%O$oU;y# z;_e_};Itetd1Q=-{y=3T)tJtEs(N_IgIcvT+kdg#KWsO3^g8ceOPuHy57g`kuLGfN z_iLG%3riBB{5}7HAcpU1lLTm;2$`s-LIPy^h(u{1A+bW!VN@82wV|K**2ia=wdY%P z5GAXIV#E?WczL*v0EIwYw=WZBAm$L!ma_Mr4?$tM9FtQRvGrtnS=d4RnW;u zlrse#|5O3=^Etk@>TcV|(Ij>qfBP+2S|C0%9>^;N`8H_b-RdT}!S>a#pL`@+GAS35 zJd!O>BU^6tM%B#AB zOXA7h5&^B>jz9J5Mu4`VYTR*TzcG~-ynw7-EcLtc`#g%o`+6-q<+)FL`N@8MwKFJf z{6ip*wblj9lbP)pD8tfNd)``XLaqGbw|_!O8RVEG+zq(elKy!qRCjiF`2ya@AvHCN zY7C=!h=1nYCgb(lIRhHV%NqSwX`CnkPFx4}-c;NZ3$knV7JjStqW zUXt*2$&hPM)yw$UTg=t_e$UkX48gxesjz~l@XJn_0~eN|b$b3NSY9`$Tb((h#;CI& z|6)27-HWN+f`olaE)}K79BQ=7dCW+lFBiWF#DMdVN`t;J$Qqu|SfYjXc13byCi+pxwaYa?yYHr9ZG7Ykoedz&2+V=iwmtQWA`x*4+;R^ncHN%EDo;X7f+fyO|Nx1gsh z4hWg3dcY-%S^z`aLSU?)qG)o0A=GE17bPnrqf6^@bo}xiPeO@AbIB#hTyI+ zd7foRc&c176PV0M^g_j{_T+9BEpBj{j_J<4?KbNG*19*>%!69k)e=hUz!qVz4!9X6 zxp`4;1?3+;y{oK0l;bvD12+Cl==NyjD&BWA$TBt2)0Hj`w%g*PSn$VqNLokHa2HDj z?HbGmt{m&`MixYW5MfOojVw5JHuc~#O!Hesg22OmpWuUIw3#AU?)F2VAk-mF^M50t zPAGnOJ+MZbHO2c->G@!Y9I}3oBME@kV-`S&LqTgK%$T5ZwHu1rgs-BhRAA=5EIEhB zRPSsdDuJ)7!`Qh73uV9WgqfeM!nb}t6zMdRYEjFA${}SRWq|ZUt+Y#?Cmn%iOv7ij zaIppIzMHu}(azjLEdMsH#v0~@PS;+uck{=hse7ff1(b1&eKvZWWzh>0T#I;?p14m? zhZ?3D6R}Lib%`R&j0?6xBL#Zp(E&E^)L_R*EBKIrsKyt${Vd| z^E%jH)HOV8Hql!M~-Ry{>5ghQ*qqqiuoFX=D^OZz4}vbIc_#IhSd%Lu)rsk zOa|m{JyTwa9+CdbHBj|G%df7>BE!;USpmc~mgWg<`=J2iZ!UGYpw;JQuByTN4u|42O?+?>tXAO9c1=|BRjX8@-mNTJ>3 z__n^2NM6$ZA4XwHZ9U%Wlw|dmp}7>^i;QaFp1!~cZ;Z}-pw>;HH8hOmlA!r4Eot*E zpg;SUEfmv}(&Ld{E>AHf-s}zA2?doa=>2qGKawo7 zXWFl>lfyXRc!R8$%+9eo!hYP*qOl3S&^s>;n6jEnmjsJQ*)24{;#I%m)KTX6AC-qA zvl7cGaNUwt!h*1Nk>NbomdP|NKct1>ZA&UcGPXtg?Yarbr7skM>`tiNX^5_2TEHrR z7XHD==zkqn+q5s!xDj#@W3J2qJu{P;hcQ5X{CI$7;#iLtk=Dc#H6|XJDN#UjDT6n} zku6%lcHvXzaewIo zEi#&-GxtU}q$-^#aR>F6}6>!HT?4_QRLQ zwFxD~{Mxe2ewm%{!PK0Bef%=kFb@CrGaW}L+BkWa67|owmP3gFq%k&wkxZ*@30-9Q zp1~jir&$WRYve`mU0AAqc2b=L99{GlQUAz`@onSztf)!mn)k3CxRKu*zyaVfUL3un zSB+@TMPKY44LZA=G-qxTg1O$dkvcggSee75+Y_~O6(wa?6!awbuuB$ns689?{M2-`kf66B)$u5Y^&s18v z?V8&TvLSYL!!{A?FYtVjyUHB~{< zDt&krGH%%<>B{q6LcRhbuK;eLOH9)Q(*Ty?M$E5lw3+DNcfYAhg~$8p&BtpCq->{b zCR7P-7SF)IjdIjiAqowqeyYXs?#$C#M*I@ASX}zf`lPcR;@5cyX}n10yuVrZ#U2c@rq}`yA2|64-0l zo_K{l-xfYZr;+rWqG?TvYj^2Rz^NiiRlQ@yuuCvKADwJ4>(;4nHcfA=Y7ltz{29kf zt5AspGn~ODY#h^tuSGWM98j?vgck4%Z#84`#e=xIWa;Kp(47$^(C=}0BkM-wH1`Ap zA`FKfmd~H;B%OjI_C$SaRAWrOqRYvQ-uGChqgH-z#zVebP>w4h(*volFT%2|R6D%^ z$i`uKv|xBPw5q|<1Y&|d14Z#){0XZ7Z;C!aXp6HDLD@P6ey5h3jbVo-(snCl5#I*% zz;9aC*Tl?yB=YJl3(E6SekGX*DF`$BDkp+eJ!*6HJ{;ZRUjX(fJ<(tV^v!ofU)-tD z20G-4*3#*x_76GB`?r@xqQnbpLMtf*@6U|&fAHT#%%f6v0a$TFHrsGMcPjdgWl|tI zmA2^)V`|EjW~Fkv$YW+gI&d2bfxi<9Y8^XRU(>X=6>FFf)Hv?AFcn4B#kXJhd9+8RwUnfF*Su~;*b{GNg)9}_6XBX7pV=S^l>XN_&t z(Y#FNeVuH_HWje;<#wJ(Ftv`k5>(Ql-JP?#!2Z>*rii`NlBD-x-qqTZg%QT1VDyod zeg(iUwE?w*E*cG_!Ai~e^i`3 z$D=9E6{T_mHr=0mfcUEsFO9fOHv7et4016dZaE_KJugqRF+5*%AbIj#R+|zIqmHh> z;q|7W&aNELv>5s3gLiX&hVCe>esj6{NTmk2u^h|Y1fef|vj@9N`f5OeqjN}CwRt5RW|1Pb9bE-Sf86^^V$fb)Z@cJ@>S-oWsPKmui_Z?8pO~Uv4 zNZ+&6TO4}6$1RaDY;Z};*$qH4EiAK|1y3S<<~kV%qagb69oHYcs7yIQ-MQ%5Vv@Q?X=_HBsiMxF z3|yAJ<->_-bV3!sY3tJbYH9di$*{AgsnLr(zePpl(OhRmQXHXGGY@;ncMYhN-tmJ6 z8*FpO2rW&tKUI!Q8Rt{05j2&ANYb}b%q8694B`z5kn)SrP&}7;r($SGYar^4mH%a% zJuN;3Gm-G^{5uS%(cw)sRVM?ED$$|XA5WB8pg0Z1`>Y`n6WJLf;`I-So-%eu+jPK> zx;O~!<}l7zb*qHhWhV43nP1|K6Z4@BYDh(Bd*czv@{ooGd*Dx%4-bTeTr>Y0vx;`0 zMaN?>+Ejz@4I_LQgJ$$(@K@DhDY^7{KIvJ-yO4m<9v5vb>cq#NW z?}4U3ckaN6y3obNQ8Y2*7;pfKSEzoNC8s%pSAxitYkMkN4RCIJz9`iG#aq z6XKhjpQQue>Txm&M%%b6#}3*2{T8LVHFGc3Eck@;sIvx?{dhd-;8$RwdR)t*H#A7I zOgH@9d>w}Z+t+idEtPBa7ou5U@@)I|qv*M# z4n!)kGUUy2J+L)?gT{q`R$&gJlQmaULXuGyqmox#R%a+buh?5xY>%E^pZL^N(Y@Te z*Ub6^24PM6S;)zpqDD3g>@EC&x7p72@3kmG(-YDF=iAfb#Bk)OE%(#qR#~1Z0%pa` zkMh#}|Ez=SMFf&;Ad;M!L=aFYL8aW%7OFY_AFOOMw|7Lqr)+L-Y;xeP)QO!sCsex( zi0v#gbgvFQSBC;wX6%)Vt@>thWaoRKmNPQP0Jl{qN&o$PuTSscXx=`Yo)BMNQ zC`Sw_!2uHPHc|xbVZIc=6znTnf`_oNo6X$jp@$Hf{V{%pjDdccl%L5HZ`mwl%*(gN ziKaatBX_U(xkTU-S6f(MTos47(T`@hLI2FG+hJyna$R`qVdmpilXqml(!qoRghTo* z&(Ue9a;uo@kV3f6;}m`hp5JZZjzhUHsax-3!C8m~*<@W#c<5>{mrF*q!yRO17ab6Y zp5;vu2DA;7DC{(#ln+j2?-9aT4Z2LECGLen+_{q!fc>)UP*TIEb_AuPz`0)ZEIJSn z*v{EYznrS}t8O42Q)=Qy?q0o~I=t#sI}IE|#lc&!3P;@|8KPOaVbEtxk@1QeO zpVU>swpZ-w4Z~;~J$km@#9)?!>|IUN=zLB;?F){)1Nkx~hqG|`P(VC0d!=-m5X+Pq zwY$u_1j*NJ`wUVXpB>;43OnchR`-LtXq;-l_t?9w+<$uaW>l%n~d zM*5>C;6Qf?px#AJ!_T={nVGmvN*h^FnVC)5WX$7cHjQ-Zhi4v*nKx7PUYP076-pqs zW<2&`1G$6CWe7CbO#>G(ksUQ)jaSB&Pl_sb?#!CgRs0$CVc&SLb3~my7sLM?E~Wpx zjei0+N;^xJLhT0&-a^H1k|k-jtK(EZ4f0~pWp|}Hyg096&E|QpKlc+0t3Ls;*J}w} z{D!g*h(S5yP_Ax$7vh>29MAQA?4}v+`um@|=3&o$Z6pBHWPycc#d{ruq#D6o*^uct^gnN5g4utaF0AlSFDu*Nyn<2>4*lRiPs+t4Qk9PZN351p1pA^i>StxkGNJK(L&9h% zn&v}}#r2^fi8?ZrO4E%qgaeu}(kVgo!y}2+mlCd)@==w|q`+;%9j4uIUU%+O0&2SK zHA4IJ(YsB9nWyo9B`;lDlK^<2?%dyUe<56XyB!KyScDpTbfwbA+o>;IkZj(>PwE8M z7}_gbO0-)byD|Nr9c1oI$@%+s*d~Wx6mX23L*dqaYb}aCeOR+hjB3QKc6vQboT{#p z*`y%n_XK04FO{sw5eADDFo-@i;Y^G z)NOOax}XH}Ie*~!_*UTSlXA?V#v%yBOc7zwW@=7&)cC&@hI)@^M~S!;6%O_AR$OUl zN~*WrgrNg!znA26uU4!r0ZV-=7Yr8=z4=*t8SP}Pn$>}wYem)cI#I*x3xe<=H}ZO3 z6|mS4Y5u~jzFqqT048mS+o*$2i%f;*$Az!z1ek|SoS}0?^}26}U5%LwFndMytB#0# zOy^2J`g&A@)>MO(8-HA2DkUR?LLJuS7WIWIV%cbdh4cp78g_j^zq%~NakQ=4bHkYy zCks;Kem=RPJWmI=s*U}l zIy@3QVLpt3Xm|*o045_lkuH|Cp<*h_ept*~(3-p@W)*MgS$UBhaYSXFZlm)6YtGsZ(+RIq<9VZ9Pf!@`UJZ{Fpvw z9rwURE`1$^lnmeG`3_h++2`75tXd?%7Fv@bY!4-Zt4x^ao$y>K#uacyD8p>@cQ}S1 z))0f8Tq+tDkKZH~DIix45m z>$=9LU4s2zZ1bUt#r%i*{{c24xZg8_cg}CTiJsn<(Wf^QJo^+ z?4I_Dgb+adarGxcnSt6ExF^u(wn)}CTZ!i6KQ9xiUR{xsKm-=xVPIg-91RcE4M)Lp z;mXv`FJ>Gp0>jhZTiddGz$~fEGMEIwnTcwW%{%e2A4!;KaChu-I0H7K(m&PHa$DNF z=nACQl_bDvwcP)FJp169bjKoi4$%12xEbu3MVCyDsf0(ncK`^3)jc4vMPy)7dql5s z0ARDy8D9D;HEhBR)k9>bl^kQNM4vLYNUbQFxQO0A-?4m`8RAIkub5gjLOzqit^yr( zH$=>nFp6wR72Cj;srC)Eh0!lVx}bJ?K$+a#@y@PA>k%!%v6l4^G3wpyA|nfvcL*l%+IXl3qc2Dko5ln||N7HE(fdQ>sF! z-7Zy!`ifuxT${l1?Y(n*osZrBP>$c_-ip&7<(^bmC75o2))}dBB@W zTJom`BKWS`K7ZbhG41WAzD*EEm=M-7{~BDnc$s&y&_3S>+6FlYzxv+O z;Qz3oBm4Kqr_Kz2@$jAk7TnF^*uo&z=UWgq+|Phr?n>^Wo7-31u4L4*E>PLHC!>Z* z!5TDLH}O>R-}Q?ikPkj?0r;L|p-k3AWd4iBDQv+v68r6s(ZdaJBg5&izxh2*UrM{8CXG2U`=#`zjUAD6)<(`^r1xxsV7sXBM*+(?{HkSYt+4C#;>mCCOn={lJ0IM@Az*WR%<&f_#(jRZoMHtwN2K)j%U?p6j_2yS^Ow3*|uDis#wYnq8hUh}ar;unG$M)A?6b;r< zKh#&dnVI1RX zzvjej9M|`t499qq9>q2H>H4U{xY@}jknLK+x_H(cSK+%{(e6-*!X$BC&(wEU=ujrTdT(Hi+%JiDo#-JTZ3;grm2$m6i~m( z(xH`y_Oe4Y@=V>Bs43sBeqt;dyxFNX+5tq}Vf4go$aT~_&i~QMF?F>pbJMTjT!=Y_ zM_yX@1@8Q714X`EOBo(Ftb=&0mog+$qQ`)@U4|EcytM(eUPVWj9`q1b6MG8K${%`)z>jTDoC>Vwedzzf<~2oHUhFBmU$KCrA120yl#=!smA{clu)M1&7 zxmnrb6S14#I-R%(DPR5-Wd@WzASDBj#MZA^HtFyyeht>+`9p zS*^VH3}1>COIGU~W@CO)u;pFKAPqFsWlG>OjK1;m7_M+yoyoN}#vwCiI34$l9%aSV7(Wmi8f z1+FIcY{af@rG+gx)I1RbNz&2_-K`YKzpbvYcCZ?P86h=}cv!__ zDvvcl6a`8uSP5KM4ix*;vXC4$1Qr~G!vR6h&SsE7Ih>|%IfTJo2+ZHrr3&Lv@&9=5 zn*(~Hf&WK2>TVz*LtapLolHuSc_cHlPYbiO$?g zm_rsWX*U0eaC>8;FI^!iXNmf&VDPGolMr|L3kZH}U96aceB4TVn5A^E9_?z!!+lAY!rhmRg6*?G{+dM|Bu zI8;birWDb}0_MCBMm$+JuES@&R?pdvE1#2@kE+q14}At7`WG8Gaga~>4>%ja@EGbS zKkNH^)aWLQI(~8rfm$r=U)B6P$6|(Hg0{H!vQMknLoQMq%c|c58&pA6uVls8TW5O|hk)&3F8O1Bs*9~pMwQ3i{g!KngFuzE?9L&bmO!N*AW$|lH)2gY=kM|K#^ zIR|hNVQ_h>!=<7RB5x}^px5zeLL`*}rpVZn&t&9d@~3Gou4P-QgPYv(F{cyW-b3m) zr0my9a`5;phyplDZn^>ITE43$gNn|krB>jTO#!Dm0m2$QRb7{wtL*Sd3J_7l8RFdh!}&(0e;2AwnwmLc=?n1p{hmp( z)L+JXr_q{Ac9Q@SR2{zUyy#HuOnusM3tK(Rd|aWS`{%79Kud3}1)Xtm6m-KPf0X(% z&{4hhIjBI~AyZQhVG~((mNM5jVC{JR3?P>h1HkHeCfR=lRWURZM;J#26grK)q`l+u z4RV9@T>AWWb=xBvuIsE$$5tZVfefRM3)+aR%luybcHmDEVK@gM=7!bIdWWSQ8ri6= zY?{uV4o_kFW$^O9VCM!cd|VFsS$TEl^7Shg690n@vAXz(5KF>?cfvYXgFtEvQcp5= zUwGKys?pyfY6>BJP4lPIA@V?VLvhbwgnxZFt>3_VE)1bk@yTfEqQ*x>Q)vFJ8bJEQ z=-B^|iXk2E2zT%*vj?+2Ly|ImsQn+YOyP=^>R0R_2Ntn{Bon|8*RuJ7HK{Jpz|-<+uLI4erV&!-?LZxbX+S}9 zs^RR^0sn-P@81rE6vT0I_bAD!|1*Vv_aJX$Tb(=>UcCBm$56NR;z&H(86Z4|wFgLZ zU79Z9=z|w{%1+l6h9cMQj9BJkU)iUZ?TN8p(BGf|HV%x{Mtw7FrLsP_%E_MJ#y=0i6{Fp4a z<%9eI24zi!T4sfnqc@X3u!oC~lSXJ<3ip_H)xoZ8cYp6oEtzMm(gX2vnirkR^!Hh_ zhE*Q66GT%Z8mMr@J5ER16{g=%RxzGcD%o`C0i5Umm%L5Q7dv6)``^9UVZq$->Dtm8a~>F8_shV@c+f5M*p~4-=Ve0(-=l5^SmC`` zuU-4=upLLvJRYD)Ha^TA6wh&H@3fF4MnwFQC(rCr0WyfZClUrlj1iYwUj_H|n_QvaFFXySoZg5$?sQeP6u90xzdFI^u#mK%JQb(yqG` z8WHs{e8P2Pq%9iqqq6s&VikCvV6sHSvt}zQsQa$L8F;ZcU?BV^6n2w0ABp8%95FCb z0N)Fw5>)-`9g>(Iot5<{AYx}gfZ@&ivDN_RXn@Q$5j(S{uv}dPOJ3cI{)^e4zjvAZ z5>%RehUI$XCE&yjdLU>ikW1-4oQ;MaMOLe0AL8m`%sNf0FN)4I!DW>> zD3Wv+Zxh8QZUZdCZ=9o*g`YoLS_0-w#+ypt)oj;2bV`gf(+eDKR=qR3F=(tQGY2>d zJMToseBTW8>j4s!L3u1{{9(l zvvMKUCNxu6&-}~XD{Q!|+M=hGNhwhentSNMc6TD}!L>w!a5h15d`^jbs@wKkY$N!M zsLSGBM3cn5p7mw_P1gaoL@;5Uqb&glYA*l070CqIO&~-IfN8{mnHz^}@FC^dGh?qO zeC9>Y#CdhFLT-RntboX4-USgVu6>G|aqVui!s+zWh&x~jcdYk6kF~L8hLkm#;7kKt z==7FM#Tc)cQ%nMsk3986k*sg_Qy=n$j_x0FKhpWH#WO|$w=xNU;97D~mvvg$4kI^b^vL~QLq8T5U;e5hswS=C;QD+Cy(o+5b)u92F&*b&?G2XOU)VX68@ zC4&8+-F-xO5t#kcZNrb49Od~VLqnj|PYxf3rwk$VLL*RbXqnn~lAbZCf(C{4MoU)@ zqUp~JE0GHa5#%m>oTL?vb%z+Io&q6!IVCB;djh#qS6XAvV|}?b*T!NsH2XKvh{-Nk z6-KtjEf;~)7Mg&NW{MKVKM)INYDU1+U{fo8W?6?uz=Jr|?dPx9Y9K%y1RGp$jc)3QU-GnjvA>Uj45A!M@!vX znSj-E1OPD^xX$dX*n0vomi;&5Y|T1R4N0Qu{dw(BbStd3!UxQzeTdgJmleCUO~D|doi3!zt`dyYv4I4j}k zs{gqY{Fws@0O0c`@~QhCB(_>R6_>B}qT~x=gIaKkUd$&1HElPMO>KFnpM?i9Mm2VG%Zp~RCDCmgA0TFZ3rEJ)KQmC4ohRlUJYZ3 zg%#kTNdzoJPHonMsIs=^Y(HLX>caW0t1$qYz$*v$SK4xe0nF|qpn@^5$Y>>(x^~m7 zi(PLu5;9$V23}1+RzYEMnr#5 z89Xi6vMJB~AT@U``~_tdHwFU6a}C{avCVA&GeFG0OAr7C;>}`m*!UYNQPvK9w2RqA zx?|Dl0y>%JIs(Gepzq(fE|Km z3_AkGP*HH58G8ZZu_Qqreff)~epD1%&ygG{74yuQOn(fED6qw;d!jyyRl7zZ*It9Q zNBvk&DT~|79ryTB3x4Y6IX;W(J3wNN{XSliT7F$7GvAMd7$z>7#>(tktTeP(5Hp2Y zv14L>Yt;z^)`SGxn*MUqZCDJR8S`vCKh zb@s7@&I%&Y-*;@eFF&4gv3F?yE?;hvB zGD<11Aoxl}D7(h(jjqP0FuoQPddNkev?=B$+rawTW)}8mZ9PV1>ly3DQInSekBhf7 z7V!MWyER4CE3IhcLK8^du)CTfP1Rt@32Res+_Jcpqh|;`h!z$Wy&tYz)BOA}w0TtB z-ee>TL8Yk;WngSC473%eGSco?Mp#bV5X`f_6a}?ZeQ~P%F(;1Xskz-F!s11~ypj}e z+n=6aNsx{0C<@cu06>F{d2NwAe#K0j;6@}o;r0B)8{g=|ux7|R$$j# znt{7+EsIwe+Sc(F#Pi(u$Ey4!+UVp%APgwMVsJg z*((<1fh^^dJps-UKGIO*W6Nsuu2J(RqyAHW$0x8oW3VL)BTWH9psZbR=^1hZitb%q z9_Jk-GV>UkT5XZ4j+|_0d3BXx8CD!%ksQ;yV044IX*LcqFTHv^Y(-0Lj0_tSwI4k4 z8Rl^v0rp!Rw)9EByO!}h+pv&loVp5@`kB1C~&z-u{3(XOA1&d@gV}E)s zD|E>T_BE~yoEwtb7U8uk9|BQdyLDUszM+9u(vv}}r6CIUE2R;wKgSdViT{-m(_;49 z-XzF1w$00_nmZ`qNZb56&*Io^won|DZ zY$c0FU@JF{WX={AyZ0QK7E4$?UI{@3t)a)7$DXO(D>}i(WMN7_xil8VCTuBKR`{kX z>dQRDLDbB;q-}q9w`Kbv{G;JjTz3jdO)X4f@Bz3T5!} z@C!S@sI!HH`<+q~ZG+9hh{z=|9gwz^ffNd zr{%{we!vW6v-J)B2BX>IUdkEQmZaPW)739V^}gYt4J!BF?lD1Y$eld5o3qP~FCo_d zDNNq!PW>n{=3H2UVaU}!DRlL2L&XO*At;s%)n0_{POB0fdUtbv5Yia9oDTP*BQ z{nCcZ4TXnFVBzFIJ{8HOXq@jF6n6ZPeSV)w6QgJBr*pKF1kW~Pgi#5*E)?WLw&<%vXQ|Ys=u7gB; z?5OEUCipvXohdhS#>~Z-LS$Kknber(agC`R+;`5Y6kSL7gBzI6SSyH^)IBukYDi+_ zLrqnPy9LrV0}gjOcSUf^CjB76E%WEHLpWVJN>Wor+HP=bey}pQzQm_uhojlp-CtN# zFyvBZXF$ittWzj9z?Oa?F)Ye-Qe*4CB8fmJ| z09ZcJ0iaY+M$@M_$%k(2i5p{|;jB)j?-IH}Y&AA9=Q3Zn&37X*%6&olQNTHk>W{N0okx2_aNyy4*22 zCJH&+7|39v!Zzjqt)x+YEZ7{X5wG==3c4h7>miX^QnL~v$MezUEp-cm zqXKtqtEw-FEs^0W!Hl8$5%|7RR5Zkh-ky98?-PO)umI$C##iIS#Dh!0-^%X~U zQp24u$zOLo31agia+x)tqPTudTS3jj3 zAM}BB{)Lv)tV(p-dE62`*2ZYv|8Y8uoUS=ZtbIs?Z}jlEjB>BTsh7Tu-UEIrtB?6( z2rl|1x>s-hxp(L4< zI54m!1m+)gS`2Fsx$>6q;I?ollDL?#r5Mr+H4$ebBjk=!`s{kojGu9BLmMyH`RmDk zTp}Y=6^ls)zRns;^=&ybG3YyOY~e=aD1pkBf_MD%wfvLY;Ph%YKx7bj+U)aStWrxt zgSt0t$Y=}&Aw)*>I|)CU;Q?G4JFAq|kELb72gG?-nfyu72s-Vig8iAHn1?kq#m=}Z zQAXR|a-YJ~b~%4E#D<1)g9qGx#ueUnBNsR{b{fEz8x`QBSe1qUnuSKJ7-ynU2-;VM z{at2hf-V6Tx+!B3P~$-CvCv*OJ>OX~EB}5-;DXH1vTF%nn^6hzf;|O^8576-sp-1p z4z%Rhbb81U-3bSpf)#Et z+ouB5HUk4N&nV#GkTiO!aZ0@cO>jl^oRhs&(11o^XCCTsdH(;xJ=x)mZezwp3{{#c z!YgZN&W=Bu@Nw(XOVF%3YF;vf_uhTSOR@jNKv20;~b19Zz=ZJ8B3Z|>z8UMc_@1b9?V((tfH0V+2nvZnU~oG}_i7xgfI6wkm8mqD76P2M_mFoZV(=>6PI+6RZv=S^#V^|L zjbCQJkC#o|PFvwh(pr`a)}@@Q;-zeolwOek$*dxO{*PkFV-Hng=A<(V*W8DK0n6s^ zb2AFu2P)qXWPe|51y9v*aCOa^>ZrHa0H{WS?8;gfw*u5wL4kHt=}G3Me>NL)7R8it z{r(qby;&C$T?ZudJW-f;mF*#$-iMRaSM2}(5%Oh5{ab!*?Q53j2bB$CQ{7cQPD<(8 z?`%EuYZK(*obs}|9GE+IGR7H-rVuTc+;L8pb2zB$VD(j6^cTGY5)WfM1-bqagKqEL zBOcea&^a(9C8GdCFXre5RIx*$@w_y9iPmxmVw`w!30@$7i;=fO1RcXG1lBM*ac1aD zLa#8B=keO)xLLzhLq{27Q*<0j2{qFt;OtM2yss`chaTKf1)qRDetC`V_Ww4Jcbs}v zL4yyVpI@hQJi7p!#wSmh2H-!hL8t9%U;qLSn(8G`;T5YEZsToY^`U{pgPV=a%tAJG z0j{U-df=iP^;unT3EBvE2LIl|))`eTz?W{JV)2RWh4;Q98buB6v*$Pj6Zq&@b;s>4 zM%nryFgQ8s_=GjLHs3?1u`3~%qbI&F#DN^mkJWB1c~w2hedQ1%bll+I{?5JKOX@9+ z12(O8&}0Ew&l@=kS|o5zVfuN|K;iLVv%%3HH1~@Jy10CGYcRWFKaAm&Z(K5%rB6W) z={@YbVX*?r+tjOz?M49b9e_T0OYyl}Zc2xtMU6%#>6^@ST`IzbF|D7bJDG}sK+dzi zPru-hAme#~Cb4|1z}}?&i#;oW=vQ^jS^03e|K-JX*HdaA>gcuLgfx;SDZ={T;yhk| zz5$1cZ?KXuzbnBjSkg0PVRhv7*&-Tm8xY%gOK?#D~4RC$&H$6yI}xt~Bao zp0-gc8Hboom}sXA^6H&O?qAHm5G$(aJwvmxz;S!8H4Do*b>O~;!vfkA4dy;!zE|x; z+Bb`{CHW`{q9?-sS9GUn)0o)SSo-=D7UWUIr-_6JyvCA6^PP=M-;1yNL<KjscU!&JL?Ml)+<;0gC}wpEg$<)hljayY*x!e!X^j1^ym&M?Ayjt1k(ut8PIU+ z2_MfISrVeVRL9|5NAB%u;6-OBu`Kry`kdQLTslhtiT`**8PUDOIGeEcE4dD0%ct9b z;v=`lCdnBqwbKR}UT0{ED9;y0lHI6bREiHC%Fnu z<{(7zdY$=KH)|R4({~2YaP;kCrUYk0eJVUz-Rde=cH988x~~_b7wt)unP^T9;NrGi>Rj*XeCyx&ab&@xqaT>o#zq;cgZL3?_T zz-guVMsjd(F>2>ze44;9q4=8?XfN;GzxPi={x;N>{qG*~h}^d5koE6}&bv&8+l?8c zgCdZR-026J)C#G<_?88rUUO&2RUdGaZN+*tsY6ffFV+NMn$4O^DJ=T;b~vayNGK=APkBK)ZOkZjkyp-n zC=!101<)(8>t6fSd_dwI_yt$9y^iT50hb$f9TGyZPbT|ws*s2*+b59_U$3^8X?w3b z6Gyg2nCK5jvbRNbct<#Ta$3T@tS2!b{)9+pt5+jef8syr3dScy%jSe{7E&iGBp)O^ zI+MwgQv3s|qY~cKa5#^5s`6l(=RZ+(R0M!bs!?9;Gov!~T>m^&dg>*hbOx(ZRSlx$ zC(v^Lj}#eQ9{T%XHzpMotb*#17#FagwVJ6lY=VV11{%ODseAAGvd@7lvSmRG^EgAJ z-!T1QK#|M7t0ukspjpXE1|5ZA40@@Jgoj2357FY})9@h|Tl<`UwjIgQY8olEQ`j&a zLmQSm68;lIFM?apML1DT?f>VjaA>orKt*LoJ4Mu)t74PsIz{-s2u`T;Ufvy~;xt5xv+2L^dMt#vB*MZROL zUQ2}UosN_8&8e1Mgli;vO4S=)zXr7#ZkdE?A2(;xVx+gW9~pz~Eo{bntTRGmA(1j@ zCl1*qTkXXZOGNlX-`YVnRIV1Lc@G5w{L+3k8JXmnj9GymtGyBL1CWM)>A8}iA{poi zKvejKbH#xYo^h%)DEhb0-|9`*sp;GUuRO{6sRR~tM;xaxb#%Z{56koX8d^#Vj|)`k zo$u&3{*@e`Z+cryxm_EH9U8qGU!M%;T;q~ko5pX!209cYpM%{ohNN5|hjF5NnSy89 zW=KY6KTUZUtGuNhervPpTaaFv&HrH^uGnFu=)!7-Cm{&=1T7>XH`&zX z7e+^tPW-3Uc8(zmX${6S;if6bw1aw5vjl$+vV$BjTEwe$+w~3QZTjkUOoJ$!4`8)W zws`1rJju3rZXv0XwBymbu@!8~s9nW#uckd2ke4p#xllxW4!~)_wsfx{IvQ?{ufgO3 z4{G#++;KB*l7gz6)KlW-Oe7SAQvvlXS#O_UVr%x#iBq>)pXa8JFZN@xH0C%)kD_!b zQS4=k8Iw=7eB%7@5MaXizd&we9AA zt-ml(;+yU-41{b1a{uJH!=1&m!230+h6fo!{?D{&aZLG$1Vee<^-SyX{KnFSmD>6_ z4|kQ*`LH3fI9GvHIv@p*AV=+zps?4^9ET1k@n++S!^)pineD_&32}8vjVi9u^dIVl zfu2J^deJk0D5_|h0`&owr8!kd+V73F+EEf3>|KEm9c)Zi*}BU-#Q@QSOXOH3u2ZTA z=zU{D`?UdUeq&gE5@<>06!_Zm3vrHni>*2*?itx!Fh$`UUy*%Gsa96yXvF|w&JY34 zqdzeXsfnr>^}b5;RppbHP`+4H{sw;33Qo`t!`i;IA8Y5B&FZ_vQ%hoJ7;Ej+(2l1T zC~i!iBY_7e@S}xi`F;ACWn5_8!}1o3+#IQEcy9EyaeL~&x|*5Ip6MD={dXVH!aa+N zix8$39u(_FI;gw_yG{O^PQqz+y2|mmG_T>%v4-N~qwWHMY?2# zA}}H@7h$TiEf?Cx*gUU4eDU&_%CH8I$mX>vk?aDFRXc*9F~7ts8A=8K z<5+j~s#opR>gb`M%d`Iu5!!z0pXTrm+!?Bovi3rjB1i?zwxJo)h`Vy}% z7+Y!b!k&ZZ;d8e!S!7%{u!tKJ(IN2_?#_H^=q72Z)AGL(Ld+3ps98=z5wmU=nTc00 zzZ+;n!DG8}wg%9heRUZvLKc*8IxtEnl~rLjhu2^u7AL>2m;hQTs(OTJET@P#Iz1c} z%W9*xr=qluCvV^XmCfm!PJ{*@agskiaL-(#dtcv-hw>OO3iZg3zGs8Ih!bSj$fT0C zHXY0KfS7$-y4+Jc_Zhbh7<#4N1`)uG5nFz9KEB0FTZ#?~!J!SH!vQ?WF{#>z0HoqX zBw%07>PN3|FTsW87X8)oAec!xxHW%&W}NsR9p;a2MhldszXDnbl-1!*Vu%l#H7LdR zS_i`6UtPfXX8QH9MZ6W2LY^2C$}csKdo*~fmm-cZ45as783o= z4Kfa>R+`5oEeE^k(6V0pB+`*Xna<94T=RZBJu5B&-jyk{)pcQfq7z!hn&y95$emeY z90Ll}4GGztSf)=~Z2t~_ zrbkm2HzKbCazq4h&(9!2V?1kRijnyefQjo!M}J#G95syw%#8C0JD>vLyugB-ITl+m&X_3yAz{jocngNe4<+xG z{!fc4WD=@5IxL`USh1V|1-Hv+P4#V^-8#JOBaIia;}!m=&$yoeaoPv5T};h#O%aB=nKt z^=2pr)gdOIer>SHzH%B?p zKnS6ccKrcmdKXeO)xGWRp!rk327f@!1%I&tePQONV~}-VV>p>-b-{QPc9w8mYndvbd_cDY zj!?G;d*{`VKo{W1Aw;rB3#wvBl_RFHBsh_LF&ALLq4^gR-M&YGmZK!Fh$NaZ|n`X94MUeW^yI3BqJl{OzQo$F$fFB$_r zh;sW90uyd(L9+)0p9cvvABgyZZpzKP3pg*9S9lztny81y9)4n$*lal)GPYHIxxYuq zX_9RdX>m5ML-h8@nw{=*N3D_QX=yNxcK8_EoGMZ0cvyo>(vo`p)2V*S=aVzC{%YEz*?r=_u2t0KTD3Uz z_-Tdh6Q;{QN*gX%c3#)gGVDh>uPuLKOaHqS@!DtPu_-Ff`aD;QOadqL9E72guaS_c)(E4W&gJw&)r*BY0H>7r_8g)=I>ys=Ms6736wPe@6aWik0xNT0l zIvw+v?^6yH$`c&Dsv1OvQ3@eC?01293*TPq_Uu!G8(T9i+QR(%F~ZC%h(Tsa%>`6GL=n zO6?ajbp311esD}dtQYPsdONsc!FCzj8%Q9`C}$Mc(Xo5$wqbyc*+xMj>q>OgTG7Yq z96IUAj{1M=A(K{49R^`{eNs%Km4?GlHEWrLAl9f1SHI@S;M<_Bo#rhk8UKwrIt>s~ zC;_$2Mu_^H+mQ?}G3~T9I3!clS?Is(2|tQf<-_AdfG*dS`;mf_*i}^i+mSYp!pt9X z3DqSSV`uu4wfza_AN?})%)|ct%Y-MPt+gK|*ou7P)w{sGscb}6A|`8?dk!+UFuVvf8Zron(UCPk=&%RKG#z1)qO zGb3ov>G>|W9(aCp*oMo?KQs6%5;J?7v!U)yi(M9di}Unk7_kHbrQgYL3oK&14xg7@ zN<{-qt=S$rmjEyv(qvfg+wpt-T@?gx^w!Aj`NWPvoGBdKi1)qD8W0HymgDF!uMSgh z;HumBSNEb`#|*Q2s1vfxmR%Kk)1j;woyr51$8G(d`h>1TLfa+8bQS(EkZxrR zebqA;b9PLY2C?o5=dNt+;>g1r>INgvr6;5v00t<;5H(_F|v*jgs4U4@@M ziU0_~AS#%meKgQO&Wd3juSlFbsB=tQT%7G6CF0TuofUi0qC_Td`5YnBtsg}>WKB3L zEuFk((?ubmz#K_P zA<`l+;Uz{+c?KCx&E{t`)#JGdGKs3q+fg>cgK|R5QrfV>w2~F@isAM;T?&-FiiT^a z+4bXCad%3A9OnJb8Cf~yB~pDwAvdE(F&~|htT?&tYOMjL8CM{E7BF9+c#W(B+sQsP zL51iB-vOZM*J;IHwICw4&Z-2N}$3MA$>WL`SOGOhnst-@lQ)LZ{AwiEv+ zd!jYb#*xY{j|cC%tdi5ZPgeqBX8M!{D@{+Qw@#kBxhRgr@T~Op$(o!^W1nhjM2WZz zzDIe=Ixxld;dIg#thEb^O=<@og^~&)j*y|W6Z3k{kl4!(#P?`y7DcJJW6$X!r2^m&p1_#VDWpKABhdz*;>SLQRzoOymY<{PE_}E~wrw zTV`ZY=pZso0bU|$2eyd|RE_u^LAz_-;$>TbVt_Gwsi~|g4=tH8)=50x?|!~ZJESY$ za{TdZOX_L2zU1sHk}3Z>zFCZ&I{95SkOj5X6eD4Au`~0vf$ZJ^Z9 z+vxxwL2Il%^(+xwO5~OGT)>*@x+`3TGi6Ctcl0qr(({`&T)K~_N>5Zn&2Ir4s3?E< zb%O2*@*NWH#vx%7gQ$KGb(8>GoRTw@>Tk6V&t=N^yC5=-bb7K+g*E^ZVV753XM_5# zcL5Dsa0`6lfZf?betdt+jL#}T1!IXjX2kAmaB@nNm_1C?wp)r298Xan-Uncn8|sys zTmS!5<~~6yNjZi#If0s5ooN*EyjOj5ihkOgN@L)xD+#eGFq!>=58tXmnbnJ9Yg6^; zCg3fSKlW2bf`+R~E^p*FJq;hNyd2s?B!U9wuQxb{#aenX+W10isJW7!ogZLsQ0W6Z zHZfNqx{)^uNL~oazSP0;)%#02&rAhRW!VLfmSGm@4Nw4D18dH_PJ$JewcJXRXq0XJ zZ7n8jyML1S`MSi_PZ*tY9e{~l%De7@0oKPQihUEmF0<2xU|{;iV$m`@wF6y638*B4 z+WEpWsrZ5|#_1-7oAG(%EjIQOi;5EjzP=l><}>=DjRwHTr4GPa+5@%&egeX~Y!7nI z<~B|`?V30pp1DCLHm?`%DdN$(DWoy*_QNeRH=&F2Wo#7DwY1=4`-r3W6h8=vCH5Pw zhk2%Eh&?afOT>fYq>xy@HAbghPN5nS=ATDd*6$oXY_kTaP=U`hr`c?@(! zTsaqmd{q^#9MKRs>Nd;P6226}9|()|16kAAz`60^;i^u*i%T`*M*L=!zJ;#Mm^xFR z*pm_u>9|x_C~PGQ%#ez07vN0F8;X;JwJy#Y)lG}An9u)5-p3YjLO8R!Bd$*EPU7`1 zPmGHO)WTGv(JoNp*oLbwj-LZ<;c1JDWZtW-58dZ?9G0JC>a;N9MHKd<{ddgN!~DJ1Y_q5= zOc^3L-@_S&x5L@%MOvVI>iMUA=Mjl!&q6z5a8SL4?xDlmH$`5%CiymusTBG*v0G7= zp)krFJ&KN3`_}o-@8o^{x^%gj?FE!DU{{g-l)HIlkr-FKe1gGxcUVJjFM@J z%&OJbrftOPPOl|;A2oFqTg27w4p)&EXOdZ$_+*B!ZYWwE2+M1zT(9cYy7Af;<1Dz- ze`K>mJj&GPxcwn#ejSRVSYZ&y>UX_rXzHK3Ws4~Tn?Pmp47x`RZU{e9ERkKacqEa3^fEm{IGc8x)gp5G_c3QBgRUBK zApk1T^gPlbrdTY(dfy)Pwv0_NiVfz=kM0@yH^}o_*|4rkh6*fA5t+gV_=->7kCFs5 z!UanmvZeoVE$ zthBsHNR&`g^`n~UVjg@b(g9Q^AM10V9$idtmrBg)tWr5HK(H zWgS^GNV9klym|T*J~{vfG_CpYdplxk4q-ivMNP}4?er!k6!4 z()-4n;L>&fpxsmq1S5?vy)N8TS|Bc2eXfDkL@FcQf_OOF@xW^dNnJq^Qzj{7HffVH&&|u+nF%v!}w|Nje8zs7j}G7E5qC=;g2fK7=%F<_mLLD1Bcw612;r&qGvrH1sEvxhMu&~Zum zhNwC_671KBqkRczpqdux{3J0V4O-F7utU_L+%G1yDFn?}3ega#Zd^j~MAL|(&6<;R zcVW~qYzHMNE~nLJdYp{8YgQhjv|=xNjv#qmq8fkZJTn>fal(NED72Lq+OVN5nviVI zmq&?OKe~fkC0c!5!uG*_@lzcPS6_qD;X^Y#`#m#q`eaqAo`EAqNpd7Z0IR3{-Wy)K zJd`7lE-!NJcObmhXoCkTFqysMW88){TnkzBHeZVQY3-@u#{X^!0@s!JW`ob7hDIK_ys9$Nv;^+|-#AJjXULoT9%6 zFD0eaNIn>l8O^X)K^|VsK~cSB;4N@eLLfNH z`|9)u_ZUD7Rlz~u5`2?kPgWpStr$MFRXbbNIe5?PH|mDToiijYOV3t%@BUKgBIDEl+;CN_m%dN#bbIh(+=z3W=2kqi(`ak6rt$58zrk zEsB)xJ@DJDTIJj78CdvYN*seU3jcgAgjMOg5$HEmUd#=wMd1CL|As@u=$vj4Z1PdC zV55wn^TK92lu!*wRZ$5v)c9|$$snB|a)jOuogg0HR+RJp3UWmqTisB$Le$Fw$vLx` zNyd_`H)EI09lTm+XQgsvArK#P319vo_7R-1LRF0zq=d`sW8W)2kT{plU&VKK;8C*V zS#MP{tB4g|&Yeb-2T}PDc5x`>Nriyf%sQ3ligIEPfanOE*Tg`}xN{c-G zT;3#dv%AE+Z(Q5pwy2hNrcr6^w;jr3+jP`^C;@v6i*aTRRSs{(;mhZr!euy#BOjHE zs*X83<@Jv-zgl9VZ%J{}tIi=xG>`dCEyUyI!D(JzfG(Bw`G193pED!iTHcYzw5X^k#++7q#u|2@`-^I3K{TL;-x`x=&> z2!=Am#%N8+5LIU31X;JR4|6R$0bYKUzoI{ep*9qoC3<+fCy7^(Bx4`x9#E^vHNr@d zf~H7}Z2L;UZ~bes;xwHiJ9se-zu`IpFc3zymE~`-?3GJS41(Nm8;)+w^AOoeqyBQ2 zt_6q!*JF*D64w^H-zC%JB$c0g}I9-fKUv=lJTYi;NM<>q%zJS4uc zA#ksn7x?JLu$^B9@^uNgV^B;}lLeT+7x(>jLjTB`DmYYXF3xd8bmyz z_7^*3N`UXI6DE|*#TXwJ1R3C`^%+d>;`lYtrlXn*| zJy_Vbp^YYc+oveB|5D!nP}Dnq;$9jiL?FKQvuBzt8A>f*hgkoQ={%Oi?58Kd5R10Yc33+qfw3lM9&54`DU6+C{ICbSf0oNJ#C5}R^h6b9mo^5}FZHpn` z)Lkje)T`tA=e>Mris1x2mt%QV^PEHf_YuNYv_#b&JM+qE*WwCFBNJx0OrA-8054~} zGcVREwP^8iMO*{+yxM4aC?7sQH(5pmo{q45`SShAnlL0*nC9zWMDZ2BwhaM@Y#gwxq@gRa?R|=8et&GR2^^yltBINriE2>8d$u!zi$LQv z)>q4>hB0f3&b@hiqo_z0mML%OeXP_mxI1|!d5@I+6bq}Ja$I(IZP5&(v4RZd9H%?2 zao9z?vE##;A@l8W>}qp1{GS?pha9uzyz;=N+OMK#jeKP94;N4UpNl7BISziWRS}>v zb1E>gvD5tIo#wa)9A;?3^3+Rnj%5hS1dQSyQ?-^%@r!hTryPUe*^{|R_tPHkzfM4n ztcbcaY>gIU;f;yZF%Lf`k<<~2c58rz+!BcJY9;WbU~H2wf75R4`$E~xKd;>>B@XiO7<)>LmoYJvlR>6H92DI^cEF{8 zNmM04Y(LLQQ1rY{sk8^~3&GCMcszVQE#iLk0Ku;0p@`Cmfj?ocJhd53) zBcFS}f(DCB&zZ5aTsscSIB{vPtwZ=zxxX(pf=JzK4gz)+u`n0U!F#q4w{^XE#8CL& zij!XnlK1Z9&lF*3^9Z{p$9IVl>i95QAZ;~Q>z3bfcp2k);0Q1*c*ML7BX5|xKEL^6 z$kG$s1SJT=Z)h;s%xUIv3u|%%f#Lw%R_w4vJ0Nz+f0p;PPlhS#@Twf%faDz7Ja)8r@BejjA=OrM) z!IQ{b?1CvXFFd!^-)h-A0%u#6!kw`Ek&?D0qY0AN$m3gL*mRg#L_>?m=_G7>qN&$_ zusz~vgYmdP#eu(HI*K-sY_5X~`no$w@@gU&I93I&!uYsf-~l0jXn!LlH{lPIR@!(| z(+Ki+C`2K+?b(W~4BQc}?Y_CYt(h091ZL9%cFr-2CM>CNnmgcXqX{&s^Z*2>8Fdb2 zt%Bu|j+EQJ-%)YGKdx@0tqpfWEK8eOJRgDfD^d}uZ55F_sRxevpj9?weQ&$6W|3v=ve2XlUO7N^TRP zoFleu5T{B*wEVhN8CVQZq$OZdwoD^Hhn!*!Et$Kg*)hRTHxBV!*(1(ce(Z!Auh_Xe zj+r?|QnNM?E*#z{CX(PLGJM9j-TVc>3SQc4Dv<4ANnl&0A*7d!w**z=_ch+-dzKaL zW&h#Cg+tij1%k+ceql z=-~`BD4{n?zLUd@ji$nIwc~5&U7K;;@dK@2|E#>}Fj+e$6aK+Lk}>Z9S25@`(P`LAcoWSarN3s*oKB~DSE@rP#!aNNK=;DVI{*T?rw))vf_BzhN-}(G zu{-p)A!eU2B?bBzW^f+>Ojb6M?72kQhUAy||9g7X9Z?Vv)pF7R7jPB&i@-35-o;!R z-GvKxAIhaQEa${S1wlAdQS&PlLF?|Y59?Pdg2^2s__*kCUwuC}t-OpynS<+#L_zI! z<(V2buLl>#-Ko|lFS;sC8f!Zl^~IB0X01 z3+GvuyDEQ&ggiZVo97~0Q+`pRGH`vmpa{80(MW(6#oNr)woE2ca%zUvxj^+9N;9rV z1=->&@t8`H0`SetnwO*5?z+MnNU9`#GT}Ym2KbCC>S*d zRZ8>IU^#%Wji#w<s?gWLu{#Ajw$UQ(5_GgiJay z4L(y9wg_irI6aPS;nse^f8G8AM1L)Y|I}=~0ZK)x%&{ye(Tf0uX*^Tkv$n0Kk;0w9 zjg#wHPt}UH=r%T7|*dp1v?W63h>&%Q;(l+{5L2+<~U z95)i#w@>2_=!gfZdC|(jpUAk;bs&+Pq-2l6AB~%nsmv?KX5uS%3}1rX zloY56o<6U*cZ#6!IJo`qZ$Z0YK=s$hkkMiw#d+g%1W%rajBSQ!<|8J+!&kMb8lX@f zN?>Lj8h$5xdd=dTKwsOM3X8O=U11omhbQJ-worF5gxxt-SXR&`KhdRqHIzs*&V18} zsb#{~_Bpcv&L1Zyll|rOm~{omoscoCf-dci0)S~R68Q|PZvzY0bB6u60RM)gpr{P% zc_MNf4YZ)9n6{r0d*^lcepiA|c@#+93xTtcnC@D6sh$Tbuw^wm1rBqhD6SQL2z8Lk z@+ISy=j;y*dkw5A*oS<<4aF2Y_5niUlgx^PTGL3~)vVsfk()@N36rBzHocQ_Fk_>e zOKew^-A(8yd2$T8EW(#md21m44$DTESVLj9n(z;^GgCR?<)A5X>}fVfmKoPT<@6vF zNn6qcjWQ><*o7eBF=rVYV0AN$umdD18I7qcvEw;io zkL7%3CD`AybZ>?};A>|a;XyG_+3Yemu>)6Eww+G}uYylb{c9-ftIp5St~guYFk_9*MF`7XOl(gn$@xz7GjlItFoxqiWvBGp{l&@oQIQINjYI| z?v2~a;gsJ36mttP`qZ9*L4_Pn`ld6q;zZJ*DtMN6`gi*cArs|z&6M{}Ss+&u2#A`^ z14F5vGb*zHUU*NJ0VJL3Pc2roCne zBWja22{i_yebVqcQl^@*5uuL=NFbVCL^6csEXjiQfW! znUuin{XP(XzLzLWEp_!{x83JvBWq?^ z)e3)R*S$OqtRon^vpU4xsdQxaDUa}ZQswS5-`sg^aCje znP>5F4zcKSV)}j=&=$te^I~2rO2&H%_Kdp;_6V`Rle8f2zYA{h5Z8;A$A(E__o7jG zQC4i~1=~ra6L?xXwOFzgmOY|!Lh&GRUj?{+b9{9>W~eau=SFHThe?EIr|-0zi$v3S z=)hxLSmxVFz+#8-OP8{cTXH`Y8AX64 z4>ii&y+2)Y{&%%GG1vL-1p-Mj^!)Oay6{%<=HbXwFsU-NBW_`#<3`%tdUj#%q27O} zv;D?{KKDAGdk{~23# zyK(#w@KQW(F@AO^f@RJje5i@CUeP(sZ;Xogk&`V*76pxJqt#8WWvNKKGLub?REOlM5u zDPq~Te2m!Jq?g+cGt;heY`Gb>eL#sax*QskA^Y0O&^Dal`!n;D{-@)jk&9;Rc_NqO^8ZjFjf1?cSXsyEiHbWq;Eex!U68l^SzP zOvsi`>aQu&iILrcHc@mg1HIl}7U$rYka>#z?{@DR%&91HbgM`4 zzU{@x&_&R0!j83*%Y^oQaQi{PLTRp{mYNhoNbw=J)B!oxaaeJ5ZZB8GJcEJ8P{h}$ ziWAOVsIu4q!s4f}=_|@vclPyhCqQLpKYX8C!ty;`C~rY*w$ltek#p@31n_R9^btO$ z$%c>(!~;tqDd3wB)Rqw@1pjf7EW*$ZbU`A+UA1$7jqiv=Aim|FJUC~78Ke{m7r2vD z(Vha|Fifr0>}AINaXJ}^`bZhq?Q#4q@Vs>`DOQ>p;ABtx2qQ=HUJ8NA1;i4EU_@*( z*K4-tBymO%>&IDPIo>sIVI>h4I6xyw?sdzP}-lE%En=0$m&OIoXGl5Reg?-A1&R&dmPp=IL1PVjl9l~jk zVWQFnmqR1Q6qBJiKG@U@u#=Gy&O!(B>Y-3SH#N@lr{@`?FVm6f3e*MgP1>f=K~{d; z@_#*M=pZq7R@>2&UDJ&Zz#rKijFz6vC|wkdRJfsaQf$3--YbjImwnM$NFG0e+7ohb zvm+bV0rQTGaf7##hT~4H{wgDmWeO7I!?xb^n1%UC5x}KZY67dz86i*&>7~7G(?zFN z`|||_KqyW$6O4xQL+pipmSyAlm?*7XeO@Dk1K8LSa`yMFwTy(GxA6sk@!aR~l)`Q9 z`kwy^+gWG>AYbcI`f}zs-#1p61&_Y77L z^7OvE3`2_ZK`wC;Bh!K+OD>j3g7P9Ax|(!FIJ&M5s#WXmI&TFU0#LU@daDFSpPh;_ zT}uIDA6F^onFU`vSp2-Zsb2K%+Sm1wSFW}~iY4C!SjDOsZI2Tkr*I~w=f6{4+ z^Ju?3H1D6%nEpG_Ly8vd^gvqPG{S9-@mjwQ0;KiuOeGh)Vv*B6H`d;=eGTE+6~_fU z?7dD%?BjuQ)meOpYxL~=-pw6;o~9C{0wM=GnpJy`Lh(#?gNK%&USJI2=X)(rySZ!P zVmIDLwdELDhPqnJi$_2dZ;@2>y{Oh~bn>@lrxzsDTFeg<*DWqYp;zUiwm^t9Y`tk7 z-XQcTaivY7LjCy9OtKB1p0hg5{avQTrgO@_iTN?9J#EU|KPE2|rm{-PGtEa#anAtQ zpey2R{2sretXZ+PcpJyl%BCdy96MBm3PjIO<*{XZ0<;+jmB zn*6|4h>@7MjCAHy`wF4$iwfbJ#q#2|L^*UC4R7qzqJP>y{cE^HVI@U>V!_#Hx=PaP zESIf2CI2hyeGU!CV4OIVkiYz5MdTA~Z<(c`Tg@*8zz7FR@@rb_a&)W}7JFqIC*N7U z*sZb6E=4M8N}3kxzR1u54z;_8%~>WT(pCoPGW0!b7$$Dq zejE2?p{d)g^LhtxH@>qoHNSj4xT!f9D=`Fxgr2z*igOmqvrPFuN@m_9fz~E)*)`@~ zD&0kU%X{$g@Z>D*Ac2pX3F4k&Zw1BLdGuh$urQAW_Kzc2;wp;{t5}W2!;>UzL7JoSy-nQ&qD;0OFjLu@|XCP23Fcjcy?O;Fe0P^p&W*n z!sg59bOrYg?wE4*!pxGx;ml`*^%#j--~L8&bF`1kv+8$GI1W{X_%%`|JS`0L#MI!y z)6Q0E!)2~49@h&BD9!BrFjT^Wazanh$!^Ow?%}{u35}6C5GmfZ|87L302km*WAR&V zVl=AL&gW-`Q|@<3FE!7ZU(U0DH*8W!C|YSl!f0F?NfsuG!pwZ7UI}1RsxcF^D%+6B zJS?r%hm!_fI(6yr#$RWE_cUyy40S?8^`l?iYMjL(#E=14bM_Xu z`+q+A22RFhrdQ;eRMWzB^$~EqZa+^Y`t66TbbDmLJIu?SX;n6j*jPTacUp9i&ZkT(sQs)NlKKdWl80qMbFb}UUCcbBr)TNjBmw)(;EU!5JOWe$ zZ78)Ki*HwJ!!icD<3`xp3a0>wzA`}d3~%H7*b=|Tekp>;)=-w}3&fxx__?IJ%pU4# zRWqaf)U6m`JF-8x*QIOb@y1|0GT%Oo0ZHpX4|n%gC=%B@G(7VGZ+gs|SjmMVJu1VQ(EMsYE4 z1a5{^W{tLurOdK15}x9{SHFe$NGOHaN}YZ)C625bclDl$l{=QP9D6(JU157 zpL^{u&BvrUA6UT*7#iK^4iaLaWYZ=n>v~6A!gQow5}F}j;0OsU5fOoy*(fPH%-if8 z_mG|ZKeW~Bwd_}4)jta*B-lN-B8bm@O5m_h=FCKgQOF#tBPhkp;kYz^2=F`(**m-$ zprg511<+76s1$eOq|)w+1#&7mUZ(E+ZfWabNou5gaeuI>>i*2?eU8vwCwx~V^wDiN z`v-+9k!f&ohqsR6oxYZXVN_E*M?o1M#98<#5{76hzX7jUcGJqe+xc+_nj_2vyJv4? zZb(hB5tVPTSn?;+-%R0(drOQ^ZP7d^DP6?Nb2)_jRC+bIq2sQh0!=iT-}amYN?JFSUXkaen$yp;MI_%%7C z&&Juf<5>dyJQ}ekVSY}Cb;E6|T#L1i7b{=+?AfImHI=VfIk^Rfw5Mep#=_HiPIY*S zrX%}px+^jFD>}#^Bg5@Z0(dXv zLle)2h$|YCY4qAX%T47rRRufI!?PZTzR%afH~n!YY>g!B8U+@NjS;I>Op!LhJxiV~G9@I0QZ3cT#m??jZRBI%O1&h+4nIicq(&|Lg|=+*>%Wl zN0{)l_Q6!&*gVsB8UU(!(z)A$#*B&m7FL(+xV)%U6OFXFmarwiIl)fi`pb9*^Lm{< zu2UD{{#;vA2Lrvm(=wo#?V%4p)ff9EBxD#Qb>~UMM&@rVkddcRK`aNAmD;o8T@ z2X=(We!n2K_LCX zHi^}dG6KG;b)OC}6H(7M!R_L*3*imgRkHlzN0Se)gN0UDTi;o%(+;G&8s`q0NfQMi z+P@sk35LT{Em}mDRQkmSx0RUUzti}q{vt-&jAcWJlH`6YeM1yO`_4lxAJ zxap2JTh3AR@r0Tocg7A~Z>tc!Ls@#9T!m5D<>X`6u4gEYCNo;6tSbn$+WXOmqFi8$ z+dg$hkmTH*vSU`;w)>=@g2B~i*b+q>+t7YZ)RB%50ht0B=gvurEmimr1**DJ1aS_q zuxANl*ZMT1vG_KTe@^!uabZ}_p7l<(_7N|CsKQw|XOma>U3)*PO^hc$nQmAYU{A%xN8 z)yV(_vm9SSbRe-Nv&L{ zQ8G)qH4|$3H90qyV_D6VyggIsr|O1?B8|#;O3GfMJ!&XJ%AqY*(<1``YE%nCS<{UTvSrPci4vcAEN)^<9}wz(J4oj9gb&_uD^`VP%K zbYJ=yzp@I@k2inoAWOriYObJ1B+sXD6vT+0A;;&@^d_y}POmM?GD)G0uO^Ty90;w5 z`ERi5lvbk~EPF_fj>$Bycf#XM5miFFS3`)WhI(^YpE)BNKf2aC5@4 zZh7mOejXt??gclhna6Aty>U^^kBocH{&rU(W3*&Ef-J`1XxY=PZT&A%w{d3B6+0KsE7cj8iQz%Js|cIhQDPs&z#3As z7BfDuozMV#MW?%L+Cnb(Bo#NLSJ^oe$s5qCl>Z6s>x4A*N;QlwS#Tz@VaX;2ke!#H z-SO1$=36n47bHebX$g_S_wYopa;z~CmE6o3t{xaYytRruNu0;*23KuuFks(MXqXCP zw%93=;ER*`C-?<*(s)txF$`xw?;)5dJBU+tYOUw44n42r1tk&%%H`uI!001!@*q>f zeRirZ@GA4{)QS#chwaZul+G}7#hV`TZ3U4gMh9MS6jx{|#SwNzhW`+yD$olF@fAfof-3+MbtanO8K3$SrL-NT! z4nGSh!;Gr{Z;tQxAYfs;PlB~r_w=FQHb8-YVhr;yRLkH}V;*Jz#-x^7~ zb##8pdbya#1o6;a^)+iWJdB@e>z);Hi&rnqW(ZA!w@W`|P!5-2+%W%ry5JeCtH?p* zk$&VnS0&)zLq#hVGm4JjwIGx8sYL_1e7NO@+e_D&P)=<)@IHC%lTlEnR`KA|?%rV4 zyl(U+A9HBW^+sRzcFkWAO>GptYqw=;r#?e_($Y$9e>n4e zRl6umXtgFlw+V@O4Q!0A6|_qZi;=B;yf)rpSeypcUo5fPmA3)@JYi07wlkYDW=8FNzph8fi`YtN#H?Dyg8& zs_MEm8i7t@Ttc*j$u;G*ZH{*ZD;{DOS=cujXr)Bl0d4%p+*x3_`#>6@0{l)vD7S-) zzcN*3^0V`04ZkV-yjVcA@bRD?h!Y!#kcrr2H~acFI7XDBb*sLg z+O1v_?U>CGYY}>-Za^E;E|7T?!uHtn$?Vu*Ux>rAZm%$Y*q%9D^F0e}$P}<7Pvi9N z5N9ykwe^IaFI6?t>bs5~@=d9pcKD#r`AgYJ4g-45T*L17>K^*)mjvP^a-U)d5`AW9 zc%M56KgBPne(l)0G-vowHK~c*mO!jE6WO0ZL2k%}k8QvQo2W>cfNF@3bdfp=w)yQn zWfCR}6+t+cJ5r^>E?Ha+^sE*EE|eNDxGwN@RVneP8*fk5yHyK##TxV~qx=57NueeD zK63R>V6vYezVgj3M^;bJYkv*r2DovTV~Si+*EQ~qK;4;P0N#zAp|&sEx=0gd2CZh)aT;5*4CZv4KNI4_aN$J;tl{RJwnRx+4Rke0%nZT zf)c+KT3Z~pdx=o_VmEnq93nI%k1g3Siw=33*E|u!yzl-V9kknYPkElS-iurX!gm7p zsy^OvE>bksUw3!;G}MB));Zxdv&wzl`PwFN2kJg?0BP4K0h8pPV;M|NkHur&uYYx< z;qzJ`(EYB>1nd)%!03z^XUTa5 z+ajSoti3%L`x0Tr6D#tavOOXP|Nbbkq;BV`{?)S!9$4tw<@zFkQ9=)WQ@p^jjxRUY z^PA2e&itI)4Xi(a;ZRw*=!ImXS2)XIsioP%QJ*AT1~|}JbwR&K>ft=jx`rrP@1is9 zf+MsDzwf&z7GDQJLVVM9CM5j|U1aC12a&W-wQCn=8AjIB6&2H0lw9ww-s}lWrs?J5 zxc&w)hwklPeutDSsD0Vt1^fS1?jw4>&LBai|M1?#m@{k~II=DWgd3l0$bcr)c#7c8 z!5_~f3amCG7}SF)5p&PJb9Cqw1Yfmkb#|csnMO@K+3xZf$l{){FC%H=d!!dy6855Y z+PviOuxh9B+7u`7(DqRJvD3&Xl|x1o%nX#QDd02Gsa$bHt&s%dJ}mQ@*vI-HybPYL z#<|KcavgAQC8I8}2r9um#~GZ&{rJ5&D&%UVsz{RmbN;zDev zCYsReP~`B&;65ul1uHM5yIrQR%f4J<$>nWUbr!6G6lu&rU4i4nFA+cjQek(STNQ_& zoi&h=q|N1K&l8|-x~~SYm-(|`+nBji8duV63(0q!Q!W2? z)?qw|f=Umlu_;v?FGit30ZYiDLQG}3re|MKxcQ*L#Hd!VG-WGmWX zII^`nOko6VCzxz%Xm9UyknInSE9l|F{c%`jN$LRG3UjAqb6TJmz8J0^vxOskp9MB| zdI4kdLEyh$9Qc%URU&yk@{OgZsMZ#k5q;kxAN0{IIUefKlTN{CXf7XIZ#{P6r`M!) z`t7uThvE&5`0o`L#Mc-5%TV?j1>c;AM|#sdozA?W8!lyyW@ zTS6j=gBHOMYmy+Q7o(9ol9w>(6MX~#WcVHpp2LZ4z8v&P_)lBaPX}-!ahW;x8cqOO zQcNp1ry?UCw_oG;-Zjv!;5zwMT3s|R$M_zdJwFwx6mCFm zU{u2XbEs#cajrS;VLSu|=5RRK09Ol*qJtyd{Y7dvXDLHxkgnL8H*OWBf+o?XRD?DQ{#cmRsAurPuDL`do~DD%3#e*_OU`jQx*;eq(EUL zJG;6OWYbL_@1D0_kAU1H-+1_@HNcj@&?k#X$l>Lja8=Gp}?F#LCfTZ&3c}IXv@7t=Bd}2>6OiS&2|-ax~5B5;r^cY6P6X85>Koq*d@Fk zfxQLGkW_PCCDp9Lk8GvQC25+X>^U@>gj?b;fe{|x6eMLwC7<+zHOQ_&k;9cFp`*Bf|@{bkl zqljB|2@v|Wfz%wYxUfdZZ)H6meK?)SzOT zo;+RQz7$op;sVAAeOrScj5_KvM_o0bd%Wcp6SP+n<)xofLE@S*0s~}0smBGC5du=g zJX)$crd`O2>s*FI?Y1JQjyPExk;Ac)T3%=E(@IhM{6YfKkeUg?lSLJ%33VsxM{Nff zo2&aiOh}7o$8SFv!{wNp4xm7omDp7Q=nMsK(BR!NXhRjec!YfipLdWp2OBZyxjvwq4rt`+{4Q zI8+j$1oOhm0ZXh~aP;D&x`c#7w0$qg&CtsAfD(H=h&_)4LBp7Ln(d9eyB_1_7|+`C zCRnPev0-gO**;MKWaLD&-0>q!9(tIf&!?d6;t}lK)k*2FhYkAZQnYL)@c3rx%myGX z8-+w*ODw6Xo8IoUf|BynMP=_ps$TI7l&zHLhI>zXIU(TETsxw z7=Z;s+W~q81Ih&;?OI-3EqCfI8cZLE$&9_4%Cm6bX*rTPtp$fPUC$NYI-XFMoH6)p|tuRP|?N#1%q0=g7hoUT9t?&bH8r{1F` zc7HT41=|hZUli(GvMC^RnGhg*3=V3HN5G->(psnnK#+(tqz8mZ7fv8VKXDgkXDwj{NM zUkT?Gd#V~$uMNuJYc#bV!7YCYfiJ@I4%-E^;po|;dWLJ1xwF_)?-~oO946YvEfJ}> zJ~rH4psi!Hj`xK(0_fzeI3ejRg}iAnVx0i<60vUDSybs{MV1+E>xj`-u9wvOXcZq> zRMaz4i8oAwQ5|P@KcWUD?4Hiq`ov@tWC~dl+QWga3?z5xy1^KrQAA75Bc?=;aRbs6 zn%`fA(X2;?z5U{Echk-KzCikUow=sL-D#Ni%NcWV^rC+BI2?TDTb0tKa^rOWM=M}QK3o7+uD#P@*Jp(yGAL$qr=fJA z?<*7L_+Sz$MFKLJR!`C|wCP{L@t`?iSt2aX4j=%?pT@JCt57DFRUMX`C{s~VjD zl_j1i<}w9Ob;s9$xg>BV2|s)Qdej#ddk@{6zEsBljr!SWA4wl8r^ju;(Cy2?jH;{Q zF)e`(F7mH;^(agEy6K?T0&6Q*XW(kbHZ58|sGJIzt=HGMudcPLE}BGO1XUL*|JQ&p zH*&_K)NRxhb<85g{}VGza8&sKex5f67t=%+%zwm-Isu|S0_SB^4qwE~1v zJhB~PMCAwS7K*l!wsOn2!utcgrLby9R+{JaSUi&>sl7>|dh5%$p}}x74Wq`HzQ!rL z7TpR!>rL=19)iVSHS(MV&O2owMR0p9bt@<0M zmX#e6Tpwx2blVUqS#*53Km@oCT6TWkyli_qe!pBvUx;46p_*;I|N-nY?K#4Mvm zgzWv5*hW`h_weJ7}?Tx@)!Lwa1rqVfKbL|Pv3S=rdGJUBAy0sq-9%HPM z2L=!ON#&Lm{{|}yl{Zoo=@l;mwJF+9tAaU!ILEy@X~xD({h&%Lscc(ZX&81X5MZNG ztKX`;vo38E{qfc(cJOJ%gryWs?t{xkcv$oF^hqm+-Vp7e5Z6y(#%g9CD|R=&A?}${ zmhQ?((1ild?@d^?VS6&GV_$(GM0ZN^dTqBb?E9-z7_u^T@wZ6`@p?Jn8J8}qX}j2G z5v6Zgk}sbW4HqpTuM_I=rFtPec0u78W@lj6t1dx8(|@tpDYv$n_4G`T6Sx0)2SWr1 z+K=tlFfIU8)rCZ;Uml0@RSGW$FKH^Q%3MmpQvD~IkQ-wj%UGyYxeUhg6#2}MyE33P zj*eaXRan`_gzvq!X@YXYp;4FPBFXj3F3uhmg2F2jZC2v|@;^m>&lsEF(LjvdN%q;yL7e3rtO#VyoF7QQ&u{*foBn<9fp)@3RB@j)I$pJK0(vslcACefj^P2b^e3>@4J~|}Iz`fVC5dptJ782eD?gE^ zx!4G@rNGt*-xohv2(S_SVsxGbQ6L(UaC9~Mp_z;sx0tb{w~D5vB;IpWc~SqP+F=$=_+=FCE4>9o=7cUFqFX! zZgj^53hZeZg9CKT=$ofN``cp4<5(?fk^!Z_f6MtHO^!+K1g z>#TG?q=bt19-m3mERN%$-D?w^8#9Sow$Ze?#U&{I5%8>1E({~CF8C4ciqyxT0Lc9r z_hlQ0D~{I!+wGvpy0+J-^w zlzhIAu;3J^$pN~h=ASs(RmR@c7JILGPy-R5ej%$g-4**1nX0%~2{!goY-roG12iY9 zuQjag0l;SOL8(fCG3D#`+q?w~ZA>E;6_lfC<-2Oi!xzi=ygy2l_@g9WN*1JPk_K^v z9~5X`VB)%$zpO5PNapxGDaF;~(9N@As|+(LnOq!-i7$3iO-GvII$t~9%>m{b0Bu+a z^cVTAiZQJFZJIzC^WDN}{2{_qv%WI;_>`IDN9pzLrj;(5erW_*pAlI_7S+eWQNI_X zDKCuSxvzCkP%TK6_yw)csSxgzZ8^BmH}?3uOtEDu{gVfD8d^vGFOU#3?){`OsDP1$ zYaO)NWrml7Jajj9-d+1~l)qp+8@nwLte)pFt`wXp>bJWAGi-*^2gR1Fh6J1;Q5~?` zlM!OG1g%d2s~mlN#sI8CnINVGzYTmXvVnTeWJ9sy!<}bzjLGzf;^SosOaw2r>X#zB|@2%(_S zhdae?m3rkod@D=ZkC^+Bhg^kqKQY*hb%GrhQTUR`y|U4Io+%`#Y$h8JGnU$vFw7tHjacn z+DgIEo!#0BNC}5IaR@M?l>95HhEt{HOk8&%lF_uLkWVAQ4V}% z`m8;9lWu{Q&m+RVmzm_QSjzGwI7~D@9&7c_m&nQWKp2_0G>Z7lJHF|Nu>&x2zqnEg zS8>8DF9i(N>{7$^ly{ULrZ)F&3srsTM-~~y(^?@Z2wHjQyyWm!!Jvy%N~m`gigrD4 z4f!ORBj594*nK~i$58z;({J>|M-)2|ze4VEjv^v#1tKva05a7liwQ9eKbT5H2)CU) zJl*W4K4K5k`Da`_=3wIx&6n0z)qn5aMyb_JdE-b=Lz}d0pK(P4ojV7=uo8yydL=-8 zI`l>LzVc<BBb#vWv{mr?KpEGr(>x9ZkIaUylF~{(FeUx`( zd^3P8jrBW?`KDL3QN~HW_z%`tCI}}aOhOe=_$Z1{S`rkkqy}xrHfFR# zr+9>jn5=dTpLC8<*u&SJiO{R!H4)RPYXQV7CUm6a^Z#NE8Ki$2B8}bncGEfV`ol_2 zoXAF?;cQ#-Dss}@_)b7gj_jbdxuSFxquGpR67%tAJE9)6MA&Sb#iK~ab8-F#hJQNr zgY!$Q#qGtkUaF|Mjp1AMNiCVs0<)_+xaFOP*vGJ%?(U{0q%>+fNm9w_ zj}l=MFZf3K3KoGmOunmRECcH!J`cPR;&O`l9rdojJ;@LoBMtcM0TN zrg=g%QX_|ZPuA&_ni4=cypaXhLx~d5a_k|4h_q2-?*E#WE4Ry-E~8vwo+o@{N>N)Dg+BE-VEDUX7lrCo4~^ka zE7LUQmlfuorhDN_mg2i3u}UTR*(A7uXkzPz1oBgUH|XA!X+_?A?O5s81wdbn(vuuvLab8I>adCo zZ(F)L1au&sDBjRDX=*0QsnLbkE&Tj}B z_0ONOB3sqGU+?oDjiJsXHxB|h);c$>@HWJr(SP$Wug?%yvJ}LzCkJ-E8-rrghC-YC z0f^BqlY@v`kQQT@ws9}hl=esZ-NFYT7dfcz4P_wo_*mE-+cjqxENo3mE{(bF1f{9% zsjNPK>_7aB8NF+KArzy3w);n307H0FnQlpbe-WJQ@-mkE4Hpt?kkI=N)A9(GdWM$b5-eTsLqwLp)!A8&wrelt{e zWy8>R`+uL2#wj@UBxPvQ-P1GFva5VPT7M;iny?d9-*1GzBJ#I2o*q#IAh5ib1L(@m zLL}PX6Wlk2Y3;UnWK>-t2j%i{QeUZ+iraLz15<(U%N)!WuJe8@kbl=l4gM&UVBJ$ttQLlGTHC-Me(^|BKskYCSl_cg~1gYXY<)P|4Di4GqvB02?)0i)sjF|$z0Y@o!Id&#_d&Ekps0#%ZlTe&h^UY zPsjr7hO77ay`Skt|4J%r&7s0btr;)87LQe#bLT`uT;t81lspCX_jhZVdpyOn>m$cjd45aU%JFlXUG*x@I4 z`aPYedsD@cj)8^52fjea068O8P?zom%+5N%>4lONZgCKNSY|Cr*&aLhZZwbVGFanD zl5Lu)s;v;2dNZ??W3GuNk9*fy?&wRh!{2M`ztS0`HhecVX^AoDT`Gex;fbl|I9h`Y z40>WqC*a6hjpY#Xcne><8&=4RNPmfK-bq^k{16PFyfH4)Yo@4Y_+@j~SL zR%uoW1sjJ&&U$So@ld$TRLRm}jjDn36nd%XqcbOL!SK@Kvo24mZp6%_zHTvE?ToS& z?4D&x{DGB0HtJzhF*0E5f72@$eZ`LW9!t8(9KKi9Dk0u&s@i|`zTK2A0PVa%i&CL z00t#}Bu4I)$aSQhf&rN+@@kDF z9V3L_`HnZ;6(Wukr$NXK#N8AloXsoNo>G)7;3Ig@=HQ*p!L=~q`6`3Jig#G#4gB5Y6Zl{{lFirHr%)oO6*KTL!Y3`Bd=?KU? z)JC^D6Eo#w~Ir`?OD*tqoh$6{uGg|xfB&g+Va?UVkvp_0Dk z;TUSzm}{CG|K%&x-X9KghW>0ey;vYW1yx@%&I{eWto%R+{1kkB^P>!(nSX7DIGSw4 zxEO>BBpBB9?Vhc?b39Yfx28m0cAf*Gk*=Io<`pIRL>_qfu^E~>f7;MfoE{3erFfV|R4Xnl8+j<-6=D0KsS*gy~U3gl)RA5h&%@@V{r52}GD=WL`jc4drE$lOj13 z`91wGs8BobdC1rlIhKNjU$r`56Tf;(DaVSMN8ONnk{jXD#(DIn`Iz%<4c0+_w}Y(0 zY8QNwyU8}ZF;-8@US=`lF}TeYHmv77cb>xE2ExY&Is~qj&~Nht_@?f@HnH1M%YURF z{}AwcP}cE-0Ez^OAu=|K%SKzyJ?9dv;^KgqlExKAe5+rR)d#1%!*bD`WLesCW zlKzW_Ilmhh9)!Y8N~qMaACY>BJvkCu{Y;h9MNy$5Ei;)TDeE@+5S;T^%4G7 zVU^s`xT+f~Vo#)Uzzbo}AT%?~x)`anlp0qlizXjz;h00uXW>a=>=wL~^?BuWrws@s zdD@bi_vX06OYZZ)2!4U@X09IRJBcViN4HaOor=_i7^Br1YvYO;IRC>3`hfUX5cXlh zK4QaGStfD)3k0dTE)!cPFtVh~N&qC8LFVX~DenXESL#;uIe#Hz`y&c>#sADnf-=^$h{xO|UOT6@SK1Yf`O#E%8C(GGL2JoCOSP*wr&^C4@oUbBA7QeC`~`>Yqdzv`wm@u+vJLQ@0-li7H1x5>9BVJ8>n`Y z($gQDe5Bs#?22D9e)P&ZPA$zc2tmAq@DB6d~; z3z>qV08Bu$zb9dBLGd6nJW{7Sh$r;&Y7_JSW1DK4DEnzR8hi^ZRcO__!_>Fb)F)nF zVhQRoO`MC{;tlJ6WfJP8u$1}Ju$7r*j_h)m_1fc_boI<(TK&r?&=;e^nO370ljdi( z%4RKqUAp)LT*otXjrD_bt&6S$&Q<#zP&j|!LvA73<*9BH44M^v?`n$?FIYh+m}hl$2oUJdTAF6GT_CGRG^Q*{?JgyFAkX27AXyB( zsA~s?C_objK0*n=~88Oo586ASOZ$l$;m-puW~u%VtD3lCa)YFBGL z7QqWHZ@V5NM+322d0e>9A|ms^$#eZLvoex5ng9h3$)pkz8glD$BclV42rC#{pA{m4 z!uRtjEf8NzhsGmb=%-vLrE@AZ@RhdL(z0LKXJb%N-B(yt0)4<~0^eQvZP3OgOyZdk zu7=}R$K@jT7;%==Lrcf6=_I<|U+P%E;?Qrqi|x>o$_{;0CJ2zajde3{vxtp@MJu5L z`ocm`?k1%lVAmQb$3*OZ^q7~p1v$@0&R|OKqO#xc&>DS~Z}hdn|6lyib3r0GTiimX zB|rKqO%#MZup~n@U}ixZc=mK7nUBm?J;PDa-_E+Fs^l1hd#1b3=48&<2g0X|kt)_&2W>mqIsd02XVtSm zbTU80Du6REzQ9g}?y}$8%f7U9kA00Vi=Z*n!*HG>RY5l&kU$^vt6t$&IX1e{ z<^DNB=%`pbQxkNo%=rTVF|A^OnjQJv8My8|kJS`3{Kg5S1EMes%xv>n*RKe9O*@Wx z7yEdvXc1o2W1?->|7GoM`+HN7sK4l?^TgU^N|02%gLTxG{zf}YgMKHkXLPs3cv9`f z;bgWd2ByTGh#5dc(<2(up-rM*gaAjRH9}O_H^i=*Q(o`{^88#w3`O2guW-*Vg$jXi zcX8pBoU^a_kpeW@cK|#B+nzGrf zRaEfNSdD6AE5{Pn@)>jy&+yt_G&;djK_8ruIjk6n$u<5^v zP2SAUO>lbw*4>cXZmr|(NeHF^-4L|u{A`>JuxE3d^uk~NM{E(_^JO(e zXuB}=&S=T($9a!;FH#e{&QI%G+D-CD%okCPjT;{!XJ<{6Ev|S3-pw$e z>|1)I80-y>n8LUcG1B^f7XHEVpd$JPsDOewL)!C^`DVWTo8d>Jj48~nSH6evdgZM) z>UR}o4&e+1nAdRLZW7oi0>^VlfO`kn~;c0G2ud9F{MBE5AP_GRvZjz97! z@5XP1(}2xsEKKI_UCKG~6VcHVWpIDk(*(u0wGZ;nR!q4LtL2yCk673X_*$K(L)?~x z)P>_E^%0S3#>`GsYV6uRVEHKZ6Fc(lXgUF}!!uMTyXGSU^Iv5$EiHMS7BB1pgQ&H) zew+^Y^^M-b?@K2b2sg%gaiW2BSPz0P%+B?z5XJ$4Lwxw0^ zq5Q}=Gw+1o0?Wy4=x-Izqs11EkdO~Tv}0^j7iSN>FPeD_Z~*1@@J#+H=SWWgTH4=( zg~KyUKq$R9Gpm^(TpA$}3`>3IcOH#DRLKX)CBsDWZC!h6D5;exrvq;31EL1vvaacW zH1WRZTw`@)1Y3+9RTezfbTaj$uJSsQ&^NPvYY)!$MALp9A9h(y0S5g2$xlhbnLUzu zY+e1rkacA|P!wGmMHKYfmhw6^9HV&oieP%HP%5Cpe||E!%`%oNHTO;+BPi0WMHBm% zG*2ZB#Kuv4^3_30dw+EYU!=07`xBsotac=f>4CccE|R}5Sq3(~Ak8)bn8~z`fs|Nz zQ!H0{{aaX6uUk4+7 z1i!9l#S@5auNlN~>m8>ln;Kc`<>htiS=F2LL+j^YBwwt-wqIbP1q`E)k$W` z1aRM-8GFZgI4**IlC7}Rbqse7qi=~nG;Hnx{fi$wzLcph&T262m`vydyF!h&q+KrqNuIxu8^fNFj~CHnJR)#g-UXmoSu2vCx&< zYf^@!N1LeFSBNaAd{*o7=PlBjTI{O(+6+w`O8t_2S2!rv6>;oj;}_k_vkMsqHrG99 zF73q$Abi6V+PC2ksRT~$fSK#OzLys+ZQV;~BJBb4S%AvJUj3sKXt~$%2*JAo^$r{i zQ|%lgh~iy)m?c8a9ax^VIsR(5ykKy;|K&Hft?W(Kv703{W{aJ5Xg|{N`zX#mZ#{Gm zc7lP)@3gzmy`IN5wC7 zoZaKRdh2lgADrJT$&r+lV%~QFc6OA%@;#8l2M6T&xaFKO0rCTf>GZHBn@1EQ_?d~; zZ()vpM?OYECnW)qLX1xuSY5)*kR9XShNb%eW6z?d6+h7Ji-ndtD=C- z4{l8o(-8A+3hYI0bCc!LBC~gI-*Z1_RCvNXD1>mpNb7Yx=dCto+zEb0>@O}mGlQA~ z^mbi7eT8=9OKY)Hb6=P+s7)oSxOoY<{sM9hAgmSIqoPpkVHrd6(!`iXc>k)kQqscK z(^b6q&Ovi5K9#&q6l0&wr%(|pQ_a_Yl-Rcl)El-}n`O(dm-+Zo6&#W^6<8os>qz=J zc8-rQj3Y$1gg97U3HNTa%?MrwG`Xk;&Rif{Sb_-3CxikBr9M;6>qEm{ja5`M1#v{9$@->O{{04~|W9#Vzch9XCr5C>cU`uf6Ere~_0DOq*>?f!0p)`+~ zJ8M({zu935@|<>3M2I`i`R=c^EB_PKOmBu{czjJMe&PQ3k(pbKSEYQF>+AL&ac}0P z-O6i~h5##x9R>FrC|`2RE3Y3F<WC{41R0i55xR#o8kR?Io?M~l!) z6WvW`MqskGA*Q=KKgk^pY!nOK&)r?cm%&}@DrZ_2MT$ug1~><;`1oFDx!V^^m2~qQ zM$F5@)j#Q`>aM&(U0IMjGwPIW~)E>CZmJ z2lc%rm|gb-(>{IgMo6+EF{!X>tbRkR?v|&3GQ64%za>6znt{H|!s6P4-p*2a3d5$r zV^*1UMQjD!#uRyyIGQ-|5cpm*=`Qn!ek^uY|dXKt;%{wRwjl}+Qg zu&3K*ou5ggC}qL?6ygtLx}7?aOxrrak=AKgz=VKLM>x9%y}?46A=&P<+Z^un#aTgH zb7GC$w7>y0I8=PX#w)>_)@8#4_cAIl`A#Ml{Z>e1=5JvU5My;Y9h{Z37IC#$4yXS2 zF9)EM)`O*Ss-o=Ib%}o`;29Kd;5p&HUvI=myk9?KnN7UIvncCU57$vP;ju=)nfk_r z`81c$?eA@=?afiGuYEu@ibtUhyLk30)KPE@in^r+8DAiRuuol8SQSwTA8K$@YUegO z{!Oq_j=F)seyzNt`F@L|%x7R9lb|?L@mCQ;2hqS-Z(j?-}p_2J^j zZ>5*}w3`;5h2r;#+*!zV2fT4H#Ziti#8rnULfPePZr6LB6}nGYF;>}!5@4w|WBy@` zlm%S$JUw5}n9h{eFlmSuK!SFVj%c2W4X1XwEXCI121?E}!R!fgS%?Y8ezpbfI^T1i28(ho^OWl0*|R~~IbbY)emEU@W5bJ8GGyB!DAMD-xqiB9A_H?> zc(?aV1z4&`q@H#`K~Up+*jEYqy-0t_Mfu*G$yv!Vi|7+(Q3O1Bc%JZ(F@703REhDN z0_<7x;j^7U%NdR8tA3R4SCXg{NHXIDssIH$s_#q;?;rKDvf4ivTsmgIu-NVs`7x^c zAl|zJ4ZXS=-<<4gTc(BZLzGQnsLg_hn=KznaB61iRP{lnM%1+2F?@PR({FHqS3Wo* z46Pf z-Zo*lqq7R8DsaJaW&?T=;K`*-4KXxhoD{cM^tjjmIe(Q+MR(0fLLAq%bmYrfQh43z z0`K201k%QNhKtCIqhF4+;dho=jh3&{%5MLVRYRK|?Cu1FXcSg_M$QOT^KHgMSQ5`w zLp{q~Ibf0@|L*`AFQUEkCb-elboJsRc%C0}(&d`iG@Y;T|Bgk#t!LRP>zAjsN_-QCk47gE8fuWBpH+t4lS49qAGtbgpc{KKWcjd(LI`2w4|BWblp zY?mE}n75W;a0|gj&Wayse-69{)1xr97s)Qz{gqgDhqT!bl*{>jaDo1-Q^12|6gxuI6feUXp#R`8I@x zWcxf6RwZ$RtB!`dPwlp-kP1<;WJ@bXy&tWH#)Z|UI7VT)tS=IQJ_kWNVb+FZ6Ysm4 z+By@WyHT26e&C&7?zSw;=zHL{v^s)8ak!9#{V$N)0zX%x{V+YDYk z1;~>aVxwK~5RDpbn{pdk%Lf~VbG4$th-?RSBl{U!qoWzLRDET58)3SYM_fM7$s|Cz zwSep*z2;}Em2-2(yMB=QBON|yKELjsE71P+aCU$)R zI6ub5_Qh7l^m!ArCh?hj2`scBB=2a?#fWzDq@6Yhq(Hxm{VX5Qos*r`y*TYx`N(Ex zR~0=!Y3H8jO=Kqfg+&`j-92M0miP?IA$UD%1TAxuMPW?sEG@lbZ15TJBbqtMsR904 z^aAYCLRMw@ZGQrK)JwHUgpo+$=DrgC1KCOmD_MiKKyA$ylUcD2n9ep8G19hZ*H@j; zaqmcz?PkbN?_4*qk^v{_w_?_{wXzIWcRYTRGYD&~3$OfgGp;v90uK{>y$CQrl*9xV zJUY1qZQOb$6|XlWUDl(Gv=fVC5gD@1j9*~^4c8NNzf5D~Ytkq73=W7S&9-t9-LFs^ z<#FTV!GYCdS3(9lMsTm30E$5Z()ahl;3y|r;V-m2K|6sMH`}O<;sDq0UWYhNEv(3c z*eA&v`iU`=G)#7W%>7_MW*MdZ#k7bad0)}J+D`J&c8mZ0-DT09*7*SdY4S3PYus`9-Ux zrNi5=U*+96qL;hOk)m|YBC^2JYn5pVlP4Rxmaa)H&KBJy7B#07yg^f}ak}dHT?1k0 ziFH7nSM@D6#53x=10o!tP=Zrf;%YMt@$XbT;%NFIzozG-c>&oVGbC$D)ubhK#1i~7 zIsElmD25Erq>OnWmEaop?O0=CKWGTl>~MP6T7`A!`4p|A`is!P;(;)Nim{ac9xN2{ z{%DT499OX}YY+gV&Mp5hMmegQ`8IA7!|$D*!Unqb#EWB_ z|F7=Qr{}SXTk9ce2Ft=a4=bZ3QOBl~HTOky8g=PuFLsF*91E_(bZ>zPq-UHhPQgyy zOhodx2WGfrKBbvL;8hJZ$Nf2Ns|f`bEvu0?qq`-+>d@Id>iTB9NjZ%Z&bajcPHXui zf<&!WYO|(u@}j?6Uey<=`hqW=AYv5EX)3aUNl(-=NHQWjKL?@CUtQiIm*~L7`3&b3 z6T0RaWb$4!dzD;9aXs0J;@p(u(jf5ZyDm9GIt{yiMqWLNMXbK?1-KGEXA7 z^Sms@e=s{|pr#;U176M-cGNOG*i5S4%n;fF2+?A3-iV`zrAsAEqc&s3km3idx>V-- zVw7?2D({Ll0_#FZ#H9G~ENYnzam%w*JqkQvMeBm{4b2C}4cI)iL9!E>jy-jyUwhD~ zsr4(+>^X8yFr<-tEQfI4=z1ZJolDp-{))-)=Cje42fj&w+_}T@ZqfF%1k?qE=Gp#P ze?)hs?fe?w&H(6$FLslf@3I@CNqGSP-z|EY2%%WLG3}0CNxy3vvoY}QtUag(?*azg zOBn0Be27_zLphAOYpt^l{~mFC|V+H`Mj`+7$!7UKS zrTxf^ej~GRL^8<1LYdJyE4>a*T=_b*nR1YF@*7(Y9QNH9p5os3rA6muqUP#bijYMw zD6ppmTjKS$DC|2g9dqF)ij<(tbsa+5@9#5K4=rXv>sg=p1veA4#Ksv;W$l#Rh?0_a zrd{8yZLTrC3XP9x<@9IuNTpC0@F=aPYboQ0yG$i8{sq_XJdh?v@~?O{Ju~@K?FH(c zBBwjf7t0oV?6S1AVTrQTrS0LTs=F?b>L@1P(+p;rBRuTPG9bz1*`+lC(zg?y%0|3H z?$4r=^`KR>zSkx8U(p~a;ic)x&ISN z$lqEeu?ekFznoUJf+0)qT6Jd>S()k^4A2g|c>)<;2TY|{E|pdWQ*W|Viy3K^>&IQ= zci>+roj#A-uGHuX)PD2cTathg{Cqt0x&H}n$KrZnath_aV9D@uAMyB_K_N7Zr#!Qd zHbz~LP&|z!#EfdZ)qwnnkV$N!$zEPXZnRZk7aq*40*}?R($z^b4CmBcZU*8tiH7w>f9V;;S--*Rl#tMb^msx;rk4np>*am#Bp7jAlXCp{og9Xfgl{x}uEB z3XZxoN6T{roE-3}`+&X4e~S`DY>JzXf#T6Y9%eMrB(#tS$LV(#%6q@To>B?g`J>ej zVI@s(R&wIzGf`>BDeLpew$kP1$S^fy%?GF|ghHm7oM@^Lkpa)sKP(m=r{{bBqxH1o z1t9hR1|(tz!@Ld@Sw!{G-f7oIkiUO99g?pOwhq@lZFyF=JzQ%p9RDZY&G`PMmTwuRZcE zzcJv;Wl5>&#tFlD8hpT%2)r}V%&?lZ{MYs;&gE&zQ3b4sQAhDMR@TOxnv z%#+2khQ47m9Zn2_jhr>AG9f>)JtCzNMD5 zxGFXYH2}!P?WCJvu)an~othB<4;WFp;r_RX)7wrn*jJA%bZ$Jy zBTaC|M?7P5Popwg^zD#T_0TO3LS3#H#W{eQu!Ar(U~{I_SDg#!|GnZbW80iOoo-sG zLV0pN{ouFS>w6DeRY-aN=V|bR;xjixY>cDWyaRC|6T5YKpyOJoz0G1&z8=&M|F;%0l7wQet&ZStTRzBsp3JToFa zIc{-KGK`B%RXo8=wC`ELLO3KvK|$WjS7(60w6%Ev~cRGp=bCCj-z|c;hf+ zEwPkrV9hI&P7?YFL*<&e^ou!k>RMq8!*;WG3A)O5ioV68JnzmmY43SX)QzU9v4{;LfflH zSUf)usnU7i&slv;HpEeHVU9pxT6nsEZ_8&k-m;7BG#Rx+Q| zI4cq1RF+pRQc%y3iVoZzk>)lrKt*4mq0XH~;R<}ApvDiXCj2n3DYRr1E=$ki_ zk3H0tlOr;;4!NaFHBEh0OgHO+mwPNIHzhyXl-9#?AJIiGjQx-S2L{cN9lVA8lay

zia_J+}$zUdO|3owpyH@6>dNmU3>vGxvqdQ zSO+`}LMIf!&U2JFtY132OlVi2FsZP4;c_dFI|Ki7wU9mno8c+Wb^w=Z?>sv&I(s7h ze^~1EgfrkE)wH#y>wR4bY>1aLlxIUN7S3cr-14t3?S^Ru)0RMOMbZCNaEpFtwh9~X z()lj`3DY@Wd58?Q3uWF8e>FNmvQ>JUKC@p@V+;-6YF8}q&IDJ7@t9AZ!qjX2ThxDAsg=?azh5MgCl^;TPB!X9;;N_?1y z@yav4VwASaOQ*S8P(e5hJU{opIVMU}+lvXBVrw!5l{j46#@SCA?Y%&S`))PFK~1C( zh!xi&Z01qJsGgbC)5or)756`^DmoI+N<6Tpt<#ReZLB<@s@}+KaT-fe%z81u!c-~C zNuu3b5Ff-5jTv76FA9x?vu|8AwB=Iv>KLJw^bnMNzO24RRs_z)3M*Ms8Mr4vTo>Ss zFu)rJ`Nu|ZasCf@F-BFaHAm>KFz2RsEQh!QiCov}vy{B=`TL-rN=MwKe-`T5S37|~ zh;VVId!(0U=i;upYM^@)bx0`U@CK#`F$4;Z2c~c*#TD}Oj#@faurvuca?<1n52>6a zA671wJt^;hX0j?WgCU-8S=?szaNGmdks?-!@`g!2BP}m*q>8X5@U@qQu25g>7pPHa zbbflOZIe_BcA#UB-+*dWvwI(I>w^hk=waM9&xKJL?}UlX7I!K&syiA02CF}i0tdX& z?H41m+zP!TXnh|N_`tZ|pfFcfJ7^OY1c#T+hHDIe-&HoNdKi#&qff(6)Rv<+6KFyFrufm%VskHPI+GYs7i`S`f}7_%+vRxnCgZOK(^CmA!> zmKh574POPd9oud_0WdF@fY^xEGU|=_I>6YY&NpPpM8s95s?|zsF=}JeZ-<;LO}1J) z>lQ6548__(`wTx9YM0{g5_gclb|DRnENX>B**f9#f6!sK(NNDIZJBC_%Nl#O`EVZ=#&J$we`>g{1fntS_#dpK0zu9zzUxG@IzZFe#=bSGN`M z-Y8C2Bu;w77L>h71mz<(L?{6P93aSQA;2*!Fk$9c`nMththT;}k^Y?*v%N9S7})z%+Npfg5I4IXp;So)&E;NATAm zXEyZCDcNjhift<3M%0CF3co35ZR1-d`7+|XU_A)oQpFV%9~d~iUO6TO9$bA za1=A}HsAmV#+8RkX9C4?%cm1?Upe}5OL6m8yV0~0d#=QH#m_z8rObe7ysL6yKMmw#;vhwVRTcM!k^Ws zCPfNA_Hg$InsChi27tx(S6H5YT(ly+NmdeE^e!upq-~(bX-u3Zf>SI%8;rS|M_pzr zKzQrHaxU{3;3-m?N0XC9nR8KifDSB{kYzfp}y8@d? zaEODjGg%*lOBV)5^2-iXeWNLxlW=AcoOta!5qQ#+IaW)b+Wb(-<*?sB@=?$ci^JPE zB8vgEe(L}@#+Q)iiBzrYMEungU2&1^4Rwg@hFFLlsh$KXo=+!*jckIE3ty_Pk!a|zi z7e#jl#jGun{o01Y0pwczlp~?9?q&gLAjBQN$!emqrvwLdnq}i}G#To%zN>3~)APFKpjW#} zXXOg-8?($<3_B>ttd09~4yKXyC`9(C7%RX}qItm7UDAfP0+9FrH`aI7)uW?*{(b{x0b5}~tyD|~7d>XO&%qX{<7 z+eKBroFo_tz#kDQ$rdVr;ADf(oz_8Hw$>ujl(Q=jk$3l=X9)9l*H`f zl+;lbIFC~J4Ap(*ZXN}lEQz8?S)2qua`g#_Sk-b?E7)D7C~=~73xp=?>9d%w5_1SD zByZh{*K=N<6zX_pv301PQ}uuljFcta$bVVT);3^gkLsN`3zfZw%jmVGD{(vMApfNz z6LOYF1Rm&4-kW_Za=ur*xqn#qblRhY!t#;@g6)=Rw9dN?!`ejgTX@(VWN|Mqr&02N zVvS$UV?1xd7Z>s}s%8NhK#VypjXb86`Vx=IaIHYOIHur#&S7>iT6H>iq$;GK;a9Hpoz;H1Rsb|DPdMCOhcKG?%eSe{9G{1{d0zWd-D z_GF=L$I4(WB)D(tAvf%y+oY4f?8o06Rre6tUssVlSn*RBmx7tHD?u%+i{vSnK2cAV zsAcl1q%)acZX1gv2dmZ)MY1IX9HG+jz?60oYMd#oThKit{B7eMaY6jfyzHhM_?V&e zWJ}8gVgxCP^{3r_+(go<1IzI~=pze%uFA~N*xu`)gEVBkKirOYpkpG_Km^Hv1(X+F zN0FSeg@lc&rp#lGK%YV!h(rFl@ooDTWbN~klEUGix^HY{NNR8?F5-Uv_mY*3i4d_Ax0@1ubf@*Doro#MBz<(cY83Fv&teF{s_#L4pEt`t#iFR( zTjP|qQP@MYby0Gs%p|WxGSc4o`*9(P<^$ZjHlb5R$7C2+uB0QG7IKLg5@Rn;XyjJi;K3E+ zf2-s?4{6s_E^5Yk&NSLUdE}vsmp9xUzi8Jg+F&2Z_GA*yb8lqdKI}-Cc=M4=MAK&C zfDvURVH?HAG;~pSOShjlYt7E59P)vg7gFD}&BLCo|0HIM#J)&UfCvs4k1)A-M z>dE$p)v;7;r`_S2aZWJb18^y5LHw3r`_F)9SQP+N@^Hrhv=1n8+v{Gez}`y89fb}L zQt0^0XZ>wb6JUmrKFzC}vcS;@$7;{?vs%2#qJPNkLV0Fi=2(puM;&~07GWD7Cq0P>so6#WYQx(2>w;^PI?=+u8jY2orz z*OQZx1v&a#k&gw0-(lCYJT2>exQ(G8)Q6 zlIeJ&86w->!FKK#x&f1%ETyAQ3-+ z&*bpF1$=MEwyJ%SPS(+%a*9=;nDxLjDxW9wCS;R|5YPjHI|pPDyrgWtL-65d5&`~= zM<ZaPRVMcv$spl6s>QOo6yBMg8CRP*jqCW z>mij07It-y0>-=8sd|LCOO~yLd`uW8cx~vpCUWiHMX9k}{i@!>Fc8O+Q z<@kp=W{h&ZbdQxRQ&e6GaS?#xxj6v~L6zB`#CC?0yZ!CVS4NgmT~3e%oH5EWB+U}W zHl>l2lrilTG=TS+vRjrqM`C{ z2VIt!Vd`1ePh<>fvTR--Pm!j+*<>2xqTErdnJMzG7&eYg0`HD5?Sd+H)2lB2EXk9m z@uf_zu+s&aqCIeq`E{ni1mm>fCB@B-rWB*%1Ie>prfBg5FG6XIxiy|7Svx%?2E#j#QZI;|_DYA+=Q868_>8I(Ev0TufA3>0xp zOq@i0IH{|ZbXv&u>^ii^l5Ms_l1mp~y0cv3Tp@{){&BYaH*X3};Sj<~Z!ye!K=Jvd z2<|xv)1y@8MPBs&KX@ZevMA0-I?I>s4CaanmwNgTH9n6pwV{pEW z-N#_am53G=xqztOVQ3<|dOGL4&jy}*W12IySs?BkdEe2HpUSAcpv(+SpYp@R1A$b1 zf27AF4LSPY!?eHE>2*7GU}C)5|@E-Dt{z)YV9qwRsOr?FHeWAKXc6qxb7m(|G48VDFU$h389>Yp`?)ZgtEU3qlkM5KyjECZ) zf-67QCS7x~&EFdG)4L9M(;hW;Q0v{M&9vPj*8*?DdQ4tSHw8=3{$gBpkQga>UV`8? zKQT;pi}OU-mD5}ExhM*PS>%Aefipl5>Ofz6G8+WmO8Y+gCYo71WJfA1aWnh}ru40D zvzNwyn?KeKc1#<@!90hlPV+0w9b$~`ve`P!QOl8)lKwGr)N2z7%|jt}njyCLAbAF1 z?C6m`=!$@WxCfN4AoxZFFUgH4_ri9txa~dI|9wCOkr^{@$0OmxbXOptn`Nj;@Fz9kUI@#&HY=(j6-nFu+b;Q=u#;ngS{Y$^!zZ@xI=#2xz4ynL z{LMqIBc$hFeJS0zVe|L}4dWQ=#uqV$29>XmSTmq#Py;CH&>>#&c>T$dNu9%s30v>n zxFNiEo~Aa4r1NltWho1+Y=WrM4`i`f>|y8QKeAe9+%_Zi=WvM?> z0MrA5)kh+dn?IB`HXf09j6lgbRTW#c>tvMVh@kLTakD~f zb?9P_IAfztjH{CxRz?}Lw~wrKlm8U$jiBxaIATh=UY1j8nB1}De8J=+K{AJiiAeHB z`jj4UNj&08Ny~dVnLZ8D$)CFb&6b@SROkb4rtMgo<*NgCtBY|YEiorDM<6{`@^X_& z+Ky^ebx%owkGUuv>Yrue&vb)t$+U`IrAUywO0V2qCUL!!Wk5tH$gkMIM@gHEq*9~b zAQgoBD;hZZdEU?l7YG7|=L7B6qaWdz50l@C2`}1xHB+eoT2Ow(GrN6xh{>aXD&612 zacqG>?48=hMiCW3YNAL+!F(6d<@W)5OMlj&q0M#NS5+WXZE}Q?6ZX27Z@QJ{(U!q z4-03w@t^&K$Pkuc!v@plif+Y{hPDG3jY;~SzPE4vZ6L@EUXKu#^c%CZ8pJByv+f1) z!~zxHUAfGi{%#fH9!Le~e2h-zJtIc}#LSaPufWu(j_1rd`Z?lIR@1?%2`r+mQiC2I z_0vmGn-Ki1@datk;3n$)cc89EHv~*W8>x$tJd@H1HH#a}EdWtKuD@Iwkj1LyFH_!D zfg8;q{S!?WSYu%jz0Q9E9jnEwC&AEY6Mpu4UL()1-AW;-Gn46_P-3=uumy8G46k0Z zhpa)EZ@jb^*bb}`qSx|01{aRJz6SKw928X~OYA+1_Hw;JsU<iV=Oy7tyVK@#ckXwg*OgKw{S@)WT4isa z1%!eZ^qiUzDe9OwPyv4YNtzg4W=%= zCG5NQCR^%?zinE~2lx*$o7yHy`J?&Zd0Vk{|r}lou6j;iG=N|3pai^Y=9z>uKSqJbY;vYgO7xmhwmqAzVjb? zb(=0ZbN4nl7m_-8i!Y=D$h;f8^>!uN9l9Q^U<=?1Jn!G?Ob);*pNON`sq*I{B zIiWCHMmc8l58cX#qo7RMBrMMe5H6a?ybz{r*fYV7fqMo;Eb{^AJ>v2CI!{`e_Rx2Z zuCl>7X*QeMQG|VA_D)IN=95&a=-6Cx2(B(O&1=oWl=LVFoc*~Q_&=}93NBYzXpSBN zfPGQN_gUe*RNcN)m$&zPK8{zwY7L#X7?4RS!};FYg(^8c7-n25V}s7OG*&B3Z3lhA z*CmQ@h@w^d2lSt=mC-KCZ30}+g*O_}RSg$<)&&q(_8n?9elFLZ3K^DF=AM2@CA}=& z-}V{e^DTj>&IF#;=#|rd0?JA2jkqKFSz@pNfTRwS#EQsIWT&q@t7U9P!Rs;~lL%I_ z5}*ponjfmwBv?bm)8S?cwM$5!EPPP*@m^08MsG>Myg68LeS5N5Z6pATHJ2Z;z+|ez zBL~l_T}msO@9#5osKyz9TRFS_7AyS${i77bj|vFF05IDZ9iz8u4Gl{w8@Na6CF!%n zF@r*ztb)4QWUJ(qwcvWkBfLdfHeU;iD#km??^e}3{Qn@a2Ae&*SS|25@WOTsEbrqS zt6cPzlSPXwKy|jsD6dsIv6zU>SG8axQv7xc^9Y`rpZ^#BhO_gatlzeJFtRO{!ztaw zyDI!V*7$b`BCVdaHC3B>YEK@OrdNuY2@}7Co3OUbqx4YS1otVXvgauA_j(zurnL-W zIJesS;$L<7^ScX~on*Osta|k-zsI$-;4Sv+i*T{K>>Nv>6p-1xlkQ%2)!@MVIxR7VWMw**Og}xS8|qo`##c-wT5) z3@S%2)c5NoAI!&773uUZ8b}jv7-?Xlp8et?T(*{E_FxVUf;h#dy@h4uqE}dl5|isx zA0TfN#cK!wP{9A{@m80M8R2(|ZQZN5GnMmKpT$C#~zElxGLa%&hkXk|82s~riT zgAq&1!r;@#`$QP-{8hs_O?~rP=NJfB z#iEn+FNE#yaNWC}G(4y-u>7DV@@h^D)MHsxpSDeb&uM`WX((6KCa(EFFQ6B<=`lPw zDDwE@JSST1U=8sL$qC(w4*)^}d{bR5$o3y9%MARusRqCSnn8*Ij^T5{le*UeRwC5^+TEEY@)5_vVXo1S9eqAMJq#kVNf2ENAV;0;^7DMHE%#<* zSK?m62eP=C+>7+}6CrKacB%T4hi*DOTKM47M+$%Aj$*urv$2Kd`IA&Px$RkBv|TZV zqaP3<5mMu^6ZA3(0b7U1zpJ-Rxw`0$Q# zdf&|T?}>nF25JJAeK8{?wfg3xN4Hum(J0|3B(Ry*NM*Jq%C|H2LQ=YF`?;FC_KLc5 z)e`GV7G^PYctrs1vW@?@`pL8uQ#Z^hZY=>8ZiufNhEzp(fOnsNl$B?>`kB*S)s@~& z_L)@KiMyf2bPD~9LBX@rx6DTo;l#WT4}>Qcr=#uJO#1P=&)?Kl^+T4{1A ze+|4nzE$7HyG+LM?VmnWf|N&JBy32MN54TfQ~F}4olJ$G=yr2Nk*by_Z;x+R-pkoJ7~=I{rj{!V>`=#nYxtIx zRQ3DaSQob%R#s(m{U5bap}bvh;V3ywT{|NHJ(cx`7kkRve{W==myRYN*d4O%{G$N9 zARGbac@SLBda=1i`%>n=0?NV(R9>X7{=$g{xZu(OdP{DpK>?)im(Wv=IXI+nXTeM< zv$5faG@J3ga_c9fwuj<@$sHho8Jc9vZ8V6k8U1TbH>P2Obcmb@Lr*a0O5DQYIqT6X zcRAWIKDCqUouOEw9D-4y!c7=&NJ)#L0}i?Z8A76^ek-U1!fRKuz|W`!yuzAa;(*V= zHZuHbxI{~z+mUOiC_)WZ8m#?EBUwu8E0c$P4itVG)olo07qUW3>@lm+7sm9Cab~`y zvw=w)S@3fe`S{d?Wq~$&q^3kpO-4HPE{8Nl#Dk4Mo6~Gq_-iqAI$|C#1paUcK!6H# z++#>c?(3DoCH#5BLF)f`F^LpnQKn>jK8AE-;7qT)yGq8ly0YrbmqlJ-;b(e>5_xrc zsm8I8T{sNq>YAiFHGc-_z0h4hb6^0T72hRb~JSsS-NBMnfdujwOhmI$yi5{Hu6p2GjceL{jgnU_80{fatz<%SoV24 z|Ekv-jsGU2xW;T94Ks%A_+W82yp7;*0@VsYN-$0V1zLWe(F|35%{qrRyV)^Zj3P*H zdvjZqj;Q}0oLL>uqt&EskS`>E8*L_o9TMo!Zlqi>)WOf2j6e}_P_C%VC<@)li;stW zQLG*tJbSJJH+VkBX^mSRAYdz3BgspC$MaO(x*kzkpT@8}G2*NO?MvGuvEy;eB6 zw~P40#}tpS%X&te-vh0~*@zD&g$Y_!M_13CZuR=rdo)w>Jp0lDpM{4)P%|0NO zrUNAX^O*bs5Ovw>LtUy!8uHLbh<_7iCbt3Ri=_|MHT`OL4pBXIcI!&X-)rvygaR+wLM!GHCuFAt3R@#jvy+Rb=@lq)4?w2WF8g8Mqa6VICQdGd5yd@=5;zxKaR z&G>h)a@YL%R(7Q2&fyKE zct$^}s{fhXQI0q6{6D zNb)v(nH&RQ1ipGmF^e2Zo(72Hx{$Y~7FOkb*<&e|Bv1QYQBVSAs8>~)&$Xc;8yCzH z{^b)leEZ_d{DR^aqUqrAAZ)%8p}O*4w<0jRoJf7r-q2?;510B5sBn=cN*pb^$Sxv{ zQDCW~Ah=L;uOT1UB;nS%P5L->d5I7?Ib_%eAJL#5`UsGU684gN-?ME+?Wydk7-XFw zR>qy5Zxdc{Ce-6G9PQ9^RM#>sXeqYNQ8tI{v{6(Z0f_XHjn5PzonqKt*SU@8SZ{5tp!nXqR3C_CTFg#o3X?~g`Q3n-5@zA1y-~v zBSx*B5XAR@IA^eJh|oeZfqdFU-38Wu5RD=k8uqpmlnu2l`jL;8zwOZG8^B^4UJZ$! z84lX^5uiAyP3*`YDP-WE$7USv)S@e=2Ifn#p?thGFD?>o9D`OIeJ%+&T3^!t%ui$7 z(uxMT8_iSnmKlwJ(}{JKs}8BUf?P0k3ZdEz(uW_NQen?Zp-l0nHrGmu-@K_wzQVhB z=$S(e&&8`1Tg-v}j@5bc>LpQU1Ki^{T9PHv9jv{c<2QWEQ=BLg2 zt!-(u`4M}gYdj_8H?>6ikSXGO&CVO;7~jDekOVWZb5G9>`m#EHHO_jMzyTa*3AC|@ z2~D1~zd&+n1K*Z1Shv^>r0Dg$bA)%feDHVOw8pm!b_#E4=4pLyyXFySw?1bNpYGrw z^h~g@TCSY%iF2&&ZbFFj<`E3{WZaL2u-e^;94&3%X~u295v4U4$kfB(-SdEe319FT zP(~%(-eI*2+KiG(IO3NrMqWtpM`-I3qD`bx%EAo_O ztmQ-Ek9bLn7>(bvbxs=ln~XNnMNK68KYa$MrV$Ou`=wj@-?;Lf&|H9U6Y^yF-m)$;)=#+a(idV|n2$iHpZ$;3_8Di&s=DX$%L z|ATDE*>y9Rc~n{ae?|KT%Sh8phFRMZcrdZwcS_2q)_$>xCG@V~KqoKwa_4NK9{4jv z5yzu1%J*Rd9SX(7ehT1S^X*gG-)oO2RO%BTk*Q>@vNSpq$$j?eUsQb6_CV7ll|un% zk&$tr|KCKOGOl-OM)kQKyVG@M0AFR(Tk}u%Aao>T7B>;t&<8Hie>tWJ((i+1hXi5r z*%M$ZM#ojVju*sIsWawJZc0AhG78^$?eQ8;YH4S_Wm9DZ?2 zLx_>0Xn(ad#Ol-04#1_9YtNnp=uR7kajc9{tM7D|%s5<`EJ;J@<1t2HfgHCRP!Oc& zJHWy-@+e%--o-CiqQ2ykU!x#h(5Bl&XCTC{g4}ACrd!U~P!T>Z1-qIOsc86xzwyBV zUhnn0`i%N3DzU>oCYH-oQw#+Qabs@LkM6_8*cX?l%(N*nEgWZih%UT(#;R&D^a;vZ z3K^~O2-P!av5}I>8FpQEE~wWqgSt!=WrRxPAZy| zb}I7~yM_kMsz`dIe=;#_H1ud*W&sy+sC@%uA#cA5Rh_5p54Vg?oE_?~Y3S@A6%n^m zF(0=DC8i_N%u;=_7|6_m_YGVu+5Vh8Vx-3!ncDmEI zn*&n>iuX`u{{spx&c;m{7Gh?7*bl$xT*>qjvtddc>#fe`nT> z_|-|xn+D+6vl*HjSs=`X*a$sJX$3T2igqq?v#1TFs3!NrhyPU>7)@OgLZLdVymiHYa9Nf!@Dd@Bn|d z&&DIetz=fUOmwUvKZy%9&2Z;XRA=4Mk9#rV8tNd0`5*%gJ|Q^P;gEi3PM=pjc%U#T z+UWlBXH=47fq#Ngc7_Jt(il$G(v0myGFQ0EpFPWB7m5qZ{+MUYB9Si!hZ4l+C>5>s zU%F1RPH_>T0rzf)=$P>R7I1Jbv66)Q0~1JVZRqBMrsMOuSmOj*a?GZ@>oD`M)}5HD z4)V0Zw=-#TmU5+PR;0v8<{tbCdi%$mSWilYsSa&p>Z1a>cc^lxa0+HFSx5xOm>L;l zVX!loLk+5|T?ph7*bZ-(FJH#y5ybrM;!Ill=h4XDUaL_ex|e?RD;7q!h)k(Ms637t z1dJMj1d{ zl_P7uVwa8srI-$yNlptFX6kKO6O}Ov0;5vfT-^Ih554VN6{CFw>_6RDu5p`7d9G(H z4c57}N2BvpWK#?S;YT<}U|m{|psm5Vooqy*4?x}MP^XVOmGs8su6MAOye({lB zK*9j19jHVz`#HB-ccNlz@~?*VER;zwBMV7%6f6y7mMeyh#!l=lJ^QWhcj>ycL3;+a z&H3{M?|nl_a5)tMRS6?KZ~YM9;2L1j6|^mesq1UH`^0&Cw{234tcgGuvyJwa)pCOf zvbm0V8tqc13vx*T*ZTDcu@X6rYDeg=T2ZQl74p`Zh4@#KK>DVFZqDuz-(dJ`4Vx96 zR^DXL>GQ;quJnKX%=4_`-@CT(ONIQLx?bp<)d+&~4nO3@8$X~h)!mvS=~=!e7i??n zD+@f(pS1Ta+^ps6Rf;|13Um3ZVjhY;3}-#y1OLYQ`7Q(XT1W?VjbmwuUFph-;t3Qytq8jJ zvF?fis$g8B^Qme6rRSA^Xz;$pP<APiM!mec7G3y2M$Bi9A*I<}!=GK3#(cz!V5q&8Fr%QlGCqT1}_2xDP0 zbT%-I;H>_#W<>9kyHaVldc-Mwpcwn1V?ZpQe?I;*OH2BFRDqZ`1Rf31@RwIE#|;~U@0=diHr<2bsw zx3y%`<_;1`Dkk!ah9@a4rhPc}NYDR28 zLMI?T^r@aVeOo4Au?}>op*pl7e$w`oer7Wza`~@^fPjrSZjlXR8Fxo#ve2#K08NYs zN65kAvdao_+RvRGGHlsx05}fIH-h;xx2Yz;;-b~^C{fGHEJ6TsseXT3snpEt7!64q zgAq+1y94q5DUB2f<9#BIJilFzLxlR9AMqG!>IJKjih-<5)*xX2%6pd(=QJKQw^ta< z)b@~8b!Nid4~h1?OgPMJ+mCRN-Q6@ub;|Kh*XEg?$Q9Vt4d4M$ zA`5bPvh&)BO~h79&pAnTufH6YabR)fUq%8(YWU&XXWzMr1eCzPTzM{tR0M>tCd7sT z^n&tGUJvRxXtUr?vj19|7!M^Xs6WTt(rpk^o@>ZugeBeM+!lylhWONpH#C&^DbtXv z;PO!#VnHyeJBByfXdHS(X}xMAKS@Z)=AYB6SmiMT=)lL}VOhH8=s6nu5J>mVot&ek zz9itaqF`-L0rUmQKe>+t0Pe_pqRcswFEnz|R3>Pcvvc#-)5+x9%B3y!0P^Q)`}YGa z4Mg6M0hxZSb`{hi@c1c^cD~)6BI1hPa0Xo9SH9P!EFWL#g9_gcPDO_Wjf4y}?Wv8u zAPhCiX4e@0=0&}HUi((GEBwZ{w*gpm#%A^6;2E@ic;f$$Lo~h!dYh&SIoF@rlICeG2&l&7WW>06etv@3Z?RK|Fio^YQLeIB+{!N&_j z%GG+yQNFu_YdMFd4S9KLLkn_S-TT&pSW3B+yVRA|@ACEA>6b3 z7f>$Gd&hdcM9yLT_qu;d)5P?Sp0m$8RMtyUu$sSxz4>`?Y7anvwX|0bb5pzSMOdYg zRi77OXXgcW*eEix8T9|g-I^SoqhmJ+kW}@!MY{!{RcuPD;FtW^Wu}*3>tubUxLUnj zvzDEM0QKTG(HRx)_m$uUViawgxYH^@#TJuvdIqQoUN9X>3`8CG;=3Zc?yV}95a~M_ z!RT(1NxlHQqr?|UltS8>_ylTah{TUB;32quF?6oX$>4T7sh8AMT_?EpyIP>vjcqCO z#WA)&nghfN%Yg~+fjtWmt2a$2ZV<#=X&Rzx>k(yDB>aRtT(^ZBC$e&USD_!dk(|Cg zK9Wqe9yAD>0Smf#6eq;nzpd>J*t3GvM%W&w&(!nbJDkc5w`-o~3PftvNjQk8l#Xt# zB>2y&IuZmxkgSYmc4vCX zcb>tt~C!j;R+hZ^4 zF@P^BqLXe?Bs_pGciGE#B&?K^*CX+dLiCu`zFkezeud1@`zGgO81V9h6&V){M#sd> zGWq!@_lYB4czut!#^#xU<`?}Lh|Ne(@ZL$(@2>hi!>;(EF%*Gx)R`OXW|O(PD)HUI z#=#c<2eV|>o0|#APhQ%36S|-M+61&YUp&%pXf3`flVNKpkdfx#$s8EdS#Sr(rsiBW z3H?NVYi4#)HV)H5m>7XmcVU2&>XRaI_af3wD)CTGiLWQ;{gbq_;$u z(_59n7(X>)7z=$t$Zns44qSk~=fW`*Y^>|k;1Wi_kVWiVv|+e8ogeQ|X!VS?_0U{C zE3^co{6FLCtm*YWb)+_BctIPCr%F|PFzyKxqQqy9zA zWHTeI5-CJMS6sU~^T7oc%<&%O3luAN%V&^o_IJtGtL(ndpg}kfGDm*+gW<4!s;B3> z4d;iJbx?AZ0=CQG()jfje`BNK zXZ45~@WAYFKOaq7f<;7($nY29PynIC!}YQtfxA*2inLEpw}AkbX82rT|AnqyiOO}b zfHd>O`HzE)ctGTBc*>h*&gs`R2c7dFI(!i*5^e1sq@6uv1X>3%^9KIO6Bb>a@=h`i>~hBmxhseEO()5ldX0&BI7Yz=tC4nJ+OL+ z*|S>04{DzPsZhFv-4c0owrYc>Z6;tjy!pxxZ$+j0eWS>99p9SW%gfZE5ed|RmgB>x zcMS$si`5l}^d}wJU)&8Z09vT1BD+V?GBhUG8!GA-e;>R{ zNOhy~8C7WS#)}r(?aOjd(C)efQE{7Wq%*#m`iIu+0lSjyOiBaS3V=p2kTV5kviHXg z;W0_t-X4Y74^{bIrO{JAzKZcC%rcBM3n>&)%g-Df+tkRfaA77rNM~m?DD9wdcnL2w zL-Hi5lXP{No;vG4Q$|SPCVp&BQDDA$Ow1@s9?y;iMMvKHn$&Dm1y_X zyT7+fU%;B)nJPRA$4XKVj7ec$x=MPDu7CEB{4ne-Q5@PDG9L3U>q~`6f6bzaAIemt z(L!*k+EyFX5!^IbN{CGV>hMM+0tfi9xSI9 z4Cp({T%89>> zp_As_8x-H$4)v5SOi+3+tX2khP^}qdsYN$efejDDWtNk4&J(TDh4~Xt03hCgwG_18 zozkGQa^K=J@vWeZ~blejUv=RuT1{|i@$c5|WazfhyB zB69Exc}!=b?!R*PiyzG)z82RXIerA4g!jR38D)1W8+zDOt3;Jf@2DPrWqmFM)Gr-2 z($5Gz@Lb1h7XMu6$X^=A6q;d;;NZb zGy=W`RXp>A`oi}_1}_09`6T!YTk>R8hbpESKk&5ALzUhlA1dhRuiD!>-wkGb#*>aC;7XKPCSmy`4 z!6~N)$_Heiz++FG(7c$6FRXBT;UmUBv_h${Bc-=$c|K>(feKAOnE~hqz$e;+_wbWy zFRa|ws9||g`%Dpsit zPMndg^*C0Vl=LFH@me;VL;Vm>mh_uCFeOBXYm*yOe-pVh7f+@Gk}GMljU;JVUJ@9_ zcKQeBW7p|dpgr;k-xwG&_i~kA@E5(o`rS$)$Lnp0u$mKa7^w41lC9EejANcgB4%2m zp{d|9<13kG8;0dX{S*IbHckG#y7EwLY@#6Oym%XalqS#qm(bC`X&%MdConQ-m;rw3 zgOjTm?|xsj94X7qx1H~#7IO&~F9*V^J(6^Kx;Rv^!{WAIn!(#-%q9M_R~zR)vCwaQ z{1)w>Th2kpvdG5Fw@sMg7G|xhqaXFrO#usqbJzj3gpA>59czrv^abnuMC2>cv=Qnu z(&DHURc`pc5GN2DwdhKD@8|R7j8f|1d=pe!<<#CKZb^~*<`-3f|N3d9do=QaJ}Cd> zfw}@z_5za#T~SC})&kO(;oZbxl&-C4Xi4bRQHaZwhO{noMw8*_<*1A7?<$Af9w<(#2Jw#vVmW7pnj!lMl&NzE%B_GG<(Y4J+OY|= z~8ks5zb-ZlFroTw) z_>q0gBU$8isug2?w$KKU1D_TaqFqy4oZTg5Tx_={u?WI>B1uxBlp@yrBJcZ{PbnSatsIS$}Nw0}R;``b1hafH& z*6%|_LQPMFlqkmeebU@0uF=Y)YN)gB2M@O91CeWsqsBO-(lN574_^T>s9o)?uRyIa zY4<$;F{Y{NAPnHq&%}$AMzKEpN9kHd-B+)Wj(ZsWHJZH=jUt7{GPX4mG`v|Z8JhY@ z=YUwu|I>o*e5gqiOLI+`q;zqJ5u8P4i9Y$BzzJi%59Wvh8!D0%Sao{)mX>A`fCFmy zS&aR)jDcqE(nnI0{y0Dr1+%SE*|19Qt5~i}pdazNvzh}b+zjVwdP1vaW1&Z7Hkiy~ zjiSZu3-94haiI(Te7ys;KC7&N1pY?0vo-sRnt+vA?L&bsi;)>@`6(<<9Aqm@C#CD6 zp(!m?>30*~Ds17i!OoNKjWYWRPd#K!JiV8-=Ee*ACFsd_7L-!Etp^*pw(RTpCUJ?8 zx8;eJG6iiVPWL9!*(rKWh;lNj_Z`y=p? zy1xoe++;x!1_jTxlgXxR2s=}%M_bW|`PTNv9<+#9Jo3hmm&4HVc&5pp=*Jr0x}n&W z+F*Rp(R|z2^*OG_Re6C0eH-q)CY;>lOcuCMQ~;YG#0!^SL`v~Tq0xPd>y*cjOm~#;s(QD=piDr}YRU&MiqJHx|6L}5L9!e~t2=Q;hRDt_($KxV z+g=W={^{!|_Ohs@LE-h)&&2H5()AhM(P5zNZ3Pxr?u2U^GkIKk?4xZtsxo5Dk5ltC zrfj0xYPak=4@%3rgjk59Z$5pje>C_HApy0q`cNpt8+cUqlba#A21)aeDBC^E%f5Mv zOc>U_4n}hHX=B8`wg@&kuge*t3*5J)#_PY$j1a?BJ61omEz;I-&;atww}#|+cQ5U| z2`5wyArh`+nbt!A>nl;`PmXP&s02bJP|9xn{@t)Il!&Jg4CAfcLFHD!q%zws4G*YHz$usRHxxwt^O(Ju9H(vb8RiKx2i%+<&lDf@`$nU zp#`@(ttGaf1zd}Q;Rz*nVwUYo$6*b1Ku zlIn@#p!YYLg}hlEB!h@G1KYom^7tV=2FBM-b0?dAo`3f<-)-(+C9~kEHI(M_ov2MB zX|ufeeV&ak$zX%YREw@`(z2xTZ9>Q2<}w*wJlyg((k@W2s17i7UqcdA+{F@}Ntw*| z*}p3Ts(wai894yY827S^AUKx-;irRt_r*ba+$%EKqOTo9b6U2S>t5fBrxREK_FfR`%E8k4_V6W6fGd>V;B+QWz@U8zkc z7ASZkjJ%^l#A?{CgEp6lUdS=krxnGH?|NL9{+D7)3tUyX zIm&*+?w0<;?@ZgOb$qrWKR{RcdAG--M=Y+{A(UP9Zu5DLt{kb{+dZ=Y1r&KS`1FX& zzPHhi^ zy>jspUWqLY0KKoYd!eJ(^rNE?QO7p&lOvMlQ^U#WwqaU;ib+d75dJM~5oN~>F_EYZ z^>}8T6-oe5debr;MqLE%1*w!TV}&Ux1V{Vvar&ckp|FwojurllS%HU?e_3#XSiwEY z#qC%i#ZBdRY1e4VPLF+it+3+RfHBDvO$iQ35I_#(#OuRRxMW@))t3dBS>JhG;9IFA zT4c2y-G{qFfN?;!o(1;Ib$DmJ_-a|(6^OvE_D{@K*z;w@N&JYDmQF3Sq9RKU&a`Rp zO(cG}upXscjZJiWbnxHyemppA5+9X%Ww)(@f^XI3q#ahVOBq~|G66m{h-~@|gAPZc z?%%ss!MRYWZ0vE5(Gru@pSO>8I`tFSIg7X5NVy8seP2qrL{+P-x+@Yrmo3Cs8*QNm zUNafRqOsvn)?_$_-4)<*Dp}3Mdn&Ifjw^O1T|@f5f;XRdk2^rf3rB!hzJoZNSazYt z17^hUTH%p(VUmZ+)cF}R%JlO6Ts(i?Aul+MKLD2=z*zWuCBd(C@P`F4--o4tu$ZDJ zG`rwaFA07!aNi)rTdDAPla788PP2@NFXA=bjovkH@uzF-hDXB>RscwK!f;iN>ViNo zCnOU^Z(uTu6bWXjyj1ht>w(lY zx@52oDTdoIe^>YqY1r69R%`56Uoi!+iHXEgv2D+Ja*@1jc-fwp zJD`%WHt5}g4H<&2;SnHmi`ycOvFbk@VzhqvM7;m=c7tjux39QT%Dao zdeP@_)#PP*=~{Wnpj-@<*H>4gu`d@YikKAy+k>q~5A%BeqOvC#AjF7woj{tgP~53N zG;;e8eQ&o4XPgSfMa4T1k$dhW+W%l6sja(FdS2no8?_M(Kpc9a-Z6g<<6h?^@-gKM z00Z?DVsY9^U@Q~o$vDO(+(sD58NoitJO+J2@NR`W+|agG`Sj8#hu+DgtcAdC@V_@< zrl?WRP~xx;A5U8mNH06eY`5KYh_D|q1#=fFlK75k^4te7!*|9)2G7ifDtrsRUx`XF zI)kOQhA`d+&->@g3{qS?d}X9*_=1S|lt63?8b$kK?`7d4z#OI7$fE|8>;eG24Xc zdwV*I$@|trj)_3izN6MTi7@G?FF`Ay#CDfhMMx)7pu;^3iReBIN2wQ!)v>k(eTv~Y zbS$2zHY#}y435x2JKppgx0?Z~G5H7!rew zHsZp~OuduF%Z5>gEkf9G0(pCeEQHjoquNK0B}qcWQ4m5gXwoD5C}Ut@BMh^J@)j!K*m8b<98U`wbOkx#0J@Y ztpbJv3?*gpryVSWpjUy+Jm-Be!}pT{*jCrx3Kn&0r4fB_MAq#~m~6rL0o4*qxZYDA z-_Wq==U{NSy67|h3*}{vqdbjCu#BYCX*248DgWOuoLmZf`eliC{g*Pggs;<&=+wB< zf5sSOvqb9hjF@EB;c=TUA6DS3y9^+Cl^>y=hDHs}O_#}0>`1`XY<|q*^RmZueUNL< zi?vj$g#OXmiHn)_IEhwF3Q@$SAa0pYTk4L|rFuj9CX3N|J;v7ckIF7c_}Si;X;|h% zNtKKduv9hwDw;%eJ=@`)npk&gWBW|XYhp%$%4XX?;Gj##-9PuV#E*V7%79?%xwLHM zvZ6%jmi|JBQirAK*_JR?EuTxD31Q90D0kVDq-I|kH==JS(2q3)Dgpi$!2!^VX3=?L zAA%l zP|Im0%36E@rg1XuMVcbbP1dp+(TVvwv~M8CiLSrnh%C4rWBR=0@|H4iM4ZgR%K7(? z!u5hFL3wlRm3Mhz5Li#za}Ox6Ueh5_5n%_0OMJwfa9Z^(af`c7iyS2jid7|eQx)wM zAwWTpa1f(h&hE7~fhO(g{R81ays*capgHh}F9-9U80YlR3Br_P8Mz&fILQNe0RZu; z{2l$Q0394{XXInSUzC;PoQjKu1rW3SUf+G}vawXfp%2MBOaRF_Lv{<7oe?70Q>vHcQX0v`p9z(aIAeV1{o(P)$LL zm;7mXc^&J;GcA<+9r+=<;Q?hqBf0q!gvcDZGf3BHZp2v3i6L9y4spYBpR4j!`bgmq zYe%5F>VC@+h=h*wKcb?jOi?u3r0MKlel|oWXk^W)!j6MQz&#GmRaAh9JW4Qj{M%R{;$o~F3F&y&!aJZS&rhm?iHhtN#tB~# zou)a(p`~7O6eKDb!{?oceoz5tQng0p$v|Wa!s%_{Sj6m(4L8qx?2(s0sKrrmXc7!? zknK1q;TJM94^wjAAE5)NIg zt7^VR_SQVlooC_G2$ESDh!@UjOLS3ty%{91Bei?rV%S4jU!guMBS|j}|KW3nciKPj z^om~9`~*Itd(Z#Ux^B=(+OWhq0x3f8?`ba@p`rW-zE#!<6RQQ%g~Rr&JyS;^MSHK!#9n`tha=r#N(pw%RkeR1dw>N@7^wjQhX_K8t>fcCUr^ z-z4F}xL_r6uE532&L?Ls1^mDDA=A$4sXq;+Cv zHY^Vuv!Il1$#Uo@rM*w_-CEBCVMdSPM3fo=%UBGw1(GS0evg zGFIA|NtxpAwhdfP9JyKtjxZZW*J{5au=45#b2Y@|WD4beB z*(2Q)21&}G$N$7?kK6oM$o<82NCaUaXcd@Kx;=+i94xC3W0T- ze#1{V<^$^5hRc0p3wXof1MxD8<-SXlz$D1ke=O^dlTp_uN*bq z(E>Y{1!S7YC8$N%G1c4{vY`VwJUFs9l{+_U9y4AR?NVhHvLNA#7ZP?;h5TuN17?Um zjYQ5P2N#KBr^wO5wMgA8#kj3gTU2emoI@Hb=Md}V_x2tsFm3)7XtI9DhFaz1Q3HKH zGR2$zatn!sWUNp&iSsNY%vGyerdZ&IkOVv6zTIO4=!HHDrsvUTyA-EyoN$3RylYpl zR)z)u5l_2etARuJ?C&xdRC*1%^`sixw#X4QUG0tc`cslWRg)ti90Xg$5Md)B_CQ@t zdEdOJMRrx>^MvF+@ zII6q*E|^%~s&HOvj-|09?-?5o;y;M_`6?-LQq7K0%V-YVy^p}nf(!TT{$-=VSqPC&80k052f$g&lsv%PTW((i?xmUpzp9+dR* zzl#8aJ)SBljIS~UH*389i!tYlD%Wn1=_~N+9v+1G^)W4ct+t>Q<)gWdO*uOIOO6+_Ch%$)msLMFFzsJG)$Q|O16c~i-x(N%R+W1ilAlwzO`Tp$dS30dJk_{+56LpdshuzX zt3q`c=ihQ{{}Mt3v!Sz!MV&viW*R24GNEf8y7F#7x;pc+kJk$D>4z;lkr?KI0wC=m z^(jxp8|5Fx${25t(@Ddc79{O0@pj$sZH^*u zf@I3?3LKt>t?mRQ$rLPQDSXV;Hl`5PdjK#!pUI{ zTgrO1&;s9{^Tk!I6oIjw^vVVuo)faoN$K^v<*?5%aFzrEyS%YQ>*gw_n^6x^6@J zMklN9WZ4thrhi$4i!M*j+bq?t4>ePSk>x;CIMey1vG9fu{UMpyQn2Y&-}>l(R6$)0 zw!q#*Hc)j&anQeR=$8if)9iJtrfkPf*O+=o$jc7y{<_BCmEhB{DEl zHB0^joxQm2iNt1%%WW8hhen)Z8rjK8+}(nW?fcYMVE_^5OIY{Y#1mRFi9ap5MHaKp z=qI@ldTp@?HuCeI=O~KCt4T|XE(Ae+CaIiS7>KBL_c6ogf9p7H_FQ!g@BP6;usrtxa~3OojAm*Z zi0S!Q6)yGO*=^miTF$tVyB_T(HkBS!96TTtrEK!>l?L7r{{<%G=f(9lD}@ptq^Z+y z1j>d|b+Bd3W+rN)&3ClOc_nQ~$eC5m%RI)$c7UY8EiS4<-pDm5lXaCdqHcUgk){|M z6b{^z^K9CU_gR*xF1QHWZOo)fSV;;|gFn*}6;j`e2nw1|7vx1%<>&BJ1{F5TD^VL> zaBN>MDd*zRCn98YF~taYhfi9KDn%{1G1yP8vo^rTbyE%92B3dxnvHRRIDWsWfZV6D za6v6WdzWv4P^qZ&jeHvK$WIsE$yG)LY_-zPUuBr-a+q#O`rs`}h*_!oo_5j0^Llft z#@oSZ!4iuz{gA=IOlzOYg?^UHT%Qk=UCeQph}KQ-&lEp95{G|rY{s%wh7!xSjzLLp zCg6yn8h{+Qfa{cWQZ8{PfrI=gKF_X2VYDB6b&Uk6*GK+2`!QTy^4-S7W?NLvT?-YW(v!JUDkFc7FFkDsc3+F;p6uKN0ZFwqOETGE``(@?w4aIPAL%3 zRsX*zm5sT06SWh&4QiQJ;e{Tls&E5I*(cVh)MfRO`s7RSQiL-v1sp)xL1^tNg}dPF z@uEG@db&IEsNB{OHjq0Luko2O&HA?1(=Q$TJN#Jv$pIwOCh4l#6m{ARGUU z`Z2i^X>+twB(ZAnTliXWLNm;4=9VIrymH0B5^O#nnrs_2k`ggrUSakwIpDiGUD0e+ zKI~5@&KBbHAbOPG|JV-@@-0yKPH5V|YecRhw0ova#Hdj<)F+E5gAAx}>Xj;T(Kj>t zAPiM!dJ>_AOmj;*rv;WoA8eR30^df|@#2Z}l%n{H{aOrfScg$6a=i|c~=Ihb0cIHZMot$wNWPwW^^Skm~=fErw3q^|oAp7IL>P%g|1Xs@V z!W@HtC)_w}YKMqdt;}{Pr{kP2QChai_`R1pEws@h8px!%?$ABzdK$0eM0!M57G#ua z_dd9wuks;sqI<<`3{`@Fz?j4^MK}8q>somH6?AA`+0%naN)r;O#a=2+f$D=ys9ikxO|RY7dB z1^TPqzKaYhypQg~lg4CrK^C25sY`!Y0BcNd#YrloyP7+WL6s>}f{foE%sT9T@i`Sa zg{$;Pa24i|S};m?=Q8ww4^NWQ*CpOn%iRvjvZu1kfJEFkPC{dtpLN9kTq?IFk`b&sLx&Fmy9uUNw{xY$!U%Ny z<9vC2VcDPW5*HivIj_z(2AYCm#gliZ9rO5Q(oii*2tZ zLCJ@0#RzjyY!Wpo8q#BejQuw2`Pzc=4**$~D3uac)OTwSmk4nvIFkY*I}%%@S597cBtLqIi@VlLmPu3l z@n4NdpHM+gE(87S!jqEB>TPOayPUO)WbJioqT?aG`z6yB4d=ytyAGuHmtymr`bsJO z>4e6wsTO`6E%5^Ib%R*J|o1g+P9zPFSkSg601`g&zePx%rG6V$$FInPnO{vVu~|^qh%<1 z+x(*i;X7W1rH%aN3_K8xD_!!={)t%%?Ou(|e-sG zswVn)#4xjvq6+BeUesD4kU`ppA8(;gkl&+R%l974nROa+mK%bL2O#l5{^+XLE;B8`7Y~EXWO3 z#wCUi=UlYtOt8m^?lEp1E=j7l_Q1y+XE1_LIwOk^EMjU^f8^a3R&-%;l3Ej5WJT2J zBmqK}-?WNgRV5bTUB&IP$*j(%&zVjdcD7QcwBIPY`DmSr(g(n+K=P$@L#@3=;~~2E z88!*^6QEfs+Jij(UXrEt>LnCXD~pF{GC8=u>#X(KJZ>J*3k7~HF|_KRU(4d5uN{yB zaWZ>`-|iXf5MRZvFQM@MFiV?v&Z$3bCD<6me!Nyy0F-K9Ky3?W87{fXG)7(H&fG>s zfV)_6c@Dwc%p5LHAl+Msx;)_t!(zxfUOt;Am}fu%bW*HH>5QG3MolCXwK&6AGn=j$ zNqW;F@_^vjBPU#H&7B*tP#4?b_fnoHHK(q95~8k3($YSAbUo=kaDi@jtD5bX9hgje z4MHbm?c9d}=V5=LQK>;QX}vb~6Mm2HF04BGib_&B?NHW#DbnxyDvU}zF(C8>se8>0 z*qjQ40w1Z3C1V)W*IcMhb9Gn%#$xxVasH|o@x-?QFil>uGaY23e`MxMc54*1t_ru$!g zKV%huz7$*YLRuYI2vk$;ZPj!$4?)F5M+xJC2=&;gRb+96LvTS(Nj^(TxfKMqjv#j4K`XQa8a$W@E)RPWsD(s)8ZJo0=a z|LH*_U!Se6e-bQg%bm7|zU*{2Ef^r;d$0-u5{`dAEMJ?6qZ~=C?lzW-9MP_{sp9A| z1r)Ud&1%aj0f+V!>Lz@`Zh7E(o-DEw6LG{I&Ka-eGQ-W9a|3+ZAEdHSxX`TnN`b0wysB zJ@dKGiK5mx7!#LhtTh}afL|7TwVGZT&D%j!in}-{SM-uNIpAm?BE3)U_$9D?f}%6;7~XIYX8$x)*B0uh1%ASxgLQ7&UVZ<}1RsccjuU zoh zh|y$-@%r>~9I}ndc4VYJuiu_3Q-N$mZWAN(gXXEP6hTx4B0^AvJApu?12GVtvA3oR zv&t!&N`uqRyrH{-{d)9*=G;|cvKmc7p??ah)x-bF-To_8~b7JP^$aA9p$J?M1& zWd5C(%m#_$ChHCRQ>Dw2P)8gt1TH>%y4FE+;A9L=R_cb=zKEy6gQUxANljrvLK2VB zte^*IQ&Fajn+hj4V2x^gMb<|V_OZQ|-9wIC2NXsqCF!?>x;Dgp8`bx$4oI)#g2=g$_r%in*ZGe^HKf*rs2x*yd5OqCCNW% zEXYHV2~XugcGSqbOhk$pI=|QmfekG=5EU#B%a28C*xUK7I;zYfv_fK*Qa3I-tL=H$ zd~-UgdPcHjCX2A!XST#U)7^oGur;N~rapZZij#ItS!%(Z0Q9V)-hI$v?o17RGdo(+ zvvGkxzFTUGDbbN=9!mjK#om}zWFL_<%nq`=%p&yBL}cSUED{LOpyh$FF~7CTbnS}* z7lKLL)<;_3CKte+0vPL8Ts-R`{@oCjmrKGIuS3+V$d@|ch@V6*vV0r>vT!*LxNp4s z@45LNL4HaMu@vYN{{JDe&+tVV!1)mCG5nBe;jXoVP5(*&k_>^4UZInaBq1#&kNIpP_|+>Fs%GrFJU4(qtYxS22kE4&`h|6=m2hN$+Q z;al_nlvsz;Ad5q=n0dRl=-Kd4Uh~(+YMsM@s$@gZ56led*~}J*-Yg5)efsp*y~4^b z>s@`(bsel9?=|tU=#~vku-2Hsenm?0+sFSv2cTt+te&;Q1$jY7qL0Og2)TiVzXtay zH4KEj!7t{q>0C!!SX&c>#>U%)RnKa2!`VrQ+VJfcE*pbH7N%-dRFU(>Dj;mc3w{A? zLP;v}d!kQE$OP~4>yiz<~+GTI!2|{O~Um_h#t%h;@^(I9(Ng~?fOE8koT@0M9>g| zIwO>*;>R5&zx`?B*QK_2z528BNdf5bm=8ZCCz&UI{>Ikm!^e5y@2J&|G$}j8-JOJb zMhAtf<*-l4>^GyHJEx)eHKVsV`fNhdY#_IB<(i@??_5@83vW;)0@Y0qPQCv-uT1HS zr%JA@g*M4SQilza2TPQ_)jykY40{cA)qHAKoQDnMZzroC8@=J_#W4#vuqSda$7jr10>qQ zmCaEG`059B-LC^@9`3EyPF^$WZ0Luw3f!IiQEC?qbDA}q=SPDne&W16My>Ztb*U`S zjLzwIZ3B);3PPPSEc-c}jvH8{A$?x<=@x<@8q}p?^W8>?uLgaCE)!wN)gNOPaAb*H zHJUf7oT8@2k}M$ENR1529zZ!uw=`=t`k6kEtp=uGh^+cCVK!aw)XLSH_#&E}i~dMl zhZPHr+H9ljDrKhDL)ljud8+Mkn-}Nb_fCV^(6!_qXjp{SeGaoJV%bsJa#6G`$xc>U zG>t6S>{P$(ekArJWQr~Z@>r1WSSS$2lZqOGUGi6$BPfp{w7$%Clh~Oe4}&4Ky)?9y zW|FS=ov(2(0<5BkvB@UAJ6H*TU&6abXJ}54b@u0#XvLS6z2JDzt+)r= zS%5i}U(wxcKv>(wkRj9uuK9UCBU1KTT*k41t66W-CEn1K-@GTru%~?|D%;CAAxWKR-* zM-}c)5U|29w&ccO*WTPtru8To5`-34H!kfM9}!;p|9-3%h)c##H@Wlq6rjsD^V7aT zRU3V6h6*A3)YL9w*||8DbG<8iGJou22e2~Zx{4m$$0OcEt-~PXP{bpm__hy+ux6_I z)N}^wbRHo=$@QQVydt%wr0InPr^AGtZ-lkLDjp&x&%Lkfn>v_Eb0)S7e z-ys*Zval0d10Q$y=Z@)+UGjf|!q}8?0lmnwR!80MXfuOC3E_-`G}{mO@n@!>!n+si zE0D)vA6{q}PecGv`b1K>$Bi($AwKGjPFUdA{R!HeM>|}(eBieLayUCYm6XUBd9wp{ zM&}r}@*hn`e)(>HUG$g_->Zh^(2T4-V&a5c={zACtmC5UA@>e>(u60YaS+nD&Vh+; zYEjtFG6u!{e5T{$v?GN9^>_|;8`of6* zF-~Pu!IoK>89>y2z53+=--{gePEQryC%k^JJNjIHYqllH2CY0&bejA|V=;Fw;W{iQ&vs_i?*J;3-GgtPn2^(Vu)p zn68`Jk9I>j`nYOj5(;D91`x!(+*k8e<)|#~HEv5CUliHIp*@-=m&xwIG75XhM||!& z#G`~Z7mBSeHCyM8G?>d)gB%|zAkvLF9|tCq!6|3jf)`gj$FDqS?Cc1i!T%++q1xI` zvtx;vcK1I8_j(b|-2&WZnJw*u%P4oH0&(g&-Dm8ol9(M3p9~QGnZao=m;H zF#-3RCi8Gp5$e%>u#6Jpyq8YTjvnkg)sBT17tp@{KM^N>S$$87KlqoF61jR2)M#HW z(x0CyvhGdZ*oCc$oI9SMVXFWt!VUJc{%m|ceP4)<|1Hlt?mNBjPGP*4j>EDD9SggA zL1fI`#C}0 zc%FRYjCM?p>}2H^wjLA9YN1!U7d_h#do66B0NB0r`^%L%Yv0*^ePW{~WGzBt-Y%`bzT3m$tQ8DR7Jk2aT7yDE2BgKm?$dXz*Ru(e zg1AoU2KefhC|uyNqH=#+`XGQBSdpw=uj}NIzBxLi1Q~)!V*)$0TmIL!x_})GzI>yT zrq*TGrV9vmaCcslNeU|qm8lXXScte`3v<^TEs8f@FK>!7 z+C7o7U^{s#g8$SdBHq4k+gD%O%KK*rC$<+vJ>!wl1#^emj7^9_{QOFJ6=lQnpas75 z#oW+5r`S8QMiVOIAUk)lxno&PCiTr(;m;o8B|>txBZq5mUar~f!*7k-sPjpx`f9+Q z(Z*EL*CWsh>8?yvZ>+6q^2_Iid6+5n&#jo3XH=sULZ6Dwh!ICKo3HYs{Y(DiFt87tOdd)4KTCvn*moDkZ8akEPSyXH#JbG_#+uB=@; zN`+5558f7@n!KxxSK~H-Zx5-IwVCG?D3u%{H2_p$XcKy2D@d~BX%*pW?0pV{*1}1%& zQw$v@p(v6MUwqTm-4>*)Kteq_TM&P2v4 zf(7;*>58=yW1yuFsAv~?pQ4q?k_YxV@ zrp)d-3InOj=}6k@dLP&7F-c`J$R_*(-*iq7+EbQ^b!)@#$ra5r}avS0kf`>|}x>i+ZK4@LAO;lsG{mSsC&h!VAU_WFAE zY&${zE0*t#HkqZ3|eAS(9(dJYWTULyHM6sGs$K<-+o zhB&a-l(lLa6``@hR-M72&+tPAlI6ag8KY@oFPAa6f?={^PI1d2Mb-%&Atz(6RgF+~ z0B_3uu@ zQn|13`2|4%8W)j92rCG!srV1yh7Xw;w|0&?)L0yyO)!a8(WBG2#;% zm=SD>p1A!qVQd4*8MfEbsD(pbdyXL-*yqGZ7T1uXIH(p>w~bwakV=Hls-LPYFt zdsXa4TT<|%?6aRl^KxG^3(A8o#*zLk85EuwSS54OGd9zxN*b@CuZ%?lWU>KfJ+pH# zf#>VacY#~X2h<6+FDvJfN*e^Y0#=l=_Ipm;G3QtyaYAp5irr5?m@5rLeFom-@^(JX zJB?mJZ?iCCF@G=b8dk(gJ%8 zhs%i}BJPi7_>{atE7*?K<<6iIq;Ai3qb9l~5oi&y#st`@X^Pq8%x1;mSla6r4!F=j z;pZQ7ChF3`O8LI5b+(241ijX~UPJPU6S!xH@-w|$psix<6=xZj;iuQLDvI(Iz!DdF zYaM^onbKIf(y?|@O-H}oaIC6+`Vj>B$dEj|v_ zHbAB@y!sMUwsRv7P@#7fFBo(sM>X<~Pc9ks_$vdPowG(UN)Rt0K-WerkJu0>xnM${ zu)YH<)T81EIF8&Yhr z*kN`9gCG1hn=%kQ?+0ZCZBkuY?#i^r@n*7isLjlDDz!SRtCY$0My%#1Pvol!OhH7o z)D7Edg3|#Y_W%rRpc;TvAph*mkRQeXRiCP{s$ai2GYTLI>5>KjZ z+uTD&mA=BWppw5b|MT9*p#}hvj4S#^u=S0w%DE@+ZXZp^R{zRdor2*qXxC$j2pv`_z8aUml-$Q+iF36sS7D* z$G1pTC8>QmC3AhR%yh%T8QPAbrx*t{z7;uoyU|8a?$ID%LNbj(aAmBn^F(huA&OXl zUHiSW#-%>gi|dKT8v=UW&;`C-gcsL5#^N@q3~$+PC`e*^UZ6eyci5jauf#ca*C=3W zV(O7@%qqV#BsidmyP)M6kfj<8C~J1rtv(iZi&KcMZeCn`>Rjy`uLZQlB)WULJ_}8PkJxDWDMZ%L5F?9nRuXS-Wgn6c*px+1Hi-IZ1FGFyg{;9$BAx-|TTTsUtTpMa&w3~V7 zOX(pOmHYIw1uAr#hSH0R)(P%)rGpk)&q_*M$5*vV^PjK8c$-9Xt@J+t`DLYSxyS$qv2rK)nvYRxBt2g-_?D`ALTw8do=Olb@+exU)Z) zEk5Di{9^R~oLnC{LLsEjHSCj5R_y8(Fo4)O0wSk24S|#?Gc#S9W`1@aKy^Z)yKh>( zRmmO1hLInqtc)9nzLAjMk(PJ!a+xB|Ym}vYqJ`KrYq&%0 zh9Ca+4x`YF1w)ZO%EsQ_B2<6}p8>Mt^04yy+yI%IsS(tbQ|LXMz2a_OvPCR7ZGgI& zmEFq$8^umlmFp~JF3W12QEB`y(D(pDrP+9%WjWsm36x9M^$e>{3(G#(BdB4(kE#+l ztdN?vmOE6|th9}BlNrbmWUa1VBD@?T`d>iWEe4rhEr8I)$&HFBqQ{ia(_cNUR|fNH zrYpKxo5WBse5gV-@ofYfm#STskt6ay`h{ZMR*CMiZVE)e!kB4Zj_TxxvC`wKl9ZyW zN7fiy;DPMRc-zai89@uErGY1Rgty@wd(<|v+DJ-<>Myj3G+fJ_>N;hXih>osjJ|v1uJKvbn0Ig2L?%6kRhvyYx}Gk#jnYe)}FpdP{_q$jlXH4 ztOfM5pC!T)6{ctKie}gi7a3Z>HTKb+r*Pv;DnYmG9f0DQtv|O8{`v&6J%rWlWwwd^ zQgr5Jf!KTAS3t<07R~*kXcC_#0*OabIrwu!1zfl~B}{!(iI#EE*jEI{yq(#8Wvd{8FX#S&KxFEQ6N!1%Rt4%u(*CD{f2`MU ze^hQ3Tj*5=#D&QOH{qLG&2ZWfT*5&PqN}uQbc~<;IQ8gH;m%qjC3|dF4jnICUb2Sl z1nJ;i6KWX#*R0b~CqIb@<&3Sw)u-T2^3{`keMi3RbzhS-M&~)3YR2l$u8nH!d`6Yx zAn|>jDRNlz1#E>ZN-{k0f|jrsaJ~E8vCF6=p!4_NYye$ooY}RI(Q^F_K5svsEO;90 zsLf)+Jw|>M;kqD7)$LdPRS}C7ax!VvR&t(qlf3&-EpTp*Jpj&|DZ!_qMl7^qrB&LF zadbLL)ze@@4pQm&qxA&Y#;LVR6+EyM(~3md*aiDb56#EaCuE4;-KqorSc0E3GuR26 zFcnRbG)mVNiuV{2Wow{k4d!^F^~X#fKgSJs<0nIZS2o0?e583wTwTRGTbGju%C z0@yQ&qcK%Io{pfMB7|$FZB*|%nncKqX`VEu-^Zd0WARhNIrh^}-NM2fvy->W?T~-` zjHH$sAk_&dF=t$IpcFQ}lyF*UJpV4G;$B9Eg>cb2svvG86qdCwZUEe3u+xr+>)BB~ z;pG>hJnhMaeMwavr*tWy!k@H7is|+zM?1C*D|!~+jZRhDSo|1U+?kt&5;nk24q4Xw zD+}BY^z=aMmi!l?`%*IrWj3|1V#)wuT+Hx*k2CB%|2PG!)9U%7W2nL0no-aTd#Q1@u8c20Mi-(e_~i|@nw|@+Pb&)YY)8u#L>v|WG}ZDb_( znL6=qlpm=Pq|bUgoWT|xGV;y;$)*A>Z;;v+!I;=t%4wp1(z}tZ5p=K5kam_!B_}(h z3t(rtLfqPF;W78Yl1^`tGlfuym)Rg;x+`-2!*b{(8(w4A2?1mFd~#Gzkmi)ne%96hJg$@7#EAL zG4N$JL*P;Wd=x1P{0|E^gOSEKU0eEY4JV+nzCCdkK#v_KxHLi53~Mtxry#UbtfeDW zsXPiBppMlQ5ncD-P9S zuF}6~ri~mg&wpdG*9F0@Zer3Oo_9^A z;>FR|$9()AXEyA=V|VG+!m69O70F=}$WsU@OiirQHr(@_ zA7Sn}?6aY&GF9!+Dbn=(MOP#lp=R3rY1%$_0i^=F>y{MKMf+6y`)}JaOC!SeT$CU( z>fepxkS7bgs**ZmWjKwUlD(A~2Kfil7wxa9xL@B%yw2q25R;ShSyAfPrIoXbzHE*q zL`3rkz6M$+@k~MlM|0TWS+uBR4wlzSG+%kLKTKxwgR!dirmA*MA-HhQxDiulx73~1 zaRxabtutXYf6*wKaY7hm6ycybJ-{r1G?M3U#P{+l<_{KToA|U@tD?I(c8SNvkFA5< zihZuP-I$hLy-Jj1%BUITR*+`1u_>C;XlWSFEar5<7G5fY1sk-qLNRQTS`$)Rm{jv< z%cKKxta{Y-5%qohoYioxEs?S->b|}Ax4+B_DqP&3%2Va8^25tzdWo zX_AKv(A^tv)Dd{ci(X+u;9V55vWEV%)UjP>?w&7QM?}9+m;VvU0XZ}H3pADFW;7CY zk@gr>h@@HqpORU*WLzVH!u_4M+*ByqFZ~Rz>Ye^m{H{E^?m0_fMZGsM*59{vPE6eM zO8$X|Rs^qi6=Pu$!(C%(xH@vmYbz*H(CmU5N5UG}bd!GH0SJh@TAbaZ zQB8EOg7inHqWyt+jCRSWj#n9|t|i1S)7^`Lvg2d%fw*K`UETN%o<;T`7=t<@AE$y^ z>M8CCXmOcMQoWVVs}^1vSMA<;NG#W@6|77b9`$BVpE22IVF!!SCg1ap$lpMRpxoT@@^{4favdlX=A5_-9vupi7Mn;s4j zGkDZG(YX+M>-yd=2F_5lUQ?KA(AH;je_}tX=!5dG2yq5NXDX=Rj9j*E@7G%M8p0n= zOjJSD;8E=kN}jDxbX&S60}K)17+Hj(Q|u?Y97eTajzt7lqA0!ap$3f4-WLN6`v@a& zQL%Mm_#U9?xXs-OziW&vM;$BoL7^%InVF!9qg{Xr3Al}|kYcz@{&UYIUiBJ_4wI+YaUSQyYmpJk zY^_O&v_(hT=!c6%iAH3&=A(^19<65gUaS;#S)u32gY@!qddX ziT?buL;C&qb^oy)<+oG=mnl)UfYuObb1r~FE#zV5^?77K>gbDCL&I<3n|@>Aj&ap6 z|7Hn(BM&8B2fOW84aTA5<}BNMjBRm0l*>)KTSy^V5>(Xn1TJ)WF{+Z-*m3Ara!h$( zkmw%tov^Pl02HPCm|4b%1&yeY%dBE>3Xw~*2O=|*M5UkucXxhht{mraQf)R+ah^|! zq-CH&9cHx1NH`X$Kbvg8HKIb0u8NW_V}jFQ-pCaRwr>U!24Q_VP{JAJoJ6K_=G=f@ zj~$sU%wKjxuL&;!t+=vwzr|$X!cX)DQ#Yz81(bX6&yZFn{D}h#{n zvgFupCbny;*)wr12XQ=z_7N*j=iZskW=ht<22SKG3_o76V2QZkw;``u=p>*AZs=tJ z8WW7AK(tXU6LC5uVGyLs$R=VmO?f;?5a<%;9>f8KQ$iRdR;rFLBV7L}=mR7}uieZG za3LNdpn@P>U1fA6Kv{>EuSCv|wWj{D}p6Mr_g1N9LPHmuJgUchM_g)Ja)8X0pz>E?KEH1_D~#;f8KG*F zAj}rgMt56nd;7nzg$@^L!`rG70CiIe$uK`9>wdI?84><5!`?r9J+cw$wV49E1Zvqs zHw>j7ed`t9{q^F-vm>vVs~`yU$<^D>IU)2)W9T+6ih-f{*I007J^YRd0QZ{1nAzP# zR5zO|Y5A-@i=t0Y6D$TV+`daW_bgvFYoyv&m&F@_iW*X(n|Cn+RRc&HMvp=)0P}l1 zXtQ|#PqgKJ&?&JRj#NXCd&_*YgYwE2@r5_uYJZ0=RcG99xp_jUHHhzRaWLO*PQE^1 zt)hC9URxd7l~-sOEG21RTyIQJ{0i^YK~w9gvQz!H;~DFO1Ul*hc<0E7UmY+9vcI1HEhxgpro3gj5Zmo!8ErcUh8P5f8XBP1Fkd}upU1iWC2U*aI;h|=q zjqg2)Vu)%de!1L*b#}fDT*008#os%J@?Ttw63IpGd&qQO6I{BQ*WcjdZ3>cwZ|r?C z0)LHy;BFyv$~Di-Pc|yT%H;qmt>YU)Y9pYc6*tzZ+%I<>T{l&>W}Ro zPg7Z4&j$1iVExy^--56>W*0;FIv`eTX4Zz{;Mc0zhLcrObF=HI$Lz!d0?pw~)@J(7 z7UG&g*&sxJU3)-nA&TRqnB-O&QsA9 zD3C#D-Mv#}Y7(=?e|ff2%hffqLC(4a3LMTz9FqLiH#u88allnLSI^Q-7d8-1C&p0_Q@{15bxdUE z*sk^&it*PlB`BNF%1ldSL+4n;Yoece`wXH-cYL`>LYFBA;_P@C3^LV13(NkSok1fX zfvd(UUZ+0lQ&IvTV;ib;z?_J0;q3vkxq?iW>D=BhEy>KY>zYeoci2QmsOAZeL1N({ zkvD_O7KRc_dXK-YEyk_xq-1_xAJI&EcJekwv~6Jn3qyp zv?NoTZEv79btEg@uK47ivaxq8daOqO@l#VjM;n1H)sNQ&Hs3iiA55HrHQLij_f`8E(lp zz|M_`w(K=M(A}SxgFr1ITy*&5|A~7a=+$5aJK8tOOM{Wc?I@7Hw0)2XU8XRXx#HniC zry&SFcmi=;dA6A0*;djB6dR&UZ7>VIpC$h%uV}0-#{!2}l{o8uOxu|Xs z3BixZbZz|lR8lxxIeFA`jZMz*+*38OY{3WMVd1)Ad5PQp~O5>5N zj&!2svjLGu>BbR}Fm!Nt*iCeLmMVonrh^=&vPtHuC~LSbW;c|p0})BS0 zV{5}|1dMa)PXGF`L|fezsbs^|;}PDXE_S#euMuacBjA_4s*&UeS_pI1H8lo`JS2D# z`EUp)!2{x_QWXE7)8@07v$H-PS!YsD5$bs&u;BLvx0oTmZzP00|KZjk9O0^q(+f<- zfi?vos&UfLGYCVm^{DeGNnFs>wN$jab}yu{{@-lb#2}et`(+PsX9pd5!|((~`wAr| z6h&W^B|HE>K)}D}5Y|;FpNN8VFH{yWbO|*Jd$`AmZz)7hF8wYO=G<*(zL|pLaUcu@ zBZ`1km`(^^_rJXdozi(scZd}CoAloR*(XWaiXCZC6dEOx(~1_7r)Su8rl0oA>osUa zzLf$0xN~;}8gG(`{mR$H&LJObq^F2rT`PEyW(;q;++`!4g}&T1l|J{JbCq=g*noYv zFH)@w!zxt|l-<9Z;`G>o4y}=Xy5qKltm&z2H1Ac;nEFlR+dUrUt-DZow!XF(vO~qA z)9}b^NskRGOp-PT;D@b`0etIDXio@!YS@*~!r+#J0^)I5xZqz);dF>@dZaYowAhXN zrGwQlKzEKh`_nAG{XbshbMDP}uyQo&C=9+bulI$UMWPRLMAO`A%R~46A=|(Y4cVa1 zHDaD*?UA%9+>^rP{UG!6+zERB9x2)-7elE-{r^!eGAA|Tt2inE%U*PYV%AWv@t&u? za_;T2y~rv#X25r2ZTysy!)<={=W986{{(pXVG`>vl6QV-qd^Be+Tk*8Lwy_r>3$2> z(yG<$gsA%HjIP;uS>-eNMDky}npJ~Qu;fZP`4IdNJFf}dYIg*Mt0kRaI)9ohK{d6X zr7a;9-jX)LOP+;PO&bmwKHnhjt`U!zn*hd!4vZI+l^*wJZK+C&Xi{l&lbI-sF@#vk zF3lL4!}0tlp_x@P(SD68STUXKES zC`fS3#BY{H=&vtNnY^sF*;6om4SwUO)l%s^L;OZWtC3sWFDBPY^J-WKWPa`Wp>fy- zO56~ADPNM-*);D{-nbg7Fw4EYF=X+jlYT$NWJYV3chLiwsIFEHA|Z;J7*k#5{7N$0 zwT;MI=t)(o?rknR%J_>tIpwldzjZ>m&1au7H=TS%nSJZ%QBcN*2k%xhb9BN5l0hLd zbF>2-7VNy@f8@yHvFm;*ek+0twqTfhOet8xNzY!LJ=%E9l+bdIF+iZcetRcY) zD!8F;mL@w?*;wrk6`Kfa+~mk*R~Q2WFngRHR;%*(bAqx`9od-YAjv22@rjxk$x2_Co4oodw1%NW_YgbZcvqS@(jP8xhKKRuH+ zx&b7@;KT-o`3NqZsJW?Bn5~)O|}aK20iM5j`*wXS$xn z+32lfD0L16%-es=CPc1gOF!DOZ;!o3MqK=0qI$v{xQ=$}mPO+q!Dfnp&_#*sD4-** zM)?{U3|lhH6w|BOj6yC;9zNccaESlH>)irQ6P_SW(sUUwfx4a>3-mWKcLrWDIbie7sqJ5=o2Sy{3!2{q3=D*m`Jj+)8#1Cp||)}eC7bUE5tS;3v} zp(=^&ZGHz42Ti6;$R80NP4XK^bCvQ#r;KXVS@?kKy|)UQgB>x&B8?eHoJ`oPm>XFy z^{rpFY>hQk6B&O_p@NY9bDK*YXIu{*wY_-@UD$EnZ;mfG!?Ob>YQJT+Y zT$fI?=?!G;&~R&Y$kDJn_fuayiAVvXLrYO(?X;9BxC1cvR8)ERmZYEVSuu0SSlaTg zSvyDc+~Fo%j`Xk$5`@g?3l@RMq0B-mt!NLnjjXodo-Oe3gk|;3b>yy2{~?&($}5bw zuUhtn_MSR}TfaBfhxep4l|LOR%wJQUrvMPqI1mfXIF-H!+3Kb;7ZIBVGIxaH8w2d= zX7hcieO>iG*soO6_mg+D427zj?3?7i)uhh+v?2_aGErMvaxAVWVsrE`tSu+>r(!jR zrrcg7CliWgn|4JMnAwoMfq@!?xk>e{4oLcvC-q6flpTw88Xc4ikQ%J^R$T$7<)XXod`z|vC93mK8e==P{9o6hIzA*f+8mkB z4k1T6eJ6^@pL^700F`oa1C%{Aa9;(}406BVU~@*ndXQJBOTC?j(zlr`a9#aQd>>4jN!eF!vJ6V*N^^~=67NgI#3);tzoMXj;yOp z*kKeix=gy)jsy1%aq|6NWvRfl36gR2)B$?ef*Y-r7APv<(?`j*zwVntKaTe7q#6Fd zQ8neOfH+Awy5JDgWhWAt_|boFdfF8l;Rc8Sw|wsTVKDWCXI~u&xKfVI3zq)L&h>y8 zd8~?;J$J+-u!OIOED}UFUv#Iw^l?wE<16|&Kl^!IUvV)<)tfMDBx`a?a)nXHlTKYH zIiU6$aLNcMZE%BeIWQPQYf?CFQt#e=38ej*=YKm;-<}77r}1A4bCO=~*AAPrmjf?-$p#o0T=O#iqUG=r z`F-};Gj;$w30=z|^*1YC;pMT`aIHv@-X9+&h(7heOWuZSiEYPE;OdP8aUf%V0J1%w z-@X2yRoopd_3Ym~DBsIInfWJ;CTGNz3DM;?7*H{~BbhT(5}ic$_mf&Prjc3P`uZ<@ zuT$@KBy0)YrZo*53vUJO-`Igcx7{NZJ=~Xbf#8II(<#DzYhQd-vqJ;){;Ffi4~lF=v9hK7?OGS?Vn6!f5RiHD6EVm*7P z2R6gJ+vlHDn%Gj^4T@E|ZA4>I@SEq&1oW0S2N5Yyhv`3DK~aVXF$P>+u!c z`4^v?lbiyUV8kv6NHF!*%zwuiM-Uz*ZC~KFm30zY*y+gO|t}>x7^=Dg<_z28)x`UF$x?g%&Wy3Ogjog z#I7+>H+yX7za_G=?Mng~_B@IEXXXP&Iq&;@Apm)P(ZL|CUhhY9k{FgJD@t@iZdm8% z1{yX9>Zv>4J@%vWy*t&MdGjvksUVHnC|In0heRT(F0-XHwZS4!W}dqAC}- zk+4Gep}s`G!cIU!p_qyQ`g|PtWXk0|T_d(*o8NQYel zEZeLJlRoKPi#y2eNQ zUk>;gW)tdOV3wt8$)>&s;&d~(26LFm4+P3AZ;f&^bM-?j`FqM9Aa_{+21g=+wY7Ry z!g2CJWTBIm>t!`#pi+|gS5ip-p+2{0VR#kIzrAl1aka>Tdr&7D@O${^@uQYgNI z^#<}7``tOQ;O)cljb^i5Al*YRU^~SbUFwQFS+;4d%NI==dr>5`LD)jOOHIP8=4O6|>rT&4 zumfF*J@pVE7G{7jqeQE9L{?p9rtouGX0q8(0ec6PR3_7nkl_q9DR0y0bFQau*1ZbA&-qxKLH`bKfxl-k# zPV(d`X*d$0{ms)X!pB$R5~{Z6&oigUc#8)%rP_D_9&-J#OYVu&{QM&>KbjSty9baa z$ZC@!NLoh30s&hDwp5Eu{tEjjeK@oQXSZqltW&TaemD<`R-8%0-v=MD2AsaHeOL6{ zUca8w1105p(RU_A{JHp=|8FB9;NmDzsbAh23m7>F;EX8A0wLC>n@AfhjF=-|2Oo3e zgC2X<$Ft|g4>Xq&_YjaZtj4*1MeOkr&AZRT7iJ4H<}XbX-NT6J7Wv)ozaCQ@Bon$_@?eZSdz?E^VYB^zz;HXN83fGu>8V~dITCBv zvF74i8klcle;zjSl^m2a6+Mg|9>!5U8#7lH+Ps@n&e4Nx0Ji!)INPFGxCa}YmeVmN zxrwm(3%oes`aCb19`Zav8txPI<-sEv-=*M*9*=}{HAy(bhEu^>=Vt@JaMp}8ZHjKeR9%W7$FPozrm%d^xaWGMH8E&Jd;<0|2zaPFN)`#CBjdBlS( zJ5y|!q&vBBm@wmAt6BqQtGzof8YtWr?cwSO9kFt&`C5SQxNTIMq7Gwx_u5C2iL`P^ zNRQga(OZMCPys7L{qmJ$Af4x%o9)I-Kr15eAOjq}B$DCa>5vIAqRez4PpgG75=|ex zYM!Q!`l@})7c!p;x{9xXlukaErZtQO&iXb^{AO9yU6p-5qOnR_n{MSETV<>n#}0>j z32Nmr-Q!<3^@xD#vJKP`l!0_2sT2WHhTEMt_re3#E4{(yAZ2Q#n3Zj;s%L(Ytz|W zze6Og%VhhyWtC|O5_Le#~9l9u2ln(1Aodx_#V-Qo=ABxW)=%` z%DR5b3!mhq^HL!Lt9sct6m3^Q$!v7_v{I@Zea$wZ8+5>rAJe(+02gv~w7Fbjo+62e zVFgN_o@DIQtRoA%Ca2kC2173ylrViC@ItXg%*aQ6;V@87EiPk$Wix|WAkAx~ogiwB zn&Z--jC9{Vt?sP+$1l`Hw!@qTcbRm|pMC7}u{V~K&gP)H z-u?hD$d>6HDsZ%Rgi7tqu8TVl`T%#?z8Y(S+dn|cU0 zSO^o=&)ZP@qqAdW3#exXO@N4#;RNkQ)GVP72zoJLc;}(8GE(MnMyQDe``VI3cab&I zeU?i2I>tj7vlz+UEkqNsK)>4-ELBuKK}*UW`vgbHndM7{TfaqS@FgO%Tm&w(LdaeV zaV4;{)y@Kwif27!(Rlyi&9i?;m(~<;2JAb+nfg{$+W?<%Amz4nE(|gDc?P=!`@wVd zS(<%`9M{nz(aei}HM9eel~`;jD0DBUy#t;@m#sV12xdQr^JAl66l@V{Cu2OS{8P-& z-%x%vjL>&RDqBuy$m?txZC~pV_t7W>1&Oi{(A=d|91be)EcUG`+YMoL8eXJ75HrAi zKm=L~k~z$^@f^`5Knnd~6IlKkpfko6TjnGB6?Vdt4khS~CJ%LB=KgLC%A2u55C)tqOIDyO_YqN&g zJxkxaMsNQvIj;o6)OWjf(S6qh)dk=1jT;E6Rd8h|K8z%@lM}qKJ7_UX-=>uQfvg8n z&$cR&u?%`^I0KfKrd=A;e$4qF>Vi`$oWa~sWjv z6DgNL!jQAfD;Tb@kVQ!MZ!~_EEW(oo5%SjvjfEJGemV(49 z4qa$KAm-|BDA5A(QJzJyecAE{=fW!CPA-dPeHod^ClMuIg3&31%z~I&z9dEQk9+ z_@!hUS)J+Sscx;&(LeT8wyjLRNw~55}p5tD2GR(!YWz=paqFlYS{LEAuGl})=WZX+@ zk#pHswUIlAx5+mX+406XQ@)Uw5jn9OEkoQeu<7E-XQ*7DlAS=2n&BMKiJ_ydl+h0f z7Tkh+p%>bMvdC!LSp4M8_&fp;$YyyudWT#mmpq>h^|#edbU_wWGJYR`@?si5@ z#@UU@fLWb0SdK9yJEsbN1R6{cj%nr=Ocam=YLGHGdG#doT-mSz@HMG0J7^r98zNX%tn5Zg_i$LYk zJ$)#k4A?_8KRv(_YWk!NkP-yvVdC2qp!zk2mL$~?n5hL&eRPn0+hyb80C)jqwX(5C z=P?~#Nh%>X5wsCvk(a!`L!9tUDQ~Oa_i=}hbiWU$wg!j5j4S(BaVv?ROjOHm4F@yl ze4`Z||IY9#a7(I&Ze%f0E$E_t*Zq%+{(vtk++K)U-{XqeFadw~jfG(Qwte#o6*N0F z2a5vc`uw5khmYJy@IR z^dUNgEhu}Oc8U|WC(db*fdjOUsZ)vK)p!q7k20Eg3ftI%rq|fjvO~w5(%wqyy4lnM z8=0v`r2g_{9E+UW1DY&tDute{ysZEH0v0=8Ye7FJcCNP>Lz z-;eeTa!)i!1wJ%6gDBT}sk(FIw~jiZrMgd(LE_9DbVPF_O8jBd^5oDqMBD`3G zNlI@T08A&B>{~rB@fE678g^)tNLF~>bm{OkDX?$x6*mJ&;Xl1`kY%<>F*oM)4Vsboad#lIYe(K3K6F*_#GdMbpc3(ms@wfd`{y1 z!{5D`{#b#9XiH|vVu=?%-a!U(hBcFkcxXS>B1&r4XN^I=-&ks#~KS@4ZXAL4d|p8*kWE*2DZ*m2r-Ie zv&c1X5H9~Iiujic^DhwEpsQ!EyG8rNCdfH$>@;YXAm3IW2cjWsl|w%PL~yNy1LOGM zotGWgt5Q%6s#Q1RYnfQ3(CHM3^E%vU{0>V(|Mhqa$e@4gP}9Dg02-qoU<+p$%yv(B zn+z*11WvI-MdmKmvs^eQd9F|dnZfC0Y*}Xk4Qk0=V?$`=;d~% zAy@V96o%0{4J~c&Tsni}&;UL0O>u5epN!!7(%=lp;y$02-$d&By3R%O=hI71jfB}m zrnZqZSHw0^McZi`GEO1I@cm)9b2akBFcD~7^usOMHIXY;ksvFW4av8&srvJGh%G$d zo*{adlK)hzpB58KRVSLW| z+ZcX>VNs(~VZw5av*N@ROvN2ifM}<}Wiqj7)Un!{0C!t&T(3j-hq$pPx*Vhx*htjT zO8AC@jb-=p%uLO@1i0dd(-^1nB#V(55l<+pb@+0g*a@}P!^o0Y`6~tp5(W!FZ}BDH zh}sG6k(A;t`J1{(fU-k0`}6wQK$pQMJ5%Tk(BHEbo1~(?n1g10Uvl%>V_a6I;d`rn zA{ko-*_#J36g+h)=0!AEnNhme|FTs#$+@1QA*w@_fO#&I$UDxd3!%E0Tbd=BTqUvz z&PfHu6bXfKVaI1WI(*bQD8@B_n~=&FTA2o}N{kpmtmOiyBE&xJM(t926hqCodj zL&5jeaUbmaYSM{)Q}KQLbn|?tKb^-@h03zRu3EC~N&2;CRv;x2)rfHhTgO#<9M&#v zsM7AJE*$~(L!(1~n0Z=f0JMJQ^0BGL6WZUK6N;Zx$+kxV-W`csb&$*?Q;MCbWo77U zQ;Z`+mElziCWG)85To`G64$=h$rwHkB2c#C z!Cz6i0RM5v@ekvs7{(lxF---XW=&p%G)xob)_+<&27leGdYbI@cERF<-w!DH{afPU z5MLscJrDPu$R{J!N*`C;gh`tgrQ%YJ{CCk5sLfXq$5Wuj?o@C?>L+$^Q9&f$ID{SH z^ipcvTYp@f?^_K`BEcE_gt1wSM-GtL;7QJusWktKU*Ab9MqCotU8&c6)NZ9|Nnkj% zs9g}lpAqmzwF#vQXhUpGKHo$4pjsDcgj`wfUJmHqlBE1@C-^PGl+}#qc<7@YQ3i*b z;drg*wy12<3XrY%2d1rs7>|)%#nc2uG=lRnmd2JFY-a|Jt*=wLn!K5&86mP8GnZ$; zN(Xib@Cro5JlQvGi$+0zdNR87F(@gi;OLVDX}pn{uANFxjER8rmJ1iuG-Ya#DxPW| z>>=wb6|K#UWE7>$m3Z@#CKMp1(ziPoEeL{G2C$)~Hr+JaZZthmm0`<#1Fn`xa}E-Yzn+zg^Y(-Nz= zuJ3IPH(yX+Tc}%WkYQ^;%qcS4;N^JE_{uAf((j7q$Ml1B=)ur@4WI{btj;pDrp^4$ z4zA6=X;{|J4@~h3Jjh9aBQ+CHp^N=F?j7Vz&y<-PXcy<&_p6*AJ!9blR z{J3_4+a0lT9JrvqF#l>e_TK$a#<#ssaOM6gXp*h{i4(;(vg8!?KJ6F)ki>H4OpTxL z{g&hw?QRd)-%)4dXP(8jzoHUO6lG|}I_em|pu3(PX0k-^2u9T8iHD=2e>WQ4ODn#H zyAMXY)#nKU(uBeXg{S3p$~_L$)257NQAC%rkT(?d=HWLt-9sd~Hvt9A8@{?ATe&;) zqY+>7yo+(Bn!cwo2{g-#85JPT7%oa$9{!Xer{o%{Ln_24LV-xVPuVG5#AVQzP#ROw zXVRB0O+SW_Df%BnZ0CDT)S@|>ipd}71CP~1=P}Hrz=(*et(I4{LROKwquT4Gf}gsn zLE`?ZoxED!2uf`w#!XAi0^OBataSvpYsWKg{n`wjF(Lo?4_7&5QzNNRjX#0ZUU~?QIm+_WAz{DDLx*93SJND`-&D5TA~Cb1PRU~ zhLQt>#e67O&aMNVj+Dnfgi4@1_a^u2NFwAKIn-Sa+A0(pn~D5#00Gc`J}Fkuhnh3j zZG(VL#D_N==HIL@b;Q}+X^5Dmr(QE;S$`B(#I5&94ls#C_mbV5U7)Mo$*VM}m()W~ zW}iBp?mzIs<3talcDqd-V=>%BarJNiiCsIO1B9S^6|+yP0qA_gnNaCRN@6IT@lN9H z8ho7g&tt7|s?0#e_7T8$0kl7*ADJ4bU2t~!2T>yIL0!wWyzyHrk74!nHutVlz0dsk zwChWmFshXsYck`@r*J?>-E55ViDf9a5S?L=9Lddu*dUAG`?WPgI#T!Hkqz41dIi|^ zmvk_)un_Qe`ni7LUFm^~H*T==1xVmkKc!U;?%RYMc_@9q2~ zrmQ#BDWZW!e(7tNbA284{BJoD9X6I>;}cmt1&*c0t> zaAP<>yZ_>Tl9Yh0=duB9{sh=2D^(`SeTRZm3vAd1Y8%TqOZaJ6HnwG>VzcZ6da6Gu zsit5N?J#P!@vybjPeJ`~^30O)J6DO?Eu?D~`;{fnM0Kyd819Oz>iUPvVpVgo#cZjG z&;!>OPy!&n*B(4xOE5aaW#oENhP!MWXYOAd>UTlE?u;`pmk!W`-FPm@} zQn;onv?9eID~@fj`f)(ZW-htotXifMqW$+iC5KPghLq#}b^>{Q;Yf#EZdjLpMU1hb z0egIx%x>b0*=2f-`C@-H7iDI}8=$)6F%z7=Bv(PQ^^v+L8Uh8S(*K=urRk1{bY7QlW*%Uh5KO;-NdS@dqHhDTTxcN0l#j1X`3 z_@~88s_>i4?^Y{A&)KkYYPfEJGaAB5xVNSc9X;Pl+>i#f!M6+1TiP8GnL6sbj3)2 zh^N0HQB<)OMp~!?|AjUa(H9~dO9T6bJMU4G95vSTHplNPQ$!$FyMfs%ys7{mLKqry z5Ol_X_>J9V%OqsTK@vq9Gu!ea#MSj*|pm$%zH)gOId~hP{Q1$-J+SAgt1d>U2G>dOk z3Q}4qyh_e?Xkx7f6#)>Tv+w0kf}GxKgr-}fc))^duE3{^5fAbxi1VfX4M0U;rU-dt z3CD%t5Zsj5W6I_)eOd4YEi72_LQe&#vaKm1)8Qg}#JrW3A3S0}!4bt2cc|rv$kqzv zo;7qBnJmUf!_K=aC53RjEY436J;_F|qJZ_M@>yTCl@n3DRwl`Cmx$im7%j9|)eSph zUl)UOoP9k^2+HJ2;1N$U{lkH@5V`ghcNH$?Upp>my>MJ5lhcJZM(>&&kpV>PNJ5)u zRcoa17Jn`zCX?n|;E`;I8J8R2^#QGm)en~% z6*y^GFc;yhViFr1e)vi z5`n1-uW37G7z~Rg^}ra!Qr-jQf#qxIezSld9^1(&toH-Xq3tm=8rq-AJPT0i4oQ+c zxE5=LUuu=kV!A^%Vq=|}%IXJ{0finPxR^Rd(P{+jYgTZ>zL1~e@1dCO|E(krArdph z6tYX>XPuJ@m7M|VbsF=rSY_O~g#9pfVPirXic}8%;l6lx!&RVEoV`v!qgUCTih;-f z%bgauH}^Fs`2|&cbe6fj%cf1a3*W>R+9d5vxh<;M!M@*l-1MtlS~8vBas%T+LNsYi zmcBI7D9$2oR#>SHxs*Oz(cpb54B!}DTm0PMT_HhDC1Ffl_^dxhhSiv}mYt%?u;=Uj`zxXQZbh$ggAsI1KmSFYGzTJwCX6EJK*LjxVhQ{)up)Gxbioffe{objb zec$1VEy17XoVlN!H>@I${6&OYjfH0nTQwrNMSL0Nb_SdFUbT#CNFMj}bTNTrB-unn z>AEEUuF+V5DIbX79O9R!iNLadPhz96k_9y!ZR6Xq{VF54^V*&w5no6rR4}2?m z82hnY{&~_b%{SrF9?j?ZQp&&7@qg9?EcSgaKZeUyBf+=25?imAqWUw9z})3hX6DB z6_S;gFg$-IIBj9CXvw3CF)9(m=tQ@m@F^Rt%=MmV=jFShGGL=t|2o<^mCUS$Vj6K#l~i zE=l5W445W}$XiG@+)swqQbo3~jIZDupr=CS_U!S5=};W@43(Ib6+7U0jc6=(+hM32 zyF3Mj*G%VJJYd3V_a8Eh-LvfG)SXs+u$n+?Hvb)&_cmArQx9zfFi-zif(|&ab}WU~ z9ivSKqqO_F%7BbOx4g2Ezf&-G&|Aop#d615{d)b^_j(}1(Uke5TcXJ?Vw|i)Rs#`@ zv!Ox8-DtS~K50pM*q-IH?Ob)k(@?aVhWnt7j0yxLlxebB6~mSjsG&j_=B0U9|^Y{nNXqawq7Dk6MQ z)6;14h|jcD!E=$hAo4KU6N$(GLwQ7FzQlV%^N193@B-tI-FNzypYxVErh* zWbCwOzMG0L5u$I->Vl&cLl!~|d_hu4ru8Lu{f(iPJoYd9nRTX>!L4s}5oK^LxLKSEC=9MZ`iFaw=pONmad7P~ z93TSiiSF@1cP@4`-JGn+W_7>&u1vXHH;u#fJibd8pvZp>NR#?3={*=9sbI6l^t`3Q z{q!2*kMw_$qiWf<_M<8pML-qahi$xk#lPW((+IF>{{#_c6?IoMgsM>h?B2sTFEV`o zWW~dYWY)}F;7FN0#LauQKt#go0~@WycGdP@m4r((c53pw+xc8n?K%}2IPZx5QsadlY2v3`~;+rN_v>n*BXa4W}{+-?45*J=?e)FqGi z*`d4zv%XA`H%%dXw4wDWg?Czah5t5+#(6_XMfqL<{Y-tkxhW zkgy2{ZGi!v&s{7sj|!x?PZC)O!W-0MG(UcK!@ivsRJvzFt!@x(3EnI_FLGW{neF0D z6HPDxh|E)#pxzx9DuKg>eZ+Pdf0n7!^hQkV2aNL(x!Ri%xrF~W=k61}nrR=^`XY`n zrZ$p^%kLA?!TZiUBrS~~8oe}5xa%yc&|tYJlvEd};z@H;3+-6{>2r0RJNw;18D3da zmyH^+JTINo*YB#gkI0}3Wt{GckVui$lx|DF719!W-Rqm>(zb>Q2s)|(OBX$ry5P{i z(yKiV&1zmfF*sCfGXWz`ibgajy{1Og-0}#2Td$()HNTwbA_$16!X?vm$RrlWEaX2G zll)$l@VC<+$nlyZBP(csi}-bOX_M+0EtL9>sk8C{^CTq-4ijlT{D;8?uRV#uI=ny8 zRou@-R`W@$mX0u?y1!gA|7+;h8N;d_3?HxwT2SH zmkMYobYo)*Vl#@apQJNwDz5ubBF>Yfw&Xi)>~A-8`W}8SkULmB7A$7RrMR)}K9Gjs z_HwQH7&;%Wn1$TL`TUr{7PGA5X8VHIXB zfp?Mm#?TG*`#f(D2_ydFC)jy|Yb9cFgqzP_9kjX3eWoI=oxN$#SfXL6)c-`QCh4&_1JPQ=+7L|hBN z=gH+%@k6T#^h)Tc_+<)UGCComz(IN{^tpf}AkB+*!pmn%|yt z@TOuk+j~*g*(;_}jmL7yF(Fv3U>I)x&N_p=5lPQBq8{Fb#^eY)pJ82N2H^`J3KT|m ztix*&*~2#&D{1pzK7n2j(c!Ll&T$bLm3=Vil{(h|}-zM>Y zL_$+P5k}*lp_Q>_Plq%0DZQ?bl=1o82YmV&nFD|Zl?IRwt<{Qg)?2-(e}?q_arEdf z+eE=tAAlMp;7Q*C6^e}p#e)3uF9EZ6an{4 z5B#_biQOKPjxyO$&p9;K%Um6qnsl6!xK;oo+dx*lWKyZ>Hw<7X8vpv?oel>LwZ-Ff zn?KKQGzss|aNvY7r0>Ei@*p?aSJgK*Awt2t#`v zH0;k831tfd%R=;auedat60pzw%r)EC-q>+-*OqH~^Ncl)3qpoL4=*Z-uM3DA@chMJ zN@Gcl*F;FS8p^k#Qy!9Nk&_CRHJ-F7#N~`G&#|E1xwh^k&JtF7>}^~*i;(uA2QBME zRR^|AfhkYQU(eMfDm1NKm))ovc(u`KqA~-NK9OvhRI^ZOkt+qiS2&Y|>GF|F@`<{^ zwz)#gs);vkk~n9^--0n&O3pjaMAr@!@w&kLAD_jPrhI3-o!rTh6%^cE|Aq&P$#1w- zLAMJtYw4L@A>Ka=c#p3K(qFH*%^vHI=N!0?ChXLutzAztJ$xj6v>I>c=HdD_c@(;2 z+PWyV(9TxOKQT4_ALXQKM@qd`@T9WD9c}zG1|nOcD)_&px% zt#JX zo#+m}>2cNQX@SZdq}gXSEa=C;Qx;L~%ZXRcoNv14g*`2^H5=EJt#t2a`!8Q)#^nM{ z+PAy6YYBg}&NEefW@knLAp+yCF!(RzYJnFgL6f?bU*qLwA9-U`($J2|p6z3%fmGyL z>xya_?pMawOeaoow^JuG-GRVc0P4t%>^&dAUE|VVTrLb7?OOW#p?NjolccDLgf82$ zmHsn@0(}@RRIt3ynVeft0|Gb zI9q6jD0~)6mGsv>wm!}A2^QYK&Crv;xyC(Ro3>amAU4~vCoG~%)0?IuC%XfjdZ-^T0toz+zpw1Kh9){;E~lZ zjrJz%#^o`cO~BS|fY7%#AXQc6BQ2W{4;ORp0~ZthYL&Rcl)oMzej4zyOx{~?yqnkl z9V1PWqD<R-!mo$ z?Dy?(V!KiU6>a1`z+tN5*KbFuMju~`4W%_qTS7-oAM)ecs>+a#E8MW)>`t)51h z04>{(5Ts^h;B!kTrB50$G>LrEkPs#Do%-qA1YU$;mI6;-{m!xm&eioJ;Kr^m?f9X0 zGSQ_&=RyCf=qW(EA5w=pbDHxXmQjLp1zz+Hit-Y1T0RUQToaNPZ_maORoBO2)rMpX z*%J8>x_c5}Umx;(LzNj6I2WqDdIHxn`+xi`*?#(67YtH6T~NlDLa7671<)(!Jk^{m zW8is`%oalJNryu!g#R6z%`qzt#Mgw-BNvSS^0xCXA-&*M(t%Ie+kA3hU#ftLN&)_d z;4;Wx$I`fx;NsaaOiAMIL8-Ew%dOSaxUg?pOGl!-h*Ca?#@F&VW}T$5ZTP7yJjFGA zno&?3#(N;Lxdoc@Gn-!+fb_`HP%b=Uh#T1CgtHLA&dsBWXO-&W&?5j_uh<>VC!3{r zZp69JlE3j>x^6coiJeGHQcyedqog(gAWLz{1}t~==AiB%lnXdHNOmi`2Fk=PG%4Q+ z@-X{|1~+A_68j8rp>Kx*ss6KB8%vR!L|K4{Xh#=Wgkf@}T4(lR;7XsJf$>D`8^%!Tj48i;z zM#_D7?a?1HdZt=^kPgsH7av%$)dxXdNG4lEpk2SwBkn!s6Y-hu1z5=!zxOWaSl7;&nMXO}toToN_CRTUWOXfC-dM(}}wM%9oc;Z^^VYaIg zxRa*+F=yg*a{)95(#-RN?s^8%7sDTNCV)|?)`+mks`{0+(E`hHR+lo=b8A$a%q$N1 z^K>74uDY}&!?g0kQz{B_0`WDxgQSgq8+{S*edmk6=|qa`bhmk|-bUC9gV2oyaTdwCES7Ub^PQc~$e9kTvqd6q zr0J`{0Hp#j8-^ccNr)@@5sCOcH38sXvSkO|=Ndug9ta_EeU~tB2_zAYE?5-C7nb3L_JaB zGUMtJ^{u~@e?!-j2%uZMBM4=Ua{g-vCK`;{g6QOD1!I~|K8keXhtgd22Ylc%42thInn+(T+DWsnPaGUEmkK>bm1aL!rzX7;V4#FlwvD9-#pV<8S=esXkv2U7 zOa1F%%ipRVSE%9teamC>&M>0jH=|@anKI(8mkYUy|ESnmW*K3FgSvpu@MoONZx#2+ z?c0Yhj>he41}lVW)jB>AS!+984O&{Egm(ZbK-RxlFD1;?k!wqwq;z0dx=|qh-h`up zKlmxErm+Q$^dUDu+L z-kOxmTF~PrQOzNQDpJ*J3nw;IA0K$XZr2bz*N#9k*>1Xz>zN29kH|=%DXjr*x`-;S zp&z*dnEW62d7CiiA{Pm6pcS((BAK%VTUQqV!P%c&2Pp=eOhfXbh~PdlreR4trPsxpZTa+adgOnW{0@Q$ysL21EAsx z&sVgC3LlA2|>$)K$D9rE`@ zDN+U+X?q3Vp>N;%GAXasF0L6bmrSx;RTzhuFbcynGmq0KO9`5Iry4E%XrMjow4R-7 z-W-zYdd`2iLJ>`R+U?n+taOOYr$H{1V8MoMN)y~4<*Vsgn}0E>y7-)!JrIB#v0&8H zT1whxr^Fp)_HPuCK=zMht{)kwa@@_;jaxoaGAZn{jRJg&y--dUQ>bm$;c$OmX!M#Z z$B6o)b;B#fwg8)?CJ_FytIJ3lw;Q};4g0nkk0~^vYou-g`d_E<-e?=hQ52ojR#$)0 z2lOIU4fijD67yk<@PQJd;r%`4e;h>RyeyMQ?5>2n1Vq%@julR~i5a`!Z zpOeCHl&w2+qr{xlM5t<ZA0@xgc95pN{u^OIWB^fX;@2?%;I*1v-5$ z06#em$d*&g21GQP;O0N`@(ddrlp^08PdbLa;rOJa)o;Mb!UBr9A7%y|OpDU1;Om7~ zsH|A+@caK-SSxrhoH>IVo~p|ZV~a%c!W2pLbP zK^}z0Laoa z{{@;eCoH#p$LyW|B6)#!r9{!r=4eCK?{G98&&H+jHV=U@cId;wKlhnNA*hnOv^Q@b znA{hD4`Wa=cssC(S1DqdCU5&~{qu&N26^#~rmDPtOj^@KJw;E(ylHmrWx{MEE`hXg zf({IVz@su3J0%g4<6Z~G#`r1+#0#i{zJ&t_iJu)XoF||Yh>^Pgy^^;9dhOP*({2v*jiQ zoWJ^12ZgOQ@^wyX%o^$2F2sS7uyOEKqGoNE@S+NY7_6b$@|!s15v1JPs8{Lh5gbKud@tQYV8niW+-bt*%%B6BvHijw2kOGLw9noQ5 ziKJI<1ztl(`=SSvo7YWFPi-Pfh|r9pkiM&MmZNj$eHm1Lfe6eE5AloSE>Y8SZCC=xb~)LgAdNPwlQ23tHG%nxsJJ5<5=xY1b4Z-k z)g_VBRR*@ofW(LNys51j;e4VqcFRH`U!vJo-|tPo)zPEFNTu$%LDxD-+X9tY&Zqn= zk+x{@K+eMqL@s+#nZNVvffZ6IyYnPL=KlsLKE>3RC1LINhMN+lReYuY$@=nQZt7rV zd641Gr%_q)N1fxAY?rP4Dwlm;ou>f`odc&DBQ}s5!>@G1~#%2KHD$k7^ZmH{O zomY!*8A@Tc+O(a@sqRt3a~HBr}&nJkYzt@Sa9bb8`hBC%;uL!e)67f%{7X=^WtplKh(Lu|v@x%ORsh{^O zu(`vk$c8zf>zfm7qV_SKYFSLvb`axFH)P|nXlcXb?V$~b!MrK=AMtO@LG&yyJe1@3 z0{G8>fcbi^EC4Bi73%nlRnTWmG?6#9xYBfwYvPU8(M&^ljpf|ZwVqUN=@!1y+u>_$ z9WU+T!iMGD>)Q|$CEY=)tH|#DJvsqcj~~x=9OJl~V6}1WnA1EB+X;K(oo4U)&O?Uu z*?EGQuUYKm;aSVs-@c&LHL7bkV#xYT3 zhQIE3DL6^G8JvRJB}TIAI(JL^J*vfmkt+(r z%#u>)2)v+TR6I^JtWSq$!m<#a%JptDc;(*1bz9y2$Z|2!mMov49YP-%YXZa_QDR9^ zf96O_hgB$bq>{~lGW~Ci4fA|I7wIxw0uEkz5E5TpI%|I&ubPi)TqU8>l@&tQ64IKy zKZD;I32{CxgLmRl@IY`ir_GA@%JCx2agVUuGM!Wzp}|b5gGju85+#vTYf*r<6!mWI zxVPOTYCwi=z%uc;@fMaV#_o}zm~yiEJbKq*ekqWN9hh=mnIjchx_O22!y+Z%Q4~y) zBojnod?cGkB+;quk5YS26owRu?vem`7M~>wWUXu>B;tJ% zhZA`btsv|BzYyV*eeZl>Zz{7#6$vW%97`=PY&{7U(56j1l4$Vk8oWuL?EsK+bhT0c z-s0G_eI2^AaiH##&Su8TbnbrJkKf7BtQCROYFZ&b)|gT_;c$2lD-&b&V@10CUgA33G?4FY*=hG`DA(!p+d=T5ibrM(E+)H$` z>O!FIGx1ya#!zFTyS96i5j>ieRE49T-#qHT$$o!}l7S%XN;s5qEm>9H#0g^gYN`^k z4@b0N$u>SBZ0q5n%F$y%%^md!qWRYvkh`!mx$;7i(l#2~&LDQyGK;nw^vLY2!O?Sf zPMuSq;xs|YYSbr%0JfUiI^i-L&Ly*%Q=i*1(`MS+%Qh1(>0U`E-eS~nuMfRayQfSD zaF=KQEe98tM#u=0OP=XldmK=wk5WI+tTgeqArA4t!korE3|IxAlDagp z=OdUIbl}A5MS0}A8hbTY7DF=#%WJugZ+BZZW46djjoI{iz$C*baXnq;f-+kk>2a>c zY}js+NcW#-F#*4mY>s@p@a=3CQEl7k3FlzeprI}^wWsJymu^-f+G;E+t{`+Y<=e+u zek)}{ll2Hg<@q1Vl4iz#O1H!T=^{_$azcRIPc97;<)liJ8t~yVGG#*sAEMcFrSx^j z3XyJsCdqo-mAB)aT{JdRDWxUZb=JJ+kT1DsgPYc>ibDmTc0Y*OSeE4oFEcDT%yOhn z5dILio{Py_ZG?o+FiUy4j!D06Bo~ zsc`wnSt{kWRvnIlCo>|bMEh)B;!C;eEsNnChITMQZI^GIkbHstNC{~8^9R#+_|>I^ zF#E;WincLBupBz+G9(sbLNr`on}Nvm|6?A~ljAat?7~}E(!Kub42aO+&pXy_R0T8+ zIbLzs<%JuB$((scxvvatf=KaP4v~lss`U#V1@zBw7sIZy~Lxe8|#yNl%#W+iAuO%ZvzrEP9It( z&w*$vh_hb@uY2g%72KsAmj?EO#;m`pH$Hz_Vl!122?ALxi{Y$>Vrct<$9`~Ie8oDtSeie=1mp)_bPZNm{Ruzd6-K4JI^fxt1)J z?w0l_sD-s$;#J0E1n9@zR-mufAqcUmLh-1P0->T=aOfHkEs&qE55%3(t&HqZB+Dp? z)fu@X!7ed;Kx(-~FX08cv?ZMd`(cGPr04Evq~5>HqNT^Nccbd~5o9*}jMlELW+5jj z{IQfY6BuPuJt7yBKvq@vwL+PT<(ND4vIv&v8h2qbv+oiYTHFs-<-4w6|8o|xP)!3U z8lOUSe9bSu0Kt1%127l2Bn3!DjnjZH=E-IH--4`d0J4=XkUAp0PDrwap*7?L*8nZm zJ%Z7sy2FOzPuMNfOI*v6E__+&$*Eyh@tbfwy1OXZn}j*%yo+}iLm`gC#kMpjHrv1* z9a;<=s3%?TNhuCJl!^aiIwwgFt0Ui`CTOHEQSw<=Ldq1c-jpu-b0n^@c#0|guiGP_ z8CjskN&*pL&R1&rS;%s?73l?B;bUjn zC2@0)m`mrHtE{%G9k;@p ze2ple>iy_%Fjb@lVM&YoM{SP8bkHkFlBh&?pu=I>v zSd4rH__tjduXsEWAK;eK$#3Y$LXiFD9F=6gP;V}XJ!ww2y@4Kq>U_8j0TApH`f`Xu z;`^fIM1i(xQm0FJ$k52llI48KhoH_-xFeZ&gy^)P4wVU;ExMdqm!*Kx9mQI+gutca8Oq&ox9KsB8}N`boekA!=8RGDES-ppwHXI`g?6>6gqud~{LR)Uy@ z+y;Qb0-BU*z!l=B>0lKOO+smVfrFF2jQ4?$cCQ=tb^2a+0h+T|{4JvTjC~x6sO510@}F@$tv@bgeB`m#J|0S_m&b=_+c_t0_UUwbD1ZI z8cQk6(8ld~^mp{P^H*F0J8WmYE2x-Hx3(ykr};=2+ob*UX+-=f*Hg|9Jqf=p8W<=* z>G&>})s05=+6e^$uam6%a$)FKW3T4c+5^kzu?`5B@^Q}`0!uWs4!Zl`;Uf86o+mj* z`1v~pvJ^Wol5@yZ7N<3ci?7en5VE{9iw6EaAIz4N=Nt+3z)cHc!DO-j00k#4;$KyQ ze4U0dyQ_*Q0oL(IpzR3;B#6*%+aB?;=@d}Xa{7v#(`1g+#sf62(n%_%A8oa2Lfv|P zoCecj#JB7IV=~vKoJRLqd(&~(G@t#ENpVS~SI@Ta!~#)XlSO15BT^_V{2eeXV_j-E zN^{+rrk?9{(#=p236gvS!gTzSg>77XfB~vkvkjO+>~FZuT+37i^rx;^J2+YpapD=q z4GEFg^Tmao7K!gJRvb_5Y4AFDT|GuD!F{+m^pruqNfHv}lMW%enz_Ov1^2kpvwng` zei0aY0|xND38%EGENu|wZUEx)=dG|b;%c8J}Z$STiF` zy8Eenu3IDdr^-uCAS3O&Cp{sOWab~gQr-<<-kGTK>Bhjqk->$?*)01c##P0}u%TyU z|rOffjy*t}BNE zbKU3qB!c+b7%d9wBdDKnpMD6p%E?lEEU-Q2@WshW(Q<;G8pyto3FIH%0*WM{&qK&=w1`8qv9Lt5*xA8o(GJc&1{J}J;P1}XO`^)nR@dl`7l9e|7KTY zw)bNi8OD;Y5v|IP2q{%PSqg7VyI$fgYIs@milGCyRD8(r01q^UX)DLmv8R+}Sk}r| z6@ekCrBG?O**I%>7_8C2?8>{lE5bHJ-CD9Kcc8uHIPdu-v%z&9@N`U78whOPoDI$Z zsNy4{@XW8di8nCUc~Lw0d9Xe%b7L+aZjwV=Do=H9A$I|66VrwiEcPkpcXdn3&Uxj( zS7=QUT)d@;KR-wx{<{w9J<=hE-LPWS_nbb(LBn^hdCF&!l(xu-icuGVJrC*(W{?Ui z0ZQf%(3h-u#xNmROTv=X_R;YW1zuhYUVia!dXtEi%nF4kpMI&uqlzb#D^!CnV&-6V zQ{>x)HG?H96o`}XMf^%g>5Q5^pa;VnpkN)!b^k18T z0S+r~$anNGe^puULVCuBs-E@WOJ!0y*9-?Jjcaz7GNLP~e)Cz0>9Uhmo<#@P9;pXV z(8lOUO-P62M)Ku`5e#sU!uw#qNn>-Uw^Yi{==!9N4z;=x{Ohg+=*evNBXf6(jBf4P zyZvbkR`hEpJLzB{fZm&hLuNQAaQ%%IP!qJ3=7TJJLwIf>tqTSLbWrjG3l}$pakde%)aZt}9U0SW3F~HJwRInzxZ7Vlu zVBR{gM93>)UQWvevFZbRiZrhz9$M+%j^e94AjOS)Tm?QfPunj!1IJMwVS^sG1w3r{ zPbv|S;UR0G)Jt1YKP+1g{CulXE;?15xUl&sW1!FnWIyZ+vjpBJ(oPit>58%6pCltR z2iwvmY+sSYEzt)&c6nLaHq{%RH*Cx@R5`-d*p7+;3fLb{14`2`L8{mx`nsnD;S-xS zs?w&p_dldrg*Aa~&B zFWlTiy03w3?&}Hz0(bcs`1y+DFL3yk zz%43;$uYR+B6iW2jcMyj-B{sl^L~cWHEN4~amR}_$6~uFzU;*KSl>b62xmW$3mNhF zrbm00f}fq`*>p@VnpGtF`O%`${xn~lb`L3tQG!B0m5gSXnujRubT#dSrS;!!wcuHS zMyGR<+z#%@6|&4kdv{5OvirfI7vg1HNk{QQL_B4(5=>6s)4Emr#%yN1ore$)Sf9aa zg#Xm_>d7o0t1#F{D3x)F7VN(E<8mFbAHv z?Hs?Bu(~qVhj~xbOEK-|2Mlh(G0|{5pdw?}t-;MUa!EBzFDlSKl|2pThEOTi+7*~gWD5UDR-bEDuv=4($KSUd30=8O&p$fxbpMnF0=ei&8e zb+LQ~J6ErigDFb(YA4R$l_g1rY11fx*ru|N*R$#lD4aTgNa$dlA;xpPhxFgSUo7K) zvf#SjK|I1SnbVpj*wJ}2_*mno`?2#OPh0w}w56tGb{?bwM6utM2L;33d}M&AXT!cW zyI8lUV86DBy5cV+jt{Se<-*UBW2;QRLv7gAuJPsDCtH2r!&l;F4;?qvikE%69L1C9 zNZ_RI%5wq*&PnvX=s66=?DdX%1Mxnv6zUruFGa9|kf&sC2WExuioo58#it#DA2PjV z<~G`_!xzaXeYZ}C35@p6!M;nSUR&gOmf)0nkx+@!H7op!v>MY8Ua}MjW8xVqz zsED*TD>c!Isvt$ANk|pI;NZ)~JTaUmhUsH@CMP@G-QI;sOK`Jc{_Icv#LL})1sx1( z5skqoVXeKa{4Kxz1NPrJ%gP0dTeiy5S3@w2Ls11G?5`kR^?K#aCfSDkjEWo1bBM|? zz%KzJ&FINL;8*ZyotSv)(T79m=dQC_UyvjZ*C233dU8kFrxkvM*kF9 zPpT(aiJ}pS`NkK_FkI`PiOsw=dzBXlK3&>(o&pfR|3>O4dMO7bGs!;hSYP zmW+tNo1{XmmwBwujAFlyk%n*#i_P9p9`ED@Us;Dw61@uW4%KGpuZGf=I)E*{VGE>j z#AeddYUqQ(V<=q?9jU+;-K+ZrEtkenQNcTw!fKMR6Dp-lVFED6iG4>#oclH~1}|&? z4D;UA*T2SoimlKXQ2Wce+|)VvQKBqg|LCYL6HkpMWi$nBO>1{Q5x0UoZR36VqZ*?) zYq2Gceb{q0Dg;K+DBxkVguDtMeH-s{9hWJ!H<8O|<(3tItdzbMI2(WNM_2q|Oscuz zcEJX_HMHeSnt~+XJvtX=>(b|eipOKBU}uJMg^0bBE6XpjTUW-ebCI!qgg|$hKNJz% ztM;u!Edg9mo1ptcRiiGUa>52*he`CI5DG)j^ay#dKDL7m@_2=>fV<{OYPzpG-*Yb; z7QDMrMG}3)fnO#nTXP+i?rHE=>-0R9(nG0D-ByocVS=s}qC`&Osi;K$%KTB@9xrjS zV{(xxg^-_=AyB@a?X@rea~Z$lBL7DvJPwsri3mn6xnM^sq+XLn zVeYjyv2;l(s_@cXQP3MZz3W7NSH`ZqM2A0((U^%Gd)CdVVOXyo>s?tZ6C=a8--D}9e z57_YHdWy2u`SO@NRi%Bl4Icoddb%Gypjt<&B7i{^R|LtAa$4!um&L!52cz^l z1(AHI*vlC~Yk|T2(`+k7*B(P58Ww>ls7vOqf*5%DiTOe>2ns58Khi+B?5WsDYsil# zG-vNkGP&)h_Cnv`3q@@Zc!%60toz+~bl$(TF-Al6dcWa~YPzb~Zj2Tb*fu0O&&}{L z&nUr5v(Vd_Bv-V6V`O^cq_p(vHRWW7}A z>Fsx@M=|n6y0fnExDG+HU{bFX|x=(Er7zrIX~!Kse(+j8n96@qKl7zK`|n7uW(hxYXA8vz#vuYk609>Q)B;Jb0wo4OPmr=yszLpMY>j>BNE)Q=@$jg%CltS&^P~T(^Bo^U_8;PaWK%M+v8y@ z%D|t<$@AZI>68d>Sb{?QjdoFI$A@{SHTci}han`MU0azHwC#$6inGb-01u(z5MgSM zV69Edo$rLhwIOrDJ)9Nq1OQ>jde{+IH)!$P_Ch3}knPXgIRY0(l^o;{=p+!Q)gw&^ z$W7UJ1*`BZWH@(&@c6!7#q8ekAKQ{R09I!drBEW^7*tC#R$`a$zb$bCFfI0q17V@` zu^@Qw)hjT8!G2x?`tnpB|2i}gefLq3=CC~L1dY-WhOIPJSZtzJ0_u%;uF-yev3(E% zDl{z7XDCl@UELp?_{@~5q+!eT`3fh{NG1q`$_2U<<*)wVbqloj9yv&=;N#yQ3oq26 zpJZm0b;8o($T~w%xxKfIhsOa1k-nl+Wm~(cne-iDZy<&g*Ny{yGr>lQ6{qHs6kuiU zd!Z-3KuZN7p_gXrpsjZigVBIFQAOWulH4-w9Uv3~Hj~l!R3gi1ymbItXAo?N44rcW z(Sq~ZtrbJt7{}30O7Y-G{@7EwZ4w5>i z1bA3p7jzb%?HBY@I zOuTb}A=3n;EcL$O@Q7lw5tH0`oU*%0Jb2t91D%ysWd+>x2`1mUDzYa_&Qv{b8F41o=Buk)Ke(?t+$W#W zHxZ)SKb~B%UJ$9q>p8283iBS(ZpDMH;<WHAy%_{AeZy{rLBQDUz%L2bT0 z#9$6qyT-?9a!bhi^S(m$2&C?7R<;3B87Cw>Y4c3a`4 zSf07R{6N|`VP|IGkJL!LB93+u6wi%-#7<$>@WRUW_0Tx@3c(9Vr`koVMxwUNYurRI z3qun`DS2wPOJEc9RfeR|BJ)~=nuW)p?^2%4g+O%fc!Yjof@EQe@gHzHG>VuO-R<0` zGo%DfCeJ;mo_qQ!uw=q5mKETWOT*w)H7`jv2O~C}b>jiB6*Ng#xfJ}5(CZtSy9SLTW_;o!} z!}E&@TrG!xlMEkg)9RR@k(bKPA`U3fD`u?INJ%hK*k>gl_3_&LiV}dhDIDRA0uxmt z2ek<0QAv|~=GZz??N-+K!MTAyc7A;F&FpKo+yQMnEM2H7Vnd28OEec=ySW*_=6{>f zjF~ekO|h83ajJ#N(E8`(S;{IXd(Z)~zkz%GZ~AW|A+`SrNc?FGTXZQMsCD6@(M2YO z`@&}p?m!;HE_0zllSSI^PA{I2Q&|S0_9rE9XBd|@;{US6DqqFc3|0qUsKZLdtZe;t z=jjOwu!zrAyzl<^VY^^m)Y@tUo>QBQq*g_DH~Dj$2w%u$$cHO5r93v4XjGaXTtckj$lsNqObwX4IV!0 zVX0KMG9_smmsvEo@6#73p!>_P6(i@hjtIso3ZZl6^$;wqLtjP5sZOz>I!s*di7E#Y zKYqb@AG@VS5KRmUA?u_ArcB{P)~DAdiuWckOdEX|^; zI8Q^^)8^0Yst2tV7|@G~{_JL9Qmh+!4B2R)c=5#GjaUAuy1&RbLv_W`0IQm3 zXbRG#Qy(6#hXBHM^)pJQ+-8BBS`q93cU#(~KIM*bZ-ir= zicj&<+IdgpFP(y7o_IKx-D-N-wf~~qwqER0>+Qbx)gatoo+Jf}XzyWeGa)%Lc=TL- zc_+#?ef*^|l0??AFQP@wjismS*~Doe??3u|^GD}p&c<2;zGB!f6VZSJNsP^)4vnU2 zF!iTps*?}WnOUhI;*&6?sNR*3c8#a-F6{?79YrsunOD?4Vls6NQRzb0<|0zsdOd=l ze2k-%?4NZUzbA+_q<{wOyXx*v3_@LELg_i1SDCHR(EH2cruKtK+|J$ z_4MRnj`>ShdJ3wbJBO`^r|b+n z2>aU2UKVkO(EUObdDboUn>8)QuNOA(<`H`c{mUKg=9!16O$XqU%Mz3Alg4O~A zE_VanF;S>tESTQi{NUwnqsY$Xp#fchXMU+}SpAL?QY)Q>%^tVA21mCV@A~Nf+hkxLIkg&9oW>W!{@r#cZo4-pQe#ndxF5k2{Wdd(5DVY6(}kQqfoy*Y&@Uk4Z1`zsqEk-!JC zoig1(1bI-kMW^wPF+#JUE!gD^+@{m03L8FS54{wU2a0MMw@kGq3r#=DB?F%2bG(&eTw3 z>sX`oReF<`M@*V%&3Cwx`Z^7`u+ehmW{RY`{dMNGSHs~xjwjWdp{0;@^pkO2HH4S@ zuN^pB*hc#(IA?Ly{_ZKQ6~^4G&%!HP&sNIa`F8uPQzHpJebY{}OS9%G92Vzm#tm5# zJVNo%@q!y{ZErNd9jLn1R^u4P^oi~`7Y@;VIk30j02rEJzm8a8`}ZisXDTn0dDI$t zpm|x3Yr{h9;!n&rvF8KOKW)1Y@7Xj~O0{4%;WA zd7TFKWGV`4Ah`hASF_+`hy+&5J-9`L3~w8sHpHGFSn+@AsYFR&~1O4ZZv`1q|miTD-l}y1C*z z7f~Xf*JpTk^?txJy0>K`-!6AIM(9Yizl`vqg!@Rj_@cGeYP-?I13nD+L{|@|DQ}{F zifn5LgaQ_PBEAF&LoE&{F`5_fd~fh0yn`kLvvU>8hOoJ1Z0Ok8@Ov?12X3@eLQyV9 zb#;v#PQFrU@YcX)dbIVgP+Lf^!%#iDz_5QSLAS!dF~cq@&Y1DHS|t2-AZyl@hFRg# z`)uF83gu71SpDL1lpCSz`MWUg6U6Nn=< zqxFGloUMe~J6ZODdZ|$|O70212pp85zN_AFajvmex*88cHx+jY%}&h|RsBkaezg-S zGV@MZhCS&P63Px^d>^t1mA6f(tHRkDJ6f#dDA50;!u;P#9SjDrY?3ewq^3fgM<2(x zg5LN*-hnX>Zu;IIJ8y~B?vTfjV^dOYSS}zIf}n!d-&*jKwfTui0N6_sL>!Mz(mmsud9^gWKO?;(p;^#-S(|cZnITOy`nhs62=Y!#xbkZXB0!S zrYK=^Ff_JHJ`c*tctY&N&_(_K5jOfx!Fl*bpRoLAvNxvrL}}B^zXz}J$q@xFO&jTB zB2=5a$SMX1o0wXrwBTN@kSW#F;&8X%k1!yOyJ&BOJj!N&&^Jayb{E`Y}jypsKnoi~?O&TaH+Q;?J!6{(Ym}6DZQ3G^J z`OF$<(DFe7-lg=0Pu9tjB?$`;@*9*6^AIpkd$ECc)@3WyMT$Roo7}z}ZG|bd4qQ0Z z1D5Wb!oq%lITb>m=Mm{eJ ziyk zba{o}CmK(B?yCDGQQH38CzwB!l$w!Lj?CE3hZ{^*`ojamU8WT*ixKMk-6phDq#8CcE`_wxaD|Ci*tHKQc7bB$UN znOg3W-X-N3C{S?5Ezp8P$ocR38Jx*Sag&YeRwHa1`fAIC+5}0 z-J&QfIrKv6@P2?{<$U{xozB4 zuukfNjjnuvv1_+;cDuAVcN0nwUCim?n;Lgq#5lu2Hxag{8EC--?y%)}^I$few04Z$ zG5YacU^wDFMuBk{*ekF@)u@)Q#%hP`#Pcbi-)_Tfsxtv5ne6Tiz2hYN8zk0Sf$C~3 zf=AQxU{N3RPvCo0LKKsW)2#IA=ojJA3lJFF5P~M8 zNSkR0yWCtINBLmVd#%uk#(c^5j6SfIy8vD&WyUU01u%`*4->Kei^ zRfcF^&KIplb}7RJDne}3-5PSZA=q?jro8p(8&-lFQm_Oaq4u{jfHajPNlNmdc^)(| z73aw;cDe6P3YFB}Z|Atkey<0v0h$dEv>wBDIETtW{X2+<$Bh%Vwh|}s$p*)q>}Vgx z2r~60-WfaKtZ~1{+6q+Lu#z*wu`qCrHF|qXeUr(5|3D#X_CCA{BRh^&IGck%=hQ_q zud>$Dnk1D zP=8vKW8?rM$y#9*YI>xjhZc*n9H-8(4rMUI+tGDo3J}roWqmwTP%a%@XHyT$z;Q6V-kxoC@(jj5YZIY4%;byg%>S zUKkCrK==CgM|AJ0;yME_;XGTzk>tl}uBGL-V3~UXe@DL(9j-UMG-lo0@s+meCy%~A z=o2XQI%ApQExk)6^wPArBf$OVS&PP&AA4MHaaUrgly`pOC&a&qIhE>WQO z`ISqXC}r6+f&}dwpae0Ov18`jK*GuLUEXs5tvbz-lhPMc}?3+l^44Ly<;uz(gi zbn8m3>!o%@6|tq&ktg<9@99FUHVl*bk=w=nVhVShCUh+m=Y#-b?w2611dl7ZEhMd|9;I+E9I+9_O30{1RkL`gX#D`ug~ce*=pGP~ zhrpUO{_}b)ja4UYaafZFqFbTEo>>C`^X3Oa^CS4{)SrfTGLF&~sVZfJ(+I$Lvz zye2VO1vh;G8R_O5=)_>-|hDF6GU_X#vLqA zYt~r8iec(r40R;RaM}TFc6=Twa$xpUCZ843{DtQ?NmRNyW=OjbHtq{=7^J(N=4#Cj zrO1s9yiinpywA+E7}bOT=m)W!MLZv#a4KECu3~F0vb1U6VY_m$@97wr8h2$ zZTjccSUwxsEPb%FVCHDj`iKN7QyN8TbS1VWGMgeD8C?r)dTIfP>Ehfx!nKK<3w`m@zT^*Qxp%Xq|}(2yV~gGNOe$H704gol_~AkFTcvX##`T~R^n znuN6-YcCDXt#1KOsS_~*LOa_Lnd7U(0StkC$hJ! zIVO{4x<_-6(2CjPQL4BDJUkRVWObqtzz68h7+%CiC`Lu9 z&XgyZ?n8Nwh=xs_J;mOHie-^;0&V{B<%qUp0AJi%CnY^6>jV+5K1I>R0<$A8L+e~Q z;ja#tMy#)H_*iFq}zxj4nnfvz;=`Rqs!T z`HKTJ=^XNxuD7%V@29u&`}Mj_&l`z<08v1$zb^|lBPK}bMf~5V*Y07n0({GpBJnpv zjV!sAUHNN!Rwm_AU80T|=GfPy9~SKvCkOHT1xACcXPz#C7P2CzqtZ8A0|a9|rB?XF27wiw0qxR&&v*R{MyfLA#2eXOvh^A%`oK zOT-v^<|tyn@V-HHc4&lW8a*PHnC!9-i_x^K14s$ZYpfC79$1blOi1g=)qN*W5=Hw< zzS*d#(|bC11%7w)@93Dtl85ngq3;&SjuX_`Ah2qU5e&5MeFnspzv~efUl$=uJWzHF`-ljB5}ZY{OH#eVQ6B1j~7*=7JnSr;e^61 zNsZYXciR=~8H#ysO~C7XWBxr}Ub*+pvb%4^5i$@INmmIJ2w=NYw5Mt1V06p$T!^zq9hAEM6TZ^tF4>iL341`2I7eF! zjzDCnjWd_Mi8fitMg}H{^eem#vurq`7=PjBBmP;mSu5g^j#`ZpBa-do`rYu}xE4@v@oGp>2Hv*g$8jk>t6`A(~w4?9YndDf>^qLmA+!Cm-+4ZY^*BN-SJ7!F128muLI>HYGua>& zJ!sJ(Gm53#_8+>?CBW8B=G{{x$@>hKzL*}cs-r@QM<;t>YL>fI zp9~zpqa1}uC;#MUPD5O_PDn~ezM-mfFZ+>i6yl+xz`r+YppJGJkw?c8gt7kY*NdqO znGyAd$EqKyuRb0s7s;@+Kq?9uxPriP)Z`WwDrfGN$nLP7*D!IG6=8L%#4l+vXvu(8 zu@0;x>#XC~=DAoFxj6{$ld~1t8blBxohjHBmd)Uh^dvjSyc~2^rBq=6%{sUbhI?FZ zDCTG+L^lv2v3MUf9S+@uA@H`*0lF1b^}S>C*I+KVCUWR7a!2`GxecVCivzk1l+2n@ zDwJzp_hLRJO3enl@-jzhNd}j)w9caE=C!N!b0y?|Lv|;p{{J_aLWw$|CgkXPFdkkp@g6* z0)}pXZ` z+_t&=FOVwlo^r&9i3~<6H^sej zGG+ySsyx@&SrE)E@p8k~Kk4PhJ(iqUPt4Xhjh6NG|^zG|(> zTW-%}(lL99zyN|W!Rlpr>Fc4=;ZwI_#Ycnqd5i4S}=@>R+s#2yRL*p0;s?!%YthSMx8xKkvoFmQwTGk;` zc53t2B@q1-+qQ3sn7f!Oja2mGDI0x~Ok8LZNH|33++&hJFPS?nYPUXS|Gh*BV2nCD zx|4WSG`xcUHQZd%PA%05Q)nhocV(2>_6^z?OV^q;-R{nQ9^VIA((>YHZ? zgtXeh)i9Jlh^0Wow5{Y8(h`^ADN3%r=eLyv(XQJ-`am-mGE&V_@ZMLrB`Acr6Nd8SIA$y$+iUg5Uc$|6txu}F9Ckt{2a3b9TP(1c&HC)J zA+covKOE(?qf5sK)ZWBc-ApB|WqKZ5DH!w=zjll2o^s^?+wN)_W-}NysLn3vHATQK>>dbRn15L@Oyyr7Z?u_ABRxP)MFl=tHegtsGY@u z2vKMtG25VUQx@v#IC`Nw&@VXK3rodsz6piDmAG+$5l0Ad@g^Wm5u*gZ;Ja1*BGx$G z_mnoAm}J<``q~Dt9lPPgImYwC?Ub~d_#@|6hrIrL>9+|Tym8!pG~e@X6g&p{Og#7* zkAQPcc zWhxU)ll-UMC0lv(7>{&?d}S~nvj))be?AFFWKWUOx1E4lT<*oNTHa#?^pzQzRW$#W z%$)~o+x~LRkK;nUK%}&ILA)B5l8+1KEWBOY-DK9{t~BPZ5zg<`Quh(j&<4@71#hCu z{yLQq0?uXRGxI>uq!=~<`dZ($)BCk4k5C-agcWW45}B;Ib9+pJJ0TR#*+d;&=V3)E zxY<~lNkk^bax=ArEZc%MmUI&N5b|u!C*x5|4$xW(ynxuFO8pz|a*x?W=v!jpY`0N- zO@lACYRP4fRzKBcONng58d1ruO>Pw_=C^|MoV7Q*tkgPm9FbjF6%3FF z?*kNu`v?I{`mV-wHxWW*;7Y(%TVCQffVwPqAnmBT<>yTEKxJ7Gn~y*^wDLPQ?2rz-R)vYbQV z{3osG@BNZX?_Fx<|K2MyA3EI49!td_7+7fMVu_On5prD4VIL=?J;jFg>;Ri*80=S3 z4G$bXK#~qOaA4)W4~y^t&x|I|DX2Hf!H1pL+2k%0-SME>2O|h@dmw8}q`TiTkhh3v zux;wSl45#&y8S6)VHUeCtm5CzZo~r5wsL_TXl)_LtcJCz521Wu`T+BH_A&f$Ayzlf z9@cI$++@TY_R{cT8VrfT7LQNB{p~G6LIwa=hmNMr0xo(a@DftSWhe7((Bo;R(`c6e zb7t?i&VR$O_{Eklw$e`R&NcLUkxD!^?a~I1tNfeEtq+y>3dKHVbC!(EcU3nv(Q>7h z#HbattIHW?5 zJ@uU@imM70s)lz}m^9lKF@q1*cT~L&`Nm?d^0+Gpwll|@yW0Nc(0u9D2%+L^!^*x$ z1~*M%rNVY};F$qLV%1rQ_vvl?l8%plC#e@$-*M*VnCQre3Dl)EW&_4VU&i;;MU0Wn zJnd_^hRpSE*+}spk|cz~KP({;|EJhRd65tGQ;e{J`3>duhLdyo$`5Tlm^1GL(9)EW z4I*0TH{~W0>&5<0LdnZL9Au=t@8U^0&B3LNRyX`mOS9Iwyo1!}vuW4;G(}?MS6m{0 zUSa&>9i8Q+A*DU!`RRs7uxD3c7$1GC!WW5mG8#V={ z0Z08{UQ45kX@U(^L)6yeW*7aoONt@x%9BbQTIoX@_!*eNI8+K!@O5*=kRQfeA8@UV zal0@n7_XxlX|Yd2Q)w44&Ln+yF_B%zm=6V_sK;K=L%~_u#LJ!_HH=>06?s@VBl!_A z!mSi*OjL@SVu1bnDeL87AbP|RQJULC5Rqxy1zIB#B9+2fkh)c)bkg8emW?~~sVM}A zICLh5GQzNbgLcfzJ^n|GF&K-z^(48-hIsbLpuZIXwXwWBT)iCc8|dq(G!x7bG6Yh2 zORZ;|_PDh!i)5zbtv7XW?m~?5cp3TVx}RdKingdqnlvFT#Tc9_6o(2vH}tA;2)?ke z4KE}JbK2Z0UAXP51j}*x8Q}VsD*dy)c|fh9Y6N2D1P)29iM!?~1dl`6j)g7R zjWc5wCK7r%-Y)f|Kd*(bQWyl>4c6wKDmW8bG(aY1^Gwi$&ekiIs0sx0H5lky?)y$f zR|L_e!b#pVu1^}isB|2jWvGh zP#)_@oD+@sZE6{^#Ogt|8imNb8oWDMH{=q;-7AZUG0atxl;BX0;SnBPQfPK7&m7Gx zh*(YBi&fuuGqDY#Mj!90A3Gxlf`!Yr;;8*wV^O7hFN~op9ftPiYAliU_p8Ab42o8N z{c4MWYfWR~OFH*BYPsA*pzo{23@KV_B zIqDTaRpFnF!42Q>4Z0#kd6Y-x^=K&sKQqwvr~vF0vnPbF`jit9@P%tGPSQ_CLc@Vd zCS_Nwo0!e2E;uOK%rfC#Hdm-drPWFf4KX~OH7@p(UBJR8g;|Edy8R& z&EU{xOcK2>qW^t_#K3=V(n<1bDALz9hhrG*ALxA8w9gABJGA!R zFGSEAP1I1K^JdxS7Dn~RXrdU+d{OO@DeOdTmz-1-f5-p97_n6qOHRLFGgymA4nQ&U zeY5XtQu?$K3F`Ve=IqQ{L+FhBiq%?~Mab`kS}$(|hj$>WTqEYNvp?C*SvUC*Gg|^K zXMO+ybbc?ep_C+ENTjA2L=eeHw1k@M7`+|LanJmY+azXe0>67vfGx4XsPI4|djv3G zo}KC&eFLgE6d1myk_=^l&_21XuNhoschBgSq%_Y9?fKPpNS1AWKzIeYx5OAb)E`ov zTRamh+KcCr^1GH9$ks|R#1h2DIym%P<=VaZHLWMQ=Z`lEhsYDGB!H)%^Ez^->bATCGrdD|M%;kiMOH`LCSP25eJl{4(gdzuO{c5#4;nHO_HK0%6pRY41*Ew=|ZC!FHD z6GVX|)gR>byn^(eMq;_x46Np)Aczh^42-g7&NR~g2B+^!Y8KTE5<8sQaEg$0;J7Er zw_cFrH%WMh=D7eGy_sxX-4dPmuis;n^|JKG;mb6-)(>LQA6&bda8f~tZZ{=Pngu64 z;tCj{TBs)G^60oqrRN43L+zgyG*OC(w zE+DKjv)e#1wCdl(=9q$RJpij=1M2(jay?>wE) zFE!CPqw@KbxGO54y8F=aX6nmNyJ!lvXLzZou>xF(VG-B|*KqZ6auGP_Om6Zt-|);C z-WdD##tRXS*IHg4<)COz)=BNQukNdQ@}91h|~POSl!xDjq3;5$Duhg8GKpsHOIa3R8BsDk$j;xV+ zNlwswPn<=rL6M{D0zOW&TDd}%g%~>_AL2;7{m~;U4uu!@M&bYfmCzIxo z2^lGQ7pm>iVeSgfl>F^~rp+YdpF8b6P4Jb-KmbSE?WsQNqHK7oODEB%A(&6jQpnTh1~j3>DhrX9CUuJJx0MRk zFO>~(eLGj0ywIJ$K+DjM=y_w6rAFhWj=w=P^y35rhEjj^KdDHsLWZ`AQVEc?=YwZk zLgEUUUStc<`kKzF^NXIBlA&d#nsmNv|M#l4DCY~5FZBgVMniRLf|iB$*oAe;R(5tKOUm^rPc1c zWW4`di2-+n_dnE9j+>nfS zBL&qX=zt#_tvHXk&wi)`(K)o}z^c!?3qoCq)Gn9yTqekt5ZKGi6q{)ac2d>9oF{k@ zqJ3CMZTEXb$`_(^K@-tCaFpVkVHq5<$YXpJ zBD7vM^IlZy_2*+xUT9`^9OGImp9jA|3?7kLZKmn>IGw;14R0(kil`oYr=8tWM!0=u zqh!PLYII`?S2s%6Bx-JAN2$hYONfxPd^}~qELeic8Vi76%Rf-1S%p~<_K?K2o=7ym zdQlxwx32}y5t9g`X_3)c6lD*sGAZ>j_hTlWa;>C+{$L3x@aj6*)@C~) zThN0D2lr}?9*;&Ewb0onGI69=BnA$bEao6xG-s0V$Fi((3#BuaT&7SmJD{z;gePSz z&Ox*3#~O&OB#mXe!|COhlV&(2?_Q)l_iL*^q|WBEPX1B`t?LuaEgwOIq*m4&1b^xV zpzKJvkBxHw)lMA^A)ln!ed(%71dlW2%b4^iA9@pziwoa zozf8WvL?^qNiZ-z#)`FwNTm~SFMBgy@CR8%G5`w~CY)_qdva=&5^sRC)kQWkSnef< zF3CWLfD_GbBH0;eA=of17&|Lhl&uZ_xELIz@{vGl9rF$tkTspM8|3|`bP)wPp`js` zq3bYfmaq2I5g$J^u9fj004bhwR4WA3vmf{Rzjm=yJtY1Gj(L`KRzyHP2zY9MKRX~~ zihI9!aix#crjxCe6<0gA+-rQB63Hneo9o;pnET?q)*)ky4+~Y5!3Dn`l)uCi6to4& zd^3#DlY+83Z~}fsPEg0o87pK8U1bs}(%!zZ!3_3Q&88=b9Ht@`f-$AHMG4T=EN;CX z#z$vCgw*%eUEV`9YVpSen0nnw?YRycdHghn1fMzmMnJ$i`lzr!Q|OVi`^}LPQVMmJ z@f#;7cs`wP`Zyc<5$NO2f^Rj9QSN`+!;*+GZH2i39ntWNO=@In`TttSeW?Lt;-vQ)XcZ{<%xsb-Kd8Qu84W*d&oX&eilV`>92zi@qu_nIiiG(9ZGtY&#!dcdGGTkp z(lP^&If#k>RG2|D`)rIi7G}NsKY!+c{G5~FQEVo{$G6^ z6vj5MmsvD9(URR|UeVJR5rw-q7e)h-B)xt}K(lkmAA73_Mh?Oj4qS1xuznSTiZWP{ zBY(?5cqHt1f_@{!Z{r zOQd93rEN;iO%kVM(U?RKZ8^1+FD==v#m5+Tg z_kgT}0jNtlLq`Fy!=6D);|38r(hYsQ?S2>6%Vr}vLxLv)^CD;4<KAnSY1^CEy$=F%QtF8ocj9(+nnAnl&e z{%|*WE|rofwfhaf0Nhh?239YS*B$B>Ql$6+kr?6S^$nvVxuL{!&?=Tl*w7Q49YhdZ z20Mrl9uh+T;WDSsV*$qK9`nTGvN=0kG_nGAB35>`G0;?KS<-t6JZe58A>GX7FoPW4 zEz27s5q{-8XY_t%wjQt#dp99fA`%_gq%Px12YNg?K=7J(xPww^uF{yl##oL^quk@# zBV016PsM`cc(5BzP~`zR+@8O;ClpQsy|H;&oNO~%|A?i(v3q^^ERhJ0A0yUFi=ul2 zb`_+wlz_BJyWKm>w@-=Eu|+7*?ZQu){(ux|k7soYGWEPI-{aIkuPBs`))+s*OO3$B z4X}qcmsRhjbd&c6vWY%Jrka;u4$O!hYsFS{YgOt8mCmT&QNVPM z-Eq+COHTUMESEyrm2nR!iIljA;`ZY$fc90}jWJnl&_Z-~67Oq8ivn^p4HOQs3&_ZB z$ou$4(-U(&@J0jND*xSmixCw^u4{fwc7ZBrG0L8-=xA?7_A0QBNQ2q zGl;KuRAR>RS&D?!Lxwuz!9AepkwpfEcJ&Y|uzc&7H3C1PZ(YgCu- z{Ou|pYr+mHgZQ4gK9_mAyvbmklOozzBZ}>m#rgR|&?>LQ#jv%~kl99mS~y=hcLDBTQXJCB=9X1-;8K*oLL|BXp$Uz_ z)Ze7H-P3qvsB7D9+CFk!spWqaM=^mUv_tMtQp&q3VnOY_gk3BqO=uy)^eQQS0AwtZrI@5R>Zw(ywXkyWT0Ttoh9iX z4+F1{V;aep`)@o}7o#PBj4~|W(ei*4QQ-oh&Rc+)ITlAz;~AosxNj4((&LqV!&L8| ztzojbd?`nKIZ){)`p}qduk@t`Rkdx!drI*P~8}8h}p5~uF46R zFb_u&6jtJDdLtN44gYO&Y)O{)`zBib4L9%7IT`lA%`n}dkk)H~gtBk(&dfxX&)=ZN zcNmd^+b}WgjDhSm2$`R=`kDK*6un1~T$9EkxMLC>V@19GW1mTJ?5v-}`fGZq?UBm@ zdwtd#PtPz|aU|5-(%f#@j67*%mu2=^0gegi^7;P=84cz>VDSoHVqqF)|~v4D0R zM?Pqtx6zE5`!EZY| zkDEd{ZY)A1@&5EJ={%yX*{k?GpvyqV^K=|G_-A$4&P#-0hh;1F zvErj`wRbIQA&f)GiF5MyMj{PCM@9^;@gKHq9Hz7|=?+D=j`!CQ|KTX~NC`2Ye60R?hs=mum#p;DbXS{rSmGI8mMJok^4&}<#ZdBf5IJ=zzcI0rJ4qfTWO}C z->bM_d`jK1XIrc?#=|0uwI{0$?K1HRksNqn)^SgrMjlGXI|5lpD;UHRjsWz4N;CJ( zjyVLHAb`wIC}y@^zqHHrf`6*uI#{!3pOcm%Gw)0LK?Bx`X^}$}MiQ&&4zL8=yI=U1 zKpN^|k9N6}FXTd<&?G1clM%SI>f(+X~tHDUx zEvX0!0W4+Es#=g?mLv}A<}~Xxf=KdX0J>EY_Xp$YhaBx~8oOabud2ye_T46Xg z`%El5QIqc_80v}D_S3C2jmy|*p>2~KT43p0r9RjtH^oZV%;W}Oc_&>6P*59katgYr zmuzz}y?%Pvv=zl)s5WVuuivC;8y_9yNR>DOdqxp9swO?0#xnZ%h@2%Y zx*F$LF7-C5FKj+4*rmu*zWW~!ygE`iWHz8{kK|Nn&jo8@rOR1UjnN2dF6#g-u?6dp z^F3t+ONbP3>qjGkp5ONlM5Yzu9kxMapf`rQ@jsU|;29e!wcd`=f!2B&%#5R8-F zsZ(J?YQVc1z=M&0f{i`mixF~qKw8El3}YgcZ9HJ4xA@_=>d6jiR*u>u*%jB_@& zr~K<9?_5PCh~3k14Ih0ataeEjw2&Vp1ue7#9Ub|`OLr+X9;Cxtp;&#Fg<24jvu|Gb zz?A6(fp%|22g;uMJ_HkUA%$JF4PayOPln>SNVrCq6A6U1!P4yn{FkX179O)6eH*fU zFo)qM%dpDxty3$Zf%E5MZe4F(X8Rv4+0pcBgJoF|5s)Whanm^2X&MS)i{Tl@A zO69BJO64Se?LDEw)F;}Q3sn)AUL#kCG3);NUEv*XjFuI&VldCX3z03bX4uC8o+EB@ zN0sShJ9oz&oxC3Fp(fUN!&od2NycpNHAft8$o zM`rF7zS4+k+@spPEO!|_vgju>8ptW%w_dkzS+m!{6>4=TSzc=y#Wc z7y(MiF2qBbpC&NhcCFFz4@MdEf7zHeuFAmZMgEh5Iwyz{>iF$6TIy3c6Fun&BGW&I zqN|Zm!-gv|5fliEpbqE5{{ro1pL~A`&qoL*U-->HUP8>XJwAPgsPmwGK=7+9{YnfU zcJ?hP^*Hhxp+VUcjFzir_n(Fy4P18ltBgEDzQjzG|_|BPw z55Gnor%g%tf$i!%3(n5c^b-XxW?QO_iD5O7`r&4NyJnw?hrvRNvr}{@lpn_U#bDdM zSFyGs5I&(1?c1&7`PVf;7i5#SgXmsEfNTj>6{oxP>`e8uNFrQsp+zU z#MQpR9qhv-Yq$BSS}`RJ(DccmO~M8@v2fkf2h71((n&BCm4^$i4C2))sdXd4^)*;m zaTHy3c4E7Y0OD>}x9X&XiyngVSh6Qp>zyB_^O|~vI5TyDD6abW!QdwTazq7r4JV}; z-JfHb`yAQQMY_=E1Sa1HWOdCpp|mJmi)$Zl>&}dr$*eg_*MBDAj_XE#Dn1PV@^)#gs+Y-A2Ak*en2O3pQnWo&TJ#s5r zv^E4-z{9OIrw~SvXNSJQW1TxF9NGUQxEPEewPvgbUOW2)Mf%qfFR>?Dd@p9yqrGL{ z(Lrxaghkc9g5a6oy!HXYd&~uK($s!b@tulst!J2`zDN9^s?oYZv>*%BW3?Y1+6&BR z5%p#s6`s>t6R7uKTm37oP;@NIcZsmZ<87ZI7~cMT9J(>9GL< zJhU6x_$@G`GmUVla;hU-H~7hMV`tMCR^|T@3!VXgg2%(pqA?k7lIy0LhJF z#N}aLv|nNad)+CwmeWlmK-2H-t>~E!`NE>bA!Qx>K3kPXowV_{k-m>q`s}MAQ$#iU zZ2%zyt#zYs6nD>X7%A*vJO#&hfm^tdo$)%dOP0SH`uDeLzyC>&cw&iv@QqmaRN`Z< z-}k*>)8DRc_(Y4rwjz6}519_lE>#~u-kjJUD-zYP$pd7rig(Jwmm<|CH+D2A;{&gl zg+M+U7jkTu>$^}BS{~TuKbeu9FzQja6_EaedTvS(cvKFvF9i>uMD#F3DjVb5I=Q!b zXMyad)gsyzq2cw{Xi1iRDc@(=-m+AVjBGF=_qmraMekXReN2!Va2(6{} zTkDDJ<4~iP)(8d){@QHo8{D>;R-8=yLWOTua>A!{-nU*P4ftHsjs&v?mjHKa&rkU# z2oWCHaX&*K%F_z{XU7lH?Rj5*FO_eapJgTw#M4T?UPJFvnZ3Zxo{ zExq{$gC&OIx1ugyD2F#+Zu2=#k1exraga=Lbt(wj+Ju}4|ptI1J{iuz$cP(S#9Y>|N-?YarK!WZYg zcC<$t5EzK9R?2;Q{T#$g54DC91kRh7V6V$<`VcmPXyC)WDP+QUOLQvdHJ*5l%Qn&d zNGPfc9>3wafIxa&1nakKSY`J>NKsod-zF+hOwN~C!mO6Wue-gLMW5E}XasLM8hyC* zO##c007SDiY{nkkq%H=*0>xfn8938A#<0HxQcD1~#Zpc6R zemOrih9mdVkk!0F2-vvFA!Dvsm=0J*VR@)Xq`Hd|H}v!L5fJBLO;BvCFjuI7gXzk- zMnqnY7W?^CAKv_3qcNq!Sgo=d-O+I%LjA}d00q2>v9qcS6QprQ5Trcp9e7HiAdkJY z-=xq9iICU^NucbjW2PPM9dk(DaIw%ZAVt*?p>j0oF)!m*#EvvP1F5uBYq9xr9~ z)PG}Zs{}xEB^G-+cBRQK0oC&rlDEG4Ii78muZ1S%Knw@geHpUtk9no+S31_YP8F`XZAuyGMf79&1N?#7gSJGa@7dix2fb()+7EjC4KJQB3|O^biZLXRgxLr zOtcYd#&>e#GcS}&vnla%%F2a#ICjE{X{O%^m5YzaxpZ4Ta|vSA<#YBhH#++2@wxGC z6@=&x-q=@E1rBqb3x17tuF>k+{i1{@ko&YdbLS41FGB0KXQEmM(`0t4w48I^l|uK9u0@p>h1Z&QhA3mKgZwYW+fDY z_+5g6&#eJ7!;bDkQm`O`(La{ zsIVAY>f!j~9bj*&q$aM&&yGFM1^O=1is{@St!#nxR$1{eZhQH20SHbqZf_!rpwg~k zFTk}r>7uhFuva9$5CrP|gqwgLD|67oeRB9%thSbg8u> zQT6g}m$>()CubGrf(X9L8a%8gRpitGk^}+l{W(?Rzgja0kU8rKfK%=^+}OCM#ILc!6tD`OSe(+`AG{|J>N5 z9fxEoRg!wJ3+|fR*=HU=ZJKRx&-DLSR~&*!kI7-=rZ>U--OMyj{=wTduG@U--E#_?!4y-Dunwt2NMmhi z=bhEH@)NCc_|n%MkepGdX@A!^{0ob;W6zDBpKIS4MXepV@SAkJ$FAy|hM&x<7Y5|Hubq z*=~D;K8ueBbx`=jNT}k3h*GB6FVg+$N#0sKO(P<=#BY=FT)Ket?Xsjvfgwy^mksGv zl)5g8senheT7Yz+o{5$dT$`(`)$k$Lxwb67M9#J63hWuZd;(QdL~d}dWGf|NfBTVxN>}LvBb#H2S-+G%*)YacvmqSiUdR&E}OTvfSG2N1d}OG5BQ6$**O5NpJi9tKkP zxg`cs%%m-y)jLe*2jxcUzKfLu^c1P00$<6vxK;y$;p7*+!uY%?vh1bS_M$xY1M>HWqR3XV9>oACShvl}3J<*kE)&aUR3NC_L9V_B>MRbgvn_ zR|GAQE}HCV5WLwhMi>OEL2i@RU~pw zmb0vBuKjcZBDEFBuegl_K|TZZ)C@X-Jjh`}G&M9%yz)K9>gjQ(+t-O^z2wst{!*N}3*{X9ov##@&YOxvrU1?w)8vhH7E?rl z7jnCbXlQmFsPHkZC36;Pj*7l z!Av;&jU`ocV-00UEuf@%;55US>Q4stM#8@kU+t-m{P}$fUIq^KQsuMnnO|;4*7Dxc z?5MFm_dJR~pTh=T{^Us*pN;!^OWJR@QHAb>_f; zE+x|rL-3_4%WiLyqxpuxKZS7>$PY1@BlJBhm?(8;z8?QV($9PEHAuzsjJ); zE$GBDex@uEFkZn8EKU?G1M529NiMW#)~!$OZl=y%S8~$IVX!d zj?HvG(0y_X2S&lEghUj>s~yq=SA&wSuKaa5h$v< z&sfX>c>4zbrP^h?2mn4$5z)4G?3w5C$M50k<>}IkSZco3cpvtX1 zO{Df5o!f(xa3-w;KF!TNJCh)OxoE;A4; zYk~YO*Dl4+Ti3y1E42f9*|E_hZR6>o62byG8V(+Dg*PSeF1g?SHP=0Z-`6`w44C$! zDtnZ&y8`)R0Cn1XkJVS`x>7S%*6!~dlM#FyE5ZXg zMJi-dhgLblFLS>ZP{WQa%^#$7)C8W9TE+oVDSV8+B5KIAMxtpVv;J{p#X&^9gL?g?Kjw6`p4kr!pE_fkJ>hZPi* zqv0L-eQ&(natohr|7sod91t^{RKYdw1B7H;rTYPz)9XFxOx81I&{t{`23;OB)jmVC z&O-JMedH?-n({Y!;j*m1$AvzwpN81_P{ixU#l^@;0rCl=_irIeScE}8Y z>GCQZt30nCHln*)X%C)ySbx7rfEKF0~(jlD`ylvBg09 zJJk$%$JT)Z=@Y7N&)d(n-x-r$cuvtZA_?Xckz62 zW*AOdfZ-6y!mR*DK)AmtWlz9TUwgH3OLFryLgQqY8j#Xxoe)K*)B6W__jd z;nVhdAiMAS7)topgYCpSz#qA@(7I>jM=-`6FA)9|ufo2Gp;G>t+Tql*9Yx}&R3aAY zn2vlHk45FYX4dx$n7eT6?WO6vW^(?32jy)1YL6!f znvBugcn5wXlAZ5=LS5i47(|-WKB+ozaxx4@k$vF)0{|V@-R;*F%cb)rFwCJ>HvY}T z=~H%Dv&DW;3Q>*$i{OiMMyY=kp+25FggEjvd^b^5|4f-8$l;MwfHXu*r^{b(odM8g ztbvnWG(37n77Sse5F%vg;B-%<86ew^MB_XTIW{UYdo)Ai{h1x%Lv4It+sFL#G>`F1 z29@~%y7C@35~MA*`-IfG%vnO+)E(!7NfED3({m1sixEDJYIWpykKL=F_nq6FNd#I# zDTZe1<9{Z`h`Lt`in*}X0E7<3X~*_&h$|U?^+sz?@cZ=M(w_uj%4FbBKIdau-^OFo z^Are*L+W$Xt@9Z+*cA09wbOY{lb)}K2E>^4{G~xm-?rFLLDx*vYnjU=?ZO`H3m*kG z@?s9oS!$Fw=dz&)XPfGUJ;`AO)ieKo;F|PfXqi+G7xP*)sCJEGSC{k>E_P`c;8uD) zZ^ir88hnsgs#SvXx~E`(T2%+=OJV7YUvajA*zsR^|5u+jw`8(pNtbK)bjm;im?N#-sJ;rG{&>$~m$X4)5$A52m)-l*$)g zpZ*mJY-x9V&zydsgOaoPp(!nHDo8zZMPnIrhM#!0Q_jK+dS66flt^I|Zv22^b*BY; z_YNmks(~FaYdSEeK^b<|$DzfkcyxUEt!anG6&U#EuO)`_p67#~QQ>z`CU-TofC&CgApik6ulALl zHK$>sxH@O0oqUGnCr$1@0+Oa*?gSK+ciHAWLYne_Qo zwgu#kKXH%AOFe9Yi%CS&IOx>sruxG*jolJo@)>3UhYPUaD^zAF`K?XaBl?Mr9&(#r z0!xcWB!gU*XWvSZosZd7=^;A{B1B*DFXuu(o71sSq7aONU#N^99^zRrmnwy9UFJsv ze|S?3eDk{J$o6(psKpf+KdX17ni-Ry&5Og>3vA~7p2Zwa??>oh3eh05ZQ}IWqGj#B z`}r+GNA=RE-y6j>QMb$NAyXjX^N0rorOl?NG}u#Qqa}esSlpk$uAX>2ad+n@O+Ih` zvAx~ll*25Pz|{I|4MIIHUB#YVvOK&oT$ld7HR#TOD>DnQ!PSt--~1{sp=d4!1{j)D zTEItrV{L6S8=4@{8R0dR z(qjLzV2$8Ae4R*YHC#zXDX+V<2%!Nu|0G7z?^ShjXfTJrENME(=I4{_t4gzxC>jAd zxlmxWsz1sRW}u5xI-eXL`J6g*>~{e(1Ee3Me;-ugwAeVhH7yvt7yey@PN6AnjDStjc*RWj9W!d|_1uI2b%p^zN3i z-d-oGm|a={|I;TCMuJtYy-TSk(50S{o>xM!6Z@3()cq&gUDJB8YCNe)yBplN`hp$b zxYPj(G`V@T%3hJA)$7~WVg zDOqa#X8$Qh#%}U8SS~>ndI`uWVuT0^00m>b+|f}|vGxC{ZE4twfoSR}AkX&M8C>5B z`x${-ZZl12c+g)Rkke)1;SHId+0$Om;2U6nxc4Vf*<+=fD%KH~NMNdzbc4A{f?*A6 zrJ1xqgOTV)l!t@+zhw0Ol;YKbl&bisnxPA;k}!a7`+~5qrmw|E1yR<0xhCIQbA}~x zE{GV{HB+*m26PYTF#k9Sg_dua9QPDV_Spg$9ZqX&KmqJ=A)Ve_1pti0EesEkrCA*J$?hmNWM4=I-*NO#B!`sVdUMGIrKxojf)jZmRzS z4>&VLU}3QYh)Pm77Fh5(APZ0qD}Tl@^G^-CO^sL&KqJKR;Tup``o^9UApT8EBMXM; z;%tLNZiuw(yBrOaRGuh_wgco(BByfppV~Re`UyeePS3!l21&GS-8Ne1{o-8Hvw}yG zBDj_9xJlgoi_i|NS*WO7?Q%@KqXGX6&h1gUr?KOtD@X?inv8-peSI z74w_6xkq-`v3tp12CQwI`Q^3Syo6bQe+v8P$|?4f)i(GXsd~!KR4=W5)N~R+#Bth& z|CNr7NdKNgput=MS3Rudyq0IpzWbNX>rrZGKrS|;2xETR4i!& zGe)+?2viIft^PiWp8?g?@QeiOjLiP+V=|3;fR7H9f3T!eGF(7Pj}HG5=@~r#&N@th zlA#MXJuEp7XSxcl)DW-A$-tJAK|uPjJ3Mq8okY}v+bt-^nh4x+1!WdofsIQn!vH)>1j zT?B!%^Kz_M-MW9(c`A$xaBYctEJu>8HCH6V29Ah{GsSDb!qTJ+mtX8V2c4%7ag!=} zSqxzmx7D_i8Jj4pVocU*NiUf*K*8mvtR5pS&DYgIS?k|_%%3kel`UOVfF+O3#HJ(G z{Dp12+pP+tNOl92@}cp^>pHJfWznm3C(geXdd0L&yu|{ckGShF#aL?i=U9z21-cBV z>j+clw>*FfNl2-J1A0@*MW~EHooc>-0D~Z|#g?w^ttFo^%-P%;fuUZ)lqUauNOp)A z+DsL!Sspxykwf2RA#@J!fyH__e13XQJK=_>VM}A!fF(yl-CE5{JpV=xRwi!h&g6Z? zx!06;u5%wOc=zRf_M;Ou-p!O(eO%3%FF*+ep3s__Y;SsY!maQH^MbGr+k z0f6j_<7T*oconcxOy(BTL%{h1lLKOEGeQyO&1}9FpP{?*vNg^;20ne5va?m6O`OC} z_-fWDndczB1mS$;+&v3J1cSxawf%;Pv6HK9yb(ENjC)QvB1^SsaVw;a=L;NFaQ#nd z56w(i=n9}D+RaV0Os)P4gA+wKIT{jGOFY}HB-H@9Nxwi*Z_|z$X-C}UCxcP=la2$MO)JLVcgv`)46O>r0#c}SENiY;rtly0Pz&EA+4g~3mbjaA&3USM zypdiR=(VB3|Emm4CWt+7)_VanNwgz;WAw{=8GR>H((CE923CSP*wvX8Xmell;6}GC z5K`1swe-Dsv4oV>k;2JM<2NNJcB=6@A>yBks%ePRqif)DG}=&P4)W)kMTF3&g1nB4 zsN8}C=)b>zP423IeMTl(m=gwrnU&9-nw6AtEB?;(vJmlI>-tII{54C|+890xMvD&1 z4l!6UJ_$GP0Xfzl86KJo@c4PJtn#x{MvS(PLD75?oT&0jUfHNXCDKg*(q>GDHf@hu zb)XU`_AE`v1&>BR|C+|N%_sxndT4vwIm5Tmz25VMV|Ws$#df}jj&(o( zGxn`IlO9AqX+=$O6N5$o!-nXOV9@aF;c9EkdMW}|-aw~JCz_@y|TC0>H zOqiLx)6rcV1nQaoQ#-BoVU*yTso`WCyo59hpkkULTb>XvasbGftdKh&n#~aOh$LCD zxS1Z^AzjF!=GL+(EKT?%!IK294ovy!62nm<8jhgHGiZ8d^QfX|eaUQ8d*fN3Hl%+}1I&1z1PK`KupQHz5%;8^f3nLU zF6$e@nI9cQZ z3}#4I_pu$W;S=!WVa_X%eB)|P(*4SNHolpfQ%U4sXAL_(Ur+}M z-g7hD=|KS+dWgP?8{zDJ`2AX1;fs|hIuv&zz3?#T;#v!R&#W~rVK3Z_t?C)CkqNjy z(n8HePpBcreS}HpLahLY%k}puYdxhA9&bj2>G4vCuX<&|+~w5esGo!QOTmxhDz-EW z6@TnLOZ`1jw0lUVVQ=S*b@8<51W>BQu9&pq!6G<4wDbfr#GNi5Eof z)SgmDTimpRz#{g=53Wjy7)V4C;-wJy3GER?rO$ZGyvh|%xEniM3_7jDs?!l+ zmA|kOLD7edu=x=%*h=-fW4Ibz+IGOvI^3F{MXlxof4-$Rvx=N~z@SVxaDReSQdIqx z)^g>b74f|xAPW14$$~c7&OSj#j?qiRX03|$!xQ>$0=%WgKcMc&R{25}g;-38kE#LUk zM`8x7H^Sfy+)az2#JN)`X;8v4Px&5^XVknZ3f>4?t`+@6HgIG}1hKoKyWu6Gws2?+ zup%l;e*xzS;s2^46e#_J&e{ksrBf=MG@qd&t)wc|r?oSRwZiG^8ywN_A_9>li` z4>6cT;aU)YHu!8O9u$8mLcOTqbVPTJb7A+S4?+-aBgoC2a&mfQ$>=<84{eWjQnNHY z=QD`VjLb)V!KmWc(EqZC7NrkMbb==*J1@W|SAy9&+2l$8+C_6p-*v?C7NMkT!uY<(5^+qpplQIZq=P_eB` z&PnZVF+Pi}YaOS5ih`Lr>Y5F73Wyt2FGmAZk}SH8``px1(Af5CPr3v*1U(jxNoDEsLE8D6KH?>gEz_ncpcDUhiQS+lx* z$n0&TK-r!JhTmA*rxOeW4iwchPtc28G)5N!>=zo(vakR@g%KYs8i`AG`-pM9V2kSIzT?x6rQ0-kav&qs08YlP`30B5(vwTx_YLy<-R9zOo- z$04>U%#9u*vJ-|5=~KEC)*>(twG+e7JCIYlux2+=R8Jz6q_24!4A}Wj*#TY1NU`5! zIO|KTYt0ERau5kkpJ)Wz*V+M{0V6wNA6x9_<#-#k>}(rN+2yDRy)zd{HQ8io)rKts zW+kw>ONprlwuKebZspuK1&@xi zYlcK;2Y0H5A^)Q)4cpq%)iDuZ=f)0fa`}N5Czmw@D1aAFjn;i`*Vk%g`CDtnE^b*3 zIy@(ArNvs&24DqYK#ma}Up;_MaEZRk{XEnw($u1;0_0N^bpfd!Mq*UQ9}NkmNdYFA zO4uFKv$G|w%$ThkcXs{PrHY@pObU zZ&vdxe}^d(DMHf%LQEAQE^gE54KDO(dclSsLDz68*7%VM$oq5fFDgVZEHF$vY=`(N zZ0^GaM(#N1JKDo0vL^-( zv)jCt;ytzQj4Aek?N1wGzk_DgbF5Zj2_2p272-U0VF+V9lw;P<^lRQ$u+oz~#%H3j zEk%G_jtp}@f?dmHX#j-gPbFL0iibfx`2wIC_QBLDo_~`d;d8bDN%xFu*1>)@>0~He zX;}4K&~Mt6V;CA?7@(M}U&Xh%5ZtmJk|DD!)-*|WeqB88g2m?TfE!U{d7dZ-sl~g? zmiJWp=(>x2Qhy|L)^zi0?Sg<>vUb;Jn+-5fJk2efCr*s;ds$j30NcBL{P0G(B&*=WF#*Q6A%Tpzn?FM7ia z1PXO494i4O_s$b{xA4861T@egUIy**HgOA=B)J(Y8jncY{yYR`53C8OK@yL1-D6L0 zmKWV-=a_E?NZC#ualC!9M-|3&_rjTavAhLl@KW-2!&bNG8FhETlVK?$G9ZbJg_bFC zn4}5}xBU2yGZVZ=NvAItC%q%m=&fv%CdbLWG2^)kyt9CIW0C{<@;3f~3SJAXb+}Ui zVh50WY~wizXkw3wEiPlr(;6VtX%_d25*J23X&vKD6-5BbS-+xm`?B5@tn8(G?_lT? zS{}oz<+K)slMnd2L=~I%4S%H63ogFK(6R(Fp~fk2Sr5|&PLG9-!H64G_-+-Uj~RI8 z-XGFUHM~va)y;U6Px3rsP)durd5vv_PW~%m9w!ct3 z47n#ER-LN?aN>k(MXnKr0)RAr$K{C&H%>e_R5E{q4WnG+$^M4TIYQDbLEb1@_v~??T)K_fI$p zl)6;B30wu^I;6n-70BueN3@eA2xstB2GApZp=1Az6KN!L4qzb^7E5E-eCoJU%H~$F zJr6CcUIO+y)1^c0+7=TGS;%Si;md{GYx+N;vmrE#9EU1!A^U$<7|6~-mv&RrVU@k6 z-BPBe5;I??U(Uo-G{%hgdiUB2uk?zyrMr&`+xb)TIkD+r(JkFAPyt!oY;(CsgNFm& z51^%b3(TY@v&sgdyg1rUmzcsEe7sYNn$Z`Q)23~+8nYVVlc@J(V{6Ci9;(CMnAG*! zr7nDzVaeaP4)fI~$3u1y?rN6|PDVK_dnxcpzyhR4YLR=mQ^o*ptFXoqn0nqbr8`OR z)GD^le4y}Gpc^HnPWvV+7Wigm3@=3Y?CnSeSBHjKQeSk~PlufzmHJl+LD&%~T6yxl{`&)vmD3}W0rV=Ayc`MQtxI2L z9Ay3e(Sm+E<^s5q%E59H!_Wk*I0GEhzwDL=U*Gue&6eJ!>2fut`7P$}ZkMogR^DG<1$ z>zBu;r9Dy`S4#fqfw$Sbjr2vcEt#%9jm3k{OZUBSg1U{XRWs*uPZvlu0cG zx7{hpzBz!RP@W(_MCM=}z%dF`Ab$`aaidsfFz;QOEAQn?c)#VCie%(0IV1(4g}t$d z^(W}VX6!E-zN9f$-USEpoe>`tLChW|eGikfR2;zt%hG0Mn9_7_jw^{|KXmt~Z*v*I zHE`zvt^YM0{-YO^KM6vq#9O(^)FtMSRMy6z(wcK(%+9s8R|9cLL6t$|=Y`wq=SBwG z?C6|d1xQ@!+D5=~Q}WC{`ijBhye1KSE(AU=Y*gc`E@jZTiQnYkBR7S6B?dB>tM03+ zl+!clicV-r7ek%4TDs00&{f(GT+Qn*c4#q=jG6})qN-4V)3KKtjbYfZhat71SID-T zZc3t7_pq+t`%6NZrmxVT;BBCXm2|xhOEzz%I1sgnZLaS8Lt3A^{o(MYz#wr-hat-I zQJ=Oogm)#`^Mim&sj{Xs5U>5uM?eYTYh8t*f*v<{7)K#-Ro$S2u)R_+?{L4F~2{BG1us}cm5{CG3p;41otY%j1+=S)fFl}(X4W$ z@$HI>W;1~Y(+@E0E@p=}M>0_kS3HcCYxa8;BiN3$kPeY8JFrSRlMbq-aU=}PPrOiq zVT6aHx{l2?Tbrp%rPZ(TjfX9(u57xv(e`_NLTW)fU%t=OC|_-1W&3|RNvc}D<)DP# zaws{VJ<3UhxydxIU#H@r*e#edOrC)y5kS00HhsW5HX=kp`Zm4^r0?;5MKGV(6iZJ0 z78u+zF@pokygN&l=#ZKMYwsk~W6eJ&Rkoyv8{nivfDrqe8!q(sk4#5_gto>)FCE|D zpwIu7Ob~r1zmTQ)b;BxUVw8RB#9rr#I#%#0Y?sf(kT*{xd7pCz$rE$$Z4TQ-Q4sj%zvy^Rb&r!qWdp#pE;Yj@JWi{ueu5q^Z0|66EQD4X`tmIq^jzr@eH z($L`k@8``sCm9?BFmC#!c_fprB(o+TbnNPuWF8u|(YU2Rz~pDDX!dLqX=9s@T#OZr z)uJ7K)2ZCXk!6eBI{&-!zRu>G@2bn>O)i0~0(w)@8!P)phuMfAPHIuLB3NtX!o_ND zPvgi7pFba+zHFutvg?5NCAQ~VW(uO#pQ3D}RlU;DiHvN#2-l7rq9=ZAZA3m{h1(k#E;ozlCL8w8=uwdlZOF;>71OiqPDh!*c-b zQ>n7LB?JIrbsuQ31cW{fOZ|sFHfwO<^*w}w7asIT^?UkcwN>Gl;mj@F3rh!|Am`ai_dbxbSes7tc z<}W!VAt#RXrsZW-D%&BjyfBISoI8X)cloyH_Q?ck!gws?Cw~Tb*`;bUGcuy3=jqf- z0Pj5{F!P`6U=jhPz-2HpF?a8FJCiKnDRCR|Cf5$afFxZ{d}_?jn9X}k)Sn6h_G`@T zB+&lD8vHocVf0Ih9_^}Dy1?X}nAVdX#N5qIJD!}(lKu8%W5;ik2b%CjCO|{c*}IO8 z<5W=#r*SiTB{ z*y2`FVM5sXcxq!As~Cn4L$D*q1*vANee-|%CbS*$r-24Lp#aFLr+QU!euH2H>##<8=eG;&VT6X+Q-;<+_TY7 z{I&laKDaK1I?;DIJuCvW4VkM(&l?OYRv{M9O(>Lfox{D43>?FgmnsAb6$JjtPER|< zU24MVC@C`-3K+pS(6usQCfNF-v@!}94+D1BzKK>;O*t$qBkZwc9ITBcD8szCr|zMtSQky=hGrnylp%fnTNI^cri@0SJ_CS!}qh`Ss_%}Ja@ z!d%e=wy;&`4I^Id6WOva(!|UhvX?1B<%-u-7~u zEFw{Y>Yapaaz&kSDWTo$@4i2O#EINt0e+la6)@*>tk|V}MVz3m{OPkkoL_GLUiRhW zZB6a?k8L%kwqKa87mJ$-)OPH&cubc<>ohv)x4@ZKNIx2^+;ckmZd%)&QY;&`5WjeB z+1r`xKfRAV9jI*J_z!V}s;Q`lMoK4PxH{R*7l)X5)vi~DFiVxQ4J67`o zE8wF`GJyPez>%-^hTwHxy^&9sc&c@T8=@2)_D#p86t7VOTByA|n1~J`%=)+PQjQ|^ zxX-1Dgx&3N@F^$lo6xs<2GlDx@mKn~kZ#I65zC}$&kBCxE&SjvJ9xv~Z8)pw?g9`_ z)6594YB1j&*)!{|6hC~CgvH90C|ril$MCY&UJs`d#|7FL#yVah6q$U3R||tOe0@*% z-H8z7X9y0Z`ASz4*HZqczY)Zf5GK5=>Heq#QVN@xVlUjAx+$KRq%|pVQp-Y3B;%(6 zICeLidr8S?&n49T%Cjcau#uxYTwOHi{r(2S1y!&ZobiES#o=-pgkljJG9J8+jp9Ob z9{q;q9HvZm|K~SO6CTH~%J%aiDw78wKl7xx$%X`*4|c{heyX|S>Xv`qy{i*WK}i>V zb7y1d>T*{>Vp6*kx6%X(PUATX1H|3n_vg~7E&fVdPp+_%$FmDQhpPvNL3j?cNlUw; zH6x=I0>prqV1M1`Y06>Rd#|yRpE!||6MaaS;}w1e4RjR}L7aGvAiH@rdEP|t!#9g6=FbS_rmv`1U*+`7Lbhl1!r z&X21SG;A=G2lZP=d)>!>Q+qR#iqT_<@WwXtp{)z@m5Dd#?ecDvcxk)0emO@E2=hPaim{a+A?NNp9NKbLQ{zT!nmK0x|l1<$<@LvFF+XHouNyieH@MxcM=@*^E*|XxxeHF^1p=ez4mr zAXtJp=xY)k$2wBkR(hKR31YIFZ}X0>SX)U)+ir_}AmPTOJoogFVVXuGJQF+kqW13= zvp;R2KOytcf?A##z~-hSb?l)Lakvgoh{~cyZ@wq47^#;5r*|73nr0pq%cjT}S~Gh) zK)nOd@Fvt_7M*I;%A8;2$Q>v9$2>D#bBm=0(oV}cg!npj7j&dh*QFwJyPbNJEMk(_ zOvKZaIh&#(+3?sEtnM4H_2Y>y@w7W#xCc2EMyR9qBT@(iLsgYWgFBu1K;* z(1wDi+u521>~a_y<|x>u@T4Vw*S|-NOHx+jS0$1D;)nL-6FT9mrIQ&jybH)1>r@tU z`2|YMI?u2+YO5^nEILM0M%0hl(RvK}d$7w?)$NXoRNZ!52bjaL)JvjAM_w_OaGm(A z@VGW~EFjOW+LL4cyo=i>CIqm65|e(QN0)=iL3HRC;oGON2Ns!QWz=_g1{u)MGSv7a|r# zqUlo6OgWvas%f@#rHW-g6F{j=7w3tEIz6DWM;9!Ab{=i`@E~e%vqq+4f&6cCaN6cy zqqG$@tSWXW_gnlj6S@*}8#;?3&7QWYlo5iOj|d~+E}p!6Ih+t(Ocaj^>3&(C;TWQX zx*r`P1e?G(SwN<6CsJIi1BkCga~7Vi-*OAuU;dCZ#j6Rx(fi=9vsdGM+W0WWkzc)A zJh@Y!xpqOc#}hJLN*C{5n%UtLj%oi!1BNjT7}Zjmz-}l|$%&QYlsYHSLtgt!fke=K za+`HNMWUjL=ObO=B)#06XpL`r(LZfbVk5~&U@G!VZYVG^{C$DOp0c4xHPNWjo&Mb6M z%f@%!CePb=jXvZ<^ZpA!m&Ri{&e+X)am6f``s;W($O+3X1XpLe5&pn{^R^^m=Deei=H$*3aexbi1?yq5hvqGdHyub;*X(JPxC_dv{sM{&z4)W2xy zD+)bGdKU`!L0KQ2LsajDPOqzzIBxaw#z7Gz`fpE zz$eVyG1gWPgsvPbNxGwD%DEf8L08*0HOj0AVD3-!rb{F8i88?X$YQciLgv`goHZi( z=gh4L_wv09cx{KjC!L*%_PC~M?5VDU;ad;Tlu<{h$coV9bZstse!nWMAk;z|3jc$q zcD@#$x(J-^QKT@5Jd#r`WMh0hZ=h+Lwf(`)p;3^tuSV}1B+ z;nHR>bTsY@@7s?)Q}QLO#*1-=vgU$x0cE8a;Ld0NFh<1#ABg_F$86fA=wZ<$$>M3@ zzJ98?%OJ2lB1mP?o6SD*%8_hjX8TM7Wl;Gg$Ys56>|G0r)F{I83-TMVYYim#OS7X~ zjCHaoflhr48DY=dmR$<4NBRjsT4l`__Y%sF5AD;|ck!%WcPBg_P?s)cc{_2Po}hCB zkwMHri>*q4hlzEuOb>9@KhcA>0pYx;!6mwIklA;N8D zW(+zeDuY8Y2P!*}d6W-pJB3mlzGh}R_jG z;-k)jL&ry&(`rTi70e*M&ZzJ#9J<%8zTY?W7{Hs(E0M9ojurMwMiQEBNOlI#)Ri%p zvs_xV1^&lr076EO>>4QaZFyZt;rM20Z$n1GH=Q@Wk#E}np#BYYiW_Fo z=v(<9$jkoM^X%v6nGrB@jb_(;b;Xrs&0`#nyuc8kxuK0iH6{)Q4I}j$fOeJ*ZP>Yc z$b1;A=J1f3TE+YZ zs+K;?JF+nNZD>PIbh&6D5h28FeI}2`mw7h&UPFBiqK%w|QDht}m?I-jXl+3LYY{2I zSkpXlnzB9II36^*cY%P%j=auP{OX#98cQ3+2YS?b!eC$tL4;OK9`*2>6$|Qq@k6HW zkl%auXDSG^(+z@zH|8+EglwUmNKfshpNrRyB8lD~?5vqyH432Gkte1LzX(2IjPc*S zT&YSp>UIl}<#)XhvFSdwj1np^Ai8_kUtyFoKT)@w^37SX`Y(mc|-PqG0CG%_A$mxn3ey*z4_k z@+g$^s}@vRp=9Ldz*|`eRt=sedAN%1Fx>zAS zFHkVm^f_d^Yw(PEg%n6x0N6>|OrIcv*jkJv#poF)|^TfeJqiEu_#(z2d)kZcNqwF4^@op>1R? z%P-#GdEi|iqtfEqc!Ch2qNqkPgaM$&E=%mR}7)WgFSG|UkZj|SDPV(}W@sebj7W_(lJeZS<{vE8- za|Ktpx5P`GfN#SvoV-!hKx7&veVIt)6|=QZ8g0KO@2BC{b7IolhLp!}^Tb@8Ii0hT z;U~I{L_-m2gc{GWnps{VjR`0@TXv`Mc_Ob2966zlPiro}kkOPh=DTR(Iguf(3pixk+6$@*fds zf(6p5uBq~0r-LBf4^0Pmb$1LxtSSHFrOX&;yCwg}9o41D>&XveIN){k-Rs;_8%a+XiEmLupq zX3HHj!&h02i)c);!k3JYDx8({e|v*xFEXzN z_t>SjJS<%js_NT?RfFjqgqX_304$*qg2?hsSEq&L4XXpldA?Ixi-!ToM-M4j!5{4` zu3C&m_-Oslu1Z2tM>A5FvHHBdIn8yeRtVngAl?vjpv9ca^?qQPZtMChd5c@P3B z38d|<)>7Sr0P^m?U=EuO18)CoJR&(TcS`l(dbSWRcG%vEE|AQR)u%*U&jh0hGw4@S1mhLx+|7DmX;Ff5s zkZ5%8nox+1|DFxfWr9}+=v1&<;?K{3d#<1%mBzVzn;udu}h}pbxQ9`%BJckIY$X_3r1s%Cz9;D_^zjobP5Xzm$lT-nETS16(#}%c&UX2(e0o|t0ru9uSUf`IuK>m zUCk+ZUFtPNgoLe|X(Xuh(plpNZ!pEii#W2Kti% zuc{R3WD8YR=3MY|9MjHQMFcb8m*)h ztH>TFE-Uik8vCX+4ozZqjORaLO<<9FtE zXV)u2NE(~W-2QA}Jyw??@$0%H3|1UNOEZ3#Uyl>5J(H=Z=4G%_`*$#o3I;CFPy|Fc zr9SYsc%GwAHHVS|Qiy`7b^-DeH4sZqyY6_7e3eYZpAhPV3(l0cIue+DnhYwtQ|T(RdW&Sd!t#K>s^egvAFe=7UDSi~H!K-0?Cb&)6;gF$42> z_8T@YmZ5Gq`j25+VYzje=Jq(UO6vpFu+m3=g4CYyd#&mh*iyXDze><_P(rXIuG(O5xwh8QKi z?rJs;9!)8|rL~c;@(!Tabj5^^y5&rr>ovUaM0B)BInpntJ&@89pb@L-EC1CTlR-UY zx64#`CUKiq{VRugGN%kzBTQM6Zw;a?9 z)oVi3$Xn7Gkil^#0b_wNDS^Xa8wIx59(niS2KqFrtl>R}u3%TS*CK6-M(%^dv?;|W z+X-H)PIBsjA;N-XmTE)nQb`4Qqp(3VyXr1-h)sSm8d4&kbC&5J49B)EnH>%ti9ta1 z_#1LqyloQ_taj)|l~4G(`7WghS;->{YwhxCl5@C53`GTmju4e|wJWV71ZVeH1iY-1 zZI|aPvKly2XZ%orlX$0Na%-5fqR-EiIG^l@QIDCD@VNsjv6iCHz^u`;s`EwzJMfT zIE0tHJ|Ten5@{ZmDlHgYdl&l=n&@EEM4hvA4j!^N3k~L$aVY+2drZcmvY&{F6+y`Q^nTN*_ zuPCb>93|w{DkK)5#na9^Zd$i*(1O6(@TgH8mYEYcqlL>f(Qi%UDb3#(Kw2+ns8PS|}0a|Da z-1Ky0i0mE^f9hbBXQO{U%YnqS6%xAa76)5iIL^$0yIB|STK%Ug7#@68)ivM2zk}h# z9L>R2h$Kl_Q+##TgllyOM!rR;Zn8Hg2;+s&)uooPO61a#Hp^08HA2AvmcFAG2id9g zAD##W)!()-xOn8S9~p*K2V(DUmZhdoE<_JWcs9$i4Kx+XYM5akDPXIaCs9)e(^JIg zMnf{=CPCZTRhYWNiM6J^)X?TE-)P>pWm4bgiKd}k~H>RzJ9-jGG~VYsYo1| zHZplMH`S?0b-sS4_@9v@SgY|HX!D~FVmwwxA4>FDs{zXka|5@v4(ko&>E5LKw&V(a z3n?+Zt-K`d1UbaH%(WEjr;>rKCq30%KToH<<={O$XP;wXX9NI4K)k;%l(1jXOX?D% zDWDy0(R?%zgzNO~SM;ePB`B0P#lsLJmZjVAa6-^W_{wdxk(>d4El8l0_n!-3I*F-1 zwXb_}Kkd^O^B6oLqZ`1_F$2Vh?(zfFc@hr{1C423j?kt}9(ixK#c;Xo!y_dJ7y}OS zV`#5*<4PGIeHe@;?$tuEXIQCOGXKdv`6Z2@$>+ci+?h~Hf1%Ez!m9M741i_Et?A>9 z&MXgS`_cV!O~Tp={>DxTOvMqr`UGMI11KvuxS>?t`Q;-UB88I$&VY?XlveihpaV;se{plGB9<}|E$zR7~ha{seH ziqAu`IOjvt3GP&LjrQu7E)EwC%yZsT9S8F``6Gw4#eW#aw(QQ-Pg#pB8G%|^MBb8c zM-BPmw+|=|=2ZwWL^e&>Pd2YcZZi0lqE;R^dCdB^pHWl{Qze2s`bO7&_lL7hLu8UE zQX9F3$U3*r79a4EJZ3s^3Z)6{2%AU6#rYY#7gK;SkMLTY5#;#fMb9>iKW4*Nv`yrmH_|xu#fY2~{rUw0;X=%}*b*Ne$22w_ch>hAwqMKPQS$9oQ%DNDoI8VEHQ+ z^K!mvAAQdgk#5YTGj)B(jfKogN@(Il`cOFEdi6sv`$9i^_TQO8`6Y<()}W@_b+WP| zUwQW=)@0Sd$5(G>8G&k`PqR(8j9b0XR0%ss59Qnc9b`67 zcny7ezmGc~fih?-f7>#bv-s;-4UwoB32_t}y8MIGr+uQ}66QlK!?3DK2qo-tOl z`M6ik3ajB6tlFSppJVh2{X1&OK}g`ZqLE8&Y=YU9nw}QWHlpu?ryCe@2gTrEPh^Jx zMqq+&=`x^w7yJ=M5Q^uK0a^8?kiyc9tj2{iWIErW-bIXWqnv8<&ZP#Ub{4GU0-Di{ z%ZL!bTK7PQ4Cyo=B&*96S%{Z>_f@lfV@jf9H^e&4m`c)-;WL3;%fsUD_gINgqdo9~ zC{vYX7{z8oj7w#Rr=p7R9y|>=v28@4#>$*Ap% z$03xRvDCGWvU$#*vZLLan#_fNm z&VCDuDvjxXet-AQr4No&XJ!qtGq9ZVKcFJN?Jg!?achK?CJcdiD&VJfH6U_8ukWS% zrge#4xZy=)+D^_Q^c7z(;3d)Ntgdm*Z?wc;QG8-&%9hVx`vnUPz}rkfnqUMj!%0K; zN@uI6R7S&TP(eNH=H-N(M2mp;1^O|9S*q()6aiC;2>HXL#ve=ow)0)wM6p9*p@y%1{wm98gamPDA6o7%)Z6d@zT$bZ9M3a---4OL3zl#Uh z?TNs-3ApF~)|K9&oKNoFuvpup=PTpqg_b+MDPE@kuv`le$Cju&(ZIuLlL%K5{YR|} zCZ1y|I}6?_lJOW5PX4mHo3z1&9X|b>(-k95?`z-GegRz*;kkg&M@D_R%dXh$t17{Ee$BN5iICG;Fv~gd&(s9 z+;XEXxwW{@yu}?h-B7QVbP#s{WEHCas@c)@n%$62r)I(h;%5{Op%TwCjd-j#NCAQ|xq^u+$VC}_fcJ3;6 zQZ+Hr=n81YT}mUASE6wDFc?G`xrJco8FS1(C@Y+ zR0};9I?L5mk1-!9tgKU*w%T)z{M2J+G)Ey^BV>j`wubI)gED~LbNkXEV=6>Oen6#J z)OHcFtk{vTu~_Db-_49cAjxR=uZ?h}mP?OV$MX)lEPRR!kPP#v5wK1lmGqbr+rk~H zqSJ^}S|92VBcOp(>(?mdWN^JZT;B2{Nq2V}#WW|q+Rwd)X{?MM=oVn?a*ze+?j`zN zaS4NOl4t=MLQ=&FA~-JT#Asgzcl&lZ2{q25fYQ|UvYV>X45dbR@8vautSI?{LduJm zt&U}Fk!A5%A)lmUlJtITRe8}h9=dB$5k*yIJ-2A_=4~~|!fQr08(LO37VO7 zE1*prSXOrh_i8MssxJ2dCh4aWEWK(A(kpB#%FgOf_;6FigUI%OtJiyCHRMMO0Hg=8 z`3e}I3Z>ecOa-C5GSSTBRbY!CnyhPA`k(_hJgovNA_GA+K;y!-8d2-8(4 z``$`Qv(&tgB*wCxiZRE|!)aHGI%ur_8yZx$w@CFPSrJos8)j&TvfjsJYe@~E6yk_K z5fTaBbY2L7)DouUyM~o*88Lh|>oSV`0cC*OvdrXr-qE`85Q|9qYdzxR{>4WYKS9bO zZuk#D&2Xf_%EZxWi`aezLYXJM31T3)_Ts4H^Cj_4KZz4L7oY8*0ci|(2n%z^J&+&& zZ$Z3#ra2ChSc3Jm+Zm3YL^|X;b_!7`WL;bp%MN^)6&^X0?I9Fit4&v3)&f8x4y|+7MLxTmEN!dRo%Q59DLMJ| z-pCh?WeLDbEgB_Hx@eiis=F7!g4p!;rZU#Am~6wK^KaJCoa3-bu>q9axFAmIh8wDq z%j}Zf;x&>AOsgVY){i*Q`NQwyw-Yci)_O}rx?x()PBrT+BdO(7ihegSLdwjYER}D* zpOO;qy|LdpQ;8fbZ=D199II%16?sl>W5jpAjk5L^1c)x7scDB-e}k*B z8rN%osVnMhTLKR^CmcKnqN)3I(=zF-w6V{H`%rOSi2>a~CWVi#Adkto?4>lrMpV1O zyElfQiZYcHa_GKWBx$IV70#iNjd-{gB(RA5&tRW~#>y7PvX%~%An8*EaM^q!b)~4U4+sRuXHN9k}YF(6z)jsj; z{it$!<~EzYr=t-KLB45oqed;Cc*dllKW4k z=f(OM3~hO|mVB{pk7t1SBrGk^>N}l4_9D?fklLWyFh*<7bP6l)(3+qzKrNFt8d6fJ z!XzA<#IP1}Wca`KW4uLqr)+<0rC0*&YMW?tVB0u_FpvlQThRZMg)tadwZl(Se^FN} z5KF2D)Rp3mqqF!JAz}hgN`|xq)M(T<(8WiS4fIc7xWv?5ZLl8O`=5tynBaZX~yzO5a>H(d-gBDbv9K^H$X_JZ8QWbkj`&JXt-t*N(-0SK#v)|@B1 z0Em_D{iv2kH(xrIY;32rJb*~FFOfU-fSf?i}c8bM{~T3(@L38 z6B0;-s3%*6cNGJDo;7$)>!=C@sL4UpGeIdL9X}uXTpf?}H#`qJR?9W$ZVBSedaUn{ zY}%5J2-5IMf-{*HoY;`c0rLRwslt-J$5Z^`IRL3_676U7l|h1qB|QAs#L!`;les2Q zYF-&~yhGm&v0DV+Yez6BeFUe-dAW?;rc=7vrfqD$)tOs;O+~3N&0$MQF#W8?k*uDry(& z=kTHRe(dAqxDtg5N2lq+uKcEu7v-oHVh<>9d=a<*V(dsCkxOM9#=Y6Hd8}H6IK5Rj zE$R=D8l-3h4B!P%M`+u#Ed8`iCaht-jD~~K#e!pu+nFTN-nx81whCp^@@ocz8cp)M zwrqScoG#dfj$6$KFsgX{1wzY?Bf~YK`8M*xEi@n}0U|XfSkTX>8xhsq7O`BgT?+>B zN!SzDiR&QZmAevSeU6i8e`QbuEICt9j>i?tlB~I0fSzFoGa@;_ps#`N*BS&Ev<+_W z79JIKIa571c;}i1$KE*Cl6NTP^I=QyH^u=~ti~vD$$9ijv5-(&~ZXgn^^tnwFVcB(#J@G5ho+US5_E<06raFC3#t42s0+!eqeq>4C zEXnofa5o-kwR?Zz)}DUgVi0di6%h%R(753(lCVS!w?~UfKz6raPe}K4q?xFL2jRI( zz>lT~eX;UKW>OQe>~MX{Hce{Ml!6&#_!_3sd(%pKqC|g4!pRiR1?K2;wXNT^RxvKK z2n=HD5>>Uz%tB=55}rQ#q0tv{v1b!I$!u*bSFQTHx*Kr|s%tPn|M~pU3a|eB&lE5H z1{S4dLk|-9-rQ|eZtFyXibtB5mjRlss3?_ad*1R{1$f~s7Sl;x4iq;FgjW=JuTo81Rv=7|gfVA3`#t-A(y5DJ(~sy18to)5m&%8d@4A<`e>!-OO;|KyQI zc=!l-bhcJda-fA!+qe>3Yh0Pa;_~rOvJd?L`H`(y8myMTs#ab5q7Ofb6*)h>X$16m zk8Dxe=rb0}kRgjeG-3zly!S#`C_H3=pO5(=ozdYlYG!*cdLReYEE~>@062%rU~HK_ z<&V`>8hs@3!e=qQ+kt6~i=!fL379meUw|V9@OMvxI$c0SR0dgtc!1njUR8fhZwPc1 z4@3X>-n$=omIj)UCYj@C-L}s9dJ6hB?m{c$6m(&L*^`B}%EBLVcFkoHV9pifZU3_T znWX+i>v|zuK0_50rXF^Cuit*=>ZJ&Q_*di>55=wVM{VKcAvU~>mu2YlH1)0F1>RT0 z9C`&UjNAok$If7FM3T5>ho!w3Q(qmfeOnjs!ltclXHWU<;6$XhPa1{OA_P#=RkDLS zPKEijf}tKagdUCD+bpSha!p5;(ParZUH_3#c(k&k(f61_M=NLCBQWLKev};3XH81| zQ1Zf~{gEAnzPXX_kYYRE;yVnD8QOO(Ebsm$WVgNV#-DHdVbgH_{vFNmf~74fi5O)y zoOa;n{7{ITM>8`VHs=fxUYmaGireTWdmEP7`X9D#EC#;MHZ&3ZC`SC z9MyF^i>FJJG#Ob59Kh`C;hZnvBMknseFln6*@WBC1=XlIP{`B8Zl~}ofxa7vgWfH% zhZb)A#s}kvNL4G&MWN_g09+UE@+>81By_JVgGQ@^f)EzkvmjuwrwU z7Olwr(bG4(Hw7z)50t%Tdr27D$wkjSSb4?@N=*i{Oh01P54fz@T}g*;A|>M-Wm+zd zU-_qL7F7%1b9)kA&n9;vlpBRe*vfF^5nQ&`sdIQ7i5`Tuh{tF^+~dXFOqc!=pbYAN z&`3!pm3xUsjS8PtWX*-=h^Q*gH@BrblcFQNHpwLf|9x7zsVn&R#FBvqmge5OI;A>h zd+SSBa(uR`hU8yKF)bk7;boOAITH#w)hi{Um}bsFuRZe3VC83+Ehlo`V|ufe{;pr z5>1)H18riA%s|=adWpNt83h&|9oI3azJ>%dkq+Y%U;g27SkSn_|1%FGko_Fb_NZCs zY6fYN{Hvz*=JQIHutg861u-NM1gDVuaYQ^OfEteS_Lv_(qX$>>_cOBYmDa`z8{A~s z`^1j$IcTdUZb~sRW3K{l#2e6uhNSP09`)(U+sPjIM$BgDqI6H!J+6d_j!TB%CD7oq z%n_1uhwLtp;!T3TovA$y3-SnUjcZth{?~#z~V3?_Ver4H+ zXLqWa?xMgLVW&O>)P>pv2prAsgl#&?j8o7FWGC}7)~O^24rC1?jCANK%Zc{oCjcLiD%y0RVoT@Q?k> zcgt&eMs&p2z{^HYLAh&-~lq5G;YVcI;@8rlwa?d{wyGolNGVHaukeA!p0d-20q#;ATF! zOyEHh609ey#bKCD zBBEnOiEFA28z8v>Yfwg{IZFi7MJir!QYPG2UwEr14|KFNWpm;jvlPAdzKyhjI5rV{ zmce}M&I9(kJ1Uz*%or)((9V}jptZv%g6^#!xjI`1R4CA;z*H#B?;+qg#`#FS!Gt<9 z{yw(s^QnTIdE4;@B&#kAFnkq(YFNZU$1bf5@EH<$Uv{@P7Q4W(52%V^A2C4^D4O&` zvOfD76m~B=_LzPsp+Kjzee*8wPHF=2GYztw4-sSFcsOoP%H5X##TRwD(O*^S?dum| zOpvj0Dq2EyZh&io@Qd;zBLNia)I+W{N{ki_C7F9zE}R@mIyx;!>3d;nkD5V&+fE- zgGyY%-1!e8w0PwZS21oqLHwrm^(1j^7hOD7IRD)GW=$u9t(y#-e;e9 zLky|-0**+iIxM9o*&8_mN@DVxp!6ff*!Wp>i`@)>WItME-K&&XMr7LJ2Wqp^2FSDM z4YRHtDR=a?Ym@A5<6Z`~MjXY)BholLW`ER>j7n-3mdfsi437b}H|q3VpH^e*PRk;=Cpap2_9 z2Oj^VSBV{MGAu%Y$vUEI9KDlViQ+n9+pB*U#d>G(sQ!cb%8Ru{ZQvVkigP(!Ueo(N z2hbMZWQ`bg#ko{SNXk8Gk}A4M3%`Ftd~cLWzHDu|K$JDWXk5qXt!wcY6@AX3mO?dN z&BMpPt`C0janG}I){{CH)?I>lpA)ZIYDQ+VmmGu2`X~`s_nDwZrZT=JlCV~@rEXi^ zgtCsWuIsXmeyfM6tVdR%{i+-^(vyJhoYrl#8f}@E`<#^CTX=#Xm0ff%)YE0?Y=aa4 zq6YJ%ce^=#|9>vmF#8?2?I0lvt4CnU8GR))#sK6I|H`W~s3#R6L8Jg6yIWp^L&rXa ziO}npc+1k#s|Udoqe(#b*F(PQm`e?rBMPL)ZhImrmfdb5F{Zy2v_= z?}`_shv=$5y5yy#Rw3ffeP`|C~K7jAy%5|L%T|EusXh23f+ z-Ijliu-dcl7iSjwZP|eL8hb6d=f#;GYNW_eQh@!cVRckgN(eYpgqS|f5CjkVPD@dIlI zTFO~~WNzgK`bn$3g1E4kf?D4Dq|4aSkiFO6gRv|tb0&Lma1s}-8!#MLnC+*rk{S5< zxC1F2AezxGIH1NUm7TgP@t?*>=^+smPzhJLUuUaBX#(*>6w3QG=$GKYW-My+(#Ret zr+uh9I;`;UJy@G9otb>`uZfT-^21pJKAUiJk{xc>mA9~{?d<7fFPNs~w-=wTRJ8Q4 zvQ93kK0{X?2KfUPn!bD#XnIFTZT?QZravxQNo5LlNDhY3Za^BG%|kUf`8@+GKNGyN z3fJ*oN!IfcP!odiLfwCI2NPI76c$DVIz#Ij;+g8qTQ)Kv%;XoLhl{@Ii{UzsDFM#-;U;(ICVCsAW#=+Mzvl#eRso7jT32!GZ2kLt%fXIJUf+wbfJKwzDPa ze?#$0oi;@5nxgM2oynt^s3Se5uM89^q`SQFv3qI+yKP_1VZ+f}hh*<>i>g&ItaEQ= z$l)+1u~Gj(aD6Rk=Y_V1z~8hruZ~{gLBd$C3~%uD;3s^@nSP*w+xu_@mFL(6UA}#$ z!c%F&U2@{vB1ROe)wRcGo?pPyM~tv|0!TjZ;GNHZ`=rZ*0JjaIO0`B%=}8`H!9|iu zq-&ON6#=+IVsN}vl%UjDr07K2l(%U=C_v3`>YAw2IT7VX_Dp;7z4M#h7p39T%EH?} z2wSA%R>W*7P?WZ|Iib|T3qRT}6bG*g6{R)!wq(G^kKt=0|5K$8x|)7l7Cni2oxMi6 zv{03Zen|uKelQRnMzz^ZmctGs?LAyh@F~o?Sm!CtyEo+FEW^$vg8beAbD~vM>(-ZM zIeVD1gv-%AA-de)$ec5_l|k{g&6|w=5QrsP!f&PE%02&RTYoGK=tV@GkjIR|!LX(I zSk1^$l7#<^o@8akWxS9XtZzF{&CC1m!H37n?87j)4c@?E7)5#>U0G`HO)e0G9m9ugHXC9OoRV z{L3+#kR1(zkkR4AXY3*>{(Z~&T%BnYYR_;*chg^>e{Ww2@RyWvl1&o`fU7Lh{;U*r zuPDr>p4eh^`%cPHz%SGBSmBLVrg%R*Xp2AsE>p>jFh(*)Ck~>chID+X$-nSO49taR z6Ui7&XOqD$+H;dK5{n!YPbJway$8M;UN=Hfjvt5Rw3HltQv_dN;nxsOr+2`Gy5L8R+f83r8 zOFGDd$lI@eu2|-)gUCh}e+!V6E@#NC*5NlJp_!$_CW6}LUJKBUMxUXI!y#*}EmG8= zxA?dCaw)BP!{Zfz)mk2!^2b*plWO7oKU1|DRYHk(r_>Q&woD+ zpY-8{PWUL2$>d^**2<6j#9rc#BD|K5v6XAAgu4BhjY#bbC?}nHCgb9XgkkWVa+6&R ziU51KBCzt*WVG6;rC7^~*~UyLO0|00r(?u7re`3Svv|4bFR^wxO$FhagWkg?4Pp0{ z5N^C~0yjtqx9tcZ<7U0DB`~Ky`pPzM+CfHxJtKeyzMOU@1Qf9Ip#*k!@hqy%5qi(c z$;22W`gTA59$fK@X~5enV7hJdt|3KWWtNk$O%H)$uH1YXuwdsJtxE}lmkwSQ?dm$K zkZT~>iIDTWij^--Mw)dKlPUuslwmkC3EHN8Y8u@>ZW+T02`g+C-xTpEKLR3m#SFojasv~}48rlmB zQ5Q!Fz7w#|!LA|MUWKR-4{)CZ3XY3HzNHjCqpEgxsMbzzHlghGDW}I5&YmE^>$l5@ zEW~FgURSs_Xd6s|o92d))DyyzLJu2fqTK>}8SLyXX}e9bE``;mDp|-axw*CPirN<& ztaB$Z6E3r3D3*hn_%IEH^&ZXv>eO*RB$c{z{t_TJLQ(JJA`)c68rr~Y5|O^U&VZ)t z_=$*iN%XkOsF(p|X=LK@l^|+ zkLdN84bTr^r$V~t%)((=r!=P_#5=kiJqV_V(qv4D$Zu0k!bm&5p3@#(Al7+id0NX@ zK4z3oo?#uycp!*BeOV@G@c_&}vf6n2+y@{a6v-Or{Y4HFiz(}X7dkV0=f{MXh^rk^6Ex;EzCTQnu{29G80dlmL>FMkK_R0ow z7x1>#EALP3>aO#-IABf5tH2~5={eX-y2@tj66VLu2Wrt+Z6A|74FQOMomsaoa@6_p zAAT##NA~`Zd1=O0R!RMD@|pF+#PB(96pi;~BOZPPpl2iP2M>s@9zsr!Bp%vZ5x4Z~ zaedZYV+hEmuNfUJgPV*SP%@9Tp4zI)^#Tfz8SZJ{G3p9Z1>-UmkjkT@Gl1&NA=w|t zV!L<$n!&%{&j@lN7A?>OkdOdhV);^TL?wnZ2$Q#9Vwb2YFj%Hd8#wS2f=61mTYyn$bbiAqd^w2_&-tJTAOD*NXjIzuC|_VX#Q&@8a>P0dy@9fr`$;h< z^hfh|?kuKaUHz;d95R%Z&2OutIOd1oXptwwS#BNgY*UuYq7-8_w0)x%?m#u9DBt_`X~4bsNpV2>YoRaJ^#W!0Tta6N9Wm$25pX8q)SyBpLZ)A^{%Qzy_`)y2YuXoz1r1e3je7gs! z0Pp!nBvr$p)d@)^`C!(VA##d@d72!toq&*Q;rwBk51b@D8SssXERl6K6h~!eC;jD& z0ai8RNd3^odEKzXBE>>d)4EtA2VHKL-1&W7v;ApaD;Vu3AB zadrZo*M{LC{mYJS*mxzm*FyG|A3EXM`>)??ylu#^SJhgg7LJ)>+(c(kUF^RnrJn{if`A$zC<+Fu3seXltA?|Ga{M}t`ENuf%1?h8i1FB})G2)b zZepg_(m%PSbQ~GDV7ggrAh`&$1~^QtY`Qt=j)V$AJYgZUBAcjhrhCl09S+k|o1dy8 z|INazdJdPTx72^D&1Y^9Z@z6TC7_2Uz3ft-yD#nO^mCSA-gVglLj%!$>5e=1mN^=1 z6(s$I`k)T&IwRheAjgx@$J0emu^y^I&XovX6h}7T3|igG)h+Wpi#M&Q04sOt$~=%G zwp;H_DG!V898Lpyuv!mkm{`*kkZBuAtv$xpau_4b6#Iw49$^qEks!*I*WeqNk^pD9 z{m{%3T1&8}EV<+8@*QzGRS8P}bs=x9Jxb7Lclf^=_T4AF^?+7PBXGrdPb9IqQo^mdQP8*K64`LU?t3Cx1JoWiHYO391dYcvoa zU!N2dT^GCjINZJoM?-60@zn3$RBdk#*%r_q@ZI0LT0`u_l zNI|9B7?}3YOIg)~iR>$8yIWt_pwyUky1}D$Z98W^L3c602!RGO{0fg`9=!fQgBZ51 z^jPtvuB3)$)-|^i4(Jzl*(KGEyUW_UeeNrb`_E!*doDtUBVK zr)ajLF0EYD45UBP4Jm36?%kh8>6mi&UK38L=YYhAO_^hgmH$EMx`Eg7C)Ht~VQQot z4hY0l(n@_@vPG;sA4VKF%eDKi3L_T*oahy%svr4_cI2baK6wwXZV2UmLp?|t7-sUF z#HOyJsM95#?b2x#KXV4VotWeDHdIPsg*{tDvqoH_Jt;JVJlUsFFH!8=5il7!%U^lY_Q1x||Ie_W9N53aK7x~7! z8BS-%sw>QsG~ie`6d`f}GvhMs=po*SCiygwcw)E;ykOwpEBR0^x0Bq82);iZ!4C#c zb6500*&zhC-}hPkJK$Wo)bwTECrMiGaM+O<&AQFCp`C4Rka*yeq3ArVM&Lv+{91oJ zl2Rpr@q7I5Iplxak zQ9=*srS$2va7-f=i;1xV6S4c@2y+saVBy?%k_no?8FYuHn{!%)!#PzDF(`9}<;MkZ zbumzz`wHNK#tkxsd{^JI<-J&Ij5yl>uRx`5hY*Okqj)u*(SjnWTa1|*_W<35Ves~Z z+Fm#RZ%6skp{`&xAl)IxHJIg;1Jaup#!Mw1?3+m$ctun;WLA}f6j^3LGVrd1ra0LO zq#-7)Br7xff6Nj1S$pc3V%ysPm%|X{yhYf**%bgX{=ET`ibklJdiYY?FPF}UG{LQV&9+Ervu~W zZ!$<2wMi^#r?v}YMBq<-TrIyoQv8?^AMsO-S5AuU19fddqKPW6W{^Owr&& zjt^G`(LG%lK=xx!rY5m!I=dQxrMQJ5Z6dv1==ir2{rpcufq`7*7E0bw()RAjKAJ2< zc_V_1WS>KwLNpr_(HsREMdDQ}_~Q@h3*?K*+EnrKh#YR1+fHZC^{%+WHB(B~`rj3e zAkAT&Qhx8g>&_=;Su&XR(HJt|aANNl0f0^4dt`0jt`-9xuKh8!lFV^W@sTyzf*>+g5tf1yxRuGKxBdD&v$N);E%zS3n^`!)jNG1tM~J3VvP zj%2a7Qm)eSTyMj|598(JwbbFXiOiTS8KH>#z40$wI2A2dy#rQ+uFGmjBKFeM#l za|7J&4w>lP8&T}eNWq7rWAlKvV$WI{sA6Ix?8pSmujjAXBq6=0OU{o?|NC(#cIl3W za@nC{x`GUjbRecLZmIjQz(NT>UwP%at8{08E7|#G&xHlCq*D;TSN8!h8NoWa=PGOo z|E^%bV(m^_cXpB{)v}jaI5wA_!+BKB!9GpfD91d(2ok6xs-S`bpzCFza4&dFr)M|;xNJKWetXQ< zAVk{U-6}5m(ak5OD*&UF>Zwi{M~vEa+su(u^Ug7IzWM+r_F8R#H2P3W^;<{(7fcfrQ( zpK5^f6hX*}2}<#*(y;AXu0`VJ`siG^;FLVI!k-y*q^p|`)GTj-`R6jsfv?xt$Fg54?zq+nd5xTK00u_8 z(^tbtC4Rz*3@Cd{<3vKMeM7Dy*D`}*X-lumq1M6rZskT?L%I3}kp4+@683S$IK4I4 z9$n($6D|A}q4@J&ifmlhFGeE)-Ze1t2?8}7IXys-D^}kW09M`v1=qght(V;wWWmrR zzs6cS9u{l$w{Z*SHp~Y7F!HDJ0q^aSgB6ZCYuU{4kBuAWKXg6~AF?i0f#)>dwo@Wt zHo-0@71(s*gP~q44RQkPk+3-5=^WlX8<6nonDI9F}u)U46zxvm@@M4L-GfY_aH3QuUJ_uMC4 zuL{+Va&h;F71}9>Gr02UcU)E0!p=q9w^1ycOi(S`PM77qRA)^R-;+Kop2G-&kDqRL zf?hTs_^2S5J<}i}TN(bISXoN7jiKT!*#f^<}xHI^QiWj5oi9`${%W15@7D~2ihw!y5=FLz~=QAj6=Hmh7;3#>l; z(od_;?O5*!(53G#%DETL10^5rgIgVAS;H;txygv8ImjsZQfMz1=~{v1!Vrw>pobm1 znW@hU#atO(n?&Xw1~LO?kb*E0$cA9&Gs(G^CmkQm%6?cJ;mX-qNqVH{Hn{_rOG zw`ASUMN6*1g^txh(QWe(> z>NnVpLl@SObv!Lf>Jx${;U?gzlkYNe|Ji~fYhkJL>7{-+`i*SY>;DC=G;7GJElGW> zMiI^1VQIhqmQR|C--$DT`#GQ@SeK6qhwHc9BV}`att8H|O4KtZWHWp=JUY0_mgGg< zU`k56Y)aX3Ne3NAho=IflqK+G26%(_**?+!N`c?B!k9aUahZpPsn=`=BygB>=zql8V zK~23gan0HtssBRFQp=fih#rTKfhBCltYkk!_VH_r$Q}ohvVTGRZY#cO-@+X*plMS@e z0(DyH{Zb%FjCp%haVsxwo?^2#XIg#xIM{4&O8JBrD~wwwNq7W%H@*RyDp=BDq;Sv@rn<6Y|^oo)(2e+U$ZmT4^uX*!?b*#9eK@ z5KhQ44X#}knMN>`9AQi|J~3N0GP0jZvW|B^TPOkK`qW{f1d}?oJqUGQ128!`+#-^< zmpO{*FQ*uQ;+(5orLHo|{2we9E4$JR7;Db`PW2e2f_zCr;bi`#0u#aSb&RB$DOF}* z?Q;9UqWd@1*UI+f8oNDJ-U>_!)UtJ~!3Qt^flrAsHyRfuNy%>R%u2v7(Y3~NL^ds5 zevT1ud`H0@ZEqHPWh17HPq)ilEtRyPwFL||Q}eLF%6Hn=^Ip=FiI2|o^3tn@yP_7_>iuZ1 z0NwOI!R5Ey5`+Gvi#Q?AH8vA6lO^@yNkBy+?1uDYTvpa4DR{5g?OxyIkCiHe&?k&) zfu$|rwsceLl3?KfO6OSp$#~|G9M5ylOLU)B3H3rg4STV{3DV&{dlM!WrU~)9Q$Yre zdy~=Fk=o^AE&E}t8W39!Q9Z|9(FyQQarWdo`Gu(?emTW8^z>4&O!k$}crh5m`G|dm zE`9S}U@9luP*SWnAFe_IB(@{?KT?0YEIHc5H4hpawi2+Vw+_c__830QEt!gKLSi0A ziIH9b#Cel+KD50Ngu%xktz=`n5JwvK`^3S4#v6s)$=X+jowGwx8xWEB6}ft7t=ej- zl>ObpyRm{4ll;ZiayH58z~-azx&S|F>6Rw22-gbn845KOyr^@HR++4Z{dppul10v~ zx2Bl=9OjO8wPq*D;`CI{myUloh*Yuzk{RMgmD18qQdG47PRl4+0j;G&4u=_+LpAl; zdtTaXQ=_2--EdNEd8YG-Wh$Zs{2N8$W;2;8!mR*dT%#=R-Ns^)i&_Q< zwdu1m|6lP~$tpB;5C8ifHhQxAz?8P@UbAWkBNm>xg(PAkiY=-6`7wbp^fkqkdkWNG zoQMyPcd;{o(sugA?(qZ{o;7kTlCu>~@J$!7U4NTpD@N}K$Lmv!1*1`cMSPf^ z8D8?Z1l}jvNTcgHT7ZZ$^oIv`oJj+Dmg6^2lesl;>lfP*Nn=Rn_@UiwfSFNTUy0dQz+zyupt;BR%0kGX0ckB#v^IhpL56I zHMU*&?Df$?#GyzJCh`FtbIr48oogy1*@bgVV=h{0x#@OmsmuIU=6!am7}#t^raud1 zN^Q-*g4D`$yz`kLLzcu%dL%6iI4WGMnEfNAGH5zc=i{`ya>2S@t7&^OXX4BnKA)k8 zKATuFhb5hUDr$60uW$<1gi>2cG9dzN=;#|)KxPmqM3k3}D!L=5_C>q6;Q?8jAkybG zqh&O}*-H<2H#4~!g`LO{zXl<7V|}%b_2KrpAr**G_{4xKNwqLaqN3Qxa2~7-xa7P( zp1E2LK#8+R)mu=J>h8lZCHqPHwJL6pqm2SFcy;z-xpl7p9apDHo3&|vxQbJSJ!JPh zggdUocXUWZZ|zN{?Ne$!%?6J`;6?=|K$D}IvoeI~O4Kf%Tg-px6nUkO(q_h0tZ@llgOb-{jZZFML9@ zc5~9DDwo&&7|`0JDO(rl$AOOqgj&XJNLR7K`^%4&$EF1>y8OZtBjwe0{UlT&(+2rn zGhn9O2v`1rAzn8%XSKK^r|hG+_6wuGSOhER0+vbG&h*OPH|=D@%fxp~)0=sq*gIhE z)Zeyk??LFiMwKnsu0swJWiA5RAsOmuS>sEs>yBCQ=|5PUV2hxBh!_KgSM_t$GZdSN z>oy;%GiOTVb7XbmJYKLqv}!T0L>Zor|Aa=sV-0N$ghH9D*2)HA<6+>{S5PqHSsQv4O1Qh7!!PQj1l= zTcM}$ZrLSH(Y&mf6|83%TUVQEa)8W8-brJu|19I}&+KGf5M%p4?=aqj#N%C1`&2`s zto}ae3fX*@1ITC(<$yGv<+(|K86mEXg|R{#!$Mx%uc+xb4m&)Dbt?5*Sdt;-Dw_?0 zz6?-dj1rXuGNEc|J&lKdtLv^^;)ul(hI9v99I5anUK3Z5KY2!3RM#@|qrnhPcnVxp zOF{rWC@NM^^=KqRIe4K)%1N@5y~;XMv&pCzGoiz*1+@;lGdW5L%kwChksbv76yZ`>g=A zY^xaxitS0g5C z44GX3T!=*9U#k-dxmRLV^`BGkag&4)4co7T)?(y&dZ0TzcDDD`O(2?e1*!?NFnfhj zHSCa08J7m^mEElY){@Jx$c2(rV+QBiBEp+&FlBbRFTg?^tohC*v7P7YyTbjJ?#9f{ zRQd1eNvf7kMxQp?C95~N^OoCh%I-kw^f6){srwF*ozii%+TS>7>P2&h)s| zJc_TGONWjVbJ*N#>Q}91qt^PD$pub~kzlhX&VI(6RQY_}EbYaa+F$fynYZl_3KXb|IvwR(@3APzLkPZcx4JE?v%I_o+I=&U+cR5I@F2kektwgJ)raRc zR9-f57LeGW(hZ_`w*1NORpH)b`)&^|Hjz(~I`|f0vl%7M47JRN_NHrIW7=Dj7?NTm z8x@xV=nKg21U~804}1wks~zXVoA;LJQC0e}-=uXN#W2Xc3AaiG%ODW_DJyEytYH`m zoPpl|0q^o>cYo@kMi*ASoJ_F{JVXLm|5(AN zi9W~EJ<$jMWSYQ=tW(ahjN;+rSWW43ItbS!{7jX4VWd$4bXWs_>;Z#n;)5x-i!lX& z;nGkGpCn9e^X_anEo=N!vp(Pw9P3fuNB^z_Ktp37XXgx(feBV=Sy*FEZagDxmQte> zJ?X`SO0S9t!$MkdL2a|4C?h!( zTfAKU{iNRKOF;~rzG-`>a$@Tz4<;sT3+Utgr|`|cyLco;CsbB0=|>dCRzt!o8)qLZ zY@OU8xpu3_((MjwTT5`j*8oMDdjslh-q+X2kMa7=>GL$g8IhklG0rjQ{===g3p4br z3*tx2P(RboMy^crin8gw>?*fhzzU9hC|Kq;h{`0SjXM# z)b1}Ru;R#{M!0_cJQ|%EYIiO&txyP5-SGUl6(=)|xi!Pfy9rq)kW&r*4Z<1w*?T_1 zTTsZVzKqe@rGa_||L+F8iZn}d>xU+vueo9xexZn%Arsx6PR zpDyI+^T^<>&EmdO38R3{VwmueBo3>GqORQP==q7=S!k`1cZ?!j@`9D7?e$AWdPs~* z@VL+xXY)`%3?D3)L46{y>QcMLQVwTuP}c{f3UOsVX~qZRw?t@r06*eE7Z%hX?st;f z2E6MaOL%zfJ z3feA>I((JAuu1FOc%?T2aucZt*mX1vW=gT2KtN1skpMoJeV!`@rGs`olTyli#VQ|F zA?0RW>VYkRluXBqB#AvbvEOIc=@$qulNq5BL6tw2vu}b1Wq%QHh*7E{_y(-Ml4xs( zk|lIZLD{ukZN!+ybI%RXaHC&clk-0YLz0eYbC&@pKM#gaX*TkUl#V11s1P$-*q03_ zT^4}zXAwvOp0Yh}StCYt;^yK~wCqKUX@AQeCk;M+>|sPtlx|$D|K=?xq`A@9?Kng4 z@3ql-Hr*NdZ~o;=LI@k@Jr*X!Ize8+Oc=oM;{9_KvQ9KwyAU|x@y$cL?<_Fl?Zm=c z&GEL~OO+1!q&1q;HT;g5&lvxjOcu^V58s($73~*m`Mpc{cK3(DjPEZ_bBUtr-cYPZ zhyhZ;cx+D=s1J*=R4cU4>T`BJAZQUZ;f;X7KYtM3S9JWJu0OetDv&+A(%`d_`pS&b zc7vPh5qX2m6>41B@23b}BgGakSpgct(z?z@5W#;dS~l+A;B!Jc_x7FhHw;4Jh#!_t zry>>``D!PJAyGE=H6P%mz*;A;JFWVE%%rco{@NYA-vq{nV=T~g;Sy%p1lP= z?n!u{{@|u+FCqK*?^&w+i}wLao0x*XsFV1txqk-74)9yidw7q|80VEeK^e|WBVBZW#bjxt9rgXUy{wq3MHrjksX3!@`6er&p`*xf-Voq zbuSx+2uWKihp&ctTeb}i$U9=gMPH}-+S%ZT_=Alr3JT;;6-@X|7C(@!Lmy#J3mB%z z&M0QoP-punu$L!Klkbijr56sNdoW9l0P^PV+gFb&8po_SoOhhjk@` zbYRk`R-7cmJgBN5=sxjtuhKes^eZVEn8YjM`SWaOzUz4e@O%A(U%!Y{$Q@8(ij zvp+-_2V4c>N?uygm4FU4EV*r0X$hF%y@TTZXJ*u3m546@lpLu@Saj+ib&*_uUmFEM z%WSx>C<|}EzG8bxRJSI>Mq-)Zo%`C8**Mb5VV>1bwi;z3La*YaANbL_&>e#!YV+d- z(@{?rkFFQ=&(jst>v4`#bvkwuo-`F`j_%^sU%n&NL<%jo?3#0QycN;#cZAV(?8=XwE* z;sDFhs2TGDtD~L!WJS2n?O(go;EfkMUjAxXO*LSCFSAK1B>ysVVHiy0yXs(R*3QD> z3%F4pl1xGvCV9DH3NOCevLI%z3RbS^2G+@G@eYo}WHCdq0b;7G9o_kY*v+T899j;G zMB3f#Q@*NzPJT0L4^AqhQ!h<#$h?6*a$;k`4*=tqL{RCcAPdw2N(s-VwDxjGBkZ`{EdAuV0u>Tb7TU3kBCnPEkrM4}z~4dHYr#ocID|K{(##G>%Obt0Evv={ z1vD;y5H|&fRO&9fnz5?peVX&|?!Wmt*jIqU8rqaBP~%K`;dgV8Tv-VlHypB|R%ED- zQg@M=_TzSc%4d0KNW=#(cucC}p;WYS>dTc*wwy{8S44vBTaT{K$U2*%ZwajYu`gU+ z`z_(O@<#G470XTQH&a$s)&l24t&QOUYY;5bSBk2vGhw<;+fPsRPl(riYHipO>n$xO zQT4-CSJ8U9A8NFt!yAmG|C(tUFP;Pr{4-!*riA5uP+W%?LM@Pc#&~qp**b3g3rmn; zXE%QRD-(WU6 zH^LPise#UCA%vf(To{kW(|zKk+=fI9L52I&h4y*l(xqtYfaMj$)YmWnO7iOLA9hu2C}z#wi&d{?=flEnkmmw1pV(~Hu-t-rZn9bq zk0+u8Mm|Cv7G?cd+DBqUB4&ncpdVcNv18ZW24j6JeDcN4IRcg@??Z81ybvUlMLakjvlPjAp1P0hyQg7wKUlJW7FPZrx`mc#5pAI z(a$2N9%EEJ0x*9kpG?XTBH8wHw#AV%IL>^YXM?4&m|N-}SWv3Vb_h-XAx~kPg6Xn# z?*V?*v`&T9JbUQ2X%)>`+(_+!I`tPtx9droYthF5Sgs`sJK}x|Y29&ZZ$|bip!D0e zrH3YG9~9%;kry0NM5KqugSG^8%jzK7sRm_(p;V&`F~$X^v=q%-|GJ)z_^TBsb>eCv ztdc2l-!~tD;QK$4gndO|`)nW>4>vS8cMo0LZEJyUhNl#b`kxv8uAuS?tM3h9NCGZ9 zCHD(N@p>u>eXDy*w2ZrS*%78%LvP2ElKrH`LsyQy7^|Y}TQL{YO=iuYi_h)%+c8Cd z8*crE^@1l_n-KHGq>^-PGKY%8+`e7IJV6H`_D%6~S5u5Y~bIf@B+h5Tkhenx4JHRcCQtd(0u z&mqRs7jhwedBeV`h&u^2lDjh8*SHMWt(45&Wi7AN%qJo6)2hRNkUV**Fkf@&W)B}B z=}M&^n5if8ACDiG!0Ew`-WHQLzwm^u1@S2N#8PtkS)o+qR~-VrDrJnC=ch?n`iU3RQ?z2f zI8Y;q@6Y6wnP+30+<>(`w~SlF8z@hgQKpk&k2sZ;W_mwbCM*@s!Jv*e4)Ka2A% zP`ha`{d-e;1HizqG&Rvo1`5quGc<7Njvi74?H_|hx58s}7<@*iJL)4_kVc23Ub)mE zf8Vo3uv*j_E8hx*qTjHsPN4b|G?EvZ6v!*^oIG{_|F zh>lE|$ucBxJ6T5(OLf?UzeOy!)Y9N1DSPKOtzSY0Qb|kq_s>pT&|ik0y^*RVf$YpN ziXT~*hSb$CQ6~?=#q^RYEyCIg6Vk%&r9V-WUJf8;Q3;FJ=g_w^;>aOKNBxfhPn!=E zwFIgLxC2_I!V4a4&aDf6e@v3o+4Pv2QudM?^CDWgj;)b`L6hGvYUZ~aLcV7sYsO;; zy~9PT`h%?W%44zPoro*jFSODNmQ^$}S{e{&{{a!%h|3(_3?Xsf8(_u^aMn$RQEwFF zX^hE#(a^+}Ldwim!}??-(LY3^L&s@*9#D8eU`4i*DoWLTBjOuax!%_o2*JpqwbNWZ zxN6xzf{|svTOn;ALdJLC)e@_Lcr;Y4J~}&NU1aJzBDTZcYtaf?>43CUFEvewr2|AM zSvW5|<#Ab}`zk7^)`4_)jQZ5oG6fzpgCl5SiKk|HMiZ*Od@T-yCQ>(y1$CUbfC);| z8$-ct>`@cbF*(!vXHD$ocwS<55zjvexFqc9SfL6!q;7Qu&gE6XOG%Rum%}BjGXr(Iq+?vX*z7UGWdkBLgs< zVHyk_-C}&xU~f4 z1BV9|kKssp;S(44&8M%*y31?(HVVd#jPP)YRt998OpmRDPrtl&sx$lUZBfM6&-aSQ z^W3K^A0;_?j-KRVa+Lpcj)}H=~1=8-x#a?kL({p%(rql`nHN5wodvMDg{{l>I zrprhoETDq8t|jzZ1>2{$lE6*tUj?pRIxU8&9`dzk2V>I$RzXsmf{F1yDFpWV{66{) znYr$JvOu>xhpjxY)ozgl`WvZ!uP70_KL%>J5*IKEOIKQn=}S(p(r^cL9E(f8amI(o z)>CLEu1RNeOF(Iz{9-yqlGZ6^LXh2=%@E;pLS?7sQz0h6Aeqjai$fr_oDi*q7O+sk zk8yMFO-p*qN=C427jL6upZDW5^#DQPl^%tM%ll#~vpguqH-!n^37cWA#hH1sJ~Rc+ zfq4P7F6TcCo?cDy7f5jEVkSAHL&b{JF)AF_3mkVTXXWM5?WnXNa}q#M2Vb0ZdcVKi z^u`+bj=h}4&h%Npj54Rm8_($u1@n2E{c!DRfm zx%9iqw5N#f`g67DN_s(;t?R%W0Q%N-(!*9EXcPvI9F)=et)R>jUNhx(i7mz6xR7`- zb(~2@&Da-#Km-m=Ghn6M?a|{W*Qz%BPNde>FSk$Li?^7CqZ>u|R?^a=5o7h+U;Img zo?&53y#zI~eYWAv?Zs)4miL`VZvN$MJ?rHk%7Q8r1cHGs(zB*PBS~MA?R}V`&9RkU z98y0mgJ)XBa1z-ny+mr{6I% zUgJe1nZfIa=(t=+=8M#HLPqX~ms9%TwrW!X@^Urmu23OU^o+4!0a6)^F<+itdn5{R zc*->HQU(7+KPy{_w4HN7yJhahT6C@7ea|+4GY_ew0@>XM-im%xtpmtm?(QR`S0~pe z6GlwN*vH?U4ket7&5mqmj~L2VxDuQ}EcG`5schQ8UWAPm2P^CY}6B8|@R~lT*;i-=kI&+-Y z`Iz-kOfAw)*We~wqTyYy3E_EXWu!a~Yq`Q~`j$`G%HApSyyp9W%W8$z4^lMC96&NF z<~UpCR&Lf{12iKl+TER`{=4M?xrPKA8+l@IZXqJR3W0N3qLb{Q7L8s>#Tcf>G4`F` z?TIF@HV+T(Fb)l>;*}^jJUT?>n>KimT#C0*+)Gnp!QK>u0ep))bj`DF!8HQWoy zj!j(!MzkqWzPBsZ#`b1|j;#ts2BFkM!}b^kcWRt2^=4+`nv{$R6(;^C3TMXg5``gK zy`QCV2fS&@yr25z^SieManrHLg*1xrA>oHu@szlfv z6Fr8lSCG*^*C(s&?sNO*&~>vaR#dFGlSkTTR|hAv6Ne*6>S}AFmGYA)tA)oHjhP!| zRgSe{c-eU{Z8pVVY8X3$<7Y=&p8|}p$bZ=iLtaqzBs{!)GC8V$xq5BVL&+t+Wf6B% zR0&@J-oJRP_DvGfd)yH6H$BPrWo|XTKesnJEUCo406e49e`<#k0PIV%j>?3`GgL{) z1u6#MiTEY7qJ*-An3Nq#-xima{#^nK$RIP?`VH#2c_fenoS|1CAS>rlELZ_(x_?C=sHzmfKGXSw=)UhT2i?VVUJ9*5DBnCTKX;*;kEV|B z#VmgXTuWYng62}?HXwnzg7GWs#ni{Y!Od{DDdJ8loIC|xLJ+H@Ue3c->0yWs^Z%Q3 zpH%VWdR9cJ`Ahp$d_FT_RaFyWdm{K4d)MCq!0rO&Jjh{GY?N2E_Xep+C_i|Q@UF+h zXB#W}&+#Mk#6mgxKZu4mW2D}FCj6N%H~{a9(lRq(FTU|gp>hC6;2cIeB99rshucj~ zuhO_zBDG!_uaGPJX+IZ+d4^%u9CDfEU^A2gXtXz}9x`U_7@~ti#Ur0jbbU#>ueoFd zTYh{uVE-Sj`9WNd8pSw1*yK@#QMXHTfpBtHc?$1tAvyvdB^oa* ziX85q>Oo$=!&~9v29OvZAb5V02a@a&4a`8Ww13?Ya6f(A4nh1;=l z6Z@3E2(QytMpTz{A9jZ2g0)ycz6Kp_ueLTZNZm=U{-fXF?}1z1?_~rO=F(g7@Knhi zaW1ieWT0zJ@4K2}wY1*87d3r$ma>_gx3*C_>zxLMKFR|#dvJG}x$vE2)Rwhwi_@!8UL z#1+f#cn8>9D998u#M8wAn?iD>vXDG`u-6+Q&fY|!mkQ(#%E7a72Ot=W5W%`LpQY0Z|7_ZXjM}6*ZtI z1=qkrp4{t4V6MDKbZelMNpyx&ly)GR^eUZeFScPrldzm&Z2Pcqz%}%q)nol+0ek85 zmF(v@=w+aaIXM!WF*}j>WOb0nvgBuzpir5~qhoN+%7p9Q?tNXW``i%sx~TU6`@Zhz z!9OgIKXl^VsV<3c1di&u#;Ngjg2l) zx;DEj@U@Ol>TQuP+3Dr-z(bKY{VkMfQ^MY#3Y zJ^A^Eu!8C~uR?le)QUWse-B|o-s%{TMQAAFI5_SiA|n_o4E*r~g#CFeMHyI? zURQcA(0njMdDVDSjok7pU48_=;Jk>V2}BgN49#j-z?C+K#04%Eb@r%T10`mqp)frc z2*pv`W2Ibn;O{TBJ!B*a&sUamUr*hYZya1SvwE*uBU`9P+et|DpSEq64y_&Z33o{- zeqhvnz<*QZpv0QEYTv_CVb>xp z3X<^MOGlt=izL@KxeCaKJ{jPV77VsY#$$`R{m>pMnnZS%s)nN~lvI>D+KwgTv|dKy zc#y(c2wV0qvOZrY*9LQFno$OQ0lh3@F(8lp!FU0mm~E8$lG?51MDuwOrKT8bAtgzT z@)7K%fR=Rr=eiNz|3MK%q3KEG+fbK{K@uSo;$q)OWHhh>v#BX-uRG(kz`i_}puVnE zPYaF|qRx?2RO@oMfEga=6N-x+!D_nd@AAZa(=`eq`Gz^~T3A5n;G-0(?#L8D6;Q&r z?MkH@bt#0>drh4*5t|fE;u9X601e(ybQw7`1vSncvit{p-GIaT?~j^G+ETYg<2Y(9 zm7~wUAb!an*>%TN7&{uJnSy03DAf17b5`u~-=}8)LT*{wG7Q{Wh8F_jS{{J2(B_GZ z8UEt>Gf*_v>NM-Q2j2hD0_aWChGzMP>rf`oWh(LrqM2%}&7yeLxL4w+@T|7tWkx>( z<94uUM!{uO=Vo-Sohm6D3F|}46f7+pf(*VB75T}m0BQgQOz2p@@o|p(S*8wWAHH$Z z3X>#>*Bvow^@vNSX@611rz_QDBwJeLQ~s2kz-QQom9XwN zU4vRJI(~E@r~xC7PZl`aAMKX&w}_d)Nsw1Z1Hx|069W$%Ed)`4PCgMyzXD=-pzLBE zp|NN{S>=G>Z{~}(4Ouje;GAfO;!(K4V|S*GuoFG(@3)^MKV@tFt%h>K_W!zHpWw)uz$!W4Mf*w2S18ehd%d!jypk4!;3my;-;lh&5{PiiC@&YNay(BXZ&Q|WMU@#T*V9eBRrnT>fMCO=(7N}tW z+H;wH40~L11>F^}??Q{9foNG-1tskW!BIxZTPF|PF-0qs9D$PXAODLnp27Bs8(Y2= zNI{!br$v7q2KG#gR~AB~X3chVuHM9Gx90Vza(%Jqek(t3iqJt9R+DeeC%G_d9B5g} z8c-Vn#(isvX{L+1kwOp4;ZWs z_FzW4UZpb`C;+)bBEH|0z-9E8VFym81zqTulnHyv@dB6(z~&{c2*>kBNUV`0`fI!% zw_rTO^pLa|1aYN`oFo`kt#=~05VmqF!nVpmS2S|#^yu_{3F?Px>7%GeVfvn3(8kHnFuxOI@lb@LJ#H+y3hwAGG;7< zS(OdxXDlhWE1`yNiQ6Yr?*9_%{h9i&(~hfjZ-?+J(i|KNb~HausGE0E$6uob&eQt0 z4Feit5Gv_K7CW7ZMkDnHHPZemj`S&O zohhG^Xbgr8noBjs>yh!x{)5BdMuHnG^#3-l6_X1MI2Oyb&Wt^o2AyFVY)> z#(;3Lc}tZJv+7kM>8aW}CPdygyk>ACXou6|vyZO^+j(^%J#=)sK1AyTIHMShD4pr* zLPmO23@yULG^ND|6M?!c^<dLpy`g+_P-5XQ$YGCp#XGw8P9l_Z!5iajkjAT#96gOs1uMeZN!jJ(3YHfRcM^l% zq;jTMvQIe#mzkECb)iP9ktcc;+?+^pXz&!ePA3W+`bXoW0f!qpg}%gz!-zu%i)t&R zUNp0*=4MJhO{PV!c$k|WEaejLK|G9(a7Ve$gfUz_Q!geg$>iyUO-}O@pTV%3|4^rk z;jd{TLmb(c8}sV4Sw}AX`}fd4imk!20O8 z4djsY3*YQif)Uiv0nL`41?~DYB;Bu9+2AGQZRtL%P>jDEav)J43}>0bh2%;H@J=1C zL-Ju(Au|=moQu~Ej0T^l*~h-n-DZH7-UC1u=Fgv0@`C(Wth)4;{#7#v&3`Y*R$x4q z9q#u?YOl|+nDFnnF}5k$aH1X?$*a`cydu|kbv@VQzfuQbBiiytYjUGtbdx~3(QA*| zsM3Zgd|ti6(KhqL?c6)2tEhS!(IBI-QUKaBcydW@t`WUTfc(u5ci(FqwV#TioYl}G zugE-n?S^x?t*1 zhT)19@7(p#2BNvsnU_6Jb^Z8_9$sRy&RYXyVqjbr0g<2 zE8l!1b&|*x@9@}PO$cN{cLBbjh|?4|LqWTYb7dJ&!flH{4=G$|s)G`{kv6)<#fa(( zgF*|9xkN+DurwZFJ*-%15muKg6mG3_m67umM{$K5vX8w&;eVK z#FFVI*pLFXD~cf8=IHoL23j-su=2s6MhVk2Y0C2tc{&G||FJfneviNGhO39jRGZsg z4zApuD|lyEgZ@3#G3<|ea`j*iLUOu;0v~uyM^dg`b_Ml7>}B8<+S1vci3n=6$2fMR z-n;oB+dPXx>bv!ccU7ct@)-4Jvb?sZd`pM>Z%uRMz&_8jjwue>-?!s>n6i^@*@%O~ zGu1?8LhJGa;;mNn!s&x2oR8ppB_9oN|8Vo?Vzh2=Rb^YO?-D6$R_zssTS!b!n;(vlgKQg!1GAKWSq82+>8_lAIe z_K|<0d~p#c$il1B2ryY#bGPJvrHkg-^4j}%<~&0qK1Oav|6j+6{z_wDXfy9QqS#+X z`w!%MmNTbJ)if&HM_IK*!#;~w@WYd@_5`s;X=QT>DsZVx464{rml|Y4=k+V{GyKX^ zC_wK26^KxZ2BrUC+jIIC2m#H*Tj|6#5-~L)@o3o%Zn=DG?{X#^BX~{0o7{^(?h&n!6T{FzFSC&d z=QU@f&Wr)gqY5NsA#gOPKI#A?dbjxKA_gMWaJt6 zdZrzfugIjZao{?#TKrf)O~h~A*gSxo7KKyB6dL{Ci}>*R&5{~}6BxOTXy`a+2!%b8 zAbS)`6$iWMJP{KA7k+KSnCLzQ${KZ0#WoaQ!}r7iHG!G4M!q?UkJrtFB>^U$HsKM< z>!Jsnyhl*XV^ftY=;F*OH`=O1zghN3Kef9$&~8p`;Hd(TGv9-vn%kWaX^ViPR+O0f z9)oNDBLog&8?frg#uu5i&S)@WD*;<-Rnd>722l?xbG<{@al$-NSzp~{nl1$X&OR5b zW*dZBR^nPewl>4KzpN`38v3JM(lVys66(B>0z_8%IY@aUy3iJl0q_USb$s|}VV+|4 zw!*n{z%3LXmU%^WTQGKqb2WL;oh?PERF-78M|mlJeQh5fvt|!4l4TZYiG+YjRK+@F zEnX(Ki1RY}N zkDTLC*#NgcBZVM!)oD4(TDA0SRj;pu>^J;~CM=8^l-Q;3>$J*^LOcuW#D6K&c+N;*Ii3*BpG@6aQICX{am1wogS+cNlZmm^Z$oI61eHo9=6^DkfWQ= z^n4iRmG&nQaM`-PPI)C69B!D+fQ8jNp2~Zn1o~#nosW)C-++= z>!{$Df`l^BnHq8B-NIuk>%LRt2(0%!KH-Q;lJ}Q3`uE(H<7~(0Q%PHYO;no=b}UK+ z8k!CoL1+aYo6sTH@H9_Y-*1=A)yyf6pM?9RoZFY;wG&}<6&I3GyC_x~Qgqs=wX?Hx zty|sGMg#K$HLdBhHWn^kHJA?C#Yf?)zoHV)!3Nzi7Z#ZW^t9g_u+7Rl?`zy<^crJ3 zjg2P6wlj;@p>!taXyp5N$9aDkld*e__Q^La;SpScV zu&5juRAuV?nE1>u7Po(}D1Nj3pBq&Mw?&wu@EI*^xO5%V&T*AfT`V;abG$Zj>5&}F z!0`)ZhEjru~B3SwVLRxLry+mU!7!k2+2;>@9sHDT3GI{Yoas+?|J~!kM}+%6VqOmg9m) z#E8$%8I^8N?EW_7ctKZcd;h@aVL8x5ltdR}2u{JJ`H#4RjRhKa!%3&dzHik^A~#QW zomCt?Cbx2iwl)slq^E9{17Wy|NB85~MvmB4oP+6$J>&)W1!dbi=D@vx#HmtrR}F7X ztQBtVAmzZea=-l6vu=q@afi*-)bZMNDPXmQ3vC{z*0Wn$S=5S;E5823Pl*#3Q}l%6 zQf=Q*qmoNm3m*o2`~mPHsC)%zI~zW*6b+3RcjlMbS$%(oG0NLzfAy7O)fyBcF{JXn zA0*fCf6o?)c_`~)=p7ohdiQ< zBIW>`S^+vyco{Tl*qNkY9{jsUrGKbh=TxfEqSV$T#Ji3pC$4^?^Ita9w?l4Lx%>ct ze4{POSqLN6<8z%$DRLqr*NRHrz7)2ruA@*y7u{!(dZn7C1T7HW29Wh9+R&%GsXrMrOEtK&%m)cj=7zyLUcJ9}}2!1}Q@>cN?r z&0F+GTwD{*y} z7X^exQ~=EL3)X^6MSwS!R3mpiF4MGJ@q~eZ0iuN*zl;)cW%^7fRhj}kh@V+LtmGrJ zaWst}wKK0*Tm_Gs~xPX?p`{L5&mJ_8{5=4wxj+ zWOkR327R-hVoakbR63n3)?l1%VhWCEUG-=GLc@Lky`?RUx9-Zf%^Yg(iGeR&OR>h} zP!2jEPu(j@gbD*z)wSO^OKum8(#T?Dl3=^Wx3p~Xlx0>X7*22tPh#4T=1iLXo}8v<^(*xh zK$5qmKQYcJTJ)=^slM@@aR*Q1|A%`dBHN~B7n%9r-%)xq>2erWjpR3PeMYbT?j)xf zenNKdourW18xOq6kH_#tiQIS}Vha>8U=6mvKA8Bngcqv2`$CiuZDrf6rqrsa-w<2hh_=%JbKjP3#GNl#<*^BLoF=%VZOS!wV$Ks?S@N(uI*b?JRgg* zgOhU5I>Uow`=7P?767%t0V;=B5+D$7Sl6r)=kkPsL?_&-wmgQV+I=a66CE;5QT=}` z%oiCG<}q$F3}ADr^2jdPCR9O~9`=L~X-+=EQPDwl(X#j2$a|1i?Ujyf@T(B8@O{kN zWgI`5dPR>KnffP^cJwG4KA(hX~L>u_LXfJSaXX3;gawq97gABe0foqTU&X}_t z-^FX!n0FV-3xPpExzd!Xw2*gLEoqS%Dx6YMbGY=QifBNDE9>7Z_1MPSDpniO!}niL zLTp0B_4v&N2J5(={%}ceo}itx*AeGIJe}DcVg@{?mJ<5qHZ4Ox)(gqY3)F0Ta$i~R zkLH4UTj~Mr0X^0AI3s)=lQX<+vY}2e7HZbVamYHa512bu;nQ-t=;#jDhXEO!HImIi zea*!p95(+}O~&QeLQhv2~@r$L}6Ev{~L&C4ijE<2b#V zfN^x4-~rSSv-Wmbzog1Zg?ry=0rhaIQw#n1=ZYt>BSEx4IiTZDrgZ+E1rk<3ACV-& z<%Cd+_b*j>Pc$t(Fb0IBxD71xg3n7*fp1IQ^XPMF=f)H!Rl+mdP)Tu^qY|D@qh|o# z+xr^~g#?t+<=~sPG>u^oKWcen5U+hy#b*`cUc%(t60uFoB3eA~7%M@8j%}*x&8&$8 z!uL&4*yf#u!H*0{*~kXz?6vyAsfQFcdFFTR^d`Wh-ChjKkd(0P8Ahlz`ZsX#P_wjr zd&AZTkO9ZD?PF%l(C}OZn6u(kNi$;xT{TvV63GsyD(%iSZe4>V76~O>|J>p#k%{Uw z_si4x76H7KPF@jFx&|AYkdxiMY=9}IS}ePika95Y^If2DtxOHRhFroD0ffAsR*`rl zdot{avYe~Of4L{rj1(%Y^5v%HN4?%hgq$G^L zfgNA?RvJNJ5EAz_+K_#}dkc@vh(Dju{9gAPB5T4kC+DdE&%AAW#@`g93YN&jTiv}3 zNJ5>Zft|>wT>$an&gh9UB1&k(@yO3n*tWqt3A*?!rmeH(!-Q`?9$^H3k@f+ic{d}N zlUZ~UHG%GMmSeMJ_4!LVCdEKYq!*Qyr&j~Xq@;mno_+A@^I_*7u>|1{{RG2$M?cf> z&M^nkE8e?Rd23`v=Y^wp<_jSvegU%Qj(yWaAunB->1n}LHC=kny_CE+tj8Q_cr`vc zb}Y6_&qS)bgBc68{GEPY5YG0zO^h+hT=|me%2hJ-^(`7G5rruO0^Ye46lgNikB(zb zynOSHsOx^#lcEQ?@zqS7vnq$uZ6hX;&m|_Gw?jZD@jxtr09|Mc48lXHPWJf#IG2uR z*1lS%_}Qzm)m?l-g-*5)9*p@mW`ne|uMq)N0x@c^JUZuNXqOvcC9HqJ^92G!wQ_R} z?}0VsTw@(HaQc+{8Z(~J4cj6YgBRs{*NFF3U%N(x9N`pL?=4)nj$2HNoU=d>>>{)I zW)iL*%g5c_3Oo~@i}1ti2$H3yR_cY;y&(J3?tGtPEzmZ;cy>%H!#%ugKM}xmp(%xh z2SYVQ;fK!+lUCru;U9rbyX^tYT==%eos#wPJRf?py^V*ZAkGsq4LEZ!?NNU5U(n^` z78a65QwE@-ARpQ>mJXNwc_>cC8OE8R^5e>;B0sc^C6FxrDt{oX^PF=f@zDX}tb2OJ z-`ke%sYUoA zV}Ki8Me!rxgjQ4Ib6oDsh=>rif*bfOxN_mM359Bm)b|1wQwR^?+R|Y_oCP->Es_kC zK2#nvZLpOk4-4rI$u02;j{A3}+%jB{^{q zqf8l%yS6Fg%1cSUGK4pK zDUx3N&o<{ct0-pDWGOci%ZD%as29NQ{RwLW`&_ZW`%im$y4UBUz&I`vw8s(l_ti&WxVmt5DWWk~{vKO^8QAL+ z!ZXJWb46m@jbdLia)|aR;ew$J4&zw8t ziXLVhX*~@?Jj9clIc!87{i#7Pa^*u_vz5*_7Or#gVafgTLL;$%62-G@J9WgPO&mjMd*!KJor?(91Pgx>BI5j4{JUz}kW<_9ZH3-1(C_$E$zc91XK5a-zRkG#bzN)#Y z;eu2W-V0KgL{p^Z(v8%hNNN^8OMfimG$Asu@y5E-2qinL0xa6{_a0j_ibtf45lFuV z&hOP-{6hm*_zMpp2Ur=Cdksq3VL55eH1QIc%MUYixVjt40~6^Ug=q}WIUa&CV<(Q{ zz9)(CVG=ak^4?8GJ$|kpMTqJZ%0)m15r6n%y8)0LHu0=;DTc2u$m3kK`T}~kHV~`a zg+D&ydd#=+qqR9zQLL(rHSh=xA*lPC%qm$j(@Hs%KSz#(a6)k$m(-$oUij{cq6h)w zjm+<@B15QU)6(`3c4QX?00J~(g0W1zM~m$(|BDS@1Z6n zJi};ej5QhD>v_JIog6|A3v9Op8T3w9cW0gSEib`FHu0F*26vQ27U61bUcLi3p)xv` zNmYWh!`Z>lEuA(%rUR$yIDpp)6V_33-5m)u{bz9?X-RWr7~1`o=?XS|yftS{smn}o z`P0oYS{VnPXSkmFh^1E{NHCJdkJNW`zms#bzn08T07pQ$zsQihTb48R~zzG}dD_G6k)+3-` zba(+@2;uFHUf_r@b0={IcdzO5Q~U-j$@Dqlb*eAel7!lq^*{T6n=ys0^g=BN*z zd?#%qQIv*GA_-30q|R9QoohY5y^)*A<#JO0sydG5AytO4XQ_yO&(hqw+XPlk2Z6uM z{XHHqLO0;N)rOFM@d&1|JsL+}2iSrvGv8q7f9iFuZqsiJ^ZYX4+HXU)$Nspzagmbd1 z!E?G?1RlJUhv-XX0$EYuKhC=3a?vr;|P>tw;DEEywcG-KG*VkjAr9@ zVFjB?SOxQ>BVTp}Q)j)mZ2I1%(+Ic(N{mQGQ(0VMEUE7p+SO}~-)3Q!wQX^~@Pe*t z@|MH>J=$$zkrfhb7Gi!Vo2X!iQkg}n*gw`4&TXJ6gJ2+-@gi%4(KBTv!P@WsEA{&# z8rW2VZk>(`Aqf-4jQTcUtYhG^1xwx{I}SfHEU0*1B%V)hU*C0Yd7f8YUS0R-{*=cA z#)1b$;c_5;Ssf+-5V!KlxANc5R7 zVOioy<)*+u-kQnrjIl4v3K_UTB^=FglLyoU6>l#%4WGVbHHa*zV{x{0{CEXOB2ucy zBL= z!H|`20|AXBspu5)M8OH}n=M_|9MUaT>p5sevmP9Sg7}Rv+1i$C5fEUANM;IF9{yc* zP#BP&GdSl|Z({NxxjlXu0K8yR+#S5?LH!apFs3i@n{#UK5QE-nie2X#qGM5v3&H+3 zTqMw16V-lPrH#Xtt;|QV+NVBe4eK#Ar*Te(@e89O(R>H{bJjY2&gaJ`X#9UORlIc(UI{I5u>)Larfl_yU5R zQXpxAxP@e(pq)6=o8X2pG&G5T(ePgXWK2d%*jy01gC#k?^<&3vDcKsxM z%R7Cag9L?P4S8?Kov${bY%h?roYJKz%+HmH`3@+d;d^g3>|z$3vRT*PSk`wbs&h7c zK7;|pE|swTF2-4hZy}Oi{dg({^5?N1E$<`=gR7q|xJ}a)?u&sH+(q5Hw*Bp<%1Tw^ zOJqx_?1PLnqq~3!zsRCqMeKr5sFXjlchr-YWC`&_2J`U8|J@zw<|)cnMU_B)I2;5h zzISyj)T0H$;6?Pjg;X~kqn^PC09|TeF#1CU6d>BZO)-6u4xCt@q%Z^9oTw6Ad;~aA zGF-p#_h2{6nF(MskZu!JOS^nLE8^l6va7~2>pubcVGdyrRGalutc)-Q%Ry&Mc9->n zw6hB0nbry3YT`qhU5}IW^&%8C2kAV`UNrsqqzK0ZlnHv7S|t9{l=z}U_U~TxaJRH_ zu+g-M0bAd!$u=RAuj`L}sFuK0;;s&yc9Al~9LDuTqKvkZ<(^+Ip$jWfKanP08AL$3 zwgB#*7hs4asIMG~7))PCrCM1PLmJ;*s((|4SCc(`&_Dzb@|b3O2|WkK|v~ zp}i_@*Qdqjo+H`E~ z*SMsPw*Ia%ap_qYlwnWoeNQ=8v>Cg+WsW9!V|t^&ZR^|==p;wr>Vwo7;MCgyDfdV+ z*DIQlAcRtW2+=(msXYyCWfjvqk*5l?1{xt=)CoN>j*B=jhj9#^lH%p8)P)}XExz!r zMMq9bY%#BN|Bp_v=K{?WYe!gqGzMG50a`e=K1_2!To z5=Y+AaQ47~a>#gpIM#!VC1Oj?wI#E+>X^Vhi54C+neyMIf_2wnO`dQILmSxG-DqT# znb5%JW9mt3Fu8}I$vnn|T4<)l#n^?|5kV?noJ}8CIAMq6mMva`6cv7CKAgah4X3nn zp*60}lHf*MA)6ZVEZ1bkxS01Jf^ev2XaAkPE@Rpc*v7&rTspk4SOwIU(Z8uXVL-{p z^QO}XbGC4Zj~4Mz*JeXpJ@r2%R^b~i+3{N;IHgm?K9rcQJ}Iy0nJ&P^fdbLl;aeh5 zC_%jt3wvhK*Ba+%A!;!)eq)|5r=&`KXfHCuMjHy9uHDtA`)FjYcMS)Us7j2f24ibNoH^e*f)W#ieOSvq zkt7}4gnHcTKVa8||J}uu>AfE25cvKdkF9qwmr5nD1@~32H%U%LnAs6fpL;zPx>*PQ z7BNtTdTO@nM)i?RZ9)^SXIZR83%gIa+x~z(ceO#$s{^S<64qE;ldD)-1Kh%Q*=*t^8}U<_DhA>}vc^RW zew-#mLwNu*y{xM1ZHo#5^|#3L})+pjV z6Cf)J##~AH)_gtGA_}pGawL2H0j&|Bc5Bz@G5CtCs*2TdqaWA^hh^4y37|OkH$&Q?=lxhE3-ECw2KGoz>m)CRvgtQ;@SRGCxkHJLHSBXlCc-r)c5QtPkLl_kWA?b* zl#Q38d0yIVp4ZmkuSbWwBtfpJ*fTv>I_)`C1|lFYE||b`%^K63-(2_Zq#oB6YbLxhx!?iI{BfZ1Rwm5Y(V4%^f0#Da6sF8Y~_VhEl!Bj#4))_!WoNV zK|0aUKFta~Dob(~;B7-_RN>9mTg;aM8Hwfr~318u5T4(u{ z!N|66+79SSEWdMNT}zoa)t+T~l8?(fd&<#jo*X5p;DpH{uL;|M9!}3bO~h(jR5Sd7GYZ#^YFx5Mlr2KnrZ9VMxKpP z1xR^dDrn<#dE}NP+l5upr5Ys&Ex;B6NWMv(=@jL4ho$HE6z+W{D^zZ4E_*M^R4=qYtt&ild9YTI(b~RCd|hMjns=W%Quzetl;$^{b5WWY+oWQS)Mt zBye5WtR0Ur&T7@&k#~ahDt*bx zAE}?xx|yBfEtbEqmD;$(uzb8I`s1KH#Y;cj+FRmVZtj(NqXE4Oj;GQcM2QX2Q?edh z17X$9HmYN-RiNn#;pNVw%AyTS` zn;mu2M|fh^E-SrineanIz7zqqROwP{9>-ObG@tc}6*S4)Lcu_paYG_eV7?QE7OTXB z4Mz(~#8MC<#1=VN885W%G1Qp?`xmN*{@#6<+YK>RKLK4G`6}or0)#lW%q3zw)Km+|(z_ z^D91!f7%|nF=X^BJr_9_OuqLtf)NM#<`ZrFjG;Spd4j>4Ii%AI4Vv2Ns`<{`?OSG1 zaGp?qrNNYCon(d=pTq)0!u3leFkjM%IqR%~jpM6QPRzfTv^{|DkB(_c`UD&^^sGN- zJ-!n2oPA-dq|CHWk@&$n5o87@*o0Xg{C6Nq&ZxG}MD*Vl8Oru&7GLU}T@c$qD3!A^ zl$P5W4mRW2=!rh}sC&W)Ch2=k1=u#95u z%@)X=04UPd)eh_2xhl;u;CGa!NssTn6DmpY6(^5+-1A>>&q=Fki|KGclCx;BLWj4G ze_=>_Q#9X}3yu|&Q#o4cJgs+9jq*G*q};i4KyZ?-WA2^k1jDoy*`FU&YK9e_qvB5u z{h%V+ApPYEvt}xlo3hV|=4*JUXTo8Ce_+~a9@r`JZO9y`F(JJ(VK&vbV6>4-VYscx zZTgI-N91=hrqp(KtVoU`?pESFVGH<`P8>j!qJRA*o49j#?#;1Oc2XP!I??M{Uk|h4 zW?w!}-@|_4N9IolhB|^7Z0ef$lCR)147@!u#yA$$vC3d$+eAsz>2&rE?J^pu)uiI2 z{5gH2@4(GDV%>~0E|ENQ1u}|L*{s4!NK}yhNxw92>mpkM?HPFSh#O;VS8+W;o8+qu zA9*Y5Chdjvc9Zr>soQ07HbafLOl%6AqQC%j%g=<)a7`!C+i#?N2CmtL?iO6)1)IEt zLRE^u(Bxs6fN?QeLO^moV4t%}SbT8c`Y}G(7E4hOXE{+xJX~#g?|84={EoF~A4N`4 zWggDHcQ-hmSFLTG%fUXmD~xyE`ZfQDxg|6{J3g}%`FTBL>FdxVnFNxw{R3qql0m4_ zt`Tdl-`g~X*w4OVBhs442$D-LN!BnnbD95%kCB{@t00LI^dq|>hpHOQ@Nx1?T{v}I zA~<;frS13Eh$yGF1zoVx_Xco<*P zr(4)WM)Klwif7$xmsmO`>x*1I#6~JbK_}9fN0av}!b>hIO;M!zBK? ztwq>UB+5=Y4Lkqywwkt>zsElUJD zvgZW@lFXysxCO9&KMD|~^p`ui0Uyr(=L|yJ7~ab{hB(Vy=GQwlNGE6JtDRw}TnUr? zpCn{EkY~=%Adf+izLqM{KsU6cOuMCR%A^+EYz;}(d^&IGbPa?H5K}x<1~QkKos+Q} zg}sh>@n<%{O3jlj;v*pK>*lhNyX)4^JMi4cFB(efvHW?ZH`kla7AO^w+?VOfqZ!e~ zenqcr15ukbW>D6tdM4@>V`e|Mrl=2r)C7r2{-w99C^@@Fjc7#0^$Eo#q9PLG(v=#H znv(6O&LlYOqmgf|pezUrfOC4ZqzO(NN7r$e-JreJ`+riY`R=C4-GTjH;c^a~chG3v zS!y%MrTOq);cf)rtG@B$+{zi$hyC_^ybXHCz^mFAr5EU|gbFl$#aP8?$c%MSjF z(b=SQ17z#LPpGAg**fx0VB~1>$p3ZJ2jGC4Hj=)eItQxm8nJ^!W)-?ySVHbjDP&>; zV<8oodz(-F@*XydRhSfHqU@#-eA;0vFLhE)thI!;hmf-^%<3{>%%r(PifRzv>Cnte zWNhfjGouUm$1_$SU}8yq%Q{Y23r0UlLr>mJ9re~grA5F2%6dj%O>R&04joFS1GfOh zL4^h!q%60(9GoDxBOJ@&pSGiv{{`d(M=)#K>mjG2JA>1)xW)x`Ucs%G+V9TtWqp&i z%iAdLcsO3T4w~~~K2={F0R0Q-e!5bda$Rv90MS{@&x%7cPpBE0e|;VegxjzS)wR2& zkTd5;qO4Q-$aXzpsx+rXGb-)!{J9Jk;syuNfpw;Q#{Er3@TxN;R<$cOvIR}=QyHDmbk zyiT?@O3<|z9SXz7j=ey-VFc_#uOrj5xvHpUS1IrQ!`TVaIl5w8)8+Ah6q<4!dNx5U zboyY4ueVxfrvD>+F%5}pr?SPBVj78gxQ~haT#l>3;_eteD?RV{lSm*5y~cT!(sMKm zQQ^f2nX(rA#wTpfJ>&RL$$S|@!!9S&D>S->015vt5UZ}VD!2}rx;VagQJwwAi91pz zoK}tZyOgk1i*B}}#-yo+J;Y_GAdbZK7kd|&Z~EXq$9-3PReWa+m1xdkoTkEqs-spp zdzxe_4y%!Scy8SlxJ4~fwRGB26@+(RjULmP)ORT$jS0oIqlxYksOI{_u>5o`h%Ag> z=lE^}`Cl*%@ZbX?Jt=Jnb!g(FH{fBIIS;O^SU1el8Z(le%6hhjq|0GlqaxSyT%FK#p}_;F%Y=L!SU?MmDnonpHNRZ26= zq=o6|kQVO)hgA`|>)NKLLN>3SGW#s7;6nHG|BhSZe*N@3?nSHq3zMFZ&`dh2E>gluJ3RimALa@ZV|n;jg;*8wluz;8y$HJG~p}ql-?o z_8-;5f#&!DN9wH``EA0H1d_(VPU7T4s$mi|uu_8~Q%{E?@9-aEBW|RL3z7~XZXnCB z1T~`Za9@*vjv8jfW+Hu7)7G#~=C*8|Ny>!I(M}W2=#wFrw-Y*|km5_c3k35NG^FwA zixM;1*^;u0aSB1Lp}bHuR#i_%DjQ7Dsf3J(NfR4jL@$tJY90w-EgA_o1{NI(1{r}cx%;jc*IJ1}GXOwuK`Q&|12#5q1 zGiZmlT`HE=Yw}acIlCeM*~YWvh9uZYnLCH1&%%`&pk_?JE?JvpaxN*&?To*iWG2H0 zvz90*67N^`dFg97WXXGKA>8?-i(^e9@)vD@e^mwQaKr)&icD6an_a%?S zY!Debpm^V8-b>h9A6x-~KSkjB(!2R4J1YEx$W9su=Hhz=nx$aOpUB%Lck7bIs4^^i zH!XBBst`(vVwWTj>;onY)KS+;5l03dpp-CTkDrM5WQGB`s2ybcOGipPHX3s7hc?z@ znq4oYU#}l&tNa+Ddt6#++Q3ApKmgjDnumD_oNMb)h&W_aU?1*dE5m5JQrehd? zEI*tSw4%{&73Y$}54EEzr?|$DgVMYA5?gD$MC=kQF8FZ=)!RPH4`Dgai!mM$Sb-sC zT8}llI0`rnbS~-&EnXM?6sOIBq*DzlR6=w%+ZfzYDUJ)N@j?03%(gE7n7wOoFTVZt zbM7(=af(MgejXRqXu=@R%xb!|LViu6y|?Txs6csNovx%ak_7)khI@Uzf0ge){+SYJ zFl42eQQlw+0K_8hf(HbbLaW)saxjCz=R>mY3GbK6p<=S@P_B=-Us%X z7itx(85*gyI~JH!qK4w2(o_2)=tb@NT$rhk{{$!UDdlvKWIme!GzadbAK7^YjuuHs zy9SjFrmW%WI~+50nhc8S>buO~MZQ6=J4(r9YiH6g1Q8cQ4rt^tKcQhU7;bJnhr)Gz zC|+L4p#X$7x*2VDmmP97W6M)F_JtTC*v^TDf3gKZ1nmp$;~CLW0&8={B;r7q z=l>%%+4gfb+8LiMph073fuf)Qh}X!k8A}oEe_>r_fLk~Rzw=7I(Jh#Lpj}D)r73#B zzy!zKqaRKVJQl2vq916_Ob%bux;ceXUjK1oKn3zNhF24$g?3MFSWkdv)`YpA>CgH# zRc3|y=N+22~iwqD%lw4 zdXvA^#NSCC+cp>Ba)=C+I_cfO!0*8ZF!7*UE}5qPmczhVs8N$3O@X z94>@@H<$jyzD_$l96)D!I+42t%yqAkOJI4O+zRh}eX&2^+8xuaXdI)wd(JG8Ge5X9 zv=DunHbP&;*~VFQJMQ06J>`Gg1iR=&UV&>@ZYx7O)PQ%iK=0-30BLVK$8Vamja(tN z6+{u{M!{H)sa zSo%z7v>dPRQ%rqALx+@7|tI3$p zlH6cfDj5=LyxmGk1IW>y>ob^#a0I~0t5qRyS%-+piE45u^l!UE$8litpp?7Dg1ZwEE|Es)m6gjfKh zzNp=G9cr*i3fUo?n61SKO`i%ibr#NpN6wj&LCo5Z)x(!5EK8q5QKL3HgsyzpGt(J~ zA)^xlXe{Y?33e5Jdaox)~-k%(6 zQ~SP729*c}JCo@jGQz}!$_PGmP;))K*58ZXiUkTGiBhgJ1ynnm9uaJt^uD|iKdR7T z#D3i=U0e4Iqsz^Aw+{IuZ)siIw3wzxOPAF@S&IBXBj&E;l{czaxQ>{dJTBQ`9BGv1 zs~!v-Gtuxht67d^UWh|lU$y#?HB%yIzFaCev2>?%#5=5j}Y{ zLHh4B08`%fBm~ytJZ-dnyqpD6>vP(0V{0gC`OD#`+F6zQdfLQ!fo=PUd|c6@67Etk z^buT-r(hWM8>VLVK&)oZcm+5Gq%P^tm3+6Wqz&B~?`VzP(4OCsFISTU9w zPZ3v%x(D;or*|#ZPu$1x{E=wP*)`78a1uq%Fj6(x2VxCc-;F&Q^5F7(^KB#u;sQrd zjfxM7C8>J%2A6nTYFWQ7j62nvS7zce8$Xe)4wc#1A$%<@mJvT`@a>PzzPTtP@iyTM z`cNoAJQ1k4G;)BuO!5Rar(0eSmNDsZDtvJZif>*r9D3>yaWA47UsrSxX8tX?HUUSi zI!iJ_M%&5;xdogim(PREo`>+7&4R7AX3@annnvM^wnGarMiY`DUVBrXu1ciGUoYRS z4U%RSG?M^kM2co(qb=W|p64b{P^WSJ%!H;^{Tu@|>F16lh&$G9Fba@cK%osq%JL)1 z0)zky8qWnZq)<_W#6sGdK9=0_d)#SQu%tgNk*g! zrT;^mA~C)ZjA)Qm&9a&>|Ld0zV!&OHsOvr>5j_n)BS6u1TkN$54ZA1#=L`As)z#q{ zn+ASMJUJCN%wc;sw(C?c0yjijApyI_EVwc5@^x#NeJoOuga1$j!B7VzlE4|Lpb&rJ zXS}DZq%-BV;sl2iE{~5Wu$2)eUPoW^Wa|duVqN+PVNEtf5z`X`Hfv$=b>B z)r2Y&;DTG>c)Du;w%SB%(Uv1u-_%ukTjuK20b(=111eu8Mazvg$N+;_W z1hbOPJrhjCe~#9UlEY1#1?S)cki|aD&Y(Lrix(yJ$stq6;c>ahy&7P_2t-Rfn|y)P zjIC`)ReJxQaXH@F4;8m!kll5N#HG{#>%wuC^W2?`go|u=5YK!jc*DykaIW}=g?ZpX zQ`R7>UGQy-FeS4`EboI^M>K*qUI2@m@`M?H0kCn$WdZ6&>IQzn)BE*YE)=`KSV&Gm zzj-B28c*2jV78;#s9De&kZz0Sb`%f>Z{PEImOIW=_oowzKFFt(3I50XEfWH&;0Y2I zrR=QZurD7AepOt+3^q6t@(V`GE?cZ4EBD61-!*msf3$3j7|agrr8nCbseb#5NZ83* z&qG&01mK+e3%Aw83|c#ncg*HlDpft+rFg`18i3h4-cXU%(>q3?niA8>*PHjKVe;u3 zEM#e7`<#vB?^7Vp)cu)|A+;&TbEEE@)p!Mw71WWp|$B<`fH&cLqfOy4w6H@knyhq?xm>c^B$0I8uiF57LKzHg3XREi;gT zoFG8D!xSEtWd^xN27`v;quG-Ic-2x3C#&#Jkuc>AE^RpshrqY9WlWvNRMRE;W}TT zEfKeQ-2$6i9wzAi+1VOGzJijmu$&MhF`D&VQG$&OmH9N2tN-*- z@MWit*j>7XD10tUQ%mxPl>v_Qk+}@Udb#@7FHBM{=RJ z3TXXBr2Z5yRX9D@`oR#~=Tu&?mtHf!v7ppIMN_lI6R=$vETAnp zC^S2o@u$!|PcZ#8AayteFyd}ItgaflQBpde+n)Z$cH9r;k%xKGIkVjFtqGKJpU?;= zD5u7UEN;grI{O0xf#?<7lS%cL!5?Vdz=6Kr&wJ|Iukz$_<&jk-r|1fxHAn5Gw2y7t zklhqqgXkp14|SVTM!QkEG$WHHei}L1PE5Xj-$Sh^6+F(}o7skW7bXh7f;#MU;O?$7Pd&2|^h0?Ic0yrs?Je?Wa z2SkI!i+Y`I+>VlbX;5`p<(SrO-eNKz%Gl|zudNL8LoA0v$1#cLbiEQvDw*~-9?f@P z^h4`wMCKuX>iZ8fd!ko;gUUx#Tzhk zoMy#r> z*$eD*K?dY|WJc6bZ38gxIw*mwxt$oBmAE=g`l~CKgYgSQ>AUxACn9FDw<5TQ38P&v z-NM-DfOU5S;P;QB!6c#0f`;_aj1*)q!mLMk1@dYWSlm_s&gu*|nzsY+?x+KWV7SYO z@uP9G;Uo&B3SU2uLig;?u9Jlww{rMwIkPra|49?NX_m=z3e8{-4t}Y62ZO2LSMWHB z-P`T=TkSR*atoLmF%peu84=uPp|-#%59x#OE=mR?>HU~9|B&lW{$DtTR!)53?%Fe8 zgPO$XH_?3+jY{2bgJ|~*Rj1{9Opt4pL;-@%1@a>BAwYtfxWlY5K_D!$2!&p^pn@jU zfGjRvq~0n$VxPNbGu&Xfe}13;B-tqDt;%rZ_YE9zLTkL&Lsr_XI+^QBy ziA#P)2W-xE6a4fOYxtZE+Tx)Gz3Qq9xgtX2T#rXZxMV@9jlS}zpMkYJUwJEkf~y+) zVM_8OfVg9dr%bT+VaT%3?EVrXLE)*<8$)+SWt6?-wwL(~GusOGDhE z#mu1f(*n!<;pxNCp}^;~S07k%5%kV(k#cVwPw0ZJo-)Z)*9L176ki8-NNQ|HW-1Gy zX6?6}Sc7)z@A-VE-6$NOZPmc3A55~j+jpekM?(E(A{q zT{P812yPP|xO*@oCiibx2D;AOWxF@5dU}QKV;KHyeaF2%@{W>QiC#q@1SOx(W^tC0 zSvVNL&E-1Iv+W121-aC3e(iUo?fUn>f_=vs7v22n{Zxzb1c+1U+v?Isie&1e-!f%2a$ z;wF&?SA0BJYmV`$uD+4SbzNNpIdy@@62u`Gwr_jz`#TviwiLv@iHo_$YWAyhe1PFm zSe>;Bx-uLxo?ez_@kBMMdw+gY8+Q9xD=>PC853|4ni6()z3GQ2jee(Y*r+n?hHx}h z+w^5ho@t-{GM>gkrzUM_=vhzVU8S>Clb2wNh0u6dc9*&52TQ0sFoQnhD~67LjR(=D zXG`84PHAU}yk}Wl_1UrNwAyu7&r23Bn(+&=X3bqnzm|t4S;4D_{b8V}b=%N6&P+Ig z4z$wqv3DSFZNI(BKk9%dRR{*m8w0H))w8lTPt>7I#sCk_q?Tgj#F-^yTSp{6k8FJEtON2plw!P`==$TAJWUC<(7t-|YhMs{k!7x5)%1x5H^ z8P#!9GBOu9%fGnQXcSpG@Y@2fKVyVIX70*w3LJ?Ir!sx-jN?cA{ zB?WiNZG~711PMYktqMEUj)1%|4Y^03;Ak3n(`-xwIPJo3h*e=Uq;GfD?)_zT3CwZr z)H+x&1fW){E~Vkb{qP6`PP{eazeV*0i^BUUo)`J3RvJ81s~8E{TL5%bF4M#hGkxEC zZaGkBNXBO0D-pOyUhD9R81-}OSsH@{2FRFqPij`*SC?2y-t`1T;eboUAW2X~iK{5P zo)C0i{A`%BhwD(ziSR9<)cP8+$dRd7wM&5)#YNhZ+B2~62rk9vt`H9|N~ZdYWNET1 z1!{gCr`*AXmRh3p6IZbL>cs1~_)j=ozU@{c=_1q)k>9?g=@X^X^5<0_Ryq1(}_r?LkO7PZx$jZ`CsAz&iX-?0-Yb3`qcn+2~Y@9&PuFtItkZTC-NuNC)Chc%C8o3Jn%GoyO*5X7J2{W zmwIxu40Hj>D#kSF4Rk#R`NexlF&8@Sp|s^*rx>_zk=0YS2|8R6eDa)YHZnl^(EmF< zidWAfYuGV5axGg+ptzdK;}pQgX7xArw7~NX@!!^HeT}T{qaaLzj>lnN_js)xx8Q*0 zIZF6$f)Po>ihi<1AP$Z5HT?xz4Ic#Cq$YaK9*=lFA)WJ@?BgQ2I9)Y1&l8I^Gfn;% zkA4d@(griE1!cs+Yk+&ftxR~_f($KNt312pe_1$ye7lW?t+(rci-uLRt z9`|o-?MZA)FnM&#K4#FVc&xl~bS%Zzb)hv3zzl>h93jRHteXJRfbWD)wrqq1|J^9r zOh?$LiC~j%T&p9**#(Sd)d7OnX_FuB5XlF7(bIOkS&yaga~LxncS6S$iNw5OtnENM zl-9U5y;g40cAVP$0coVa_;tEm%=O7U$Zb2k*;gJ|zZv?8;5(s1neY~lCn>hMo}j{2 zk|EF^k)P@D-_<2Txu*!GnlNVi-iIE-k3w)%hR5H%!@wPj-q&@W7%eMMhPXRDXja^X zm7gG9cbsy_$EsGJcBz6q59Z|P4d-oxx-HLv&lIP?WwTb~Sm!;@%d!~qx6~`3$!1g3 zjM@YUpo$0roBad14#U!Qz4EB|eUUaj*I(}+&*T8NY;reb9y;aey3={-LG^iiX?lGj z;pV8lGPw%~aH~A2Md_B|JWi-H^C*cu_$IE>0h<|G-vt4ox@N%#Tb6L?R}@qMx)dD= z?=~XVOHSMBh>zVHt>VaASj|tDRW}Ry<9BVG$F$`racSZ|wr!lic=Dd-Wqc95lHhTj zv2>-|w8(&9B1X;t@*CR;!8_>k#zfbH%QY1GK8K@yV^t173#x`Am$}X7=85K-+4zqK z`*7>-V{ZZt#<4&tn!NswNQBF(#%pKyGm3(nr7A}d04Lb!8s>R`!Ufc_1w`nxDQe$( z|D)vzB5(U0>hv(dE3LpJVqn!?X7Hx8O@MkX90kXNjL@>Axu-|bOnZ9y1(^rf#zLYH z6BuKS zW{oxw8`A&vo7XWv<*XKO*rY`{jh!!d>h1o`E-_D{$|9qL8@$RX#~Y4(gE@!j`fc|%8^Jj^=E#8Q+NNymVfGJPyLTY;#GQgi## zCOGrQZ4mb;suF7A6veFNxJe60emduXEKjAyJ-9N5K(#e~EA1hzlA!08fHByZhG7PP zt?*L~f)#|3yn^vrb*6_djDrscRB^p<7*uDGF&ervqPrJ{66BGR%6rG)FSJXTx=p3CR|9Mv>k*YVm3ivgc5ugzOZsGRAtaRsLqG zQylYX#o~io!AM@Ix3^Q|{4Bk)nY;IY_IVn(wrIPeDKZK^=#hYJtbiViwgx+>T!=mo z*ZU$22n2%Z92(uAlMV#2SX)$&?%RrBzBdZ2WPBU>6`u`TT|pL;NBVTWL_V+g;b9;4 ztJ-%RZcHSoW>gKa{ey|yU$R||GF5HQhJw1Y_$ffW4pWQAjdSQ{zu9iENs6^!vg3vf zmfruJ(?W)T-?~xK$+Sp&6qk36mxtXOPwAK(aVIE? z(8c;vd6Hd2qIP2#hx@U}ryRPQ**zmN38;8TihAsx#kIlCqiUOhX9Q#%V=s!&8iCT` zkKsougE|_G9HW;PO1ibbyGcE<6jbl}!ZWJZeN3^v0$Hrw=Uhp0`l{*l8XVc$N!?F6 z{2Hjv`@X(ifzZu>&hEoC(asbVLD_f>QwU`~a%is+Qx7XWZIU(z3cqL=+q(Fw9`B|Ly*CWG%l~NHvlALn{DuAC ztK`HtATbdV{iHod*v(wOpSu4l(WJj`1}IDg-|L`23>#)(cWUtp0*?3jx4U95=bQGD z9?|ULS(~jTdb-~<%Id}yui*EfSA&7~)`O{4JB8qCrsOO-Ke2#LOf|S`P>S~`dPs~T z2cA-OH`&mdCwXJ>>d$GFHB$%nWf;!dK#xNA)FUr~6wYpYrpW)ZOErzhsY}e#L^|v9 z^?%w=I?~&*NTjd0VTG{sKVKaw}&MJxnL|`glAfOPU1oE4t zLDn?U=@JupM4&+C3`mqd=Go;zH-H=eTCe@Od+NqA9FA?#JpVruZO@W{fY+N|T$zx4 z)rT6@Tnk}hDg~6Dh_m0SEpIQ>j<|RVVzU);h+)KIMm@Usj7@?z4=}`RzVL7M?gFn z^2}Q22vz)Soy~d%K?>)41(1tN`T>ow>jJtc>G7Gz4=dA%XhV+B!Y~;>ISo2b3zndF zhA)NHd>LaGwe)=x#@2>2C;^+TgxyudHPV|Gn$T+w6Ypij)Y|1c<6UJUaO1%`bAcEW zWr>Z8f&&f&+s(O~fTDO~&weg$MPd7Lz{!`KCg5~^b2Fg55}Yp2#lWBDd>pjdb_SWL z%Xzz`z1ez~G88Yb0Zd0RTcT3a&gbx4Dz>Q*sibRF=-O5#e15*`z}&J-V23_KrqR;} zJ)WQGehn1Y6*!%SK zc~PyHW7YfNZ59E`{jiW<@;LS}29VXN^#n>0IvoqnO{*mJCKn}9t4ke%?Al}QO;%nJ zRK?;1IseW3>0=KZpnY42gq8hCngg$#=87EjYi$ zN*>URc3NlhCi;X)b0`;|fbF4nCBMzU7av<1j-?`(BVt`h#}=1| zIXv*4kc%j#A>lFRCdCu!D&z-%}-aPdfZzo z`m7x&txT@|_>XiJ>44elC}=@?%d> z%Y{1p{ee|G63Wo4YYzA`bzl=?wM17Xrxj^#M}&6EpA*z3BRxkZ=YepCwNfYL^>7Fa zj*Z=K%&|7SPn;HTOKIAPs31*Hm;i)c@TSlXimsTsU+gU`x}i}b7~&Z9CPA@79@r6v zX#}`;rFk@l&rSZua`=;`vYeBP^?4^B%*rj(Ox`!i*aLbZDi(nvy>~gLwG1H>jm3;G zIJrw^Qt{U@P5}K~;qk0DbU#igXd``)(Zc9P;~PgNzXX z1*zh7P+?M<=E+H_q(pKi2i$A_8T=zj6bkAbeZ0PIY@MxLG#5?f&>=dUa`+0X+dYWW z=~%0-3@jdZ95?rohvES!yXI?{xbq|#ZP;v$cxpTYR~&}U4T9Sunzt~~4A2xFkoUi^ z)=6X?G-_z;5v+dhYDQ`UU$neeuciTv^ka1?@2XrN@j_9h@JTOp5{pe!LR#J6!GbMt z_T}~b&4t{#3;5Ue6*AMhNp-!eNubcBKv6XVKm4A{&WR63y7vu)UjO3PjXXaj7?sZn z*q$dv3OifiOI1FUAnh6iBm7ZXnBoM(mpgH%fj=(yL&#ca+DH<^F)l9XIn!2>;D(bN z31zy!6h!)Uu|CTHScoZU_xWurN4PJq)vb*GM~I$eMGNTyjTCnAGg=(6BEpz{j=+C65-vPC{a8zUFDD7F z7|lz$JIbm@%A@sNyWZE~xskFgo?Mb|KhK?ZYDJL2AY^k|*gG;Q8cS%VvlRy_Zg62p zwJdD|x0UF_5@P|G39$nZ$*S;44p6!Ts{=<+`Lo8ymFV^OJ|YY*G9XN@M@U7CJqahX zzZt_jfj?9I*nK@UM&cHkT8zXSB9(ah!RE_H8jmtq9=2BOhG%{b8)oV{vR?C$6aB{- z09TrXj9Xt(4K$vsgsI9Jy}&jb{KRI&2`O4j!+tlzH7J0`)u2isf03&4o9jwfZckjh z(zif<=f*%DQvocsab{Wt!8F)$3B4UTs2iM6C7ZRFK%sgbyER9v+&*r1LxFG5oignd zGe65%GW}Pa?S!OlesMIpM=fRj1r+$6VAIo{ferOxj#;FbR+^c3$?16zxzG_#SNcO5 zbHJVMXEc^5+|;LNhZj3DV*JPoG6Z7`70~%CUO*$YCK|o+6UDA@58fB`=el{{ExGE{ zEk@*M2(AAlFi(G^ z4iK+SH7=@296X2`0nMx{vxF(@^pSC)g;dWiNCp^UGR81KgjHg0D%E#<5*~pam7dVI zO0%x>Vu-xyxC{Vqgn2>G8hSg-UyBwOPs?WYPbKY1jLIMi>qVGb69@FWu;u;wjr4TQ z&_e3zG+_1J6)Ffti)rJq&6;!K^tvbbCJ_4UDsqoE0nJ-r>x5xq(qvhrV>vl&Vqju^ zd8kqn%SVYZ3x2Zb+jLnyzi5s?5g|Kt&Yk7O7`7_1|9t-cInarZAmD?kSx1vdeoU1>k0D_fEN_?0AWNnJ<_D}$SX*8}8mWby? z-Fmq=hQG3EX#W>h@3)VT1DO@II_X z>C;N}LLT9i>{pGXeQe*Z$d{FQJlq-rIbl7nfYSBzuD4!`3*mh}i(gLlu`L}DH6v!P zQu*lX!A^BJ3sjuuY_nPk1@uJ76q*;!5bu2YAjDnq^47F*dWJ?ba&Q+4`}N6vnFX6N zr^uyXisuR^2Kx8+lZhASW4AiG|55*6oER0^z-%XF)V?YkXGpO4g0V#83y(ft6AwXc zZ=w2aF4$AebuJjLued0C3B{-fLT^rODL;Q`o9{Sr*~%4%D0tSx{b)L>TDkf(epeOEwjCvJ3rHYqy`!kpc+bvHPxm69AQ0NCN37pTC1psOWh z7oxokn`iXi3sa;omhzjsf`P1u(G=30)b!1@EzweJ*c0!AsD;1d4Q8QfI~6AVn2t)_ zi+jTY6OL?=&AHY#p>#<)gY0GhSVs+gatQPugFr@DApvncEOd(K#>1ad$#y7}v=NY> zxlK{7xGgRgoGz&^-r#Gw3Bc6gkHIDpf*cHzJ#4IZ>WElW^GK=k+6gdWjA3zO(H1rG z)SqNBC^C=tsagIPB#a(|RMAEU2JFG=jDz9yy&gS#(G&27;Vo>)Bh!MIIy>cT2AaF6 zpxntM9WQ5Hye1&(5Vd}qj2SnY>_$k%ZTije)k}Nxdfy%1MIQX*$jL00{K3s6E~<~j zi~Py?pEBdG%cAad0_mswo^Y1_c+`aiM6WUo0n_Le*}aK=k9g2Q1~@wQH$P4#`bycj zm}9Lt6aebJuZbxTg+vD5cP+Fs{|lv?JU?C?l99o0r=dDh@Y9Pa3f{HC-dc%SI>Hpn zVI9x{MBX1w*6DLKktvc5DZnUbm8HFNS4pFnM0=$9BK-SZAgae&egFPs>bo+zrAn2? zcdq!|GpG!UrOB5m+m|shiU&*B$rHs$pm;ID1!8%>!fr*e3a?qSpW4fWKz9EOG>Nai3^f$1xT0QKT5UadGvf2Qv&pa( znD;8Y#CB?lSU6h; zZ?#3DVwsGB{2cK)aOldsZm5H_R&V;N=>0ZesWZ_(h0t5<6coxJDgi>_eG%at`Gqjx z_;ZAFY>vm?aULsA$Yl8u$HdmgPBygR`(<~SvZ_HZ_i7PKwmx|Q9(!)`d^Z0xc&{C_ z!Sf~=3ypNtCr9(U0dVP*c#o%wxvC`>J>|wg-O{_+PrM8@$(C_&NX@Lec9Xi@N*_Jt zBaw>+bn26~g#qe^nNjl#y7&xVSRwP6KLa6gUK@NE#hCTepXr0zltyX3UW!tqI9N;# zoZd1mzwk>5s}x2={2b}Q;wfje=>cwYUrLp@D3CGEKeDj`f*RN(dB!*nyP3XK5!X>M zIhD+$tVjP}C1RX?g99hzy)lBEJ#YSXkKKK;ejeOoI+<))5IIY41O3B>HOjT2{c;1s zY!xz0l~Ok7+J3InM!2*17CtD}=+ z1BD6!Xs)f~nD`?|c!f*%Y#s5T{;q+O(7n$lyhjw3HF!R&Fi=49EgXfj3?oL1Aghq? zx#0byW5SJJhmRc#s%4x!@rn!qf}OP~{545`R?v#5Yp@CZH+IJ4SbU=E_)&t?ocGA0 zkw>^tu!B0j;T245n^fTn!Z3z=)M~veZu!j!P#LfGdNq`$xB$wG%=c=y!@$$^E31TG`Zi%2;aj*AYHki|ZOEA4*FYL>SVPeWVS1TeQJaVF6ij&I=gE zBZVwL-`kcRr5BR_kC4Sz8aZo1Xe@CyFR*MKuto9pQ2ha2X-YhI<|o|IHroBf3&-S)5EIQr7v{WaF#_NiK0c zH9kwjr{kIuW;MX@qEJ&XdVOQXrWI-c`Y8Z<#LTJz1*XpHqg&_2UinE}mQ@pi4i!^; z<$JMX6>R4H(z#R+q(UPRxogTlBJXz%@~5d6Iq*dhhaH$oEc24~6DI(F@V-kbtbB=) zyb2+GU+!-e%o5of+ZC4uE^EXZyH6S;paS;-YNPR4wcjf2D2b|lZIPqFu{COPQHgj( z)!tTlf>ayMCJK-;3>3~x=)bGkHk^l*DksHlNt{iM6#gnq4xd@A;?RuQpCn6aRh>QX zV$tM`_C?4X_O^j*j1I$;=vLvlbZKC^WtnIM{RB$HRP`f(>VS_LYMR5v5YIfwWI!oc zuC!$X(&oOUUqq#m|9o?;Oga~ss+Q)nJD<&@(?;P`b`Jtp8JzAZ{%%5l3%>nvr2g%2 zqx#P6!Cc?PWTY%-YyKZpVq7BaF+X-INfs+qR0F@X~*#UczTCVv3^&C?8@9yOUD!U4cJ zS6VwT__}gAm!lHdN4_(DpwoTM4q_dvBH^u6;bcZpop;$)Lydf5_?o zuFK+~68{|r|AGT~sR`sJVyk(p9F*Ohyr~ha*=U+UGpx~-%#hVO2^9|pB$W2-lGAPP z@7c=W(7DO`uEX@9NE3KJ?>c}y0>E+E&D``9ax@0cC@_zD*LUW~xxo}~kT;t2lVZG# zndCw>iRNM$gMn~TY{A|w-vp`UkiCm6-U&8@#x2Ae0$(ZDMD5?o5k5` zy^HZjmpjiBZ1Y24h!;`9Q0F1TVTL0^?vkxvz>gwuCZqB@pZ>3}7KN@9_>2AU;299^ z3>QrIg@trjgZM>ixY@2cbGdK`Ak!KDB{gt&t?Kx5Lyb87v(8OI}-2>;U?G z_Q%Q{>h~(`B5}qLQcxWR6|lU!=F z>m0O5%g#!arx{%A7BrnohhC!{^Wsm(Yd{e|xHVN4d3iZK>LeB3 z@D@y&%SGgw{=zd!s3HCQZx$*RjRI3e4DcK=Plo|6OOUu4%A)jJdE$K4LKSfRPoSyu zW2A1ED+>P3BfFj2y1RG6oE^Ey#G4XtsXhl!k9gRW=!hzn(Uf{1qXB~DPBD7QL&&&n z6C+JElDOJaozIcNo`fx$v}hehb!ClknfG$m-H1EFt520WypGr3bX)$G$EpVBO_o3( z!VD9`r}xZfrQgdT{HtSzi5Z4`rFYH9I1vnS3FK;ih5l5SQceQA?q)MW_kdxj`qQ`e$qxSztxA8A$J|3AG`w0-AT=N=>u$iwhhfA z97#P%Nuoc7Ps-ZYp~J7hA75;qpGNfwZ}xfL<1^k z_GXeqd$QYsoXS6lQpDM`W54N;bugh=*Jet=Fn{w*k!857`<4+XHzE+t!LGsvE2(XlFJ-u5Iu+vD zf^EUEmKn%2=*bSZrCFykH`!hM{nY21_F}kh&5WB=m}x~OdJ7>GtTw2re*1tp4*)L1 zB53pZPA_`kxU{@eIJqorgrYO}U8nL(8(9DQ6$9fvv;f2?MVPICgDv^cD{#tnn1$bt zQGR8^W&~x}?3RIN@xz})^DGvk?m-k(`SvoJSEi{iqnxSx7K*n#o@2}(YI&fy!_ zJ~1YxQ1ptwpv;H@Q_Jvh=oqqtYn4|QT&UP^^2hgw1%){DpS~@x1X_eTfrVIoMpTpB zNwauvJn}n8ZWSoRyh|qMIS9k`fY@~&p*OoyV*F&^z0nk`hUgurw=GpdLKc(SG_-VKo9w|*}06aBAUuIF3LsE z%oH-#>T6ZsV;-!LJNX^6`Z#DTgS9F=u?<2TyeR+r*k9sMoF`qdW!n^TWxUTi`!Cwc z7R4|Jk`=a|CfrP!er|zdbhr@I=kVig-7BiTF%!{;oXDdk!s=BP)viZU(t|=$syAU3 zC8z^jo`jvzEF4bC4RirCN_m3lYkFrd50&`@2-XP8WXL9KHpEY{52A~CM4i(CQJR#r zrW+-rmELqe#CyTA;Xa-mY6Br~mGKq}-3{*rDkzs`dL#dzG@Zv@?B5V`d{6+n9kdU$bb6Xkqihnqe9wZLNGtXBuw?~*SKQ}cm%%SlfY;CFM|E`{3BJxp zn)mWol)3*&pS`vjAdR$uT7wp&H|89MSoQAj(0mke%^*xcx-d zKhf#PENsWVQiiQ@EFii&u$cfa32;f9hWQm;Z>pgtz!PjXkL$s=O7g^dRTI;xQ5b=- z4zT8yO~*)r7)&o>obu*5&>)+p%o(8`^0%amX|mo9-mQ0+*2v_zA|{L=Ooci8XL^qu zp9zz$`xQ?a#Ysd;4zm{KPpw*KOAJT3a6WwVHa4IZM0h@YjFkx_R$6e=N4tHuRheE} z5hx!QebOCga}l5}h?n-G*u^#DmDIksRFveY{3ylfJ`>*asH?GSt15bqlN}#}PMk-J z&~ne(P7&EiK~OXrcm{sWLmDhRTz%!wxID*^IWo==OKHlEFH5IemieiT^s1ceGEfKS ztpH||=lULuR4&a35QaC@Kz=f#>asF4CD1+qZT&>c1WBfBcYZ!;b5BDHaF51Za{cih zbUx?Yg99*@gLglM3~uic=w$(uvlMO|SwF zL3T!HGLbIf`a7j}>u(Arilc;XH2|crkKfdBt>JrU@^qZ%6v}EF_*pKg9<7&0?}VSW zqSd5wS&CT;Qm;vKDPy<|7Scp6mF?sN}_Ja1ifkt4o@e-Xm5c`3$#Tg z*H23=%S}{@-wU6Mv+7p2djSd{$VHC$N=?DPI#?A-{bry-F}W%EOcmlxv?+k^u+%0r zk;@CfVrp#?EFfY7juO#jnulZg+D0zGWPIJ#XM%j(g%XQT^CxR)1m=cNcYT$9prA!j zzj3V@$AK|f11Z#Rk?%D)SMzM>>Y8YtncN->An>FT3Y(k*Fo8g>;>y`9CUYn5_e5IT z;|pEkvvuNR6CSfE^$Eai{*^*-+L`|15Mr+M)7ogh}p4J3U`#nlH@9 zvVMt6gDZ})qkT-kr=77Dr%aY3QV*V1mK*qM=6PIk7E9EFAyGXAPpD&NEisAuMrAQJ|nTCB8ci!4qkB0xFjg<=8J<@zZcnu zbE4+LjleJen$Y|w|Tt%sEE0LnXUL8Ta~a+OcawIBC4 z6LS-rXQWbBp5MRI+lj1|Qze={sSDw0!#xlti~vx?nFozsV)c29p~oo2hkemkIP#+6 zg5Y&lF3PNPGOFGbo&tbpoiTuNh2S|H-Onsw9jKolu3$4_cQ&kNHJnb3VoG4GK}0sH zs3Cc2F(se(!oQm)vMg36~ldHp&Yy%Cv{x9!Z*dBx#)RVYF}2yQ8g$LRe3JjU;Xl z{fRsM()>v7|I){RbX^ftEfT-LjK-ENMT+x;g43Hy%WPffkoKGINY$V8BlS2A^L#dF z$6r1B@N4k+gZcCF?A!*(3gwqSDj z4KTzxm=fr9tYkJ4Z*it$h%d(t0bzr|@x*g+N+`ukQ@#Setb#w=M|pw@p(jd_1f?Xx z!g_eh;pMkL@_Ma z6RRWe<|%v;6tdvAJK zuB54j$&y&vojz?W_G()gsK&kH-h%e&(YyU?T%=VJ>Z$~j#nukAT&J9O1bisNxr(_enL|yStu)T5NX@dc z9{~j+aR_e{%gN6np&=ey5F8X|C*Zxe=w2WY7092g60q12TY9P_uV==?enQ(8$;5o$ zliw^o$Q|`)-wZ@L=GPEEUpAhPyjjJ0M3O_sG(6r*Fe|cT%UngnAu~{7BgFYZBJKCF z*&K*zO$0a&&tk4V3v3?CQ!H>txCZS4k9gGbDX)I%BXT3-IR0t8CDONXOGejW`+FSL z1GXwuKua|vJh^1a7#4>X>NBpN;pwg9QkMDnXGv~bX727mye*l7H5^J%4*a}Iu0%gC zWyXGXfIYV2#!bmjS$eR?La#f9ROznhcCHqFVh<2^nyDi6u=SF2M}n_HZ+m-;$=DnR z<#7SCzdxgzkY&F5`SR|m>5{i(tk{`ATe~_rh5)ki8Lc0~l1`M}@@Efj5gt|lGX$1^ zCuQwmk#o_;j!&5NkttD%J(o_4y#WesCZI|yRbm|<=yy%ADXbR$@=5WC2so-B5N*m5 z+i^VfsRLN5+5+F#r&Eo(h87-=7Bk3%hvCZanK=U;fKygxFCj~SANXPm`a#;@f+<`&C7C& z-|-4c%}awHRbY3&*G12j9xMd1J|n*c!Pzz}HaSZh1}jd7JI;IXiP)BWxGUvy^$mDMn< z6hHR~vrYNx+eGGzh%4fos;GxcFnP7gust2t?k3LCWqXMl(*8uQ`jk_|qwx<-8{t6D z^K2(nxiD5{KH<}>c0(n>2D7vqW+J7nYOZ6oorBWiW<@jNXd?^xP`3>U*F-e1l~vm9 zlwYE3bvxo$DNep)h1kiX*HcwcYE0BN2hDyt4sKI`2gmC6`sW#4NStR}!&8cPvhj@j z_>1hT?icwhAH5?pv-{J>a}=h669K=iA#wvx+krW%CHheJ8RyjZw{L#)vGf2-0b+_@ z6QMG_yYw>GJI7lC8Pux(A9D8Fz*;z;j`_WK)!e%u;Ww=m6(14N{CybK5XFwI+wt_8 zKoKxUR@wyeK|mEZ_qSSB-SY|Xv#7JpCQzCMxokY zM1tA5sUUkK8T0NZZ10)LNi@ov9Q>lH+bnXfz+MSJz~%)}G#xrC@eZ!Fyt8T}q$9hG z5+qE^K)69qDU$0oGxv7^gZ&U>@9sAQq!$vNk=-#@%dlbnCPQ}+=o)};P2-JV_~kFE z@%4TnUKp>-c;XD@6Pr1RtLeBwPBVNFmn7r2>|tGxz<*JXiOgpI70q3w-U($%m?|U* zJk=_-m^$;ZlSO3i2}Up3q#+D$OiNYQwj6QU?niLErFP1_SL}fa#`n$*`e-oq@&ZftJ^3650IsND_uZ)Of($+iG9?i{ z4gTC*X_-uRBP3iRh}(XNh1#zN*L+7=O66MIJ|}2vm9zg+6vbo?q@~O?h6l7&^1h!n zeBs$?0blp9Ap_D;VX#mau|Cu_m@CnTdP{E7*1B5`w@sxrSy(`Vs4%LI5&-^Mg2)P} z@p_lr6Nmu&I#UzbE4&XzUXLk``a5r=LNo1BYFwx)zS_(Gm?odNf{%;Dk&?+y4_aLoSnJmR0vV*B(5Nt!tD?KH$Rs$Fg*bu54O};bFsfH>=D}q_oSm>odrs&(RZ>1I2ZbDsik0m~FBZv% zPjCbM+Vi>dj$==A9F?3VLxVp#a?iyQG)RSa_3 z=Sr9etl@ffTiHeKZMCP{EvYPZdpN~jpEd^+Enp#_{0t8}fa~LLdw>`2XP_t&sFLai zp!wz`62b4dokElCAsvpP_;biRKHR|)8yx5L>N%_Wb`wAwQ&?CpOunD0Skw?Ld>S~p zIR5W`IHUC~ZWU~Nle03gum1}f2y#5I(R!(ZjmSAH7O?ez!i}H-C~0>C?$O{~I2um- z#f%#qF$q$ODZEIF-Wx--Q-Ed|s0C}yrcJ&w;TcY+lQyBjL2txIJV>c(?WkK|!Mm6e z-u!X|n62ujM7As%cRl@Q{j?6_Yheu*k8-Y#+LzS7$>2*UeYfy*??6M0rPCjNw|8Kxb-xQ@AT=f;Bu0WsLt<-pepoF6NE znVT!Ig?3FCDWc2%gR_Y>0;EzRsl0~#;V&m^Ngb)jJsgI=EKNl-+?Resx9K8dR%m{k zfB>Y$mo${9xH3+|vYXz6RW|&QVmDcpoL`UMfzJ|v0CIuM?b-V7YhS^pin4p&3!iKk z8(hA}7EW1u&=^m83RHxRSmgBzL{2e=bk9vDkUKrE&x>5NBbj*U0cQV@F<+iWpgvR* zm=e|dCHxFI677Wtr=+@Qz`l!wQnT;^0B>*?8=~Q#Fmt2LSfIpG-jn4Ja`S4%7R(?t z1F_-cm#;IsW&tl-zqd?|O#5(?x<5T#E^n*wb;c99l^cdj9 z1uj_cM}4u22%RGCW9d@={fh(>(s`R{=KeUh_eU^? zLc1w>Xe8uRd<`X9yKar19tG5H1o~EurTYm0l7dR0TtXukE_n{Y;uHUlbnEy|DdI4e ztOJKqQsAD-)J@Jo*0Ce4?g?OIWLr0g;$xrjAD>tRZALw^yk{FfJbkq^`?mjme^IZd ze5%tE8Y1X0b|h_9=42z`ITzC=S`IR-m<}r|5)LuwmVGHU*b!_`n>?a(Flkt^#ICrok4XMQ0gLyxEw& z?YM`2k~mVQ6C zLGqo?z#0D7(M}-f7*GW5(zD}L{nA4qZ3UcR!~|GFpQCifP?g^_^oD)%91bb3 zFjVt>hl%N9$-Mg~Yf1STK|Yog56R!b<9w zZ!7Q-Vkore#9v(o7>G!-iW6=L&)3;0N~fTMD+Jm>c}E4OHy*S&Tei2_a%1LT-p|OeW#kR>U-vQ}pX|g_tRXX3jNjvo1QV{63z7H>K@0 zG3OCzX^_j`6^EFiGY#X8Dw#+m9=y8)s?#1LeF`b3o0qi(#V(=jK#=q_JwlGr3M-fN zwvHGnu$~Vq#ul_^@~+MJ(TCjM8J9kG`g4J;$)H4TqoG+%N!#$se;$Ltt=L|cp}V2^ zck?7-@=gR3Sje8i*hPT!_o-4Y?f3X(Qqa=;M>A@T&XB?{weyULC(~vG!}-iMwg#4l%(2OxM8d6V<)c7W6F@A(NdYE>Fl(Pe~>I?Mg>exantnSgBPAEaW^o^b!R#Qtbma3SB6mZnLeyw;O){N)DRdOmufQ2Th%C~HMALP! z@`pSO4SVw>!r0ZB#b5Z?X6?kq@=pSau6e$AI&WOW|B;tcc9Pj|CDQ27Nay}>O-61w z_HkcKZk&LQ&Bl_FLQ^K|E}pB|=60YixZKRim-{;u+LFQP=dVFZR)BN@u~p&8aAfAVB{GVOym0TCKgB{dCcJO|TX) zLTtY@1~FQ#e^FBe#}lU6o@%eZW`C<&-_obwlQ*MSl26FQt&PLpIonHa#89Aclw`z;`b)#DGOm*v)eCe^`FzB0QKH;t_&y10aR*;cXPi36~Q+UCmd3z-X z$pSj}(Wf?9I@CcdokG+->bO~u4Wjq#vr=wpb@J5fNfO1h>$XH~2yo+pu%=hCaILYV z5l76(WRF{7Lhygqy8IVky`AS%Yi*3q?b)Q}@&Clo^{HFhY?~TL_O!Zb zhC2B8@fs)is&7TT(xov@f+Uk6!7n((_TtSTwLxyH0g-!RMR)br*1%DvQW2=a8;FbG z;M$adCbQFO!8698iK$&449y}mASoBrZo_^$ckXxU_#ZMNgj1yQ$}&3H2*lBZTWTio z>mA;hJF3LRw`uB((tq?|Pu_QHxDL7kT7M;F-23I>?8IAq6(()11y}vxnjKav$qQ;f z7DSV2zO^3m8vS%Y3`n(ceJ|FU1=PR?Gy)!d*sCdi%-ls%sU*3pAB%Gfi81K)yIKhr zSMXA2Y36KYzQCgLOyYqlyJ?@%f4y3u{B4lH&s!Yq0QQ8DL_6ul5nbgT=U9@x3O%t@jD%OApy4uDRJcF3f`h`XMi_-r9r zMBC&BSCxO(942J2NsVXV?rA8Jx%vVd*q$o#H8tkYmz zLg7ZbnfR>_W0|!4MVzy291BEgYZq7}co;JZAsz-iV*z6?lps$%sHqO8^mI_MYUJvl z&-V`0`^a~%e5gq%66kvHh)Da358%y|Y$qan=Pd6SV~{#|)4N52h+#xs+EXPVcQKl7 z)QtY`@qy7T0$tODtP)1&SJ5mKB38;FF{fyJjWVBZ;J#aIad5ora8{TXzq;78Dv0(W z^*ncxnY5$Kn-F^)uzb}Kbl*gm9TYR7oeE)5Fr|%2#XUkqw!_UHKV%=O(M#_^BF)pE zE2C=;AC>uJS4NHxM--M@gXiEYJu{iTA1DTeR?*_wb{YtyeC`F}xvn9>CEz6V#*+xtM2ztU<`4YHuTrW+$LY)iE^H z%sKyaJvW9V0PKI77noU{0=Pg-5JsS=4a99WVgV_}PpzlGAA?(e#0&Wr>s3jJ<+`#g z0Am)$UUA;hma)%z5@^sWFQx`}TS}9+cjZ%`iY{VttApJIp(~C|_wGC>t8S zxBNC>E2a%KovrOu*k=aIs^z1KdAx-64Qc|}wEP*FzEiU%#56Dz&>Z~ z=nADKmNGEQ@arAPuG4dIIc{az#+RUIrEn-EE-#Z%kJf-XU+=DkoX1EjTKGjZbF+7mq&3SoWhbfuw!#f*MmeOmcP}w%1I~hw4>29F zQLKR!3XjSbHVjt3ZleqPe|_qYEeQ>GX9F|+r*&cBTs)c51;MW91}%448_u127c2>a z`4=z*k_+CM%JGU7+V090o&5aFAn%v_Er}De>@xo;t=md6dKVW<@qyHXW!D4Ty;RZX_BqNwF;A=?l>U`miCw14TRn_(v zRuMM@oPb7(20dXz_k8e^a|Nw%CQ#Ct$cJJ>jPn#S3q#qqA%u1>HOlM7jM4%N?qt+L zA&CuPyxMea@G*t0waw(%qOEw7w#I@~?hg*)^lM3_?iC&5i^DzR=QD-?dDkNK=JX zkSm5B9lBV!f78Ni2;x!8^X3Q5&uzLuF+m#ZBf)jYkO`Zf>T=zdo~kDbnYGt^$exWp z5*=HZnK{8`OTk|>o{ZlVWKYwted619VPxJPP}}}p?X_&{6jDsyn=Eu8c)OUTGBN$4 zZw`f(X@2yGf@$}b?+XamM4E28be6AHji-W0gYd3O7(gEyeDlXYud#Yc-0mu!_(cGo zje$=X2%WMG8PF{d~F(4EjdXd-V{5BHthF*<@(K@NXck>iEAGkFKVGOndfl9Hg zU-^IcYxK)#hBOYhn6@X`EQ?FIQjuMDBck0#kvCy4Fmt}AA~z0KxL1zx5R=H?;@*(X z$_xqD$osF*8l{BWM{CF(59-sd0J!@G-!fR-q_=gZvw17XAK)3=oM6k%3oS7HfJz z^$gxCwI9J~k;yP3k(Ko3+;R#ABM`iPohRUowvoHuGE8FmPRLi#7lS407-eJew|!LF zq&Gaf(XBxxa65hs72=2Sl`K4@sXc;DWM{&0y>QRr3~Hb7GvdDrWssp_7@2K_v>x|h`aL= zB?zpCnAUJY@%D>F&ztyFVr=JrWv{eSmbjvhow=IGzYNec2A5S#<+zPKQlYa9P^pGa zP`0-2{n)aIPPP$Xiq;)e2=t)sp2 zttk=&4BpYGjI82E(;=f-az9Zp%}DkJtLTh5N=@zCdi1a;vh$yO6s7mV&duxI+;TR& z6wC9n5J(kw>e%N4I_BhIM?&TQd;ZkLzbaP;sJ08d5@{q@)zaZd3C^1z^nB=!WpF*E zqJ`e|OXA>l#W|s4L~y;)$CIIK{BrsKTE{_ULqZo%zL|3~ z>~74z;74OJ^z&0G1o!&$GUHN)#6|YiPz#=s*vdKvdeSK7v6<<5Slg@St&Z*0mc8dygNVilqPL;=KT8l3oP*> zyiyXYtr67v$VI4IG5jHQlsypYGKO`((nAcI?5{*hWmH8g zvz;-_mfrBz=%SR)7vlSKrJB`#Mo=sl&!#p`^kQ2^dL0H>R__pa#}#C6j`^hz;NjlWJ7{+AkiHQJxz<9%TJA_Q5|fE1N(yoWei=zZ{6Gt1OZQix0cdOm$hwL72oCf zIGogRt}`V*8b&Ta3E)&l1tKq@uNj=4M}rvwJYwPrRx;6wx=ES>QzuxQ&5i4Rwq)yEN*dNX;FvZJoIFD_s>)M`#dwv}M$Ffx#g=j6r>)=QZ*pel_v68+?ned9c zk0Di_VrwMg@nElUUB_vryHB6dT|mCBRM&~rswq~|uY(1|P_7;fI*yUD?ZkGft+$fgY{|}9BKq2%MQ7cVYeR*#b({^)ziwL9Vs=;Gm6MRxsDQ_CI6DQB0?>w@{zriQtHo417hp!K3n>IG2(bR+u zr=dAk4*)fsw{h}9<%NekXfjRHbCWwmE%f`b`Cv(v)vjmn;9$%CMF z8NE-@6)is1g9y<%LTR!`yvmhKl;{wXL6M(nBX0=| zB^cqGBHN-pK}}RQ$3%M;2k*trS5O$&O^OkU_^`tgVK;JtB(Sl-_2gy1ru+?ZL+(|? zXxcSSp=)aD!@JlSCqQfJPlFVP?L`u+cRijRGv6$8 zVY_R)T@J!QbwnNwS7kY zDu4=WO9Gb`PxLy{ok+ZN$orM;^35J&$D$%kkuKRhhDKQ-b}A6=wS$zWOXYuOhM|WA z*rzGB2XPd2Bge3;01}H$DB_S@KBLgk5%NKcxD0(`dfNBz+_`d%LERFk=%ymb+S3>I z46hSE*rwz6X2ZllntVI1VkD6cQOpf9u5i_KkHWES(afM{9O3NhJk)^is)g$Yx$l^7 z&W6CLLfWv4#cyZ#O$rt~kF8wL!DgYXqMj(^ORtcV?KzG_;-|nqO{#|xgaj7;Oi*yd zf-iEk*A1xu-0`q&X+_RC&K;&hXP=+UUM|g`CB%b=&uHx<-xcB(2fptVs>O8&VC7a> z7xKuGVZ**mCV6i`B&^m^cgN|yh-N!dSX$+8vIqkU(%#1235-*?+4L6g^3Yc>H4CI% zNbW=aP_zqM*dHSU|D75Y@JCO=h}%{(z2}%c@H;}~UF2(caP+P^=)iEc!yn3tH|lni z)?bM0i*ghOowwHXFdLMWZ;(xQ0%ajeBq$gcYaSlVjNQkHVitbvjCRM=YDwFOX5oi2 z_MzBst5Aa;BtU`IybvOb%CNZ8UXr@hhi=@NZ4BAr z2Jld9CaGMT4jz0t3JA0HnN5Fe5&#wL-TFO23@}@gbE_fHAf{pDv z?q!G5N4|L-jewrvq3c{-816CLLYL}F9}uoMiRCt)1V1hM-~f$@S^SQ?;^xiv|@E)^i(a~%D&Zb zPXzy>+3D^aWh#r>9~<|cp}TytzzerJatDs9 z_H0RKLMp+bk!w{zw^{{=TO9X+3S(^zSqdAAv?pFN3*E@u1_*%i)be2cR)6 zptMbg6b)#j+}?hQQKl?lq#YgH5OSVl(g`3EX7~eZps%<%drFDSZuh% zx+4O;!-iVC$Yd%Zwbte;pcZPitMuH0Z;tynMb3`v@EF72ATurxdO!Yj7s6_ntQxqY zkXSB6--yzc7Is^ou3!W@9HrXGY4F(-c2bo2--5-mR8aYD`e9;iBfMi9O3ymS0SsOH z!i2@5(tnIOIi9`Ja1a;xXF63J#(nQOi_l=>&wPtg_h18tp4VX^qj)vvEV)A?Swgz_ zb^L&E?~(SSWtdrN3-0Yp)cE!&%d3v5mlkPjGhfXv*+S_ z%V>l3Dtp`+Jn#U9no%2^*W9WsBF8V*`b{$X37RJ^=CV{BM{U4@*AFIiQ*~)HV~RAc z8>GMZk8GJi@z9NAqGKsbf=9tX#`-CrJq`$RIx$CvQ)$A4QcEw(s);=NzjmK-CH$@y zpI3N7z4Z!$)TCVcJ)kSSiK^BWo)Mr6OLYXJ_UB?sn0wH`CD z_D3CRDI2O;F?=4I^Cc)wk4*ddinC$i{p z=-adn9{ViS9BQ)ls-_cT4BR4H&EIK>ZhGJbNSSKr_is9%4S<+6kT~ma)jF`b3G6O! zW=0w=3-yN3EM|OV(r^!<3hJ_>qxrf683_Cr*<-c8%FyH&%1GdEP#JR%rD9Fxx4ng# zm{w{4)A$)R?MhyfQi3>bz*MMSY+V_P90K*>W^0!(VNKN3y6h-o5-sEH#Gk2X-U$E~ zPN$@jquMyD9@bq6rT@5(b2l_A)9_tr%)@=G4)0VDBYVWxDuWf_9&bW{H`-U@aRKMj zM6dGcUh(78*OCaebb8#iyZc*hxm=o9*F*s_cI2ZM1(F!LfBF^d{9Uzz~+yGzKD=0(8WLY-Zf= zCT0R6_R1}oHfxFCQ#ok&J1=UA-Hqxwr7c8M(q_9(?ff&cJq>n=R1VDitQ--f3Q5I9&*lajTgNafm0Tn$(U3` z_o^W3%=(L0H*2-%#~pLPyQ2Z{zq z=*_A_a%u_I^GDs|EIdtnf>ijg`l!pTB)K_CLl0U*}v24Hf9Z^XhU{CVmB>;^*PAJdowzCLmq^F!nljgs>61 z>6k;HIFCw8;GaQBEgtjmJJRSo4^sJeD< zXScM9O}A`U|H`cqx(t0}LQK9fA{QaJ^xi*hfge>xm)shrv+O-}6sWuf zOJ>JTg{n1{B^s*`MNbOyo|_bkMPy?1$eI$_PN|mk9F%$9`%Z_i#c8`2ujHw-ROd%4 z@9Zdo`A@?T8vZ2x|D7s75s;cuN&yAhnB)9x@L#(iB0*LWe^K`K_MY6K z*9eG-$${a6H$<%9l1=r7>=l*lPj9 zsy}UWrMR!`6s&v?9xsHeQ7d`TM9za;{Nx zUx;3EabiUDDSHFjn!I^1{sm-N(}>KfUPN>x8M8^^OyBV|WOt=V#1=dSEM`l4-3F(= z-6UhNQ!>NQ0W_H^qaqPw7xz%Kg4^doN2e9E;+f5pMhN1J=87`C+7(`%S8~r%(Xhx1 zuoZ9YjuoJuSN{9CJ8DLRD6}@9s#tmqUWgNs(hfy^)W|stkIjd?!lr%mxlHQmQP@8y zevXLOHbQ+i(MV6rLB1jmq zah+jvIa9O)6E34ll8lU^`C#!3a5+GHg0!e7U9#YwDxJN{d8zmD9{0rp4mh;Jn?Z)2 zUNj0+F$SxEz&KDBf=UOS5B6uxn2Yhz`ed=1t*kovZT{MFY^L|0`o`fex*Up=MzwR6| zJ_gn)8xg2$U9p0}LTGw%bcgu&lPY_6t*T+b6DwpU98dJE9e__3lcQi7hP_`^~g*#;u?sYSps_czAIyJ za^B({1)haHov|5lU+Lx2DqTZ*@sdSpSwYpCs?#MlnA{*Cv~mLClp;*ntsooJEN{3A zuxaJT>1a++vI+|C@)A4_U{rBs*C3=u=cA~ZPFaw%Aom5eUmvjqo?85~vtJ)7CJ7YD z;7}+<5rCH2l>5-(QX^fr@iTLdCn_8^(l~%F?327FFkgk8f1x2-8UuA|b`p8}i9M&X zcpoy9s%GTEnxz~J;ei|qToDf_&HKka)adqW41Azhi4DcK`&e?@yMxyVLd_s>1=9%XNBKN+?d}l7iUdzCkv1x1vDLkm)k-`Qn+vSK@Z>$6fRh+0PSFT5l1+N5OS+2u7T7 zBeZmkM>TRR z5x1MQ&QUw{fz4+^R8cOXGb)eY5XN;IhSTJ71^USFWsC`?B_;#zyz>%~EChVy9B(jA z6jMJ?2jTmv;7N8G9z?>qR7oHAAaD99dLk4C6a%*^N4G8z*h?I;g#vxcgUVa)gRSqT z|Fgxg(<@qA_yq=fYeDQxqy;f6SZ|leK7m#5Rna;KC+c?@u^Kdo*2%<7AKHb^JK$yo z%4O;Kn$U*#|Kp-O*nZ=IC@1p+v36BpI=Z4j4Xn6*Jz7KI)U@FJ`kHt-vEcTJ_`vM{ zWr3Ua1)YgOVDNWjXvHWYAQgjdU3~LeoW5=e(%vnfq0Pi1T#>2>;F8bqD_~CMkjAq~ z^KS*es0a{PJ&}wYtT#%@m}Np$HanS{za>2fJHjcjQW8_;saWCXD`eYVU6t!k+9$_D z$C_pA@XXw@!jmxP-9kXbURi^a9>7f)mfuz{W9}Em=Kn~GnpgIUgdp{db`4S6lC{}C zbD6+Q?GJqk8pnu=cEzHn0N`dEMuid@n6`LXp+g~!%l8f24;k8LY%?qC4ipi3FIVms zKT!&dIwV{^R}YKRLt2wBYXuVf!n?D!U!@W}bWhm-$fAEj_?z#U!5U9{NCu@(_~;pu zn6*cs__1p~-O@)CXSuTorrueU@x#<_tc^nMT+gc;+D>jV(E8_8c&iFxO8nr*0FmcZ zDW9!?Q;$FpYH8?D`}WV1Av~YQ_$0?jzh5IrAa=j^F+#JnF_r0c#Ac`4SiuR~febK1 z#4R%GIdn(9e>T%Hxm!b3>^CEH#G&xvB7D}_UhLO#jj-8mB${g=lnsz>)_4*h7z*j( zr@y$j`jJ)IUZK_4*ueOl!1+P09eyxg0e}%d*r!qn-;%fkL&_3BY8B9e;Fd0qDl@@u z!wokmUe_%kSjr>y;NJ)|(Ihi*|G&`9LuM?LNEkUW&&+lFka0T`nJwFlQ8<1KQvCdw zjHIEA+9@?5!5STm+* zPls6tRFal)I$?p>@DukNeQ)JQaw4!DcV&=uTZuvF*RBJvxR!c1VX)z%|7(=YMW1qe zt_=1p1PCHi5Bx%wk;K8F54Z=H&AXzC%j+}E@9@jNKi$;B4-#J48Pxz9!QEJn-MF{t zg$#dmS1VL+2}}B}L2q|NI752g(p;*q<1xG5D|UYx;$2Ms^9kI<1gjG-A2&|`>jT&V zlTwrRp@UMX)R5vJxc_BcQWZ4V>p79CUcc7l3cKBiuv@Z%Vh;Dlj;$5U6J75?ZBqP? z0&(1acq{K2vQ(6)DA2V#JRnae6o36Y-zcPp%!F!-0)acu5OVa;^AmkLkn4YTLf5_q z8W1DxEBqgw*6S;?Jiu!R7ydY@7?nkb6VwD&L}rJJ|EB0D+bPrh;v-d?qo1+Ji9J)f z63-1j1wknr9(g21QHD=AYTsnZ11POWh8Sg9l|HccEu~PyPL z=l@yKDj_KRH&I3qTtGboi?Rk=EAm?76w-m7Y?8)Y;YmBgF4h%TVHV`Q%J&I9ur zc7i%7{iWxAH=kuFZOR)`o{@kq5yD%nxWRiF0xnt+Z-r&HmIcj|nXXk}z6iv4>5F+k z!tf28=fD!MTf*@KUsvA;a#utkY?}<{;-&w_B05fHeECH-bGakUu-g)5@RUa^daPb+B3fydO%17K>SVDRXG zv`{juBlixXb_L>(w7~2r8Pb_Qt_5|f%rKt4w?PvY3P`Dk9(m|%$V8ldgD3##2aFTs ziMBrr9gvjeNMl@BtM^7_ipR+eSSY(z+JoO=up|}a^#UG%ooAV+%$iYn9NMPI3es1~ zr_d8s1Mt280L#j>cS06H9r?#$*<5Afg7BL&$%`LCcu^pj|H1EhmluopZ20Il@;r*OT7? z8lXeNzhfWilFB_-dk=hRYb>({MhQ|d=(Z0KaDH1NY;t}IA1KtiR@%a<&5Sx*A{ILi z5YxDZXT&H#wA=7tNP+8C-=LRX3zQyHuCXf2bs1km@&HUfQg`HpEW-s?@#VC?4 z9l2=kr(Pnesa4wii`r__Rupsix+6W>AspUMhh+^P^dBe?BKA`Yb{ibR2$ne%(;_J2 zuqguexrR0uwlLHxqiNW+72myyKr>Hni^ZK(;J4X zMybRIl;jgx`7HzOSK5$8o*X{T3kqU>bn4QBTO-SQS_MgR>Syz{QDeB9LkvTyfFk}F zrIQcXD_{=tmVD?!+}2`(vW z-gEYb>PC@4Z(EyqL48Qsfs{Mb0Eh17gs0xZVxg%NpfkG&qE2EH!Zas zdpd@dxs|Cu^NQRSobRXXQA(Mqzkw`=0XT0VR2kNrTZN=SuJW(;HyN`b`24puy;tsD(Sted?|%p97Rh5hoNh7D@g@`tU?|amAS*2$ zik+bs0_yPUvMeg6soYR;O;X2Kz-Nyx_&Sg^2&#aTa!)5#fA-Z4Ilc*cg7T*g} zx2jkPNe5B~?I4g-=__9&V0!q%um^NH-9b#5+EU(7$1(~Qj+>{HzhNkJ#Jw4IS%2fi z&dvP;tQi(PziHD`MY{e@lc|g6UY?X_VkL_EPU?d?z%-p%nheso&J^vtx?w$G=na!o z-MtBve3;DJqz(L8iHq9_s5303^2rmd+H*Q5L-mDs$Jj3r$T&UYN-b;_$s}H%tbtG< z#p*n1avn406Oscjcv)lQcO61WUm-zdrj5|N9a4HL?>kMCn#ypaDu$0H)(5caL$gxDhh? z3aT1lfrm(Ev_+BUiqV4WZAQ_#N~xow9v%tu(Fj1R123?@g6gEg?Fmbq=2E9N0YGR0ix#9oAs6Q`%xoU^St|`kVWIMztCLCqG*An+mbBTrGYIv z0erKN0^(GodEi9e##E|Mq3xnnvKGqOlhKk^^moilBc8-4iVN8F@`wBqG?7F6c9HIh zt40>~1}Kmo(0iW30~cvh(&zhY59Ck)*pB(>>N_gUCkF)p^!;OOQ~8u46^SPS3p!~U z>{gnKJE?3Ho}^8+m4hJmXKObn$w0IeJ3eQy`S-R#@1@2sXO!)0Ilj`6(zp5z7uhzx z>qyhrQ|)#pHe5k8?nRdNU7Cgwl#1(*@eN})FIngKS%{QtkKdmK?8(eKU*SmWXX65l z&~~d4OEn~rd5Nv~V9zwKSn;RB+j~5z;Q^Ahz>a*LUI`74s1x&j!2XP1(PRH5Tjf2{ zOq>V08L15i_RwJ$Ty>B?-12w+a!$%>5$w=F5_qQL%XTEkawI?1*Ei+Jcru$asuFmD zjw7yVRXCSZ0>s6sfO_|2_YKT)^Gm0r%}U$-m0JX!~tKy+?(bEQwPh?8R-5q+uK zG#OI~?2@G)r+#dmTo6RQqd1)1>8YZ&1|Qz-6SulkD?Nx|epWa7TO=cAp&}-`419Xe zbpUKmS6gZxj7HbrEFQ`dx6N^x@Oco(Sj9bs={3x2DcS+mzs#*m9JiD-MxaFGuv;ef zbCi8P^WjNZtNemAX7qZ`T96Dd5!|zhf#sH)okL_*XuIWDy2D3UQ{OTO8dXz&&59I_ zF%`iLB!Dq8bu>aA1!qL_Tb#?!r1Gixu@RL2T(k57 z!~w;o!l$t$|2AC@SXwpIOcX}(Y~s2#0liGF_nMN#^P+du_Cm@}3AUN7{#@ZZFxZfM zVt9OysEt^io8H+pi}&5*19R#~Olt02{*6NDngYAo^38*B zgHqme;)NdHZr#Tk^3E|$tgPjD{^_Es*TZQE0UOO}HTh@;~;=;AT*;5b_wu1G< z+Z(55unlqsXFjpv06EL-5T_&oyX(dBPB#ZGUzYJ;f$ej{`uzj~||C zL3k{tk+n6@%IJ2BB#q1dMYrD7mu2mkPBDdb?;oIGGqb61<7cr1pPK}PE3|{JPevF{ zt*Zk47pg^osc~3JjFW(o`HNv%;m&e5AGx$#L3tT`b7D#4U!r-M2^G8tUsiB@ztL`S z;qX~3t9OpHpAr&arV@>h=UGcYwj(Ks@P~Ys(vRlhMVQ2Yd)8jxH?zW(i+VrP*CO|x zSZiJMzGa#SZG|}7ndE#P6@2mUFIbPL9xLV{IqagTdJpU*{O=*5C14te0CEYu4YT{X zn*vzMoCUBE^Yh@udX^w?Sl*1p^74q@Q?IC%?8Pj-s7~BA5SJ4Cd5B9k)=_2k7EuR8 zyh3UopC1LlGMb_!R42*J-yKsacKRJfKM6Q8a{;A=ioI+H6_i#{HMG71AZ*J!s8IrC zFr7i$=%GhY1n9D^3Rnzv%T0IaS46?xkept%hh(i8``MJ!3l$$CLRYlM<^z{o1{H}vGGk(`*V4 z2vLQtRjnG2?Nr6 zWWqm7$?$Eq%K~EpAl#7FYnre4eq-U_M@#Vz^;JFYAo#OxjAY}O7_q(jNHDyGB4R!b zIYseA1{_l2D&JoO^ngnZK1=B;Pjdx_=4joP@9uYl_Glv(q#;#}gv#@%-SN*?qp>mP zz7bVv51V>(#K~#0K0dN~0hq780KMs$=a`=iEh>fy=4fiZtGS$BrhWSFDugY{38=FU zJvr5dnQ0t0+k#~GRL91XL@-++iM&`P)b0))9w=sfz?ymr4zT63%i9s;JD3+H!o|e= zy#FE})!ff5WM}+@9SmrRK`hr&Y@(ud*Ifw{ze8>2^VfEg+fj0uaKG@QXz!rZ`VLx6QPp&t zTkdgactbr^MO5s0@eG?S<7%F^E_eO64RjFB?<-Wt~d(;l~D9F3I3${)zhFV3{@()X#paK0F`Oo9lC?Oen60Y1(SNHqgr3z>(RGQh3 zNL%1lA^Sfo>4-l6$29~Px`Dx>MUTH{wS&`EIpd>k9x#m@}{S);Lo=b8@!C)eg(c$%Rv)=;wzD^XI0^?37ExdY0VKpIOfGV zm$lU4skR6ncGw=!{=E|5waZX^O^;Qqa?kUGNObUY%2|bRpUcff=Q7G_7^sv9WP)~l zqwnQlN_-B<8+!8eTjTlN#5KsXV75n*ssc%#qXJjRlJ*cKT->o;yYA;(2Fxbu@>;(+ zi>O;*u4W&xX2v5x&S)10)1opPN!9^1;^;(9x6Fpr59saSBAiVjJm2M1xb-{x--Qu` zck)~BOXjuKA*Cce-KQV)PH?9Ra*kF5Z>o-2Z3Tkhy*JI|E`X)X2voT zJ|0zB#XtB*_HSd|-?C0UH>w(OEb4e`wi$xqYvb@I7#_#acEg|M$9>{gTbS&L?-Lu-7Z-54 zY+PwaWL$db;p-BK#&BK?b zh%)Gv1pbdtsdqI#8)T3ZEx^1O|12*Uw17aoU<;$-)j`m%OfWkbY9L!R+n_!@RL>DC zRnv7}0#luSsKcmhT&}nPHK7ZrI6&-M`Qwl1#@^azFUnE0S`hhyJctcL zuVqN9vQHEku2?Fbvn_zJy@Mr-XoJa%R>#u{M6PJs16=-SW_}a=?E14@fh=XhU?wM8g zt8CF_Yj}$k6Iug0v~_OQGz(W?*jZ3b(XavB(3n)`sZ#uJ;G)rn_Iq9bEZhTro}xsX zlq5s&%a@!xv7(L;qf4ZQkGI~k1v&E?sHp(H4`W%}_Qv<7Ci~9tIVUD_<4@HfYD9lX zq}EX}!JLXBG}?$*ac4?tLJ1hfnd$LVtCT&Vk9}5-)!(jpBEXr*z0yQ=0^N9eeR)=1 z#bo> zG7Y&039$8wy8X1r8(yu{d7OS*+vpWXjZ1RR5|6t$g^_%hL@qDs`%h>6iM|IM4q5Z#E!J)FuFOqbE;4{}Rw-+vH%WJA-gk?>r7o zgHH5Ec4^33QtkN}@p$#ug#-~Z%0C^_7TPCEdW$?Vf-DG2|HTzq8^_sLyZpdCdPhuT z5#0n0=#G1MTX0hhk9fZ(chtrn-rv8 zIs(|K!!5FVpc*-$-w1JxR!@5GpJtTOM?T~ng(?M(PQyD(Yq(U!o#JQ9=7RV7ayA$G zOaVbFD4&Bd$nZ{RprU&^{AY|f`qwHlH)4Kyt~Kt6YKOCx;;-ttQv!~7)~Cq&!bwGT zYr_(woEmrgsrL=Q?_rI;KCKelOD#V9k)jU59XYz;hUW-?>uhLf4NGY`#!c$94<2ki zJc4}}-nessVWfhPsxIvTQ{etI|4@INwl6-4y5mTpAtzkq2{zxWH}8UU7zIJxEs#uC?e9^hm_$p4TQvJcy3mX zuS6w4JiShK+$%A=kcC~Kh5r)jT)1yN-*R>$JQ*N!)R(}9V-Q3T+_u`L&QxJYV#-Q9 zw)rviMa4ww=-lvGK;q=!$kj^3>d+wQY>{<+EeTp0NhKL3Y2#oC9@ywEulBCngL^Zv z)Gp)4n$I8gv;?TLN7zN*X=3N!A)4>?=gR3Gd|ktuuxuO2AgYt7E`5*JJe7zCw$&Es zO2hlXOuW*?;Q?;upb)0e0i!0x1j)z+7Th)#WBFjWTGcwSK>+Waz6XqMCS%Z74U`6V zWrS~83zX9Av-_wX9E3l{D*(w))`;>BTFWDQA19)$wDV@6QLu4kYd*wHD$#hIu?;9b zXl?=4wIp^HETSm*FA`iZHzR;AiZ&2+Fj;>5O+DALh<0-!SSxtkjtk)Mtbfk35Y%6y zA5Xk+x=~HxHRn;IU3jQ4$yR4O!v`Z>*p)anfk8Q@Y3JvPLL^nOpdx1kD9tGK==7(q z1avuFg^PR)Gu=I!jL1j6hENSNdkF9l)ccXmI8kUBFY&e z#HCJyupkDZo_1z8X*cxfm;&gS&rgE14BY9K3<=sA=7P4iV!dS^Vqd5v&*sGAu~l;d z)q{bWGecafy^(&Q225G~aRX3=W)%IWvdGC1GgruT(jTY2+Q{AgI8BkJa8ct8A8;V^lsAkX98mYgv0Gr&}gGC;$ zn@@xK!rodV*h{lm)l$KLcR5o$-CGXk9m=k)loDSlNSc9Txo{KkAao1!aUB=5__iC8 znS67|FqF)s%f^IeXph^W6+ioOd+F??+$%YaBvKKD@*FPx!8-sYdkAr7qvfVgZK(o( zLwPl9hl8m>Fq#$z?2w(`ny}h)(tM1Xgm4BY^wKF=bEHJZU3Z^Uvkv<|vIoO=CDia( z@;5!;2_|#iS^rdnhA_|;6>O)h5~?1bZo{KvYih{ePmlZobvQRX1?6FMW``3nCS_QW zmcv$TfF(qGK!hri8)0V$N?zy*1<%g_Y8}^d2mD!popkZ}wLg!?xv%aWhF&cJ$AR5@ z&%4)KG2J_n7(;u{h?}Wk&XNut3Z%BHjbZgIspH4UL&vm+mlP2kaP1f&w2vJ;dxZ8s z8?Ez3xc~!=zhZi$&($A+{9Vtb9j{(+KBdNSt=4)b=$p0mLH{3Xf%3}^TY&w|Lh6Wg z90YlQpH>hhQ1&`t8ptBw%u-BsW zN}8KLSg$p0-;3CENk`jLeql*f^~bT5n^&dE^q*H_JRAy6rkBK8qR-!5ebXSM@a4+0 zu!+lM^G2e}gE2*I{eaFLtF>3(^Mw@O5^*veiH~il7ECpEduIS5%9+jy@RPC~v3J2Z zTt?f$8wM=oYUZUfbzU(PWolgV`SigTk0WE_;f(ZzU#*fq;|lY|DT%f4#1U+X2@R#5 zH;FAH4sYFfPPQbQsI=vZ<1@=mVE|W)F1tQO%StncbZy)etKEfxZQNx;Xv0dCa-z%r zjS4GX$JQ;FEB2jU9%*c$tvo?Sm2P~yK;=R>#a`2G&=({EX%@6pyy!oDN8xjvXR7av z6_7cm=%VEAxzHvDNXOH45rU}f5wFM>&{~jnxjWORnDfSZ)=)2Y9*qFREhqJpJgG}q zSON{h^lF$KdaY~goJX~Hp^@`!%cA)?)zt3~1tJjR>QDzW`zSoVjr@X9b4AgXFN2Nu z;I<5@sch3;8e9;RbG87AGc*O4*_)}`sZcA@G1_(=l)5_X?QS#Jq+ojy6B$*Q ze%O?`W~kc6FH>Gdv_>CuYfrHdYHw-c!z^D-tZfZj8@lvl&6^`^M%kp%STi1@W=!+p zs`~>C*Yc)O&Wvg-(I&v*jW)`@+pIS4XT9H5sk4T&BddO=q%UBezPP`3qy217H3SVH zAH*3tQrFvQp{Q|2z*X77%b=;U$WO2-QK2Vtq0S_@NW|?W)k&MdnjCwQ3)%6f1~5s3Y{(+l{}#Fo4Sdv`gd9jWKjHEO0y%=RuB{~CDhqR_wP1ag(@X>NS-p_(-hiAB~n*p51Q}+_#6M^Yh31$Vr=N?e}(WRpyXf zw(vZjm)29%V(YYAO!gE~uV%%vYn(=%j1l_!&OD>BG(_+d4rMpWgn(`p)j`#F1(|U* zOhdiafR&i$2P>`0@4W10dFTHu*DZ&u-;6~FxNBjSqFLpG{iH}Fz!tD{?cAEUBe>Qj z2#ecomXrr{<;*T7qJ<R0X=RuU<}dId7??QEopif zOMSsJFb6q?ML(~G}tr`!J zWm?A=xY}F6vF8T~dQ>N?`EX+0J(SJBghJdQOH)!~I!qpUe6^CW_V9y^$!`3XqF4PD zwaNX*mdb{zblVE9LRS8~_mqR~>g7*`dSrJApIn;!Cz-A|p%!5*FNio44EcW)QbE4q zc~ECpL4OO00REwe_kq>pP8q`-(vJA(PeEk`KO;)(7b-66^8Z8*9+*P&h?$f5Y9tQt z#uOv+gYao_dYy9e-;Rc6R(; zPAwAZ@q&H$5lXFxv3AS83Q6^OomtieRhbL|TSBV%&q~Ud_4F7a(G0Jx+cauxxLMx> z=YSu$3WKK)3`v9H$|f`01J*|l3~{zif)X9iyk-n;F_8dsbvh%&gAwWMvWR!6Xf_v2 zu2D}%LpS&IU_Rt=z7x3H_tPvkh?=JhIKX--;BI|h(LEG!TNU^+Z zN@SD9$v|jAXwma>2`Z%8N!zW9!lY^qd}i$b>xX50-2(vK*iS@u+J3v z*~*`vt)ihw?lbI0<#*Xx{_LmvVS8KIiAgDn04OuodG^Rg zEOW-(q5h9-LI3s%uroreASi1|ZKoRNl-Hug#y$W_n8cdLZH>!3uJv^gDSCY>8wEM- zaHf$I-<~yD0A6+p;6WM*ixe9`+z~1**>x?P&L%n5a2Wg`kw`<^apYejV|?D_=1ZV* z8#HX(rBMo&Zh}>+Dy4`n5EZ>QZ>i*Qf42w81PS&oFIA<*>SYw2rMcMV4yII8E3VK6 zwA2^kQRq?TwgYA4o&P3GnSklda2_{9T>CRcv6G*TrVzF_G@;5Mto3;l!Ge_??ft4p zo! zV3zs=1YD;HJ`P`_BHW$nC92b$>3&4SdnU6(p&I~HOgMJS2lhC=Wkg4t(>lSHpwEaU zPpOyb@UrJgTBR*SBWT5*4oV9)eV0o`vJkBjm7%Z(ONCoQRisPAbR(!ibhYr%W*FDh zhLb!V)d@GqQVv?sN(#XU)!zmLsRe>0sT&-N?XVf)P_qS_vcC~h&CX?WsT{e5`_?$- z@nX@x>2f1_4e7v_P4)P|%%|lz9U6el#MQ$abKrFRTMPdDF4xC;z7h4)nT(%QcA9A>N8!%dB2p$n{a@T$j?9?;3DgSTSMRZ^<$k^{=3;{@ zjX&s9BBK6q&LHj$r`pm>nbp?@fJqIs^nZdUo2r=1)-a>D)cFzh5Uzv6WTdMgtoTN) zoy)nyjy&ZGtLgJPI$2TczNr#f9#t(W?=Zspu&4%$a}40;e;^1oHb7|8RJ$pywOziB zk|#*!gJ{(69KXW;5o4^t%YnNI9=Dt=Pw0WQzX{d?>+a-=AZ8ME%Xx1*Wqe}Ev^OfI zo7E+P7BdU3O z(2>e=8Xbu0r{i-F7%SC9uTUfHOkEgxuE#45RRjD64naXp(SF2neWz-W6qLq90v>xX z>bWj*xK)vJV~N_wghZl9c^=ynI?%?b(n^F;ykU5oO{1w6KwSzz`+8w0nK&6q^2F#^ zQPx(5oK!G(5L2q#h(Y8ki1W?Elmg9G8GTk!1F?;uu+s>)MWyN!R!{6DRr4hvqIy~z zj9~F;7G7zcQ#=sC_m#hxJmusupAcz0v|Sl)mxyL_%QZ@$I6&#v-3s^CuW$s zj4uL(JUNt#OXy9ez2i2zh*lknIeSEi>8`rjr}br?TgE;ob&L@CvZmrMvI=8;*`PI4D;)Q&Y)E z@uE23nZnSBTjzO64)yIKWg~G^Ko=OW4)e3cGAQ;+CP_^n6Nr0)@G#fVCLNW0EQK47 zFr)-hG(FC-nh)$`$~0+74FT^BSFY?#vhr=XExCEJ82Te}>X+fz<8zUlGhr`D$g)GD5!qoX@iX{xm@M~FTF&svJ<^igb( zkh;1H-GolOI{N2P9BOyzt^$b7Ced7SHfbanM+tPsLVP_1;f#_yB|a+df(t%#+n0Zll<@5)VW4(TB<7E2j#=t?{i za{aY2lF%y(Jo-d&I1Q-9xXrv@%YanbRPL8j{AnNCHY~)bgYR1QMm){Jr(A_@a%Vo9 z-*lpxgZEQREW6Ofb1jgzfIWz`PX_SLWeC2!vh($Ok2fuUR)rRBtI<&4)dfslbkH@& ztWEIOesbkz`=JQM{x-=VZ|7>~9dF9Q#6S>*A4*<7`ZLEZ^l92nc(i;WK>;w6+$-ao z)SdLhBp7GT^#GR|m3(uDtzPU8@Ujv74${^_RUy^6Y~V5{;&Sd`khTxG_*w0Elr5&C zoe3S2=4wODO*zx0gJM1ov?VWbwwN8?IUr?wQmqh^Qb@dnLw zZCw`7{#LxSGwDAgc(6-rTXbw@C{D7Eb_7dzNs{ z%s9%8_*BYjg*3~B&t~k-oWWlM{@TB=zKcYc>@=B|X)9%x-{h8-Tn!+4HNbUo(s|9S zAepW_1#sRXST{SA1y~(6D1~i)=IM?D*53dpK-j;@uMfLzXvYZjw6uSXv~e-Jr^p+A zV;SyrKvY3C7r}P@%~Dm+Z;vXL-kQUe?DgQrovsCk*o3wiYvGw{vano)`G`pY|+|^!H7ar#V;K!*M9j#9OZLYF#`rhH4a;B zn1e-xHLjyqGf7d_^Ut^UI&}Bf0GxrS_`>YUcvkmx)VYXbe}ISh3?>#oXz#x#j}MogqT_^-WnUj$MVJHo8g!P56VSA_Xk26D><{jp};X=ruhN z3UUCNFW6dUN~(u$p9t3RNV0+2yq-f|!5>0~t5u0cBjU}QSM#5uctVFkdwT5ovu5~nPauii8&7<~zhZKa)q=R7LvV+?dhua$C^nFD z1GP-H8-qClkHc>wP)K=fXE)-~!&n~zECG9DwfAhP8-_cq@L>xj{l#F_h+Rupr^}>M zS|`i$x#*!TcvJWGYyzi;8@D40HJwX^>W&vwE{@^*u+zQ)#yvL3$%E!1plhTAoxQ4xoGbxTU?&8RV^{ll?8I6>mEGc#*x{4+Y6OW9 z(l_+`snb_COlRcQck|0MPR{@|{_EJDbUqq&7pwfGB|;i%~&1R9u;Wpd(-Y6 zD?xyKI(pc9?>G?BtXEkI*Y9s2iqP>kmQ!Od_Vu!>VjA(xu~b+-E_bVxb{rS@dpOlN z{}^)w0mRR2(=N^%sJhRf2>>1YkwX%Om&_GMO$`w(@9*Rto|hM9!9%P~(LJN1s4s%U z=ec4#7KFA^H+yW4#F@}OFUUiW2Pp5>2CXM*&6nLZCV4qBx0Uh{P?=joB*om*$HX28 zGk#b0D$f@eZ!IKGYSj8Wq55+oo5-ADCEAoU(zhyYL!ay5#?;>`skT0PZ3!I$i89{Y z{>ds|ji}R>`@N>=R80mC(W>Mcycy3cImJuk&U1$ZjOS^IfpX%3+r53CChK*SOfn!m zLryI`F(Z~W7XEOhEMcX@9A=ADt3VD}P(*V-_yj7zzExB4|qZ_htgsXaLcl$10%6l&jwv zbi}n(nB633K9%iB(iGV<|Sm(HMSoIuRVnam%I`kK~Os)O?w0ANYL?1FWf5`hXg zlLkc$9c+9z(0!ZP&mr|UGwCEi5~T}qdM*Z({7sA4tsb#!PZDFiuSs0nKFc z)p_q4OLwp8NtOY)XA*JLracn#sMV*^HK$k(5Qoq_)2~A|D%wpd8xVmSxL@m;z0=!0 z^9Sz`YTx!_*@$+Fc)k zPYr(*45#xB$LNH=EiS!WdO#y13S+e|!}s`9u$U_kA0v+`ipa#zj2xCQ&dvYbZQL)I=0=Jm5xx?v0^k4xQr58_`D_(aHNE4C{JU!ne{HA~ z>6QJ^d!h=?f=EanYgPbneJe;GSP@S{3gF`8d1Z!RWpEalzeQrqM|}bWHt-DYO%+J` zS`NpK2SJ_S`{PZ4iTY`$`2!=_I;uh(!*g0Gau7*bo)R%2?Xn)(#c*A0ieJfkbYgQ)WGuZ%k;k z7Er&Kt}v4qvK0sKW84um)I!yfix6XWX_dTHGUrc*u^u2}87!T}YZc1Ys}F?@^mrSN zTq`o1X#@!Mk>)k}+QQddSk_&EqB@9~`N~^=Tejvk1!v|uFC9WIUQdWw+*EiFciiu0 zV3Y~>Y&D;J&r9|j*P;;Ao|0JqV^%da|9JdEcqa8lbbgp3P!P0Y-5_)){L}7iOJ@w^ zUYJF8C()mC2+{^w?ql9xW82U8-n-34ngO_fZgttnlWSTV^|%bHI!eU{ILW8k3>iIt z%hf#jK?)S+EJU{0Q9M)6VZjR4d2$sGR0^uuHm7YnGWVapG5fY(`Q3P2bYTLU)Iv5> zrSikLM31Yz;!gw@j$rRNO*AKP?-X|(SU7)raz?M#4)3PIJX36KsHTf*J$X@Pp7wEP z#-UH1xyBNfkQMp(DWJ}fWeouRr+p#$aO>%FBwA(CqfDtf^=O@B1g+( zI*;m9rdjpSaFmtdh1;VULofs_)s2ggWi*MSVrl!dUirC-@LdLNWx`HA+jH|){C6Kk z#-V5pEz0%G59cO23TBs4cDdb5X&)b?IYaIG-!?khBhu--WKu6~){6z#VK6}DUy_JO z8ZwJaIr41JMQ;dj?vFaV%uUc@63`G#!x4CC<7w7<1_%hdafSBt#;~R~9xJ_1HefBZ z**QqPqH$qQUCP(o7_=3P zj}SvE-evj@gtXSgM;_&vIGatUj#HUbUP_)LohQ{W3zw|u61@kUhGKd|x&SGXl(k_y zZ*W-L|Hu54-qm4z#J5~Zxw~h(eg40Jhp_WHvH-w>H-rM@nZlJ5PKafGdzijL=t^M* zp9N<+OEu7Y?#(U|!h4JfL+evs_RKbH;E~=TW1>q}Dd1mlqLgRriy-!&N9%yVcjx|h z^hG%A^~o6spS(v$F)k~YC@(f790wuGL{eZX+Iwb5*B}!L!#s58DHn0$%fDX#x@NwJ zOhdt1EXa|TF~MaPGmr5PsddW=2gd0pT^rW(P-Oui7%3mWr9%(-SZODf4m&yy@Eto~ zUNuzRur6un+HW>obtt-x;G}g@T7V)iM4eCr2>Ew`jt$}~`$;zgy`Rxk6R6>etUtYj zE>7FR{laAwO>|Uzok)@k8+JzR3LgEVHPa2U%nKpEHDp)t$i)4B($jx8TF!Azo6zJT za^4CJf0Ie^N(s-@XTl!kijb41fnY5&LEz;b8V_3Ov)U1+$#%O%>SnC|p+F8BTrNDm_JLYR5@=V7u=32 zrjTVKr~Ku21C@?6a1fEk!ooGzKWt&44;Ufp!tQw!A~ z7TQx4IBlzcSZx;BIF@K{MHg$AsoLa~wX&TRW;D?y;}p1eE_mkH$J9`=2&@0e2lvY&hov zF%j-BtwIEogT`Z37A>P@F0+q3`4#Qs0N1VgV3v_*m8F--fS4GD^~fU$v-YqU>H8A z0RhQ_ll`+c-S?w;{6S}R)|ZYO&h!EO@=u)S01Gd;)N*z>*h(>GiFSyJT_jm1sF8WW(Qq%r}=ZgkjBLBjAM zVV@f_G8YlT7cvu`FM&8OrlTgA3?k%l;)S+0((wRtI}|n95^$``Y=as2A!1LJr1~%WWCmk)S)kg$1-bgu zy6K7rmM+QHO<)_JP!zn7E?*={a@mGf!yYDS^aG2UE-RH+x-A1M*%d5qCxZV&SfCP| z1|%c%yHUzt1-r#v?1zCx-(`2vPoAL(Fp<+Y3e#aA@n>CGCBSNJQ2o$nPr$PxKs+q7uK?6%X@|*BKeILx0tsvst6x0Ltv49s|nu5X# z(}S6uRxvqkXzDekP`xG24iZbR9A%%nIj6X%MXvAa(hB;Lx+jsUvwp>xv=kK_5nNRS z1DL}CgPWa)Ex9xhJ&TZc&_^w=f5ALRGfhEuOQbt$P|i!M2)mrQW6q>CEV1{SjLn;J zf>-kR{A6G!&w<1!cd4LrnXfH6ObVKRek0kXs>VrChmy0u_Z81R zrD#&>0+ z+rJSXIyh!m5%fuCU>4sl4LlgxaeRBt@`W@8t{dc6fVYH{ zlBW|CVuLG_1^JV7C$Ak34m%sW?qNAeZeO}i*_9=!9-jW3#+Fq6_fOdO4n`iKu9aPp zt~tP*nCp@0zvz9iYg(`SWVsN)4xf_2!`rrE(7CyEYf3K*pvb7GA{N5g{6i5f3tO$d z+c&}sLr@5XDAcN94u(H0bVXYFA!kf8*VM8F0PDCJ7#5FleMF)5G9q6e^pII944M&~ z_2cps&O_&f8hy_BqtLmNe21{nIQ7B?=)6DiMs>wsx{@)cnT}FD@#e))4J$6HaH zgI~;%%%l$S%OuT(3k&HOhpx1~c9s?677!UFA2T3EwvU#FOqELb$R$=J_pf0(k|s#6+5S#DHK z-6?MavuS1#80n=*Qd!3JB*``XRewS2Hr3L>ZZHWsbwJ>$QOPGsRTGYHJ= z;x1FfQV%0pk_}wEkz2oKD2&MRIH$8BSod-GlXm99q-znso)7T;BZluIZUg)$4m+qr zJ`k~Ei+8Qtj@{kbI6IFOO016|Pu~lPw!9)|>l+~0Q?WVlV#-OUW!_Sl&VCY?*gGXx ze2H^@P~j}QJE3{+pPMDx2@;b``c2DdIO|DhI_6b@$k~xfhzGWttqsWOCH}Kvq#QC}{i@Z{uKD0)ODI-qgLC6UihvVg8qfu*cte zr|7Qp#Di-Jbv^M|UUyDj%->&PDx8<=#7FH-5eJji;uYx$A^2{-w>|fBjTZrP#9s}} zfTZwGPy;@j%z>&I*gehCmlsT;`Z3SfB2+9wXz1uxU9NmtYyXv;lTi$s2|bP^B>sBsRh4J z4Osgxd>n%?)x}do1ETEF4T1?CtYisTw}ER^uL~u z74H=&CpFswQ7m|u=P8z z^x^*N0Mo;LT;f3y9al=XyaQd8j$?!Juw`<;M2mp`k^DsWC*r0MOOE<_p;zAX`w7%S??X^0HX6_ zsFGK7jbr^toIqm%5n6mceC=9uaw!~#%=7@V^K1>Ck(RjVz*@Bgat}0Rb*o7WZUKW8 zHwFLIbkBq+t~TQ-(M}HP)-}pzM2Kr5J;I}#|C6ce2^mUE7?-bc=u9dTx?7{dm3$xC zTaC8of-Wz-E5RnZfk_NwJlzJ#A*VjOrw;5M>2MfH6?qvy^5Y-3Os5LJ z694(Luwt@as^EFNt&7HC3fl1DL0rHhN`Gb6l7?>TG4;jh_EuJx=cO5mRGWwY;ji2~ zB+8WfySs*|j6fYta;kERx+gFe_ou+D3>sEa*e1~Hv|nZYct-RHHHFi2-aK(ixQp(* z*SVO*xO&MO`(9eHQe%p^i&`OyuD(SU}7i#v7q?8@9H`$a%%TFkIqZHsSE z=NYh^Thhp_JHX(Hu>;|BYf}>&oI682Yg}X`tY^v&IAzcGy}3cJ)JnR0Rkc%S^4b2dsA%!$L#)6QJq%xEJ3cx@nYZv(l`ro2Yo1P5(?Bf;E()G37_e-IZ$gHPz#^}6 zubC}DXx%_wl#XQHb0w^HJ(P=Rz^?n^d=X@@emM_v&=7l!HFWI$g-L(~r4=%wv>W99 zR5>@EBmOLFeN-kyMji^DN9wuo0^c7?bqj}G>|F07DHPcYa?C!sgRTqJO_|pCC*2~~ zTpYGExUNEgB_sZLR8ifD6w2&&uJYfYRt@pg-;$qpatwtmC+)F9pxcpshzi;w*^|FR zx~}>ykwTjzBuJ+P_}qD6HDb;58#@9`sNbv(2FE?FJl^hxe5;Kfe^p~W5y?}^pK6g_ zzXAn+?!_qgriEXJ+H+X8jkL^!ob^$GD{GP^F#W4&h8`Sm`jI?eB}JN%Z=srSt3nvo z7eVSW=qZGW-h`E$JJ%lF*wd}$8u4~&7Y&^sF2lDtf%)wYdR2xww6ae1rX9&p@lwK? z{H7y;k*Y#-YPWA44`Z90R7=dWWX!hmUREg8q&K)@?e;{3m+m_}Gk$NLpHL*{ z{JUP?fCwoBpCV`$f#~yqZ#?l!xAq!HX1HI-`cK#~ZC>(Amec>;%ydX%p)ZJAL~R-V z#s_+QB$R6!kn^^c{@q=*3;s3Z&D3rM>nq#ciPj`TMc3P_?l68`E!cd*XCH7;_Yn!@ zTwu(FIbfIt7k>rIjYNyU^Fl@q?Hr1pX}f=!9w0B^+?$M!qy}M@_ffs0z6B&IQP9AcK|UoK%D6qoGDWHH_0>R7PZoWLR~_ zlki=p>G|;Q{|NE3%Q&()Uo=Y{iwbx)}_;93$i5}W=oJd#|5qfTI|le_{1r3+DQ zR<>fxs?qK7Brf3uU3#j)Y2QuP-~;3?@jEqviydd^8^dMK{e{dzlC!`qOD8M;c>EeJFFf{rO1J?{i3)lUptz4 z7c!hBbs{}1Xhy1i%6=U81Zbldi1#1X)kpa#7E>MkG1me@9{Z^6y7V`jcH?9yw)%D5 z^*|^V93@2heO=9;{0J~{wdpano=x)pH>}oS>TVqOFEKOS0369oLB_GB_KQ8QdV@Ys z^^{ioARNYSSK)t#3G*{Qw)~bKyqBL?gO|~Z%Jk5AYK@C;r!U*n~ z^{Bk)Ci_->I~Im>zSV1l(z%zKkXt2kq)=4J{%2SPOb|ceBx++?1wmZ}t7+3gjsd(w zKRi_WQQL8f_#NBBR1F|R1S0GOu4@r4T|eI%#iDU&I}CA~cmF?{l<)90<^$I?QFPD2 z1xsz7q@{OsJLGjqBC={z5OVAp7HhhNc64QehK5#P4YK^KZ!7uJnG>@8U3U2A+obBs zXsebYv>VKo4ImI>iSGXIQ5xz>X2ejyf@E(M*ka?7fL1*&bxoBl1pdB`FeWn&Tot4} zWd~T_Pq9V(tZ`a&U{Gg8PurUuSAVp|>=Mr2?sxQTdfO5rsEr)%MY0jZS3uN~WMkY& zCXLAa+V^i$3!s{cQwYp?1mu`M+1TNvlQx2_;jc$pz;6Pd)M;=7)(S;>5@px0Iv=@u z^)fY2g{3+!Q7K`~TBI(0q)H*c`4=`9I87cC`Vpm%4KVp_D2*2OhJdH9riDXnKR;hu zU@!rYggNB%fX9fqWT1|Jab+Yjr4sN?8*8(prQ3~JnZEdm$aM?F30_7FTJrW;XN?iV zAn!4S=2&A2(Va}sX<*_aES}*`qTeOse=OiG2W4iX8?WrukZ4Do^PHXDG>^Jlkz8{( zvl>_LLa7B;-I_1R&^|x%6oVo4+~Tu25gW2{>i(G38eYJeYxS+mq#9}p++!=-4N*)} zuFLt$9!?32mLGzc%iMv#*i!r=O`l=^6e z#4R9NoC6I%H;ZZ8gz4Wn70c}Cdv8VjeTm_<@Fx*bu@DX6#14aA0_{FJCCrOWbugi5 zSfBqn|No%?m+Ti%x8y0KRgonKkReDoOrac~Q#6c}B`K5Q8G@8{J5}{6j4WgV#&hebhK$zJfs8C5KF|Hx_C z|*$lYqjy889=iI}nX|0J!v6~&3HWS>=92Ym!mDefu<@X;6&B}(M zK+Nm(Q@3wzjs?BHmI*?M%!nY8{1MJw7U-T(ilImb8P#=pkibFf+5N_;p}!%8R~xx+ zE8wc{812EiGCiQ1-4Mc#sN{x}Y-gJaR9+ngG+I@E&8);OGpp?3^zx`O*S$n z{J05{=VbS$4xlDAANN&JjLrj5>LcdeUJh3rpU0snANkDO0n#fI{&x(lA0Wbp*MpMR zC?3C`+y3a!iHayL8z+FK%R&JRA)Jsv*gU)JT{mXNY@x(a*fS%ml_^KHj2=9s3(&{S=1+!htoxGb~HOhq$*j z-#{`XALb;MCt|gN4V;8_Z%T&c{VS;sqU19r<+MJa$b9~e^V+lG^96Dgu2-Zf_VlR| zz9Birhl08XEQIg68Y<9-4fWqzB2!%x05i$V&;85|29{RI~gz1y|6wmWR zbKfO_a1d|#2y_)b#S1yw;4%Es#PZ6`DjUtp@Hh0iqfZsqQKiRSaeB6yr1!p@U4N-@z#y-c$X52e zFK|~D8cw+XWJ)eZB#dJ0nttyDo<3%I7*KNMXf%QOb)OD)YQ(}(TgchcV16U8%-f^s zhT#U)_E|l5=P#?o+@X3?4{9zae<5hcCYz9e7fYNqz=v}CA91%pi{T4&cLRl7&bfl= z(v;>9D3p2Q6!$YtEhgPH0$ZJ zo@5T?8zO z;Mmto_xkX%9#t9u6;6Sko>A8IILIj0WDUuC41h>=^7Q+c$)vVWeILoZK@7Ek=< zBFla4o#$7oG55>4MMsnM4LICUT;8(tHR@Y89 z&S|A`AlYJ7SXjJ~0ioqZFf>TYHqU_7&Q{GJ`IRC#T);SB{Bf!aHYjk0Y@U!>;NWyt zNT-t!nt_=%lkJ65moJUmS=%nXDet2l0Ex$#b`LT^9h;^HRPbaW`>=j){!msI{Bbp@jf zOd2A!-ZZmNYwiT}PjoE_V)H|>COGuBbGCU|s7*TOulQ;Q%@Xh@8mt%<_x+vNUY{MN zm;dnx)X4Hr2R&NoD7>RmntSD$J_EFCtcJ&IKOM=9y$gg8LNp zhj`bQ<6u&>)Tl{fhI5K7FN(Jm6i)6AR05!jn_Vp@No$z?vrFh#ik966DmJT8>L8pk z@KgVB+eDuLG1Q<|Y`PcBmzlpt`otvrwQVKq_d4|Z{HE9hlrG-wxBj8!H{XS<`57vV<+B(lhTV{mw#zW!g5B;#=9N_{xxCZ(aFKBc;o)I!lY}Ko z!ZYD<0KMO2-_azODwOAkZnuLcY*Rf`$_Bkj=+a38m-WlqD4K?+G^K*ri&m#DX9i); zSR=Je^0b?g`tqx@&SwU8T?1j#=#y*!%IZSL+mnRXi-C542r?jz#nKi&DQCWz;^^F; zNJ+Ax!wO;G#Fz4}9{$_ky9gQ!#&xAue1~=V(B7wTSz?N%v3mpZo*HFd0eq`lM~t2+aw$h5e>3btlFK6RGP{PvM)#z;A_$-=!wwQZ z^(=Y6Oh(ymo$^jO7sHG(y56p&{1)9NFody`@=XE}KB7Lwyz!(z2~+}24F3~B?hzS{ zOR9mN!Zh!?BxsF!!Pw9#O1S~&{%o+4D(`K10n>#=MO48h$tP< zfCmG-oimgPMdyw2x%+@^A}dQ(b2yiqz@-Lpg=pe!-$K0Ht{>ZMzhI@K*%~Sq8c~d* zuu^P3ywhca3v0L+m!7jz>t!awB)T)nEOkXY62DD4Kw02!b5a`*Fgt6?>zclKYVff? zU*lamzfjxn=*^~1<9E(+eZ&l6Y$xcEDG~QOiE{+d5FnszOTl9@`g2FFjR#VnQVRC^ z*llYI$Hz><@3uK1EA0L;RWR1)R0`2((&zVSN`r^J*62=F-GkH5El7Yjh-m!RahO69 zOvFX1J=iE>DcczEY*aaj=<|NV7Pk-s!yLovYq%2=AmgG_)rqyK-~0?`jbQBz8dX6A z^B?iXcyv;?YuNV4WSVG%Tk6qu5>#aT8+nlpbsZ>6{uNQb8&myg{E@YH5^s9C$>BwY zM`5>AzpWiW2qB=O&hewTa+qEo$I**X?2WKDUW5t(eG`gx`gt08pKZ@27A7Lj-@JzY zaOcuSj^+G&Fc?(+`r86ucN+xLYNPbwSCJBOvkp7eP(8DV799XqD;?1^@aXMIaBn>~ zC0c|Dk#L6uMMmWvejrmaOO7u5ZEK=KzNj(joO1YCe4= z7|j#O>Nq@E_fhv92RHKZzK`w=WGr+|w3DO3c=#J9miN zwosw`&(tA31ui(Jy(;R7JBmwMy}`!i=luUtCAYgZUI803LM!oug51DoVE}Z3i*SZN zWyJrlV6j_NJbvSBu*;|0ZvZwc8!#-%ACFWEVsWm3#E4w!Lq{cn0MGvh zlvwxra_T&8@p)T=hpKYoGZkGI095Gn1(uoE+c}&1Qbcub8bHf-8bie0a2}P9Es(&!NtXIDL1dgj}PE%+&8L{z}%dfpxSkURL# z<;%JEzr;qTe>k0=JtkZm@t?i~WI>a5>P~X3zz`>Pa{T9RUdPjpJ{0iX^aHsFBH-39dFVz`>yvF5kJ$e3rnG>~O z>^2TpAvwFGI<|J^;qh+Rhj4$#DgQT$bXK&J z0beg=_C;RHW-&O^(w3hmu;f@rRyQ=^nzrhpLeH|4X0yQ;Y zIbE<|b*c>d8F@=dPrRH&E0gBqbcB%BX5PD$s%X?;t$jkWl*MCb`ME~K7zAu>&kFxO zMW!W}li1`%jxO++3E=#qRCi^4m_vrkReOE5%SfWPA8XPI_*8jth*hq_$KfXg;rL2O zAwuhVTe7&QSoKJm4g8|ze47sl2S1j|l5%y1=1}5pR2{`^$Yd!G{0Q;5eTiHZjGxlE z3_#J%UaD|+j{Fm|MODT`Scma9>SGfnSE6kW?^?&fakO-L#F75cyT(;2ol$}4$%jFB zH%+&N!=%#V;u_AKWCzaBrYfsh=Wk z?TJr@6z*(}j?_g3pO#f^zi}M#iotdiM*iv1WN=fVq@TMv9FH56UO!TXvlo1N*L1vh7W*j0Uj!@pGfVY(8Nh-2`0Y;m6%+P%4fR2VlpIH9X4p0`b;_ zBH|wOB?5k0F@bRM=j0f6vJT?;_2~U0v~0!r)21#>i@Ys1#@moj$P2Z^hGxuxVBLX}OK6lT{wQ>UvB|7vfeFR1g6V2v#Or@gclq^6s@>8f+WfCZKn zX8(W@fu4iK23NvyRoE5`v=Hc1`o1A$CRjAd`j?}|_M{t6m;j+7V9L^fQfKrxnt%S3 zxBzIX!iNOaAc~{NN`b^aNZiR3C2y*eIdIDVAuS60*QIQ08J(;AfctH9<3%|)-~0${ zfx1Z#aYKQ@m2)RskH9W44tMR#O~%q7No6OYz2dPeCRiqm(Do+^kMX5;Q^TQdhAc603vqFJmIY(PRQ5lk;oW##;rDqs>jxXqw>|)UK|IuD>f-sKs(K{#MQ`+C;RYEO zFsn@MT~+W*02Q78i>h@BJS=!}C6$mDZ0^`a7k5vMNhzlsW8QO^p$sI($3*P(ytPBo zHp(st1B3H=KBMDRua;3z$VSBb6EF7V>a65mlJ#0Ls!9)1WEMPxvxEkjcV&=FUJ4&; z3!aB{wJy{<+t=XD`4@bBMsN1mnK8E)?XZBTtA1^ELE#D~3e+l=G2$qIZHzX=oa&pJ zaZL3mZzn4mvONkE;Ud8M3JZs|q>bnAH`I=TaqNcS=goO;)bo3kbynwz zTqv5e*!P{M7@6YY0|~0@ik>S7YP@yRcFGfFjaJ-oL6NB>TIO%_3!Vo>M*&C>MT%A> zaJkoB)x4s2@&y9uyC@F)C^x?p5ovsgz0cH|!4P@Cq`wN>noZtdK{U_kP*;~>{mEO>zyLO0y*D+?&+>M)q__ia`rEh{GL{dkUrF=#9S2X zW^y{)K^lt@Q+rE2g;L$=WE|6!u~G|+88^c^myYd-TlLnJ=XRnwX|!e>mLR@Ql+K&T zMLL_=*Y33NCd81JFV*)8svC)4`xR;<%Gb{4YtPyw1nmgfw{rYKuNhX=i&RH~N#9@` z41g)gZc);^Vuzz|L~@osWF9}|BvKhdZ+`rpjpMUBV2+{?(x3cxg_eLv^@Zfs?t^3D z-itemE4C%>oFNf9hyse_I~L5Q$yf+#5G%|JqfsFuy99hJiBaOMGpdZ)0NAh8=-`ox z<5WFIsT2~hP5R1*I=mG;m&KwNe6){`#$YjiRKP>_JxcInNe;KHIlvPY@VU-BMzi+b zO`v%-WT!bYQICQ>{DrBWBI^{zbmjq7i^pbCcx6XNKKsPZ6{hOC;Cs{GGNeilq{0yM z{++rTW}4tp(GHNWidUwPZE^1h4W+9rH8m9$*1l}HvPbDNTyav*o0sZDu})@byJh<_ zdWXk}Y%Szndp;$g?6@0bNp|zo+%8= z+)73|z}ecMfW5@G8>gJRC6e+INk;aQy#SufNguRpY8wwPNHL$OT z&{?l(TRFdzIVMTuZB*kFFNR(zH!XIYj^EHH>H-rxx1gyK`#9%W@SRqFom}E7=d5AZ z{_3)$%~y?6?!3Uq&&>xBZ{OD+f{o)TVUr|a6KA_8EN|uEu|bD8O0N{@2-D z)nw0GhK*SuIBngivI5+d*oQ`jbb0vn@)iC*0KPsDt(3!z$qXS=ZL73rfcU5PAZm{EOdjQ$9)MNf%8o1q5?5iXA`-J^#tS)+ZxiV=D`){O;KySuUm zDVmB&c=!nSKr76b{ONKmRP$d7PXT%Ej&vNDmQw(Ff#L&~yvDo&eIKd4G`Wiq;Mprr zKuP6=MP(!eH&4dn>7)fum=bdA$~FPWlhGQaok#3TN+mjJWZ{E0X&8)D!s~2wO&?E41Z5qT*0!!M`vALadw|(uER?s3$ZU|N4yV zxndLbNrYiSUxM>KQg|!;1eYZiQY0g8Cn&@c; z4u|7h4kV6g{}3#7xUWwpuK>zbsbeT{cx2i#+u(!9cQc*ixixIGjHJ?Ky5TAuw)pJ6 zM6pV6&5FBfv6Q1w$|7;>BbZP2kQ{qvCuig0#ajxyqq7W^M0+G`LYK|c=a^*MLYcN{h?Q9aUbhWE1D)rGq6I;`1poZOsg4N#Kp+>%OW4c+4XV*XVaJ~x zvgYL5G8=uL5DiKqpe2#1YZ%76vbzN%#BA-OxS9fR z5esgD_G~91Kg-!@mEj})wRG8i1-5#A_1r14*Za zhZaH!+yM>UA|iFj`_DAAYS|2}1nIp%8U-{}*ky_Pr{aUlKw#I)90a3bv~JZi?#@4K zQT^qO=g=_}1UHP)XWblINB^XxP!Axx!kk(%i|g_4v7b~z*F7^z0ZJUz?QR)hoSK6U zY?EnA`<=?B3%W zCCiwxMW?zA0vMkcRJ1Z5j?{I9jSjdk%5f@_>$A`ZPQoi$>BNsg5Tmtoz6-YqfcXt$ zfFl=1oHhkxm}PH$2~1TS+{Ib=yDEq}VV_n+ntWxaf^q|bG^~tZluADJzhZxmY;oPr z({ep63gb=Ypu#atZIcTQP7F}D1E`QFkoBHz_y^vFgqxBaqE)?;v&S>Rpyqu!xQ(Hz zZQcUuU&YGvY18w&J4xqL*v$6MUto}bO)@WfI}poOOlW0^5UNC@hgIvpZG~15&N*fD zZfp$}{NofpW7CEtpGV?`yVbhcG+of3_`+nQm$Ov1ok9D}M&YY%tuHooF9{KFWd^th z`f6f*G-DbH^1gH*DS~Q^-;+!UClFzSFuC0*n}jwKn26kE?{Jjm3l*0RkP8_Y%&0SX zGx)tLN}i7Xn{O=t_vS;s6=^Puz$}|jmrer-oByXQ3rzo|b{%a?OhLWl4?D>pSB9@q z0vz+NSOYxLKhtmbW8MY*j53Ww&j5VHH}u{3PMts6ZWR zJ+^?+HMDx#7(-C!$Y)8~j2_kT_YM4R`lf+)ak^lb84M6Sx;#yCv3FbzjW8Hc#DvUU zs&&=`4)*xib)sJ_+t`L+53eyP4;tG*lNec>50bGuuFBk|PaM)d;)7ECfxIqsmeP|d z2s!4M-*`wo`PBqV)mNJ&PWIzIe@``5Xvkt?!HUVB;5CbZRW2aFG;K8pLH zIvZK+xs(GZzNm0Os_vc<{JPq}7#x=E|MM;AW*O7^#0^Z#BslHk?MbuBkw({>t$++QXsiqX?_KW5FW*T^hlHsA#g9e>Vnl?7p1 z`JgCia_wH%c28S!1sR~tjNO8$z$!9{FHZ_te^F+##mpSX(e{dssk?mQj`C@anoXn0 zUr?lxm6#)FSAmo!>VIf|mN?9dEsO))85@bey6hh>Nfc;Wb&9y?5M>=jUh(_tnrxx& z$_J(qBZf#R%)9MZZ!lD?S)`*}^5ZYsp8`{%%j(oW&=a0Hzb~P+&47gJLpiSZjB>A& zy^BDXt8KXflx6ON)vKw9MamO|wwEy}>Wj*`UfOTDd3{j;hRe3FTASB{9B{_H$NL8M zu6Pz(VHfv4KmqA{u!yIrD_=jtbFP`20dvvYAA32;Cud)85V!WaDeM@Q#TXrVE1}+! z?ES+be)+BJdGYZ^@IVBZoska`0ISIowH@{gKi7L*TaxUzDhSPbSn7fMdFoim8n-WV zFZ*sQ#jSiY1-%!(@+){Ug@Mkjwf2AJTtaA!ft;uUGCUwzpVT&Pz=;EksFmB*r(e%+ z1OH%^(8aAIxj%JKdnd=5_O;xus{H%pN*IB9@T7MxlN zq$B_>9emqqScRHHjR2`xUPI+y0y&H{ew_}A@Mja@dT$^RpDDhPWyth061}%xSdqOR zVb?~50`AK6WzLkFQ&;@HK=aFCY?YDqYW@L{r}!uPeCcD&LlRxY<1%5;m(b2( z7pOe=OJrp$>t5i-w}_6Q-R9IKY>i+!brC-$*~27}heL0TLN_7R#g$RupMH#Ik{d`2N#lN4{zQmOT603M5pOfl`~3{a`_Y4HtXd%Jj4`Oaf`P_uA*N z@aTXp5~CUN^^chIE6tMj3?fE9o>%}uK)$~w-^>aG`Nh0)_5s$x8};-$xzBJtu|1Z} z+JnfrQu7~>P>sLfpRJ$<$-M$2FZO4t%?_xLQ=zw%>>3V7Y*qI-HIZZ(ZtAopLz_zA zEpSzFW5l$pHe7C?!eU(<*+6Ol{+d<#AywhlRjIb;XjXHriXF;&%`Nr+xe9|8N2if@ zKf=s!^Xili0V=otVo9+H!iL!9h~lvTZh)E?UUob;sJ%!?cKr9ZK8q+q*r-@k!|>}xBvi@gE6 zg&G_xo6GT3vd7UaC=&&FWAC7f+ZEFTXg@19P87lHkHKSUoYXWH>YK%2bNa*^0t$Q{Y09g6>cQDW66S8jGR znb%}a7Uv31(ObJJo(lkYk1#i&X!r6njyUk&A1&JbXAzm@vEvL@gw{jv(>x&$Qy03l z8aE{Fq$=g%{IlDoB#jn4#zN`iYaUG_lmZt~(M-4ORwS+@_7m}D@=b^!z@w6jn>$4& z;RzxdG&j=-#?CY85}S+`2j^7+CuTIC?xJ}JqtHqMXf!eghkNDVQa*rbA%a~87n+iq zm8sBCnm8d&+m_{KH0;%o z`P_zxOuy=AK#ER?JUcY7x+f|8X}E7kQBL+En+&F@W$DuSI3hu@5(3PnaGb0xU#L>v zKZrq4+iTexP>OoP=bDnHBAW6*>^a^@vNz<>(nuoSgiIQ7oaLn;7gtU&<+8k znl?*^xXcXJaJmalMaZa%#6>&!xx}~>xw9X7*9CE_@aRX(WWYXb*0Mp&Et0UF0&|@i z)9;pELo5ov(!S-C$@u(#v`6$>>PGGC3#Xk9htsTHOzg7KV~+*V;#Xwry7@E|SIIJD zK(mPqyzPZ^$J+-*d6wvpf=AhIhJxujFUrejVOkD>HD2P zM8xNdf0=%BdMP*0W;Ozh4cAiAxZX75T{J6AAdFyxp;O;i2K#3^X(Bj*;(da`0fGu& zWG6FTmP!J|*>s8?s>>4_uj0^yn@lt0mPA60nEiAQ=v0Wm)!<2o?BkoCe_yclt2ZbH zm#6c_I->{UHm_y+$!|#KThPGIErfhag#DGd8(#4tlX432YmrY>oenP+d9!? zls@m{=Lz$zya*bGWGc6Ph+7F>lnsqK_smhpCn)_vmBT(Ug(V?;bd1Fvd64JAOXf(2 zpU?a#zjd~XaPuf}wO)adMT|Maix{6BJ@S=r##hGZG=pUO>vT6|I2$!OuM?A0PE45~9Cj*Bs|dDvx}s z5sZ}j6qwti3?^WVy(E<%K`YHLQtYwDLq{T{G3|CL1+e?j)w8@0o7+&2x2wfWCbELSe?CnMvei}D>zn<7i zEM<-4vQLk=*R)zm6(UEjG}~w+*h(NvUv%ZC+tVE~b7%sTWI>}iLl9`asS->BkQ1C) zl+-Dyikje@2TTit2Sb^ZRWRp=p0bWbRcbN@>l3Jfel`3is4zQ?fe2Hkj6agikwvr9ckRV8a8Iu>mS%%7~UA zDY7z4GaHew5#imTuyGo$27+W2y3ZzNHG7yip=(2b6p+)caoc^E2fKgp79X@Sy+276 z)!*an@y>E7(}7@_AHtpH4X9YBM14$!ezi^_QsDo@??7y{jL1N#^Ttn@W$yave&jm+ zq;svVE-qqy_?k??H2vmrbsrIrzVuj5rms~_f#xdLo&s;4G)CBjtb`Z4z8uImh9{il z^K#A!nFZ{r?B_z@%Pa!!Ic%^CJZek}89q2A$~^PVTFIX|BpD(@s!=SJ#s>CDpw{{!gs#7w z)eq`=?%g`}J|F4hdQqXPOtCZWnd8^7ZQ3`wM)i{=7#^rtc1%nBzt~B6hwTQ$=cPIe z_(1J}%4@Gd_@FGb5JF36YuVR*9D!X~>}c>#8w8M!rO@(DxUsBQG8Cw9Bzv70^m9jv zeaF`B)DNq|ax7uB>2>|i7edXhM{x;Kc5y`>BAW*%~S&CskExX6%qFl;1Q zpO2$8BjitabGa&T(V%Y@wL5)-fBsCCyz75KpMo2|L|SibXkEbbn-i%a zOuG5{Ru3W7^_L8rC52*0;Q{>@+FstrOC&4Tpaa4HjezFtc6va`g+|HGEE?q?9Moan z&gMeSvZoW1lR1>534x>Zy2Yk7$4yl}??UvI2qQ9gH75f#jsS#72)vg_E$FiS%%^uM z8t<^Zg?y<0p2{ab&8QSJZEn$OuSqsGnAFE6Scb!P71wj;MWRto+Xo6|!Ai{I z*y`T0=mSGPc$8U$P|#W@j?5XNze9?QjKHjU(mIgk%ZRdsu`#^cRVfzyp_kZEqzI?+ znNKCTyArSeEO}q)e$CdkUn8Wyu7Nqne)>l%uP(w(kteq+U*Q*xIU7Ajh48r>TEnwRGq0NN(xGb~l-|E7vP0cR6=NNxA z(}8;{db5Es%Djfp7Tgw+k+f^;<0tqL(vflwO0)bQuTE7E_Ar85DcH4m;HgyB8SH3a z-)^ba*QxTy^5+uGOGyKOeUL7DfwW5_obnlVNN*b?B?U{&O#x^F!#+afR6tfPDs{Nv z2%f643CK3qm@(DJs%Dib43`bMMpz<7;?bBJFGxobyYL>vm_$HLO>W=V=Z0fSyOs;* zAt56iz$OlI57v*aJ*jSF$#37{os=JXAje_YwK*>-5FNXhd|eqRJ&6ZJx$=g2+-G_> znH&o1G9QwT6MBCWWmZ)#cVZ{KF<`0ZvZb zf%WmWuEN&8xp~Hr3C-&uZfFT-3KP`CkX6XVS=p%sDf@Ps*+{l87);4B%0@c$UzLe`+myCgU;y5L}MTmS5^fu0-f3w$=n|cdm&4 zGy?(MhIMWeMaAZ-(2$SX#9f11Gh*COgQ-%j+#9p$khl^-Ns@EU>bG9*s^qQ5{`2k3 zWBmw$F(Sp91?+HY)Iy}XWVt%2wSpSt`0 zQ?(R9sYND|o4{@YhD5EV@_!+RD?xJEk@+FlGVmZGwA$aiOQp}ZS= z2+YO(S=x|~P%gSKA$o(!C?^1ws)?K_W6ygc%*~W0ENIY)s2IriFUh^TpY~*U$Q_!tCF$TTH)~H zcyOinqX_P$-2$G4^*pLtI*&!KnO0%KqH_AxATl;DyXB_;!b0)$Jon9g4gKS|ho0d! zd?}M}0j2Y~HTnqJETSz+_B4MQsdHqwP}IOqcFHWfApQdVMwQ`b9BM>#l|5-4dp{UB zk9|km!%L0KM%lykJWd&1sC-1^adh50OIi~RfC++@;!4SnY-L=K7}AOF+UznPP14WfsIc6fr;n1QOluSp`Yjnd~HH$wwhd0-~QU>95P>$Jb4 zBoYUJ)vktuj%)v=2O;)|W7#J%m#YTF4?a3k1@@QX_;zTx?t+gFEw)4gcayRhb!q~1 z<+FBG4*`uNDFFkkR_HA1#t1Zcs%Buy?GmQTG++mLHI(Xt=RM|xWLa5$Slj?-qO!Ho zy9J0xj~vr_aWO9C{&sA*{E{QOT5cfWTRm66+>@AB9`xm{+cjDKRV+N53i&t@c-?e2 zU$NH>PazfwbEE-A%#H5RR%-HoJayo}v6E~MKkVcF!{7Z;Jk_FnDw zMhDx4%15^boXvZodC0O;7JvyFiDJz^{NUYXlzz+wfQJ#o<_44NZNGZ@kC({%%gaDv zqXLUx&uxpWirPIM)0>9WCBsC-#-~Ajs{oU=u|}auq&pJ<@pyuiySS0CZt6@b`K*B- zp}aAqfPkbyIHC^I@AZ?4d@k}NbtUyQ@_}U zqOTo(#*{fvU3LP2MLf13IlS0t|9HK;5D0s^tiJ$(o5DRCnw8l()Ff@XAL?-M98-ZF&jC85i` zclRQ?-(P2q)s{is23-_{Z@LZRu8Csh=;2WG`{kX?6s?TX8SsWqg=!V5b6(d2m~uDu z(oVpNPBX3~%b_Xgb3mJ2R2XJ%z3@K4@xuQ8<@844te%pSi+&?~ETHH%mDPfbXNm&@ z2}r3DGU2KRHZmS(Rwx&KWyLl`N!cGk-MxWF%~~X@)%8iG9u#X zgWmyQmFs{wm6Ekn!&;kuYAN>FdWhmot`^thq}+T3b(=_md^KE6J9FfE5e=rhJ|{CG z&D?za8dfZWUjUSwn#2_x1Z5kc(C!x}RV!nnqNecWp&d$t=D12%_Dx1-KB3tVir!}N zGF|X_Pt&%^sF+iIaA7P^%6)CNTespZ2^2dfAu;P0r7UjlCvg4e>$EBgtTSF~(QX|y zaV#Q~Y3u1oxd8Z=p#`gLrH=M^3VbzAX|#}w51v!G3_L7%byAIcHhVO*wKPBI=Ru-dO<2VoKSl%S z3?FwX06<2H7TVF8KI~#ZyLzk{$F)4=Yg-Wu3VSou>9;MmybobJA7RG!9zMj&?G?so zb*UXr7#GD^#lJ_tL-ccA?KDsrdv{g%m+_C{XU%1Nl$e~z6~bfi8hbvn(6uwKGZywi z)w_P!b%ER%%l8;gk}`YMB)gXqU#HRuM+S&9wy!$FxJO>M^n8MPJ@l9I$*uWf;C>s{ zI`%QK6ZwCGXpR=F+Wpd<;|*m>KhP<~+5d9pTNP{uIurWq&1gAbo?mhX zo18$VoFcn-J0WqQ;7P0^*uR5dH?23lG!oEsXHS`w-$!rv=~-*2spk^^%^OJ3KL~#0 zYZ44}YV zK*ClQQHPeTn49N<20;jFR^H5k+vfQT_Rk1pcw7v7>HKMGt5>%-XH6;4=&PuMdvk?JMN3p*_3BLM5?DJZ54U3IbW%{hke?f^w5K~WbE;8!1VT*YngRWh%J zFry08DRte{_!KT!wKca6)@>?>0I4Eg*$;39g&&9XF!TfJSO>=F{djKIPH9}1`ne-n ztp}eJ2hi#pd3rgPh0#Zn&d{3Yx-h24$iezb?H*=5cE2&TO+QhTJV^o01&j%cUDHaq zoNmrD8#uLL_GS8B!tQ0 z#~sHTDo(|P;ot`Ty6hV5UKkkl$xh_Dnw=Uo_nhn(k8C z5cNBr3(My`E~Xi}I@IG8Q=gOJJjxYLz~WFJU3{DUuU(~;A(WXDtr;GsQTsbPpb^?z zc`PUxJ2WXTLw`7m@0{dO=;Pc@mtJ0Y^m(}BQ3^b5a3FT;h8lPq&E9oQi>grJ45TIb#{j5NSw;_089Akc*rX~Ena3D{_|*>=p!nGoKD!&o z?FLBWy@nK&6w_X@)Ph@*mCF5qdRaDixA}g>Kd39~CS*$DyU8crT}*83U}!b)IXJ!3 z!_23z+Y7&IG<$7`13SPLAp!7cM^!9mk`3odS`7 zA_tfQCjsHD8J!&obU4oPW1t`_i$yk!wcDd*7*&)Y52o06c0w0+?5;mz3=rc$HWgiK zs2W_c`j*cKQYbAC8^J8*nsrKK1ge=D2Jd24hwP%ubh;eM_p3O5dd{=uEnR0t9s`U= zj7yBc7AH5UOiwWaiFrc$m0V~IJWa>)w;{tTDrdyYHlX0l(V<7@UHELy*7mgJt0UOHw{Tz+CTIGS`4(79rykT8TK5c6AsTud!hka-ZtB`z4$k_s4-mNXi@zea~b!~<2<`e|4YPzb+; z%#$V=aLGBtjgok#q><7o+6FO*fpOo~(?{*vGDz^T8LS=bn&AKItPMHhXZH`T6h!Tp zSWxeh{E4c^OO5vcRUn}&In(=HVkHLc-(};ee0&u+L%uz? zWeK-uoOCVld|FES(~qCcUxe~UwCYfJz*qkpm`&3bCiL)f5^iM1rG`-+FFiE{_9-Sh zZp`MvfiCswNW8!0yZ2NzNrM{_H}vKKq1;AG-AHmh#|}xh{*IOyC)&>G>Pf6iv3*Ho zTp#X8QkzH3kR$bR!xY7fjRNv>2pZxEnH$UfPenGrhz*1mK>wKuzq3|V-i=wGoljAb z)LKlO|4RxT+Qi%tnQKgmA+2UGhE_h?#6;MZF*_br(H#f^nmWw7l*(@ZH+L=^zw1OAvDKUFHxd4QGmiyC_C||Q*|1%_B|1{nS*GTtm)+P3tlz9?dM}G=<(I z?+;WX3Nc5b)@7bh60qDi+og*p#4|!}q@)6@E0MIzw6$^)B%$0rI4z1ltZoZX=c^(P z!@OkAnA}WlNNPzcmZC$FS^cp_3TWOC~|AGndGxkyOlteZ(>7Do~)3~0;DapvurT-?$W&3!*9{- z{kzO4cNG54E}8IvkdQoei)od+fGI+W5w@LkX4A->OuDW%vVefH=3oeW#gAL${ZK?Pg4&JS%(pe@gnpDSWp4w{f$JEcbfb8w9{Nbl6 zF#*Y%B;tbX-gF7Q>y% zk2&M^M6K&@i&4O`hsPilNu>B>C=YqY;qhA39gZzpQ+O(#Q7K9CJltmAN?zIOfITfV z6<)med;QPKS;?bg(O!*%g1Frr3oyuNMtHJtpQ!b`_t(EV`2#$($vyNs?+k9$2u!O^ z3=KP3x#$82Tm_V-miE7rgW)!auQovfi(Lth@d)F1m7o)@W}Tg6TZq5?yleh}Mh8y=&-aIQFm9le-;eN5 z9_(%nbFdz)%#P#P5pJGN_|4Rt8z@*Ww^!4|JsqQ?)ppX^C13vUAYjo($oyHW;-^en zcpo`B+DEM7(|R)rL{)xGs<=~dS>f8pWbBY8e<@o+AhI3m8_jf>G2@jB!M${@!T`ag zn;6pvE`b@k&2@%T)qqALVOuM<=}*~SQY89+f&}ENO>SqS9|z&4JJ$BL#f|x(w~k0Y zv6D`QN|g_|wqmmmU3p%!qpm*8vcdXJGxL@3C~t+5$k(g8XX<*NiSL!|#M|ofKdOOa z6(Ao~*HUNQ%qm$R-+X}CrYIn*6(L`_qNRIU_v!NPs@EDR+}7!!7=22fkX&q6xO0oCP=^o@nQJ@7mGt}u&b9r z_f>zxN|SW|UNck|br-(Bzcd#09NZiQ!GY6Yfw; zew>ku!^;DCC|WrRIiNs3_OrrbP&A37Z2`U818q4U!+|wp94tf6z@YR>3>Lio=E@$Wenk28}j{0~-`a$H4 zX9_+2yGt3g)tql@Eu;B^_j8^ab4wXVPrQUpoWr-7>6i{ z4|_FUPA^|ph{x~{)w>iXu%~^+j=XY?&xD=dud-iz!9_XLpk8gCB`VYM^!%9}5y3>O zt?16Of(>F*&>6#FJwhS0RQz5@XZRV34k!IW`{F(uO}^u$O5XtAhSQ+DXf@*8+0xxI z5!*)BKiQdS3cV>mm%j+CQAJ}%cjEmGOp7%(gWm7RcQxeU*vXZ`8jh?ccL^Pf>B7q%68=^*EzJM@KqmL zgok5}iC<`EFFK`L>uD%7H_*c1X5-jl9-#W9_F7Rf43JatfmtYCijvFQ;2dyl}dWD;eDC9;T9 z!OBteY7~d!%jH#-5`bcqhvY{h+iV3+n3AHJ;|2i_1E*T0lb*|QYgjtg|9S~k4z58??P1L6vL-h|c0m-Y1~z@;0RYEt&L^ijw5 zR}(wQ7BI^mIhQMIG!jk632#_(T<0M?sbpozepNVei*JXz43f^9O^`cdnTz^wQ$SH# zF-T#>8fl3K;s47DWiCwwktdE|{$YPK_q7?H5)Wo z>dCz~O*?25!h1}`b20Je;oG=E4_Bm7o| z7(=3E(~d9@-*b!}nE7q#UCC#Cw4~42>Jo$8Z(sG}K`Q`0L>pq+SQiald6h^0oyRFed}?#b4X)4F)hkx>J7 zkPp^$d`F**K&&)yC5@8wcSEQ2)wO`Po%dK3z`Nd`6ibwrkVwDc56nu*M(#Yif$xo) ztuaO*a(75^HiCIyzEFjo3UO_v#4JmVYwHj}S^I28mV)IMZ*egZkx`D;hH28#Sb05; zeq=;Q1enr-djL;?#;jSZEUtChMT%pB=VSbQuPLyAc1+(=sXROA6rp=jou#q?w{^4Q zjfir&ucSG-);b+21#ckB1eb8jL(A-SO8TFm`)Wbp3bbbJ+?rnJ+1px33RF(?XWImz zlT-!jlbq7C6TEz1*&$KbaauN<2akIy>+NgqA8n*4z|aGu84d;-u}^o-`J()q6ZgmS zGqEJYFX754|IS)SD6m9cAj)d;=!H5q`wr%jO6?{(JvTJ>jgFjYK1?U{_r8Z)G>2xT#p2CATNVH~Lz9%zsDXh>!zkcSGIa zMajm-jHru({`@SGFz|OcbJ&BxSAHcnwBS@is60AV%{k|uZqmh5*w>}J+{8Ctb|L>{ z8I@;Pw9ODk(Y4BbrAK7^jF1>F7;sPwOAG0@maCqdpzG6kKa~;40N{wsQt`tt;zaet{i=cKOCS-zO^`P z{i@j(&}G(r+y~!}mKslHdJS?zC^{YHby!+qdRlPY!ZcNUZZE6$K`qe64m~&s{hBrqGd_`{k!oSh8{PTqt?k+PkCbFa>4&jyZ-r&&|?nTG=({S+~2R(2;vnN4;gqt#>ngUb| z70x-S7Lrv1u-8>*W4&bq(b*q*29n4AhQRFyb5n#L#S8W;=%E#j2HIo_Guk7ZTd{b= zVGNCb`U4Qe4TcGZ&D=Lv*=jn>cw~sZL$A-zVvl*sX!zZoHm=gnV7h7eX%&1Qx(wwr zG+RVYk|tJ16+?`9;7b4kp1SJO&vP2=270=jf}Cu~>S=%YQ+4w~FRMBcIM&YH+X|WJ zA!IO)@tX@qT5YR5!K^kdW)zI!Nlarmmr#ZP;8D!w^MA1$m4?MUvD;aO+W+M}cKwjK zQnTu;*Am+s`v$GaY2Mv^l&8|^VgXXCAddv;UWVe!b4&tAhv_0F-(g^hAJ?5v@&@RC zU@W0(mIBg9KTAW_4cm<@*|q{9N411i)rYg76{MWxSXI?nbAe=*D{=K9IquuztV$7l zJRQHZlDs%y{$iO_fCGJ+_)*u+Qp@o}roz7Dgn zBDY43(oU$Ao{N8*PLX+!zuGhom_9YIt0~JMjI4i-!cGzkLvq^)e?Xmgl;9*pSgC+~}6aum*$P&g zD~LcPKA&TVuv5-cFWyr=9puO&k1!Y{9PbY>?NnSC`t>-+%KNYG{p+VH)_sxY@)wK*1TuGRU%uqZ>VH5pjm5z;YO%Zgv|`04ezolmWBTk*}5 zH3}ehME7B}uD<0FvQ*qUM)-;K&&-B0$`&0qO~R36V~-4~G2qaPqr@I9kI6kxjFLn% z-K;-b6L3Qxf5+49xuEMGLjiI``LtH(;pN7;xi|#~FPTyl5`{5JlAp@XbPzEw{x(>u z(9<&c(+L~LcFI1{n;neVJrUY;GT(^!#>#Idf!V>y5i-?SSG5+uE3}^ZfB=p-V6w2Z z6X==9cj-+~aU*LNd-DKNqe499#9Y8K!SZ4U9F(IVODebNk4z+Y zsgDp?K&fpW!X2U`evF7rwhc$$=Prj;C`o{<>YXUsH>9-f=M2dq?ct6SJT=MLr!G-g8`8O;}06s?LRNRLz+3o^= zhzjLR+#m-_GrxR%{tX~d102|+%0kG$Xdm;XRrzD0~YOStkZ> zaRkyLkJ+75#2QL2=+!idePpLiws%~pYi^99EG&KYicN3fWvariirgOq2#^O0e&A|6 zr8<$CeZVhM)LThA!>b=#mp7^i8TYU(W(G>7QPSpSOEzul+6CpZL{>J&K(0g+%a^?` zd6+0s-6y2$5&=TQrg)@*7uO1);8HiFlMRkQK;(nY5=OEv3B?fKH8L|KWV{zPeIo}! z1pI2ACvh++=*{rEmTHe~FLbw46#*8 zma%aXJUa+=xuFh<1B-JNV;K1nLyP9yfOo!(=e#-UtWw%EQgVXo30@`kPUf4r*!MG6 zrVNvjuD96#hwE^j$+*;MR|tVXkZJ;+>_GjKk{^hkSi?5%5!G(DHIOGODGM>;lMmXL zo)xRD9yT{K%IXT2Kv6fxTRbuKHuW)zR+O!Pz|-opEi zCE~VtnWfr=i^o!C1RROqAa{lTLTP1ZdE#BRLqH@7Oga6we5v!y?GAdl%*8G>l22K}VtlZHIK;iy>}5Nj&0-D5h&pM@L6_fs-xw~df8 z@AGV290&S$(a^&mlVD%R=yn!h{0sVs?~%zO>sY~sV8K;Dk^~qpP#$NwVEkzyU;qlj$PX!B7F9jp>HiYka4e15W9j*rDR|F9IY&JR2a<( zJ7bcTEz}`#`0lh`@`zej4)>WIReHw71!>Ic(rwU*TuG80BYNv008(lde9}ejbO6nC znsDwdqQ6Tc--m^V!wN`G<1(g)Na2r%y^I(5wDG4^N^9eTUCJ2Iex5HxuC$3Y)P2IX z=)*h@ZDAYx`wxeCsn(34kQ_BQI4bmW@BjbCfNuUK=#PEcC#@}y6c`}`z+h4~oILo{ zsTzP-5Ni2%dGCzo;G&QsA8L#J=O-?o-P;f?0}@8mJ~v`?ar>d_0uNUZjDc#YE|p!T z+Px)eBeH50-H#ARKJ)P+pv{L%yb0Y_PcrlxmAzwZEVY>S$+)aU_Y32{3R!l2H$f)I z4Wt4@yr&W;EMnI+*riImf>~U$6%G9VB7}1oE2|N|qfR0y14lF+DRwN!&v7$B)%;6~ znNPfaxnBPiSbuRZa6Vxv7!^E|KF<8w>GH((3aV^d z02m zjRc#{d=VZPcY%MZi|cuqsALO!n&g#jgf>Eid}zuC0kyu+TQ6kvwQC}75lRwyFInVU zcmnwB;25}?)0etVR=nZucZ4uO6RJxR`*zQI6}i99r_u{SD!LF7$lD&9`#Avz2UYxM z-Jqo(FzUpba~PRb{=6v_*Ak#yOLG@CA@k8{;LbE$lE^z`M_~~}+im|8N24wun>|O8 z>5O>ugac_n7wp4gR=3(e92KIIu#!bVL7GEKbzxAe9*?hOCb^wm@fHK0bzbbydQSDa z-(qRu+_9~mgEDt9@o7sb=&!J7JSUD#mF^3Grz#^uWi=kcJLHWH7I5X_wNdISq%F#z zPJhoahFv2MX{g;EM6r?D`0<61y?kbbf4ASnk9kGm(*cMK0&a+%=IKV2d$+-eYthfu`!w;O7q_6 zc73Y24arjA7B#FL#xQ0VyDJs$p^NH~S;aXwOwGA+EV zduaPbs{@y#CC+g*zV|moMCqwZhG#uU0c}+55t7D_4>bC6}hx)q0JA60{Hd`{66iTl~S zSut^N$=4Yhko!T{ffqQARKG$d;2q;OZZQ1wT(yuiu65~w?}@g(iaF~|oc|vDi7P9V z-WUDSt!n-(4~<<(FK0|fnHYAmXWqpj~bnnce9*gS1`scSA>I_KU5#Ub)WPxP(L zqhLxI#BMZ5z9%?>y1C5md&Jyl_mGiF*VEC7wjZ-z9@Xm4ByMl1iQC|%(xkOOt(HIs zk7m{d^3eYOKk^x!b@@}tV?7$GoY8m!S)7<+C>ZhnNw{g0%q(!}U?0E&*B#<})sUf) zdZM#>rX#2H;SidOMcw>=V-3u|{l4FRU7xEoG+o-F(s`D?odrOg60&Z7Acy4S&hz^a zg{8vt-O46mbL)_#P1mJ!hbs1cfN%)VIHf%?#|UZkd__x$thFRkLF^0en|#+a&&i!0 z6dX(dA3XzcqDx)I*<|~x)W`Z5%qV*1wpwv)u1w0E( z2uXAON)~v)hi8{Ayho#khf(}@0{k{yIU(CRF5NKil`9~p%2Dp&_zF_#nNaq3V`0az z9lEJC>_Nkna#LpVXYT?yk577b0~ol%=Ip~*2CFrLT9R9E{h&N+x=uNe68pNV+U=n_ z{wwQ0g6mxJ2(j*CU{FVr5n1tY33rufzkrHmLXc%_l1LFUNLttlcxOm8Xk%-42TP&;Fk1B5wp4~uWPP5!=DdJYWflXKdu@G0Ye`(mz+5puP^*1N}^ zE%4^Vf<>|PXv0=dnFpuiI$&Pd{cs~b;NQ{kzS?$($fCWw0~~Z>=>U5iu1{PYtQiR> zBSkZ8fvo(G&4yJwDeqh;5 ztz@ne(wZc_#`A&ts2>w-+=zq8-5F&{alZumkC{xNd*7vP^5>BqOEMMBYrPduGTi@Q za08L7JgyJ!tyeD^$Kcv>DYcp(Ij4?&l>rD%!vR?dKwg8S;s?k#$ks6K4EGz2{j#H@j!COxZqsTc7#{-5Zmc;A&dT4w`sqI|HU4?Ii3q} zVpU1lA_zbSg$4Rg3m(#YCMV@5YT>TxZ^`MHM{7&>*@u=No_wR_7CCEKZu$ILbT6k4cC`x))PDtFw?2f$nL56=av!8{yQMR*>+nC5QvU5!<4Kr^^H%Le}sN74=ZyPS@cK=a&du#&QgsqzQF zF$D{Ffi##rdG@zq=lI08mw!b*b3q4yK9-;3u2iAA{(h)Te^X#nLt;E}rKO1fY!zgR zrg|c1w;H(!N<+e1?Z4*2ooJO7@$yz)4f0*sm&O_0$TuAHe1V`0t6Xs3SG!u+B7wR~ zi|+kXV}c`IvrUJ^K7LrVz>4AdlsPYbf*+bq_2YSBLsCLR9Y2Te^5{1PciWwllI}fn zscVeZI99p8V7t?mZq#!2EF+bJ0D^W(Y7_0|4C6W^@r3jpHHeF{e)Wa=^q)tb;kiQc zHe&9W;k5@;I7$bz(lcj#!{UaZxxJHTHX6@YMzdAb1|r~*2ZHTX9$4{xYn{=+B~xdf z0E3gE((1iCiy<}ROWhbSYvsjY#0>mP#Y4rx^yLZWlDZ4X<~zCmWp9LmKbuNyVf8k7}3>!G@j zf?f&u5j|wtW7;3X|EL-N$3HK%OXAf!8^3Z^J+Ezj)GeHWRUFl@6eCy+Zc@Cw{T4%5an)AT5|ns;0P20dJuL= zI-(`yV}NNEq^Hc-8Yn9u0^5jN>)cm6RVc=QK(*>{sJQ2@|Lska>6m4(08T)$zyEcQ z_{iwh49ZJGLPTc)GW;>N85R;`SSlhz$~!BGX4yS-Hl@gv8hRvH*TGfFNxta6P$fcw zLuQeMF8j53?xV=TPaUC0YUwH{)Dzutu7LcM#~7u=g+`Nq9fY=BZN0{6>v{Mnk7~&F zIRTTT-o`-xIeXF_>FGmNoy&_re%a`GtxFC2kW%6`F}6NwNki6L0JGbzd7rrc_-u^3 z{h2U>UU4ds3C+es`BYOr$=_aMhA9lda~~#08CrsVUUL5*+PGQ`3kB53X?*9kEXAS? z<==bR9F8vue5LnE_oa6q@R6&d;>lI@Clvs0fq?HNtG4_4E3Wd!yBW~f@#Mrrm1|I9 z{34IfQIUrE;Y%-)V`n1}?jTNa!ar4^D6JX+6yamhHAwFTu5{Y_Pw>8y@m7Xg_6y@^ zQw9{;CiNr?XTHfwRZa@S50a8Qsg%(z}tIn25yI#gw zJViA4WfMWqt{#V9VyP_zh+=exm-0R>A(VvOaG&`2=kPKx5er$u7?mhX#0j8dFsg3j zr08FPpn4sDW!7=p*;|N~>kDO2Vme`Eg04IS2Pbg{u=pW8zs)-^wYWS4M^3B64OBEa0U1NmPHO9~ z0`tr!hq-d9%|E4dhxcUP+$!-6|%*>lW z8g|=uYlgGMaLpKKf^f%}Eleh^49@o{7y`}x^UJ5$4KsC+jvf}B%>u8mWAu~+GmVW~ zH3O{BMYC3ZtpHhAe%BoQE$SU8OdRxdq&o}8VHKOlK}BE|-RC-EE+n}42h$N+1M&FJ z>cJ=8-9N8+hVz0sHU?`2RX)egE4*0TkuAo?snUp2n0j+^bAqG8A%^FUZ`z8!(k`l6Zq-?9$5m~T8x%S@%YN45fRYnF- zlux3(NNYM9XP%bq!Vw*h$ zwWTHGgn=+l-9z{2@xy)0<*}CI7>n_9SM2fH6ip&JvSn0I2uCnZvbcb}XaT+j0lskP z*p3h`Vj@3F5<^5;7>OvfnZUP`lorDr<-m3?y{qetd{}CsN<+IGv0`X2Njim#fJHMa z#diU2_}(FqDSlh?kgQ1&0S*>;aDX74c^;LN+l-$u66UU=td_pV1)Uk@hl-C-acS(| z?d`z&AAmBVgOrSJQ=PkYA9Ew3xHt-EpCokme<>4dhKl>*)+(Gm0)oW|2) z8GPs7tz;CuH!P4Pas)S%?o4M<=2QnUpHZNn`}HHAWNnnmDpkZnSJmJ9lDg9UO2KM% zji|%j(gQR?>TJf3&6#3bbD=l%PWj;qOrif_NS2hCgp7O}*NtWiQZl^|erd|K7aS3XkygOTT3fW>Ny!LyTqq7D2wbg*rhdgr zf_WuD*;kLnrFK4qn~nFk15-w89$z70YrW9RrkxUKZHV z>%b-c92Q~Mk&OPC*mYtT=d!~`#Oiup4qd*YWk7ltR}?n)DHxQ75>D}mZ6{Hqps>2+ z5y~g?c$gj1ns|@KroRZ>YNJEt%T0sdnvWM4NDI$#lk+iY3^TE=|2|JxGA7Kv=vF>K z$|I3_TV`RNzDUducGR;Ya z0rFA4e9g|v1n(wt5s@C=_zzTGC0uKB3TpU1j2#doTCx1?^o~C^O|DREmu5!!<`<9hIJC2GnhU;To*=ADl^U@G_;y`nWV!Yh(P=)=v8ZU49|WIs#lh zeXQEiLM^L4b$W(2t#KC@pYt}o_4>3-=KP-!;CjF>$G{%g#i=hFeGwh?kdxavlwOF0 zNZg|^(at85_LkH}dcQ0LpQ1RaoFBP`h>ya~OfoSc&Ux0h66gjLpiEE$O+OiZZ*d5U z4uu+5tY=Y1^ArIM)KN|<*ckr6KgpT8y^fMp#$Y7LPMom|zsx+b3a_^BoieMLMTQGv zuYP2oP(iq(Am0wvC{dMw+@Q0(FyaMET6e7PX;`4VHB&6cAa;>QD5wA~J4px)j*%L; z(({&<26_ciAS<3TyP5hEB6%ikbxNNuV#lFTmC34v{M{wnK0Dqs9+)s;X#CK-iuV6R z?kh%3fhs772XYVllh^5xvXo_`g=_90v%z3Ux_hgjEdn|4|9fOp$OB;14z5%TZ};`* zN=eEJnfZt$!+f?s>3_&jg1#llBQy=Y3iKc#x#0dyG`v-$sgm7Rt6by!IM1u-2J-Xv z_k$It$O-(7Y6ZR24Ed?$1WjGzaQ_i{xRvD-d8q8-4BuJ$a^;@|UD84O3|MD=8q&$? zjb-HYDnvD>Z%p;1!y=CSUsB}JHZ+0MIko?%&K;r)3XhK+958RM;7ILpG2N~>4NK?p zS=mj?GAFNmrPJosk4fbM%ufFuC5S6LW>qWhG=zz5cAIrk#<{aLsd=EPOz;?3jg0@X zHe~s$+|zJ45fHDsmTa9=rQBLBqwFRJP1xj!5Oy}rl4!QL?0Cp^zsUCNmlHkxEgbTF z|91A;j>KbM0z|TXe73XA^DCu+eQelrYAg7EM{x=_sAV{95`KU|9#2&KMH&^xF^ny? zJe_E_m9bcVHrjG0{xD*`C@i9iyt?fyv1;SX?max^o>4R4!68atri9NEs^m7f+U$+c zVvPo$Pl%tsqV<}P-Z<*-fS*aUZIpBd?tGW&teK*fP@cSeshbw9INRM#2(8v^?{r?(|v$#<8uCE=PR3S;fduBk5 zgtQJtz{gK; zm|6q7IBe+?%n;1Tq;9yM4+!pHH>Fx6ydt5qRI}3CVzv zO@m9m;yhn?oBl|PjjXUo!#74|c~G$J>hRcn71V{3zeJR(1qeyON@HGEfR{nLKaxvQ zMN(*p^v9a_QxQj@Wt?@}lp<^cB83>IuVZc*L`#z*XT7X76y$fsiGm9f#nt}i6#~AK z6-*=KPEgcR81m$FV3lF!Ly})I{ym7?r>YDRKsz<&#yH?h?1RK4Kj1IPj|Bg+oBnj) z)TY!q1FBD-W{Yax000$iKPyDKu|Kq%2x?I-+|i>$VKE!FL!Zm?#wn@Lhq*|&iN}}$ zB1!JHq4d#C{dn4NOy2XNJgz$K9h#m&UbxpD2C2)o>cO;XDryPPrmI$W;3xnWym{Cd z>H)5@DmUo#CtYUKaF{Cn*z`mrxD%}zuV`S2`_qA^tyx6qwErvOYy6lNRfP$ri{gjN z(g5)dPC zcy#={;sP-6y~%t%4E=h1!<;9;pVdw`q{v}iE{&>8@RDqm2eRVr0~J_1V`U}1VHV}H zIihPf;L_Sd>-JtG6xwR~_0a71}*5^&z zM!qYtpNeHz@qT}d!*C@t-OW>NVD*D{_su0lEv!j!-X%)4lU>0oArX%v)x(f@Bs4pD zD2wmwI!ZpolhX6&;})E?LiX1Ju+_#KVPgjz>6A=_qLPD2 ztY6!18C?zj1O!U@YnkX}U1B*A?LtIs<8ZB|O88fkWd*vvx^b2iutiNE>QW*@6lzjD z;rk7l92Fg=clWc9RL3O_3jofWO_EE}&1~{uIh5GcNv6K}1d)T|(-RPe-7~R$B`zs7 zG9nnWdp6SWBQK@6v6gq79N*+akzgB`;pQ&mFGL;M1L*Oi07F16*-XNfe#X9cDTbU| zTxc6BIxdt-NYfn@xp^$_8%iootDR%9@H&jnyT}T4Q*fp|l`8qB%DPDNI*sm0U@orh z2KACDjZZ82*?9YuLQ4Sos9#^vHQ4P7<3N0PWwbypA@N=@a@jJX{n=0`yn&yLFVcZ*A?SE(10|B1F%awk zr=I8e%_ZUrJ^I_DjE&1XU02HU?9V&vqx~(KE-MNThl(6)t%I3z!r}F9WW3)5SJ5$s z*a_Fv7YfC7o%h6M;{3!afLNUWD2EITawmcpN0uB-&{-;m&zxZnHC=gQ09}pzg?6{1 z7`28E_OKV0)q{djMX^&&c9Zq6tpZajd!RTBw+EvtNWzy@6f3uXu!GoUUWA5Q&)m1Y zhxp`d5~3tUPd`4WFy*%_gck6rY7h^70sUNMO<2pu-485VTV2|Gwg}qw!3?67GQ4h{ zFxV?c1ho~$Zl#KO%;cJHGwx^frjYLEkgPgdIsH5qvB12Nvy=g+_ERxl(EBXgiQuT_ zh^RWHUEgZ7QEwKm(n1{I;sdyn;HdAPxV}bJBME9=x0*J5;+RsE zf<-dCc{%qHBoR^Dr(>fqnV5BsG5-;WDNckaA9*&24poA~dUnrP-%7g}*GKlT*Po_1 z8#C{}q_Bq%&dn4*^AHEgB^a7cO@HAw7#X5z>Xc$N8XN|_Mr$EGHFPt|S9S9Ovmb9* zj>t&Dv9=Oc;o|7|ZHf$pEd$E}jC=clTH0%NnUe5H#te{(YmifaYPd5H1JM3^XEgJo zsS51ua7%4fjXXqzxU~A2Qob`Bi$(6^U9se;VTh*qoz*RQb$1BfB(Nk;W) zAVM1gN6t#ta`QLCa0+a+XH;9HMq?j0-@!?+k*AID^M{%2U{8MtT6jDNuEV}uGJqAX z^jgxwpuDDxv2DE}+duHNgnL|hjCKQ*lInv*Iuy4XW)k`%3TObiY;Mo>2a7sQ59GtK zOVv7MpA0j9_qo5tT6Tn*H?FzcvuEzhV^_g(!70^UG6K;cz{Xf%k#(6M|1-2u)MJtW z9$hLP;8=Io6!ze$dU))gtQpPghr%uw9KV)+g0uxB-%!@AgO;}y!1t%w8)8tT|KA%A z^FNkXccphVk z!PvUx`#EqyEocvGqt( zuoUR3Rq1y^BK}5Fb4Xn}lp6;evdCY#k>vObBTzt>JN%p7R`a{iqN9FF64v_mSbvC9 zi2&T&rC?Po^V}^p3%rddBzH&owN*Iv>OZ=^WYo(EE2m2#Fi%a3Y4hX+Y{nH@To3d% zgfD5(fKrl=5)XUD749MiMR==lRTW7Sv4djuZZ7X;$Ax8*Fi0uAluvWoKh9+%wbYub zMtt&xkNq@K=iwuX$%kD=T~(xtk? z7Lg@SaVNepoHG8gAEUAfuq6J~fT?@DZc;@yx&>-?F_`o(zNYEy$0Q~*iAPsl^$R=9 zm3*1WjL^foO&4heRDlt&A42+HLL8T$;RWmFceh&Z=%~H1#a_F?vfPDi_LO)*1mk*# z>6=A7Y7V87nYcP5Hx?YA0vwXaa{wmsuBBl^K7f$2JNfN*nu-KQ%!u{oOWj5L4+j^o znik*f;TQB;5U6sc)bm0(dLT0D-Rvg{^Ewuod?3^v2z->z8?}#Ev!$QWR=SP#1OKZ} zT;_sM0R@6yo!X5oKIZf$Y9!PT7Lh9coo|r_YK6s_R@$w~;8S_t*`z^sjq9ZMWMeEi z(1XHe%{-Jfoq5EFX8?_lNYCVSlW!45Bg-a=A1`d^DDspZV>X6QZ06^r_l=fMfs?1? zo-1w7vR8;#AfES-en?of-U5cv`=B9gR`wnR$q6=ooB}P-#RMt0x{E@u#oV~^`PJ)P z@v^x!N+>Be_xf^K$Wtdx=5ZJg`Y$s&2 zcLg=mGK6gSh*&l5vE!qk_*VzJb8&nw;_%j+FF8vFZE2rC;RVd2`+fD1p`#Du2@N9^ z8q5jZY^DzIl6$7sNU!$LXkn*r=v%>kn^WXikXcQe-!-qI)HnfD=@3ABpJzbUf`!S~ zjjp4aUs zc8{HVi8N?YRl2bNfBu`e`vKH=bWb@LI}s{JOs>?y`l95>Nu>HYXL;mzXT1!T*s-ge zgg<0;zk6l6)zlO0nc}{bYg)f8KVSfaabfTN01oyE@T>xXj^ELh&bKSmq(?m-HiMi% ziVUjf*nbrAnKo$_%AgBaQZB+SQ3OjDI7KFaUsE?NDX0ZZ{DfpVDRI_BlzVq^`8yXUf7%JhY)K%&)506 z6xbBK8NNcOo)+ssfRiBqm0k>AU|j3F&ps8GyBoKG{K(}Ol$~ioOBdf;44lPC^wH*E zYOqB0bhB0!)shO-zNMHwF^yWr^1S%}7Z7+v=)0F5#|fz`0meFo0#J>tqe_zBDG#E@ zr5cB9ipOsFwZ*CF1rVHOm6bNH(K?e17MByqzfIWHdn_zPAU`6?0yb8E65f@!U6Ox z3v{%Sszww}+4nfRo33_5$&gm88`Q?M4r*aJRE)S76C^HADQ{8gw=4mi)r?gVL5>8D zDQ+MdYtG})$crFNc65HF>ka5KaxrAY4DqR-ErmQC`po8vxMi+B63&ojic5yny9?Um zk$isRPcs2TaA2sS4yIfYH>5NWSx|XZ9867Iy5o4(ep%AX-`U|FF48u>Zht6K&znfW z7xvQMl6Gc(-SL>IJYub_5Aj3X?DT+!pDS$G$%m7e-Ps#^r`NY-8S4oe zpc08tc0nJ`x%XYL?37!6{K$A}80%;^$i0UFpY2&KZ78GuXiffFwXmn-eb9UUBwjaQ zlJ^!}1EM)`hz4HWL>MVuSE>k1p!tTM^$+E}<&E(@pG~8kh!?LqKKo*lFNbbAVrpiv zOQ|CV=o6pm@jtxm4O3X&?CBKsG`yqI;$1*B>09dxl742qkr5;_>&|~I@uwL$h$Ujb zGba;ZVpv8zK1r{t$Orkl4^O3O0@aQKQp3CAbz+=w2uEDide^lc{hste33~d_qg6mQ zYlItL0j934)nrHDQM_i85;k~QFP%k`1Lry3HxGEoN`A;7k>w4FyWW!Ep)vX|1Pz5o|^njZM%Zd!!3Z#DeoErB45(-} zX9oK!1F56Qn%z1}G7)euWY#drZ(jx<_xWS^$T_D4t!vSZO<8IO^Uw}7Ak^L8&=MY% zm=_-4y`DSFIS8vOHV_Io z-inIw6JkHCD5}_uOTj4yxuV;eDK(2QUsyscsU`YI$l777T)o$hZ#&;a?{sp#pB5*E zyqMQHqIzuAQiFDwIT_!(7j?dWaf%(a<^kJ*x7D@u==NfmUan015S1|p1;25gKfOJ+ z-$Vi(`L2_D&jk@}Vr6T82&JTL--ohCUU* zL(AN-F`<-7x=NLmgyYnz%WE^uDYOqYyVSD&e4Xtv(RrTIme)7- zNb(DC!<_q^!;(gQBYEuy6BGw4j6^$uf1V*$1Qq`;pPG~b_rmGmv-635!1W-w|K(3} z65u__^NHu?1TrcZXSGmq2=T9Q)bMO2E6oYuZ}{DexJE$`SX(-+GHUCLcXi)@9T6i% zNbuCmN;GCquGz+QE!l-jr^J${?*-J!UgMTyr6CP+!7dz*U-x>C%2!U6Hkf=wc6FPz z6jOgZ4#t9zN!+W6tXX1zT8!^f3v<+CI0Prg1k{wce9w*>+*j9cITi@Ko#Zu`FOgDc zOLi<*y=V%Qa!S3w5x1Y(Ct%_n!el$s`SF-0`lvPAglw%gdYXNM&vm0&h22X9-P^^E zwe8=+g0`L!AyRw401S(C^iaIb`rtpTnYFn)Sw;ZVoY5C_C3sWrg~H<<&F)e&0P5`u zA04q}vsCl0vp@=^Wg?+G0X(VYJC1q2CNb9?h4SDOgu#tV1?G0K+Rs(C#Ht}qC(TuJ zOMY$`{wiXk)84}8zLJd2XfG<~5pZ5;e3;=bhJZkimN18=e}Vfy#F9Ov4mfd9*D$+` z(Y91c$;C7xr`NkWVS|UNocSo_wN^(TTMSvy^@qmd>6d~IyVnaj^)AV zg131U&iHUTX!aZ_agj*)aH=QcdsWD*aV@wLrYJH*@FlSAD7%uvbH|aGrSVjTk~}$M8ms-m;M$50rhH23$918ROs}RyM#G z^G8h~h;4M@i1dBERJLO)D!Z((4U~;L2nu8~+cd9OT$d6qA!UF9J!l?WF$K4l@8|iR z6F%igkJX43W#mpHOo|jT7|yU|G86}R6UPruKrtY<8=axKrco;$>?xO?|^%TO=rarv4w<#*Ou3vIj5+!tB#I*RHF<<~qAU zQufm7ecoP}{LVv~UPuhUeA)Vu6V#}q0&U{Rn2@*I)dhOb)Qc0+$;yWZW;lU~$KpYo zxgfiGsbttMj~ICTX7><6O= zpVn6PYt!Vd@o7w^L1W3|xu3!n;idsVvS*VL*Cb%r zK@qVxqb=h7jm?V0IL3gL$dM{rJR|FK|Dp|#pH>_h>ulZ7NK5Xd*rHU>bu0Ni9Rj_d zyKB-ZEXmz$9cfz6vPBVF&szM8qboOEY{`5+OCJYPEwBar`c5)l|TT!64`Og zuhzCKo?fXMntt&pnSkHT5_hmfa3aIM1iJ%~6j2ptXJbD+-yQ(NE)I!5FTJA+!Iv

qtDt9nrWKS(1qN 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

s`MiNiC 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

, P>; diff --git a/db4-storage/src/pages/edge_store.rs b/db4-storage/src/pages/edge_store.rs index c59ff84056..2072d3b4c8 100644 --- a/db4-storage/src/pages/edge_store.rs +++ b/db4-storage/src/pages/edge_store.rs @@ -355,22 +355,28 @@ impl, EXT: Clone + Send + Sync> EdgeStorageI ) } + /// Retrieve the segment for an edge given its EID + pub fn get_edge_segment(&self, eid: EID) -> Option<&Arc> { + let (segment_id, _) = resolve_pos(eid, self.max_page_len); + self.segments.get(segment_id) + } + pub fn get_edge(&self, e_id: ELID) -> Option<(VID, VID)> { let layer = e_id.layer(); let e_id = e_id.edge; - let (chunk, local_edge) = resolve_pos(e_id, self.max_page_len); - let page = self.segments.get(chunk)?; - page.get_edge(local_edge, layer, page.head()) + let (segment_id, local_edge) = resolve_pos(e_id, self.max_page_len); + let segment = self.segments.get(segment_id)?; + segment.get_edge(local_edge, layer, segment.head()) } pub fn edge(&self, e_id: impl Into) -> ES::Entry<'_> { let e_id = e_id.into(); - let (page_id, local_edge) = resolve_pos(e_id, self.max_page_len); - let page = self + let (segment_id, local_edge) = resolve_pos(e_id, self.max_page_len); + let segment = self .segments - .get(page_id) + .get(segment_id) .expect("Internal error: page not found"); - page.entry(local_edge) + segment.entry(local_edge) } pub fn num_edges(&self) -> usize { diff --git a/db4-storage/src/properties/props_meta_writer.rs b/db4-storage/src/properties/props_meta_writer.rs index eac64eb861..39fc29c8aa 100644 --- a/db4-storage/src/properties/props_meta_writer.rs +++ b/db4-storage/src/properties/props_meta_writer.rs @@ -3,9 +3,17 @@ use raphtory_api::core::entities::properties::{ meta::{LockedPropMapper, Meta, PropMapper}, prop::{Prop, unify_types}, }; +use raphtory_api::core::storage::dict_mapper::MaybeNew; use crate::error::DBV4Error; +// TODO: Rename constant props to metadata +#[derive(Debug, Clone, Copy)] +pub enum PropType { + Temporal, + Constant, +} + pub enum PropsMetaWriter<'a, PN: AsRef> { Change { props: Vec>, @@ -13,7 +21,7 @@ pub enum PropsMetaWriter<'a, PN: AsRef> { meta: &'a Meta, }, NoChange { - props: Vec<(usize, Prop)>, + props: Vec<(PN, usize, Prop)>, }, } @@ -56,31 +64,39 @@ impl<'a, PN: AsRef> PropsMetaWriter<'a, PN> { .unwrap_or_default(); let mut no_type_changes = true; + + // See if any type unification is required while merging props for (prop_name, prop) in props { let dtype = prop.dtype(); let outcome @ (_, _, type_check) = locked_meta .fast_proptype_check(prop_name.as_ref(), dtype) .map(|outcome| (prop_name, prop, outcome))?; let nothing_to_do = type_check.map(|x| x.is_right()).unwrap_or_default(); + no_type_changes &= nothing_to_do; in_props.push(outcome); } + // If no type changes are required, we can just return the existing prop ids if no_type_changes { - return Ok(Self::NoChange { - props: in_props - .into_iter() - .filter_map(|(prop_name, prop, _)| { - Some((locked_meta.get_id(prop_name.as_ref())?, prop)) - }) - .collect(), - }); + let props = in_props + .into_iter() + .filter_map(|(prop_name, prop, _)| { + locked_meta + .get_id(prop_name.as_ref()) + .map(|id| (prop_name, id, prop)) + }) + .collect(); + + return Ok(Self::NoChange { props }); } let mut props = vec![]; + for (prop_name, prop, outcome) in in_props { props.push(Self::as_prop_entry(prop_name, prop, outcome)); } + Ok(Self::Change { props, mapper: locked_meta, @@ -114,29 +130,63 @@ impl<'a, PN: AsRef> PropsMetaWriter<'a, PN> { } pub fn into_props_temporal(self) -> Result, DBV4Error> { - self.into_props_inner(|mapper| mapper.temporal_prop_meta()) + self.into_props_inner(PropType::Temporal) + } + + /// Returns temporal prop names, prop ids and prop values, along with their MaybeNew status. + pub fn into_props_temporal_with_status( + self, + ) -> Result>, DBV4Error> { + self.into_props_inner_with_status(PropType::Temporal) } pub fn into_props_const(self) -> Result, DBV4Error> { - self.into_props_inner(|mapper| mapper.const_prop_meta()) + self.into_props_inner(PropType::Constant) } - pub fn into_props_inner( + /// Returns constant prop names, prop ids and prop values, along with their MaybeNew status. + pub fn into_props_const_with_status( self, - mapper_fn: impl Fn(&Meta) -> &PropMapper, - ) -> Result, DBV4Error> { + ) -> Result>, DBV4Error> { + self.into_props_inner_with_status(PropType::Constant) + } + + pub fn into_props_inner(self, prop_type: PropType) -> Result, DBV4Error> { + self.into_props_inner_with_status(prop_type).map(|props| { + props + .into_iter() + .map(|maybe_new| { + let (_, prop_id, prop) = maybe_new.inner(); + (prop_id, prop) + }) + .collect() + }) + } + + pub fn into_props_inner_with_status( + self, + prop_type: PropType, + ) -> Result>, DBV4Error> { match self { - Self::NoChange { props } => Ok(props), + Self::NoChange { props } => Ok(props + .into_iter() + .map(|(prop_name, prop_id, prop)| MaybeNew::Existing((prop_name, prop_id, prop))) + .collect()), Self::Change { props, mapper, meta, } => { let mut prop_with_ids = vec![]; + drop(mapper); - let mut mapper = mapper_fn(meta).write_locked(); - // revalidate + let mut mapper = match prop_type { + PropType::Temporal => meta.temporal_prop_meta().write_locked(), + PropType::Constant => meta.const_prop_meta().write_locked(), + }; + + // Revalidate prop types let props = props .into_iter() .map(|entry| match entry { @@ -144,12 +194,14 @@ impl<'a, PN: AsRef> PropsMetaWriter<'a, PN> { let new_entry = mapper .fast_proptype_check(name.as_ref(), prop.dtype()) .map(|outcome| Self::as_prop_entry(name, prop, outcome))?; + Ok(new_entry) } PropEntry::Change { name, prop, .. } => { let new_entry = mapper .fast_proptype_check(name.as_ref(), prop.dtype()) .map(|outcome| Self::as_prop_entry(name, prop, outcome))?; + Ok(new_entry) } }) @@ -157,8 +209,8 @@ impl<'a, PN: AsRef> PropsMetaWriter<'a, PN> { for entry in props { match entry { - PropEntry::NoChange(_, prop_id, prop) => { - prop_with_ids.push((prop_id, prop)); + PropEntry::NoChange(name, prop_id, prop) => { + prop_with_ids.push(MaybeNew::Existing((name, prop_id, prop))); } PropEntry::Change { name, @@ -166,20 +218,25 @@ impl<'a, PN: AsRef> PropsMetaWriter<'a, PN> { prop, .. } => { + // prop_id already exists, so we need to unify the types let new_prop_type = prop.dtype(); let existing_type = mapper.get_dtype(prop_id).unwrap(); let new_prop_type = unify_types(&new_prop_type, existing_type, &mut false)?; + mapper.set_id_and_dtype(name.as_ref(), prop_id, new_prop_type); - prop_with_ids.push((prop_id, prop)); + prop_with_ids.push(MaybeNew::Existing((name, prop_id, prop))); } PropEntry::Change { name, prop, .. } => { + // prop_id doesn't exist, so we need to create a new one let new_prop_type = prop.dtype(); let prop_id = mapper.new_id_and_dtype(name.as_ref(), new_prop_type); - prop_with_ids.push((prop_id, prop)); + + prop_with_ids.push(MaybeNew::New((name, prop_id, prop))); } } } + Ok(prop_with_ids) } } diff --git a/db4-storage/src/wal/entries.rs b/db4-storage/src/wal/entries.rs deleted file mode 100644 index e7da2d1cc8..0000000000 --- a/db4-storage/src/wal/entries.rs +++ /dev/null @@ -1,124 +0,0 @@ -use raphtory_api::core::entities::properties::prop::Prop; -use raphtory_core::{ - entities::{EID, GID, VID}, - storage::timeindex::TimeIndexEntry, -}; -use serde::{Deserialize, Serialize}; -use std::borrow::Cow; - -use crate::wal::LSN; - -#[derive(Debug, Serialize, Deserialize)] -pub enum WalEntry<'a> { - AddEdge(AddEdge<'a>), - AddNodeID(AddNodeID), - AddConstPropIDs(Vec>), - AddTemporalPropIDs(Vec>), - AddLayerID(AddLayerID), - Checkpoint(Checkpoint), -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct AddEdge<'a> { - pub t: TimeIndexEntry, - pub src: VID, - pub dst: VID, - pub eid: EID, - pub layer_id: usize, - pub t_props: Cow<'a, Vec<(usize, Prop)>>, - pub c_props: Cow<'a, Vec<(usize, Prop)>>, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct AddNodeID { - pub gid: GID, - pub vid: VID, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct AddNodeTypeID { - pub name: String, - pub id: usize, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct AddConstPropID<'a> { - pub name: Cow<'a, str>, - pub id: usize, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct AddTemporalPropID<'a> { - pub name: Cow<'a, str>, - pub id: usize, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct AddLayerID { - pub name: String, - pub id: usize, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct Checkpoint { - pub lsn: LSN, -} - -// Constructors -impl<'a> WalEntry<'a> { - pub fn add_edge( - t: TimeIndexEntry, - src: VID, - dst: VID, - eid: EID, - layer_id: usize, - t_props: Cow<'a, Vec<(usize, Prop)>>, - c_props: Cow<'a, Vec<(usize, Prop)>>, - ) -> WalEntry<'a> { - WalEntry::AddEdge(AddEdge { - t, - src, - dst, - eid, - layer_id, - t_props, - c_props, - }) - } - - pub fn add_node_id(gid: GID, vid: VID) -> WalEntry<'static> { - WalEntry::AddNodeID(AddNodeID { gid, vid }) - } - - pub fn add_const_prop_ids(props: Vec<(Cow<'a, str>, usize)>) -> WalEntry<'a> { - WalEntry::AddConstPropIDs( - props - .into_iter() - .map(|(name, id)| AddConstPropID { name, id }) - .collect(), - ) - } - - pub fn add_temporal_prop_ids(props: Vec<(Cow<'a, str>, usize)>) -> WalEntry<'a> { - WalEntry::AddTemporalPropIDs( - props - .into_iter() - .map(|(name, id)| AddTemporalPropID { name, id }) - .collect(), - ) - } - - pub fn add_layer_id(name: String, id: usize) -> WalEntry<'static> { - WalEntry::AddLayerID(AddLayerID { name, id }) - } -} - -impl<'a> WalEntry<'a> { - pub fn to_bytes(&self) -> Result, postcard::Error> { - postcard::to_stdvec(self) - } - - pub fn from_bytes(bytes: &[u8]) -> Result, postcard::Error> { - postcard::from_bytes(bytes) - } -} diff --git a/db4-storage/src/wal/entry.rs b/db4-storage/src/wal/entry.rs new file mode 100644 index 0000000000..e028b65cdb --- /dev/null +++ b/db4-storage/src/wal/entry.rs @@ -0,0 +1,108 @@ +use std::path::Path; + +use raphtory_api::core::{entities::properties::prop::Prop, storage::dict_mapper::MaybeNew}; +use raphtory_core::{ + entities::{EID, GID, VID}, + storage::timeindex::TimeIndexEntry, +}; + +use crate::wal::no_wal::NoWal; +use crate::{ + error::DBV4Error, + wal::{GraphReplayer, GraphWal, LSN, TransactionID}, +}; + +impl GraphWal for NoWal { + type ReplayEntry = (); + + fn log_begin_transaction(&self, _transaction_id: TransactionID) -> Result { + Ok(0) + } + + fn log_end_transaction(&self, _transaction_id: TransactionID) -> Result { + Ok(0) + } + + fn log_add_static_edge( + &self, + _transaction_id: TransactionID, + _t: TimeIndexEntry, + _src: VID, + _dst: VID, + ) -> Result { + Ok(0) + } + + fn log_add_edge( + &self, + _transaction_id: TransactionID, + _t: TimeIndexEntry, + _src: VID, + _dst: VID, + _eid: EID, + _layer_id: usize, + _props: &[(usize, Prop)], + ) -> Result { + Ok(0) + } + + fn log_node_id( + &self, + _transaction_id: TransactionID, + _gid: GID, + _vid: VID, + ) -> Result { + Ok(0) + } + + fn log_edge_id( + &self, + _transaction_id: TransactionID, + _src: VID, + _dst: VID, + _eid: EID, + _layer_id: usize, + ) -> Result { + Ok(0) + } + + fn log_const_prop_ids>( + &self, + _transaction_id: TransactionID, + _props: &[MaybeNew<(PN, usize, Prop)>], + ) -> Result { + Ok(0) + } + + fn log_temporal_prop_ids>( + &self, + _transaction_id: TransactionID, + _props: &[MaybeNew<(PN, usize, Prop)>], + ) -> Result { + Ok(0) + } + + fn log_layer_id( + &self, + _transaction_id: TransactionID, + _name: &str, + _id: usize, + ) -> Result { + Ok(0) + } + + fn log_checkpoint(&self, _lsn: LSN) -> Result { + Ok(0) + } + + fn replay_iter(_dir: impl AsRef) -> impl Iterator> { + std::iter::once(Ok((0, ()))) + } + + fn replay_to_graph( + _dir: impl AsRef, + _graph: &mut G, + ) -> Result<(), DBV4Error> { + todo!() + } +} diff --git a/db4-storage/src/wal/mod.rs b/db4-storage/src/wal/mod.rs index b001750e07..265d0678be 100644 --- a/db4-storage/src/wal/mod.rs +++ b/db4-storage/src/wal/mod.rs @@ -1,11 +1,16 @@ -use std::path::Path; - use crate::error::DBV4Error; +use raphtory_api::core::{entities::properties::prop::Prop, storage::dict_mapper::MaybeNew}; +use raphtory_core::{ + entities::{EID, GID, VID}, + storage::timeindex::TimeIndexEntry, +}; +use std::path::Path; -pub mod entries; +pub mod entry; pub mod no_wal; pub type LSN = u64; +pub type TransactionID = u64; #[derive(Debug)] pub struct WalRecord { @@ -13,27 +18,20 @@ pub struct WalRecord { pub data: Vec, } -pub trait WalOps { +/// Core Wal methods. +pub trait Wal { fn new(dir: impl AsRef) -> Result where Self: Sized; + /// Returns the directory the WAL is stored in. fn dir(&self) -> &Path; - /// Reserves and returns the next available LSN without writing any data. - fn reserve(&self) -> LSN; - - /// Appends data to the WAL with the specified LSN. - /// The LSN must have been previously reserved. - fn append_with_lsn(&self, lsn: LSN, data: &[u8]) -> Result<(), DBV4Error>; - /// Appends data to the WAL and returns the assigned LSN. - /// This is a convenience method that combines reserve() and append_with_lsn(). - fn append(&self, data: &[u8]) -> Result { - let lsn = self.reserve(); - self.append_with_lsn(lsn, data)?; - Ok(lsn) - } + fn append(&self, data: &[u8]) -> Result; + + /// Immediately flushes in-memory WAL entries to disk. + fn sync(&self) -> Result<(), DBV4Error>; /// Blocks until the WAL has fsynced the given LSN to disk. fn wait_for_sync(&self, lsn: LSN); @@ -42,5 +40,182 @@ pub trait WalOps { /// `cutoff_lsn` acts as a hint for which records can be safely discarded during rotation. fn rotate(&self, cutoff_lsn: LSN) -> Result<(), DBV4Error>; + /// Returns an iterator over the wal entries in the given directory. fn replay(dir: impl AsRef) -> impl Iterator>; } + +// Raphtory-specific logging & replay methods. +pub trait GraphWal { + /// ReplayEntry represents the type of the wal entry returned during replay. + type ReplayEntry; + + fn log_begin_transaction(&self, transaction_id: TransactionID) -> Result; + + fn log_end_transaction(&self, transaction_id: TransactionID) -> Result; + + /// Log a static edge addition. + /// + /// # Arguments + /// + /// * `transaction_id` - The transaction ID + /// * `t` - The timestamp of the edge addition + /// * `src` - The source vertex ID + /// * `dst` - The destination vertex ID + fn log_add_static_edge( + &self, + transaction_id: TransactionID, + t: TimeIndexEntry, + src: VID, + dst: VID, + ) -> Result; + + /// Log an edge addition to a layer with temporal props. + /// + /// # Arguments + /// + /// * `transaction_id` - The transaction ID + /// * `t` - The timestamp of the edge addition + /// * `src` - The source vertex ID + /// * `dst` - The destination vertex ID + /// * `eid` - The edge ID + /// * `layer_id` - The layer ID + /// * `props` - The temporal properties of the edge + fn log_add_edge( + &self, + transaction_id: TransactionID, + t: TimeIndexEntry, + src: VID, + dst: VID, + eid: EID, + layer_id: usize, + props: &[(usize, Prop)], + ) -> Result; + + fn log_node_id( + &self, + transaction_id: TransactionID, + gid: GID, + vid: VID, + ) -> Result; + + fn log_edge_id( + &self, + transaction_id: TransactionID, + src: VID, + dst: VID, + eid: EID, + layer_id: usize, + ) -> Result; + + /// Log constant prop name -> prop id mappings. + /// + /// # Arguments + /// + /// * `transaction_id` - The transaction ID + /// * `props` - A slice containing new or existing tuples of (prop name, id, value) + fn log_const_prop_ids>( + &self, + transaction_id: TransactionID, + props: &[MaybeNew<(PN, usize, Prop)>], + ) -> Result; + + /// Log temporal prop name -> prop id mappings. + /// + /// # Arguments + /// + /// * `transaction_id` - The transaction ID + /// * `props` - A slice containing new or existing tuples of (prop name, id, value). + fn log_temporal_prop_ids>( + &self, + transaction_id: TransactionID, + props: &[MaybeNew<(PN, usize, Prop)>], + ) -> Result; + + fn log_layer_id( + &self, + transaction_id: TransactionID, + name: &str, + id: usize, + ) -> Result; + + /// Logs a checkpoint record, indicating that all Wal operations upto and including + /// `lsn` has been persisted to disk. + fn log_checkpoint(&self, lsn: LSN) -> Result; + + /// Returns an iterator over the wal entries in the given directory. + fn replay_iter( + dir: impl AsRef, + ) -> impl Iterator>; + + /// Replays and applies all the wal entries in the given directory to the given graph. + fn replay_to_graph( + dir: impl AsRef, + graph: &mut G, + ) -> Result<(), DBV4Error>; +} + +/// Trait for defining callbacks for replaying from wal +pub trait GraphReplayer { + fn replay_begin_transaction( + &self, + lsn: LSN, + transaction_id: TransactionID, + ) -> Result<(), DBV4Error>; + + fn replay_end_transaction( + &self, + lsn: LSN, + transaction_id: TransactionID, + ) -> Result<(), DBV4Error>; + + fn replay_add_static_edge( + &self, + lsn: LSN, + transaction_id: TransactionID, + t: TimeIndexEntry, + src: VID, + dst: VID, + ) -> Result<(), DBV4Error>; + + fn replay_add_edge( + &self, + lsn: LSN, + transaction_id: TransactionID, + t: TimeIndexEntry, + src: VID, + dst: VID, + eid: EID, + layer_id: usize, + props: &[(usize, Prop)], + ) -> Result<(), DBV4Error>; + + fn replay_node_id( + &self, + lsn: LSN, + transaction_id: TransactionID, + gid: GID, + vid: VID, + ) -> Result<(), DBV4Error>; + + fn replay_const_prop_ids>( + &self, + lsn: LSN, + transaction_id: TransactionID, + props: &[MaybeNew<(PN, usize, Prop)>], + ) -> Result<(), DBV4Error>; + + fn replay_temporal_prop_ids>( + &self, + lsn: LSN, + transaction_id: TransactionID, + props: &[MaybeNew<(PN, usize, Prop)>], + ) -> Result<(), DBV4Error>; + + fn replay_layer_id( + &self, + lsn: LSN, + transaction_id: TransactionID, + name: &str, + id: usize, + ) -> Result<(), DBV4Error>; +} diff --git a/db4-storage/src/wal/no_wal.rs b/db4-storage/src/wal/no_wal.rs index f9bfc22829..0faed8b57e 100644 --- a/db4-storage/src/wal/no_wal.rs +++ b/db4-storage/src/wal/no_wal.rs @@ -2,14 +2,17 @@ use std::path::{Path, PathBuf}; use crate::{ error::DBV4Error, - wal::{LSN, WalOps, WalRecord}, + wal::{LSN, Wal, WalRecord}, }; +/// NoWAL is a no-op WAL implementation that discards all writes. +/// Used for in-memory only graphs. +#[derive(Debug)] pub struct NoWal { dir: PathBuf, } -impl WalOps for NoWal { +impl Wal for NoWal { fn new(dir: impl AsRef) -> Result { Ok(Self { dir: dir.as_ref().to_path_buf(), @@ -20,11 +23,11 @@ impl WalOps for NoWal { &self.dir } - fn reserve(&self) -> LSN { - 0 + fn append(&self, _data: &[u8]) -> Result { + Ok(0) } - fn append_with_lsn(&self, _lsn: LSN, _data: &[u8]) -> Result<(), DBV4Error> { + fn sync(&self) -> Result<(), DBV4Error> { Ok(()) } diff --git a/raphtory-storage/src/mutation/addition_ops.rs b/raphtory-storage/src/mutation/addition_ops.rs index 930f252f3b..c712852933 100644 --- a/raphtory-storage/src/mutation/addition_ops.rs +++ b/raphtory-storage/src/mutation/addition_ops.rs @@ -5,7 +5,7 @@ use crate::{ MutationError, }, }; -use db4_graph::WriteLockedGraph; +use db4_graph::{TransactionManager, WriteLockedGraph}; use raphtory_api::{ core::{ entities::{ @@ -23,7 +23,7 @@ use raphtory_core::{ entities::{nodes::node_ref::NodeRef, ELID}, storage::{raw_edges::WriteLockedEdges, WriteLockedNodes}, }; -use storage::Extension; +use storage::{Extension, WalImpl}; pub trait InternalAdditionOps { type Error: From; @@ -97,6 +97,20 @@ pub trait InternalAdditionOps { meta: &Meta, prop: impl Iterator, ) -> Result, Self::Error>; + + /// Validates props and returns them with their creation status (new vs existing) + fn validate_props_with_status>( + &self, + is_static: bool, + meta: &Meta, + props: impl Iterator, + ) -> Result>, Self::Error>; + + /// TODO: Not sure the below methods belong here... + + fn transaction_manager(&self) -> &TransactionManager; + + fn wal(&self) -> &WalImpl; } pub trait EdgeWriteLock: Send + Sync { @@ -265,12 +279,31 @@ impl InternalAdditionOps for GraphStorage { .map_err(MutationError::from) } + fn validate_props_with_status>( + &self, + is_static: bool, + meta: &Meta, + props: impl Iterator, + ) -> Result>, Self::Error> { + self.mutable()? + .validate_props_with_status(is_static, meta, props) + .map_err(MutationError::from) + } + fn validate_gids<'a>( &self, gids: impl IntoIterator>, ) -> Result<(), Self::Error> { Ok(self.mutable()?.validate_gids(gids)?) } + + fn transaction_manager(&self) -> &TransactionManager { + self.mutable().unwrap().transaction_manager.as_ref() + } + + fn wal(&self) -> &WalImpl { + self.mutable().unwrap().wal.as_ref() + } } pub trait InheritAdditionOps: Base {} @@ -364,6 +397,16 @@ where self.base().validate_props(is_static, meta, prop) } + #[inline] + fn validate_props_with_status>( + &self, + is_static: bool, + meta: &Meta, + props: impl Iterator, + ) -> Result>, Self::Error> { + self.base().validate_props_with_status(is_static, meta, props) + } + #[inline] fn validate_gids<'a>( &self, @@ -371,4 +414,14 @@ where ) -> Result<(), Self::Error> { self.base().validate_gids(gids) } + + #[inline] + fn transaction_manager(&self) -> &TransactionManager { + self.base().transaction_manager() + } + + #[inline] + fn wal(&self) -> &WalImpl { + self.base().wal() + } } diff --git a/raphtory-storage/src/mutation/addition_ops_ext.rs b/raphtory-storage/src/mutation/addition_ops_ext.rs index 5c460b0e8c..215e89cc3f 100644 --- a/raphtory-storage/src/mutation/addition_ops_ext.rs +++ b/raphtory-storage/src/mutation/addition_ops_ext.rs @@ -1,4 +1,5 @@ -use db4_graph::{TemporalGraph, WriteLockedGraph}; + +use db4_graph::{TemporalGraph, TransactionManager, WriteLockedGraph}; use raphtory_api::core::{ entities::properties::{ meta::Meta, @@ -8,16 +9,22 @@ use raphtory_api::core::{ }; use raphtory_core::{ entities::{ - graph::tgraph::TooManyLayers, nodes::node_ref::NodeRef, GidRef, EID, ELID, MAX_LAYER, VID, + graph::tgraph::TooManyLayers, + nodes::node_ref::NodeRef, + GidRef, EID, ELID, MAX_LAYER, VID, }, storage::{raw_edges::WriteLockedEdges, timeindex::TimeIndexEntry, WriteLockedNodes}, }; use storage::{ - pages::{node_page::writer::node_info_as_props, session::WriteSession, NODE_ID_PROP_KEY}, + pages::{ + node_page::writer::{node_info_as_props}, + session::WriteSession, + NODE_ID_PROP_KEY, + }, persist::strategy::PersistentStrategy, properties::props_meta_writer::PropsMetaWriter, resolver::GIDResolverOps, - Extension, ES, NS, + Extension, ES, NS, WalImpl }; use crate::mutation::{ @@ -311,6 +318,33 @@ impl InternalAdditionOps for TemporalGraph { Ok(prop_ids) } } + + fn validate_props_with_status>( + &self, + is_static: bool, + meta: &Meta, + props: impl Iterator, + ) -> Result>, Self::Error> { + if is_static { + let prop_ids = PropsMetaWriter::constant(meta, props) + .and_then(|pmw| pmw.into_props_const_with_status()) + .map_err(MutationError::DBV4Error)?; + Ok(prop_ids) + } else { + let prop_ids = PropsMetaWriter::temporal(meta, props) + .and_then(|pmw| pmw.into_props_temporal_with_status()) + .map_err(MutationError::DBV4Error)?; + Ok(prop_ids) + } + } + + fn transaction_manager(&self) -> &TransactionManager { + &self.transaction_manager + } + + fn wal(&self) -> &WalImpl { + &self.wal + } } fn reserve_node_id_as_prop(node_meta: &Meta, id: GidRef) -> usize { diff --git a/raphtory/src/db/api/mutation/addition_ops.rs b/raphtory/src/db/api/mutation/addition_ops.rs index 31d799ea81..0eb0912c87 100644 --- a/raphtory/src/db/api/mutation/addition_ops.rs +++ b/raphtory/src/db/api/mutation/addition_ops.rs @@ -13,8 +13,11 @@ use crate::{ errors::{into_graph_err, GraphError}, prelude::{GraphViewOps, NodeViewOps}, }; -use raphtory_api::core::entities::properties::prop::Prop; -use raphtory_storage::mutation::addition_ops::{EdgeWriteLock, InternalAdditionOps}; +use raphtory_api::core::{entities::properties::prop::Prop, storage::dict_mapper::MaybeNew::New}; +use raphtory_storage::mutation::addition_ops::{ + EdgeWriteLock, InternalAdditionOps, SessionAdditionOps, +}; +use storage::wal::{GraphWal, Wal}; pub trait AdditionOps: StaticGraphViewOps + InternalAdditionOps> { // TODO: Probably add vector reference here like add @@ -278,49 +281,119 @@ impl> + StaticGraphViewOps> Addit props: PII, layer: Option<&str>, ) -> Result, GraphError> { + // Log transaction start + let transaction_id = self.transaction_manager().begin_transaction(); let session = self.write_session().map_err(|err| err.into())?; + self.validate_gids( [src.as_node_ref(), dst.as_node_ref()] .iter() .filter_map(|node_ref| node_ref.as_gid_ref().left()), ) .map_err(into_graph_err)?; - let props = self - .validate_props( + + let props_with_status = self + .validate_props_with_status( false, self.edge_meta(), props.into_iter().map(|(k, v)| (k, v.into())), ) .map_err(into_graph_err)?; + // Log prop name -> prop id mappings + self.wal() + .log_temporal_prop_ids(transaction_id, &props_with_status) + .unwrap(); + + let props = props_with_status + .into_iter() + .map(|maybe_new| { + let (_, prop_id, prop) = maybe_new.inner(); + (prop_id, prop) + }) + .collect::>(); + let ti = time_from_input_session(&session, t)?; let src_id = self .resolve_node(src.as_node_ref()) - .map_err(into_graph_err)? - .inner(); + .map_err(into_graph_err)?; let dst_id = self .resolve_node(dst.as_node_ref()) - .map_err(into_graph_err)? - .inner(); - let layer_id = self.resolve_layer(layer).map_err(into_graph_err)?.inner(); + .map_err(into_graph_err)?; + let layer_id = self.resolve_layer(layer).map_err(into_graph_err)?; + + // Log node -> node id mappings + // FIXME: We are logging node -> node id mappings AFTER they are inserted into the + // resolver. Make sure resolver mapping CANNOT get to disk before Wal. + if let Some(gid) = src.as_node_ref().as_gid_ref().left() { + self.wal() + .log_node_id(transaction_id, gid.into(), src_id.inner()) + .unwrap(); + } + if let Some(gid) = dst.as_node_ref().as_gid_ref().left() { + self.wal() + .log_node_id(transaction_id, gid.into(), dst_id.inner()) + .unwrap(); + } + + let src_id = src_id.inner(); + let dst_id = dst_id.inner(); + + // Log layer -> layer id mappings + if let Some(layer) = layer { + self.wal() + .log_layer_id(transaction_id, layer, layer_id.inner()) + .unwrap(); + } + + let layer_id = layer_id.inner(); + + // Holds all locks for nodes and edge until add_edge_op goes out of scope let mut add_edge_op = self .atomic_add_edge(src_id, dst_id, None, layer_id) .map_err(into_graph_err)?; - let edge_id = add_edge_op.internal_add_static_edge(src_id, dst_id, 0); + // Log edge addition + let add_static_edge_lsn = self + .wal() + .log_add_static_edge(transaction_id, ti, src_id, dst_id) + .unwrap(); + let edge_id = add_edge_op.internal_add_static_edge(src_id, dst_id, add_static_edge_lsn); + + // Log edge -> edge id mappings + // NOTE: We log edge id mappings after they are inserted into edge segments. + // This is fine as long as we hold onto segment locks for the entire operation. + let add_edge_lsn = self + .wal() + .log_add_edge( + transaction_id, + ti, + src_id, + dst_id, + edge_id.inner(), + layer_id, + &props, + ) + .unwrap(); let edge_id = add_edge_op.internal_add_edge( ti, src_id, dst_id, edge_id.map(|eid| eid.with_layer(layer_id)), - 0, + add_edge_lsn, props, ); add_edge_op.store_src_node_info(src_id, src.as_node_ref().as_gid_ref().left()); add_edge_op.store_dst_node_info(dst_id, dst.as_node_ref().as_gid_ref().left()); + // Log transaction end + self.transaction_manager().end_transaction(transaction_id); + + // Flush all wal entries to disk. + self.wal().sync().unwrap(); + Ok(EdgeView::new( self.clone(), EdgeRef::new_outgoing(edge_id.inner().edge, src_id, dst_id).at_layer(layer_id), diff --git a/raphtory/src/db/api/storage/storage.rs b/raphtory/src/db/api/storage/storage.rs index 70135e7798..efd2e5f1b6 100644 --- a/raphtory/src/db/api/storage/storage.rs +++ b/raphtory/src/db/api/storage/storage.rs @@ -7,7 +7,7 @@ use crate::{ Base, InheritViewOps, }, }; -use db4_graph::{TemporalGraph, WriteLockedGraph}; +use db4_graph::{TemporalGraph, TransactionManager, WriteLockedGraph}; use parking_lot::RwLockWriteGuard; use raphtory_api::core::{ entities::{properties::meta::Meta, EID, VID}, @@ -27,7 +27,7 @@ use std::{ }; use storage::{ segments::{edge::MemEdgeSegment, node::MemNodeSegment}, - Extension, + Extension, WalImpl, }; use tracing::info; @@ -457,12 +457,31 @@ impl InternalAdditionOps for Storage { Ok(self.graph.validate_props(is_static, meta, prop)?) } + fn validate_props_with_status>( + &self, + is_static: bool, + meta: &Meta, + props: impl Iterator, + ) -> Result>, Self::Error> { + Ok(self + .graph + .validate_props_with_status(is_static, meta, props)?) + } + fn validate_gids<'a>( &self, gids: impl IntoIterator>, ) -> Result<(), Self::Error> { Ok(self.graph.validate_gids(gids)?) } + + fn transaction_manager(&self) -> &TransactionManager { + self.graph.mutable().unwrap().transaction_manager.as_ref() + } + + fn wal(&self) -> &WalImpl { + self.graph.mutable().unwrap().wal.as_ref() + } } impl InternalPropertyAdditionOps for Storage { diff --git a/raphtory/src/db/mod.rs b/raphtory/src/db/mod.rs index 63e711afda..54e9c74f6c 100644 --- a/raphtory/src/db/mod.rs +++ b/raphtory/src/db/mod.rs @@ -1,3 +1,4 @@ pub mod api; pub mod graph; +pub mod replay; pub mod task; diff --git a/raphtory/src/db/replay/mod.rs b/raphtory/src/db/replay/mod.rs new file mode 100644 index 0000000000..a7fcc4a644 --- /dev/null +++ b/raphtory/src/db/replay/mod.rs @@ -0,0 +1,116 @@ +use db4_graph::TemporalGraph; +use raphtory_api::core::{ + entities::{properties::prop::Prop, EID, GID, VID}, + storage::{dict_mapper::MaybeNew, timeindex::TimeIndexEntry}, +}; +use raphtory_storage::mutation::addition_ops::{EdgeWriteLock, InternalAdditionOps}; +use storage::{ + api::edges::EdgeSegmentOps, + error::DBV4Error, + wal::{GraphReplayer, TransactionID, LSN}, + Extension, +}; + +/// Wrapper struct for implementing GraphReplayer for a TemporalGraph. +/// This is needed to workaround Rust's orphan rule since both ReplayGraph and TemporalGraph +/// are foreign to this crate. +#[derive(Debug)] +pub struct ReplayGraph { + graph: TemporalGraph, +} + +impl ReplayGraph { + pub fn new(graph: TemporalGraph) -> Self { + Self { graph } + } +} + +impl GraphReplayer for ReplayGraph { + fn replay_begin_transaction( + &self, + lsn: LSN, + transaction_id: TransactionID, + ) -> Result<(), DBV4Error> { + Ok(()) + } + + fn replay_end_transaction( + &self, + lsn: LSN, + transaction_id: TransactionID, + ) -> Result<(), DBV4Error> { + Ok(()) + } + + fn replay_add_static_edge( + &self, + lsn: LSN, + transaction_id: TransactionID, + t: TimeIndexEntry, + src: VID, + dst: VID, + ) -> Result<(), DBV4Error> { + Ok(()) + } + + fn replay_add_edge( + &self, + lsn: LSN, + transaction_id: TransactionID, + t: TimeIndexEntry, + src: VID, + dst: VID, + eid: EID, + layer_id: usize, + props: &[(usize, Prop)], + ) -> Result<(), DBV4Error> { + let edge_segment = self.graph.storage().edges().get_edge_segment(eid); + + match edge_segment { + Some(edge_segment) => { + edge_segment.head().lsn(); + } + _ => {} + } + + Ok(()) + } + + fn replay_node_id( + &self, + lsn: LSN, + transaction_id: TransactionID, + gid: GID, + vid: VID, + ) -> Result<(), DBV4Error> { + Ok(()) + } + + fn replay_const_prop_ids>( + &self, + lsn: LSN, + transaction_id: TransactionID, + props: &[MaybeNew<(PN, usize, Prop)>], + ) -> Result<(), DBV4Error> { + Ok(()) + } + + fn replay_temporal_prop_ids>( + &self, + lsn: LSN, + transaction_id: TransactionID, + props: &[MaybeNew<(PN, usize, Prop)>], + ) -> Result<(), DBV4Error> { + Ok(()) + } + + fn replay_layer_id( + &self, + lsn: LSN, + transaction_id: TransactionID, + name: &str, + id: usize, + ) -> Result<(), DBV4Error> { + Ok(()) + } +} From 8f5af28490fe5789722d8ebe3a89f8690229c752 Mon Sep 17 00:00:00 2001 From: Fadhil Abubaker Date: Tue, 5 Aug 2025 08:50:05 -0400 Subject: [PATCH 093/321] Rename DBV4Error to StorageError (#2209) --- db4-storage/src/api/edges.rs | 6 +-- db4-storage/src/api/nodes.rs | 6 +-- db4-storage/src/lib.rs | 2 +- db4-storage/src/loaders/mod.rs | 38 +++++++------- db4-storage/src/pages/edge_store.rs | 10 ++-- db4-storage/src/pages/mod.rs | 22 ++++---- db4-storage/src/pages/node_store.rs | 8 +-- db4-storage/src/pages/test_utils/checkers.rs | 6 +-- .../src/properties/props_meta_writer.rs | 22 ++++---- db4-storage/src/resolver/mod.rs | 4 +- db4-storage/src/segments/edge.rs | 6 +-- db4-storage/src/segments/node.rs | 6 +-- db4-storage/src/wal/entry.rs | 26 +++++----- db4-storage/src/wal/mod.rs | 52 +++++++++---------- db4-storage/src/wal/no_wal.rs | 14 ++--- .../src/mutation/addition_ops_ext.rs | 8 +-- raphtory-storage/src/mutation/mod.rs | 6 +-- raphtory/src/db/replay/mod.rs | 18 +++---- 18 files changed, 130 insertions(+), 130 deletions(-) diff --git a/db4-storage/src/api/edges.rs b/db4-storage/src/api/edges.rs index 6b45241c2e..c0a328fb77 100644 --- a/db4-storage/src/api/edges.rs +++ b/db4-storage/src/api/edges.rs @@ -12,7 +12,7 @@ use raphtory_core::{ }; use rayon::iter::ParallelIterator; -use crate::{LocalPOS, error::DBV4Error, segments::edge::MemEdgeSegment}; +use crate::{LocalPOS, error::StorageError, segments::edge::MemEdgeSegment}; pub trait EdgeSegmentOps: Send + Sync + std::fmt::Debug + 'static { type Extension; @@ -36,7 +36,7 @@ pub trait EdgeSegmentOps: Send + Sync + std::fmt::Debug + 'static { meta: Arc, path: impl AsRef, ext: Self::Extension, - ) -> Result + ) -> Result where Self: Sized; @@ -63,7 +63,7 @@ pub trait EdgeSegmentOps: Send + Sync + std::fmt::Debug + 'static { fn notify_write( &self, head_lock: impl DerefMut, - ) -> Result<(), DBV4Error>; + ) -> Result<(), StorageError>; fn increment_num_edges(&self) -> usize; diff --git a/db4-storage/src/api/nodes.rs b/db4-storage/src/api/nodes.rs index 9703c07fa6..8dca4ef661 100644 --- a/db4-storage/src/api/nodes.rs +++ b/db4-storage/src/api/nodes.rs @@ -26,7 +26,7 @@ use raphtory_core::{ use crate::{ LocalPOS, - error::DBV4Error, + error::StorageError, gen_ts::LayerIter, segments::node::MemNodeSegment, utils::{Iter2, Iter3, Iter4}, @@ -57,7 +57,7 @@ pub trait NodeSegmentOps: Send + Sync + std::fmt::Debug + 'static { edge_meta: Arc, path: impl AsRef, ext: Self::Extension, - ) -> Result + ) -> Result where Self: Sized; fn new( @@ -87,7 +87,7 @@ pub trait NodeSegmentOps: Send + Sync + std::fmt::Debug + 'static { fn notify_write( &self, head_lock: impl DerefMut, - ) -> Result<(), DBV4Error>; + ) -> Result<(), StorageError>; fn check_node(&self, pos: LocalPOS, layer_id: usize) -> bool; diff --git a/db4-storage/src/lib.rs b/db4-storage/src/lib.rs index f8675178f7..28eaa110a1 100644 --- a/db4-storage/src/lib.rs +++ b/db4-storage/src/lib.rs @@ -65,7 +65,7 @@ pub mod error { use raphtory_core::utils::time::ParseTimeError; #[derive(thiserror::Error, Debug)] - pub enum DBV4Error { + pub enum StorageError { #[error("External Storage Error {0}")] External(#[from] Arc), #[error("IO error: {0}")] diff --git a/db4-storage/src/loaders/mod.rs b/db4-storage/src/loaders/mod.rs index 9d84409234..dd74c252d9 100644 --- a/db4-storage/src/loaders/mod.rs +++ b/db4-storage/src/loaders/mod.rs @@ -1,4 +1,4 @@ -use crate::{error::DBV4Error, pages::GraphStore, EdgeSegmentOps, NodeSegmentOps}; +use crate::{error::StorageError, pages::GraphStore, EdgeSegmentOps, NodeSegmentOps}; use arrow::buffer::ScalarBuffer; use arrow_array::{ Array, PrimitiveArray, RecordBatch, TimestampMicrosecondArray, TimestampMillisecondArray, @@ -51,14 +51,14 @@ pub struct Rows { } impl Rows { - pub fn srcs(&self) -> Result { + pub fn srcs(&self) -> Result { let arr = self.rb.column(self.src); let arr = arr.as_ref(); let srcs = NodeCol::try_from(arr)?; Ok(srcs) } - pub fn dsts(&self) -> Result { + pub fn dsts(&self) -> Result { let arr = self.rb.column(self.dst); let arr = arr.as_ref(); let dsts = NodeCol::try_from(arr)?; @@ -71,8 +71,8 @@ impl Rows { pub fn properties( &self, - prop_id_resolver: impl Fn(&str, PropType) -> Result, DBV4Error>, - ) -> Result { + prop_id_resolver: impl Fn(&str, PropType) -> Result, StorageError>, + ) -> Result { combine_properties_arrow( &self.t_properties, &self.t_indices, @@ -81,7 +81,7 @@ impl Rows { ) } - fn new(rb: RecordBatch, src: usize, dst: usize, time: usize) -> Result { + fn new(rb: RecordBatch, src: usize, dst: usize, time: usize) -> Result { let (t_indices, t_properties): (Vec<_>, Vec<_>) = rb .schema() .fields() @@ -127,7 +127,7 @@ impl Rows { { arr.values().clone() } else { - return Err(DBV4Error::ArrowRS(ArrowError::CastError(format!( + return Err(StorageError::ArrowRS(ArrowError::CastError(format!( "failed to cast time column {} to i64", time_arr.data_type() )))); @@ -155,7 +155,7 @@ impl<'a> Loader<'a> { dst_col: Either<&'a str, usize>, time_col: Either<&'a str, usize>, format: FileFormat, - ) -> Result { + ) -> Result { Ok(Self { path: path.to_owned(), src_col, @@ -169,7 +169,7 @@ impl<'a> Loader<'a> { &self, path: &Path, rows_per_batch: usize, - ) -> Result> + Send>, DBV4Error> { + ) -> Result> + Send>, StorageError> { match &self.format { FileFormat::CSV { delimiter, @@ -193,7 +193,7 @@ impl<'a> Loader<'a> { .with_batch_size(rows_per_batch) .build(file)?; Ok(Box::new(reader.map(move |rb| { - rb.map_err(DBV4Error::from) + rb.map_err(StorageError::from) .and_then(|rb| Rows::new(rb, src, dst, time)) }))) } @@ -205,7 +205,7 @@ impl<'a> Loader<'a> { let (src, dst, time) = self.src_dst_time_cols(&builder.schema())?; let reader = builder.build()?; Ok(Box::new(reader.map(move |rb| { - rb.map_err(DBV4Error::from) + rb.map_err(StorageError::from) .and_then(|rb| Rows::new(rb, src, dst, time)) }))) } @@ -215,7 +215,7 @@ impl<'a> Loader<'a> { pub fn iter( &self, rows_per_batch: usize, - ) -> Result> + Send>, DBV4Error> { + ) -> Result> + Send>, StorageError> { if self.path.is_dir() { let mut files = vec![]; for entry in std::fs::read_dir(&self.path)? { @@ -234,7 +234,7 @@ impl<'a> Loader<'a> { } } - fn src_dst_time_cols(&self, schema: &Schema) -> Result<(usize, usize, usize), DBV4Error> { + fn src_dst_time_cols(&self, schema: &Schema) -> Result<(usize, usize, usize), StorageError> { let src_field = match self.src_col { Either::Left(name) => schema.index_of(name)?, Either::Right(idx) => idx, @@ -260,7 +260,7 @@ impl<'a> Loader<'a> { &self, graph: &GraphStore, rows_per_batch: usize, - ) -> Result { + ) -> Result { let mut src_col_resolved: Vec = vec![]; let mut dst_col_resolved: Vec = vec![]; let mut eid_col_resolved: Vec = vec![]; @@ -282,7 +282,7 @@ impl<'a> Loader<'a> { graph .edge_meta() .resolve_prop_id(name, p_type, false) - .map_err(DBV4Error::from) + .map_err(StorageError::from) })?; let srcs = rb.srcs()?; @@ -298,7 +298,7 @@ impl<'a> Loader<'a> { .unwrap() .inner(); *resolved = id; - Ok::<(), DBV4Error>(()) + Ok::<(), StorageError>(()) })?; dst_col_resolved.resize_with(rb.num_rows(), Default::default); @@ -311,7 +311,7 @@ impl<'a> Loader<'a> { .unwrap() .inner(); *resolved = id; - Ok::<(), DBV4Error>(()) + Ok::<(), StorageError>(()) })?; eid_col_resolved.resize_with(rb.num_rows(), Default::default); @@ -344,7 +344,7 @@ impl<'a> Loader<'a> { } } - Ok::<_, DBV4Error>(()) + Ok::<_, StorageError>(()) })?; node_writers.par_iter_mut().try_for_each(|locked_page| { @@ -361,7 +361,7 @@ impl<'a> Loader<'a> { } } - Ok::<_, DBV4Error>(()) + Ok::<_, StorageError>(()) })?; // now edges diff --git a/db4-storage/src/pages/edge_store.rs b/db4-storage/src/pages/edge_store.rs index 2072d3b4c8..61971681b7 100644 --- a/db4-storage/src/pages/edge_store.rs +++ b/db4-storage/src/pages/edge_store.rs @@ -8,7 +8,7 @@ use super::{edge_page::writer::EdgeWriter, resolve_pos}; use crate::{ LocalPOS, api::edges::{EdgeRefOps, EdgeSegmentOps, LockedESegment}, - error::DBV4Error, + error::StorageError, pages::{ layer_counter::GraphStats, locked::edges::{LockedEdgePage, WriteLockedEdgePages}, @@ -173,7 +173,7 @@ impl, EXT: Clone + Send + Sync> EdgeStorageI edges_path: impl AsRef, max_page_len: usize, ext: EXT, - ) -> Result { + ) -> Result { let edges_path = edges_path.as_ref(); let meta = Arc::new(Meta::new()); @@ -201,7 +201,7 @@ impl, EXT: Clone + Send + Sync> EdgeStorageI .collect::, _>>()?; if pages.is_empty() { - return Err(DBV4Error::EmptyGraphDir(edges_path.to_path_buf())); + return Err(StorageError::EmptyGraphDir(edges_path.to_path_buf())); } let max_page = Iterator::max(pages.keys().copied()).unwrap(); @@ -219,7 +219,7 @@ impl, EXT: Clone + Send + Sync> EdgeStorageI let first_p_id = first_page.segment_id(); if first_p_id != 0 { - return Err(DBV4Error::GenericFailure(format!( + return Err(StorageError::GenericFailure(format!( "First page id is not 0 in {edges_path:?}" ))); } @@ -399,7 +399,7 @@ impl, EXT: Clone + Send + Sync> EdgeStorageI pub fn try_get_writer<'a>( &'a self, e_id: EID, - ) -> Result, ES>, DBV4Error> { + ) -> Result, ES>, StorageError> { let (segment_id, _) = resolve_pos(e_id, self.max_page_len); let page = self.get_or_create_segment(segment_id); let writer = page.head_mut(); diff --git a/db4-storage/src/pages/mod.rs b/db4-storage/src/pages/mod.rs index e925433829..29e4e27bc7 100644 --- a/db4-storage/src/pages/mod.rs +++ b/db4-storage/src/pages/mod.rs @@ -9,7 +9,7 @@ use std::{ use crate::{ LocalPOS, api::{edges::EdgeSegmentOps, nodes::NodeSegmentOps}, - error::DBV4Error, + error::StorageError, pages::{ edge_store::ReadLockedEdgeStorage, flush_thread::FlushThread, node_store::ReadLockedNodeStorage, @@ -122,7 +122,7 @@ impl< self.nodes.stats().latest().max(self.edges.stats().latest()) } - pub fn load(graph_dir: impl AsRef) -> Result { + pub fn load(graph_dir: impl AsRef) -> Result { let nodes_path = graph_dir.as_ref().join("nodes"); let edges_path = graph_dir.as_ref().join("edges"); @@ -228,7 +228,7 @@ impl< t: T, src: impl Into, dst: impl Into, - ) -> Result, DBV4Error> { + ) -> Result, StorageError> { let t = self.as_time_index_entry(t)?; self.internal_add_edge(t, src, dst, 0, []) } @@ -240,7 +240,7 @@ impl< dst: impl Into, props: Vec<(PN, Prop)>, _lsn: u64, - ) -> Result, DBV4Error> { + ) -> Result, StorageError> { let t = self.as_time_index_entry(t)?; let prop_writer = PropsMetaWriter::temporal(self.edge_meta(), props.into_iter())?; self.internal_add_edge(t, src, dst, 0, prop_writer.into_props_temporal()?) @@ -253,7 +253,7 @@ impl< dst: impl Into, lsn: u64, props: impl IntoIterator, - ) -> Result, DBV4Error> { + ) -> Result, StorageError> { let src = src.into(); let dst = dst.into(); let mut session = self.write_session(src, dst, None); @@ -264,7 +264,7 @@ impl< Ok(elid) } - fn as_time_index_entry(&self, t: T) -> Result { + fn as_time_index_entry(&self, t: T) -> Result { let input_time = t.try_into_input_time()?; let t = match input_time { InputTime::Indexed(t, i) => TimeIndexEntry::new(t, i), @@ -288,7 +288,7 @@ impl< &self, eid: impl Into, props: Vec<(PN, Prop)>, - ) -> Result<(), DBV4Error> { + ) -> Result<(), StorageError> { let eid = eid.into(); let layer = eid.layer(); let (_, edge_pos) = self.edges.resolve_pos(eid.edge); @@ -308,7 +308,7 @@ impl< node: impl Into, layer_id: usize, props: Vec<(PN, Prop)>, - ) -> Result<(), DBV4Error> { + ) -> Result<(), StorageError> { let node = node.into(); let (segment, node_pos) = self.nodes.resolve_pos(node); let mut node_writer = self.nodes.writer(segment); @@ -323,7 +323,7 @@ impl< node: impl Into, layer_id: usize, props: Vec<(PN, Prop)>, - ) -> Result<(), DBV4Error> { + ) -> Result<(), StorageError> { let node = node.into(); let (segment, node_pos) = self.nodes.resolve_pos(node); @@ -388,14 +388,14 @@ impl< } } -fn write_graph_meta(graph_dir: impl AsRef, graph_meta: GraphMeta) -> Result<(), DBV4Error> { +fn write_graph_meta(graph_dir: impl AsRef, graph_meta: GraphMeta) -> Result<(), StorageError> { let meta_file = graph_dir.as_ref().join("graph_meta.json"); let meta_file = std::fs::File::create(meta_file).unwrap(); serde_json::to_writer_pretty(meta_file, &graph_meta)?; Ok(()) } -fn read_graph_meta(graph_dir: impl AsRef) -> Result { +fn read_graph_meta(graph_dir: impl AsRef) -> Result { let meta_file = graph_dir.as_ref().join("graph_meta.json"); let meta_file = std::fs::File::open(meta_file).unwrap(); let meta = serde_json::from_reader(meta_file)?; diff --git a/db4-storage/src/pages/node_store.rs b/db4-storage/src/pages/node_store.rs index 72feb6ec94..2bb1e8b150 100644 --- a/db4-storage/src/pages/node_store.rs +++ b/db4-storage/src/pages/node_store.rs @@ -2,7 +2,7 @@ use super::{node_page::writer::NodeWriter, resolve_pos}; use crate::{ LocalPOS, api::nodes::{LockedNSSegment, NodeSegmentOps}, - error::DBV4Error, + error::StorageError, pages::{ layer_counter::GraphStats, locked::nodes::{LockedNodePage, WriteLockedNodePages}, @@ -179,7 +179,7 @@ impl, EXT: Clone> NodeStorageInner max_page_len: usize, edge_meta: Arc, ext: EXT, - ) -> Result { + ) -> Result { let nodes_path = nodes_path.as_ref(); let node_meta = Arc::new(Meta::new()); @@ -211,7 +211,7 @@ impl, EXT: Clone> NodeStorageInner .collect::, _>>()?; if pages.is_empty() { - return Err(DBV4Error::EmptyGraphDir(nodes_path.to_path_buf())); + return Err(StorageError::EmptyGraphDir(nodes_path.to_path_buf())); } let max_page = Iterator::max(pages.keys().copied()).unwrap(); @@ -236,7 +236,7 @@ impl, EXT: Clone> NodeStorageInner let first_p_id = first_page.segment_id(); if first_p_id != 0 { - return Err(DBV4Error::GenericFailure(format!( + return Err(StorageError::GenericFailure(format!( "First page id is not 0 in {nodes_path:?}" ))); } diff --git a/db4-storage/src/pages/test_utils/checkers.rs b/db4-storage/src/pages/test_utils/checkers.rs index b843e66759..d89ea1584b 100644 --- a/db4-storage/src/pages/test_utils/checkers.rs +++ b/db4-storage/src/pages/test_utils/checkers.rs @@ -16,7 +16,7 @@ use crate::{ edges::{EdgeEntryOps, EdgeRefOps, EdgeSegmentOps}, nodes::{NodeEntryOps, NodeRefOps, NodeSegmentOps}, }, - error::DBV4Error, + error::StorageError, pages::GraphStore, persist::strategy::PersistentStrategy, }; @@ -73,7 +73,7 @@ pub fn check_edges_support< let elid = eid.map(|eid| eid.with_layer(layer_id)); session.add_edge_into_layer(timestamp, *src, *dst, elid, lsn, []); - Ok::<_, DBV4Error>(()) + Ok::<_, StorageError>(()) }) .expect("Failed to add edge"); } else { @@ -90,7 +90,7 @@ pub fn check_edges_support< let elid = eid.map(|e| e.with_layer(layer_id)); session.add_edge_into_layer(timestamp, *src, *dst, elid, lsn, []); - Ok::<_, DBV4Error>(()) + Ok::<_, StorageError>(()) }) .expect("Failed to add edge"); } diff --git a/db4-storage/src/properties/props_meta_writer.rs b/db4-storage/src/properties/props_meta_writer.rs index 39fc29c8aa..f5b4e62e60 100644 --- a/db4-storage/src/properties/props_meta_writer.rs +++ b/db4-storage/src/properties/props_meta_writer.rs @@ -5,7 +5,7 @@ use raphtory_api::core::entities::properties::{ }; use raphtory_api::core::storage::dict_mapper::MaybeNew; -use crate::error::DBV4Error; +use crate::error::StorageError; // TODO: Rename constant props to metadata #[derive(Debug, Clone, Copy)] @@ -39,14 +39,14 @@ impl<'a, PN: AsRef> PropsMetaWriter<'a, PN> { pub fn temporal( meta: &'a Meta, props: impl Iterator, - ) -> Result { + ) -> Result { Self::new(meta, meta.temporal_prop_meta(), props) } pub fn constant( meta: &'a Meta, props: impl Iterator, - ) -> Result { + ) -> Result { Self::new(meta, meta.const_prop_meta(), props) } @@ -54,7 +54,7 @@ impl<'a, PN: AsRef> PropsMetaWriter<'a, PN> { meta: &'a Meta, prop_mapper: &'a PropMapper, props: impl Iterator, - ) -> Result { + ) -> Result { let locked_meta = prop_mapper.locked(); let mut in_props = props @@ -129,29 +129,29 @@ impl<'a, PN: AsRef> PropsMetaWriter<'a, PN> { } } - pub fn into_props_temporal(self) -> Result, DBV4Error> { + pub fn into_props_temporal(self) -> Result, StorageError> { self.into_props_inner(PropType::Temporal) } /// Returns temporal prop names, prop ids and prop values, along with their MaybeNew status. pub fn into_props_temporal_with_status( self, - ) -> Result>, DBV4Error> { + ) -> Result>, StorageError> { self.into_props_inner_with_status(PropType::Temporal) } - pub fn into_props_const(self) -> Result, DBV4Error> { + pub fn into_props_const(self) -> Result, StorageError> { self.into_props_inner(PropType::Constant) } /// Returns constant prop names, prop ids and prop values, along with their MaybeNew status. pub fn into_props_const_with_status( self, - ) -> Result>, DBV4Error> { + ) -> Result>, StorageError> { self.into_props_inner_with_status(PropType::Constant) } - pub fn into_props_inner(self, prop_type: PropType) -> Result, DBV4Error> { + pub fn into_props_inner(self, prop_type: PropType) -> Result, StorageError> { self.into_props_inner_with_status(prop_type).map(|props| { props .into_iter() @@ -166,7 +166,7 @@ impl<'a, PN: AsRef> PropsMetaWriter<'a, PN> { pub fn into_props_inner_with_status( self, prop_type: PropType, - ) -> Result>, DBV4Error> { + ) -> Result>, StorageError> { match self { Self::NoChange { props } => Ok(props .into_iter() @@ -205,7 +205,7 @@ impl<'a, PN: AsRef> PropsMetaWriter<'a, PN> { Ok(new_entry) } }) - .collect::, DBV4Error>>()?; + .collect::, StorageError>>()?; for entry in props { match entry { diff --git a/db4-storage/src/resolver/mod.rs b/db4-storage/src/resolver/mod.rs index 4140159c22..8389f2adb8 100644 --- a/db4-storage/src/resolver/mod.rs +++ b/db4-storage/src/resolver/mod.rs @@ -1,6 +1,6 @@ use std::path::Path; -use crate::error::DBV4Error; +use crate::error::StorageError; use raphtory_api::core::{ entities::{GidRef, GidType, VID}, storage::dict_mapper::MaybeNew, @@ -12,7 +12,7 @@ pub mod mapping_resolver; #[derive(thiserror::Error, Debug)] pub enum GIDResolverError { #[error(transparent)] - DBV4Error(#[from] DBV4Error), + StorageError(#[from] StorageError), #[error(transparent)] InvalidNodeId(#[from] InvalidNodeId), } diff --git a/db4-storage/src/segments/edge.rs b/db4-storage/src/segments/edge.rs index 895233cb36..c2bd63a881 100644 --- a/db4-storage/src/segments/edge.rs +++ b/db4-storage/src/segments/edge.rs @@ -21,7 +21,7 @@ use rayon::prelude::*; use crate::{ LocalPOS, api::edges::{EdgeSegmentOps, LockedESegment}, - error::DBV4Error, + error::StorageError, persist::strategy::PersistentStrategy, properties::PropMutEntry, segments::edge_entry::MemEdgeRef, @@ -424,7 +424,7 @@ impl>> EdgeSegmentOps for EdgeSegm _meta: Arc, _path: impl AsRef, _ext: Self::Extension, - ) -> Result + ) -> Result where Self: Sized, { @@ -474,7 +474,7 @@ impl>> EdgeSegmentOps for EdgeSegm fn notify_write( &self, _head_lock: impl DerefMut, - ) -> Result<(), DBV4Error> { + ) -> Result<(), StorageError> { Ok(()) } diff --git a/db4-storage/src/segments/node.rs b/db4-storage/src/segments/node.rs index 5e362234a5..b5dee124de 100644 --- a/db4-storage/src/segments/node.rs +++ b/db4-storage/src/segments/node.rs @@ -23,7 +23,7 @@ use super::{HasRow, SegmentContainer}; use crate::{ LocalPOS, api::nodes::{LockedNSSegment, NodeSegmentOps}, - error::DBV4Error, + error::StorageError, persist::strategy::PersistentStrategy, segments::node_entry::{MemNodeEntry, MemNodeRef}, }; @@ -421,7 +421,7 @@ impl>> NodeSegmentOps for NodeSegm _edge_meta: Arc, _path: impl AsRef, _ext: Self::Extension, - ) -> Result + ) -> Result where Self: Sized, { @@ -465,7 +465,7 @@ impl>> NodeSegmentOps for NodeSegm fn notify_write( &self, _head_lock: impl DerefMut, - ) -> Result<(), DBV4Error> { + ) -> Result<(), StorageError> { Ok(()) } diff --git a/db4-storage/src/wal/entry.rs b/db4-storage/src/wal/entry.rs index e028b65cdb..5e5d469375 100644 --- a/db4-storage/src/wal/entry.rs +++ b/db4-storage/src/wal/entry.rs @@ -8,18 +8,18 @@ use raphtory_core::{ use crate::wal::no_wal::NoWal; use crate::{ - error::DBV4Error, + error::StorageError, wal::{GraphReplayer, GraphWal, LSN, TransactionID}, }; impl GraphWal for NoWal { type ReplayEntry = (); - fn log_begin_transaction(&self, _transaction_id: TransactionID) -> Result { + fn log_begin_transaction(&self, _transaction_id: TransactionID) -> Result { Ok(0) } - fn log_end_transaction(&self, _transaction_id: TransactionID) -> Result { + fn log_end_transaction(&self, _transaction_id: TransactionID) -> Result { Ok(0) } @@ -29,7 +29,7 @@ impl GraphWal for NoWal { _t: TimeIndexEntry, _src: VID, _dst: VID, - ) -> Result { + ) -> Result { Ok(0) } @@ -42,7 +42,7 @@ impl GraphWal for NoWal { _eid: EID, _layer_id: usize, _props: &[(usize, Prop)], - ) -> Result { + ) -> Result { Ok(0) } @@ -51,7 +51,7 @@ impl GraphWal for NoWal { _transaction_id: TransactionID, _gid: GID, _vid: VID, - ) -> Result { + ) -> Result { Ok(0) } @@ -62,7 +62,7 @@ impl GraphWal for NoWal { _dst: VID, _eid: EID, _layer_id: usize, - ) -> Result { + ) -> Result { Ok(0) } @@ -70,7 +70,7 @@ impl GraphWal for NoWal { &self, _transaction_id: TransactionID, _props: &[MaybeNew<(PN, usize, Prop)>], - ) -> Result { + ) -> Result { Ok(0) } @@ -78,7 +78,7 @@ impl GraphWal for NoWal { &self, _transaction_id: TransactionID, _props: &[MaybeNew<(PN, usize, Prop)>], - ) -> Result { + ) -> Result { Ok(0) } @@ -87,22 +87,22 @@ impl GraphWal for NoWal { _transaction_id: TransactionID, _name: &str, _id: usize, - ) -> Result { + ) -> Result { Ok(0) } - fn log_checkpoint(&self, _lsn: LSN) -> Result { + fn log_checkpoint(&self, _lsn: LSN) -> Result { Ok(0) } - fn replay_iter(_dir: impl AsRef) -> impl Iterator> { + fn replay_iter(_dir: impl AsRef) -> impl Iterator> { std::iter::once(Ok((0, ()))) } fn replay_to_graph( _dir: impl AsRef, _graph: &mut G, - ) -> Result<(), DBV4Error> { + ) -> Result<(), StorageError> { todo!() } } diff --git a/db4-storage/src/wal/mod.rs b/db4-storage/src/wal/mod.rs index 265d0678be..a0752a5316 100644 --- a/db4-storage/src/wal/mod.rs +++ b/db4-storage/src/wal/mod.rs @@ -1,4 +1,4 @@ -use crate::error::DBV4Error; +use crate::error::StorageError; use raphtory_api::core::{entities::properties::prop::Prop, storage::dict_mapper::MaybeNew}; use raphtory_core::{ entities::{EID, GID, VID}, @@ -20,7 +20,7 @@ pub struct WalRecord { /// Core Wal methods. pub trait Wal { - fn new(dir: impl AsRef) -> Result + fn new(dir: impl AsRef) -> Result where Self: Sized; @@ -28,20 +28,20 @@ pub trait Wal { fn dir(&self) -> &Path; /// Appends data to the WAL and returns the assigned LSN. - fn append(&self, data: &[u8]) -> Result; + fn append(&self, data: &[u8]) -> Result; /// Immediately flushes in-memory WAL entries to disk. - fn sync(&self) -> Result<(), DBV4Error>; + fn sync(&self) -> Result<(), StorageError>; /// Blocks until the WAL has fsynced the given LSN to disk. fn wait_for_sync(&self, lsn: LSN); /// Rotates the underlying WAL file. /// `cutoff_lsn` acts as a hint for which records can be safely discarded during rotation. - fn rotate(&self, cutoff_lsn: LSN) -> Result<(), DBV4Error>; + fn rotate(&self, cutoff_lsn: LSN) -> Result<(), StorageError>; /// Returns an iterator over the wal entries in the given directory. - fn replay(dir: impl AsRef) -> impl Iterator>; + fn replay(dir: impl AsRef) -> impl Iterator>; } // Raphtory-specific logging & replay methods. @@ -49,9 +49,9 @@ pub trait GraphWal { /// ReplayEntry represents the type of the wal entry returned during replay. type ReplayEntry; - fn log_begin_transaction(&self, transaction_id: TransactionID) -> Result; + fn log_begin_transaction(&self, transaction_id: TransactionID) -> Result; - fn log_end_transaction(&self, transaction_id: TransactionID) -> Result; + fn log_end_transaction(&self, transaction_id: TransactionID) -> Result; /// Log a static edge addition. /// @@ -67,7 +67,7 @@ pub trait GraphWal { t: TimeIndexEntry, src: VID, dst: VID, - ) -> Result; + ) -> Result; /// Log an edge addition to a layer with temporal props. /// @@ -89,14 +89,14 @@ pub trait GraphWal { eid: EID, layer_id: usize, props: &[(usize, Prop)], - ) -> Result; + ) -> Result; fn log_node_id( &self, transaction_id: TransactionID, gid: GID, vid: VID, - ) -> Result; + ) -> Result; fn log_edge_id( &self, @@ -105,7 +105,7 @@ pub trait GraphWal { dst: VID, eid: EID, layer_id: usize, - ) -> Result; + ) -> Result; /// Log constant prop name -> prop id mappings. /// @@ -117,7 +117,7 @@ pub trait GraphWal { &self, transaction_id: TransactionID, props: &[MaybeNew<(PN, usize, Prop)>], - ) -> Result; + ) -> Result; /// Log temporal prop name -> prop id mappings. /// @@ -129,29 +129,29 @@ pub trait GraphWal { &self, transaction_id: TransactionID, props: &[MaybeNew<(PN, usize, Prop)>], - ) -> Result; + ) -> Result; fn log_layer_id( &self, transaction_id: TransactionID, name: &str, id: usize, - ) -> Result; + ) -> Result; /// Logs a checkpoint record, indicating that all Wal operations upto and including /// `lsn` has been persisted to disk. - fn log_checkpoint(&self, lsn: LSN) -> Result; + fn log_checkpoint(&self, lsn: LSN) -> Result; /// Returns an iterator over the wal entries in the given directory. fn replay_iter( dir: impl AsRef, - ) -> impl Iterator>; + ) -> impl Iterator>; /// Replays and applies all the wal entries in the given directory to the given graph. fn replay_to_graph( dir: impl AsRef, graph: &mut G, - ) -> Result<(), DBV4Error>; + ) -> Result<(), StorageError>; } /// Trait for defining callbacks for replaying from wal @@ -160,13 +160,13 @@ pub trait GraphReplayer { &self, lsn: LSN, transaction_id: TransactionID, - ) -> Result<(), DBV4Error>; + ) -> Result<(), StorageError>; fn replay_end_transaction( &self, lsn: LSN, transaction_id: TransactionID, - ) -> Result<(), DBV4Error>; + ) -> Result<(), StorageError>; fn replay_add_static_edge( &self, @@ -175,7 +175,7 @@ pub trait GraphReplayer { t: TimeIndexEntry, src: VID, dst: VID, - ) -> Result<(), DBV4Error>; + ) -> Result<(), StorageError>; fn replay_add_edge( &self, @@ -187,7 +187,7 @@ pub trait GraphReplayer { eid: EID, layer_id: usize, props: &[(usize, Prop)], - ) -> Result<(), DBV4Error>; + ) -> Result<(), StorageError>; fn replay_node_id( &self, @@ -195,21 +195,21 @@ pub trait GraphReplayer { transaction_id: TransactionID, gid: GID, vid: VID, - ) -> Result<(), DBV4Error>; + ) -> Result<(), StorageError>; fn replay_const_prop_ids>( &self, lsn: LSN, transaction_id: TransactionID, props: &[MaybeNew<(PN, usize, Prop)>], - ) -> Result<(), DBV4Error>; + ) -> Result<(), StorageError>; fn replay_temporal_prop_ids>( &self, lsn: LSN, transaction_id: TransactionID, props: &[MaybeNew<(PN, usize, Prop)>], - ) -> Result<(), DBV4Error>; + ) -> Result<(), StorageError>; fn replay_layer_id( &self, @@ -217,5 +217,5 @@ pub trait GraphReplayer { transaction_id: TransactionID, name: &str, id: usize, - ) -> Result<(), DBV4Error>; + ) -> Result<(), StorageError>; } diff --git a/db4-storage/src/wal/no_wal.rs b/db4-storage/src/wal/no_wal.rs index 0faed8b57e..560c372b11 100644 --- a/db4-storage/src/wal/no_wal.rs +++ b/db4-storage/src/wal/no_wal.rs @@ -1,7 +1,7 @@ use std::path::{Path, PathBuf}; use crate::{ - error::DBV4Error, + error::StorageError, wal::{LSN, Wal, WalRecord}, }; @@ -13,7 +13,7 @@ pub struct NoWal { } impl Wal for NoWal { - fn new(dir: impl AsRef) -> Result { + fn new(dir: impl AsRef) -> Result { Ok(Self { dir: dir.as_ref().to_path_buf(), }) @@ -23,22 +23,22 @@ impl Wal for NoWal { &self.dir } - fn append(&self, _data: &[u8]) -> Result { + fn append(&self, _data: &[u8]) -> Result { Ok(0) } - fn sync(&self) -> Result<(), DBV4Error> { + fn sync(&self) -> Result<(), StorageError> { Ok(()) } fn wait_for_sync(&self, _lsn: LSN) {} - fn rotate(&self, _cutoff_lsn: LSN) -> Result<(), DBV4Error> { + fn rotate(&self, _cutoff_lsn: LSN) -> Result<(), StorageError> { Ok(()) } - fn replay(_dir: impl AsRef) -> impl Iterator> { + fn replay(_dir: impl AsRef) -> impl Iterator> { let error = "Recovery is not supported for NoWAL"; - std::iter::once(Err(DBV4Error::GenericFailure(error.to_string()))) + std::iter::once(Err(StorageError::GenericFailure(error.to_string()))) } } diff --git a/raphtory-storage/src/mutation/addition_ops_ext.rs b/raphtory-storage/src/mutation/addition_ops_ext.rs index 215e89cc3f..b17fde643c 100644 --- a/raphtory-storage/src/mutation/addition_ops_ext.rs +++ b/raphtory-storage/src/mutation/addition_ops_ext.rs @@ -309,12 +309,12 @@ impl InternalAdditionOps for TemporalGraph { if is_static { let prop_ids = PropsMetaWriter::constant(meta, props) .and_then(|pmw| pmw.into_props_const()) - .map_err(MutationError::DBV4Error)?; + .map_err(MutationError::StorageError)?; Ok(prop_ids) } else { let prop_ids = PropsMetaWriter::temporal(meta, props) .and_then(|pmw| pmw.into_props_temporal()) - .map_err(MutationError::DBV4Error)?; + .map_err(MutationError::StorageError)?; Ok(prop_ids) } } @@ -328,12 +328,12 @@ impl InternalAdditionOps for TemporalGraph { if is_static { let prop_ids = PropsMetaWriter::constant(meta, props) .and_then(|pmw| pmw.into_props_const_with_status()) - .map_err(MutationError::DBV4Error)?; + .map_err(MutationError::StorageError)?; Ok(prop_ids) } else { let prop_ids = PropsMetaWriter::temporal(meta, props) .and_then(|pmw| pmw.into_props_temporal_with_status()) - .map_err(MutationError::DBV4Error)?; + .map_err(MutationError::StorageError)?; Ok(prop_ids) } } diff --git a/raphtory-storage/src/mutation/mod.rs b/raphtory-storage/src/mutation/mod.rs index 370f36b862..7968c796e3 100644 --- a/raphtory-storage/src/mutation/mod.rs +++ b/raphtory-storage/src/mutation/mod.rs @@ -18,7 +18,7 @@ use raphtory_core::entities::{ }, }; use std::sync::Arc; -use storage::{error::DBV4Error, resolver::GIDResolverError}; +use storage::{error::StorageError, resolver::GIDResolverError}; use thiserror::Error; pub mod addition_ops; @@ -53,13 +53,13 @@ pub enum MutationError { dst: String, }, #[error("Storage error: {0}")] - DBV4Error(#[from] DBV4Error), + StorageError(#[from] StorageError), } impl From for MutationError { fn from(error: GIDResolverError) -> Self { match error { - GIDResolverError::DBV4Error(e) => MutationError::DBV4Error(e), + GIDResolverError::StorageError(e) => MutationError::StorageError(e), GIDResolverError::InvalidNodeId(e) => MutationError::InvalidNodeId(e), } } diff --git a/raphtory/src/db/replay/mod.rs b/raphtory/src/db/replay/mod.rs index a7fcc4a644..ae8cf1b8cb 100644 --- a/raphtory/src/db/replay/mod.rs +++ b/raphtory/src/db/replay/mod.rs @@ -6,7 +6,7 @@ use raphtory_api::core::{ use raphtory_storage::mutation::addition_ops::{EdgeWriteLock, InternalAdditionOps}; use storage::{ api::edges::EdgeSegmentOps, - error::DBV4Error, + error::StorageError, wal::{GraphReplayer, TransactionID, LSN}, Extension, }; @@ -30,7 +30,7 @@ impl GraphReplayer for ReplayGraph { &self, lsn: LSN, transaction_id: TransactionID, - ) -> Result<(), DBV4Error> { + ) -> Result<(), StorageError> { Ok(()) } @@ -38,7 +38,7 @@ impl GraphReplayer for ReplayGraph { &self, lsn: LSN, transaction_id: TransactionID, - ) -> Result<(), DBV4Error> { + ) -> Result<(), StorageError> { Ok(()) } @@ -49,7 +49,7 @@ impl GraphReplayer for ReplayGraph { t: TimeIndexEntry, src: VID, dst: VID, - ) -> Result<(), DBV4Error> { + ) -> Result<(), StorageError> { Ok(()) } @@ -63,7 +63,7 @@ impl GraphReplayer for ReplayGraph { eid: EID, layer_id: usize, props: &[(usize, Prop)], - ) -> Result<(), DBV4Error> { + ) -> Result<(), StorageError> { let edge_segment = self.graph.storage().edges().get_edge_segment(eid); match edge_segment { @@ -82,7 +82,7 @@ impl GraphReplayer for ReplayGraph { transaction_id: TransactionID, gid: GID, vid: VID, - ) -> Result<(), DBV4Error> { + ) -> Result<(), StorageError> { Ok(()) } @@ -91,7 +91,7 @@ impl GraphReplayer for ReplayGraph { lsn: LSN, transaction_id: TransactionID, props: &[MaybeNew<(PN, usize, Prop)>], - ) -> Result<(), DBV4Error> { + ) -> Result<(), StorageError> { Ok(()) } @@ -100,7 +100,7 @@ impl GraphReplayer for ReplayGraph { lsn: LSN, transaction_id: TransactionID, props: &[MaybeNew<(PN, usize, Prop)>], - ) -> Result<(), DBV4Error> { + ) -> Result<(), StorageError> { Ok(()) } @@ -110,7 +110,7 @@ impl GraphReplayer for ReplayGraph { transaction_id: TransactionID, name: &str, id: usize, - ) -> Result<(), DBV4Error> { + ) -> Result<(), StorageError> { Ok(()) } } From deed89d11b522464198e8b1d2af5a484b67a0c6b Mon Sep 17 00:00:00 2001 From: Fadhil Abubaker Date: Fri, 8 Aug 2025 07:20:28 -0400 Subject: [PATCH 094/321] Add `test_degree_iterable` as a rust test (#2222) Add test_degree_iterable as a rust test --- raphtory/src/db/api/view/node.rs | 45 ++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/raphtory/src/db/api/view/node.rs b/raphtory/src/db/api/view/node.rs index a7f56187c4..e12c911908 100644 --- a/raphtory/src/db/api/view/node.rs +++ b/raphtory/src/db/api/view/node.rs @@ -345,3 +345,48 @@ impl<'graph, V: BaseNodeViewOps<'graph> + 'graph> NodeViewOps<'graph> for V { } impl<'graph, V: BaseNodeViewOps<'graph> + OneHopFilter<'graph>> ResetFilter<'graph> for V {} + +#[cfg(test)] +mod test { + use crate::prelude::*; + + const EDGES: [(i64, u64, u64); 6] = [ + (1, 0, 1), (2, 0, 2), (-1, 1, 0), (0, 0, 0), (7, 2, 1), (1, 0, 0) + ]; + + fn create_graph() -> Graph { + let g = Graph::new(); + + g.add_node(0, 0, [("type", Prop::from("wallet")), ("cost", Prop::from(99.5))], None).unwrap(); + g.add_node(-1, 1, [("type", Prop::from("wallet")), ("cost", Prop::from(10.0))], None).unwrap(); + g.add_node(6, 2, [("type", Prop::from("wallet")), ("cost", Prop::from(76.0))], None).unwrap(); + + for edge in EDGES { + let (t, src, dst) = edge; + + g.add_edge(t, src, dst, [("prop1", Prop::from(1)), ("prop2", Prop::from(9.8)), ("prop3", Prop::from("test"))], None).unwrap(); + } + + g + } + + #[test] + fn test_degree_iterable() { + let g = create_graph(); + + assert_eq!(g.nodes().degree().min(), Some(2)); + assert_eq!(g.nodes().degree().max(), Some(3)); + + assert_eq!(g.nodes().in_degree().min(), Some(1)); + assert_eq!(g.nodes().in_degree().max(), Some(2)); + + assert_eq!(g.nodes().out_degree().min(), Some(1)); + assert_eq!(g.nodes().out_degree().max(), Some(3)); + + assert_eq!(g.nodes().degree().sum::(), 7); + + let mut degrees = g.nodes().degree().collect::>(); + degrees.sort(); + assert_eq!(degrees, [2, 2, 3]); + } +} From 109f9a61c3717795c4065cd335a5910a6b706a55 Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Tue, 12 Aug 2025 12:36:33 +0200 Subject: [PATCH 095/321] keep the Cargo.toml --- ignore_Cargo.toml => Cargo.toml | 26 ++++---------------------- 1 file changed, 4 insertions(+), 22 deletions(-) rename ignore_Cargo.toml => Cargo.toml (85%) diff --git a/ignore_Cargo.toml b/Cargo.toml similarity index 85% rename from ignore_Cargo.toml rename to Cargo.toml index 0a0764ba31..4beba3007d 100644 --- a/ignore_Cargo.toml +++ b/Cargo.toml @@ -11,16 +11,8 @@ members = [ "raphtory-api", "raphtory-core", "raphtory-storage", - # "db4-common", - "db4-storage", - "db4-graph", -] -default-members = [ - "raphtory", - # "db4-common", - "db4-storage", - "db4-graph", ] +default-members = ["raphtory"] resolver = "2" [workspace.package] @@ -60,11 +52,9 @@ incremental = false #[public-storage] pometry-storage = { version = ">=0.8.1", path = "pometry-storage" } #[private-storage] -#pometry-storage = { path = "pometry-storage-private", package = "pometry-storage-private" } +# pometry-storage = { path = "pometry-storage-private", package = "pometry-storage-private" } async-graphql = { version = "7.0.16", features = ["dynamic-schema"] } bincode = "1.3.3" -bitvec = "1.0.1" -boxcar = "0.2.8" async-graphql-poem = "7.0.16" dynamic-graphql = "0.10.1" reqwest = { version = "0.12.8", default-features = false, features = [ @@ -136,7 +126,7 @@ crossbeam-channel = "0.5.15" base64 = "0.22.1" jsonwebtoken = "9.3.1" spki = "0.7.3" -poem = { version = "3.0.1", features = ["cookie"] } +poem = { version = "3.0.1", features = ["compression"] } opentelemetry = "0.27.1" opentelemetry_sdk = { version = "0.27.1", features = ["rt-tokio"] } opentelemetry-otlp = { version = "0.27.0" } @@ -175,26 +165,18 @@ arrow-buffer = { version = "=53.4.1" } arrow-schema = { version = "=53.4.1" } arrow-array = { version = "=53.4.1" } arrow-ipc = { version = "=53.4.1" } -arrow-csv = { version = "=53.4.1" } - -moka = { version = "0.12.7", features = ["sync"] } +moka = { version = "0.12.7", features = ["future"] } indexmap = { version = "2.7.0", features = ["rayon"] } fake = { version = "3.1.0", features = ["chrono"] } strsim = { version = "0.11.1" } uuid = { version = "1.16.0", features = ["v4"] } -raphtory = { version = "0.15.1", path = "./raphtory", default-features = false } -raphtory-api = { version = "0.15.1", path = "./raphtory-api", default-features = false } -raphtory-core = { version = "0.15.1", path = "./raphtory-core", default-features = false } -raphtory-graphql = { version = "0.15.1", path = "./raphtory-graphql", default-features = false } - # Make sure that transitive dependencies stick to disk_graph 50 [patch.crates-io] arrow-buffer = { git = "https://github.com/apache/arrow-rs.git", tag = "53.4.1" } arrow-schema = { git = "https://github.com/apache/arrow-rs.git", tag = "53.4.1" } arrow-data = { git = "https://github.com/apache/arrow-rs.git", tag = "53.4.1" } arrow-array = { git = "https://github.com/apache/arrow-rs.git", tag = "53.4.1" } -arrow-csv = { git = "https://github.com/apache/arrow-rs.git", tag = "53.4.1" } [workspace.dependencies.storage] package = "db4-storage" From 1b487ef7c9de1ea567537b8fb4c6850e6f7e74f0 Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Tue, 12 Aug 2025 14:05:28 +0200 Subject: [PATCH 096/321] start tidying up renames after merge --- db4-storage/src/api/nodes.rs | 2 +- db4-storage/src/pages/mod.rs | 2 +- db4-storage/src/pages/session.rs | 2 +- db4-storage/src/pages/test_utils/checkers.rs | 8 ++++---- .../src/properties/props_meta_writer.rs | 18 +++++++++--------- db4-storage/src/segments/edge.rs | 2 +- db4-storage/src/segments/node.rs | 4 ++-- .../src/core/entities/properties/meta.rs | 6 +++--- raphtory-api/src/core/storage/dict_mapper.rs | 2 +- raphtory-storage/src/core_ops.rs | 4 ++-- .../src/graph/edges/edge_storage_ops.rs | 2 +- .../src/mutation/addition_ops_ext.rs | 4 ++-- .../src/mutation/property_addition_ops.rs | 18 +++++++----------- 13 files changed, 35 insertions(+), 39 deletions(-) diff --git a/db4-storage/src/api/nodes.rs b/db4-storage/src/api/nodes.rs index 8dca4ef661..9fd69fb9cb 100644 --- a/db4-storage/src/api/nodes.rs +++ b/db4-storage/src/api/nodes.rs @@ -239,7 +239,7 @@ pub trait NodeRefOps<'a>: Copy + Clone + Send + Sync + 'a { .map(|w| Iter2::I1(additions.range(w).iter())) .unwrap_or_else(|| Iter2::I2(additions.iter())); - let mut time_ordered_iter = (0..self.node_meta().temporal_prop_meta().len()) + let mut time_ordered_iter = (0..self.node_meta().temporal_prop_mapper().len()) .map(move |prop_id| { self.temporal_prop_layer(layer_id, prop_id) .iter_inner(w.clone()) diff --git a/db4-storage/src/pages/mod.rs b/db4-storage/src/pages/mod.rs index 29e4e27bc7..e24e913127 100644 --- a/db4-storage/src/pages/mod.rs +++ b/db4-storage/src/pages/mod.rs @@ -189,7 +189,7 @@ impl< // Reserve node_type as a const prop on init let _ = nodes .prop_meta() - .const_prop_meta() + .metadata_mapper() .get_or_create_and_validate(NODE_TYPE_PROP_KEY, PropType::U64); let graph_meta = GraphMeta { diff --git a/db4-storage/src/pages/session.rs b/db4-storage/src/pages/session.rs index 6d610bb04c..6b0189cad1 100644 --- a/db4-storage/src/pages/session.rs +++ b/db4-storage/src/pages/session.rs @@ -47,7 +47,7 @@ impl< pub fn node_id_prop_id(&self) -> usize { self.graph .node_meta() - .const_prop_meta() + .metadata_mapper() .get_id(NODE_ID_PROP_KEY) .unwrap() } diff --git a/db4-storage/src/pages/test_utils/checkers.rs b/db4-storage/src/pages/test_utils/checkers.rs index d89ea1584b..7a57112513 100644 --- a/db4-storage/src/pages/test_utils/checkers.rs +++ b/db4-storage/src/pages/test_utils/checkers.rs @@ -276,7 +276,7 @@ pub fn check_graph_with_nodes_support< let prop_id = graph .node_meta() - .const_prop_meta() + .metadata_mapper() .get_id(name) .unwrap_or_else(|| panic!("Failed to get prop id for {name}")); let actual_props = node_entry.c_prop(0, prop_id); @@ -311,7 +311,7 @@ pub fn check_graph_with_nodes_support< for ((node, prop_name), props) in nod_t_prop_groups { let prop_id = graph .node_meta() - .temporal_prop_meta() + .temporal_prop_mapper() .get_id(prop_name) .unwrap_or_else(|| panic!("Failed to get prop id for {prop_name}")); @@ -424,7 +424,7 @@ pub fn check_graph_with_props_support< // Check temporal props let prop_id = graph .edge_meta() - .temporal_prop_meta() + .temporal_prop_mapper() .get_id(prop_name) .unwrap_or_else(|| panic!("Failed to get prop id for {prop_name}")); @@ -450,7 +450,7 @@ pub fn check_graph_with_props_support< for (name, prop) in exp_const_props { let prop_id = graph .edge_meta() - .const_prop_meta() + .metadata_mapper() .get_id(name) .unwrap_or_else(|| panic!("Failed to get prop id for {name}")); let actual_props = e.c_prop(layer_id, prop_id); diff --git a/db4-storage/src/properties/props_meta_writer.rs b/db4-storage/src/properties/props_meta_writer.rs index f5b4e62e60..10a6801c1c 100644 --- a/db4-storage/src/properties/props_meta_writer.rs +++ b/db4-storage/src/properties/props_meta_writer.rs @@ -40,14 +40,14 @@ impl<'a, PN: AsRef> PropsMetaWriter<'a, PN> { meta: &'a Meta, props: impl Iterator, ) -> Result { - Self::new(meta, meta.temporal_prop_meta(), props) + Self::new(meta, meta.temporal_prop_mapper(), props) } pub fn constant( meta: &'a Meta, props: impl Iterator, ) -> Result { - Self::new(meta, meta.const_prop_meta(), props) + Self::new(meta, meta.metadata_mapper(), props) } pub fn new( @@ -182,8 +182,8 @@ impl<'a, PN: AsRef> PropsMetaWriter<'a, PN> { drop(mapper); let mut mapper = match prop_type { - PropType::Temporal => meta.temporal_prop_meta().write_locked(), - PropType::Constant => meta.const_prop_meta().write_locked(), + PropType::Temporal => meta.temporal_prop_mapper().write_locked(), + PropType::Constant => meta.metadata_mapper().write_locked(), }; // Revalidate prop types @@ -263,7 +263,7 @@ mod test { assert_eq!(props, vec![(0, Prop::U32(0)), (1, Prop::U32(1))]); - assert_eq!(meta.temporal_prop_meta().len(), 2); + assert_eq!(meta.temporal_prop_mapper().len(), 2); } #[test] @@ -278,14 +278,14 @@ mod test { let props = writer.into_props_temporal().unwrap(); assert_eq!(props.len(), 1); - assert!(meta.temporal_prop_meta().len() == 1); - assert!(meta.temporal_prop_meta().get_id("prop1").is_some()); + assert_eq!(meta.temporal_prop_mapper().len(), 1); + assert!(meta.temporal_prop_mapper().get_id("prop1").is_some()); let writer = PropsMetaWriter::temporal(&meta, vec![(ArcStr::from("prop1"), prop2)].into_iter()); assert!(writer.is_err()); - assert!(meta.temporal_prop_meta().len() == 1); - assert!(meta.temporal_prop_meta().get_id("prop1").is_some()); + assert_eq!(meta.temporal_prop_mapper().len(), 1); + assert!(meta.temporal_prop_mapper().get_id("prop1").is_some()); } } diff --git a/db4-storage/src/segments/edge.rs b/db4-storage/src/segments/edge.rs index c2bd63a881..e3ba37601c 100644 --- a/db4-storage/src/segments/edge.rs +++ b/db4-storage/src/segments/edge.rs @@ -601,7 +601,7 @@ mod test { ); let prop_id = meta - .const_prop_meta() + .metadata_mapper() .get_or_create_and_validate("a", PropType::U8) .unwrap() .inner(); diff --git a/db4-storage/src/segments/node.rs b/db4-storage/src/segments/node.rs index b5dee124de..11b1e20899 100644 --- a/db4-storage/src/segments/node.rs +++ b/db4-storage/src/segments/node.rs @@ -585,7 +585,7 @@ mod test { // add constant properties let prop_id = node_meta - .const_prop_meta() + .metadata_mapper() .get_or_create_and_validate("a", PropType::U64) .unwrap() .inner(); @@ -608,7 +608,7 @@ mod test { // add temporal properties let prop_id = node_meta - .temporal_prop_meta() + .temporal_prop_mapper() .get_or_create_and_validate("b", PropType::F64) .unwrap() .inner(); diff --git a/raphtory-api/src/core/entities/properties/meta.rs b/raphtory-api/src/core/entities/properties/meta.rs index f4b3cd6903..7c3557cb06 100644 --- a/raphtory-api/src/core/entities/properties/meta.rs +++ b/raphtory-api/src/core/entities/properties/meta.rs @@ -66,12 +66,12 @@ impl Meta { #[inline] pub fn temporal_est_row_size(&self) -> usize { - self.meta_prop_temporal.row_size() + self.temporal_prop_mapper.row_size() } #[inline] pub fn const_est_row_size(&self) -> usize { - self.meta_prop_constant.row_size() + self.metadata_mapper.row_size() } pub fn new() -> Self { @@ -416,7 +416,7 @@ fn fast_proptype_check( if can_unify { Ok(Some(Either::Left(id))) } else { - Err(PropError::PropertyTypeError { + Err(PropError { name: prop.to_string(), expected: existing_dtype.clone(), actual: dtype, diff --git a/raphtory-api/src/core/storage/dict_mapper.rs b/raphtory-api/src/core/storage/dict_mapper.rs index 9e526cb03f..490fe9f288 100644 --- a/raphtory-api/src/core/storage/dict_mapper.rs +++ b/raphtory-api/src/core/storage/dict_mapper.rs @@ -161,7 +161,7 @@ impl WriteLockedDictMapper<'_> { impl DictMapper { pub fn contains(&self, key: &str) -> bool { - self.map.contains_key(key) + self.map.read().contains_key(key) } pub fn deep_clone(&self) -> Self { diff --git a/raphtory-storage/src/core_ops.rs b/raphtory-storage/src/core_ops.rs index a48df3a517..6209a46d21 100644 --- a/raphtory-storage/src/core_ops.rs +++ b/raphtory-storage/src/core_ops.rs @@ -235,7 +235,7 @@ pub trait CoreGraphOps: Send + Sync { fn node_metadata_ids(&self, _v: VID) -> BoxedLIter { // property 0 = node type, property 1 = external node id // on an empty graph, this will return an empty range - let end = self.node_meta().const_prop_meta().len(); + let end = self.node_meta().metadata_mapper().len(); let start = 2.min(end); Box::new(start..end) } @@ -249,7 +249,7 @@ pub trait CoreGraphOps: Send + Sync { /// # Returns /// The ids of the temporal properties fn temporal_node_prop_ids(&self, _v: VID) -> Box + '_> { - Box::new(0..self.node_meta().temporal_prop_meta().len()) + Box::new(0..self.node_meta().temporal_prop_mapper().len()) } } diff --git a/raphtory-storage/src/graph/edges/edge_storage_ops.rs b/raphtory-storage/src/graph/edges/edge_storage_ops.rs index 35544aa615..1b6da45353 100644 --- a/raphtory-storage/src/graph/edges/edge_storage_ops.rs +++ b/raphtory-storage/src/graph/edges/edge_storage_ops.rs @@ -170,7 +170,7 @@ pub trait EdgeStorageOps<'a>: Copy + Sized + Send + Sync + 'a { .map(move |id| (id, self.temporal_prop_layer(id, prop_id))) } - fn constant_prop_layer(self, layer_id: usize, prop_id: usize) -> Option; + fn metadata_layer(self, layer_id: usize, prop_id: usize) -> Option; fn metadata_iter( self, diff --git a/raphtory-storage/src/mutation/addition_ops_ext.rs b/raphtory-storage/src/mutation/addition_ops_ext.rs index b17fde643c..743eedb57a 100644 --- a/raphtory-storage/src/mutation/addition_ops_ext.rs +++ b/raphtory-storage/src/mutation/addition_ops_ext.rs @@ -350,12 +350,12 @@ impl InternalAdditionOps for TemporalGraph { fn reserve_node_id_as_prop(node_meta: &Meta, id: GidRef) -> usize { match id { GidRef::U64(_) => node_meta - .const_prop_meta() + .metadata_mapper() .get_or_create_and_validate(NODE_ID_PROP_KEY, PropType::U64) .unwrap() .inner(), GidRef::Str(_) => node_meta - .const_prop_meta() + .metadata_mapper() .get_or_create_and_validate(NODE_ID_PROP_KEY, PropType::Str) .unwrap() .inner(), diff --git a/raphtory-storage/src/mutation/property_addition_ops.rs b/raphtory-storage/src/mutation/property_addition_ops.rs index c480c14a0a..818cfa8c04 100644 --- a/raphtory-storage/src/mutation/property_addition_ops.rs +++ b/raphtory-storage/src/mutation/property_addition_ops.rs @@ -185,18 +185,18 @@ impl InternalPropertyAdditionOps for db4_graph::TemporalGraph { todo!() } - fn internal_add_constant_properties(&self, props: &[(usize, Prop)]) -> Result<(), Self::Error> { + fn internal_add_metadata(&self, props: &[(usize, Prop)]) -> Result<(), Self::Error> { todo!() } - fn internal_update_constant_properties( + fn internal_update_metadata( &self, props: &[(usize, Prop)], ) -> Result<(), Self::Error> { todo!() } - fn internal_add_constant_node_properties( + fn internal_add_node_metadata( &self, vid: VID, props: &[(usize, Prop)], @@ -207,20 +207,16 @@ impl InternalPropertyAdditionOps for db4_graph::TemporalGraph { Ok(()) } - fn internal_update_constant_node_properties( - &self, - vid: VID, - props: &[(usize, Prop)], - ) -> Result<(), Self::Error> { + fn internal_update_node_metadata(&self, vid: VID, props: &[(usize, Prop)]) -> Result>, Self::Error> { todo!() } - fn internal_add_constant_edge_properties( + fn internal_add_edge_metadata( &self, eid: EID, layer: usize, props: &[(usize, Prop)], - ) -> Result<(), Self::Error> { + ) -> Result { let (_, edge_pos) = self.storage().edges().resolve_pos(eid); let mut writer = self.storage().edge_writer(eid); let (src, dst) = writer.get_edge(layer, edge_pos).unwrap_or_else(|| { @@ -230,7 +226,7 @@ impl InternalPropertyAdditionOps for db4_graph::TemporalGraph { Ok(()) } - fn internal_update_constant_edge_properties( + fn internal_update_edge_metadata( &self, eid: EID, layer: usize, From de601b7af0ef9bfbd5751554939497093d251e87 Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Tue, 12 Aug 2025 14:31:54 +0200 Subject: [PATCH 097/321] start removing dead code --- raphtory-core/src/entities/graph/mod.rs | 1 - raphtory-core/src/entities/graph/tgraph.rs | 327 +----------------- .../src/entities/graph/tgraph_storage.rs | 84 ----- raphtory-storage/src/mutation/deletion_ops.rs | 32 -- .../src/mutation/property_addition_ops.rs | 125 +------ 5 files changed, 6 insertions(+), 563 deletions(-) delete mode 100644 raphtory-core/src/entities/graph/tgraph_storage.rs diff --git a/raphtory-core/src/entities/graph/mod.rs b/raphtory-core/src/entities/graph/mod.rs index fc072dffdb..e16922dcc9 100644 --- a/raphtory-core/src/entities/graph/mod.rs +++ b/raphtory-core/src/entities/graph/mod.rs @@ -1,4 +1,3 @@ pub mod logical_to_physical; pub mod tgraph; -pub mod tgraph_storage; pub mod timer; diff --git a/raphtory-core/src/entities/graph/tgraph.rs b/raphtory-core/src/entities/graph/tgraph.rs index e9be9ebba0..283b8a1136 100644 --- a/raphtory-core/src/entities/graph/tgraph.rs +++ b/raphtory-core/src/entities/graph/tgraph.rs @@ -1,58 +1,16 @@ -use super::logical_to_physical::{InvalidNodeId, Mapping}; -use crate::{ - entities::{ - edges::edge_store::EdgeStore, - graph::{ - tgraph_storage::GraphStorage, - timer::{MaxCounter, MinCounter, TimeCounterTrait}, - }, - nodes::{node_ref::NodeRef, node_store::NodeStore}, - properties::graph_meta::GraphMeta, - LayerIds, EID, VID, - }, - storage::{ - raw_edges::EdgeWGuard, - timeindex::{AsTime, TimeIndexEntry}, - NodeEntry, PairEntryMut, - }, -}; use dashmap::DashSet; -use either::Either; use raphtory_api::core::{ - entities::{ - properties::{meta::Meta, prop::Prop}, - GidRef, Layer, Multiple, MAX_LAYER, - }, - input::input_node::InputNode, - storage::{arc_str::ArcStr, dict_mapper::MaybeNew}, - Direction, + entities::MAX_LAYER + , + storage::arc_str::ArcStr + , }; use rustc_hash::FxHasher; -use serde::{Deserialize, Serialize}; -use std::{fmt::Debug, hash::BuildHasherDefault, sync::atomic::AtomicUsize}; +use std::{fmt::Debug, hash::BuildHasherDefault}; use thiserror::Error; pub(crate) type FxDashSet = DashSet>; -#[derive(Serialize, Deserialize, Debug)] -pub struct TemporalGraph { - pub storage: GraphStorage, - // mapping between logical and physical ids - pub logical_to_physical: Mapping, - string_pool: FxDashSet, - pub event_counter: AtomicUsize, - //earliest time seen in this graph - pub earliest_time: MinCounter, - //latest time seen in this graph - pub latest_time: MaxCounter, - // props meta data for nodes (mapping between strings and ids) - pub node_meta: Meta, - // props meta data for edges (mapping between strings and ids) - pub edge_meta: Meta, - // graph properties - pub graph_meta: GraphMeta, -} - #[derive(Error, Debug)] #[error("Invalid layer: {invalid_layer}. Valid layers: {valid_layers}")] pub struct InvalidLayer { @@ -73,278 +31,3 @@ impl InvalidLayer { } } } - -impl std::fmt::Display for TemporalGraph { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "Graph(num_nodes={}, num_edges={})", - self.storage.nodes_len(), - self.storage.edges_len() - ) - } -} - -impl Default for TemporalGraph { - fn default() -> Self { - Self::new(rayon::current_num_threads()) - } -} - -impl TemporalGraph { - pub fn new(num_locks: usize) -> Self { - TemporalGraph { - logical_to_physical: Mapping::new(), - string_pool: Default::default(), - storage: GraphStorage::new(num_locks), - event_counter: AtomicUsize::new(0), - earliest_time: MinCounter::new(), - latest_time: MaxCounter::new(), - node_meta: Meta::new(), - edge_meta: Meta::new(), - graph_meta: GraphMeta::new(), - } - } - - pub fn process_prop_value(&self, prop: &Prop) -> Prop { - match prop { - Prop::Str(value) => Prop::Str(self.resolve_str(value)), - _ => prop.clone(), - } - } - - fn get_valid_layers(edge_meta: &Meta) -> Vec { - edge_meta - .layer_meta() - .get_keys() - .iter() - .map(|x| x.to_string()) - .collect::>() - } - - pub fn num_layers(&self) -> usize { - self.edge_meta.layer_meta().len() - } - - pub fn resolve_node_inner(&self, id: NodeRef) -> Result, InvalidNodeId> { - match id { - NodeRef::External(id) => self.logical_to_physical.get_or_init_node(id, || { - let node_store = NodeStore::empty(id.into()); - self.storage.push_node(node_store) - }), - NodeRef::Internal(id) => Ok(MaybeNew::Existing(id)), - } - } - - /// map layer name to id and allocate a new layer if needed - pub fn resolve_layer_inner( - &self, - layer: Option<&str>, - ) -> Result, TooManyLayers> { - let id = self.edge_meta.get_or_create_layer_id(layer); - if let MaybeNew::New(id) = id { - if id > MAX_LAYER { - Err(TooManyLayers)?; - } - } - Ok(id) - } - - pub fn layer_ids(&self, key: Layer) -> Result { - match key { - Layer::None => Ok(LayerIds::None), - Layer::All => Ok(LayerIds::All), - Layer::Default => Ok(LayerIds::One(0)), - Layer::One(id) => match self.edge_meta.get_layer_id(&id) { - Some(id) => Ok(LayerIds::One(id)), - None => Err(InvalidLayer::new( - id, - Self::get_valid_layers(&self.edge_meta), - )), - }, - Layer::Multiple(ids) => { - let mut new_layers = ids - .iter() - .map(|id| { - self.edge_meta.get_layer_id(id).ok_or_else(|| { - InvalidLayer::new(id.clone(), Self::get_valid_layers(&self.edge_meta)) - }) - }) - .collect::, InvalidLayer>>()?; - let num_layers = self.num_layers(); - let num_new_layers = new_layers.len(); - if num_new_layers == 0 { - Ok(LayerIds::None) - } else if num_new_layers == 1 { - Ok(LayerIds::One(new_layers[0])) - } else if num_new_layers == num_layers { - Ok(LayerIds::All) - } else { - new_layers.sort_unstable(); - new_layers.dedup(); - Ok(LayerIds::Multiple(new_layers.into())) - } - } - } - } - - pub fn valid_layer_ids(&self, key: Layer) -> LayerIds { - match key { - Layer::None => LayerIds::None, - Layer::All => LayerIds::All, - Layer::Default => LayerIds::One(0), - Layer::One(id) => match self.edge_meta.get_layer_id(&id) { - Some(id) => LayerIds::One(id), - None => LayerIds::None, - }, - Layer::Multiple(ids) => { - let new_layers: Multiple = ids - .iter() - .flat_map(|id| self.edge_meta.get_layer_id(id)) - .collect(); - let num_layers = self.num_layers(); - let num_new_layers = new_layers.len(); - if num_new_layers == 0 { - LayerIds::None - } else if num_new_layers == 1 { - LayerIds::One(new_layers.get_id_by_index(0).unwrap()) - } else if num_new_layers == num_layers { - LayerIds::All - } else { - LayerIds::Multiple(new_layers) - } - } - } - } - - pub fn get_layer_name(&self, layer: usize) -> ArcStr { - self.edge_meta.get_layer_name_by_id(layer) - } - - #[inline] - pub fn graph_earliest_time(&self) -> Option { - Some(self.earliest_time.get()).filter(|t| *t != i64::MAX) - } - - #[inline] - pub fn graph_latest_time(&self) -> Option { - Some(self.latest_time.get()).filter(|t| *t != i64::MIN) - } - - #[inline] - pub fn internal_num_nodes(&self) -> usize { - self.storage.nodes.len() - } - - #[inline] - pub fn update_time(&self, time: TimeIndexEntry) { - let t = time.t(); - self.earliest_time.update(t); - self.latest_time.update(t); - } - - pub(crate) fn link_nodes_inner( - &self, - node_pair: &mut PairEntryMut, - edge_id: EID, - t: TimeIndexEntry, - layer: usize, - is_deletion: bool, - ) { - self.update_time(t); - let src_id = node_pair.get_i().vid; - let dst_id = node_pair.get_j().vid; - let src = node_pair.get_mut_i(); - let elid = if is_deletion { - edge_id.with_layer_deletion(layer) - } else { - edge_id.with_layer(layer) - }; - src.add_edge(dst_id, Direction::OUT, layer, edge_id); - src.update_time(t, elid); - let dst = node_pair.get_mut_j(); - dst.add_edge(src_id, Direction::IN, layer, edge_id); - dst.update_time(t, elid); - } - - pub fn link_edge( - &self, - eid: EID, - t: TimeIndexEntry, - layer: usize, - is_deletion: bool, - ) -> EdgeWGuard { - let (src, dst) = { - let edge_r = self.storage.edges.get_edge(eid); - let edge_r = edge_r.as_mem_edge().edge_store(); - (edge_r.src, edge_r.dst) - }; - // need to get the node pair first to avoid deadlocks with link_nodes - let mut node_pair = self.storage.pair_node_mut(src, dst); - self.link_nodes_inner(&mut node_pair, eid, t, layer, is_deletion); - self.storage.edges.get_edge_mut(eid) - } - - pub fn link_nodes( - &self, - src_id: VID, - dst_id: VID, - t: TimeIndexEntry, - layer: usize, - is_deletion: bool, - ) -> MaybeNew { - let edge = { - let mut node_pair = self.storage.pair_node_mut(src_id, dst_id); - let src = node_pair.get_i(); - let mut edge = match src.find_edge_eid(dst_id, &LayerIds::All) { - Some(edge_id) => Either::Left(self.storage.get_edge_mut(edge_id)), - None => Either::Right(self.storage.push_edge(EdgeStore::new(src_id, dst_id))), - }; - let eid = match edge.as_mut() { - Either::Left(edge) => edge.as_ref().eid(), - Either::Right(edge) => edge.value().eid, - }; - self.link_nodes_inner(&mut node_pair, eid, t, layer, is_deletion); - edge - }; - - match edge { - Either::Left(edge) => MaybeNew::Existing(edge), - Either::Right(edge) => { - let edge = edge.init(); - MaybeNew::New(edge) - } - } - } - - #[inline] - pub fn resolve_node_ref(&self, v: NodeRef) -> Option { - match v { - NodeRef::Internal(vid) => Some(vid), - NodeRef::External(GidRef::U64(gid)) => self.logical_to_physical.get_u64(gid), - NodeRef::External(GidRef::Str(string)) => self - .logical_to_physical - .get_str(string) - .or_else(|| self.logical_to_physical.get_u64(string.id())), - } - } - - /// Checks if the same string value already exists and returns a pointer to the same existing value if it exists, - /// otherwise adds the string to the pool. - fn resolve_str(&self, value: &ArcStr) -> ArcStr { - match self.string_pool.get(value) { - Some(value) => value.clone(), - None => { - self.string_pool.insert(value.clone()); - self.string_pool - .get(value) - .expect("value should exist as inserted above") - .clone() - } - } - } - - pub fn node(&self, id: VID) -> NodeEntry { - self.storage.get_node(id) - } -} diff --git a/raphtory-core/src/entities/graph/tgraph_storage.rs b/raphtory-core/src/entities/graph/tgraph_storage.rs deleted file mode 100644 index 23067d4199..0000000000 --- a/raphtory-core/src/entities/graph/tgraph_storage.rs +++ /dev/null @@ -1,84 +0,0 @@ -use crate::{ - entities::{edges::edge_store::EdgeStore, nodes::node_store::NodeStore, EID, VID}, - storage::{ - self, - raw_edges::{EdgeRGuard, EdgeWGuard, EdgesStorage, LockedEdges, UninitialisedEdge}, - EntryMut, NodeEntry, NodeSlot, NodeStorage, PairEntryMut, UninitialisedEntry, - }, -}; -use parking_lot::RwLockWriteGuard; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Deserialize, Serialize, PartialEq)] -pub struct GraphStorage { - // node storage with having (id, time_index, properties, adj list for each layer) - pub nodes: NodeStorage, - pub edges: EdgesStorage, -} - -impl GraphStorage { - pub fn new(num_locks: usize) -> Self { - Self { - nodes: storage::NodeStorage::new(num_locks), - edges: EdgesStorage::new(num_locks), - } - } - - pub fn num_shards(&self) -> usize { - self.nodes.data.len() - } - - #[inline] - pub fn nodes_read_lock(&self) -> storage::ReadLockedStorage { - self.nodes.read_lock() - } - - #[inline] - pub fn edges_read_lock(&self) -> LockedEdges { - self.edges.read_lock() - } - - #[inline] - pub fn nodes_len(&self) -> usize { - self.nodes.len() - } - - #[inline] - pub fn edges_len(&self) -> usize { - self.edges.len() - } - - #[inline] - pub fn push_node(&self, node: NodeStore) -> UninitialisedEntry { - self.nodes.push(node) - } - #[inline] - pub fn push_edge(&self, edge: EdgeStore) -> UninitialisedEdge { - self.edges.push(edge) - } - - #[inline] - pub fn get_node_mut(&self, id: VID) -> EntryMut<'_, RwLockWriteGuard<'_, NodeSlot>> { - self.nodes.entry_mut(id) - } - - #[inline] - pub fn get_edge_mut(&self, eid: EID) -> EdgeWGuard { - self.edges.get_edge_mut(eid) - } - - #[inline] - pub fn get_node(&self, id: VID) -> NodeEntry<'_> { - self.nodes.entry(id) - } - - #[inline] - pub fn edge_entry(&self, eid: EID) -> EdgeRGuard { - self.edges.get_edge(eid) - } - - #[inline] - pub fn pair_node_mut(&self, i: VID, j: VID) -> PairEntryMut<'_> { - self.nodes.pair_entry_mut(i, j) - } -} diff --git a/raphtory-storage/src/mutation/deletion_ops.rs b/raphtory-storage/src/mutation/deletion_ops.rs index 54da5cbade..06b934cc3c 100644 --- a/raphtory-storage/src/mutation/deletion_ops.rs +++ b/raphtory-storage/src/mutation/deletion_ops.rs @@ -6,7 +6,6 @@ use raphtory_api::{ }, inherit::Base, }; -use raphtory_core::entities::graph::tgraph::TemporalGraph; use storage::Extension; pub trait InternalDeletionOps { @@ -58,37 +57,6 @@ impl InternalDeletionOps for db4_graph::TemporalGraph { } } -impl InternalDeletionOps for TemporalGraph { - type Error = MutationError; - - fn internal_delete_edge( - &self, - t: TimeIndexEntry, - src: VID, - dst: VID, - layer: usize, - ) -> Result, Self::Error> { - let edge = self.link_nodes(src, dst, t, layer, true); - Ok(edge.map(|mut edge| { - let mut edge = edge.as_mut(); - edge.deletions_mut(layer).insert(t); - edge.eid() - })) - } - - fn internal_delete_existing_edge( - &self, - t: TimeIndexEntry, - eid: EID, - layer: usize, - ) -> Result<(), Self::Error> { - let mut edge = self.link_edge(eid, t, layer, true); - let mut edge = edge.as_mut(); - edge.deletions_mut(layer).insert(t); - Ok(()) - } -} - impl InternalDeletionOps for GraphStorage { type Error = MutationError; diff --git a/raphtory-storage/src/mutation/property_addition_ops.rs b/raphtory-storage/src/mutation/property_addition_ops.rs index 818cfa8c04..0eb235058d 100644 --- a/raphtory-storage/src/mutation/property_addition_ops.rs +++ b/raphtory-storage/src/mutation/property_addition_ops.rs @@ -13,7 +13,6 @@ use raphtory_api::{ }, inherit::Base, }; -use raphtory_core::entities::graph::tgraph::TemporalGraph; use raphtory_core::storage::{EntryMut, NodeSlot}; use raphtory_core::storage::raw_edges::EdgeWGuard; use storage::Extension; @@ -51,129 +50,6 @@ pub trait InternalPropertyAdditionOps { ) -> Result; } -impl InternalPropertyAdditionOps for TemporalGraph { - type Error = MutationError; - fn internal_add_properties( - &self, - t: TimeIndexEntry, - props: &[(usize, Prop)], - ) -> Result<(), Self::Error> { - if !props.is_empty() { - for (prop_id, prop) in props { - let prop = self.process_prop_value(prop); - let prop = validate_prop(prop).map_err(MutationError::from)?; - self.graph_meta - .add_prop(t, *prop_id, prop) - .map_err(MutationError::from)?; - } - self.update_time(t); - } - Ok(()) - } - - fn internal_add_metadata(&self, props: &[(usize, Prop)]) -> Result<(), Self::Error> { - for (id, prop) in props { - let prop = self.process_prop_value(prop); - let prop = validate_prop(prop).map_err(MutationError::from)?; - self.graph_meta - .add_metadata(*id, prop) - .map_err(MutationError::from)?; - } - Ok(()) - } - - fn internal_update_metadata(&self, props: &[(usize, Prop)]) -> Result<(), Self::Error> { - for (id, prop) in props { - let prop = self.process_prop_value(prop); - let prop = validate_prop(prop).map_err(MutationError::from)?; - self.graph_meta.update_metadata(*id, prop); - } - Ok(()) - } - - fn internal_add_node_metadata( - &self, - vid: VID, - props: &[(usize, Prop)], - ) -> Result>, Self::Error> { - let mut node = self.storage.get_node_mut(vid); - for (prop_id, prop) in props { - let prop = self.process_prop_value(prop); - let prop = validate_prop(prop).map_err(MutationError::from)?; - node.as_mut() - .add_metadata(*prop_id, prop) - .map_err(MutationError::from)?; - } - Ok(node) - } - - fn internal_update_node_metadata( - &self, - vid: VID, - props: &[(usize, Prop)], - ) -> Result>, Self::Error> { - let mut node = self.storage.get_node_mut(vid); - for (prop_id, prop) in props { - let prop = self.process_prop_value(prop); - let prop = validate_prop(prop).map_err(MutationError::from)?; - node.as_mut() - .update_metadata(*prop_id, prop) - .map_err(MutationError::from)?; - } - Ok(node) - } - - fn internal_add_edge_metadata( - &self, - eid: EID, - layer: usize, - props: &[(usize, Prop)], - ) -> Result { - let mut edge = self.storage.get_edge_mut(eid); - let mut edge_mut = edge.as_mut(); - if let Some(edge_layer) = edge_mut.get_layer_mut(layer) { - for (prop_id, prop) in props { - let prop = self.process_prop_value(prop); - let prop = validate_prop(prop).map_err(MutationError::from)?; - edge_layer - .add_metadata(*prop_id, prop) - .map_err(MutationError::from)?; - } - Ok(edge) - } else { - let layer = self.get_layer_name(layer).to_string(); - let src = self.node(edge.as_ref().src()).as_ref().id().to_string(); - let dst = self.node(edge.as_ref().dst()).as_ref().id().to_string(); - Err(MutationError::InvalidEdgeLayer { layer, src, dst }) - } - } - - fn internal_update_edge_metadata( - &self, - eid: EID, - layer: usize, - props: &[(usize, Prop)], - ) -> Result { - let mut edge = self.storage.get_edge_mut(eid); - let mut edge_mut = edge.as_mut(); - if let Some(edge_layer) = edge_mut.get_layer_mut(layer) { - for (prop_id, prop) in props { - let prop = self.process_prop_value(prop); - let prop = validate_prop(prop).map_err(MutationError::from)?; - edge_layer - .update_metadata(*prop_id, prop) - .map_err(MutationError::from)?; - } - Ok(edge) - } else { - let layer = self.get_layer_name(layer).to_string(); - let src = self.node(edge.as_ref().src()).as_ref().id().to_string(); - let dst = self.node(edge.as_ref().dst()).as_ref().id().to_string(); - Err(MutationError::InvalidEdgeLayer { layer, src, dst }) - } - } -} - impl InternalPropertyAdditionOps for db4_graph::TemporalGraph { type Error = MutationError; @@ -189,6 +65,7 @@ impl InternalPropertyAdditionOps for db4_graph::TemporalGraph { todo!() } + fn internal_update_metadata( &self, props: &[(usize, Prop)], From 52cd18118140d14c88b38932f3fed766ec822555 Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Tue, 12 Aug 2025 16:40:29 +0200 Subject: [PATCH 098/321] fix PropertyAdditionOps trait --- raphtory-storage/src/mutation/mod.rs | 10 ++++- .../src/mutation/property_addition_ops.rs | 39 ++++++++++--------- 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/raphtory-storage/src/mutation/mod.rs b/raphtory-storage/src/mutation/mod.rs index 6f118a325b..b9ffaa218f 100644 --- a/raphtory-storage/src/mutation/mod.rs +++ b/raphtory-storage/src/mutation/mod.rs @@ -18,14 +18,22 @@ use raphtory_core::entities::{ }, }; use std::sync::Arc; -use storage::{error::StorageError, resolver::GIDResolverError}; +use parking_lot::RwLockWriteGuard; +use storage::{error::StorageError, resolver::GIDResolverError, Extension, ES, NS}; use thiserror::Error; +use storage::pages::edge_page::writer::EdgeWriter; +use storage::pages::node_page::writer::NodeWriter; +use storage::segments::edge::MemEdgeSegment; +use storage::segments::node::MemNodeSegment; pub mod addition_ops; pub mod addition_ops_ext; pub mod deletion_ops; pub mod property_addition_ops; +pub type NodeWriterT<'a> = NodeWriter<'a, RwLockWriteGuard<'a, MemNodeSegment>, NS>; +pub type EdgeWriterT<'a> = EdgeWriter<'a, RwLockWriteGuard<'a, MemEdgeSegment>, ES>; + #[derive(Error, Debug)] pub enum MutationError { #[error(transparent)] diff --git a/raphtory-storage/src/mutation/property_addition_ops.rs b/raphtory-storage/src/mutation/property_addition_ops.rs index 0eb235058d..a7f3616782 100644 --- a/raphtory-storage/src/mutation/property_addition_ops.rs +++ b/raphtory-storage/src/mutation/property_addition_ops.rs @@ -16,6 +16,7 @@ use raphtory_api::{ use raphtory_core::storage::{EntryMut, NodeSlot}; use raphtory_core::storage::raw_edges::EdgeWGuard; use storage::Extension; +use crate::mutation::{EdgeWriterT, NodeWriterT}; pub trait InternalPropertyAdditionOps { type Error: From; @@ -30,24 +31,24 @@ pub trait InternalPropertyAdditionOps { &self, vid: VID, props: &[(usize, Prop)], - ) -> Result>, Self::Error>; + ) -> Result; fn internal_update_node_metadata( &self, vid: VID, props: &[(usize, Prop)], - ) -> Result>, Self::Error>; + ) -> Result; fn internal_add_edge_metadata( &self, eid: EID, layer: usize, props: &[(usize, Prop)], - ) -> Result; + ) -> Result; fn internal_update_edge_metadata( &self, eid: EID, layer: usize, props: &[(usize, Prop)], - ) -> Result; + ) -> Result; } impl InternalPropertyAdditionOps for db4_graph::TemporalGraph { @@ -77,14 +78,14 @@ impl InternalPropertyAdditionOps for db4_graph::TemporalGraph { &self, vid: VID, props: &[(usize, Prop)], - ) -> Result<(), Self::Error> { + ) -> Result { let (segment_id, node_pos) = self.storage().nodes().resolve_pos(vid); let mut writer = self.storage().nodes().writer(segment_id); writer.update_c_props(node_pos, 0, props.iter().cloned(), 0); - Ok(()) + Ok(writer) } - fn internal_update_node_metadata(&self, vid: VID, props: &[(usize, Prop)]) -> Result>, Self::Error> { + fn internal_update_node_metadata(&self, vid: VID, props: &[(usize, Prop)]) -> Result { todo!() } @@ -93,14 +94,14 @@ impl InternalPropertyAdditionOps for db4_graph::TemporalGraph { eid: EID, layer: usize, props: &[(usize, Prop)], - ) -> Result { + ) -> Result { let (_, edge_pos) = self.storage().edges().resolve_pos(eid); let mut writer = self.storage().edge_writer(eid); let (src, dst) = writer.get_edge(layer, edge_pos).unwrap_or_else(|| { panic!("Edge with EID {eid:?} not found in layer {layer}"); }); writer.update_c_props(edge_pos, src, dst, layer, props); - Ok(()) + Ok(writer) } fn internal_update_edge_metadata( @@ -108,14 +109,14 @@ impl InternalPropertyAdditionOps for db4_graph::TemporalGraph { eid: EID, layer: usize, props: &[(usize, Prop)], - ) -> Result<(), Self::Error> { + ) -> Result { let (_, edge_pos) = self.storage().edges().resolve_pos(eid); let mut writer = self.storage().edge_writer(eid); let (src, dst) = writer.get_edge(layer, edge_pos).unwrap_or_else(|| { panic!("Edge with EID {eid:?} not found in layer {layer}"); }); writer.update_c_props(edge_pos, src, dst, layer, props); - Ok(()) + Ok(writer) } } @@ -142,7 +143,7 @@ impl InternalPropertyAdditionOps for GraphStorage { &self, vid: VID, props: &[(usize, Prop)], - ) -> Result>, Self::Error> { + ) -> Result { self.mutable()?.internal_add_node_metadata(vid, props) } @@ -150,7 +151,7 @@ impl InternalPropertyAdditionOps for GraphStorage { &self, vid: VID, props: &[(usize, Prop)], - ) -> Result>, Self::Error> { + ) -> Result { self.mutable()?.internal_update_node_metadata(vid, props) } @@ -159,7 +160,7 @@ impl InternalPropertyAdditionOps for GraphStorage { eid: EID, layer: usize, props: &[(usize, Prop)], - ) -> Result { + ) -> Result { self.mutable()? .internal_add_edge_metadata(eid, layer, props) } @@ -169,7 +170,7 @@ impl InternalPropertyAdditionOps for GraphStorage { eid: EID, layer: usize, props: &[(usize, Prop)], - ) -> Result { + ) -> Result { self.mutable()? .internal_update_edge_metadata(eid, layer, props) } @@ -207,7 +208,7 @@ where &self, vid: VID, props: &[(usize, Prop)], - ) -> Result>, Self::Error> { + ) -> Result { self.base().internal_add_node_metadata(vid, props) } @@ -216,7 +217,7 @@ where &self, vid: VID, props: &[(usize, Prop)], - ) -> Result>, Self::Error> { + ) -> Result { self.base().internal_update_node_metadata(vid, props) } @@ -226,7 +227,7 @@ where eid: EID, layer: usize, props: &[(usize, Prop)], - ) -> Result { + ) -> Result { self.base().internal_add_edge_metadata(eid, layer, props) } @@ -236,7 +237,7 @@ where eid: EID, layer: usize, props: &[(usize, Prop)], - ) -> Result { + ) -> Result { self.base().internal_update_edge_metadata(eid, layer, props) } } From 159f8a020fed68338e197e9cc734998854c46e85 Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Tue, 12 Aug 2025 16:41:32 +0200 Subject: [PATCH 099/321] tidy up some lifetime warnings --- raphtory-api/src/core/entities/mod.rs | 6 +++--- .../src/core/entities/properties/meta.rs | 4 ++-- .../entities/properties/prop/prop_array.rs | 2 +- raphtory-api/src/core/storage/dict_mapper.rs | 4 ++-- raphtory-core/src/entities/graph/tgraph.rs | 5 +---- raphtory-core/src/entities/nodes/node_ref.rs | 20 +++++++++---------- .../src/entities/nodes/node_store.rs | 2 +- .../src/entities/nodes/structure/adj.rs | 2 +- .../src/entities/nodes/structure/adjset.rs | 2 +- raphtory-core/src/storage/mod.rs | 6 +++--- 10 files changed, 25 insertions(+), 28 deletions(-) diff --git a/raphtory-api/src/core/entities/mod.rs b/raphtory-api/src/core/entities/mod.rs index 61e3cfd296..cce5d1c80a 100644 --- a/raphtory-api/src/core/entities/mod.rs +++ b/raphtory-api/src/core/entities/mod.rs @@ -232,7 +232,7 @@ impl GID { } } - pub fn to_str(&self) -> Cow { + pub fn to_str(&self) -> Cow<'_, str> { match self { GID::U64(v) => Cow::Owned(v.to_string()), GID::Str(v) => Cow::Borrowed(v), @@ -253,7 +253,7 @@ impl GID { } } - pub fn as_ref(&self) -> GidRef { + pub fn as_ref(&self) -> GidRef<'_> { match self { GID::U64(v) => GidRef::U64(*v), GID::Str(v) => GidRef::Str(v), @@ -467,7 +467,7 @@ impl LayerIds { } } - pub fn constrain_from_edge(&self, e: EdgeRef) -> Cow { + pub fn constrain_from_edge(&self, e: EdgeRef) -> Cow<'_, LayerIds> { match e.layer() { None => Cow::Borrowed(self), Some(l) => self diff --git a/raphtory-api/src/core/entities/properties/meta.rs b/raphtory-api/src/core/entities/properties/meta.rs index 7c3557cb06..3fb57b8b88 100644 --- a/raphtory-api/src/core/entities/properties/meta.rs +++ b/raphtory-api/src/core/entities/properties/meta.rs @@ -307,14 +307,14 @@ impl PropMapper { self.dtypes.as_ref() } - pub fn locked(&self) -> LockedPropMapper { + pub fn locked(&self) -> LockedPropMapper<'_> { LockedPropMapper { dict_mapper: self.id_mapper.read(), d_types: self.dtypes.read_recursive(), } } - pub fn write_locked(&self) -> WriteLockedPropMapper { + pub fn write_locked(&self) -> WriteLockedPropMapper<'_> { WriteLockedPropMapper { dict_mapper: self.id_mapper.write(), d_types: self.dtypes.write(), diff --git a/raphtory-api/src/core/entities/properties/prop/prop_array.rs b/raphtory-api/src/core/entities/properties/prop/prop_array.rs index ee0717009a..0eb0ef5c7e 100644 --- a/raphtory-api/src/core/entities/properties/prop/prop_array.rs +++ b/raphtory-api/src/core/entities/properties/prop/prop_array.rs @@ -108,7 +108,7 @@ impl PropArray { self.iter_prop_inner().into_iter().flatten() } - fn iter_prop_inner(&self) -> Option> { + fn iter_prop_inner(&self) -> Option> { let arr = self.as_array_ref()?; arr.as_any() diff --git a/raphtory-api/src/core/storage/dict_mapper.rs b/raphtory-api/src/core/storage/dict_mapper.rs index 490fe9f288..f1e6e3f88e 100644 --- a/raphtory-api/src/core/storage/dict_mapper.rs +++ b/raphtory-api/src/core/storage/dict_mapper.rs @@ -173,14 +173,14 @@ impl DictMapper { } } - pub fn read(&self) -> LockedDictMapper { + pub fn read(&self) -> LockedDictMapper<'_> { LockedDictMapper { map: self.map.read(), reverse_map: self.reverse_map.read(), } } - pub fn write(&self) -> WriteLockedDictMapper { + pub fn write(&self) -> WriteLockedDictMapper<'_> { WriteLockedDictMapper { map: self.map.write(), reverse_map: self.reverse_map.write(), diff --git a/raphtory-core/src/entities/graph/tgraph.rs b/raphtory-core/src/entities/graph/tgraph.rs index 283b8a1136..90bc105bf5 100644 --- a/raphtory-core/src/entities/graph/tgraph.rs +++ b/raphtory-core/src/entities/graph/tgraph.rs @@ -1,15 +1,12 @@ -use dashmap::DashSet; use raphtory_api::core::{ entities::MAX_LAYER , storage::arc_str::ArcStr , }; -use rustc_hash::FxHasher; -use std::{fmt::Debug, hash::BuildHasherDefault}; +use std::{fmt::Debug}; use thiserror::Error; -pub(crate) type FxDashSet = DashSet>; #[derive(Error, Debug)] #[error("Invalid layer: {invalid_layer}. Valid layers: {valid_layers}")] diff --git a/raphtory-core/src/entities/nodes/node_ref.rs b/raphtory-core/src/entities/nodes/node_ref.rs index 8cbebdc3fe..86730b671f 100644 --- a/raphtory-core/src/entities/nodes/node_ref.rs +++ b/raphtory-core/src/entities/nodes/node_ref.rs @@ -9,7 +9,7 @@ pub enum NodeRef<'a> { } pub trait AsNodeRef: Send + Sync { - fn as_node_ref(&self) -> NodeRef; + fn as_node_ref(&self) -> NodeRef<'_>; fn into_gid(self) -> Either where @@ -21,7 +21,7 @@ pub trait AsNodeRef: Send + Sync { } } - fn as_gid_ref(&self) -> Either { + fn as_gid_ref(&self) -> Either, VID> { match self.as_node_ref() { NodeRef::Internal(vid) => Either::Right(vid), NodeRef::External(u) => Either::Left(u), @@ -30,50 +30,50 @@ pub trait AsNodeRef: Send + Sync { } impl<'a> AsNodeRef for NodeRef<'a> { - fn as_node_ref(&self) -> NodeRef { + fn as_node_ref(&self) -> NodeRef<'_> { *self } } impl AsNodeRef for VID { - fn as_node_ref(&self) -> NodeRef { + fn as_node_ref(&self) -> NodeRef<'_> { NodeRef::Internal(*self) } } impl AsNodeRef for u64 { - fn as_node_ref(&self) -> NodeRef { + fn as_node_ref(&self) -> NodeRef<'_> { NodeRef::External(GidRef::U64(*self)) } } impl AsNodeRef for String { - fn as_node_ref(&self) -> NodeRef { + fn as_node_ref(&self) -> NodeRef<'_> { NodeRef::External(GidRef::Str(self)) } } impl AsNodeRef for &str { - fn as_node_ref(&self) -> NodeRef { + fn as_node_ref(&self) -> NodeRef<'_> { NodeRef::External(GidRef::Str(self)) } } impl AsNodeRef for &V { - fn as_node_ref(&self) -> NodeRef { + fn as_node_ref(&self) -> NodeRef<'_> { V::as_node_ref(self) } } impl AsNodeRef for GID { - fn as_node_ref(&self) -> NodeRef { + fn as_node_ref(&self) -> NodeRef<'_> { let gid_ref: GidRef = self.into(); NodeRef::External(gid_ref) } } impl<'a> AsNodeRef for GidRef<'a> { - fn as_node_ref(&self) -> NodeRef { + fn as_node_ref(&self) -> NodeRef<'_> { NodeRef::External(*self) } } diff --git a/raphtory-core/src/entities/nodes/node_store.rs b/raphtory-core/src/entities/nodes/node_store.rs index 8986dd4686..c53e4aef7b 100644 --- a/raphtory-core/src/entities/nodes/node_store.rs +++ b/raphtory-core/src/entities/nodes/node_store.rs @@ -287,7 +287,7 @@ impl NodeStore { iter } - fn merge_layers(&self, layers: &LayerIds, d: Direction, self_id: VID) -> BoxedLIter { + fn merge_layers(&self, layers: &LayerIds, d: Direction, self_id: VID) -> BoxedLIter<'_, EdgeRef> { match layers { LayerIds::All => Box::new( self.layers diff --git a/raphtory-core/src/entities/nodes/structure/adj.rs b/raphtory-core/src/entities/nodes/structure/adj.rs index b1f045e144..743d1533d6 100644 --- a/raphtory-core/src/entities/nodes/structure/adj.rs +++ b/raphtory-core/src/entities/nodes/structure/adj.rs @@ -68,7 +68,7 @@ impl Adj { } } - pub(crate) fn iter(&self, dir: Direction) -> BoxedLIter<(VID, EID)> { + pub(crate) fn iter(&self, dir: Direction) -> BoxedLIter<'_, (VID, EID)> { match self { Adj::Solo => Box::new(std::iter::empty()), Adj::List { out, into } => match dir { diff --git a/raphtory-core/src/entities/nodes/structure/adjset.rs b/raphtory-core/src/entities/nodes/structure/adjset.rs index dad7bc3d5f..1409f93529 100644 --- a/raphtory-core/src/entities/nodes/structure/adjset.rs +++ b/raphtory-core/src/entities/nodes/structure/adjset.rs @@ -95,7 +95,7 @@ impl + Copy + Send + Sync> Ad } } - pub fn iter(&self) -> BoxedLIter<(K, V)> { + pub fn iter(&self) -> BoxedLIter<'_, (K, V)> { match self { AdjSet::Empty => Box::new(std::iter::empty()), AdjSet::One(v, e) => Box::new(std::iter::once((*v, *e))), diff --git a/raphtory-core/src/storage/mod.rs b/raphtory-core/src/storage/mod.rs index 2b806dd167..ba238693b4 100644 --- a/raphtory-core/src/storage/mod.rs +++ b/raphtory-core/src/storage/mod.rs @@ -390,7 +390,7 @@ impl PropColumn { } } - pub fn get_ref(&self, index: usize) -> Option { + pub fn get_ref(&self, index: usize) -> Option> { match self { PropColumn::Bool(col) => col.get_opt(index).map(|prop| PropRef::Bool(*prop)), PropColumn::I64(col) => col.get_opt(index).map(|prop| PropRef::I64(*prop)), @@ -446,13 +446,13 @@ impl NodeSlot { &mut self.t_props_log } - pub fn iter(&self) -> impl Iterator { + pub fn iter(&self) -> impl Iterator> { self.nodes .iter() .map(|ns| NodePtr::new(ns, &self.t_props_log)) } - pub fn par_iter(&self) -> impl ParallelIterator { + pub fn par_iter(&self) -> impl ParallelIterator> { self.nodes .par_iter() .map(|ns| NodePtr::new(ns, &self.t_props_log)) From 32790c8cb39313d1ef961ebc0d255d162eaa7322 Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Tue, 12 Aug 2025 16:41:53 +0200 Subject: [PATCH 100/321] fix len impl --- .../src/db/api/view/internal/time_semantics/filtered_edge.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/raphtory/src/db/api/view/internal/time_semantics/filtered_edge.rs b/raphtory/src/db/api/view/internal/time_semantics/filtered_edge.rs index d879c38a4b..132d407cbc 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/filtered_edge.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/filtered_edge.rs @@ -15,6 +15,7 @@ use raphtory_storage::graph::edges::{ }; use rayon::iter::ParallelIterator; use std::{iter, ops::Range}; +use raphtory_storage::graph::edges::edge_storage_ops::TimeIndexRef; #[derive(Clone)] pub struct FilteredEdgeTimeIndex<'graph, G, TS> { @@ -109,7 +110,7 @@ impl< fn len(&self) -> usize { if self.view.internal_exploded_edge_filtered() { - self.iter().count() + self.clone().iter().count() } else { self.time_index.len() } From eb6aa53489e20001c999909f3da7d706cd7d1ac2 Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Wed, 13 Aug 2025 11:36:59 +0200 Subject: [PATCH 101/321] compiles without features --- raphtory/examples/load_graph.rs | 12 ++- raphtory/src/db/api/mutation/import_ops.rs | 2 +- raphtory/src/db/api/storage/storage.rs | 81 +++++++++++-------- .../src/db/api/view/edge_property_filter.rs | 4 +- .../api/view/exploded_edge_property_filter.rs | 9 ++- raphtory/src/db/api/view/graph.rs | 8 +- .../src/db/api/view/internal/materialize.rs | 18 ++++- .../internal/time_semantics/filtered_edge.rs | 75 +++++++++-------- .../internal/time_semantics/filtered_node.rs | 5 +- .../time_semantics/persistent_semantics.rs | 47 +++++++---- raphtory/src/lib.rs | 5 +- 11 files changed, 159 insertions(+), 107 deletions(-) diff --git a/raphtory/examples/load_graph.rs b/raphtory/examples/load_graph.rs index 68f3dfc9af..f9ca58b732 100644 --- a/raphtory/examples/load_graph.rs +++ b/raphtory/examples/load_graph.rs @@ -1,9 +1,10 @@ -use std::path::PathBuf; +#[cfg(all(feature = "io", feature = "arrow"))] +fn main() { + use std::path::PathBuf; -use raphtory::{io::parquet_loaders::load_edges_from_parquet, prelude::*}; -use raphtory_storage::core_ops::CoreGraphOps; + use raphtory::{io::parquet_loaders::load_edges_from_parquet, prelude::*}; + use raphtory_storage::core_ops::CoreGraphOps; -fn main() { let graph_path = PathBuf::from("/Volumes/Work/tether/graphs/raphtory_graph"); let layers = [ "dai_ava_edge_list", @@ -66,3 +67,6 @@ fn main() { println!("Total edges in graph: {all_edges_count}, total nodes: {all_nodes_count}"); } } + +#[cfg(not(all(feature = "io", feature = "arrow")))] +fn main() {} diff --git a/raphtory/src/db/api/mutation/import_ops.rs b/raphtory/src/db/api/mutation/import_ops.rs index f7130f1d78..2fe5d65c36 100644 --- a/raphtory/src/db/api/mutation/import_ops.rs +++ b/raphtory/src/db/api/mutation/import_ops.rs @@ -350,7 +350,7 @@ fn import_node_internal< } }; let session = graph.write_session().map_err(|err| err.into())?; - let keys = node.graph.node_meta().temporal_prop_meta().get_keys(); + let keys = node.graph.node_meta().temporal_prop_mapper().get_keys(); for (t, row) in node.rows() { let t = time_from_input_session(&session, t)?; diff --git a/raphtory/src/db/api/storage/storage.rs b/raphtory/src/db/api/storage/storage.rs index 866d530b94..923ded2509 100644 --- a/raphtory/src/db/api/storage/storage.rs +++ b/raphtory/src/db/api/storage/storage.rs @@ -1,61 +1,62 @@ -#[cfg(feature = "search")] -use crate::search::graph_index::GraphIndex; use crate::{ core::entities::nodes::node_ref::NodeRef, db::api::view::{ internal::{InheritEdgeHistoryFilter, InheritNodeHistoryFilter, InternalStorageOps}, - Base, InheritViewOps, + Base, IndexSpec, InheritViewOps, }, + errors::GraphError, }; use db4_graph::{TemporalGraph, TransactionManager, WriteLockedGraph}; use parking_lot::{RwLock, RwLockWriteGuard}; use raphtory_api::core::{ - entities::{properties::meta::Meta, EID, VID}, + entities::{ + properties::{ + meta::Meta, + prop::{Prop, PropType}, + }, + GidRef, EID, VID, + }, storage::{dict_mapper::MaybeNew, timeindex::TimeIndexEntry}, }; +use raphtory_core::{ + entities::ELID, + storage::{ + raw_edges::{EdgeWGuard, WriteLockedEdges}, + EntryMut, NodeSlot, WriteLockedNodes, + }, +}; use raphtory_storage::{ + core_ops::InheritCoreGraphOps, graph::graph::GraphStorage, + layer_ops::InheritLayerOps, mutation::{ - addition_ops::{EdgeWriteLock, SessionAdditionOps}, + addition_ops::{EdgeWriteLock, InternalAdditionOps, SessionAdditionOps}, addition_ops_ext::{UnlockedSession, WriteS}, + deletion_ops::InternalDeletionOps, + property_addition_ops::InternalPropertyAdditionOps, + EdgeWriterT, NodeWriterT, }, }; use std::{ fmt::{Display, Formatter}, + ops::{Deref, DerefMut}, path::{Path, PathBuf}, sync::Arc, }; -use std::ops::{Deref, DerefMut}; use storage::{ segments::{edge::MemEdgeSegment, node::MemNodeSegment}, Extension, WalImpl, }; use tracing::info; +#[cfg(feature = "proto")] +use crate::serialise::GraphFolder; + #[cfg(feature = "search")] -use crate::search::graph_index::MutableGraphIndex; -use crate::{db::api::view::IndexSpec, errors::GraphError}; -#[cfg(feature = "search")] -use once_cell::sync::OnceCell; -use raphtory_api::core::entities::{ - properties::prop::{Prop, PropType}, - GidRef, +use { + crate::search::graph_index::{GraphIndex, MutableGraphIndex}, + once_cell::sync::OnceCell, }; -use raphtory_core::{ - entities::ELID, - storage::{raw_edges::WriteLockedEdges, WriteLockedNodes}, -}; -use raphtory_core::storage::{EntryMut, NodeSlot}; -use raphtory_core::storage::raw_edges::EdgeWGuard; -use raphtory_storage::{ - core_ops::InheritCoreGraphOps, - layer_ops::InheritLayerOps, - mutation::{ - addition_ops::InternalAdditionOps, deletion_ops::InternalDeletionOps, - property_addition_ops::InternalPropertyAdditionOps, - }, -}; -use crate::serialise::GraphFolder; #[derive(Debug, Default)] pub struct Storage { @@ -385,7 +386,9 @@ impl<'a> SessionAdditionOps for StorageWriteSession<'a> { self.session.internal_add_node(t, v, props)?; #[cfg(feature = "search")] - self.storage.if_index_mut(|index| index.add_node_update(&self.storage.graph, t, MaybeNew::New(v), props))?; + self.storage.if_index_mut(|index| { + index.add_node_update(&self.storage.graph, t, MaybeNew::New(v), props) + })?; Ok(()) } @@ -400,7 +403,9 @@ impl<'a> SessionAdditionOps for StorageWriteSession<'a> { ) -> Result, Self::Error> { let id = self.session.internal_add_edge(t, src, dst, props, layer)?; #[cfg(feature = "search")] - self.storage.if_index_mut(|index| index.add_edge_update(&self.storage.graph, id, t, layer, props))?; + self.storage.if_index_mut(|index| { + index.add_edge_update(&self.storage.graph, id, t, layer, props) + })?; Ok(id) } @@ -417,7 +422,13 @@ impl<'a> SessionAdditionOps for StorageWriteSession<'a> { #[cfg(feature = "search")] self.storage.if_index_mut(|index| { - index.add_edge_update(&self.storage.graph, MaybeNew::Existing(edge), t, layer, props) + index.add_edge_update( + &self.storage.graph, + MaybeNew::Existing(edge), + t, + layer, + props, + ) })?; Ok(()) } @@ -565,7 +576,7 @@ impl InternalPropertyAdditionOps for Storage { &self, vid: VID, props: &[(usize, Prop)], - ) -> Result>, Self::Error> { + ) -> Result { let lock = self.graph.internal_add_node_metadata(vid, props)?; #[cfg(feature = "search")] @@ -578,7 +589,7 @@ impl InternalPropertyAdditionOps for Storage { &self, vid: VID, props: &[(usize, Prop)], - ) -> Result>, Self::Error> { + ) -> Result { let lock = self.graph.internal_update_node_metadata(vid, props)?; #[cfg(feature = "search")] @@ -592,7 +603,7 @@ impl InternalPropertyAdditionOps for Storage { eid: EID, layer: usize, props: &[(usize, Prop)], - ) -> Result { + ) -> Result { let lock = self.graph.internal_add_edge_metadata(eid, layer, props)?; #[cfg(feature = "search")] @@ -606,7 +617,7 @@ impl InternalPropertyAdditionOps for Storage { eid: EID, layer: usize, props: &[(usize, Prop)], - ) -> Result { + ) -> Result { let lock = self .graph .internal_update_edge_metadata(eid, layer, props)?; diff --git a/raphtory/src/db/api/view/edge_property_filter.rs b/raphtory/src/db/api/view/edge_property_filter.rs index 73e809d13c..6ab491cf19 100644 --- a/raphtory/src/db/api/view/edge_property_filter.rs +++ b/raphtory/src/db/api/view/edge_property_filter.rs @@ -37,7 +37,7 @@ mod test { use itertools::Itertools; use proptest::{arbitrary::any, proptest}; use raphtory_api::core::entities::properties::prop::PropType; - use raphtory_storage::mutation::addition_ops::InternalAdditionOps; + use raphtory_storage::mutation::addition_ops::{InternalAdditionOps, SessionAdditionOps}; #[test] fn test_edge_filter() { @@ -344,6 +344,8 @@ mod test { assert_eq!(gw.count_edges(), 0); let expected = PersistentGraph::new(); expected + .write_session() + .unwrap() .resolve_edge_property("test", PropType::I64, false) .unwrap(); expected.resolve_layer(None).unwrap(); diff --git a/raphtory/src/db/api/view/exploded_edge_property_filter.rs b/raphtory/src/db/api/view/exploded_edge_property_filter.rs index e7131bb7f6..31a682c4d3 100644 --- a/raphtory/src/db/api/view/exploded_edge_property_filter.rs +++ b/raphtory/src/db/api/view/exploded_edge_property_filter.rs @@ -46,7 +46,7 @@ mod test { use itertools::Itertools; use proptest::{arbitrary::any, proptest}; use raphtory_api::core::entities::properties::prop::PropType; - use raphtory_storage::mutation::addition_ops::InternalAdditionOps; + use raphtory_storage::mutation::addition_ops::{InternalAdditionOps, SessionAdditionOps}; use std::collections::HashMap; fn build_filtered_graph( @@ -119,10 +119,11 @@ mod test { } else { g_filtered.delete_edge(t, src, dst, None).unwrap(); // properties still exist after filtering - g_filtered + let session = g_filtered.write_session().unwrap(); + session .resolve_edge_property("str_prop", PropType::Str, false) .unwrap(); - g_filtered + session .resolve_edge_property("int_prop", PropType::I64, false) .unwrap(); } @@ -213,6 +214,8 @@ mod test { expected.delete_edge(0, 0, 0, None).unwrap(); //the property still exists! expected + .write_session() + .unwrap() .resolve_edge_property("test", PropType::I64, false) .unwrap(); diff --git a/raphtory/src/db/api/view/graph.rs b/raphtory/src/db/api/view/graph.rs index d2d6432118..ea9cf4f47c 100644 --- a/raphtory/src/db/api/view/graph.rs +++ b/raphtory/src/db/api/view/graph.rs @@ -225,10 +225,10 @@ impl<'graph, G: GraphView + 'graph> GraphViewOps<'graph> for G { let mut node_meta = Meta::new(); let mut edge_meta = Meta::new(); - node_meta.set_metadata_mapper(self.node_meta().const_prop_meta().deep_clone()); - node_meta.set_temporal_prop_meta(self.node_meta().temporal_prop_meta().deep_clone()); - edge_meta.set_metadata_mapper(self.edge_meta().const_prop_meta().deep_clone()); - edge_meta.set_temporal_prop_meta(self.edge_meta().temporal_prop_meta().deep_clone()); + node_meta.set_metadata_mapper(self.node_meta().metadata_mapper().deep_clone()); + node_meta.set_temporal_prop_meta(self.node_meta().temporal_prop_mapper().deep_clone()); + edge_meta.set_metadata_mapper(self.edge_meta().metadata_mapper().deep_clone()); + edge_meta.set_temporal_prop_meta(self.edge_meta().temporal_prop_mapper().deep_clone()); let mut g = TemporalGraph::new_with_meta(Default::default(), node_meta, edge_meta); // Copy all graph properties diff --git a/raphtory/src/db/api/view/internal/materialize.rs b/raphtory/src/db/api/view/internal/materialize.rs index 3179a9f427..e0625ff4b9 100644 --- a/raphtory/src/db/api/view/internal/materialize.rs +++ b/raphtory/src/db/api/view/internal/materialize.rs @@ -9,7 +9,10 @@ use crate::{ }, prelude::*, }; -use raphtory_api::{iter::BoxedLDIter, GraphType}; +use raphtory_api::{ + iter::{BoxedLDIter, BoxedLIter}, + GraphType, +}; use raphtory_storage::{graph::graph::GraphStorage, mutation::InheritMutationOps}; use std::ops::Range; @@ -138,7 +141,7 @@ impl GraphTimeSemanticsOps for MaterializedGraph { for_all!(self, g => g.has_temporal_prop(prop_id)) } - fn temporal_prop_iter(&self, prop_id: usize) -> BoxedLDIter<(TimeIndexEntry, Prop)> { + fn temporal_prop_iter(&self, prop_id: usize) -> BoxedLIter<(TimeIndexEntry, Prop)> { for_all!(self, g => g.temporal_prop_iter(prop_id)) } @@ -151,10 +154,19 @@ impl GraphTimeSemanticsOps for MaterializedGraph { prop_id: usize, start: i64, end: i64, - ) -> BoxedLDIter<(TimeIndexEntry, Prop)> { + ) -> BoxedLIter<(TimeIndexEntry, Prop)> { for_all!(self, g => g.temporal_prop_iter_window(prop_id, start, end)) } + fn temporal_prop_iter_window_rev( + &self, + prop_id: usize, + start: i64, + end: i64, + ) -> BoxedLIter<(TimeIndexEntry, Prop)> { + for_all!(self, g => g.temporal_prop_iter_window_rev(prop_id, start, end)) + } + fn temporal_prop_last_at( &self, prop_id: usize, diff --git a/raphtory/src/db/api/view/internal/time_semantics/filtered_edge.rs b/raphtory/src/db/api/view/internal/time_semantics/filtered_edge.rs index 132d407cbc..b9706540ce 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/filtered_edge.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/filtered_edge.rs @@ -11,46 +11,41 @@ use raphtory_api::core::{ storage::timeindex::{TimeIndexEntry, TimeIndexOps}, }; use raphtory_storage::graph::edges::{ - edge_ref::EdgeStorageRef, edge_storage_ops::EdgeStorageOps, edges::EdgesStorage, + edge_ref::EdgeStorageRef, + edge_storage_ops::{EdgeStorageOps, TimeIndexRef}, + edges::EdgesStorage, }; use rayon::iter::ParallelIterator; -use std::{iter, ops::Range}; -use raphtory_storage::graph::edges::edge_storage_ops::TimeIndexRef; +use std::{iter, marker::PhantomData, ops::Range}; +use storage::{EdgeAdditions, EdgeDeletions}; #[derive(Clone)] pub struct FilteredEdgeTimeIndex<'graph, G, TS> { eid: ELID, time_index: TS, view: G, - _marker: std::marker::PhantomData<&'graph ()>, + _marker: PhantomData<&'graph ()>, } -impl< - 'a, - 'graph: 'a, - TS: TimeIndexOps<'a, IndexType = TimeIndexEntry, RangeType = TS>, - G: GraphViewOps<'graph>, -> FilteredEdgeTimeIndex<'graph, G, TS> { - pub fn invert(self) -> InvertedFilteredEdgeTimeIndex<'graph, G> { +impl<'a, TS: TimeIndexOps<'a, IndexType = TimeIndexEntry, RangeType = TS>, G: GraphView + 'a> + FilteredEdgeTimeIndex<'a, G, TS> +{ + pub fn invert(self) -> InvertedFilteredEdgeTimeIndex<'a, G, TS> { InvertedFilteredEdgeTimeIndex { eid: self.eid, time_index: self.time_index, view: self.view, + _marker: Default::default(), } } - pub fn unfiltered(&self) -> TimeIndexRef<'graph> { + pub fn unfiltered(&self) -> TS { self.time_index.clone() } } -impl< - 'a, - 'graph: 'a, - TS: TimeIndexOps<'a, IndexType = TimeIndexEntry, RangeType = TS>, - G: GraphViewOps<'graph>, -> TimeIndexOps<'a> - for FilteredEdgeTimeIndex<'graph, G, TS> +impl<'a, TS: TimeIndexOps<'a, IndexType = TimeIndexEntry, RangeType = TS>, G: GraphView + 'a> + TimeIndexOps<'a> for FilteredEdgeTimeIndex<'a, G, TS> { type IndexType = TimeIndexEntry; type RangeType = Self; @@ -118,14 +113,15 @@ impl< } #[derive(Clone)] -pub struct InvertedFilteredEdgeTimeIndex<'graph, G> { +pub struct InvertedFilteredEdgeTimeIndex<'graph, G, TS> { eid: ELID, - time_index: TimeIndexRef<'graph>, + time_index: TS, view: G, + _marker: PhantomData<&'graph ()>, } -impl<'a, 'graph: 'a, G: GraphViewOps<'graph>> TimeIndexOps<'a> - for InvertedFilteredEdgeTimeIndex<'graph, G> +impl<'a, G: GraphView + 'a, TS: TimeIndexOps<'a, IndexType = TimeIndexEntry, RangeType = TS>> + TimeIndexOps<'a> for InvertedFilteredEdgeTimeIndex<'a, G, TS> { type IndexType = TimeIndexEntry; type RangeType = Self; @@ -152,6 +148,7 @@ impl<'a, 'graph: 'a, G: GraphViewOps<'graph>> TimeIndexOps<'a> eid: self.eid, time_index: self.time_index.range(w), view: self.view.clone(), + _marker: Default::default(), } } @@ -185,7 +182,7 @@ impl<'a, 'graph: 'a, G: GraphViewOps<'graph>> TimeIndexOps<'a> fn len(&self) -> usize { if self.view.internal_exploded_edge_filtered() { - self.iter().count() + self.clone().iter().count() } else { 0 } @@ -262,39 +259,39 @@ pub trait FilteredEdgeStorageOps<'a> { self, view: G, layer_ids: &'a LayerIds, - ) -> impl Iterator)>; + ) -> impl Iterator>)>; - fn filtered_deletions_iter>( + fn filtered_deletions_iter( self, view: G, layer_ids: &'a LayerIds, - ) -> impl Iterator)>; + ) -> impl Iterator>)>; - fn filtered_updates_iter>( + fn filtered_updates_iter( self, view: G, layer_ids: &'a LayerIds, ) -> impl Iterator< Item = ( usize, - FilteredEdgeTimeIndex<'a, G>, - FilteredEdgeTimeIndex<'a, G>, + FilteredEdgeTimeIndex<'a, G, EdgeAdditions<'a>>, + FilteredEdgeTimeIndex<'a, G, EdgeDeletions<'a>>, ), > + 'a; - fn filtered_additions>( + fn filtered_additions( self, layer_id: usize, view: G, - ) -> FilteredEdgeTimeIndex<'a, G>; + ) -> FilteredEdgeTimeIndex<'a, G, EdgeAdditions<'a>>; - fn filtered_deletions>( + fn filtered_deletions( self, layer_id: usize, view: G, - ) -> FilteredEdgeTimeIndex<'a, G>; + ) -> FilteredEdgeTimeIndex<'a, G, EdgeDeletions<'a>>; - fn filtered_temporal_prop_layer>( + fn filtered_temporal_prop_layer( self, layer_id: usize, prop_id: usize, @@ -308,7 +305,7 @@ pub trait FilteredEdgeStorageOps<'a> { layer_ids: &'a LayerIds, ) -> impl Iterator)> + 'a; - fn filtered_edge_metadata<'graph, G: GraphView + 'graph>( + fn filtered_edge_metadata( &self, view: G, prop_id: usize, @@ -321,7 +318,7 @@ impl<'a> FilteredEdgeStorageOps<'a> for EdgeStorageRef<'a> { self, view: G, layer_ids: &'a LayerIds, - ) -> impl Iterator>)> { + ) -> impl Iterator>)> { self.layer_ids_iter(layer_ids).filter_map(move |layer| { let view = view.clone(); view.internal_filter_edge_layer(self, layer) @@ -333,7 +330,7 @@ impl<'a> FilteredEdgeStorageOps<'a> for EdgeStorageRef<'a> { self, view: G, layer_ids: &'a LayerIds, - ) -> impl Iterator>)> { + ) -> impl Iterator>)> { self.layer_ids_iter(layer_ids).filter_map(move |layer| { let view = view.clone(); view.internal_filter_edge_layer(self, layer) @@ -422,7 +419,7 @@ impl<'a> FilteredEdgeStorageOps<'a> for EdgeStorageRef<'a> { }) } - fn filtered_edge_metadata<'graph, G: GraphView + 'graph>( + fn filtered_edge_metadata( &self, view: G, prop_id: usize, diff --git a/raphtory/src/db/api/view/internal/time_semantics/filtered_node.rs b/raphtory/src/db/api/view/internal/time_semantics/filtered_node.rs index f8a86f6617..052d8336be 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/filtered_node.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/filtered_node.rs @@ -11,8 +11,9 @@ use raphtory_api::core::{ storage::timeindex::{TimeIndexEntry, TimeIndexOps}, Direction, }; -use raphtory_storage::graph::{ - edges::edge_storage_ops::EdgeStorageOps, nodes::node_storage_ops::NodeStorageOps, +use raphtory_storage::{ + core_ops::CoreGraphOps, + graph::{edges::edge_storage_ops::EdgeStorageOps, nodes::node_storage_ops::NodeStorageOps}, }; use std::ops::Range; use storage::gen_ts::ALL_LAYERS; diff --git a/raphtory/src/db/api/view/internal/time_semantics/persistent_semantics.rs b/raphtory/src/db/api/view/internal/time_semantics/persistent_semantics.rs index 60c3419dfe..6f5b93d534 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/persistent_semantics.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/persistent_semantics.rs @@ -25,18 +25,29 @@ use raphtory_storage::graph::{ nodes::{node_ref::NodeStorageRef, node_storage_ops::NodeStorageOps}, }; use std::{iter, ops::Range}; - -fn alive_before<'a, G: GraphViewOps<'a>>( - additions: FilteredEdgeTimeIndex<'a, G>, - deletions: FilteredEdgeTimeIndex<'a, G>, +use storage::{EdgeAdditions, EdgeDeletions}; + +fn alive_before< + 'a, + G: GraphViewOps<'a>, + TSA: TimeIndexOps<'a, IndexType = TimeIndexEntry, RangeType = TSA>, + TSD: TimeIndexOps<'a, IndexType = TimeIndexEntry, RangeType = TSD>, +>( + additions: FilteredEdgeTimeIndex<'a, G, TSA>, + deletions: FilteredEdgeTimeIndex<'a, G, TSD>, t: i64, ) -> bool { last_before(additions, deletions, t).is_some() } -fn last_before<'a, G: GraphViewOps<'a>>( - additions: FilteredEdgeTimeIndex<'a, G>, - deletions: FilteredEdgeTimeIndex<'a, G>, +fn last_before< + 'a, + G: GraphViewOps<'a>, + TSA: TimeIndexOps<'a, IndexType = TimeIndexEntry, RangeType = TSA>, + TSD: TimeIndexOps<'a, IndexType = TimeIndexEntry, RangeType = TSD>, +>( + additions: FilteredEdgeTimeIndex<'a, G, TSA>, + deletions: FilteredEdgeTimeIndex<'a, G, TSD>, t: i64, ) -> Option { let last_addition_before_start = additions.range_t(i64::MIN..t).last(); @@ -51,9 +62,14 @@ fn last_before<'a, G: GraphViewOps<'a>>( } } -fn persisted_event<'a, G: GraphViewOps<'a>>( - additions: FilteredEdgeTimeIndex<'a, G>, - deletions: FilteredEdgeTimeIndex<'a, G>, +fn persisted_event< + 'a, + G: GraphViewOps<'a>, + TSA: TimeIndexOps<'a, IndexType = TimeIndexEntry, RangeType = TSA>, + TSD: TimeIndexOps<'a, IndexType = TimeIndexEntry, RangeType = TSD>, +>( + additions: FilteredEdgeTimeIndex<'a, G, TSA>, + deletions: FilteredEdgeTimeIndex<'a, G, TSD>, t: i64, ) -> Option { let active_at_start = deletions.active_t(t..t.saturating_add(1)) @@ -105,11 +121,14 @@ fn node_has_valid_edges<'graph, G: GraphView>( }) } -fn merged_deletions<'graph, G: GraphViewOps<'graph>>( - e: EdgeStorageRef<'graph>, +fn merged_deletions<'a, G: GraphView + 'a>( + e: EdgeStorageRef<'a>, view: G, layer: usize, -) -> MergedTimeIndex, InvertedFilteredEdgeTimeIndex<'graph, G>> { +) -> MergedTimeIndex< + FilteredEdgeTimeIndex<'a, G, EdgeDeletions<'a>>, + InvertedFilteredEdgeTimeIndex<'a, G, EdgeAdditions<'a>>, +> { e.filtered_deletions(layer, view.clone()) .merge(e.filtered_additions(layer, view).invert()) } @@ -275,7 +294,7 @@ impl NodeTimeSemanticsOps for PersistentSemantics { .is_some() { Some( - (0.._view.node_meta().temporal_prop_meta().len()) + (0.._view.node_meta().temporal_prop_mapper().len()) .map(|prop_id| (prop_id, node.tprop(prop_id))) .filter_map(|(i, tprop)| { if tprop.active_t(start..start.saturating_add(1)) { diff --git a/raphtory/src/lib.rs b/raphtory/src/lib.rs index 94970421cd..a98cfb34c9 100644 --- a/raphtory/src/lib.rs +++ b/raphtory/src/lib.rs @@ -173,7 +173,10 @@ mod test_utils { use proptest::{arbitrary::any, prelude::*}; use proptest_derive::Arbitrary; use raphtory_api::core::entities::properties::prop::{PropType, DECIMAL_MAX}; - use raphtory_storage::mutation::addition_ops::{InternalAdditionOps, SessionAdditionOps}; + use raphtory_storage::{ + core_ops::CoreGraphOps, + mutation::addition_ops::{InternalAdditionOps, SessionAdditionOps}, + }; use std::{collections::HashMap, sync::Arc}; #[cfg(feature = "storage")] use tempfile::TempDir; From 5562485b9a30bb41a64291b4a9bf9af53b26c944 Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Wed, 13 Aug 2025 14:50:52 +0200 Subject: [PATCH 102/321] no more private submodule --- .gitmodules | 3 - Cargo.toml | 4 - pometry-storage-private | 1 - pometry-storage/Cargo.toml | 16 - pometry-storage/src/lib.rs | 2 - .../src/entities/edges/edge_store.rs | 183 -------- raphtory-core/src/entities/edges/mod.rs | 3 - .../src/entities/nodes/node_store.rs | 438 ------------------ raphtory-core/src/storage/node_entry.rs | 144 ------ raphtory-core/src/storage/raw_edges.rs | 429 ----------------- .../src/graph/nodes/node_additions.rs | 209 --------- raphtory-storage/src/graph/nodes/row.rs | 122 ----- 12 files changed, 1554 deletions(-) delete mode 160000 pometry-storage-private delete mode 100644 pometry-storage/Cargo.toml delete mode 100644 pometry-storage/src/lib.rs delete mode 100644 raphtory-core/src/entities/edges/edge_store.rs delete mode 100644 raphtory-core/src/entities/edges/mod.rs delete mode 100644 raphtory-core/src/entities/nodes/node_store.rs delete mode 100644 raphtory-core/src/storage/node_entry.rs delete mode 100644 raphtory-core/src/storage/raw_edges.rs delete mode 100644 raphtory-storage/src/graph/nodes/node_additions.rs delete mode 100644 raphtory-storage/src/graph/nodes/row.rs diff --git a/.gitmodules b/.gitmodules index 0a34c6e8f0..e69de29bb2 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule "pometry-storage-private"] - path = pometry-storage-private - url = git@github.com:Pometry/pometry-storage.git diff --git a/Cargo.toml b/Cargo.toml index 4beba3007d..1c51bbc37a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,10 +49,6 @@ incremental = false [workspace.dependencies] -#[public-storage] -pometry-storage = { version = ">=0.8.1", path = "pometry-storage" } -#[private-storage] -# pometry-storage = { path = "pometry-storage-private", package = "pometry-storage-private" } async-graphql = { version = "7.0.16", features = ["dynamic-schema"] } bincode = "1.3.3" async-graphql-poem = "7.0.16" diff --git a/pometry-storage-private b/pometry-storage-private deleted file mode 160000 index fac0f565c8..0000000000 --- a/pometry-storage-private +++ /dev/null @@ -1 +0,0 @@ -Subproject commit fac0f565c8fafa657bbf89a2ebd368c4ee06c7f9 diff --git a/pometry-storage/Cargo.toml b/pometry-storage/Cargo.toml deleted file mode 100644 index 1d7ae0a0ba..0000000000 --- a/pometry-storage/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "pometry-storage" -description = "Storage backend for Raphtory" -edition.workspace = true -rust-version.workspace = true -version.workspace = true -keywords.workspace = true -authors.workspace = true -documentation.workspace = true -repository.workspace = true -license.workspace = true -readme.workspace = true -homepage.workspace = true - -[features] -storage = [] diff --git a/pometry-storage/src/lib.rs b/pometry-storage/src/lib.rs deleted file mode 100644 index 0851e257e4..0000000000 --- a/pometry-storage/src/lib.rs +++ /dev/null @@ -1,2 +0,0 @@ -#[cfg(feature = "storage")] -compile_error!("The 'storage' feature is private"); diff --git a/raphtory-core/src/entities/edges/edge_store.rs b/raphtory-core/src/entities/edges/edge_store.rs deleted file mode 100644 index fa9e57c049..0000000000 --- a/raphtory-core/src/entities/edges/edge_store.rs +++ /dev/null @@ -1,183 +0,0 @@ -use crate::{ - entities::{ - properties::props::{MetadataError, Props, TPropError}, - EID, VID, - }, - storage::{ - raw_edges::EdgeShard, - timeindex::{TimeIndex, TimeIndexEntry}, - }, - utils::iter::GenLockedIter, -}; -use itertools::Itertools; -use raphtory_api::core::entities::{edges::edge_ref::EdgeRef, properties::prop::Prop}; -use serde::{Deserialize, Serialize}; -use std::{ - fmt::{Debug, Formatter}, - ops::Deref, -}; - -#[derive(Clone, Serialize, Deserialize, Debug, Default, PartialEq)] -pub struct EdgeStore { - pub eid: EID, - pub src: VID, - pub dst: VID, -} - -pub trait EdgeDataLike<'a> { - fn temporal_prop_ids(self) -> impl Iterator + 'a; - fn metadata_ids(self) -> impl Iterator + 'a; -} - -impl<'a, T: Deref + 'a> EdgeDataLike<'a> for T { - fn temporal_prop_ids(self) -> impl Iterator + 'a { - GenLockedIter::from(self, |layer| { - Box::new( - layer - .props() - .into_iter() - .flat_map(|props| props.temporal_prop_ids()), - ) - }) - } - - fn metadata_ids(self) -> impl Iterator + 'a { - GenLockedIter::from(self, |layer| { - Box::new( - layer - .props() - .into_iter() - .flat_map(|props| props.metadata_ids()), - ) - }) - } -} - -#[derive(Serialize, Deserialize, Debug, Default, PartialEq)] -pub struct EdgeLayer { - props: Option, // memory optimisation: only allocate props if needed -} - -impl EdgeLayer { - pub fn props(&self) -> Option<&Props> { - self.props.as_ref() - } - - pub fn into_props(self) -> Option { - self.props - } - - pub fn add_prop( - &mut self, - t: TimeIndexEntry, - prop_id: usize, - prop: Prop, - ) -> Result<(), TPropError> { - let props = self.props.get_or_insert_with(Props::new); - props.add_prop(t, prop_id, prop) - } - - pub fn add_metadata(&mut self, prop_id: usize, prop: Prop) -> Result<(), MetadataError> { - let props = self.props.get_or_insert_with(Props::new); - props.add_metadata(prop_id, prop) - } - - pub fn update_metadata(&mut self, prop_id: usize, prop: Prop) -> Result<(), MetadataError> { - let props = self.props.get_or_insert_with(Props::new); - props.update_metadata(prop_id, prop) - } -} - -impl EdgeStore { - pub fn new(src: VID, dst: VID) -> Self { - Self { - eid: 0.into(), - src, - dst, - } - } - - pub fn initialised(&self) -> bool { - self.eid != EID::default() - } - - pub fn as_edge_ref(&self) -> EdgeRef { - EdgeRef::new_outgoing(self.eid, self.src, self.dst) - } -} - -#[derive(Clone, Copy)] -pub struct MemEdge<'a> { - edges: &'a EdgeShard, - offset: usize, -} - -impl<'a> Debug for MemEdge<'a> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Edge") - .field("src", &self.src()) - .field("dst", &self.dst()) - .field("eid", &self.eid()) - .field( - "props", - &(0..self.internal_num_layers()) - .map(|i| (i, self.props(i))) - .collect_vec(), - ) - .finish() - } -} - -impl<'a> MemEdge<'a> { - pub fn new(edges: &'a EdgeShard, offset: usize) -> Self { - MemEdge { edges, offset } - } - - pub fn src(&self) -> VID { - self.edge_store().src - } - - pub fn dst(&self) -> VID { - self.edge_store().dst - } - pub fn edge_store(&self) -> &'a EdgeStore { - self.edges.edge_store(self.offset) - } - - #[inline] - pub fn props(self, layer_id: usize) -> Option<&'a Props> { - self.edges - .props(self.offset, layer_id) - .and_then(|el| el.props()) - } - - pub fn eid(self) -> EID { - self.edge_store().eid - } - - pub fn as_edge_ref(&self) -> EdgeRef { - EdgeRef::new_outgoing(self.eid(), self.src(), self.dst()) - } - - pub fn internal_num_layers(self) -> usize { - self.edges.internal_num_layers() - } - - pub fn get_additions(self, layer_id: usize) -> Option<&'a TimeIndex> { - self.edges.additions(self.offset, layer_id) - } - - pub fn get_deletions(self, layer_id: usize) -> Option<&'a TimeIndex> { - self.edges.deletions(self.offset, layer_id) - } - - pub fn has_layer_inner(self, layer_id: usize) -> bool { - self.get_additions(layer_id) - .filter(|t_index| !t_index.is_empty()) - .is_some() - || self - .get_deletions(layer_id) - .filter(|t_index| !t_index.is_empty()) - .is_some() - } -} diff --git a/raphtory-core/src/entities/edges/mod.rs b/raphtory-core/src/entities/edges/mod.rs deleted file mode 100644 index d1f7224234..0000000000 --- a/raphtory-core/src/entities/edges/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod edge_store; - -pub use raphtory_api::core::entities::edges::*; diff --git a/raphtory-core/src/entities/nodes/node_store.rs b/raphtory-core/src/entities/nodes/node_store.rs deleted file mode 100644 index c53e4aef7b..0000000000 --- a/raphtory-core/src/entities/nodes/node_store.rs +++ /dev/null @@ -1,438 +0,0 @@ -use crate::{ - entities::{ - edges::edge_ref::EdgeRef, - nodes::structure::adj::Adj, - properties::{ - props::{MetadataError, Props}, - tcell::TCell, - }, - LayerIds, EID, GID, VID, - }, - storage::{ - timeindex::{TimeIndexEntry, TimeIndexWindow}, - NodeEntry, - }, - utils::iter::GenLockedIter, -}; -use itertools::Itertools; -use raphtory_api::{ - core::{ - entities::{properties::prop::Prop, GidRef, LayerVariants, ELID}, - storage::timeindex::{TimeIndexLike, TimeIndexOps}, - Direction, - }, - iter::BoxedLIter, -}; -use serde::{Deserialize, Serialize}; -use std::{iter, ops::Range}; - -#[derive(Serialize, Deserialize, Debug, Default, PartialEq)] -pub struct NodeStore { - pub global_id: GID, - pub vid: VID, - // each layer represents a separate view of the graph - pub(crate) layers: Vec, - // props for node - pub(crate) props: Option, - pub node_type: usize, - - /// For every property id keep a hash map of timestamps to values pointing to the property entries in the props vector - timestamps: PropTimestamps, -} - -#[derive(Serialize, Deserialize, Debug, Default, PartialEq)] -pub struct PropTimestamps { - // all the timestamps that have been seen by this node - pub edge_ts: TCell, - pub props_ts: TCell>, -} - -impl PropTimestamps { - pub fn edge_ts(&self) -> &TCell { - &self.edge_ts - } - - pub fn props_ts(&self) -> &TCell> { - &self.props_ts - } -} - -impl<'a> TimeIndexOps<'a> for &'a PropTimestamps { - type IndexType = TimeIndexEntry; - type RangeType = TimeIndexWindow<'a, TimeIndexEntry, PropTimestamps>; - - #[inline] - fn active(&self, w: Range) -> bool { - self.edge_ts().active(w.clone()) || self.props_ts().active(w) - } - - fn range(&self, w: Range) -> Self::RangeType { - TimeIndexWindow::Range { - timeindex: *self, - range: w, - } - } - - fn first(&self) -> Option { - let first = self.edge_ts().first(); - let other = self.props_ts().first(); - - first - .zip(other) - .map(|(a, b)| a.min(b)) - .or_else(|| first.or(other)) - } - - fn last(&self) -> Option { - let last = self.edge_ts().last(); - let other = self.props_ts().last(); - - last.zip(other) - .map(|(a, b)| a.max(b)) - .or_else(|| last.or(other)) - } - - fn iter(self) -> impl Iterator + Send + Sync + 'a { - self.edge_ts - .iter() - .map(|(t, _)| *t) - .merge(self.props_ts.iter().map(|(t, _)| *t)) - } - - fn iter_rev(self) -> impl Iterator + Send + Sync + 'a { - self.edge_ts - .iter() - .rev() - .map(|(t, _)| *t) - .merge_by(self.props_ts.iter().rev().map(|(t, _)| *t), |lt, rt| { - lt >= rt - }) - } - - fn len(&self) -> usize { - self.edge_ts.len() + self.props_ts.len() - } -} - -impl<'a> TimeIndexLike<'a> for &'a PropTimestamps { - fn range_iter( - self, - w: Range, - ) -> impl Iterator + Send + Sync + 'a { - self.edge_ts() - .range_iter(w.clone()) - .merge(self.props_ts().range_iter(w)) - } - - fn range_iter_rev( - self, - w: Range, - ) -> impl Iterator + Send + Sync + 'a { - self.edge_ts() - .range_iter_rev(w.clone()) - .merge_by(self.props_ts().range_iter_rev(w), |lt, rt| lt >= rt) - } - - fn range_count(&self, w: Range) -> usize { - self.edge_ts().range_count(w.clone()) + self.props_ts().range_count(w) - } - - fn first_range(&self, w: Range) -> Option { - let first = self - .edge_ts() - .iter_window(w.clone()) - .next() - .map(|(t, _)| *t); - let other = self.props_ts().iter_window(w).next().map(|(t, _)| *t); - - first - .zip(other) - .map(|(a, b)| a.min(b)) - .or_else(|| first.or(other)) - } - - fn last_range(&self, w: Range) -> Option { - let last = self - .edge_ts - .iter_window(w.clone()) - .next_back() - .map(|(t, _)| *t); - let other = self.props_ts.iter_window(w).next_back().map(|(t, _)| *t); - - last.zip(other) - .map(|(a, b)| a.max(b)) - .or_else(|| last.or(other)) - } -} - -impl NodeStore { - #[inline] - pub fn is_initialised(&self) -> bool { - self.vid != VID::default() - } - - #[inline] - pub fn init(&mut self, vid: VID, gid: GidRef) { - if !self.is_initialised() { - self.vid = vid; - self.global_id = gid.to_owned(); - } - } - - pub fn empty(global_id: GID) -> Self { - let layers = vec![Adj::Solo]; - Self { - global_id, - vid: VID(0), - timestamps: Default::default(), - layers, - props: None, - node_type: 0, - } - } - - pub fn resolved(global_id: GID, vid: VID) -> Self { - Self { - global_id, - vid, - timestamps: Default::default(), - layers: vec![], - props: None, - node_type: 0, - } - } - - pub fn global_id(&self) -> &GID { - &self.global_id - } - - pub fn timestamps(&self) -> &PropTimestamps { - &self.timestamps - } - - #[inline] - pub fn update_time(&mut self, t: TimeIndexEntry, eid: ELID) { - self.timestamps.edge_ts.set(t, eid); - } - - pub fn update_node_type(&mut self, node_type: usize) -> usize { - self.node_type = node_type; - node_type - } - - pub fn add_metadata(&mut self, prop_id: usize, prop: Prop) -> Result<(), MetadataError> { - let props = self.props.get_or_insert_with(Props::new); - props.add_metadata(prop_id, prop) - } - - pub fn update_metadata(&mut self, prop_id: usize, prop: Prop) -> Result<(), MetadataError> { - let props = self.props.get_or_insert_with(Props::new); - props.update_metadata(prop_id, prop) - } - - pub fn update_t_prop_time(&mut self, t: TimeIndexEntry, prop_i: Option) { - self.timestamps.props_ts.set(t, prop_i); - } - - #[inline(always)] - pub fn find_edge_eid(&self, dst: VID, layer_id: &LayerIds) -> Option { - match layer_id { - LayerIds::All => match self.layers.len() { - 0 => None, - 1 => self.layers[0].get_edge(dst, Direction::OUT), - _ => self - .layers - .iter() - .find_map(|layer| layer.get_edge(dst, Direction::OUT)), - }, - LayerIds::One(layer_id) => self - .layers - .get(*layer_id) - .and_then(|layer| layer.get_edge(dst, Direction::OUT)), - LayerIds::Multiple(layers) => layers.iter().find_map(|layer_id| { - self.layers - .get(layer_id) - .and_then(|layer| layer.get_edge(dst, Direction::OUT)) - }), - LayerIds::None => None, - } - } - - pub fn add_edge(&mut self, v_id: VID, dir: Direction, layer: usize, edge_id: EID) -> bool { - if layer >= self.layers.len() { - self.layers.resize_with(layer + 1, || Adj::Solo); - } - - match dir { - Direction::IN => self.layers[layer].add_edge_into(v_id, edge_id), - Direction::OUT => self.layers[layer].add_edge_out(v_id, edge_id), - _ => false, - } - } - - #[inline] - pub fn edge_tuples<'a>(&'a self, layers: &LayerIds, d: Direction) -> BoxedLIter<'a, EdgeRef> { - let self_id = self.vid; - let iter: BoxedLIter<'a, EdgeRef> = match d { - Direction::OUT => self.merge_layers(layers, Direction::OUT, self_id), - Direction::IN => self.merge_layers(layers, Direction::IN, self_id), - Direction::BOTH => Box::new( - self.edge_tuples(layers, Direction::OUT) - .filter(|e| e.src() != e.dst()) - .merge_by(self.edge_tuples(layers, Direction::IN), |e1, e2| { - e1.remote() < e2.remote() - }), - ), - }; - iter - } - - fn merge_layers(&self, layers: &LayerIds, d: Direction, self_id: VID) -> BoxedLIter<'_, EdgeRef> { - match layers { - LayerIds::All => Box::new( - self.layers - .iter() - .map(|adj| self.iter_adj(adj, d, self_id)) - .kmerge_by(|e1, e2| e1.remote() < e2.remote()) - .dedup(), - ), - LayerIds::One(id) => { - if let Some(layer) = self.layers.get(*id) { - Box::new(self.iter_adj(layer, d, self_id)) - } else { - Box::new(iter::empty()) - } - } - LayerIds::Multiple(ids) => Box::new( - ids.into_iter() - .filter_map(|id| self.layers.get(id)) - .map(|layer| self.iter_adj(layer, d, self_id)) - .kmerge_by(|e1, e2| e1.remote() < e2.remote()) - .dedup(), - ), - LayerIds::None => Box::new(iter::empty()), - } - } - - fn iter_adj<'a>( - &'a self, - layer: &'a Adj, - d: Direction, - self_id: VID, - ) -> impl Iterator + Send + Sync + 'a { - let iter: BoxedLIter<'a, EdgeRef> = match d { - Direction::IN => Box::new( - layer - .iter(d) - .map(move |(src_pid, e_id)| EdgeRef::new_incoming(e_id, src_pid, self_id)), - ), - Direction::OUT => Box::new( - layer - .iter(d) - .map(move |(dst_pid, e_id)| EdgeRef::new_outgoing(e_id, self_id, dst_pid)), - ), - _ => Box::new(iter::empty()), - }; - iter - } - - pub fn degree(&self, layers: &LayerIds, d: Direction) -> usize { - match layers { - LayerIds::All => match self.layers.len() { - 0 => 0, - 1 => self.layers[0].degree(d), - _ => self - .layers - .iter() - .map(|l| l.node_iter(d)) - .kmerge() - .dedup() - .count(), - }, - LayerIds::One(l) => self - .layers - .get(*l) - .map(|layer| layer.degree(d)) - .unwrap_or(0), - LayerIds::None => 0, - LayerIds::Multiple(ids) => ids - .iter() - .flat_map(|l_id| self.layers.get(l_id).map(|layer| layer.node_iter(d))) - .kmerge() - .dedup() - .count(), - } - } - - // every neighbour apears once in the iterator - // this is important because it calculates degree - pub fn neighbours<'a>( - &'a self, - layers: &LayerIds, - d: Direction, - ) -> impl Iterator + use<'a> { - match layers { - LayerIds::All => { - let iter = self - .layers - .iter() - .map(move |layer| layer.node_iter(d)) - .kmerge() - .dedup(); - LayerVariants::All(iter) - } - LayerIds::One(one) => { - let iter = self - .layers - .get(*one) - .into_iter() - .flat_map(move |layer| layer.node_iter(d)); - LayerVariants::One(iter) - } - LayerIds::Multiple(layers) => { - let iter = layers - .into_iter() - .filter_map(|l| self.layers.get(l)) - .map(move |layer| self.neighbours_from_adj(layer, d)) - .kmerge() - .dedup(); - LayerVariants::Multiple(iter) - } - LayerIds::None => LayerVariants::None(iter::empty()), - } - } - - fn neighbours_from_adj<'a>(&'a self, layer: &'a Adj, d: Direction) -> BoxedLIter<'a, VID> { - let iter: BoxedLIter<'a, VID> = match d { - Direction::IN => Box::new(layer.iter(d).map(|(from_v, _)| from_v)), - Direction::OUT => Box::new(layer.iter(d).map(|(to_v, _)| to_v)), - Direction::BOTH => Box::new( - self.neighbours_from_adj(layer, Direction::OUT) - .merge(self.neighbours_from_adj(layer, Direction::IN)) - .dedup(), - ), - }; - iter - } - - pub fn metadata_ids(&self) -> impl Iterator + '_ { - self.props - .as_ref() - .into_iter() - .flat_map(|ps| ps.metadata_ids()) - } - - pub fn metadata(&self, prop_id: usize) -> Option<&Prop> { - self.props.as_ref().and_then(|ps| ps.metadata(prop_id)) - } -} - -impl<'a> NodeEntry<'a> { - pub fn into_edges( - self, - layers: &LayerIds, - dir: Direction, - ) -> impl Iterator + 'a { - GenLockedIter::from(self, |node| node.as_ref().node().edge_tuples(layers, dir)) - } -} diff --git a/raphtory-core/src/storage/node_entry.rs b/raphtory-core/src/storage/node_entry.rs deleted file mode 100644 index 27aff2cebd..0000000000 --- a/raphtory-core/src/storage/node_entry.rs +++ /dev/null @@ -1,144 +0,0 @@ -use super::TColumns; -use crate::entities::{nodes::node_store::NodeStore, properties::tprop::TPropCell}; -use itertools::Itertools; -use raphtory_api::core::{ - entities::{ - edges::edge_ref::EdgeRef, - properties::{prop::Prop, tprop::TPropOps}, - GidRef, LayerIds, - }, - storage::timeindex::TimeIndexEntry, - Direction, -}; -use std::{ - fmt::{Debug, Formatter}, - ops::Range, -}; - -#[derive(Copy, Clone)] -pub struct MemRow<'a> { - cols: &'a TColumns, - row: Option, -} - -impl<'a> Debug for MemRow<'a> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_list().entries(*self).finish() - } -} - -impl<'a> MemRow<'a> { - pub fn new(cols: &'a TColumns, row: Option) -> Self { - Self { cols, row } - } -} - -impl<'a> IntoIterator for MemRow<'a> { - type Item = (usize, Option); - - type IntoIter = Box + 'a>; - - fn into_iter(self) -> Self::IntoIter { - Box::new( - self.cols - .iter() - .enumerate() - .map(move |(i, col)| (i, self.row.and_then(|row| col.get(row)))), - ) - } -} - -#[derive(Copy, Clone)] -pub struct NodePtr<'a> { - pub node: &'a NodeStore, - t_props_log: &'a TColumns, -} - -impl<'a> NodePtr<'a> { - pub fn edges_iter( - self, - layers: &LayerIds, - dir: Direction, - ) -> impl Iterator + 'a { - self.node.edge_tuples(layers, dir) - } -} - -impl<'a> Debug for NodePtr<'a> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Node") - .field("gid", self.node.global_id()) - .field("vid", &self.node.vid) - .field("node_type", &self.node.node_type) - .field("layers", &self.node.layers) - .field( - "metadata", - &self - .node - .metadata_ids() - .filter_map(|i| Some((i, self.node.metadata(i)?))) - .collect_vec(), - ) - .field("temporal_properties", &self.into_rows().collect_vec()) - .field("additions", self.node.timestamps()) - .finish() - } -} - -impl<'a> NodePtr<'a> { - pub fn new(node: &'a NodeStore, t_props_log: &'a TColumns) -> Self { - Self { node, t_props_log } - } - - pub fn node(self) -> &'a NodeStore { - self.node - } - - pub fn t_prop(self, prop_id: usize) -> TPropCell<'a> { - TPropCell::new( - &self.node.timestamps().props_ts, - self.t_props_log.get(prop_id), - ) - } - - pub fn temporal_prop_ids(self) -> impl Iterator + 'a { - self.t_props_log - .t_props_log - .iter() - .enumerate() - .filter_map(|(id, col)| (!col.is_empty()).then_some(id)) - } - - pub fn into_rows(self) -> impl Iterator)> { - self.node - .timestamps() - .props_ts - .iter() - .map(move |(t, &row)| (*t, MemRow::new(self.t_props_log, row))) - } - - pub fn last_before_row(self, t: TimeIndexEntry) -> Vec<(usize, Prop)> { - self.t_props_log - .iter() - .enumerate() - .filter_map(|(prop_id, _)| { - let t_prop = self.t_prop(prop_id); - t_prop.last_before(t).map(|(_, v)| (prop_id, v)) - }) - .collect() - } - - pub fn into_rows_window( - self, - w: Range, - ) -> impl Iterator)> + Send + Sync { - let tcell = &self.node.timestamps().props_ts; - tcell - .iter_window(w) - .map(move |(t, row)| (*t, MemRow::new(self.t_props_log, *row))) - } - - pub fn id(&self) -> GidRef { - self.node.global_id().into() - } -} diff --git a/raphtory-core/src/storage/raw_edges.rs b/raphtory-core/src/storage/raw_edges.rs deleted file mode 100644 index 00b3da1917..0000000000 --- a/raphtory-core/src/storage/raw_edges.rs +++ /dev/null @@ -1,429 +0,0 @@ -use super::{resolve, timeindex::TimeIndex}; -use crate::entities::edges::edge_store::{EdgeLayer, EdgeStore, MemEdge}; -use itertools::Itertools; -use lock_api::ArcRwLockReadGuard; -use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard}; -use raphtory_api::core::{entities::EID, storage::timeindex::TimeIndexEntry}; -use rayon::prelude::*; -use serde::{Deserialize, Serialize}; -use std::{ - fmt::{Debug, Formatter}, - ops::{Deref, DerefMut}, - sync::{ - atomic::{self, AtomicUsize, Ordering}, - Arc, - }, -}; - -#[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct EdgeShard { - edge_ids: Vec, - props: Vec>, - additions: Vec>>, - deletions: Vec>>, -} - -#[must_use] -pub struct UninitialisedEdge<'a> { - guard: RwLockWriteGuard<'a, EdgeShard>, - offset: usize, - value: EdgeStore, -} - -impl<'a> UninitialisedEdge<'a> { - pub fn init(mut self) -> EdgeWGuard<'a> { - self.guard.insert(self.offset, self.value); - EdgeWGuard { - guard: self.guard, - i: self.offset, - } - } - - pub fn value(&self) -> &EdgeStore { - &self.value - } - - pub fn value_mut(&mut self) -> &mut EdgeStore { - &mut self.value - } -} - -impl EdgeShard { - pub fn insert(&mut self, index: usize, value: EdgeStore) { - if index >= self.edge_ids.len() { - self.edge_ids.resize_with(index + 1, Default::default); - } - self.edge_ids[index] = value; - } - - pub fn edge_store(&self, index: usize) -> &EdgeStore { - &self.edge_ids[index] - } - - pub fn internal_num_layers(&self) -> usize { - self.additions.len().max(self.deletions.len()) - } - - pub fn additions(&self, index: usize, layer_id: usize) -> Option<&TimeIndex> { - self.additions.get(layer_id).and_then(|add| add.get(index)) - } - - pub fn deletions(&self, index: usize, layer_id: usize) -> Option<&TimeIndex> { - self.deletions.get(layer_id).and_then(|del| del.get(index)) - } - - pub fn props(&self, index: usize, layer_id: usize) -> Option<&EdgeLayer> { - self.props.get(layer_id).and_then(|props| props.get(index)) - } - - pub fn props_iter(&self, index: usize) -> impl Iterator { - self.props - .iter() - .enumerate() - .filter_map(move |(id, layer)| layer.get(index).map(|l| (id, l))) - } -} - -#[derive(Clone, Serialize, Deserialize)] -pub struct EdgesStorage { - shards: Arc<[Arc>]>, - len: Arc, -} - -impl Debug for EdgesStorage { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("EdgesStorage") - .field("len", &self.len()) - .field("data", &self.read_lock().iter().collect_vec()) - .finish() - } -} - -impl PartialEq for EdgesStorage { - fn eq(&self, other: &Self) -> bool { - self.shards.len() == other.shards.len() - && self - .shards - .iter() - .zip(other.shards.iter()) - .all(|(a, b)| a.read().eq(&b.read())) - } -} - -impl Default for EdgesStorage { - fn default() -> Self { - Self::new(rayon::current_num_threads()) - } -} - -impl EdgesStorage { - pub fn new(num_shards: usize) -> Self { - let shards = (0..num_shards).map(|_| { - Arc::new(RwLock::new(EdgeShard { - edge_ids: vec![], - props: Vec::with_capacity(0), - additions: Vec::with_capacity(1), - deletions: Vec::with_capacity(0), - })) - }); - EdgesStorage { - shards: shards.collect(), - len: Arc::new(AtomicUsize::new(0)), - } - } - - #[inline] - pub fn len(&self) -> usize { - self.len.load(atomic::Ordering::SeqCst) - } - - pub fn next_id(&self) -> EID { - EID(self.len.fetch_add(1, Ordering::Relaxed)) - } - - pub fn read_lock(&self) -> LockedEdges { - LockedEdges { - shards: self - .shards - .iter() - .map(|shard| Arc::new(shard.read_arc())) - .collect(), - len: self.len(), - } - } - - pub fn write_lock(&self) -> WriteLockedEdges { - WriteLockedEdges { - shards: self.shards.iter().map(|shard| shard.write()).collect(), - global_len: &self.len, - } - } - - #[inline] - fn resolve(&self, index: usize) -> (usize, usize) { - resolve(index, self.shards.len()) - } - - pub(crate) fn push(&self, mut value: EdgeStore) -> UninitialisedEdge { - let index = self.len.fetch_add(1, atomic::Ordering::Relaxed); - value.eid = EID(index); - let (bucket, offset) = self.resolve(index); - let guard = self.shards[bucket].write(); - UninitialisedEdge { - guard, - offset, - value, - } - } - - pub fn get_edge_mut(&self, eid: EID) -> EdgeWGuard { - let (bucket, offset) = self.resolve(eid.into()); - EdgeWGuard { - guard: self.shards[bucket].write(), - i: offset, - } - } - - pub fn get_edge(&self, eid: EID) -> EdgeRGuard { - let (bucket, offset) = self.resolve(eid.into()); - EdgeRGuard { - guard: self.shards[bucket].read(), - offset, - } - } -} - -pub struct EdgeWGuard<'a> { - guard: RwLockWriteGuard<'a, EdgeShard>, - i: usize, -} - -impl<'a> EdgeWGuard<'a> { - pub fn as_mut(&mut self) -> MutEdge { - MutEdge { - guard: self.guard.deref_mut(), - i: self.i, - } - } - - pub fn as_ref(&self) -> MemEdge { - MemEdge::new(&self.guard, self.i) - } - - pub fn eid(&self) -> EID { - self.as_ref().eid() - } -} - -pub struct MutEdge<'a> { - guard: &'a mut EdgeShard, - i: usize, -} - -impl<'a> MutEdge<'a> { - pub fn as_ref(&self) -> MemEdge { - MemEdge::new(self.guard, self.i) - } - pub fn eid(&self) -> EID { - self.as_ref().eid() - } - - pub fn edge_store_mut(&mut self) -> &mut EdgeStore { - &mut self.guard.edge_ids[self.i] - } - - pub fn deletions_mut(&mut self, layer_id: usize) -> &mut TimeIndex { - if layer_id >= self.guard.deletions.len() { - self.guard - .deletions - .resize_with(layer_id + 1, Default::default); - } - if self.i >= self.guard.deletions[layer_id].len() { - self.guard.deletions[layer_id].resize_with(self.i + 1, Default::default); - } - &mut self.guard.deletions[layer_id][self.i] - } - - fn has_layer(&self, layer_id: usize) -> bool { - if let Some(additions) = self.guard.additions.get(layer_id) { - if let Some(additions) = additions.get(self.i) { - return !additions.is_empty(); - } - } - if let Some(deletions) = self.guard.deletions.get(layer_id) { - if let Some(deletions) = deletions.get(self.i) { - return !deletions.is_empty(); - } - } - false - } - pub fn additions_mut(&mut self, layer_id: usize) -> &mut TimeIndex { - if layer_id >= self.guard.additions.len() { - self.guard - .additions - .resize_with(layer_id + 1, Default::default); - } - if self.i >= self.guard.additions[layer_id].len() { - self.guard.additions[layer_id].resize_with(self.i + 1, Default::default); - } - &mut self.guard.additions[layer_id][self.i] - } - - pub fn layer_mut(&mut self, layer_id: usize) -> &mut EdgeLayer { - if layer_id >= self.guard.props.len() { - self.guard.props.resize_with(layer_id + 1, Default::default); - } - if self.i >= self.guard.props[layer_id].len() { - self.guard.props[layer_id].resize_with(self.i + 1, Default::default); - } - - &mut self.guard.props[layer_id][self.i] - } - - /// Get a mutable reference to the layer only if it already exists but don't create a new one - pub fn get_layer_mut(&mut self, layer_id: usize) -> Option<&mut EdgeLayer> { - self.has_layer(layer_id).then(|| self.layer_mut(layer_id)) - } -} - -#[derive(Debug)] -pub struct EdgeRGuard<'a> { - guard: RwLockReadGuard<'a, EdgeShard>, - offset: usize, -} - -impl<'a> EdgeRGuard<'a> { - pub fn as_mem_edge(&self) -> MemEdge { - MemEdge::new(&self.guard, self.offset) - } - - pub fn layer_iter( - &self, - ) -> impl Iterator + '_)> + '_ { - self.guard.props_iter(self.offset) - } -} - -#[derive(Debug)] -pub struct LockedEdges { - shards: Arc<[Arc>]>, - len: usize, -} - -impl LockedEdges { - pub fn get_mem(&self, eid: EID) -> MemEdge { - let (bucket, offset) = resolve(eid.into(), self.shards.len()); - MemEdge::new(&self.shards[bucket], offset) - } - - pub fn len(&self) -> usize { - self.len - } - - pub fn iter(&self) -> impl Iterator + '_ { - self.shards.iter().flat_map(|shard| { - shard - .edge_ids - .iter() - .enumerate() - .map(move |(offset, _)| MemEdge::new(shard, offset)) - }) - } - - pub fn par_iter(&self) -> impl ParallelIterator + '_ { - self.shards.par_iter().flat_map(|shard| { - shard - .edge_ids - .par_iter() - .enumerate() - .map(move |(offset, _)| MemEdge::new(shard, offset)) - }) - } -} - -pub struct EdgeShardWriter<'a, S> { - shard: S, - shard_id: usize, - num_shards: usize, - global_len: &'a AtomicUsize, -} - -impl<'a, S> EdgeShardWriter<'a, S> -where - S: DerefMut, -{ - /// Map an edge id to local offset if it is in the shard - fn resolve(&self, eid: EID) -> Option { - let EID(eid) = eid; - let (bucket, offset) = resolve(eid, self.num_shards); - (bucket == self.shard_id).then_some(offset) - } - - pub fn get_mut(&mut self, eid: EID) -> Option { - let offset = self.resolve(eid)?; - if self.shard.edge_ids.len() <= offset { - self.global_len.fetch_max(eid.0 + 1, Ordering::Relaxed); - self.shard - .edge_ids - .resize_with(offset + 1, EdgeStore::default) - } - Some(MutEdge { - guard: self.shard.deref_mut(), - i: offset, - }) - } - - pub fn shard_id(&self) -> usize { - self.shard_id - } -} - -#[derive(Debug)] -pub struct WriteLockedEdges<'a> { - shards: Vec>, - global_len: &'a AtomicUsize, -} - -impl<'a> WriteLockedEdges<'a> { - pub fn par_iter_mut( - &mut self, - ) -> impl IndexedParallelIterator> + '_ { - let num_shards = self.shards.len(); - let shards: Vec<_> = self - .shards - .iter_mut() - .map(|shard| shard.deref_mut()) - .collect(); - let global_len = self.global_len; - shards - .into_par_iter() - .enumerate() - .map(move |(shard_id, shard)| EdgeShardWriter { - shard, - shard_id, - num_shards, - global_len, - }) - } - - pub fn into_par_iter_mut( - self, - ) -> impl IndexedParallelIterator>> + 'a - { - let num_shards = self.shards.len(); - let global_len = self.global_len; - self.shards - .into_par_iter() - .enumerate() - .map(move |(shard_id, shard)| EdgeShardWriter { - shard, - shard_id, - num_shards, - global_len, - }) - } - - pub fn num_shards(&self) -> usize { - self.shards.len() - } -} diff --git a/raphtory-storage/src/graph/nodes/node_additions.rs b/raphtory-storage/src/graph/nodes/node_additions.rs deleted file mode 100644 index fdb3ad585f..0000000000 --- a/raphtory-storage/src/graph/nodes/node_additions.rs +++ /dev/null @@ -1,209 +0,0 @@ -use iter_enum::{DoubleEndedIterator, ExactSizeIterator, FusedIterator, Iterator}; -use raphtory_api::core::{ - entities::ELID, - storage::timeindex::{TimeIndexEntry, TimeIndexOps}, -}; -use raphtory_core::{ - entities::nodes::node_store::PropTimestamps, - storage::timeindex::{TimeIndexWindow, TimeIndexWindowVariants}, -}; -use std::{iter, ops::Range}; - -#[cfg(feature = "storage")] -use {itertools::Itertools, pometry_storage::timestamps::LayerAdditions}; - -#[derive(Clone, Debug)] -pub enum NodeAdditions<'a> { - Mem(&'a PropTimestamps), - Range(TimeIndexWindow<'a, TimeIndexEntry, PropTimestamps>), - #[cfg(feature = "storage")] - Col(LayerAdditions<'a>), -} - -#[derive(Iterator, DoubleEndedIterator, ExactSizeIterator, FusedIterator, Debug)] -pub enum AdditionVariants { - Mem(Mem), - Range(Range), - #[cfg(feature = "storage")] - Col(Col), -} - -impl<'a> NodeAdditions<'a> { - #[inline] - pub fn prop_events(&self) -> impl Iterator + use<'a> { - match self { - NodeAdditions::Mem(index) => { - AdditionVariants::Mem(index.props_ts.iter().map(|(t, _)| *t)) - } - NodeAdditions::Range(index) => AdditionVariants::Range(match index { - TimeIndexWindow::Empty => TimeIndexWindowVariants::Empty(iter::empty()), - TimeIndexWindow::Range { timeindex, range } => TimeIndexWindowVariants::Range( - timeindex - .props_ts - .iter_window(range.clone()) - .map(|(t, _)| *t), - ), - TimeIndexWindow::All(index) => { - TimeIndexWindowVariants::All(index.props_ts.iter().map(|(t, _)| *t)) - } - }), - #[cfg(feature = "storage")] - NodeAdditions::Col(index) => { - AdditionVariants::Col(index.clone().prop_events().map(|t| t.into_iter()).kmerge()) - } - } - } - - #[inline] - pub fn prop_events_rev(&self) -> impl Iterator + use<'a> { - match self { - NodeAdditions::Mem(index) => { - AdditionVariants::Mem(index.props_ts.iter().map(|(t, _)| *t).rev()) - } - NodeAdditions::Range(index) => AdditionVariants::Range(match index { - TimeIndexWindow::Empty => TimeIndexWindowVariants::Empty(iter::empty()), - TimeIndexWindow::Range { timeindex, range } => TimeIndexWindowVariants::Range( - timeindex - .props_ts - .iter_window(range.clone()) - .map(|(t, _)| *t) - .rev(), - ), - TimeIndexWindow::All(index) => { - TimeIndexWindowVariants::All(index.props_ts.iter().map(|(t, _)| *t).rev()) - } - }), - #[cfg(feature = "storage")] - NodeAdditions::Col(index) => AdditionVariants::Col( - index - .clone() - .prop_events() - .map(|t| t.into_iter().rev()) - .kmerge_by(|t1, t2| t1 >= t2), - ), - } - } - - #[inline] - pub fn edge_events(&self) -> impl Iterator + use<'a> { - match self { - NodeAdditions::Mem(index) => { - AdditionVariants::Mem(index.edge_ts.iter().map(|(t, e)| (*t, *e))) - } - NodeAdditions::Range(index) => AdditionVariants::Range(match index { - TimeIndexWindow::Empty => TimeIndexWindowVariants::Empty(iter::empty()), - TimeIndexWindow::Range { timeindex, range } => TimeIndexWindowVariants::Range( - timeindex - .edge_ts - .iter_window(range.clone()) - .map(|(t, e)| (*t, *e)), - ), - TimeIndexWindow::All(index) => { - TimeIndexWindowVariants::All(index.edge_ts.iter().map(|(t, e)| (*t, *e))) - } - }), - #[cfg(feature = "storage")] - NodeAdditions::Col(index) => AdditionVariants::Col(index.edge_history()), - } - } - - #[inline] - pub fn edge_events_rev(&self) -> impl Iterator + use<'a> { - match self { - NodeAdditions::Mem(index) => { - AdditionVariants::Mem(index.edge_ts.iter().map(|(t, e)| (*t, *e)).rev()) - } - NodeAdditions::Range(index) => AdditionVariants::Range(match index { - TimeIndexWindow::Empty => TimeIndexWindowVariants::Empty(iter::empty()), - TimeIndexWindow::Range { timeindex, range } => TimeIndexWindowVariants::Range( - timeindex - .edge_ts - .iter_window(range.clone()) - .map(|(t, e)| (*t, *e)) - .rev(), - ), - TimeIndexWindow::All(index) => { - TimeIndexWindowVariants::All(index.edge_ts.iter().map(|(t, e)| (*t, *e)).rev()) - } - }), - #[cfg(feature = "storage")] - NodeAdditions::Col(index) => AdditionVariants::Col(index.edge_history_rev()), - } - } -} - -impl<'b> TimeIndexOps<'b> for NodeAdditions<'b> { - type IndexType = TimeIndexEntry; - type RangeType = Self; - - #[inline] - fn active(&self, w: Range) -> bool { - match self { - NodeAdditions::Mem(index) => index.active(w), - NodeAdditions::Range(index) => index.active(w), - #[cfg(feature = "storage")] - NodeAdditions::Col(index) => index.iter().any(|index| index.active(w.clone())), - } - } - - fn range(&self, w: Range) -> Self { - match self { - NodeAdditions::Mem(index) => NodeAdditions::Range(index.range(w)), - NodeAdditions::Range(index) => NodeAdditions::Range(index.range(w)), - #[cfg(feature = "storage")] - NodeAdditions::Col(index) => NodeAdditions::Col(index.with_range(w)), - } - } - - fn first(&self) -> Option { - match self { - NodeAdditions::Mem(index) => index.first(), - NodeAdditions::Range(index) => index.first(), - #[cfg(feature = "storage")] - NodeAdditions::Col(index) => index.iter().flat_map(|index| index.first()).min(), - } - } - - fn last(&self) -> Option { - match self { - NodeAdditions::Mem(index) => index.last(), - NodeAdditions::Range(index) => index.last(), - #[cfg(feature = "storage")] - NodeAdditions::Col(index) => index.iter().flat_map(|index| index.last()).max(), - } - } - - fn iter(self) -> impl Iterator + Send + Sync + 'b { - match self { - NodeAdditions::Mem(index) => AdditionVariants::Mem(index.iter()), - NodeAdditions::Range(index) => AdditionVariants::Range(index.iter()), - #[cfg(feature = "storage")] - NodeAdditions::Col(index) => { - AdditionVariants::Col(index.iter().map(|index| index.into_iter()).kmerge()) - } - } - } - - fn iter_rev(self) -> impl Iterator + Send + Sync + 'b { - match self { - NodeAdditions::Mem(index) => AdditionVariants::Mem(index.iter_rev()), - NodeAdditions::Range(index) => AdditionVariants::Range(index.iter_rev()), - #[cfg(feature = "storage")] - NodeAdditions::Col(index) => AdditionVariants::Col( - index - .iter() - .map(|index| index.into_iter().rev()) - .kmerge_by(|lt, rt| lt >= rt), - ), - } - } - - fn len(&self) -> usize { - match self { - NodeAdditions::Mem(index) => index.len(), - NodeAdditions::Range(range) => range.len(), - #[cfg(feature = "storage")] - NodeAdditions::Col(col) => col.len(), - } - } -} diff --git a/raphtory-storage/src/graph/nodes/row.rs b/raphtory-storage/src/graph/nodes/row.rs deleted file mode 100644 index 694e26218f..0000000000 --- a/raphtory-storage/src/graph/nodes/row.rs +++ /dev/null @@ -1,122 +0,0 @@ -use raphtory_api::core::entities::properties::prop::Prop; -use raphtory_core::storage::node_entry::MemRow; - -#[cfg(feature = "storage")] -use { - polars_arrow::datatypes::ArrowDataType, - pometry_storage::{ - graph::TemporalGraph, properties::TemporalProps, timestamps::TimeStamps, tprops::DiskTProp, - }, - raphtory_api::core::{entities::VID, storage::timeindex::TimeIndexEntry}, -}; - -#[derive(Debug, Copy, Clone)] -pub enum Row<'a> { - Mem(MemRow<'a>), - #[cfg(feature = "storage")] - Disk(DiskRow<'a>), -} - -impl<'a> IntoIterator for Row<'a> { - type Item = (usize, Option); - - type IntoIter = Box + 'a>; - - fn into_iter(self) -> Self::IntoIter { - match self { - Row::Mem(mem_row) => mem_row.into_iter(), - #[cfg(feature = "storage")] - Row::Disk(disk_row) => disk_row.into_iter(), - } - } -} - -#[cfg(feature = "storage")] -#[derive(Debug, Copy, Clone)] -pub struct DiskRow<'a> { - graph: &'a TemporalGraph, - ts: TimeStamps<'a, TimeIndexEntry>, - layer: usize, - row: usize, -} - -#[cfg(feature = "storage")] -impl<'a> DiskRow<'a> { - pub fn new( - graph: &'a TemporalGraph, - ts: TimeStamps<'a, TimeIndexEntry>, - row: usize, - layer: usize, - ) -> Self { - Self { - graph, - ts, - row, - layer, - } - } - - pub fn temporal_props(&'a self) -> &'a TemporalProps { - &self.graph.node_properties().temporal_props()[self.layer] - } -} - -#[cfg(feature = "storage")] -impl<'a> IntoIterator for DiskRow<'a> { - type Item = (usize, Option); - - type IntoIter = Box + 'a>; - - fn into_iter(self) -> Self::IntoIter { - let props = self.temporal_props(); - let iter = (0..props.prop_dtypes().len()).filter_map(move |prop_id| { - let global_prop = self - .graph - .prop_mapping() - .globalise_node_prop_id(self.layer, prop_id)?; - let props = self.temporal_props(); - Some(( - global_prop, - get( - &props.prop_for_ts::(self.ts, prop_id), - self.row, - ), - )) - }); - Box::new(iter) - } -} - -#[cfg(feature = "storage")] -fn get<'a>(disk_col: &DiskTProp<'a, TimeIndexEntry>, row: usize) -> Option { - use bigdecimal::BigDecimal; - use num_traits::FromPrimitive; - - match disk_col { - DiskTProp::Empty(_) => None, - DiskTProp::Bool(tprop_column) => tprop_column.get(row).map(|p| p.into()), - DiskTProp::Str64(tprop_column) => tprop_column.get(row).map(|p| p.into()), - DiskTProp::Str32(tprop_column) => tprop_column.get(row).map(|p| p.into()), - DiskTProp::Str(tprop_column) => tprop_column.get(row).map(|p| p.into()), - DiskTProp::I32(tprop_column) => tprop_column.get(row).map(|p| p.into()), - DiskTProp::I64(tprop_column) => tprop_column.get(row).map(|p| p.into()), - DiskTProp::U8(tprop_column) => tprop_column.get(row).map(|p| p.into()), - DiskTProp::U16(tprop_column) => tprop_column.get(row).map(|p| p.into()), - DiskTProp::U32(tprop_column) => tprop_column.get(row).map(|p| p.into()), - DiskTProp::U64(tprop_column) => tprop_column.get(row).map(|p| p.into()), - DiskTProp::F32(tprop_column) => tprop_column.get(row).map(|p| p.into()), - DiskTProp::F64(tprop_column) => tprop_column.get(row).map(|p| p.into()), - DiskTProp::I128(tprop_column) => { - let d_type = tprop_column.data_type()?; - match d_type { - ArrowDataType::Decimal(_, scale) => tprop_column.get(row).map(|p| { - BigDecimal::from_i128(p) - .unwrap() - .with_scale(*scale as i64) - .into() - }), - _ => unimplemented!("{d_type:?} not supported as disk_graph property"), - } - } - } -} From d75b316c027abf7d7b57fb1c4523274bc9eed9e0 Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Wed, 13 Aug 2025 14:52:52 +0200 Subject: [PATCH 103/321] remove unused code --- .../src/entities/graph/logical_to_physical.rs | 62 -- raphtory-core/src/entities/mod.rs | 1 - raphtory-core/src/entities/nodes/mod.rs | 1 - .../src/entities/properties/props.rs | 53 -- raphtory-core/src/storage/mod.rs | 664 +----------------- raphtory-storage/src/graph/nodes/mod.rs | 2 - raphtory-storage/src/mutation/addition_ops.rs | 29 +- .../src/mutation/addition_ops_ext.rs | 23 +- .../src/mutation/property_addition_ops.rs | 25 +- raphtory/Cargo.toml | 4 +- raphtory/src/db/api/mutation/addition_ops.rs | 6 +- raphtory/src/db/api/mutation/import_ops.rs | 5 +- raphtory/src/db/api/storage/storage.rs | 36 +- raphtory/src/search/entity_index.rs | 2 +- raphtory/src/serialise/serialise.rs | 628 ++++++++--------- 15 files changed, 354 insertions(+), 1187 deletions(-) diff --git a/raphtory-core/src/entities/graph/logical_to_physical.rs b/raphtory-core/src/entities/graph/logical_to_physical.rs index f95931ecdb..ab3c6609f4 100644 --- a/raphtory-core/src/entities/graph/logical_to_physical.rs +++ b/raphtory-core/src/entities/graph/logical_to_physical.rs @@ -1,7 +1,3 @@ -use crate::{ - entities::nodes::node_store::NodeStore, - storage::{NodeSlot, UninitialisedEntry}, -}; use dashmap::{mapref::entry::Entry, RwLockWriteGuard, SharedValue}; use either::Either; use hashbrown::raw::RawTable; @@ -277,27 +273,6 @@ impl Mapping { Ok(vid) } - pub fn get_or_init_node<'a>( - &self, - gid: GidRef, - f_init: impl FnOnce() -> UninitialisedEntry<'a, NodeStore, NodeSlot>, - ) -> Result, InvalidNodeId> { - let map = self.map.get_or_init(|| match &gid { - GidRef::U64(_) => Map::U64(FxDashMap::default()), - GidRef::Str(_) => Map::Str(FxDashMap::default()), - }); - match gid { - GidRef::U64(id) => map - .as_u64() - .map(|m| get_or_new(m, id, f_init)) - .ok_or(InvalidNodeId::InvalidNodeIdU64(id)), - GidRef::Str(id) => map - .as_str() - .map(|m| optim_get_or_insert(m, id, f_init)) - .ok_or_else(|| InvalidNodeId::InvalidNodeIdStr(id.into())), - } - } - pub fn validate_gids<'a>( &self, gids: impl IntoIterator>, @@ -353,43 +328,6 @@ impl Mapping { } } -#[inline] -fn optim_get_or_insert<'a>( - m: &FxDashMap, - id: &str, - f_init: impl FnOnce() -> UninitialisedEntry<'a, NodeStore, NodeSlot>, -) -> MaybeNew { - m.get(id) - .map(|vid| MaybeNew::Existing(*vid)) - .unwrap_or_else(|| get_or_new(m, id.to_owned(), f_init)) -} - -#[inline] -fn get_or_new<'a, K: Eq + Hash>( - m: &FxDashMap, - id: K, - f_init: impl FnOnce() -> UninitialisedEntry<'a, NodeStore, NodeSlot>, -) -> MaybeNew { - let entry = match m.entry(id) { - Entry::Occupied(entry) => Either::Left(*entry.get()), - Entry::Vacant(entry) => { - // This keeps the underlying storage shard locked for deferred initialisation but - // allows unlocking the map again. - let node = f_init(); - entry.insert(node.value().vid); - Either::Right(node) - } - }; - match entry { - Either::Left(vid) => MaybeNew::Existing(vid), - Either::Right(node_entry) => { - let vid = node_entry.value().vid; - node_entry.init(); - MaybeNew::New(vid) - } - } -} - impl<'de> Deserialize<'de> for Mapping { fn deserialize(deserializer: D) -> Result where diff --git a/raphtory-core/src/entities/mod.rs b/raphtory-core/src/entities/mod.rs index 0147447eaf..cd2323bd4d 100644 --- a/raphtory-core/src/entities/mod.rs +++ b/raphtory-core/src/entities/mod.rs @@ -1,4 +1,3 @@ -pub mod edges; pub mod graph; pub mod nodes; pub mod properties; diff --git a/raphtory-core/src/entities/nodes/mod.rs b/raphtory-core/src/entities/nodes/mod.rs index 094e8f0f17..3128f25de8 100644 --- a/raphtory-core/src/entities/nodes/mod.rs +++ b/raphtory-core/src/entities/nodes/mod.rs @@ -1,3 +1,2 @@ pub mod node_ref; -pub mod node_store; pub mod structure; diff --git a/raphtory-core/src/entities/properties/props.rs b/raphtory-core/src/entities/properties/props.rs index b875decb8b..69b43c6a68 100644 --- a/raphtory-core/src/entities/properties/props.rs +++ b/raphtory-core/src/entities/properties/props.rs @@ -10,13 +10,6 @@ use serde::{Deserialize, Serialize}; use std::fmt::Debug; use thiserror::Error; -#[derive(Serialize, Deserialize, Default, Debug, PartialEq)] -pub struct Props { - // properties - pub(crate) metadata: LazyVec>, - pub(crate) temporal_props: LazyVec, -} - #[derive(Error, Debug)] pub enum TPropError { #[error(transparent)] @@ -39,52 +32,6 @@ impl From>> for MetadataError { } } -impl Props { - pub fn new() -> Self { - Self { - metadata: Default::default(), - temporal_props: Default::default(), - } - } - - pub fn add_prop( - &mut self, - t: TimeIndexEntry, - prop_id: usize, - prop: Prop, - ) -> Result<(), TPropError> { - self.temporal_props.update(prop_id, |p| Ok(p.set(t, prop)?)) - } - - pub fn add_metadata(&mut self, prop_id: usize, prop: Prop) -> Result<(), MetadataError> { - Ok(self.metadata.set(prop_id, Some(prop))?) - } - - pub fn update_metadata(&mut self, prop_id: usize, prop: Prop) -> Result<(), MetadataError> { - self.metadata.update(prop_id, |n| { - *n = Some(prop); - Ok(()) - }) - } - - pub fn metadata(&self, prop_id: usize) -> Option<&Prop> { - let prop = self.metadata.get(prop_id)?; - prop.as_ref() - } - - pub fn temporal_prop(&self, prop_id: usize) -> Option<&TProp> { - self.temporal_props.get(prop_id) - } - - pub fn metadata_ids(&self) -> impl Iterator + '_ { - self.metadata.filled_ids() - } - - pub fn temporal_prop_ids(&self) -> impl Iterator + Send + Sync + '_ { - self.temporal_props.filled_ids() - } -} - #[cfg(test)] mod test { use super::*; diff --git a/raphtory-core/src/storage/mod.rs b/raphtory-core/src/storage/mod.rs index ba238693b4..822b63b172 100644 --- a/raphtory-core/src/storage/mod.rs +++ b/raphtory-core/src/storage/mod.rs @@ -1,23 +1,15 @@ use crate::{ - entities::{ - nodes::node_store::NodeStore, - properties::{props::TPropError, tprop::IllegalPropType}, - }, + entities::properties::{props::TPropError, tprop::IllegalPropType}, storage::lazy_vec::IllegalSet, }; use bigdecimal::BigDecimal; use itertools::Itertools; use lazy_vec::LazyVec; use lock_api; -use node_entry::NodePtr; -use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard}; #[cfg(feature = "arrow")] use raphtory_api::core::entities::properties::prop::PropArray; use raphtory_api::core::{ - entities::{ - properties::prop::{Prop, PropRef, PropType}, - GidRef, VID, - }, + entities::properties::prop::{Prop, PropRef, PropType}, storage::arc_str::ArcStr, }; use rayon::prelude::*; @@ -25,59 +17,17 @@ use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; use std::{ collections::HashMap, - fmt::{Debug, Formatter}, - marker::PhantomData, + fmt::Debug, ops::{Deref, DerefMut, Index, IndexMut}, - sync::{ - atomic::{AtomicUsize, Ordering}, - Arc, - }, + sync::Arc, }; use thiserror::Error; pub mod lazy_vec; pub mod locked_view; -pub mod node_entry; -pub mod raw_edges; pub mod timeindex; type ArcRwLockReadGuard = lock_api::ArcRwLockReadGuard; -#[must_use] -pub struct UninitialisedEntry<'a, T, TS> { - offset: usize, - guard: RwLockWriteGuard<'a, TS>, - value: T, -} - -impl<'a, T: Default, TS: DerefMut>> UninitialisedEntry<'a, T, TS> { - pub fn init(mut self) { - if self.offset >= self.guard.len() { - self.guard.resize_with(self.offset + 1, Default::default); - } - self.guard[self.offset] = self.value; - } - pub fn value(&self) -> &T { - &self.value - } -} - -#[inline] -fn resolve(index: usize, num_buckets: usize) -> (usize, usize) { - let bucket = index % num_buckets; - let offset = index / num_buckets; - (bucket, offset) -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct NodeVec { - data: Arc>, -} - -#[derive(Debug, Serialize, Deserialize, PartialEq, Default)] -pub struct NodeSlot { - nodes: Vec, - t_props_log: TColumns, // not the same size as nodes -} #[derive(Debug, Serialize, Deserialize, PartialEq, Default)] pub struct TColumns { @@ -437,526 +387,11 @@ impl PropColumn { } } -impl NodeSlot { - pub fn t_props_log(&self) -> &TColumns { - &self.t_props_log - } - - pub fn t_props_log_mut(&mut self) -> &mut TColumns { - &mut self.t_props_log - } - - pub fn iter(&self) -> impl Iterator> { - self.nodes - .iter() - .map(|ns| NodePtr::new(ns, &self.t_props_log)) - } - - pub fn par_iter(&self) -> impl ParallelIterator> { - self.nodes - .par_iter() - .map(|ns| NodePtr::new(ns, &self.t_props_log)) - } -} - -impl Index for NodeSlot { - type Output = NodeStore; - - fn index(&self, index: usize) -> &Self::Output { - &self.nodes[index] - } -} - -impl IndexMut for NodeSlot { - fn index_mut(&mut self, index: usize) -> &mut Self::Output { - &mut self.nodes[index] - } -} - -impl Deref for NodeSlot { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.nodes - } -} - -impl DerefMut for NodeSlot { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.nodes - } -} - -impl PartialEq for NodeVec { - fn eq(&self, other: &Self) -> bool { - let a = self.data.read(); - let b = other.data.read(); - a.deref() == b.deref() - } -} - -impl Default for NodeVec { - fn default() -> Self { - Self::new() - } -} - -impl NodeVec { - pub fn new() -> Self { - Self { - data: Arc::new(RwLock::new(Default::default())), - } - } - - #[inline] - pub fn read_arc_lock(&self) -> ArcRwLockReadGuard { - RwLock::read_arc(&self.data) - } - - #[inline] - pub fn write(&self) -> impl DerefMut + '_ { - self.data.write() - } - - #[inline] - pub fn read(&self) -> impl Deref + '_ { - self.data.read() - } -} - -#[derive(Serialize, Deserialize)] -pub struct NodeStorage { - pub(crate) data: Box<[NodeVec]>, - len: AtomicUsize, -} - -impl Debug for NodeStorage { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("NodeStorage") - .field("len", &self.len()) - .field("data", &self.read_lock().iter().collect_vec()) - .finish() - } -} - -impl PartialEq for NodeStorage { - fn eq(&self, other: &Self) -> bool { - self.data.eq(&other.data) - } -} - -#[derive(Debug)] -pub struct ReadLockedStorage { - pub(crate) locks: Vec>>, - len: usize, -} - -impl ReadLockedStorage { - fn resolve(&self, index: VID) -> (usize, usize) { - let index: usize = index.into(); - let n = self.locks.len(); - let bucket = index % n; - let offset = index / n; - (bucket, offset) - } - - pub fn len(&self) -> usize { - self.len - } - - pub fn is_empty(&self) -> bool { - self.len == 0 - } - - #[cfg(test)] - pub fn get(&self, index: VID) -> &NodeStore { - let (bucket, offset) = self.resolve(index); - let bucket = &self.locks[bucket]; - &bucket[offset] - } - - #[inline] - pub fn get_entry(&self, index: VID) -> NodePtr { - let (bucket, offset) = self.resolve(index); - let bucket = &self.locks[bucket]; - NodePtr::new(&bucket[offset], &bucket.t_props_log) - } - - pub fn iter(&self) -> impl Iterator + '_ { - self.locks.iter().flat_map(|v| v.iter()) - } - - pub fn par_iter(&self) -> impl ParallelIterator + '_ { - self.locks.par_iter().flat_map(|v| v.par_iter()) - } -} - -impl NodeStorage { - pub fn count_with_filter) -> bool + Send + Sync>(&self, f: F) -> usize { - self.read_lock().par_iter().filter(|x| f(*x)).count() - } -} - -impl NodeStorage { - #[inline] - fn resolve(&self, index: usize) -> (usize, usize) { - resolve(index, self.data.len()) - } - - #[inline] - pub fn read_lock(&self) -> ReadLockedStorage { - let guards = self - .data - .iter() - .map(|v| Arc::new(v.read_arc_lock())) - .collect(); - ReadLockedStorage { - locks: guards, - len: self.len(), - } - } - - pub fn write_lock(&self) -> WriteLockedNodes { - WriteLockedNodes { - guards: self.data.iter().map(|lock| lock.data.write()).collect(), - global_len: &self.len, - } - } - - pub fn new(n_locks: usize) -> Self { - let data: Box<[NodeVec]> = (0..n_locks) - .map(|_| NodeVec::new()) - .collect::>() - .into(); - - Self { - data, - len: AtomicUsize::new(0), - } - } - - pub fn push(&self, mut value: NodeStore) -> UninitialisedEntry { - let index = self.len.fetch_add(1, Ordering::Relaxed); - value.vid = VID(index); - let (bucket, offset) = self.resolve(index); - let guard = self.data[bucket].data.write(); - UninitialisedEntry { - offset, - guard, - value, - } - } - - pub fn set(&self, value: NodeStore) { - let VID(index) = value.vid; - self.len.fetch_max(index + 1, Ordering::Relaxed); - let (bucket, offset) = self.resolve(index); - let mut guard = self.data[bucket].data.write(); - if guard.len() <= offset { - guard.resize_with(offset + 1, NodeStore::default) - } - guard[offset] = value - } - - #[inline] - pub fn entry(&self, index: VID) -> NodeEntry<'_> { - let index = index.into(); - let (bucket, offset) = self.resolve(index); - let guard = self.data[bucket].data.read_recursive(); - NodeEntry { offset, guard } - } - - pub fn entry_mut(&self, index: VID) -> EntryMut<'_, RwLockWriteGuard<'_, NodeSlot>> { - let index = index.into(); - let (bucket, offset) = self.resolve(index); - let guard = self.data[bucket].data.write(); - EntryMut { - i: offset, - guard, - _pd: PhantomData, - } - } - - pub fn prop_entry_mut(&self, index: VID) -> impl DerefMut + '_ { - let index = index.into(); - let (bucket, _) = self.resolve(index); - let lock = self.data[bucket].data.write(); - RwLockWriteGuard::map(lock, |data| &mut data.t_props_log) - } - - // This helps get the right locks when adding an edge - pub fn pair_entry_mut(&self, i: VID, j: VID) -> PairEntryMut<'_> { - let i = i.into(); - let j = j.into(); - let (bucket_i, offset_i) = self.resolve(i); - let (bucket_j, offset_j) = self.resolve(j); - // always acquire lock for smaller bucket first to avoid deadlock between two updates for the same pair of buckets - if bucket_i < bucket_j { - let guard_i = self.data[bucket_i].data.write(); - let guard_j = self.data[bucket_j].data.write(); - PairEntryMut::Different { - i: offset_i, - j: offset_j, - guard1: guard_i, - guard2: guard_j, - } - } else if bucket_i > bucket_j { - let guard_j = self.data[bucket_j].data.write(); - let guard_i = self.data[bucket_i].data.write(); - PairEntryMut::Different { - i: offset_i, - j: offset_j, - guard1: guard_i, - guard2: guard_j, - } - } else { - PairEntryMut::Same { - i: offset_i, - j: offset_j, - guard: self.data[bucket_i].data.write(), - } - } - } - - #[inline] - pub fn len(&self) -> usize { - self.len.load(Ordering::SeqCst) - } - - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - pub fn next_id(&self) -> VID { - VID(self.len.fetch_add(1, Ordering::Relaxed)) - } -} - -pub struct WriteLockedNodes<'a> { - guards: Vec>, - global_len: &'a AtomicUsize, -} - -pub struct NodeShardWriter<'a, S> { - shard: S, - shard_id: usize, - num_shards: usize, - global_len: &'a AtomicUsize, -} - -impl<'a, S> NodeShardWriter<'a, S> -where - S: DerefMut, -{ - #[inline] - fn resolve(&self, index: VID) -> Option { - let (shard_id, offset) = resolve(index.into(), self.num_shards); - (shard_id == self.shard_id).then_some(offset) - } - - #[inline] - pub fn get_mut(&mut self, index: VID) -> Option<&mut NodeStore> { - self.resolve(index).map(|offset| &mut self.shard[offset]) - } - - #[inline] - pub fn get_mut_entry(&mut self, index: VID) -> Option> { - self.resolve(index).map(|offset| EntryMut { - i: offset, - guard: &mut self.shard, - _pd: PhantomData, - }) - } - - #[inline] - pub fn get(&self, index: VID) -> Option<&NodeStore> { - self.resolve(index).map(|offset| &self.shard[offset]) - } - - #[inline] - pub fn t_prop_log_mut(&mut self) -> &mut TColumns { - &mut self.shard.t_props_log - } - - pub fn set(&mut self, vid: VID, gid: GidRef) -> Option> { - self.resolve(vid).map(|offset| { - if offset >= self.shard.len() { - self.shard.resize_with(offset + 1, NodeStore::default); - self.global_len - .fetch_max(vid.index() + 1, Ordering::Relaxed); - } - self.shard[offset] = NodeStore::resolved(gid.to_owned(), vid); - - EntryMut { - i: offset, - guard: &mut self.shard, - _pd: PhantomData, - } - }) - } - - pub fn shard_id(&self) -> usize { - self.shard_id - } - - fn resize(&mut self, new_global_len: usize) { - let mut new_len = new_global_len / self.num_shards; - if self.shard_id < new_global_len % self.num_shards { - new_len += 1; - } - if new_len > self.shard.len() { - self.shard.resize_with(new_len, Default::default); - self.global_len.fetch_max(new_global_len, Ordering::Relaxed); - } - } -} - -impl<'a> WriteLockedNodes<'a> { - pub fn par_iter_mut( - &mut self, - ) -> impl IndexedParallelIterator> + '_ { - let num_shards = self.guards.len(); - let global_len = self.global_len; - let shards: Vec<&mut NodeSlot> = self - .guards - .iter_mut() - .map(|guard| guard.deref_mut()) - .collect(); - shards - .into_par_iter() - .enumerate() - .map(move |(shard_id, shard)| NodeShardWriter { - shard, - shard_id, - num_shards, - global_len, - }) - } - - pub fn into_par_iter_mut( - self, - ) -> impl IndexedParallelIterator>> + 'a - { - let num_shards = self.guards.len(); - let global_len = self.global_len; - self.guards - .into_par_iter() - .enumerate() - .map(move |(shard_id, shard)| NodeShardWriter { - shard, - shard_id, - num_shards, - global_len, - }) - } - - pub fn resize(&mut self, new_len: usize) { - self.par_iter_mut() - .for_each(|mut shard| shard.resize(new_len)) - } - - pub fn num_shards(&self) -> usize { - self.guards.len() - } -} - -#[derive(Debug)] -pub struct NodeEntry<'a> { - offset: usize, - guard: RwLockReadGuard<'a, NodeSlot>, -} - -impl NodeEntry<'_> { - #[inline] - pub fn as_ref(&self) -> NodePtr<'_> { - NodePtr::new(&self.guard[self.offset], &self.guard.t_props_log) - } -} - -pub enum PairEntryMut<'a> { - Same { - i: usize, - j: usize, - guard: parking_lot::RwLockWriteGuard<'a, NodeSlot>, - }, - Different { - i: usize, - j: usize, - guard1: parking_lot::RwLockWriteGuard<'a, NodeSlot>, - guard2: parking_lot::RwLockWriteGuard<'a, NodeSlot>, - }, -} - -impl<'a> PairEntryMut<'a> { - pub(crate) fn get_i(&self) -> &NodeStore { - match self { - PairEntryMut::Same { i, guard, .. } => &guard[*i], - PairEntryMut::Different { i, guard1, .. } => &guard1[*i], - } - } - pub(crate) fn get_mut_i(&mut self) -> &mut NodeStore { - match self { - PairEntryMut::Same { i, guard, .. } => &mut guard[*i], - PairEntryMut::Different { i, guard1, .. } => &mut guard1[*i], - } - } - - pub(crate) fn get_j(&self) -> &NodeStore { - match self { - PairEntryMut::Same { j, guard, .. } => &guard[*j], - PairEntryMut::Different { j, guard2, .. } => &guard2[*j], - } - } - - pub(crate) fn get_mut_j(&mut self) -> &mut NodeStore { - match self { - PairEntryMut::Same { j, guard, .. } => &mut guard[*j], - PairEntryMut::Different { j, guard2, .. } => &mut guard2[*j], - } - } -} - -pub struct EntryMut<'a, NS: 'a> { - i: usize, - guard: NS, - _pd: PhantomData<&'a ()>, -} - -impl<'a, NS> EntryMut<'a, NS> { - pub fn to_mut(&mut self) -> EntryMut<'a, &mut NS> { - EntryMut { - i: self.i, - guard: &mut self.guard, - _pd: self._pd, - } - } -} - -impl<'a, NS: DerefMut> AsMut for EntryMut<'a, NS> { - fn as_mut(&mut self) -> &mut NodeStore { - let slots = self.guard.deref_mut(); - &mut slots[self.i] - } -} - -impl<'a, NS: DerefMut + 'a> EntryMut<'a, &'a mut NS> { - pub fn node_store_mut(&mut self) -> &mut NodeStore { - &mut self.guard[self.i] - } - - pub fn t_props_log_mut(&mut self) -> &mut TColumns { - &mut self.guard.t_props_log - } -} - #[cfg(test)] mod test { - use super::{NodeStorage, TColumns}; - use crate::entities::nodes::node_store::NodeStore; - use proptest::{arbitrary::any, prop_assert_eq, proptest}; - use raphtory_api::core::entities::{properties::prop::Prop, GID, VID}; + use super::TColumns; + use raphtory_api::core::entities::properties::prop::Prop; use rayon::prelude::*; - use std::borrow::Cow; #[test] fn tcolumns_append_1() { @@ -1081,91 +516,4 @@ mod test { ] ); } - - #[test] - fn add_5_values_to_storage() { - let storage = NodeStorage::new(2); - - for i in 0..5 { - storage.push(NodeStore::empty(i.into())).init(); - } - - assert_eq!(storage.len(), 5); - - for i in 0..5 { - let entry = storage.entry(VID(i)); - assert_eq!(entry.as_ref().node().vid, VID(i)); - } - - let items = storage.read_lock(); - - let actual = items - .iter() - .map(|s| s.node().vid.index()) - .collect::>(); - - assert_eq!(actual, vec![0, 2, 4, 1, 3]); - } - - #[test] - fn test_index_correctness() { - let storage = NodeStorage::new(2); - - for i in 0..5 { - storage.push(NodeStore::empty(i.into())).init(); - } - let locked = storage.read_lock(); - let actual: Vec<_> = (0..5) - .map(|i| (i, locked.get(VID(i)).global_id.to_str())) - .collect(); - - assert_eq!( - actual, - vec![ - (0usize, Cow::Borrowed("0")), - (1, "1".into()), - (2, "2".into()), - (3, "3".into()), - (4, "4".into()) - ] - ); - } - - #[test] - fn test_entry() { - let storage = NodeStorage::new(2); - - for i in 0..5 { - storage.push(NodeStore::empty(i.into())).init(); - } - - for i in 0..5 { - let entry = storage.entry(VID(i)); - assert_eq!(*entry.as_ref().node().global_id.to_str(), i.to_string()); - } - } - - #[test] - fn concurrent_push() { - proptest!(|(v in any::>())| { - let storage = NodeStorage::new(16); - let mut expected = v - .into_par_iter() - .map(|v| { - storage.push(NodeStore::empty(GID::U64(v))).init(); - v - }) - .collect::>(); - - let locked = storage.read_lock(); - let mut actual: Vec<_> = locked - .iter() - .map(|n| n.node().global_id.as_u64().unwrap()) - .collect(); - - actual.sort(); - expected.sort(); - prop_assert_eq!(actual, expected) - }) - } } diff --git a/raphtory-storage/src/graph/nodes/mod.rs b/raphtory-storage/src/graph/nodes/mod.rs index 18fcc48daf..155a4f661d 100644 --- a/raphtory-storage/src/graph/nodes/mod.rs +++ b/raphtory-storage/src/graph/nodes/mod.rs @@ -1,7 +1,5 @@ -pub mod node_additions; pub mod node_entry; pub mod node_ref; pub mod node_storage_ops; pub mod nodes; pub mod nodes_ref; -pub mod row; diff --git a/raphtory-storage/src/mutation/addition_ops.rs b/raphtory-storage/src/mutation/addition_ops.rs index c712852933..534c274569 100644 --- a/raphtory-storage/src/mutation/addition_ops.rs +++ b/raphtory-storage/src/mutation/addition_ops.rs @@ -19,10 +19,7 @@ use raphtory_api::{ }, inherit::Base, }; -use raphtory_core::{ - entities::{nodes::node_ref::NodeRef, ELID}, - storage::{raw_edges::WriteLockedEdges, WriteLockedNodes}, -}; +use raphtory_core::entities::{nodes::node_ref::NodeRef, ELID}; use storage::{Extension, WalImpl}; pub trait InternalAdditionOps { @@ -36,8 +33,7 @@ pub trait InternalAdditionOps { Self: 'a; fn write_lock(&self) -> Result, Self::Error>; - fn write_lock_nodes(&self) -> Result; - fn write_lock_edges(&self) -> Result; + /// map layer name to id and allocate a new layer if needed fn resolve_layer(&self, layer: Option<&str>) -> Result, Self::Error>; /// map external node id to internal id, allocating a new empty node if needed @@ -218,14 +214,6 @@ impl InternalAdditionOps for GraphStorage { self.mutable()?.write_lock() } - fn write_lock_nodes(&self) -> Result { - self.mutable()?.write_lock_nodes() - } - - fn write_lock_edges(&self) -> Result { - self.mutable()?.write_lock_edges() - } - fn resolve_layer(&self, layer: Option<&str>) -> Result, Self::Error> { self.mutable()?.resolve_layer(layer) } @@ -330,16 +318,6 @@ where self.base().write_lock() } - #[inline] - fn write_lock_nodes(&self) -> Result { - self.base().write_lock_nodes() - } - - #[inline] - fn write_lock_edges(&self) -> Result { - self.base().write_lock_edges() - } - #[inline] fn resolve_layer(&self, layer: Option<&str>) -> Result, Self::Error> { self.base().resolve_layer(layer) @@ -404,7 +382,8 @@ where meta: &Meta, props: impl Iterator, ) -> Result>, Self::Error> { - self.base().validate_props_with_status(is_static, meta, props) + self.base() + .validate_props_with_status(is_static, meta, props) } #[inline] diff --git a/raphtory-storage/src/mutation/addition_ops_ext.rs b/raphtory-storage/src/mutation/addition_ops_ext.rs index 743eedb57a..8d99b20fbd 100644 --- a/raphtory-storage/src/mutation/addition_ops_ext.rs +++ b/raphtory-storage/src/mutation/addition_ops_ext.rs @@ -1,4 +1,3 @@ - use db4_graph::{TemporalGraph, TransactionManager, WriteLockedGraph}; use raphtory_api::core::{ entities::properties::{ @@ -9,22 +8,16 @@ use raphtory_api::core::{ }; use raphtory_core::{ entities::{ - graph::tgraph::TooManyLayers, - nodes::node_ref::NodeRef, - GidRef, EID, ELID, MAX_LAYER, VID, + graph::tgraph::TooManyLayers, nodes::node_ref::NodeRef, GidRef, EID, ELID, MAX_LAYER, VID, }, - storage::{raw_edges::WriteLockedEdges, timeindex::TimeIndexEntry, WriteLockedNodes}, + storage::timeindex::TimeIndexEntry, }; use storage::{ - pages::{ - node_page::writer::{node_info_as_props}, - session::WriteSession, - NODE_ID_PROP_KEY, - }, + pages::{node_page::writer::node_info_as_props, session::WriteSession, NODE_ID_PROP_KEY}, persist::strategy::PersistentStrategy, properties::props_meta_writer::PropsMetaWriter, resolver::GIDResolverOps, - Extension, ES, NS, WalImpl + Extension, WalImpl, ES, NS, }; use crate::mutation::{ @@ -205,14 +198,6 @@ impl InternalAdditionOps for TemporalGraph { Ok(locked_g) } - fn write_lock_nodes(&self) -> Result { - todo!() - } - - fn write_lock_edges(&self) -> Result { - todo!() - } - fn resolve_layer(&self, layer: Option<&str>) -> Result, Self::Error> { let id = self.edge_meta().get_or_create_layer_id(layer); // TODO: we replicate the layer id in the node meta as well, perhaps layer meta should be common diff --git a/raphtory-storage/src/mutation/property_addition_ops.rs b/raphtory-storage/src/mutation/property_addition_ops.rs index a7f3616782..fa689b0b77 100644 --- a/raphtory-storage/src/mutation/property_addition_ops.rs +++ b/raphtory-storage/src/mutation/property_addition_ops.rs @@ -1,22 +1,15 @@ use crate::{ - graph::{graph::GraphStorage, nodes::node_storage_ops::NodeStorageOps}, - mutation::MutationError, + graph::graph::GraphStorage, + mutation::{EdgeWriterT, MutationError, NodeWriterT}, }; -use parking_lot::RwLockWriteGuard; use raphtory_api::{ core::{ - entities::{ - properties::prop::{validate_prop, Prop}, - EID, VID, - }, + entities::{properties::prop::Prop, EID, VID}, storage::timeindex::TimeIndexEntry, }, inherit::Base, }; -use raphtory_core::storage::{EntryMut, NodeSlot}; -use raphtory_core::storage::raw_edges::EdgeWGuard; use storage::Extension; -use crate::mutation::{EdgeWriterT, NodeWriterT}; pub trait InternalPropertyAdditionOps { type Error: From; @@ -66,11 +59,7 @@ impl InternalPropertyAdditionOps for db4_graph::TemporalGraph { todo!() } - - fn internal_update_metadata( - &self, - props: &[(usize, Prop)], - ) -> Result<(), Self::Error> { + fn internal_update_metadata(&self, props: &[(usize, Prop)]) -> Result<(), Self::Error> { todo!() } @@ -85,7 +74,11 @@ impl InternalPropertyAdditionOps for db4_graph::TemporalGraph { Ok(writer) } - fn internal_update_node_metadata(&self, vid: VID, props: &[(usize, Prop)]) -> Result { + fn internal_update_node_metadata( + &self, + vid: VID, + props: &[(usize, Prop)], + ) -> Result { todo!() } diff --git a/raphtory/Cargo.toml b/raphtory/Cargo.toml index 5dc8295610..447b2eb177 100644 --- a/raphtory/Cargo.toml +++ b/raphtory/Cargo.toml @@ -73,7 +73,7 @@ minijinja-contrib = { workspace = true, optional = true } arroy = { workspace = true, optional = true } heed = { workspace = true, optional = true } sysinfo = { workspace = true, optional = true } -moka = { workspace = true, optional = true } +moka = { workspace = true, optional = true, version = "1.0.0", features = ["future"] } # python binding optional dependencies pyo3 = { workspace = true, optional = true } @@ -145,7 +145,7 @@ vectors = [ "dep:heed", "dep:sysinfo", "dep:moka", - "dep:tempfile", # also used for the storage feature + "dep:tempfile", # also used for the storage feature ] # Enables generating the pyo3 python bindings diff --git a/raphtory/src/db/api/mutation/addition_ops.rs b/raphtory/src/db/api/mutation/addition_ops.rs index 0eb0912c87..fe92a660dc 100644 --- a/raphtory/src/db/api/mutation/addition_ops.rs +++ b/raphtory/src/db/api/mutation/addition_ops.rs @@ -13,10 +13,8 @@ use crate::{ errors::{into_graph_err, GraphError}, prelude::{GraphViewOps, NodeViewOps}, }; -use raphtory_api::core::{entities::properties::prop::Prop, storage::dict_mapper::MaybeNew::New}; -use raphtory_storage::mutation::addition_ops::{ - EdgeWriteLock, InternalAdditionOps, SessionAdditionOps, -}; +use raphtory_api::core::entities::properties::prop::Prop; +use raphtory_storage::mutation::addition_ops::{EdgeWriteLock, InternalAdditionOps}; use storage::wal::{GraphWal, Wal}; pub trait AdditionOps: StaticGraphViewOps + InternalAdditionOps> { diff --git a/raphtory/src/db/api/mutation/import_ops.rs b/raphtory/src/db/api/mutation/import_ops.rs index 2fe5d65c36..77bf4bcce4 100644 --- a/raphtory/src/db/api/mutation/import_ops.rs +++ b/raphtory/src/db/api/mutation/import_ops.rs @@ -3,7 +3,6 @@ use crate::{ db::{ api::{ mutation::time_from_input_session, - properties::internal::InternalTemporalPropertiesOps, view::{internal::InternalMaterialize, StaticGraphViewOps}, }, graph::{edge::EdgeView, node::NodeView}, @@ -14,14 +13,12 @@ use crate::{ PropertyAdditionOps, }, }; -use itertools::Itertools; use raphtory_api::core::{ entities::GID, storage::{arc_str::OptionAsStr, timeindex::AsTime}, }; use raphtory_storage::mutation::{ - addition_ops::{InternalAdditionOps, SessionAdditionOps}, - deletion_ops::InternalDeletionOps, + addition_ops::InternalAdditionOps, deletion_ops::InternalDeletionOps, property_addition_ops::InternalPropertyAdditionOps, }; use std::{borrow::Borrow, fmt::Debug}; diff --git a/raphtory/src/db/api/storage/storage.rs b/raphtory/src/db/api/storage/storage.rs index 923ded2509..6f7ebbda58 100644 --- a/raphtory/src/db/api/storage/storage.rs +++ b/raphtory/src/db/api/storage/storage.rs @@ -2,12 +2,11 @@ use crate::{ core::entities::nodes::node_ref::NodeRef, db::api::view::{ internal::{InheritEdgeHistoryFilter, InheritNodeHistoryFilter, InternalStorageOps}, - Base, IndexSpec, InheritViewOps, + Base, InheritViewOps, }, errors::GraphError, }; use db4_graph::{TemporalGraph, TransactionManager, WriteLockedGraph}; -use parking_lot::{RwLock, RwLockWriteGuard}; use raphtory_api::core::{ entities::{ properties::{ @@ -18,13 +17,7 @@ use raphtory_api::core::{ }, storage::{dict_mapper::MaybeNew, timeindex::TimeIndexEntry}, }; -use raphtory_core::{ - entities::ELID, - storage::{ - raw_edges::{EdgeWGuard, WriteLockedEdges}, - EntryMut, NodeSlot, WriteLockedNodes, - }, -}; +use raphtory_core::entities::ELID; use raphtory_storage::{ core_ops::InheritCoreGraphOps, graph::graph::GraphStorage, @@ -39,23 +32,24 @@ use raphtory_storage::{ }; use std::{ fmt::{Display, Formatter}, - ops::{Deref, DerefMut}, - path::{Path, PathBuf}, + path::Path, sync::Arc, }; -use storage::{ - segments::{edge::MemEdgeSegment, node::MemNodeSegment}, - Extension, WalImpl, -}; -use tracing::info; +use storage::{Extension, WalImpl}; #[cfg(feature = "proto")] use crate::serialise::GraphFolder; #[cfg(feature = "search")] use { - crate::search::graph_index::{GraphIndex, MutableGraphIndex}, + crate::{ + db::api::view::IndexSpec, + search::graph_index::{GraphIndex, MutableGraphIndex}, + }, once_cell::sync::OnceCell, + parking_lot::RwLock, + std::ops::{Deref, DerefMut}, + tracing::info, }; #[derive(Debug, Default)] @@ -444,14 +438,6 @@ impl InternalAdditionOps for Storage { Ok(self.graph.write_lock()?) } - fn write_lock_nodes(&self) -> Result { - Ok(self.graph.write_lock_nodes()?) - } - - fn write_lock_edges(&self) -> Result { - Ok(self.graph.write_lock_edges()?) - } - fn resolve_layer(&self, layer: Option<&str>) -> Result, Self::Error> { let id = self.graph.resolve_layer(layer)?; diff --git a/raphtory/src/search/entity_index.rs b/raphtory/src/search/entity_index.rs index f825f7f7eb..10d781ee0a 100644 --- a/raphtory/src/search/entity_index.rs +++ b/raphtory/src/search/entity_index.rs @@ -126,7 +126,7 @@ impl EntityIndex { .into_par_iter() .try_for_each(|v_id| { let node = graph.core_node(VID(v_id)); - if let Some(prop_value) = node.prop(prop_id) { + if let Some(prop_value) = node.constant_prop_layer(0, prop_id) { let prop_doc = prop_index .create_node_metadata_document(v_id as u64, &prop_value)?; writer.add_document(prop_doc)?; diff --git a/raphtory/src/serialise/serialise.rs b/raphtory/src/serialise/serialise.rs index e7fc6d751e..be57366ae0 100644 --- a/raphtory/src/serialise/serialise.rs +++ b/raphtory/src/serialise/serialise.rs @@ -2,7 +2,7 @@ use super::{proto_ext::PropTypeExt, GraphFolder}; #[cfg(feature = "search")] use crate::prelude::IndexMutationOps; use crate::{ - core::entities::{graph::tgraph::TemporalGraph, LayerIds}, + core::entities::LayerIds, db::{ api::view::{MaterializedGraph, StaticGraphViewOps}, graph::views::deletion_graph::PersistentGraph, @@ -14,6 +14,7 @@ use crate::{ proto_ext, }, }; +use db4_graph::TemporalGraph; use itertools::Itertools; use prost::Message; use raphtory_api::core::{ @@ -278,319 +279,318 @@ impl StableEncode for MaterializedGraph { impl InternalStableDecode for TemporalGraph { fn decode_from_proto(graph: &proto::Graph) -> Result { - let storage = Self::default(); - graph.metas.par_iter().for_each(|meta| { - if let Some(meta) = meta.meta.as_ref() { - match meta { - Meta::NewNodeType(node_type) => { - storage - .node_meta - .node_type_meta() - .set_id(node_type.name.as_str(), node_type.id as usize); - } - Meta::NewNodeCprop(node_cprop) => { - let p_type = node_cprop.prop_type(); - storage.node_meta.metadata_mapper().set_id_and_dtype( - node_cprop.name.as_str(), - node_cprop.id as usize, - p_type, - ) - } - Meta::NewNodeTprop(node_tprop) => { - let p_type = node_tprop.prop_type(); - storage.node_meta.temporal_prop_mapper().set_id_and_dtype( - node_tprop.name.as_str(), - node_tprop.id as usize, - p_type, - ) - } - Meta::NewGraphCprop(graph_cprop) => storage - .graph_meta - .metadata_mapper() - .set_id(graph_cprop.name.as_str(), graph_cprop.id as usize), - Meta::NewGraphTprop(graph_tprop) => { - let p_type = graph_tprop.prop_type(); - storage.graph_meta.temporal_mapper().set_id_and_dtype( - graph_tprop.name.as_str(), - graph_tprop.id as usize, - p_type, - ) - } - Meta::NewLayer(new_layer) => storage - .edge_meta - .layer_meta() - .set_id(new_layer.name.as_str(), new_layer.id as usize), - Meta::NewEdgeCprop(edge_cprop) => { - let p_type = edge_cprop.prop_type(); - storage.edge_meta.metadata_mapper().set_id_and_dtype( - edge_cprop.name.as_str(), - edge_cprop.id as usize, - p_type, - ) - } - Meta::NewEdgeTprop(edge_tprop) => { - let p_type = edge_tprop.prop_type(); - storage.edge_meta.temporal_prop_mapper().set_id_and_dtype( - edge_tprop.name.as_str(), - edge_tprop.id as usize, - p_type, - ) - } - } - } - }); - - let new_edge_property_types = storage - .write_lock_edges()? - .into_par_iter_mut() - .map(|mut shard| { - let mut metadata_types = - vec![PropType::Empty; storage.edge_meta.metadata_mapper().len()]; - let mut temporal_prop_types = - vec![PropType::Empty; storage.edge_meta.temporal_prop_mapper().len()]; - - for edge in graph.edges.iter() { - if let Some(mut new_edge) = shard.get_mut(edge.eid()) { - let edge_store = new_edge.edge_store_mut(); - edge_store.src = edge.src(); - edge_store.dst = edge.dst(); - edge_store.eid = edge.eid(); - } - } - for update in graph.updates.iter() { - if let Some(update) = update.update.as_ref() { - match update { - Update::DelEdge(del_edge) => { - if let Some(mut edge_mut) = shard.get_mut(del_edge.eid()) { - edge_mut - .deletions_mut(del_edge.layer_id()) - .insert(del_edge.time()); - storage.update_time(del_edge.time()); - } - } - Update::UpdateEdgeCprops(update) => { - if let Some(mut edge_mut) = shard.get_mut(update.eid()) { - let edge_layer = edge_mut.layer_mut(update.layer_id()); - for prop_update in update.props() { - let (id, prop) = prop_update?; - let prop = storage.process_prop_value(&prop); - if let Ok(new_type) = unify_types( - &metadata_types[id], - &prop.dtype(), - &mut false, - ) { - metadata_types[id] = new_type; // the original types saved in protos are now incomplete we need to update them - } - edge_layer.update_metadata(id, prop)?; - } - } - } - Update::UpdateEdgeTprops(update) => { - if let Some(mut edge_mut) = shard.get_mut(update.eid()) { - edge_mut - .additions_mut(update.layer_id()) - .insert(update.time()); - if update.has_props() { - let edge_layer = edge_mut.layer_mut(update.layer_id()); - for prop_update in update.props() { - let (id, prop) = prop_update?; - let prop = storage.process_prop_value(&prop); - if let Ok(new_type) = unify_types( - &temporal_prop_types[id], - &prop.dtype(), - &mut false, - ) { - temporal_prop_types[id] = new_type; - // the original types saved in protos are now incomplete we need to update them - } - edge_layer.add_prop(update.time(), id, prop)?; - } - } - storage.update_time(update.time()) - } - } - _ => {} - } - } - } - Ok::<_, GraphError>((metadata_types, temporal_prop_types)) - }) - .try_reduce_with(|(l_const, l_temp), (r_const, r_temp)| { - unify_property_types(&l_const, &r_const, &l_temp, &r_temp) - }) - .transpose()?; - - if let Some((metadata_types, temp_prop_types)) = new_edge_property_types { - update_meta( - metadata_types, - temp_prop_types, - storage.edge_meta.metadata_mapper(), - storage.edge_meta.temporal_prop_mapper(), - ); - } - - let new_nodes_property_types = storage - .write_lock_nodes()? - .into_par_iter_mut() - .map(|mut shard| { - let mut metadata_types = - vec![PropType::Empty; storage.node_meta.metadata_mapper().len()]; - let mut temporal_prop_types = - vec![PropType::Empty; storage.node_meta.temporal_prop_mapper().len()]; - - for node in graph.nodes.iter() { - let vid = VID(node.vid as usize); - let gid = match node.gid.as_ref().unwrap() { - Gid::GidStr(name) => GidRef::Str(name), - Gid::GidU64(gid) => GidRef::U64(*gid), - }; - if let Some(mut node_store) = shard.set(vid, gid) { - storage.logical_to_physical.set(gid, vid)?; - node_store.node_store_mut().node_type = node.type_id as usize; - } - } - let edges = storage.storage.edges.read_lock(); - for edge in edges.iter() { - if let Some(src) = shard.get_mut(edge.src()) { - for layer in edge.layer_ids_iter(&LayerIds::All) { - src.add_edge(edge.dst(), Direction::OUT, layer, edge.eid()); - for t in edge.additions(layer).iter() { - src.update_time(t, edge.eid().with_layer(layer)); - } - for t in edge.deletions(layer).iter() { - src.update_time(t, edge.eid().with_layer_deletion(layer)); - } - } - } - if let Some(dst) = shard.get_mut(edge.dst()) { - for layer in edge.layer_ids_iter(&LayerIds::All) { - dst.add_edge(edge.src(), Direction::IN, layer, edge.eid()); - for t in edge.additions(layer).iter() { - dst.update_time(t, edge.eid().with_layer(layer)); - } - for t in edge.deletions(layer).iter() { - dst.update_time(t, edge.eid().with_layer_deletion(layer)); - } - } - } - } - for update in graph.updates.iter() { - if let Some(update) = update.update.as_ref() { - match update { - Update::UpdateNodeCprops(update) => { - if let Some(node) = shard.get_mut(update.vid()) { - for prop_update in update.props() { - let (id, prop) = prop_update?; - let prop = storage.process_prop_value(&prop); - if let Ok(new_type) = unify_types( - &metadata_types[id], - &prop.dtype(), - &mut false, - ) { - metadata_types[id] = new_type; // the original types saved in protos are now incomplete we need to update them - } - node.update_metadata(id, prop)?; - } - } - } - Update::UpdateNodeTprops(update) => { - if let Some(mut node) = shard.get_mut_entry(update.vid()) { - let mut props = vec![]; - for prop_update in update.props() { - let (id, prop) = prop_update?; - let prop = storage.process_prop_value(&prop); - if let Ok(new_type) = unify_types( - &temporal_prop_types[id], - &prop.dtype(), - &mut false, - ) { - temporal_prop_types[id] = new_type; // the original types saved in protos are now incomplete we need to update them - } - props.push((id, prop)); - } - - if props.is_empty() { - node.node_store_mut() - .update_t_prop_time(update.time(), None); - } else { - let prop_offset = node.t_props_log_mut().push(props)?; - node.node_store_mut() - .update_t_prop_time(update.time(), prop_offset); - } - - storage.update_time(update.time()) - } - } - Update::UpdateNodeType(update) => { - if let Some(node) = shard.get_mut(update.vid()) { - node.node_type = update.type_id(); - } - } - _ => {} - } - } - } - Ok::<_, GraphError>((metadata_types, temporal_prop_types)) - }) - .try_reduce_with(|(l_const, l_temp), (r_const, r_temp)| { - unify_property_types(&l_const, &r_const, &l_temp, &r_temp) - }) - .transpose()?; - - if let Some((metadata_types, temp_prop_types)) = new_nodes_property_types { - update_meta( - metadata_types, - temp_prop_types, - storage.node_meta.metadata_mapper(), - storage.node_meta.temporal_prop_mapper(), - ); - } - - let graph_prop_new_types = graph - .updates - .par_iter() - .map(|update| { - let mut metadata_types = - vec![PropType::Empty; storage.graph_meta.metadata_mapper().len()]; - let mut graph_prop_types = - vec![PropType::Empty; storage.graph_meta.temporal_mapper().len()]; - - if let Some(update) = update.update.as_ref() { - match update { - Update::UpdateGraphCprops(props) => { - let c_props = proto_ext::collect_props(&props.properties)?; - for (id, prop) in &c_props { - metadata_types[*id] = prop.dtype(); - } - storage.internal_update_metadata(&c_props)?; - } - Update::UpdateGraphTprops(props) => { - let time = TimeIndexEntry(props.time, props.secondary as usize); - let t_props = proto_ext::collect_props(&props.properties)?; - for (id, prop) in &t_props { - graph_prop_types[*id] = prop.dtype(); - } - storage.internal_add_properties(time, &t_props)?; - } - _ => {} - } - } - Ok::<_, GraphError>((metadata_types, graph_prop_types)) - }) - .try_reduce_with(|(l_const, l_temp), (r_const, r_temp)| { - unify_property_types(&l_const, &r_const, &l_temp, &r_temp) - }) - .transpose()?; - - if let Some((metadata_types, temp_prop_types)) = graph_prop_new_types { - update_meta( - metadata_types, - temp_prop_types, - &PropMapper::default(), - storage.graph_meta.temporal_mapper(), - ); - } - Ok(storage) + // let storage = Self::default(); + // graph.metas.par_iter().for_each(|meta| { + // if let Some(meta) = meta.meta.as_ref() { + // match meta { + // Meta::NewNodeType(node_type) => { + // storage.node_meta() + // .node_type_meta() + // .set_id(node_type.name.as_str(), node_type.id as usize); + // } + // Meta::NewNodeCprop(node_cprop) => { + // let p_type = node_cprop.prop_type(); + // storage.node_meta().metadata_mapper().set_id_and_dtype( + // node_cprop.name.as_str(), + // node_cprop.id as usize, + // p_type, + // ) + // } + // Meta::NewNodeTprop(node_tprop) => { + // let p_type = node_tprop.prop_type(); + // storage.node_meta().temporal_prop_mapper().set_id_and_dtype( + // node_tprop.name.as_str(), + // node_tprop.id as usize, + // p_type, + // ) + // } + // Meta::NewGraphCprop(graph_cprop) => storage + // .graph_meta + // .metadata_mapper() + // .set_id(graph_cprop.name.as_str(), graph_cprop.id as usize), + // Meta::NewGraphTprop(graph_tprop) => { + // let p_type = graph_tprop.prop_type(); + // storage.graph_meta.temporal_mapper().set_id_and_dtype( + // graph_tprop.name.as_str(), + // graph_tprop.id as usize, + // p_type, + // ) + // } + // Meta::NewLayer(new_layer) => storage.edge_meta() + // .layer_meta() + // .set_id(new_layer.name.as_str(), new_layer.id as usize), + // Meta::NewEdgeCprop(edge_cprop) => { + // let p_type = edge_cprop.prop_type(); + // storage.edge_meta().metadata_mapper().set_id_and_dtype( + // edge_cprop.name.as_str(), + // edge_cprop.id as usize, + // p_type, + // ) + // } + // Meta::NewEdgeTprop(edge_tprop) => { + // let p_type = edge_tprop.prop_type(); + // storage.edge_meta().temporal_prop_mapper().set_id_and_dtype( + // edge_tprop.name.as_str(), + // edge_tprop.id as usize, + // p_type, + // ) + // } + // } + // } + // }); + // + // let new_edge_property_types = storage + // .write_lock_edges()? + // .into_par_iter_mut() + // .map(|mut shard| { + // let mut metadata_types = + // vec![PropType::Empty; storage.edge_meta.metadata_mapper().len()]; + // let mut temporal_prop_types = + // vec![PropType::Empty; storage.edge_meta.temporal_prop_mapper().len()]; + // + // for edge in graph.edges.iter() { + // if let Some(mut new_edge) = shard.get_mut(edge.eid()) { + // let edge_store = new_edge.edge_store_mut(); + // edge_store.src = edge.src(); + // edge_store.dst = edge.dst(); + // edge_store.eid = edge.eid(); + // } + // } + // for update in graph.updates.iter() { + // if let Some(update) = update.update.as_ref() { + // match update { + // Update::DelEdge(del_edge) => { + // if let Some(mut edge_mut) = shard.get_mut(del_edge.eid()) { + // edge_mut + // .deletions_mut(del_edge.layer_id()) + // .insert(del_edge.time()); + // storage.update_time(del_edge.time()); + // } + // } + // Update::UpdateEdgeCprops(update) => { + // if let Some(mut edge_mut) = shard.get_mut(update.eid()) { + // let edge_layer = edge_mut.layer_mut(update.layer_id()); + // for prop_update in update.props() { + // let (id, prop) = prop_update?; + // let prop = storage.process_prop_value(&prop); + // if let Ok(new_type) = unify_types( + // &metadata_types[id], + // &prop.dtype(), + // &mut false, + // ) { + // metadata_types[id] = new_type; // the original types saved in protos are now incomplete we need to update them + // } + // edge_layer.update_metadata(id, prop)?; + // } + // } + // } + // Update::UpdateEdgeTprops(update) => { + // if let Some(mut edge_mut) = shard.get_mut(update.eid()) { + // edge_mut + // .additions_mut(update.layer_id()) + // .insert(update.time()); + // if update.has_props() { + // let edge_layer = edge_mut.layer_mut(update.layer_id()); + // for prop_update in update.props() { + // let (id, prop) = prop_update?; + // let prop = storage.process_prop_value(&prop); + // if let Ok(new_type) = unify_types( + // &temporal_prop_types[id], + // &prop.dtype(), + // &mut false, + // ) { + // temporal_prop_types[id] = new_type; + // // the original types saved in protos are now incomplete we need to update them + // } + // edge_layer.add_prop(update.time(), id, prop)?; + // } + // } + // storage.update_time(update.time()) + // } + // } + // _ => {} + // } + // } + // } + // Ok::<_, GraphError>((metadata_types, temporal_prop_types)) + // }) + // .try_reduce_with(|(l_const, l_temp), (r_const, r_temp)| { + // unify_property_types(&l_const, &r_const, &l_temp, &r_temp) + // }) + // .transpose()?; + // + // if let Some((metadata_types, temp_prop_types)) = new_edge_property_types { + // update_meta( + // metadata_types, + // temp_prop_types, + // storage.edge_meta.metadata_mapper(), + // storage.edge_meta.temporal_prop_mapper(), + // ); + // } + // + // let new_nodes_property_types = storage + // .write_lock_nodes()? + // .into_par_iter_mut() + // .map(|mut shard| { + // let mut metadata_types = + // vec![PropType::Empty; storage.node_meta.metadata_mapper().len()]; + // let mut temporal_prop_types = + // vec![PropType::Empty; storage.node_meta.temporal_prop_mapper().len()]; + // + // for node in graph.nodes.iter() { + // let vid = VID(node.vid as usize); + // let gid = match node.gid.as_ref().unwrap() { + // Gid::GidStr(name) => GidRef::Str(name), + // Gid::GidU64(gid) => GidRef::U64(*gid), + // }; + // if let Some(mut node_store) = shard.set(vid, gid) { + // storage.logical_to_physical.set(gid, vid)?; + // node_store.node_store_mut().node_type = node.type_id as usize; + // } + // } + // let edges = storage.storage.edges.read_lock(); + // for edge in edges.iter() { + // if let Some(src) = shard.get_mut(edge.src()) { + // for layer in edge.layer_ids_iter(&LayerIds::All) { + // src.add_edge(edge.dst(), Direction::OUT, layer, edge.eid()); + // for t in edge.additions(layer).iter() { + // src.update_time(t, edge.eid().with_layer(layer)); + // } + // for t in edge.deletions(layer).iter() { + // src.update_time(t, edge.eid().with_layer_deletion(layer)); + // } + // } + // } + // if let Some(dst) = shard.get_mut(edge.dst()) { + // for layer in edge.layer_ids_iter(&LayerIds::All) { + // dst.add_edge(edge.src(), Direction::IN, layer, edge.eid()); + // for t in edge.additions(layer).iter() { + // dst.update_time(t, edge.eid().with_layer(layer)); + // } + // for t in edge.deletions(layer).iter() { + // dst.update_time(t, edge.eid().with_layer_deletion(layer)); + // } + // } + // } + // } + // for update in graph.updates.iter() { + // if let Some(update) = update.update.as_ref() { + // match update { + // Update::UpdateNodeCprops(update) => { + // if let Some(node) = shard.get_mut(update.vid()) { + // for prop_update in update.props() { + // let (id, prop) = prop_update?; + // let prop = storage.process_prop_value(&prop); + // if let Ok(new_type) = unify_types( + // &metadata_types[id], + // &prop.dtype(), + // &mut false, + // ) { + // metadata_types[id] = new_type; // the original types saved in protos are now incomplete we need to update them + // } + // node.update_metadata(id, prop)?; + // } + // } + // } + // Update::UpdateNodeTprops(update) => { + // if let Some(mut node) = shard.get_mut_entry(update.vid()) { + // let mut props = vec![]; + // for prop_update in update.props() { + // let (id, prop) = prop_update?; + // let prop = storage.process_prop_value(&prop); + // if let Ok(new_type) = unify_types( + // &temporal_prop_types[id], + // &prop.dtype(), + // &mut false, + // ) { + // temporal_prop_types[id] = new_type; // the original types saved in protos are now incomplete we need to update them + // } + // props.push((id, prop)); + // } + // + // if props.is_empty() { + // node.node_store_mut() + // .update_t_prop_time(update.time(), None); + // } else { + // let prop_offset = node.t_props_log_mut().push(props)?; + // node.node_store_mut() + // .update_t_prop_time(update.time(), prop_offset); + // } + // + // storage.update_time(update.time()) + // } + // } + // Update::UpdateNodeType(update) => { + // if let Some(node) = shard.get_mut(update.vid()) { + // node.node_type = update.type_id(); + // } + // } + // _ => {} + // } + // } + // } + // Ok::<_, GraphError>((metadata_types, temporal_prop_types)) + // }) + // .try_reduce_with(|(l_const, l_temp), (r_const, r_temp)| { + // unify_property_types(&l_const, &r_const, &l_temp, &r_temp) + // }) + // .transpose()?; + // + // if let Some((metadata_types, temp_prop_types)) = new_nodes_property_types { + // update_meta( + // metadata_types, + // temp_prop_types, + // storage.node_meta.metadata_mapper(), + // storage.node_meta.temporal_prop_mapper(), + // ); + // } + // + // let graph_prop_new_types = graph + // .updates + // .par_iter() + // .map(|update| { + // let mut metadata_types = + // vec![PropType::Empty; storage.graph_meta.metadata_mapper().len()]; + // let mut graph_prop_types = + // vec![PropType::Empty; storage.graph_meta.temporal_mapper().len()]; + // + // if let Some(update) = update.update.as_ref() { + // match update { + // Update::UpdateGraphCprops(props) => { + // let c_props = proto_ext::collect_props(&props.properties)?; + // for (id, prop) in &c_props { + // metadata_types[*id] = prop.dtype(); + // } + // storage.internal_update_metadata(&c_props)?; + // } + // Update::UpdateGraphTprops(props) => { + // let time = TimeIndexEntry(props.time, props.secondary as usize); + // let t_props = proto_ext::collect_props(&props.properties)?; + // for (id, prop) in &t_props { + // graph_prop_types[*id] = prop.dtype(); + // } + // storage.internal_add_properties(time, &t_props)?; + // } + // _ => {} + // } + // } + // Ok::<_, GraphError>((metadata_types, graph_prop_types)) + // }) + // .try_reduce_with(|(l_const, l_temp), (r_const, r_temp)| { + // unify_property_types(&l_const, &r_const, &l_temp, &r_temp) + // }) + // .transpose()?; + // + // if let Some((metadata_types, temp_prop_types)) = graph_prop_new_types { + // update_meta( + // metadata_types, + // temp_prop_types, + // &PropMapper::default(), + // storage.graph_meta.temporal_mapper(), + // ); + // } + // Ok(storage) + todo!("fix this") } } From b5bff7064ecfc81c6c6475dbc2443f4309c7ae94 Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Wed, 13 Aug 2025 15:03:15 +0200 Subject: [PATCH 104/321] fix the inner workspace --- Cargo.lock | 46 ++++++++++++++++------------------- Cargo.toml | 10 ++++++++ python/Cargo.toml | 1 - raphtory-benchmark/Cargo.toml | 1 - raphtory-cypher/Cargo.toml | 4 --- raphtory-graphql/Cargo.toml | 1 - raphtory-storage/Cargo.toml | 3 --- raphtory/Cargo.toml | 14 ----------- 8 files changed, 31 insertions(+), 49 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 95fa7e8170..f8f8c47ac7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -864,6 +864,7 @@ checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" dependencies = [ "funty", "radium", + "serde", "tap", "wyz", ] @@ -901,9 +902,9 @@ dependencies = [ [[package]] name = "boxcar" -version = "0.2.12" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66bb12751a83493ef4b8da1120451a262554e216a247f14b48cb5e8fe7ed8bdf" +checksum = "36f64beae40a84da1b4b26ff2761a5b895c12adc41dc25aaee1c4f2bbfe97a6e" [[package]] name = "brotli" @@ -1923,30 +1924,32 @@ dependencies = [ ] [[package]] -name = "db4-common" -version = "0.15.1" +name = "db4-graph" +version = "0.16.0" dependencies = [ - "arrow-schema", - "parquet", - "raphtory", - "serde_json", - "thiserror 2.0.12", + "boxcar", + "db4-storage", + "parking_lot", + "raphtory-api", + "raphtory-core", + "tempfile", + "uuid", ] [[package]] name = "db4-storage" -version = "0.15.1" +version = "0.16.0" dependencies = [ "arrow", "arrow-array", "arrow-csv", "arrow-schema", "bigdecimal", + "bincode", "bitvec", "boxcar", "bytemuck", "chrono", - "db4-common", "either", "iter-enum", "itertools 0.13.0", @@ -1954,13 +1957,14 @@ dependencies = [ "parquet", "polars-arrow", "proptest", - "raphtory", "raphtory-api", + "raphtory-core", "rayon", "rustc-hash 2.1.1", "serde", "serde_json", "tempfile", + "thiserror 2.0.12", ] [[package]] @@ -4423,10 +4427,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32d19c6db79cb6a3c55af3b5a3976276edaab64cbf7f69b392617c2af30d7742" dependencies = [ "ahash", - "arrow-array", - "arrow-buffer", - "arrow-data", - "arrow-schema", "atoi_simd", "bytemuck", "chrono", @@ -4600,10 +4600,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "pometry-storage" -version = "0.16.0" - [[package]] name = "portable-atomic" version = "1.11.0" @@ -5075,6 +5071,8 @@ dependencies = [ "chrono", "csv", "dashmap", + "db4-graph", + "db4-storage", "display-error-chain", "dotenv", "either", @@ -5106,7 +5104,6 @@ dependencies = [ "polars-core", "polars-io", "polars-parquet", - "pometry-storage", "pretty_assertions", "proptest", "proptest-derive", @@ -5116,8 +5113,6 @@ dependencies = [ "pyo3", "pyo3-arrow", "quad-rand", - "quickcheck", - "quickcheck_macros", "rand 0.8.5", "rand_distr", "raphtory-api", @@ -5208,6 +5203,7 @@ dependencies = [ "chrono", "dashmap", "either", + "hashbrown 0.15.3", "iter-enum", "itertools 0.13.0", "lock_api", @@ -5241,7 +5237,6 @@ dependencies = [ "pest", "pest_derive", "polars-arrow", - "pometry-storage", "pretty_assertions", "proptest", "raphtory", @@ -5319,13 +5314,14 @@ name = "raphtory-storage" version = "0.16.0" dependencies = [ "bigdecimal", + "db4-graph", + "db4-storage", "either", "iter-enum", "itertools 0.13.0", "num-traits", "parking_lot", "polars-arrow", - "pometry-storage", "proptest", "raphtory-api", "raphtory-core", diff --git a/Cargo.toml b/Cargo.toml index 1c51bbc37a..6adecede9f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,12 @@ incremental = false [workspace.dependencies] +db4-graph = { version = "0.16.0", path = "db4-graph", default-features = false } +raphtory = { version = "0.16.0", path = "raphtory", default-features = false } +raphtory-api = { version = "0.16.0", path = "raphtory-api", default-features = false } +raphtory-core = { version = "0.16.0", path = "raphtory-core", default-features = false } +raphtory-graphql = { version = "0.16.0", path = "raphtory-graphql", default-features = false } +raphtory-storage = { version = "0.16.0", path = "raphtory-storage", default-features = false } async-graphql = { version = "7.0.16", features = ["dynamic-schema"] } bincode = "1.3.3" async-graphql-poem = "7.0.16" @@ -58,6 +64,8 @@ reqwest = { version = "0.12.8", default-features = false, features = [ "multipart", "json", ] } +boxcar = "0.2.14" +arrow-csv = { version = "=53.4.1" } iter-enum = { version = "1.2.0", features = ["rayon"] } serde = { version = "1.0.197", features = ["derive", "rc"] } serde_json = "1.0.114" @@ -166,6 +174,7 @@ indexmap = { version = "2.7.0", features = ["rayon"] } fake = { version = "3.1.0", features = ["chrono"] } strsim = { version = "0.11.1" } uuid = { version = "1.16.0", features = ["v4"] } +bitvec = "1.0.1" # Make sure that transitive dependencies stick to disk_graph 50 [patch.crates-io] @@ -173,6 +182,7 @@ arrow-buffer = { git = "https://github.com/apache/arrow-rs.git", tag = "53.4.1" arrow-schema = { git = "https://github.com/apache/arrow-rs.git", tag = "53.4.1" } arrow-data = { git = "https://github.com/apache/arrow-rs.git", tag = "53.4.1" } arrow-array = { git = "https://github.com/apache/arrow-rs.git", tag = "53.4.1" } +arrow-csv = { git = "https://github.com/apache/arrow-rs.git", tag = "53.4.1" } [workspace.dependencies.storage] package = "db4-storage" diff --git a/python/Cargo.toml b/python/Cargo.toml index ce465e8bb3..c9eb9d8672 100644 --- a/python/Cargo.toml +++ b/python/Cargo.toml @@ -30,7 +30,6 @@ raphtory_core = { path = "../raphtory", version = "0.16.0", features = [ raphtory-graphql = { workspace = true, features = ["python", "search"] } [features] -storage = ["raphtory_core/storage", "raphtory-graphql/storage"] extension-module = ["pyo3/extension-module"] [build-dependencies] diff --git a/raphtory-benchmark/Cargo.toml b/raphtory-benchmark/Cargo.toml index 34df191a1a..aaca452940 100644 --- a/raphtory-benchmark/Cargo.toml +++ b/raphtory-benchmark/Cargo.toml @@ -87,4 +87,3 @@ required-features = ["search"] [features] search = ["raphtory/search"] -storage = ["raphtory/storage"] diff --git a/raphtory-cypher/Cargo.toml b/raphtory-cypher/Cargo.toml index 88a030dc83..c79ba8d77d 100644 --- a/raphtory-cypher/Cargo.toml +++ b/raphtory-cypher/Cargo.toml @@ -15,7 +15,6 @@ edition.workspace = true [dependencies] raphtory = { path = "../raphtory" } -pometry-storage = { workspace = true, optional = true } arrow.workspace = true arrow-buffer.workspace = true arrow-schema.workspace = true @@ -45,6 +44,3 @@ pretty_assertions.workspace = true tempfile.workspace = true tokio.workspace = true clap.workspace = true - -[features] -storage = ["raphtory/storage", "pometry-storage"] diff --git a/raphtory-graphql/Cargo.toml b/raphtory-graphql/Cargo.toml index 299bc04cae..52f55f658b 100644 --- a/raphtory-graphql/Cargo.toml +++ b/raphtory-graphql/Cargo.toml @@ -67,6 +67,5 @@ pretty_assertions = { workspace = true } arrow-array = { workspace = true } [features] -storage = ["raphtory/storage"] python = ["dep:pyo3", "raphtory/python"] search = ["raphtory/search"] diff --git a/raphtory-storage/Cargo.toml b/raphtory-storage/Cargo.toml index 983f6df37e..028f74528d 100644 --- a/raphtory-storage/Cargo.toml +++ b/raphtory-storage/Cargo.toml @@ -24,7 +24,6 @@ iter-enum = { workspace = true } serde = { workspace = true, features = ["derive"] } itertools = { workspace = true } thiserror = { workspace = true } -pometry-storage = { workspace = true, optional = true } polars-arrow = { workspace = true, optional = true } bigdecimal = { workspace = true, optional = true } num-traits = { workspace = true, optional = true } @@ -33,5 +32,3 @@ num-traits = { workspace = true, optional = true } proptest = { workspace = true } tempfile = { workspace = true } -[features] -storage = ["raphtory-api/storage", "dep:pometry-storage", "dep:polars-arrow", "dep:bigdecimal", "dep:num-traits"] diff --git a/raphtory/Cargo.toml b/raphtory/Cargo.toml index 447b2eb177..9c11a210c4 100644 --- a/raphtory/Cargo.toml +++ b/raphtory/Cargo.toml @@ -86,7 +86,6 @@ parquet = { workspace = true, optional = true } arrow-json = { workspace = true, optional = true } memmap2 = { workspace = true, optional = true } tempfile = { workspace = true, optional = true } -pometry-storage = { workspace = true, optional = true } pyo3-arrow = { workspace = true, optional = true } prost = { workspace = true, optional = true } @@ -99,8 +98,6 @@ uuid = { workspace = true } [dev-dependencies] csv = { workspace = true } pretty_assertions = { workspace = true } -quickcheck = { workspace = true } -quickcheck_macros = { workspace = true } tempfile = { workspace = true } tokio = { workspace = true } # for vector testing dotenv = { workspace = true } # for vector testing @@ -165,17 +162,6 @@ python = [ "raphtory-core/python", "kdam/notebook", ] -# storage -storage = [ - "arrow", - "pometry-storage/storage", - "raphtory-api/storage", - "raphtory-storage/storage", - "dep:memmap2", - "dep:tempfile", - "polars-arrow?/io_ipc", - "polars-arrow?/arrow_rs", -] arrow = [ "dep:polars-arrow", "dep:polars-parquet", From 09fee1e6a0a1190af578046210fd4946243d4fe3 Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Wed, 13 Aug 2025 15:49:48 +0200 Subject: [PATCH 105/321] need older version of hashbrown (used by dashmap) --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f8f8c47ac7..9452f824c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5079,7 +5079,7 @@ dependencies = [ "flate2", "futures-util", "glam", - "hashbrown 0.15.3", + "hashbrown 0.14.5", "heed", "indexmap 2.9.0", "indoc", @@ -5203,7 +5203,7 @@ dependencies = [ "chrono", "dashmap", "either", - "hashbrown 0.15.3", + "hashbrown 0.14.5", "iter-enum", "itertools 0.13.0", "lock_api", diff --git a/Cargo.toml b/Cargo.toml index 6adecede9f..2a7eebc4a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -118,7 +118,7 @@ polars-core = "0.42.0" polars-io = "0.42.0" bigdecimal = { version = "0.4.7", features = ["serde"] } kdam = "0.6.2" -hashbrown = "0.15.1" +hashbrown = { version = "0.14.5", features = ["raw"] } pretty_assertions = "1.4.0" quickcheck = "1.0.3" quickcheck_macros = "1.0.0" From 4613fb2a2d96a252e1e9b8eeeaba1f6aba693e2e Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Wed, 13 Aug 2025 15:51:36 +0200 Subject: [PATCH 106/321] fmt --- db4-storage/src/pages/mod.rs | 10 +- .../src/properties/props_meta_writer.rs | 10 +- db4-storage/src/wal/entry.rs | 7 +- raphtory-core/src/entities/graph/tgraph.rs | 10 +- raphtory-storage/src/core_ops.rs | 5 +- raphtory-storage/src/mutation/mod.rs | 14 ++- .../db/api/mutation/property_addition_ops.rs | 5 +- .../api/view/internal/time_semantics/mod.rs | 2 +- raphtory/src/db/api/view/node.rs | 44 ++++++- raphtory/src/io/arrow/df_loaders.rs | 119 +++++++----------- 10 files changed, 115 insertions(+), 111 deletions(-) diff --git a/db4-storage/src/pages/mod.rs b/db4-storage/src/pages/mod.rs index e24e913127..2e671d9d5e 100644 --- a/db4-storage/src/pages/mod.rs +++ b/db4-storage/src/pages/mod.rs @@ -264,7 +264,10 @@ impl< Ok(elid) } - fn as_time_index_entry(&self, t: T) -> Result { + fn as_time_index_entry( + &self, + t: T, + ) -> Result { let input_time = t.try_into_input_time()?; let t = match input_time { InputTime::Indexed(t, i) => TimeIndexEntry::new(t, i), @@ -388,7 +391,10 @@ impl< } } -fn write_graph_meta(graph_dir: impl AsRef, graph_meta: GraphMeta) -> Result<(), StorageError> { +fn write_graph_meta( + graph_dir: impl AsRef, + graph_meta: GraphMeta, +) -> Result<(), StorageError> { let meta_file = graph_dir.as_ref().join("graph_meta.json"); let meta_file = std::fs::File::create(meta_file).unwrap(); serde_json::to_writer_pretty(meta_file, &graph_meta)?; diff --git a/db4-storage/src/properties/props_meta_writer.rs b/db4-storage/src/properties/props_meta_writer.rs index 10a6801c1c..8fd116722b 100644 --- a/db4-storage/src/properties/props_meta_writer.rs +++ b/db4-storage/src/properties/props_meta_writer.rs @@ -1,9 +1,11 @@ use either::Either; -use raphtory_api::core::entities::properties::{ - meta::{LockedPropMapper, Meta, PropMapper}, - prop::{Prop, unify_types}, +use raphtory_api::core::{ + entities::properties::{ + meta::{LockedPropMapper, Meta, PropMapper}, + prop::{Prop, unify_types}, + }, + storage::dict_mapper::MaybeNew, }; -use raphtory_api::core::storage::dict_mapper::MaybeNew; use crate::error::StorageError; diff --git a/db4-storage/src/wal/entry.rs b/db4-storage/src/wal/entry.rs index 5e5d469375..71ba54ce4a 100644 --- a/db4-storage/src/wal/entry.rs +++ b/db4-storage/src/wal/entry.rs @@ -6,10 +6,9 @@ use raphtory_core::{ storage::timeindex::TimeIndexEntry, }; -use crate::wal::no_wal::NoWal; use crate::{ error::StorageError, - wal::{GraphReplayer, GraphWal, LSN, TransactionID}, + wal::{GraphReplayer, GraphWal, LSN, TransactionID, no_wal::NoWal}, }; impl GraphWal for NoWal { @@ -95,7 +94,9 @@ impl GraphWal for NoWal { Ok(0) } - fn replay_iter(_dir: impl AsRef) -> impl Iterator> { + fn replay_iter( + _dir: impl AsRef, + ) -> impl Iterator> { std::iter::once(Ok((0, ()))) } diff --git a/raphtory-core/src/entities/graph/tgraph.rs b/raphtory-core/src/entities/graph/tgraph.rs index 90bc105bf5..d1d3e96a19 100644 --- a/raphtory-core/src/entities/graph/tgraph.rs +++ b/raphtory-core/src/entities/graph/tgraph.rs @@ -1,13 +1,7 @@ -use raphtory_api::core::{ - entities::MAX_LAYER - , - storage::arc_str::ArcStr - , -}; -use std::{fmt::Debug}; +use raphtory_api::core::{entities::MAX_LAYER, storage::arc_str::ArcStr}; +use std::fmt::Debug; use thiserror::Error; - #[derive(Error, Debug)] #[error("Invalid layer: {invalid_layer}. Valid layers: {valid_layers}")] pub struct InvalidLayer { diff --git a/raphtory-storage/src/core_ops.rs b/raphtory-storage/src/core_ops.rs index 6209a46d21..95718891a1 100644 --- a/raphtory-storage/src/core_ops.rs +++ b/raphtory-storage/src/core_ops.rs @@ -15,10 +15,7 @@ use raphtory_api::{ inherit::Base, iter::{BoxedIter, BoxedLIter}, }; -use raphtory_core::entities::{ - nodes::node_ref::NodeRef, - properties::graph_meta::GraphMeta, -}; +use raphtory_core::entities::{nodes::node_ref::NodeRef, properties::graph_meta::GraphMeta}; use std::{iter, sync::Arc}; use storage::resolver::GIDResolverOps; diff --git a/raphtory-storage/src/mutation/mod.rs b/raphtory-storage/src/mutation/mod.rs index b9ffaa218f..c470ce8257 100644 --- a/raphtory-storage/src/mutation/mod.rs +++ b/raphtory-storage/src/mutation/mod.rs @@ -6,6 +6,7 @@ use crate::{ property_addition_ops::InheritPropertyAdditionOps, }, }; +use parking_lot::RwLockWriteGuard; use raphtory_api::{ core::entities::properties::prop::{InvalidBigDecimal, PropError}, inherit::Base, @@ -18,13 +19,14 @@ use raphtory_core::entities::{ }, }; use std::sync::Arc; -use parking_lot::RwLockWriteGuard; -use storage::{error::StorageError, resolver::GIDResolverError, Extension, ES, NS}; +use storage::{ + error::StorageError, + pages::{edge_page::writer::EdgeWriter, node_page::writer::NodeWriter}, + resolver::GIDResolverError, + segments::{edge::MemEdgeSegment, node::MemNodeSegment}, + Extension, ES, NS, +}; use thiserror::Error; -use storage::pages::edge_page::writer::EdgeWriter; -use storage::pages::node_page::writer::NodeWriter; -use storage::segments::edge::MemEdgeSegment; -use storage::segments::node::MemNodeSegment; pub mod addition_ops; pub mod addition_ops_ext; diff --git a/raphtory/src/db/api/mutation/property_addition_ops.rs b/raphtory/src/db/api/mutation/property_addition_ops.rs index 3d988a2527..00e909d6ad 100644 --- a/raphtory/src/db/api/mutation/property_addition_ops.rs +++ b/raphtory/src/db/api/mutation/property_addition_ops.rs @@ -57,10 +57,7 @@ impl< Ok(()) } - fn update_metadata( - &self, - props: PI, - ) -> Result<(), GraphError> { + fn update_metadata(&self, props: PI) -> Result<(), GraphError> { let session = self.write_session().map_err(|err| err.into())?; let properties: Vec<_> = props.collect_properties(|name, dtype| { Ok(session diff --git a/raphtory/src/db/api/view/internal/time_semantics/mod.rs b/raphtory/src/db/api/view/internal/time_semantics/mod.rs index ba387572f8..7b190b80fc 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/mod.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/mod.rs @@ -1,9 +1,9 @@ use raphtory_api::{ core::{entities::properties::prop::Prop, storage::timeindex::TimeIndexEntry}, inherit::Base, + iter::BoxedLIter, }; use std::ops::Range; -use raphtory_api::iter::BoxedLIter; mod base_time_semantics; mod event_semantics; diff --git a/raphtory/src/db/api/view/node.rs b/raphtory/src/db/api/view/node.rs index a16262ce7a..bfd2084512 100644 --- a/raphtory/src/db/api/view/node.rs +++ b/raphtory/src/db/api/view/node.rs @@ -371,20 +371,54 @@ mod test { use crate::prelude::*; const EDGES: [(i64, u64, u64); 6] = [ - (1, 0, 1), (2, 0, 2), (-1, 1, 0), (0, 0, 0), (7, 2, 1), (1, 0, 0) + (1, 0, 1), + (2, 0, 2), + (-1, 1, 0), + (0, 0, 0), + (7, 2, 1), + (1, 0, 0), ]; fn create_graph() -> Graph { let g = Graph::new(); - g.add_node(0, 0, [("type", Prop::from("wallet")), ("cost", Prop::from(99.5))], None).unwrap(); - g.add_node(-1, 1, [("type", Prop::from("wallet")), ("cost", Prop::from(10.0))], None).unwrap(); - g.add_node(6, 2, [("type", Prop::from("wallet")), ("cost", Prop::from(76.0))], None).unwrap(); + g.add_node( + 0, + 0, + [("type", Prop::from("wallet")), ("cost", Prop::from(99.5))], + None, + ) + .unwrap(); + g.add_node( + -1, + 1, + [("type", Prop::from("wallet")), ("cost", Prop::from(10.0))], + None, + ) + .unwrap(); + g.add_node( + 6, + 2, + [("type", Prop::from("wallet")), ("cost", Prop::from(76.0))], + None, + ) + .unwrap(); for edge in EDGES { let (t, src, dst) = edge; - g.add_edge(t, src, dst, [("prop1", Prop::from(1)), ("prop2", Prop::from(9.8)), ("prop3", Prop::from("test"))], None).unwrap(); + g.add_edge( + t, + src, + dst, + [ + ("prop1", Prop::from(1)), + ("prop2", Prop::from(9.8)), + ("prop3", Prop::from("test")), + ], + None, + ) + .unwrap(); } g diff --git a/raphtory/src/io/arrow/df_loaders.rs b/raphtory/src/io/arrow/df_loaders.rs index a01cfe9001..87ef96e5b7 100644 --- a/raphtory/src/io/arrow/df_loaders.rs +++ b/raphtory/src/io/arrow/df_loaders.rs @@ -92,12 +92,11 @@ pub(crate) fn load_nodes_from_df< let time_index = df_view.get_index(time)?; let session = graph.write_session().map_err(into_graph_err)?; - let shared_metadata = - process_shared_properties(shared_metadata, |key, dtype| { - session - .resolve_node_property(key, dtype, true) - .map_err(into_graph_err) - })?; + let shared_metadata = process_shared_properties(shared_metadata, |key, dtype| { + session + .resolve_node_property(key, dtype, true) + .map_err(into_graph_err) + })?; #[cfg(feature = "python")] let mut pb = build_progress_bar("Loading nodes".to_string(), df_view.num_rows)?; @@ -117,16 +116,11 @@ pub(crate) fn load_nodes_from_df< .resolve_node_property(key, dtype, false) .map_err(into_graph_err) })?; - let metadata_cols = combine_properties( - metadata, - &metadata_indices, - &df, - |key, dtype| { - session - .resolve_node_property(key, dtype, true) - .map_err(into_graph_err) - }, - )?; + let metadata_cols = combine_properties(metadata, &metadata_indices, &df, |key, dtype| { + session + .resolve_node_property(key, dtype, true) + .map_err(into_graph_err) + })?; let node_type_col = lift_node_type_col(node_type, node_type_index, &df)?; let time_col = df.time_col(time_index)?; @@ -233,12 +227,11 @@ pub(crate) fn load_edges_from_df< None }; let session = graph.write_session().map_err(into_graph_err)?; - let shared_metadata = - process_shared_properties(shared_metadata, |key, dtype| { - session - .resolve_edge_property(key, dtype, true) - .map_err(into_graph_err) - })?; + let shared_metadata = process_shared_properties(shared_metadata, |key, dtype| { + session + .resolve_edge_property(key, dtype, true) + .map_err(into_graph_err) + })?; let mut pb = build_progress_bar("Loading edges".to_string(), df_view.num_rows)?; let _ = pb.update(0); @@ -289,16 +282,11 @@ pub(crate) fn load_edges_from_df< .resolve_edge_property(key, dtype, false) .map_err(into_graph_err) })?; - let metadata_cols = combine_properties( - metadata, - &metadata_indices, - &df, - |key, dtype| { - session - .resolve_edge_property(key, dtype, true) - .map_err(into_graph_err) - }, - )?; + let metadata_cols = combine_properties(metadata, &metadata_indices, &df, |key, dtype| { + session + .resolve_edge_property(key, dtype, true) + .map_err(into_graph_err) + })?; src_col_resolved.resize_with(df.len(), Default::default); dst_col_resolved.resize_with(df.len(), Default::default); @@ -647,12 +635,11 @@ pub(crate) fn load_node_props_from_df< let node_id_index = df_view.get_index(node_id)?; let session = graph.write_session().map_err(into_graph_err)?; - let shared_metadata = - process_shared_properties(shared_metadata, |key, dtype| { - session - .resolve_node_property(key, dtype, true) - .map_err(into_graph_err) - })?; + let shared_metadata = process_shared_properties(shared_metadata, |key, dtype| { + session + .resolve_node_property(key, dtype, true) + .map_err(into_graph_err) + })?; #[cfg(feature = "python")] let mut pb = build_progress_bar("Loading node properties".to_string(), df_view.num_rows)?; @@ -664,16 +651,11 @@ pub(crate) fn load_node_props_from_df< for chunk in df_view.chunks { let df = chunk?; - let metadata_cols = combine_properties( - metadata, - &metadata_indices, - &df, - |key, dtype| { - session - .resolve_node_property(key, dtype, true) - .map_err(into_graph_err) - }, - )?; + let metadata_cols = combine_properties(metadata, &metadata_indices, &df, |key, dtype| { + session + .resolve_node_property(key, dtype, true) + .map_err(into_graph_err) + })?; let node_type_col = lift_node_type_col(node_type, node_type_index, &df)?; let node_col = df.node_col(node_id_index)?; @@ -755,12 +737,11 @@ pub(crate) fn load_edges_props_from_df< None }; let session = graph.write_session().map_err(into_graph_err)?; - let shared_metadata = - process_shared_properties(shared_metadata, |key, dtype| { - session - .resolve_edge_property(key, dtype, true) - .map_err(into_graph_err) - })?; + let shared_metadata = process_shared_properties(shared_metadata, |key, dtype| { + session + .resolve_edge_property(key, dtype, true) + .map_err(into_graph_err) + })?; #[cfg(feature = "python")] let mut pb = build_progress_bar("Loading edge properties".to_string(), df_view.num_rows)?; @@ -777,16 +758,11 @@ pub(crate) fn load_edges_props_from_df< for chunk in df_view.chunks { let df = chunk?; - let metadata_cols = combine_properties( - metadata, - &metadata_indices, - &df, - |key, dtype| { - session - .resolve_edge_property(key, dtype, true) - .map_err(into_graph_err) - }, - )?; + let metadata_cols = combine_properties(metadata, &metadata_indices, &df, |key, dtype| { + session + .resolve_edge_property(key, dtype, true) + .map_err(into_graph_err) + })?; let layer = lift_layer_col(layer, layer_index, &df)?; let layer_col_resolved = layer.resolve(graph)?; @@ -917,16 +893,11 @@ pub(crate) fn load_graph_props_from_df< .resolve_graph_property(key, dtype, false) .map_err(into_graph_err) })?; - let metadata_cols = combine_properties( - metadata, - &metadata_indices, - &df, - |key, dtype| { - session - .resolve_graph_property(key, dtype, true) - .map_err(into_graph_err) - }, - )?; + let metadata_cols = combine_properties(metadata, &metadata_indices, &df, |key, dtype| { + session + .resolve_graph_property(key, dtype, true) + .map_err(into_graph_err) + })?; let time_col = df.time_col(time_index)?; time_col From 7544a9ade81a7928014aaada501ee9011d40c468 Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Wed, 13 Aug 2025 17:43:12 +0200 Subject: [PATCH 107/321] failing test --- .../src/db/api/view/edge_property_filter.rs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/raphtory/src/db/api/view/edge_property_filter.rs b/raphtory/src/db/api/view/edge_property_filter.rs index 6ab491cf19..215490cfa6 100644 --- a/raphtory/src/db/api/view/edge_property_filter.rs +++ b/raphtory/src/db/api/view/edge_property_filter.rs @@ -331,6 +331,29 @@ mod test { }) } + #[test] + fn test_persistent_graph_materialise_window_2_updates() { + let g = PersistentGraph::new(); + g.add_edge(0, 0, 0, [("test", 0)], None).unwrap(); + g.add_edge(-5, 0, 0, [("test", 1)], None).unwrap(); + let start = -3; + let end = 0; + let v = 0; + let gwf = g + .window(start, end) + .filter_edges(PropertyFilterBuilder("test".to_string()).gt(v)) + .unwrap(); + let gwfm = gwf.materialize().unwrap(); + assert_persistent_materialize_graph_equal(&gwf, &gwfm); + + let gfw = g + .filter_edges(PropertyFilterBuilder("test".to_string()).gt(v)) + .unwrap() + .window(start, end); + let gfwm = gfw.materialize().unwrap(); + assert_persistent_materialize_graph_equal(&gfw, &gfwm); + } + #[test] fn test_single_unfiltered_edge_empty_window_persistent() { let g = PersistentGraph::new(); From 3fd1f72052a39597aba3a529d5df8cc5eb5e1d4c Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Wed, 13 Aug 2025 17:51:31 +0200 Subject: [PATCH 108/321] add missing implementation --- raphtory-storage/src/mutation/addition_ops_ext.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/raphtory-storage/src/mutation/addition_ops_ext.rs b/raphtory-storage/src/mutation/addition_ops_ext.rs index 8d99b20fbd..42af86c1c7 100644 --- a/raphtory-storage/src/mutation/addition_ops_ext.rs +++ b/raphtory-storage/src/mutation/addition_ops_ext.rs @@ -128,7 +128,10 @@ impl<'a> SessionAdditionOps for UnlockedSession<'a> { dtype: PropType, is_static: bool, ) -> Result, Self::Error> { - todo!() + Ok(self + .graph + .graph_meta + .resolve_property(prop, dtype, is_static)?) } fn resolve_node_property( From 3618d84f73f32b9d668de89a253592bb69555837 Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Thu, 14 Aug 2025 09:03:57 +0200 Subject: [PATCH 109/321] add graph properties (not atomic?) --- raphtory-graphql/schema.graphql | 2 +- .../src/mutation/property_addition_ops.rs | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/raphtory-graphql/schema.graphql b/raphtory-graphql/schema.graphql index 61d2c3a62b..caeeda51c0 100644 --- a/raphtory-graphql/schema.graphql +++ b/raphtory-graphql/schema.graphql @@ -344,8 +344,8 @@ type Graph { } type GraphAlgorithmPlugin { - shortest_path(source: String!, targets: [String!]!, direction: String): [ShortestPathOutput!]! pagerank(iterCount: Int!, threads: Int, tol: Float): [PagerankOutput!]! + shortest_path(source: String!, targets: [String!]!, direction: String): [ShortestPathOutput!]! } type GraphSchema { diff --git a/raphtory-storage/src/mutation/property_addition_ops.rs b/raphtory-storage/src/mutation/property_addition_ops.rs index fa689b0b77..0d5291dc96 100644 --- a/raphtory-storage/src/mutation/property_addition_ops.rs +++ b/raphtory-storage/src/mutation/property_addition_ops.rs @@ -52,15 +52,27 @@ impl InternalPropertyAdditionOps for db4_graph::TemporalGraph { t: TimeIndexEntry, props: &[(usize, Prop)], ) -> Result<(), Self::Error> { - todo!() + // FIXME: check atomicity + for (id, prop) in props { + self.graph_meta.add_prop(t, *id, prop.clone())?; + } + Ok(()) } fn internal_add_metadata(&self, props: &[(usize, Prop)]) -> Result<(), Self::Error> { - todo!() + // FIXME: check atomicity + for (id, prop) in props { + self.graph_meta.add_metadata(*id, prop.clone())?; + } + Ok(()) } fn internal_update_metadata(&self, props: &[(usize, Prop)]) -> Result<(), Self::Error> { - todo!() + // FIXME: check atomicity + for (id, prop) in props { + self.graph_meta.update_metadata(*id, prop.clone()); + } + Ok(()) } fn internal_add_node_metadata( From aa7d78fb261a83cb8da46dec590452a4c17fc2f6 Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Thu, 14 Aug 2025 10:10:49 +0200 Subject: [PATCH 110/321] missing rename to metadata --- raphtory-graphql/src/url_encode.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/raphtory-graphql/src/url_encode.rs b/raphtory-graphql/src/url_encode.rs index 9b3cb74be4..f45fe890ce 100644 --- a/raphtory-graphql/src/url_encode.rs +++ b/raphtory-graphql/src/url_encode.rs @@ -87,11 +87,10 @@ mod tests { graph.add_edge(1, 2, 3, [("bla", "blu")], None).unwrap(); let edge = graph.add_edge(2, 3, 4, [("foo", 42)], Some("7")).unwrap(); - edge.add_constant_properties([("14", 15f64)], Some("7")) - .unwrap(); + edge.add_metadata([("14", 15f64)], Some("7")).unwrap(); let node = graph.add_node(17, 0, NO_PROPS, None).unwrap(); - node.add_constant_properties([("blerg", "test")]).unwrap(); + node.add_metadata([("blerg", "test")]).unwrap(); let bytes = url_encode_graph(graph.clone()).unwrap(); let decoded_graph = url_decode_graph(bytes).unwrap(); From d084cb1c06871bc41a18b7da7151f676e33e0dc1 Mon Sep 17 00:00:00 2001 From: Fadhil Abubaker Date: Thu, 14 Aug 2025 11:20:39 -0400 Subject: [PATCH 111/321] Iterate over edges correctly in serialise (#2237) * USe META_FILE_NAME wherever .raph is used * Iterate over edges correctly in serialise --- raphtory-graphql/src/paths.rs | 6 +++--- raphtory/src/serialise/mod.rs | 4 ++-- raphtory/src/serialise/serialise.rs | 5 ++--- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/raphtory-graphql/src/paths.rs b/raphtory-graphql/src/paths.rs index 265c28e774..1692296a95 100644 --- a/raphtory-graphql/src/paths.rs +++ b/raphtory-graphql/src/paths.rs @@ -1,7 +1,7 @@ use crate::rayon::blocking_compute; use raphtory::{ errors::{GraphError, InvalidPathReason}, - serialise::{metadata::GraphMetadata, GraphFolder}, + serialise::{metadata::GraphMetadata, GraphFolder, META_FILE_NAME}, }; use std::{ fs, @@ -119,12 +119,12 @@ pub(crate) fn valid_path( } Component::Normal(component) => { // check if some intermediate path is already a graph - if full_path.join(".raph").exists() { + if full_path.join(META_FILE_NAME).exists() { return Err(InvalidPathReason::ParentIsGraph(user_facing_path)); } full_path.push(component); //check if the path with the component is a graph - if namespace && full_path.join(".raph").exists() { + if namespace && full_path.join(META_FILE_NAME).exists() { return Err(InvalidPathReason::ParentIsGraph(user_facing_path)); } //check for symlinks diff --git a/raphtory/src/serialise/mod.rs b/raphtory/src/serialise/mod.rs index 73e5198af5..cca9e03f3f 100644 --- a/raphtory/src/serialise/mod.rs +++ b/raphtory/src/serialise/mod.rs @@ -29,8 +29,8 @@ use std::{ }; use tracing::info; -const GRAPH_FILE_NAME: &str = "graph"; -const META_FILE_NAME: &str = ".raph"; +pub const GRAPH_FILE_NAME: &str = "graph"; +pub const META_FILE_NAME: &str = ".raph"; const INDEX_PATH: &str = "index"; const VECTORS_PATH: &str = "vectors"; diff --git a/raphtory/src/serialise/serialise.rs b/raphtory/src/serialise/serialise.rs index be57366ae0..bedf986464 100644 --- a/raphtory/src/serialise/serialise.rs +++ b/raphtory/src/serialise/serialise.rs @@ -222,9 +222,8 @@ impl StableEncode for GraphStorage { // Edges let edges = storage.edges(); - for eid in 0..edges.len() { - let eid = EID(eid); - let edge = edges.edge(eid); + for edge in edges.iter(&LayerIds::All) { + let eid = edge.eid(); let edge = edge.as_ref(); graph.new_edge(edge.src(), edge.dst(), eid); for layer_id in storage.unfiltered_layer_ids() { From f139ead2ec3568c64ebcba1ca550eee5fa65f781 Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Fri, 15 Aug 2025 13:35:12 +0200 Subject: [PATCH 112/321] update handling of private fields in DictMapper (compiles but need to construct correctly still) --- db4-graph/src/lib.rs | 2 +- db4-storage/src/api/nodes.rs | 5 +- .../src/properties/props_meta_writer.rs | 6 +- .../src/core/entities/properties/meta.rs | 47 +++--- raphtory-api/src/core/storage/dict_mapper.rs | 144 ++++++++++++++++-- raphtory-api/src/core/storage/locked_vec.rs | 47 ------ .../src/entities/properties/graph_meta.rs | 22 ++- .../src/model/schema/graph_schema.rs | 2 +- .../src/model/schema/node_schema.rs | 40 ++--- raphtory-storage/src/core_ops.rs | 17 +-- raphtory/src/db/api/mutation/import_ops.rs | 2 +- .../graph/storage_ops/time_semantics.rs | 2 +- raphtory/src/db/api/view/edge.rs | 2 +- raphtory/src/db/api/view/graph.rs | 16 +- .../time_semantics/persistent_semantics.rs | 6 +- raphtory/src/db/graph/edge.rs | 31 ++-- raphtory/src/db/graph/mod.rs | 5 +- raphtory/src/db/graph/node.rs | 13 +- .../views/filter/node_type_filtered_graph.rs | 2 +- raphtory/src/python/utils/export.rs | 9 +- raphtory/src/serialise/parquet/mod.rs | 8 +- raphtory/src/serialise/parquet/nodes.rs | 7 +- raphtory/src/serialise/serialise.rs | 136 ++++++----------- 23 files changed, 311 insertions(+), 260 deletions(-) diff --git a/db4-graph/src/lib.rs b/db4-graph/src/lib.rs index 617d388503..88ae9d3f4e 100644 --- a/db4-graph/src/lib.rs +++ b/db4-graph/src/lib.rs @@ -290,7 +290,7 @@ impl, ES = ES>> TemporalGraph { fn get_valid_layers(edge_meta: &Meta) -> Vec { edge_meta .layer_meta() - .get_keys() + .keys() .iter() .map(|x| x.to_string()) .collect::>() diff --git a/db4-storage/src/api/nodes.rs b/db4-storage/src/api/nodes.rs index 9fd69fb9cb..ed849e9560 100644 --- a/db4-storage/src/api/nodes.rs +++ b/db4-storage/src/api/nodes.rs @@ -239,7 +239,10 @@ pub trait NodeRefOps<'a>: Copy + Clone + Send + Sync + 'a { .map(|w| Iter2::I1(additions.range(w).iter())) .unwrap_or_else(|| Iter2::I2(additions.iter())); - let mut time_ordered_iter = (0..self.node_meta().temporal_prop_mapper().len()) + let mut time_ordered_iter = self + .node_meta() + .temporal_prop_mapper() + .ids() .map(move |prop_id| { self.temporal_prop_layer(layer_id, prop_id) .iter_inner(w.clone()) diff --git a/db4-storage/src/properties/props_meta_writer.rs b/db4-storage/src/properties/props_meta_writer.rs index 8fd116722b..9f45e0b603 100644 --- a/db4-storage/src/properties/props_meta_writer.rs +++ b/db4-storage/src/properties/props_meta_writer.rs @@ -265,7 +265,7 @@ mod test { assert_eq!(props, vec![(0, Prop::U32(0)), (1, Prop::U32(1))]); - assert_eq!(meta.temporal_prop_mapper().len(), 2); + assert_eq!(meta.temporal_prop_mapper().keys().len(), 2); } #[test] @@ -280,14 +280,14 @@ mod test { let props = writer.into_props_temporal().unwrap(); assert_eq!(props.len(), 1); - assert_eq!(meta.temporal_prop_mapper().len(), 1); + assert_eq!(meta.temporal_prop_mapper().keys().len(), 1); assert!(meta.temporal_prop_mapper().get_id("prop1").is_some()); let writer = PropsMetaWriter::temporal(&meta, vec![(ArcStr::from("prop1"), prop2)].into_iter()); assert!(writer.is_err()); - assert_eq!(meta.temporal_prop_mapper().len(), 1); + assert_eq!(meta.temporal_prop_mapper().keys().len(), 1); assert!(meta.temporal_prop_mapper().get_id("prop1").is_some()); } } diff --git a/raphtory-api/src/core/entities/properties/meta.rs b/raphtory-api/src/core/entities/properties/meta.rs index 3fb57b8b88..c78ace628a 100644 --- a/raphtory-api/src/core/entities/properties/meta.rs +++ b/raphtory-api/src/core/entities/properties/meta.rs @@ -15,8 +15,9 @@ use crate::core::{ entities::properties::prop::{check_for_unification, unify_types, PropError, PropType}, storage::{ arc_str::ArcStr, - dict_mapper::{DictMapper, LockedDictMapper, MaybeNew, WriteLockedDictMapper}, - locked_vec::ArcReadLockedVec, + dict_mapper::{ + AllKeys, DictMapper, LockedDictMapper, MaybeNew, PublicKeys, WriteLockedDictMapper, + }, }, }; @@ -36,7 +37,7 @@ impl Default for Meta { impl Meta { pub fn layer_iter(&self) -> impl Iterator + use<'_> { - (0..self.layer_mapper.len()).map(move |id| { + self.layer_mapper.ids().map(move |id| { let name = self.layer_mapper.get_name(id); (id, name) }) @@ -160,13 +161,9 @@ impl Meta { } } - pub fn get_all_layers(&self) -> Vec { - self.layer_mapper.get_values() - } - pub fn get_all_node_types(&self) -> Vec { self.node_type_mapper - .get_keys() + .keys() .iter() .filter_map(|key| { if key != "_default" { @@ -178,11 +175,11 @@ impl Meta { .collect() } - pub fn get_all_property_names(&self, is_static: bool) -> ArcReadLockedVec { + pub fn get_all_property_names(&self, is_static: bool) -> PublicKeys { if is_static { - self.metadata_mapper.get_keys() + self.metadata_mapper.keys() } else { - self.temporal_prop_mapper.get_keys() + self.temporal_prop_mapper.keys() } } @@ -212,6 +209,20 @@ impl Deref for PropMapper { } impl PropMapper { + pub fn new_with_private_fields( + fields: impl IntoIterator>, + dtypes: impl IntoIterator, + ) -> Self { + let dtypes = Vec::from_iter(dtypes); + let row_size = dtypes.iter().map(|dtype| dtype.est_size()).sum(); + + PropMapper { + id_mapper: DictMapper::new_with_private_fields(fields), + row_size: AtomicUsize::new(row_size), + dtypes: Arc::new(RwLock::new(dtypes)), + } + } + pub fn deep_clone(&self) -> Self { let dtypes = self.dtypes.read().clone(); Self { @@ -299,14 +310,6 @@ impl PropMapper { self.dtypes.read_recursive().get(prop_id).cloned() } - pub fn dtypes(&self) -> impl Deref> + '_ { - self.dtypes.read_recursive() - } - - pub fn locked_dtypes(&self) -> &RwLock> { - self.dtypes.as_ref() - } - pub fn locked(&self) -> LockedPropMapper<'_> { LockedPropMapper { dict_mapper: self.id_mapper.read(), @@ -393,6 +396,12 @@ impl<'a> LockedPropMapper<'a> { ) -> Result>, PropError> { fast_proptype_check(self.dict_mapper.map(), &self.d_types, prop, dtype) } + + pub fn iter_ids_and_types(&self) -> impl Iterator { + self.dict_mapper + .iter_ids() + .map(move |(id, name)| (id, name, &self.d_types[id])) + } } fn fast_proptype_check( diff --git a/raphtory-api/src/core/storage/dict_mapper.rs b/raphtory-api/src/core/storage/dict_mapper.rs index f1e6e3f88e..31f1fd5087 100644 --- a/raphtory-api/src/core/storage/dict_mapper.rs +++ b/raphtory-api/src/core/storage/dict_mapper.rs @@ -1,4 +1,4 @@ -use crate::core::storage::{arc_str::ArcStr, locked_vec::ArcReadLockedVec}; +use crate::core::storage::{arc_str::ArcStr, ArcRwLockReadGuard}; use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard}; use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; @@ -6,14 +6,15 @@ use std::{ borrow::{Borrow, BorrowMut}, collections::hash_map::Entry, hash::Hash, - ops::DerefMut, + ops::{Deref, DerefMut}, sync::Arc, }; #[derive(Serialize, Deserialize, Default, Debug)] pub struct DictMapper { map: Arc>>, - reverse_map: Arc>>, //FIXME: a boxcar vector would be a great fit if it was serializable... + reverse_map: Arc>>, + num_private_fields: usize, } #[derive(Copy, Clone, Debug)] @@ -107,6 +108,7 @@ impl BorrowMut for MaybeNew { pub struct LockedDictMapper<'a> { map: RwLockReadGuard<'a, FxHashMap>, reverse_map: RwLockReadGuard<'a, Vec>, + num_private_fields: usize, } pub struct WriteLockedDictMapper<'a> { @@ -122,6 +124,13 @@ impl LockedDictMapper<'_> { pub fn map(&self) -> &FxHashMap { &self.map } + + pub fn iter_ids(&self) -> impl Iterator + '_ { + self.reverse_map + .iter() + .enumerate() + .skip(self.num_private_fields) + } } impl WriteLockedDictMapper<'_> { @@ -160,6 +169,15 @@ impl WriteLockedDictMapper<'_> { } impl DictMapper { + pub fn new_with_private_fields(fields: impl IntoIterator>) -> Self { + let fields: Vec<_> = fields.into_iter().map(|s| s.into()).collect(); + let num_private_fields = fields.len(); + DictMapper { + map: Arc::new(Default::default()), + reverse_map: Arc::new(RwLock::new(fields)), + num_private_fields, + } + } pub fn contains(&self, key: &str) -> bool { self.map.read().contains_key(key) } @@ -170,6 +188,7 @@ impl DictMapper { Self { map: self.map.clone(), reverse_map: Arc::new(RwLock::new(reverse_map)), + num_private_fields: self.num_private_fields, } } @@ -177,6 +196,7 @@ impl DictMapper { LockedDictMapper { map: self.map.read(), reverse_map: self.reverse_map.read(), + num_private_fields: self.num_private_fields, } } @@ -239,7 +259,7 @@ impl DictMapper { keys[id] = name.into(); } - pub fn has_name(&self, id: usize) -> bool { + pub fn has_id(&self, id: usize) -> bool { let guard = self.reverse_map.read(); guard.get(id).is_some() } @@ -251,22 +271,37 @@ impl DictMapper { }) } - pub fn get_keys(&self) -> ArcReadLockedVec { - ArcReadLockedVec { + /// Public ids + pub fn ids(&self) -> impl Iterator { + self.num_private_fields..self.num_all_fields() + } + + /// All ids, including private fields + pub fn all_ids(&self) -> impl Iterator { + 0..self.num_all_fields() + } + + /// Public keys + pub fn keys(&self) -> PublicKeys { + PublicKeys { guard: self.reverse_map.read_arc(), + num_private_fields: self.num_private_fields, } } - pub fn get_values(&self) -> Vec { - self.map.read().iter().map(|(_, &entry)| entry).collect() + /// All keys including private fields + pub fn all_keys(&self) -> AllKeys { + AllKeys { + guard: self.reverse_map.read_arc(), + } } - pub fn len(&self) -> usize { + pub fn num_all_fields(&self) -> usize { self.reverse_map.read().len() } - pub fn is_empty(&self) -> bool { - self.reverse_map.read().is_empty() + pub fn num_fields(&self) -> usize { + self.map.read().len() } } @@ -349,3 +384,90 @@ mod test { assert_eq!(actual, vec![0, 1, 2, 3, 4]); } } + +#[derive(Debug)] +pub struct AllKeys { + pub(crate) guard: ArcRwLockReadGuard>, +} + +impl Deref for AllKeys { + type Target = [T]; + + #[inline] + fn deref(&self) -> &Self::Target { + self.guard.deref().deref() + } +} + +impl IntoIterator for AllKeys { + type Item = T; + type IntoIter = LockedIter; + + fn into_iter(self) -> Self::IntoIter { + let guard = self.guard; + let len = guard.len(); + let pos = 0; + LockedIter { guard, pos, len } + } +} + +pub struct PublicKeys { + guard: ArcRwLockReadGuard>, + num_private_fields: usize, +} + +impl PublicKeys { + fn items(&self) -> &[T] { + &self.guard[self.num_private_fields..] + } + pub fn iter(&self) -> impl Iterator + '_ { + self.items().iter() + } + + pub fn len(&self) -> usize { + self.items().len() + } + + pub fn is_empty(&self) -> bool { + self.items().is_empty() + } +} + +impl IntoIterator for PublicKeys { + type Item = T; + type IntoIter = LockedIter; + + fn into_iter(self) -> Self::IntoIter { + let guard = self.guard; + let len = guard.len(); + let pos = self.num_private_fields; + LockedIter { guard, pos, len } + } +} + +pub struct LockedIter { + guard: ArcRwLockReadGuard>, + pos: usize, + len: usize, +} + +impl Iterator for LockedIter { + type Item = T; + + fn next(&mut self) -> Option { + if self.pos < self.len { + let next_val = Some(self.guard[self.pos].clone()); + self.pos += 1; + next_val + } else { + None + } + } + + fn size_hint(&self) -> (usize, Option) { + let len = self.len - self.pos; + (len, Some(len)) + } +} + +impl ExactSizeIterator for LockedIter {} diff --git a/raphtory-api/src/core/storage/locked_vec.rs b/raphtory-api/src/core/storage/locked_vec.rs index a675b594b4..8b13789179 100644 --- a/raphtory-api/src/core/storage/locked_vec.rs +++ b/raphtory-api/src/core/storage/locked_vec.rs @@ -1,48 +1 @@ -use crate::core::storage::ArcRwLockReadGuard; -use std::ops::Deref; -#[derive(Debug)] -pub struct ArcReadLockedVec { - pub(crate) guard: ArcRwLockReadGuard>, -} - -impl Deref for ArcReadLockedVec { - type Target = Vec; - - #[inline] - fn deref(&self) -> &Self::Target { - self.guard.deref() - } -} - -impl IntoIterator for ArcReadLockedVec { - type Item = T; - type IntoIter = LockedIter; - - fn into_iter(self) -> Self::IntoIter { - let guard = self.guard; - let len = guard.len(); - let pos = 0; - LockedIter { guard, pos, len } - } -} - -pub struct LockedIter { - guard: ArcRwLockReadGuard>, - pos: usize, - len: usize, -} - -impl Iterator for LockedIter { - type Item = T; - - fn next(&mut self) -> Option { - if self.pos < self.len { - let next_val = Some(self.guard[self.pos].clone()); - self.pos += 1; - next_val - } else { - None - } - } -} diff --git a/raphtory-core/src/entities/properties/graph_meta.rs b/raphtory-core/src/entities/properties/graph_meta.rs index ba981af740..cf4cd14770 100644 --- a/raphtory-core/src/entities/properties/graph_meta.rs +++ b/raphtory-core/src/entities/properties/graph_meta.rs @@ -10,7 +10,11 @@ use raphtory_api::core::{ meta::PropMapper, prop::{Prop, PropError, PropType}, }, - storage::{arc_str::ArcStr, dict_mapper::MaybeNew, locked_vec::ArcReadLockedVec, FxDashMap}, + storage::{ + arc_str::ArcStr, + dict_mapper::{AllKeys, MaybeNew, PublicKeys}, + FxDashMap, + }, }; use serde::{Deserialize, Serialize}; use std::ops::{Deref, DerefMut}; @@ -134,20 +138,20 @@ impl GraphMeta { self.metadata_mapper.get_dtype(prop_id) } - pub fn metadata_names(&self) -> ArcReadLockedVec { - self.metadata_mapper.get_keys() + pub fn metadata_names(&self) -> PublicKeys { + self.metadata_mapper.keys() } pub fn metadata_ids(&self) -> impl Iterator { - 0..self.metadata_mapper.len() + self.metadata_mapper.ids() } - pub fn temporal_names(&self) -> ArcReadLockedVec { - self.temporal_mapper.get_keys() + pub fn temporal_names(&self) -> PublicKeys { + self.temporal_mapper.keys() } pub fn temporal_ids(&self) -> impl Iterator { - 0..self.temporal_mapper.len() + self.temporal_mapper.ids() } pub fn metadata(&self) -> impl Iterator + '_ { @@ -159,6 +163,8 @@ impl GraphMeta { pub fn temporal_props( &self, ) -> impl Iterator + '_)> + '_ { - (0..self.temporal_mapper.len()).filter_map(|id| self.temporal.get(&id).map(|v| (id, v))) + self.temporal_mapper + .ids() + .filter_map(|id| self.temporal.get(&id).map(|v| (id, v))) } } diff --git a/raphtory-graphql/src/model/schema/graph_schema.rs b/raphtory-graphql/src/model/schema/graph_schema.rs index 30aeeb5d1e..f0c007ae39 100644 --- a/raphtory-graphql/src/model/schema/graph_schema.rs +++ b/raphtory-graphql/src/model/schema/graph_schema.rs @@ -12,7 +12,7 @@ pub(crate) struct GraphSchema { impl GraphSchema { pub fn new(graph: &DynamicGraph) -> Self { - let node_types = 0..graph.node_meta().node_type_meta().len(); + let node_types = graph.node_meta().node_type_meta().ids(); let nodes = node_types .map(|node_type| NodeSchema::new(node_type, graph.clone())) .collect(); diff --git a/raphtory-graphql/src/model/schema/node_schema.rs b/raphtory-graphql/src/model/schema/node_schema.rs index a7f76a258a..47a9fc1a07 100644 --- a/raphtory-graphql/src/model/schema/node_schema.rs +++ b/raphtory-graphql/src/model/schema/node_schema.rs @@ -50,22 +50,14 @@ impl NodeSchema { .unwrap_or_else(|| DEFAULT_NODE_TYPE.to_string()) } fn properties_inner(&self) -> Vec { - let keys: Vec = self + let (keys, property_types): (Vec<_>, Vec<_>) = self .graph .node_meta() .temporal_prop_mapper() - .get_keys() - .into_iter() - .map(|k| k.to_string()) - .collect(); - let property_types: Vec = self - .graph - .node_meta() - .temporal_prop_mapper() - .dtypes() - .iter() - .map(|dtype| dtype.to_string()) - .collect(); + .locked() + .iter_ids_and_types() + .map(|(_, name, dtype)| (name.to_string(), dtype.to_string())) + .unzip(); if self.graph.unfiltered_num_nodes() > 1000 { // large graph, do not collect detailed schema as it is expensive @@ -78,7 +70,7 @@ impl NodeSchema { .zip(property_types) .filter_map(|(key, dtype)| { let mut node_types_filter = - vec![false; self.graph.node_meta().node_type_meta().len()]; + vec![false; self.graph.node_meta().node_type_meta().num_all_fields()]; node_types_filter[self.type_id] = true; let unique_values: ahash::HashSet<_> = NodeTypeFilteredGraph::new(self.graph.clone(), node_types_filter.into()) @@ -104,22 +96,14 @@ impl NodeSchema { } fn metadata_inner(&self) -> Vec { - let keys: Vec = self + let (keys, property_types): (Vec<_>, Vec<_>) = self .graph .node_meta() .metadata_mapper() - .get_keys() - .into_iter() - .map(|k| k.to_string()) - .collect(); - let property_types: Vec = self - .graph - .node_meta() - .metadata_mapper() - .dtypes() - .iter() - .map(|dtype| dtype.to_string()) - .collect(); + .locked() + .iter_ids_and_types() + .map(|(_, k, dtype)| (k.to_string(), dtype.to_string())) + .unzip(); if self.graph.unfiltered_num_nodes() > 1000 { // large graph, do not collect detailed schema as it is expensive @@ -132,7 +116,7 @@ impl NodeSchema { .zip(property_types) .filter_map(|(key, dtype)| { let mut node_types_filter = - vec![false; self.graph.node_meta().node_type_meta().len()]; + vec![false; self.graph.node_meta().node_type_meta().num_all_fields()]; node_types_filter[self.type_id] = true; let unique_values: ahash::HashSet<_> = NodeTypeFilteredGraph::new(self.graph.clone(), node_types_filter.into()) diff --git a/raphtory-storage/src/core_ops.rs b/raphtory-storage/src/core_ops.rs index 95718891a1..a8019576b0 100644 --- a/raphtory-storage/src/core_ops.rs +++ b/raphtory-storage/src/core_ops.rs @@ -13,7 +13,7 @@ use raphtory_api::{ storage::arc_str::ArcStr, }, inherit::Base, - iter::{BoxedIter, BoxedLIter}, + iter::{BoxedIter, BoxedLIter, IntoDynBoxed}, }; use raphtory_core::entities::{nodes::node_ref::NodeRef, properties::graph_meta::GraphMeta}; use std::{iter, sync::Arc}; @@ -150,13 +150,13 @@ pub trait CoreGraphOps: Send + Sync { let layer_ids = layer_ids.clone(); match layer_ids { LayerIds::None => Box::new(iter::empty()), - LayerIds::All => Box::new(self.edge_meta().layer_meta().get_keys().into_iter().skip(1)), // first layer is static graph + LayerIds::All => Box::new(self.edge_meta().layer_meta().keys().into_iter()), // first layer is static graph and private LayerIds::One(id) => { let name = self.edge_meta().layer_meta().get_name(id).clone(); Box::new(iter::once(name)) } LayerIds::Multiple(ids) => { - let keys = self.edge_meta().layer_meta().get_keys(); + let keys = self.edge_meta().layer_meta().all_keys(); Box::new(ids.into_iter().map(move |id| keys[id].clone())) } } @@ -230,11 +230,7 @@ pub trait CoreGraphOps: Send + Sync { /// # Returns /// The keys of the metadata. fn node_metadata_ids(&self, _v: VID) -> BoxedLIter { - // property 0 = node type, property 1 = external node id - // on an empty graph, this will return an empty range - let end = self.node_meta().metadata_mapper().len(); - let start = 2.min(end); - Box::new(start..end) + self.node_meta().metadata_mapper().ids().into_dyn_boxed() } /// Returns a vector of all ids of temporal properties within the given node @@ -246,7 +242,10 @@ pub trait CoreGraphOps: Send + Sync { /// # Returns /// The ids of the temporal properties fn temporal_node_prop_ids(&self, _v: VID) -> Box + '_> { - Box::new(0..self.node_meta().temporal_prop_mapper().len()) + self.node_meta() + .temporal_prop_mapper() + .ids() + .into_dyn_boxed() } } diff --git a/raphtory/src/db/api/mutation/import_ops.rs b/raphtory/src/db/api/mutation/import_ops.rs index 77bf4bcce4..9c4fcc504b 100644 --- a/raphtory/src/db/api/mutation/import_ops.rs +++ b/raphtory/src/db/api/mutation/import_ops.rs @@ -347,7 +347,7 @@ fn import_node_internal< } }; let session = graph.write_session().map_err(|err| err.into())?; - let keys = node.graph.node_meta().temporal_prop_mapper().get_keys(); + let keys = node.graph.node_meta().temporal_prop_mapper().all_keys(); for (t, row) in node.rows() { let t = time_from_input_session(&session, t)?; diff --git a/raphtory/src/db/api/storage/graph/storage_ops/time_semantics.rs b/raphtory/src/db/api/storage/graph/storage_ops/time_semantics.rs index 6622bb1d84..4cf9f3c470 100644 --- a/raphtory/src/db/api/storage/graph/storage_ops/time_semantics.rs +++ b/raphtory/src/db/api/storage/graph/storage_ops/time_semantics.rs @@ -95,7 +95,7 @@ impl GraphTimeSemanticsOps for GraphStorage { } fn has_temporal_prop(&self, prop_id: usize) -> bool { - prop_id < self.graph_meta().temporal_mapper().len() + self.graph_meta().temporal_mapper().has_id(prop_id) } fn temporal_prop_iter(&self, prop_id: usize) -> BoxedLIter<(TimeIndexEntry, Prop)> { diff --git a/raphtory/src/db/api/view/edge.rs b/raphtory/src/db/api/view/edge.rs index 285720e6f0..6d076280c7 100644 --- a/raphtory/src/db/api/view/edge.rs +++ b/raphtory/src/db/api/view/edge.rs @@ -591,7 +591,7 @@ impl<'graph, E: BaseEdgeViewOps<'graph>> EdgeViewOps<'graph> for E { fn layer_names(&self) -> Self::ValueType> { self.map(|g, e| { if edge_valid_layer(g, e) { - let layer_names = g.edge_meta().layer_meta().get_keys(); + let layer_names = g.edge_meta().layer_meta().all_keys(); match e.layer() { None => { let time_semantics = g.edge_time_semantics(); diff --git a/raphtory/src/db/api/view/graph.rs b/raphtory/src/db/api/view/graph.rs index ea9cf4f47c..6a2e9361e2 100644 --- a/raphtory/src/db/api/view/graph.rs +++ b/raphtory/src/db/api/view/graph.rs @@ -240,24 +240,24 @@ impl<'graph, G: GraphView + 'graph> GraphViewOps<'graph> for G { vec![] } LayerIds::All => { - let mut layer_map = vec![0; self.unfiltered_num_layers() + 1]; - let layers = storage.edge_meta().layer_meta().get_keys(); - for id in 0..layers.len() { - let new_id = g.resolve_layer(Some(&layers[id]))?.inner(); + let layers = storage.edge_meta().layer_meta().keys(); + let mut layer_map = vec![0; storage.edge_meta().layer_meta().num_all_fields()]; + for (id, name) in storage.edge_meta().layer_meta().ids().zip(layers.iter()) { + let new_id = g.resolve_layer(Some(&name))?.inner(); layer_map[id] = new_id; } layer_map } LayerIds::One(l_id) => { - let mut layer_map = vec![0; self.unfiltered_num_layers() + 1]; + let mut layer_map = vec![0; storage.edge_meta().layer_meta().num_all_fields()]; let new_id = g.resolve_layer(Some(&storage.edge_meta().get_layer_name_by_id(*l_id)))?; layer_map[*l_id] = new_id.inner(); layer_map } LayerIds::Multiple(ids) => { - let mut layer_map = vec![0; self.unfiltered_num_layers() + 1]; - let layers = storage.edge_meta().layer_meta().get_keys(); + let mut layer_map = vec![0; storage.edge_meta().layer_meta().num_all_fields()]; + let layers = storage.edge_meta().layer_meta().all_keys(); for id in ids { let new_id = g.resolve_layer(Some(&layers[id]))?.inner(); layer_map[id] = new_id; @@ -893,7 +893,7 @@ impl IndexSpecBuilder { } fn extract_props(meta: &PropMapper) -> HashSet { - (0..meta.len()).collect() + meta.ids().collect() } fn extract_named_props>( diff --git a/raphtory/src/db/api/view/internal/time_semantics/persistent_semantics.rs b/raphtory/src/db/api/view/internal/time_semantics/persistent_semantics.rs index 6f5b93d534..3607b44c9e 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/persistent_semantics.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/persistent_semantics.rs @@ -282,7 +282,7 @@ impl NodeTimeSemanticsOps for PersistentSemantics { fn node_updates_window<'graph, G: GraphViewOps<'graph>>( self, node: NodeStorageRef<'graph>, - _view: G, + view: G, w: Range, ) -> impl Iterator)> + Send + Sync + 'graph { let start = w.start; @@ -294,7 +294,9 @@ impl NodeTimeSemanticsOps for PersistentSemantics { .is_some() { Some( - (0.._view.node_meta().temporal_prop_mapper().len()) + view.node_meta() + .temporal_prop_mapper() + .ids() .map(|prop_id| (prop_id, node.tprop(prop_id))) .filter_map(|(i, tprop)| { if tprop.active_t(start..start.saturating_add(1)) { diff --git a/raphtory/src/db/graph/edge.rs b/raphtory/src/db/graph/edge.rs index e702a2c5df..de56020447 100644 --- a/raphtory/src/db/graph/edge.rs +++ b/raphtory/src/db/graph/edge.rs @@ -455,12 +455,20 @@ impl<'graph, G: GraphViewOps<'graph>, GH: GraphViewOps<'graph>> InternalMetadata } fn metadata_ids(&self) -> BoxedLIter { - Box::new(0..self.graph.edge_meta().metadata_mapper().len()) + self.graph + .edge_meta() + .metadata_mapper() + .ids() + .into_dyn_boxed() } fn metadata_keys(&self) -> BoxedLIter { - let reverse_map = self.graph.edge_meta().metadata_mapper().get_keys(); - Box::new(self.metadata_ids().map(move |id| reverse_map[id].clone())) + self.graph + .edge_meta() + .metadata_mapper() + .keys() + .into_iter() + .into_dyn_boxed() } fn get_metadata(&self, id: usize) -> Option { @@ -672,15 +680,20 @@ impl<'graph, G: GraphViewOps<'graph>, GH: GraphViewOps<'graph>> InternalTemporal } fn temporal_prop_ids(&self) -> BoxedLIter { - Box::new(0..self.graph.edge_meta().temporal_prop_mapper().len()) + self.graph + .edge_meta() + .temporal_prop_mapper() + .ids() + .into_dyn_boxed() } fn temporal_prop_keys(&self) -> BoxedLIter { - let reverse_map = self.graph.edge_meta().temporal_prop_mapper().get_keys(); - Box::new( - self.temporal_prop_ids() - .map(move |id| reverse_map[id].clone()), - ) + self.graph + .edge_meta() + .temporal_prop_mapper() + .keys() + .into_iter() + .into_dyn_boxed() } } diff --git a/raphtory/src/db/graph/mod.rs b/raphtory/src/db/graph/mod.rs index 6d19a7eb3e..8d393764e3 100644 --- a/raphtory/src/db/graph/mod.rs +++ b/raphtory/src/db/graph/mod.rs @@ -14,13 +14,12 @@ pub(crate) fn create_node_type_filter, V: AsRef>( dict_mapper: &DictMapper, node_types: I, ) -> Arc<[bool]> { - let len = dict_mapper.len(); - let mut bool_arr = vec![false; len]; + let mut bool_arr = vec![false; dict_mapper.num_all_fields()]; for nt in node_types { let nt = nt.as_ref(); if nt.is_empty() { - bool_arr[0] = true; + bool_arr[0] = true; // FIXME: "" treated as default? } else if let Some(id) = dict_mapper.get_id(nt) { bool_arr[id] = true; } diff --git a/raphtory/src/db/graph/node.rs b/raphtory/src/db/graph/node.rs index b115be4831..7484d867ff 100644 --- a/raphtory/src/db/graph/node.rs +++ b/raphtory/src/db/graph/node.rs @@ -248,7 +248,11 @@ impl<'graph, G: GraphView, GH: GraphView> InternalTemporalPropertiesOps } fn temporal_prop_ids(&self) -> BoxedLIter { - Box::new(0..self.graph.node_meta().temporal_prop_mapper().len()) + self.graph + .node_meta() + .temporal_prop_mapper() + .ids() + .into_dyn_boxed() } } @@ -335,8 +339,11 @@ impl<'graph, G: Send + Sync, GH: CoreGraphOps> InternalMetadataOps for NodeView< } fn metadata_ids(&self) -> BoxedLIter { - Box::new(0..self.graph.node_meta().metadata_mapper().len()) - // self.graph.node_metadata_ids(self.node) + self.graph + .node_meta() + .metadata_mapper() + .ids() + .into_dyn_boxed() } fn get_metadata(&self, id: usize) -> Option { diff --git a/raphtory/src/db/graph/views/filter/node_type_filtered_graph.rs b/raphtory/src/db/graph/views/filter/node_type_filtered_graph.rs index 3d53d02973..0cbd1e5eda 100644 --- a/raphtory/src/db/graph/views/filter/node_type_filtered_graph.rs +++ b/raphtory/src/db/graph/views/filter/node_type_filtered_graph.rs @@ -56,7 +56,7 @@ impl CreateNodeFilter for NodeTypeFilter { let node_types_filter = graph .node_meta() .node_type_meta() - .get_keys() + .all_keys() .iter() .map(|k| self.0.matches(Some(k))) // TODO: _default check .collect::>(); diff --git a/raphtory/src/python/utils/export.rs b/raphtory/src/python/utils/export.rs index d4187b7d6f..d125d01d7e 100644 --- a/raphtory/src/python/utils/export.rs +++ b/raphtory/src/python/utils/export.rs @@ -115,16 +115,11 @@ pub(crate) fn get_column_names_from_props( let mut is_prop_both_temp_and_const: HashSet = HashSet::new(); let temporal_properties: HashSet = edge_meta .temporal_prop_mapper() - .get_keys() - .iter() - .cloned() - .collect(); - let metadata: HashSet = edge_meta - .metadata_mapper() - .get_keys() + .keys() .iter() .cloned() .collect(); + let metadata: HashSet = edge_meta.metadata_mapper().keys().iter().cloned().collect(); metadata .intersection(&temporal_properties) .for_each(|name| { diff --git a/raphtory/src/serialise/parquet/mod.rs b/raphtory/src/serialise/parquet/mod.rs index 7911d8300c..4701027586 100644 --- a/raphtory/src/serialise/parquet/mod.rs +++ b/raphtory/src/serialise/parquet/mod.rs @@ -228,10 +228,10 @@ pub(crate) fn derive_schema( } fn arrow_fields(meta: &PropMapper) -> Vec { - meta.get_keys() - .into_iter() - .filter_map(|name| { - let prop_id = meta.get_id(&name)?; + meta.keys() + .iter() + .zip(meta.ids()) + .filter_map(|(name, prop_id)| { meta.get_dtype(prop_id) .map(move |prop_type| (name, prop_type)) }) diff --git a/raphtory/src/serialise/parquet/nodes.rs b/raphtory/src/serialise/parquet/nodes.rs index c080041d15..5131541745 100644 --- a/raphtory/src/serialise/parquet/nodes.rs +++ b/raphtory/src/serialise/parquet/nodes.rs @@ -33,12 +33,7 @@ pub(crate) fn encode_nodes_tprop( |nodes, g, decoder, writer| { let row_group_size = 100_000; - let cols = g - .node_meta() - .temporal_prop_mapper() - .get_keys() - .into_iter() - .collect_vec(); + let cols = g.node_meta().temporal_prop_mapper().all_keys(); let cols = &cols; for node_rows in nodes .into_iter() diff --git a/raphtory/src/serialise/serialise.rs b/raphtory/src/serialise/serialise.rs index bedf986464..db6c6f70dc 100644 --- a/raphtory/src/serialise/serialise.rs +++ b/raphtory/src/serialise/serialise.rs @@ -112,18 +112,12 @@ impl StableEncode for GraphStorage { // Graph Properties let graph_meta = storage.graph_meta(); - for (id, key) in graph_meta.metadata_mapper().get_keys().iter().enumerate() { + for (id, key) in graph_meta.metadata_mapper().read().iter_ids() { graph.new_graph_cprop(key, id); } graph.update_graph_cprops(graph_meta.metadata()); - for (id, (key, dtype)) in graph_meta - .temporal_mapper() - .get_keys() - .iter() - .zip(graph_meta.temporal_mapper().dtypes().iter()) - .enumerate() - { + for (id, key, dtype) in graph_meta.temporal_mapper().locked().iter_ids_and_types() { graph.new_graph_tprop(key, id, dtype); } for (t, group) in &graph_meta @@ -142,44 +136,22 @@ impl StableEncode for GraphStorage { } // Layers - for (id, layer) in storage - .edge_meta() - .layer_meta() - .get_keys() - .iter() - .enumerate() - { + for (id, layer) in storage.edge_meta().layer_meta().read().iter_ids() { graph.new_layer(layer, id); } // Node Types - for (id, node_type) in storage - .node_meta() - .node_type_meta() - .get_keys() - .iter() - .enumerate() - { + for (id, node_type) in storage.node_meta().node_type_meta().read().iter_ids() { graph.new_node_type(node_type, id); } // Node Properties let n_const_meta = self.node_meta().metadata_mapper(); - for (id, (key, dtype)) in n_const_meta - .get_keys() - .iter() - .zip(n_const_meta.dtypes().iter()) - .enumerate() - { + for (id, key, dtype) in n_const_meta.locked().iter_ids_and_types() { graph.new_node_cprop(key, id, dtype); } let n_temporal_meta = self.node_meta().temporal_prop_mapper(); - for (id, (key, dtype)) in n_temporal_meta - .get_keys() - .iter() - .zip(n_temporal_meta.dtypes().iter()) - .enumerate() - { + for (id, key, dtype) in n_temporal_meta.locked().iter_ids_and_types() { graph.new_node_tprop(key, id, dtype); } @@ -195,28 +167,19 @@ impl StableEncode for GraphStorage { graph.update_node_cprops( node.vid(), - (0..n_const_meta.len()) + n_const_meta + .ids() .flat_map(|i| node.constant_prop_layer(0, i).map(|v| (i, v))), ); } // Edge Properties let e_const_meta = self.edge_meta().metadata_mapper(); - for (id, (key, dtype)) in e_const_meta - .get_keys() - .iter() - .zip(e_const_meta.dtypes().iter()) - .enumerate() - { + for (id, key, dtype) in e_const_meta.locked().iter_ids_and_types() { graph.new_edge_cprop(key, id, dtype); } let e_temporal_meta = self.edge_meta().temporal_prop_mapper(); - for (id, (key, dtype)) in e_temporal_meta - .get_keys() - .iter() - .zip(e_temporal_meta.dtypes().iter()) - .enumerate() - { + for (id, key, dtype) in e_temporal_meta.locked().iter_ids_and_types() { graph.new_edge_tprop(key, id, dtype); } @@ -227,9 +190,9 @@ impl StableEncode for GraphStorage { let edge = edge.as_ref(); graph.new_edge(edge.src(), edge.dst(), eid); for layer_id in storage.unfiltered_layer_ids() { - for (t, props) in - zip_tprop_updates!((0..e_temporal_meta.len()) - .map(|i| (i, edge.temporal_prop_layer(layer_id, i)))) + for (t, props) in zip_tprop_updates!(e_temporal_meta + .ids() + .map(|i| (i, edge.temporal_prop_layer(layer_id, i)))) { graph.update_edge_tprops(eid, t, layer_id, props.map(|(_, v)| v)); } @@ -242,7 +205,8 @@ impl StableEncode for GraphStorage { graph.update_edge_cprops( eid, layer_id, - (0..e_const_meta.len()) + e_const_meta + .ids() .filter_map(|i| edge.metadata_layer(layer_id, i).map(|prop| (i, prop))), ); } @@ -593,41 +557,41 @@ impl InternalStableDecode for TemporalGraph { } } -fn update_meta( - metadata_types: Vec, - temp_prop_types: Vec, - const_meta: &PropMapper, - temp_meta: &PropMapper, -) { - let keys = { const_meta.get_keys().iter().cloned().collect::>() }; - for ((id, prop_type), key) in metadata_types.into_iter().enumerate().zip(keys) { - const_meta.set_id_and_dtype(key, id, prop_type); - } - let keys = { temp_meta.get_keys().iter().cloned().collect::>() }; - - for ((id, prop_type), key) in temp_prop_types.into_iter().enumerate().zip(keys) { - temp_meta.set_id_and_dtype(key, id, prop_type); - } -} - -fn unify_property_types( - l_const: &[PropType], - r_const: &[PropType], - l_temp: &[PropType], - r_temp: &[PropType], -) -> Result<(Vec, Vec), GraphError> { - let const_pt = l_const - .iter() - .zip(r_const) - .map(|(l, r)| unify_types(l, r, &mut false)) - .collect::, _>>()?; - let temp_pt = l_temp - .iter() - .zip(r_temp) - .map(|(l, r)| unify_types(l, r, &mut false)) - .collect::, _>>()?; - Ok((const_pt, temp_pt)) -} +// fn update_meta( +// metadata_types: Vec, +// temp_prop_types: Vec, +// const_meta: &PropMapper, +// temp_meta: &PropMapper, +// ) { +// let keys = { const_meta.get_keys().iter().cloned().collect::>() }; +// for ((id, prop_type), key) in metadata_types.into_iter().enumerate().zip(keys) { +// const_meta.set_id_and_dtype(key, id, prop_type); +// } +// let keys = { temp_meta.get_keys().iter().cloned().collect::>() }; +// +// for ((id, prop_type), key) in temp_prop_types.into_iter().enumerate().zip(keys) { +// temp_meta.set_id_and_dtype(key, id, prop_type); +// } +// } +// +// fn unify_property_types( +// l_const: &[PropType], +// r_const: &[PropType], +// l_temp: &[PropType], +// r_temp: &[PropType], +// ) -> Result<(Vec, Vec), GraphError> { +// let const_pt = l_const +// .iter() +// .zip(r_const) +// .map(|(l, r)| unify_types(l, r, &mut false)) +// .collect::, _>>()?; +// let temp_pt = l_temp +// .iter() +// .zip(r_temp) +// .map(|(l, r)| unify_types(l, r, &mut false)) +// .collect::, _>>()?; +// Ok((const_pt, temp_pt)) +// } impl InternalStableDecode for GraphStorage { fn decode_from_proto(graph: &proto::Graph) -> Result { From 889d7c5aab4a9701620fcf54f66bb181bb19f10b Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Fri, 15 Aug 2025 14:20:25 +0200 Subject: [PATCH 113/321] fix the metadata initialisation --- db4-graph/src/lib.rs | 10 +++--- db4-storage/src/api/nodes.rs | 8 ++--- db4-storage/src/pages/edge_store.rs | 4 +-- db4-storage/src/pages/mod.rs | 13 ++----- db4-storage/src/pages/node_store.rs | 2 +- db4-storage/src/pages/session.rs | 9 ----- .../src/properties/props_meta_writer.rs | 4 +-- .../src/core/entities/properties/meta.rs | 34 +++++++++++++++++-- raphtory-api/src/core/storage/dict_mapper.rs | 9 ++++- .../src/mutation/addition_ops_ext.rs | 29 +++------------- raphtory/src/db/api/view/graph.rs | 4 +-- 11 files changed, 60 insertions(+), 66 deletions(-) diff --git a/db4-graph/src/lib.rs b/db4-graph/src/lib.rs index 88ae9d3f4e..254b11fcb9 100644 --- a/db4-graph/src/lib.rs +++ b/db4-graph/src/lib.rs @@ -116,14 +116,14 @@ impl Default for TemporalGraph { impl, ES = ES>> TemporalGraph { pub fn new() -> Self { - let node_meta = Meta::new(); - let edge_meta = Meta::new(); + let node_meta = Meta::new_for_nodes(); + let edge_meta = Meta::new_for_edges(); Self::new_with_meta(GraphDir::default(), node_meta, edge_meta) } pub fn new_with_path(path: impl AsRef) -> Self { - let node_meta = Meta::new(); - let edge_meta = Meta::new(); + let node_meta = Meta::new_for_nodes(); + let edge_meta = Meta::new_for_edges(); Self::new_with_meta(path.as_ref().into(), node_meta, edge_meta) } @@ -153,8 +153,6 @@ impl, ES = ES>> TemporalGraph { } pub fn new_with_meta(graph_dir: GraphDir, node_meta: Meta, edge_meta: Meta) -> Self { - edge_meta.get_or_create_layer_id(Some("staticgraph")); - node_meta.get_or_create_layer_id(Some("staticgraph")); std::fs::create_dir_all(&graph_dir) .unwrap_or_else(|_| panic!("Failed to create graph directory at {graph_dir:?}")); diff --git a/db4-storage/src/api/nodes.rs b/db4-storage/src/api/nodes.rs index ed849e9560..51cd6387f9 100644 --- a/db4-storage/src/api/nodes.rs +++ b/db4-storage/src/api/nodes.rs @@ -11,7 +11,7 @@ use raphtory_api::{ core::{ Direction, entities::properties::{ - meta::Meta, + meta::{Meta, NODE_ID_IDX, NODE_TYPE_IDX}, prop::{Prop, PropUnwrap}, tprop::TPropOps, }, @@ -331,17 +331,17 @@ pub trait NodeRefOps<'a>: Copy + Clone + Send + Sync + 'a { } fn gid(&self) -> GidRef<'a> { - self.c_prop_str(0, 1) + self.c_prop_str(0, NODE_ID_IDX) .map(GidRef::Str) .or_else(|| { - self.c_prop(0, 1) + self.c_prop(0, NODE_ID_IDX) .and_then(|prop| prop.into_u64().map(GidRef::U64)) }) .expect("Node GID should be present") } fn node_type_id(&self) -> usize { - self.c_prop(0, 0) + self.c_prop(0, NODE_TYPE_IDX) .and_then(|prop| prop.into_u64()) .map_or(0, |id| id as usize) } diff --git a/db4-storage/src/pages/edge_store.rs b/db4-storage/src/pages/edge_store.rs index 61971681b7..f5f0297327 100644 --- a/db4-storage/src/pages/edge_store.rs +++ b/db4-storage/src/pages/edge_store.rs @@ -136,7 +136,7 @@ impl, EXT: Clone + Send + Sync> EdgeStorageI } pub fn new(edges_path: impl AsRef, max_page_len: usize, ext: EXT) -> Self { - Self::new_with_meta(edges_path, max_page_len, Meta::new().into(), ext) + Self::new_with_meta(edges_path, max_page_len, Meta::new_for_edges().into(), ext) } pub fn pages(&self) -> &boxcar::Vec> { @@ -176,7 +176,7 @@ impl, EXT: Clone + Send + Sync> EdgeStorageI ) -> Result { let edges_path = edges_path.as_ref(); - let meta = Arc::new(Meta::new()); + let meta = Arc::new(Meta::new_for_edges()); if !edges_path.exists() { return Ok(Self::new(edges_path, max_page_len, ext.clone())); } diff --git a/db4-storage/src/pages/mod.rs b/db4-storage/src/pages/mod.rs index 2e671d9d5e..17c1d58d10 100644 --- a/db4-storage/src/pages/mod.rs +++ b/db4-storage/src/pages/mod.rs @@ -49,10 +49,6 @@ pub mod session; #[cfg(feature = "test-utils")] pub mod test_utils; -// Internal const props for node id and type -pub const NODE_ID_PROP_KEY: &str = "_raphtory_node_id"; -pub const NODE_TYPE_PROP_KEY: &str = "_raphtory_node_type"; - // graph // (node/edges) // segment // layer_ids (0, 1, 2, ...) // actual graphy bits #[derive(Debug)] @@ -186,11 +182,6 @@ impl< edge_meta, ext.clone(), )); - // Reserve node_type as a const prop on init - let _ = nodes - .prop_meta() - .metadata_mapper() - .get_or_create_and_validate(NODE_TYPE_PROP_KEY, PropType::U64); let graph_meta = GraphMeta { max_page_len_nodes, @@ -218,8 +209,8 @@ impl< graph_dir, max_page_len_nodes, max_page_len_edges, - Meta::new(), - Meta::new(), + Meta::new_for_nodes(), + Meta::new_for_edges(), ) } diff --git a/db4-storage/src/pages/node_store.rs b/db4-storage/src/pages/node_store.rs index 2bb1e8b150..3d1009ad90 100644 --- a/db4-storage/src/pages/node_store.rs +++ b/db4-storage/src/pages/node_store.rs @@ -182,7 +182,7 @@ impl, EXT: Clone> NodeStorageInner ) -> Result { let nodes_path = nodes_path.as_ref(); - let node_meta = Arc::new(Meta::new()); + let node_meta = Arc::new(Meta::new_for_nodes()); let mut pages = std::fs::read_dir(nodes_path)? .filter(|entry| { entry diff --git a/db4-storage/src/pages/session.rs b/db4-storage/src/pages/session.rs index 6b0189cad1..9152d7013b 100644 --- a/db4-storage/src/pages/session.rs +++ b/db4-storage/src/pages/session.rs @@ -4,7 +4,6 @@ use super::{ use crate::{ LocalPOS, api::{edges::EdgeSegmentOps, nodes::NodeSegmentOps}, - pages::NODE_ID_PROP_KEY, persist::strategy::PersistentStrategy, segments::{edge::MemEdgeSegment, node::MemNodeSegment}, }; @@ -44,14 +43,6 @@ impl< self.graph.nodes().resolve_pos(vid.into()).1 } - pub fn node_id_prop_id(&self) -> usize { - self.graph - .node_meta() - .metadata_mapper() - .get_id(NODE_ID_PROP_KEY) - .unwrap() - } - pub fn add_edge_into_layer( &mut self, t: T, diff --git a/db4-storage/src/properties/props_meta_writer.rs b/db4-storage/src/properties/props_meta_writer.rs index 9f45e0b603..0ed0014860 100644 --- a/db4-storage/src/properties/props_meta_writer.rs +++ b/db4-storage/src/properties/props_meta_writer.rs @@ -254,7 +254,7 @@ mod test { #[test] fn test_props_meta_writer() { - let meta = Meta::new(); + let meta = Meta::default(); let props = vec![ (ArcStr::from("prop1"), Prop::U32(0)), (ArcStr::from("prop2"), Prop::U32(1)), @@ -270,7 +270,7 @@ mod test { #[test] fn test_fail_typecheck() { - let meta = Meta::new(); + let meta = Meta::default(); let prop1 = Prop::U32(0); let prop2 = Prop::U64(1); diff --git a/raphtory-api/src/core/entities/properties/meta.rs b/raphtory-api/src/core/entities/properties/meta.rs index c78ace628a..d76ac2a324 100644 --- a/raphtory-api/src/core/entities/properties/meta.rs +++ b/raphtory-api/src/core/entities/properties/meta.rs @@ -21,6 +21,14 @@ use crate::core::{ }, }; +// Internal const props for node id and type +pub const NODE_ID_PROP_KEY: &str = "_raphtory_node_id"; +pub const NODE_ID_IDX: usize = 0; + +pub const NODE_TYPE_PROP_KEY: &str = "_raphtory_node_type"; +pub const NODE_TYPE_IDX: usize = 1; + +pub const STATIC_GRAPH_LAYER: &str = "_static_graph"; #[derive(Serialize, Deserialize, Debug)] pub struct Meta { temporal_prop_mapper: PropMapper, @@ -31,7 +39,12 @@ pub struct Meta { impl Default for Meta { fn default() -> Self { - Self::new() + Meta { + temporal_prop_mapper: Default::default(), + metadata_mapper: Default::default(), + layer_mapper: DictMapper::new_layer_mapper(), + node_type_mapper: Default::default(), + } } } @@ -75,8 +88,23 @@ impl Meta { self.metadata_mapper.row_size() } - pub fn new() -> Self { - let meta_layer = DictMapper::default(); + pub fn new_for_nodes() -> Self { + let meta_layer = DictMapper::new_layer_mapper(); + let meta_node_type = DictMapper::default(); + meta_node_type.get_or_create_id("_default"); + Self { + temporal_prop_mapper: PropMapper::default(), + metadata_mapper: PropMapper::new_with_private_fields( + [NODE_ID_PROP_KEY, NODE_TYPE_PROP_KEY], + [PropType::Empty, PropType::U64], + ), + layer_mapper: meta_layer, + node_type_mapper: meta_node_type, // type 0 is the default type for a node + } + } + + pub fn new_for_edges() -> Self { + let meta_layer = DictMapper::new_layer_mapper(); let meta_node_type = DictMapper::default(); meta_node_type.get_or_create_id("_default"); Self { diff --git a/raphtory-api/src/core/storage/dict_mapper.rs b/raphtory-api/src/core/storage/dict_mapper.rs index 31f1fd5087..c1bf571c23 100644 --- a/raphtory-api/src/core/storage/dict_mapper.rs +++ b/raphtory-api/src/core/storage/dict_mapper.rs @@ -1,4 +1,7 @@ -use crate::core::storage::{arc_str::ArcStr, ArcRwLockReadGuard}; +use crate::core::{ + entities::properties::meta::STATIC_GRAPH_LAYER, + storage::{arc_str::ArcStr, ArcRwLockReadGuard}, +}; use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard}; use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; @@ -169,6 +172,10 @@ impl WriteLockedDictMapper<'_> { } impl DictMapper { + pub fn new_layer_mapper() -> Self { + Self::new_with_private_fields([STATIC_GRAPH_LAYER]) + } + pub fn new_with_private_fields(fields: impl IntoIterator>) -> Self { let fields: Vec<_> = fields.into_iter().map(|s| s.into()).collect(); let num_private_fields = fields.len(); diff --git a/raphtory-storage/src/mutation/addition_ops_ext.rs b/raphtory-storage/src/mutation/addition_ops_ext.rs index 42af86c1c7..3d24ccb030 100644 --- a/raphtory-storage/src/mutation/addition_ops_ext.rs +++ b/raphtory-storage/src/mutation/addition_ops_ext.rs @@ -1,7 +1,7 @@ use db4_graph::{TemporalGraph, TransactionManager, WriteLockedGraph}; use raphtory_api::core::{ entities::properties::{ - meta::Meta, + meta::{Meta, NODE_ID_IDX}, prop::{Prop, PropType}, }, storage::dict_mapper::MaybeNew, @@ -13,7 +13,7 @@ use raphtory_core::{ storage::timeindex::TimeIndexEntry, }; use storage::{ - pages::{node_page::writer::node_info_as_props, session::WriteSession, NODE_ID_PROP_KEY}, + pages::{node_page::writer::node_info_as_props, session::WriteSession}, persist::strategy::PersistentStrategy, properties::props_meta_writer::PropsMetaWriter, resolver::GIDResolverOps, @@ -83,24 +83,22 @@ impl<'a, EXT: PersistentStrategy, ES = ES>> EdgeWriteLock for fn store_src_node_info(&mut self, vid: impl Into, node_id: Option) { if let Some(id) = node_id { let pos = self.static_session.resolve_node_pos(vid); - let prop_id = self.static_session.node_id_prop_id(); self.static_session .node_writers() .get_mut_src() - .update_c_props(pos, 0, [(prop_id, id.into())], 0); + .update_c_props(pos, 0, [(NODE_ID_IDX, id.into())], 0); }; } fn store_dst_node_info(&mut self, vid: impl Into, node_id: Option) { if let Some(id) = node_id { let pos = self.static_session.resolve_node_pos(vid); - let prop_id = self.static_session.node_id_prop_id(); self.static_session .node_writers() .get_mut_dst() - .update_c_props(pos, 0, [(prop_id, id.into())], 0); + .update_c_props(pos, 0, [(NODE_ID_IDX, id.into())], 0); }; } } @@ -220,10 +218,6 @@ impl InternalAdditionOps for TemporalGraph { match id { NodeRef::External(id) => { let id = self.logical_to_physical.get_or_init(id, || { - // When initializing a new node, reserve node_id as a const prop. - // Done here since the id type is not known until node creation. - reserve_node_id_as_prop(self.node_meta(), id); - self.node_count .fetch_add(1, std::sync::atomic::Ordering::Relaxed) .into() @@ -334,18 +328,3 @@ impl InternalAdditionOps for TemporalGraph { &self.wal } } - -fn reserve_node_id_as_prop(node_meta: &Meta, id: GidRef) -> usize { - match id { - GidRef::U64(_) => node_meta - .metadata_mapper() - .get_or_create_and_validate(NODE_ID_PROP_KEY, PropType::U64) - .unwrap() - .inner(), - GidRef::Str(_) => node_meta - .metadata_mapper() - .get_or_create_and_validate(NODE_ID_PROP_KEY, PropType::Str) - .unwrap() - .inner(), - } -} diff --git a/raphtory/src/db/api/view/graph.rs b/raphtory/src/db/api/view/graph.rs index 6a2e9361e2..52e6d1d80a 100644 --- a/raphtory/src/db/api/view/graph.rs +++ b/raphtory/src/db/api/view/graph.rs @@ -222,8 +222,8 @@ impl<'graph, G: GraphView + 'graph> GraphViewOps<'graph> for G { let storage = self.core_graph().lock(); // preserve all property mappings - let mut node_meta = Meta::new(); - let mut edge_meta = Meta::new(); + let mut node_meta = Meta::default(); + let mut edge_meta = Meta::default(); node_meta.set_metadata_mapper(self.node_meta().metadata_mapper().deep_clone()); node_meta.set_temporal_prop_meta(self.node_meta().temporal_prop_mapper().deep_clone()); From 436432403ccc2dd4b4e150670072a4dc3e0eccaa Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Fri, 15 Aug 2025 14:46:07 +0200 Subject: [PATCH 114/321] use the constants for node type and node id properties --- db4-storage/src/pages/node_page/writer.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/db4-storage/src/pages/node_page/writer.rs b/db4-storage/src/pages/node_page/writer.rs index 0a2e75b2c7..5bd588958c 100644 --- a/db4-storage/src/pages/node_page/writer.rs +++ b/db4-storage/src/pages/node_page/writer.rs @@ -2,7 +2,13 @@ use crate::{ LocalPOS, api::nodes::NodeSegmentOps, pages::layer_counter::GraphStats, segments::node::MemNodeSegment, }; -use raphtory_api::core::entities::{EID, VID, properties::prop::Prop}; +use raphtory_api::core::entities::{ + EID, VID, + properties::{ + meta::{NODE_ID_IDX, NODE_TYPE_IDX}, + prop::Prop, + }, +}; use raphtory_core::{ entities::{ELID, GidRef}, storage::timeindex::AsTime, @@ -200,9 +206,11 @@ pub fn node_info_as_props( gid: Option, node_type: Option, ) -> impl Iterator { - gid.into_iter() - .map(|g| (1, g.into())) - .chain(node_type.into_iter().map(|nt| (0, Prop::U64(nt as u64)))) + gid.into_iter().map(|g| (NODE_ID_IDX, g.into())).chain( + node_type + .into_iter() + .map(|nt| (NODE_TYPE_IDX, Prop::U64(nt as u64))), + ) } impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> Drop From 145b9f7060f774446b47e4bf86529c381568f20f Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Fri, 15 Aug 2025 15:04:12 +0200 Subject: [PATCH 115/321] not all EIDs are filled in the new storage --- raphtory/src/search/edge_index.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/raphtory/src/search/edge_index.rs b/raphtory/src/search/edge_index.rs index f7633fed40..938f652bed 100644 --- a/raphtory/src/search/edge_index.rs +++ b/raphtory/src/search/edge_index.rs @@ -10,7 +10,7 @@ use crate::{ }, }; use ahash::HashSet; -use raphtory_api::core::storage::dict_mapper::MaybeNew; +use raphtory_api::core::{entities::LayerIds, storage::dict_mapper::MaybeNew}; use raphtory_storage::{ core_ops::CoreGraphOps, graph::{edges::edge_storage_ops::EdgeStorageOps, graph::GraphStorage}, @@ -208,10 +208,10 @@ impl EdgeIndex { pub(crate) fn index_edges_fields(&self, graph: &GraphStorage) -> Result<(), GraphError> { let mut writer = self.entity_index.index.writer(100_000_000)?; - (0..graph.count_edges()) - .into_par_iter() - .try_for_each(|e_id| { - let edge = graph.core_edge(EID(e_id)); + graph + .edges() + .par_iter(&LayerIds::All) + .try_for_each(|edge| { let e_view = EdgeView::new(graph, edge.out_ref()); self.index_edge(e_view, &writer)?; Ok::<(), GraphError>(()) From 41ac86c2177629aa3aa3a55cb7c9484290bc9e48 Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Fri, 15 Aug 2025 15:43:12 +0200 Subject: [PATCH 116/321] fix loading of empty df --- raphtory/src/io/arrow/dataframe.rs | 4 ++++ raphtory/src/io/arrow/df_loaders.rs | 3 +++ raphtory/src/serialise/parquet/mod.rs | 6 ++++++ 3 files changed, 13 insertions(+) diff --git a/raphtory/src/io/arrow/dataframe.rs b/raphtory/src/io/arrow/dataframe.rs index 9b7ac7e424..f7fb71499f 100644 --- a/raphtory/src/io/arrow/dataframe.rs +++ b/raphtory/src/io/arrow/dataframe.rs @@ -48,6 +48,10 @@ where .position(|n| n == name) .ok_or_else(|| GraphError::ColumnDoesNotExist(name.to_string())) } + + pub fn is_empty(&self) -> bool { + self.num_rows == 0 + } } pub struct TimeCol(PrimitiveArray); diff --git a/raphtory/src/io/arrow/df_loaders.rs b/raphtory/src/io/arrow/df_loaders.rs index 87ef96e5b7..caf27d9168 100644 --- a/raphtory/src/io/arrow/df_loaders.rs +++ b/raphtory/src/io/arrow/df_loaders.rs @@ -209,6 +209,9 @@ pub(crate) fn load_edges_from_df< layer_col: Option<&str>, graph: &G, ) -> Result<(), GraphError> { + if df_view.is_empty() { + return Ok(()); + } let properties_indices = properties .iter() .map(|name| df_view.get_index(name)) diff --git a/raphtory/src/serialise/parquet/mod.rs b/raphtory/src/serialise/parquet/mod.rs index 4701027586..29f32e8bc3 100644 --- a/raphtory/src/serialise/parquet/mod.rs +++ b/raphtory/src/serialise/parquet/mod.rs @@ -991,6 +991,12 @@ mod test { }) } + #[test] + fn test_empty_graph() { + let graph = Graph::new(); + check_parquet_encoding(graph); + } + #[test] fn test_deletion() { let graph = PersistentGraph::new(); From 9646560d376fe6d8ea3fae953df8cf0a885d5724 Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Mon, 18 Aug 2025 13:46:05 +0200 Subject: [PATCH 117/321] don't set node type if new value is default --- db4-storage/src/pages/node_page/writer.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/db4-storage/src/pages/node_page/writer.rs b/db4-storage/src/pages/node_page/writer.rs index 5bd588958c..e6c423bf84 100644 --- a/db4-storage/src/pages/node_page/writer.rs +++ b/db4-storage/src/pages/node_page/writer.rs @@ -185,12 +185,8 @@ impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> NodeWri node_type: usize, lsn: u64, ) { - self.update_c_props( - pos, - layer_id, - node_info_as_props(Some(gid), Some(node_type)), - lsn, - ); + let node_type = (node_type != 0).then_some(node_type); + self.update_c_props(pos, layer_id, node_info_as_props(Some(gid), node_type), lsn); } pub fn store_node_id(&mut self, pos: LocalPOS, layer_id: usize, gid: GidRef<'_>, lsn: u64) { From 4c0e8522d7fe1c6df280c8797d4566fff90fcca9 Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Tue, 19 Aug 2025 12:26:35 +0200 Subject: [PATCH 118/321] attempt to make node type resolution atomic --- db4-storage/src/pages/node_page/writer.rs | 4 + db4-storage/src/segments/node.rs | 10 +++ raphtory-storage/src/mutation/addition_ops.rs | 77 ++++++++++--------- .../src/mutation/addition_ops_ext.rs | 77 +++++++++++++++---- raphtory/src/db/api/mutation/addition_ops.rs | 61 ++++----------- raphtory/src/db/api/mutation/import_ops.rs | 20 ++--- raphtory/src/db/api/storage/storage.rs | 49 +++++++++--- raphtory/src/db/graph/node.rs | 4 +- raphtory/src/io/arrow/df_loaders.rs | 4 +- raphtory/src/search/graph_index.rs | 16 +++- raphtory/src/search/node_index.rs | 47 +++++------ 11 files changed, 216 insertions(+), 153 deletions(-) diff --git a/db4-storage/src/pages/node_page/writer.rs b/db4-storage/src/pages/node_page/writer.rs index e6c423bf84..0c9ac3ec83 100644 --- a/db4-storage/src/pages/node_page/writer.rs +++ b/db4-storage/src/pages/node_page/writer.rs @@ -159,6 +159,10 @@ impl<'a, MP: DerefMut + 'a, NS: NodeSegmentOps> NodeWri } } + pub fn get_metadata(&self, pos: LocalPOS, layer_id: usize, prop_id: usize) -> Option { + self.mut_segment.get_metadata(pos, layer_id, prop_id) + } + pub fn update_timestamp(&mut self, t: T, pos: LocalPOS, e_id: ELID, lsn: u64) { let layer_id = e_id.layer(); self.l_counter.update_time(t.t()); diff --git a/db4-storage/src/segments/node.rs b/db4-storage/src/segments/node.rs index 11b1e20899..db76840464 100644 --- a/db4-storage/src/segments/node.rs +++ b/db4-storage/src/segments/node.rs @@ -339,6 +339,16 @@ impl MemNodeSegment { (is_new, added_size) } + pub fn get_metadata( + &self, + node_pos: LocalPOS, + layer_id: usize, + prop_id: usize, + ) -> Option { + let segment_container = &self.layers[layer_id]; + segment_container.c_prop(node_pos, prop_id) + } + pub fn latest(&self) -> Option { Iterator::max(self.layers.iter().filter_map(|seg| seg.latest())) } diff --git a/raphtory-storage/src/mutation/addition_ops.rs b/raphtory-storage/src/mutation/addition_ops.rs index 534c274569..e8bcda0460 100644 --- a/raphtory-storage/src/mutation/addition_ops.rs +++ b/raphtory-storage/src/mutation/addition_ops.rs @@ -38,29 +38,21 @@ pub trait InternalAdditionOps { fn resolve_layer(&self, layer: Option<&str>) -> Result, Self::Error>; /// map external node id to internal id, allocating a new empty node if needed fn resolve_node(&self, id: NodeRef) -> Result, Self::Error>; + /// resolve a node and corresponding type, outer MaybeNew tracks whether the type assignment is new for the node even if both node and type already existed. - fn resolve_node_and_type( + /// updates the storage atomically to set the node type + fn resolve_and_update_node_and_type( &self, id: NodeRef, - node_type: &str, + node_type: Option<&str>, ) -> Result, MaybeNew)>, Self::Error>; - fn resolve_node_and_type_fast( + /// resolve node and type without modifying the storage (use in bulk loaders only) + fn resolve_node_and_type( &self, id: NodeRef, node_type: Option<&str>, - ) -> Result<(VID, usize), Self::Error> { - match node_type { - Some(node_type) => { - let (vid, node_type_id) = self.resolve_node_and_type(id, node_type)?.inner(); - Ok((vid.inner(), node_type_id.inner())) - } - None => { - let vid = self.resolve_node(id)?.inner(); - Ok((vid, 0)) - } - } - } + ) -> Result<(VID, usize), Self::Error>; /// validate the GidRef is the correct type fn validate_gids<'a>( @@ -81,10 +73,8 @@ pub trait InternalAdditionOps { fn internal_add_node( &self, t: TimeIndexEntry, - v: impl Into, - gid: Option, - node_type: Option, - props: impl IntoIterator, + v: VID, + props: Vec<(usize, Prop)>, ) -> Result<(), Self::Error>; fn validate_props>( @@ -222,12 +212,14 @@ impl InternalAdditionOps for GraphStorage { self.mutable()?.resolve_node(id) } - fn resolve_node_and_type( + fn resolve_and_update_node_and_type( &self, id: NodeRef, - node_type: &str, + node_type: Option<&str>, ) -> Result, MaybeNew)>, Self::Error> { - Ok(self.mutable()?.resolve_node_and_type(id, node_type)?) + Ok(self + .mutable()? + .resolve_and_update_node_and_type(id, node_type)?) } fn write_session(&self) -> Result, Self::Error> { @@ -247,13 +239,10 @@ impl InternalAdditionOps for GraphStorage { fn internal_add_node( &self, t: TimeIndexEntry, - v: impl Into, - gid: Option, - node_type: Option, - props: impl IntoIterator, + v: VID, + props: Vec<(usize, Prop)>, ) -> Result<(), Self::Error> { - self.mutable()? - .internal_add_node(t, v, gid, node_type, props) + self.mutable()?.internal_add_node(t, v, props) } fn validate_props>( @@ -292,6 +281,16 @@ impl InternalAdditionOps for GraphStorage { fn wal(&self) -> &WalImpl { self.mutable().unwrap().wal.as_ref() } + + fn resolve_node_and_type( + &self, + id: NodeRef, + node_type: Option<&str>, + ) -> Result<(VID, usize), Self::Error> { + self.mutable()? + .resolve_node_and_type(id, node_type) + .map_err(MutationError::from) + } } pub trait InheritAdditionOps: Base {} @@ -329,12 +328,12 @@ where } #[inline] - fn resolve_node_and_type( + fn resolve_and_update_node_and_type( &self, id: NodeRef, - node_type: &str, + node_type: Option<&str>, ) -> Result, MaybeNew)>, Self::Error> { - self.base().resolve_node_and_type(id, node_type) + self.base().resolve_and_update_node_and_type(id, node_type) } #[inline] @@ -357,12 +356,10 @@ where fn internal_add_node( &self, t: TimeIndexEntry, - v: impl Into, - gid: Option, - node_type: Option, - props: impl IntoIterator, + v: VID, + props: Vec<(usize, Prop)>, ) -> Result<(), Self::Error> { - self.base().internal_add_node(t, v, gid, node_type, props) + self.base().internal_add_node(t, v, props) } #[inline] @@ -403,4 +400,12 @@ where fn wal(&self) -> &WalImpl { self.base().wal() } + + fn resolve_node_and_type( + &self, + id: NodeRef, + node_type: Option<&str>, + ) -> Result<(VID, usize), Self::Error> { + self.base().resolve_node_and_type(id, node_type) + } } diff --git a/raphtory-storage/src/mutation/addition_ops_ext.rs b/raphtory-storage/src/mutation/addition_ops_ext.rs index 3d24ccb030..05b22709d0 100644 --- a/raphtory-storage/src/mutation/addition_ops_ext.rs +++ b/raphtory-storage/src/mutation/addition_ops_ext.rs @@ -1,14 +1,16 @@ use db4_graph::{TemporalGraph, TransactionManager, WriteLockedGraph}; use raphtory_api::core::{ entities::properties::{ - meta::{Meta, NODE_ID_IDX}, - prop::{Prop, PropType}, + meta::{Meta, NODE_ID_IDX, NODE_TYPE_IDX}, + prop::{Prop, PropType, PropUnwrap}, }, storage::dict_mapper::MaybeNew, }; use raphtory_core::{ entities::{ - graph::tgraph::TooManyLayers, nodes::node_ref::NodeRef, GidRef, EID, ELID, MAX_LAYER, VID, + graph::tgraph::TooManyLayers, + nodes::node_ref::{AsNodeRef, NodeRef}, + GidRef, EID, ELID, MAX_LAYER, VID, }, storage::timeindex::TimeIndexEntry, }; @@ -229,16 +231,68 @@ impl InternalAdditionOps for TemporalGraph { } } - fn resolve_node_and_type( + fn resolve_and_update_node_and_type( &self, id: NodeRef, - node_type: &str, + node_type: Option<&str>, ) -> Result, MaybeNew)>, Self::Error> { let vid = self.resolve_node(id)?; - let node_type_id = self.node_meta().get_or_create_node_type_id(node_type); + let (segment_id, local_pos) = self.storage().nodes().resolve_pos(vid.inner()); + let mut writer = self.storage().nodes().writer(segment_id); + let node_type_id = match node_type { + None => { + writer.update_c_props( + local_pos, + 0, + node_info_as_props(id.as_gid_ref().left(), None), + 0, + ); + MaybeNew::Existing(0) + } + Some(node_type) => { + let old_type = writer.get_metadata(local_pos, 0, NODE_TYPE_IDX).into_u64(); + match old_type { + None => { + let node_type_id = self.node_meta().get_or_create_node_type_id(node_type); + writer.update_c_props( + local_pos, + 0, + node_info_as_props( + id.as_gid_ref().left(), + Some(node_type_id.inner()).filter(|&id| id != 0), + ), + 0, + ); + node_type_id + } + Some(old_type) => MaybeNew::Existing( + self.node_meta() + .get_node_type_id(node_type) + .filter(|&new_id| new_id == old_type as usize) + .ok_or(MutationError::NodeTypeError)?, + ), + } + } + }; Ok(vid.map(|_| (vid, node_type_id))) } + fn resolve_node_and_type( + &self, + id: NodeRef, + node_type: Option<&str>, + ) -> Result<(VID, usize), Self::Error> { + let vid = self.resolve_node(id)?.inner(); + let node_type_id = match node_type { + Some(node_type) => self + .node_meta() + .get_or_create_node_type_id(node_type) + .inner(), + None => 0, + }; + Ok((vid, node_type_id)) + } + fn validate_gids<'a>( &self, gids: impl IntoIterator>, @@ -266,19 +320,12 @@ impl InternalAdditionOps for TemporalGraph { fn internal_add_node( &self, t: TimeIndexEntry, - v: impl Into, - gid: Option, - node_type: Option, - props: impl IntoIterator, + v: VID, + props: Vec<(usize, Prop)>, ) -> Result<(), Self::Error> { - let v = v.into(); let (segment, node_pos) = self.storage().nodes().resolve_pos(v); let mut node_writer = self.storage().node_writer(segment); node_writer.add_props(t, node_pos, 0, props, 0); - if gid.is_some() || node_type.is_some() { - let node_info = node_info_as_props(gid, node_type); - node_writer.update_c_props(node_pos, 0, node_info, 0); - } Ok(()) } diff --git a/raphtory/src/db/api/mutation/addition_ops.rs b/raphtory/src/db/api/mutation/addition_ops.rs index fe92a660dc..ed319d3d97 100644 --- a/raphtory/src/db/api/mutation/addition_ops.rs +++ b/raphtory/src/db/api/mutation/addition_ops.rs @@ -173,32 +173,15 @@ impl> + StaticGraphViewOps> Addit ) .map_err(into_graph_err)?; let ti = time_from_input_session(&session, t)?; - let (node_id, node_type) = match node_type { - None => self - .resolve_node(v.as_node_ref()) - .map_err(into_graph_err)? - .map(|node_id| (node_id, None)) - .inner(), - Some(node_type) => { - let node_id = self - .resolve_node_and_type(v.as_node_ref(), node_type) - .map_err(into_graph_err)?; - node_id - .map(|(node_id, node_type)| (node_id.inner(), Some(node_type.inner()))) - .inner() - } - }; - - self.internal_add_node( - ti, - node_id, - v.as_node_ref().as_gid_ref().left(), - node_type, - props, - ) - .map_err(into_graph_err)?; + let (node_id, _) = self + .resolve_and_update_node_and_type(v.as_node_ref(), node_type) + .map_err(into_graph_err)? + .inner(); - Ok(NodeView::new_internal(self.clone(), node_id)) + self.internal_add_node(ti, node_id.inner(), props) + .map_err(into_graph_err)?; + + Ok(NodeView::new_internal(self.clone(), node_id.inner())) } fn create_node< @@ -231,35 +214,21 @@ impl> + StaticGraphViewOps> Addit ) .map_err(into_graph_err)?; let ti = time_from_input_session(&session, t)?; - let node_id = match node_type { - None => self - .resolve_node(v.as_node_ref()) - .map_err(into_graph_err)? - .map(|node_id| (node_id, None)), - Some(node_type) => { - let node_id = self - .resolve_node_and_type(v.as_node_ref(), node_type) - .map_err(into_graph_err)?; - node_id.map(|(node_id, node_type)| (node_id.inner(), Some(node_type.inner()))) - } - }; + let (node_id, _) = self + .resolve_and_update_node_and_type(v.as_node_ref(), node_type) + .map_err(into_graph_err)? + .inner(); let is_new = node_id.is_new(); - let (node_id, node_type) = node_id.inner(); + let node_id = node_id.inner(); if !is_new { let node_id = self.node(node_id).unwrap().id(); return Err(GraphError::NodeExistsError(node_id)); } - self.internal_add_node( - ti, - node_id, - v.as_node_ref().as_gid_ref().left(), - node_type, - props, - ) - .map_err(into_graph_err)?; + self.internal_add_node(ti, node_id, props) + .map_err(into_graph_err)?; Ok(NodeView::new_internal(self.clone(), node_id)) } diff --git a/raphtory/src/db/api/mutation/import_ops.rs b/raphtory/src/db/api/mutation/import_ops.rs index 9c4fcc504b..31caf59632 100644 --- a/raphtory/src/db/api/mutation/import_ops.rs +++ b/raphtory/src/db/api/mutation/import_ops.rs @@ -333,19 +333,11 @@ fn import_node_internal< } } - let (node_internal, node_type) = match node.node_type().as_str() { - None => ( - graph.resolve_node(id).map_err(into_graph_err)?.inner(), - None, - ), - Some(node_type) => { - let (node_internal, node_type) = graph - .resolve_node_and_type(id, node_type) - .map_err(into_graph_err)? - .inner(); - (node_internal.inner(), Some(node_type.inner())) - } - }; + let (node_internal, _) = graph + .resolve_and_update_node_and_type(id, node.node_type().as_str()) + .map_err(into_graph_err)? + .inner(); + let node_internal = node_internal.inner(); let session = graph.write_session().map_err(|err| err.into())?; let keys = node.graph.node_meta().temporal_prop_mapper().all_keys(); @@ -364,7 +356,7 @@ fn import_node_internal< .map_err(into_graph_err)?; graph - .internal_add_node(t, node_internal, gid_ref, node_type, props) + .internal_add_node(t, node_internal, props) .map_err(into_graph_err)?; } diff --git a/raphtory/src/db/api/storage/storage.rs b/raphtory/src/db/api/storage/storage.rs index 6f7ebbda58..9c4c3c5e96 100644 --- a/raphtory/src/db/api/storage/storage.rs +++ b/raphtory/src/db/api/storage/storage.rs @@ -7,6 +7,7 @@ use crate::{ errors::GraphError, }; use db4_graph::{TemporalGraph, TransactionManager, WriteLockedGraph}; +use either::Either; use raphtory_api::core::{ entities::{ properties::{ @@ -40,6 +41,8 @@ use storage::{Extension, WalImpl}; #[cfg(feature = "proto")] use crate::serialise::GraphFolder; +use raphtory_core::entities::nodes::node_ref::AsNodeRef; +use raphtory_storage::{core_ops::CoreGraphOps, graph::nodes::node_storage_ops::NodeStorageOps}; #[cfg(feature = "search")] use { crate::{ @@ -380,9 +383,8 @@ impl<'a> SessionAdditionOps for StorageWriteSession<'a> { self.session.internal_add_node(t, v, props)?; #[cfg(feature = "search")] - self.storage.if_index_mut(|index| { - index.add_node_update(&self.storage.graph, t, MaybeNew::New(v), props) - })?; + self.storage + .if_index_mut(|index| index.add_node_update(t, v, props))?; Ok(()) } @@ -455,12 +457,23 @@ impl InternalAdditionOps for Storage { } } - fn resolve_node_and_type( + fn resolve_and_update_node_and_type( &self, id: NodeRef, - node_type: &str, + node_type: Option<&str>, ) -> Result, MaybeNew)>, Self::Error> { - let node_and_type = self.graph.resolve_node_and_type(id, node_type)?; + let node_and_type = self.graph.resolve_and_update_node_and_type(id, node_type)?; + + #[cfg(feature = "search")] + node_and_type + .if_new(|(node_id, _)| { + let name = match id.as_gid_ref() { + Either::Left(gid) => gid.to_string(), + Either::Right(vid) => self.core_node(vid).name().to_string(), + }; + self.if_index_mut(|index| index.add_new_node(node_id.inner(), name, node_type)) + }) + .transpose()?; Ok(node_and_type) } @@ -490,12 +503,18 @@ impl InternalAdditionOps for Storage { fn internal_add_node( &self, t: TimeIndexEntry, - v: impl Into, - gid: Option, - node_type: Option, - props: impl IntoIterator, + v: VID, + props: Vec<(usize, Prop)>, ) -> Result<(), Self::Error> { - Ok(self.graph.internal_add_node(t, v, gid, node_type, props)?) + #[cfg(feature = "search")] + let index_res = self.if_index_mut(|index| index.add_node_update(t, v, &props)); + // don't fail early on indexing, actually update the graph even if indexing failed + self.graph.internal_add_node(t, v, props)?; + + #[cfg(feature = "search")] + index_res?; + + Ok(()) } fn validate_props>( @@ -532,6 +551,14 @@ impl InternalAdditionOps for Storage { fn wal(&self) -> &WalImpl { self.graph.mutable().unwrap().wal.as_ref() } + + fn resolve_node_and_type( + &self, + id: NodeRef, + node_type: Option<&str>, + ) -> Result<(VID, usize), Self::Error> { + Ok(self.graph.resolve_node_and_type(id, node_type)?) + } } impl InternalPropertyAdditionOps for Storage { diff --git a/raphtory/src/db/graph/node.rs b/raphtory/src/db/graph/node.rs index 7484d867ff..be9dcbef9e 100644 --- a/raphtory/src/db/graph/node.rs +++ b/raphtory/src/db/graph/node.rs @@ -432,7 +432,7 @@ impl NodeView<'static pub fn set_node_type(&self, new_type: &str) -> Result<(), GraphError> { self.graph - .resolve_node_and_type(NodeRef::Internal(self.node), new_type) + .resolve_and_update_node_and_type(NodeRef::Internal(self.node), Some(new_type)) .map_err(into_graph_err)?; Ok(()) } @@ -474,7 +474,7 @@ impl NodeView<'static .map_err(into_graph_err)?; let vid = self.node; self.graph - .internal_add_node(t, vid, None, None, props) + .internal_add_node(t, vid, props) .map_err(into_graph_err) } } diff --git a/raphtory/src/io/arrow/df_loaders.rs b/raphtory/src/io/arrow/df_loaders.rs index caf27d9168..c7cd3ba1e6 100644 --- a/raphtory/src/io/arrow/df_loaders.rs +++ b/raphtory/src/io/arrow/df_loaders.rs @@ -139,7 +139,7 @@ pub(crate) fn load_nodes_from_df< let (vid, res_node_type) = write_locked_graph .graph() - .resolve_node_and_type_fast(gid.as_node_ref(), node_type) + .resolve_node_and_type(gid.as_node_ref(), node_type) .map_err(|_| LoadError::FatalError)?; *resolved = vid; *node_type_resolved = res_node_type; @@ -674,7 +674,7 @@ pub(crate) fn load_node_props_from_df< let gid = gid.ok_or(LoadError::FatalError)?; let (vid, res_node_type) = write_locked_graph .graph() - .resolve_node_and_type_fast(gid.as_node_ref(), node_type) + .resolve_node_and_type(gid.as_node_ref(), node_type) .map_err(|_| LoadError::FatalError)?; *resolved = vid; *node_type_resolved = res_node_type; diff --git a/raphtory/src/search/graph_index.rs b/raphtory/src/search/graph_index.rs index ea85bfe05d..46665aae43 100644 --- a/raphtory/src/search/graph_index.rs +++ b/raphtory/src/search/graph_index.rs @@ -10,7 +10,7 @@ use crate::{ serialise::GraphFolder, }; use parking_lot::RwLock; -use raphtory_api::core::storage::dict_mapper::MaybeNew; +use raphtory_api::core::storage::{arc_str::ArcStr, dict_mapper::MaybeNew}; use raphtory_storage::graph::graph::GraphStorage; use std::{ ffi::OsStr, @@ -77,14 +77,22 @@ impl MutableGraphIndex { Ok(()) } + pub(crate) fn add_new_node( + &self, + node_id: VID, + name: String, + node_type: Option<&str>, + ) -> Result<(), GraphError> { + self.index.node_index.add_new_node(node_id, name, node_type) + } + pub(crate) fn add_node_update( &self, - graph: &GraphStorage, t: TimeIndexEntry, - v: MaybeNew, + v: VID, props: &[(usize, Prop)], ) -> Result<(), GraphError> { - self.index.node_index.add_node_update(graph, t, v, props)?; + self.index.node_index.add_node_update(t, v, props)?; Ok(()) } diff --git a/raphtory/src/search/node_index.rs b/raphtory/src/search/node_index.rs index f595d69e59..346285de54 100644 --- a/raphtory/src/search/node_index.rs +++ b/raphtory/src/search/node_index.rs @@ -13,7 +13,10 @@ use crate::{ }, }; use ahash::HashSet; -use raphtory_api::core::storage::{arc_str::ArcStr, dict_mapper::MaybeNew}; +use raphtory_api::core::storage::{ + arc_str::{ArcStr, OptionAsStr}, + dict_mapper::MaybeNew, +}; use raphtory_storage::graph::graph::GraphStorage; use rayon::{iter::IntoParallelIterator, prelude::ParallelIterator}; use std::{ @@ -193,7 +196,7 @@ impl NodeIndex { &self, node_id: u64, node_name: String, - node_type: Option, + node_type: Option<&str>, ) -> TantivyDocument { let mut document = TantivyDocument::new(); document.add_u64(self.node_id_field, node_id); @@ -215,7 +218,7 @@ impl NodeIndex { let node_name = node.name(); let node_type = node.node_type(); - let node_doc = self.create_document(node_id, node_name.clone(), node_type.clone()); + let node_doc = self.create_document(node_id, node_name.clone(), node_type.as_str()); writer.add_document(node_doc)?; Ok(()) @@ -250,31 +253,29 @@ impl NodeIndex { Ok(()) } + pub(crate) fn add_new_node( + &self, + node_id: VID, + name: String, + node_type: Option<&str>, + ) -> Result<(), GraphError> { + let vid_u64 = node_id.as_u64(); // Check if the node document is already in the index, + // if it does skip adding a new doc for same node + + let mut writer = self.entity_index.index.writer(100_000_000)?; + let node_doc = self.create_document(vid_u64, name, node_type); + writer.add_document(node_doc)?; + writer.commit()?; + Ok(()) + } + pub(crate) fn add_node_update( &self, - graph: &GraphStorage, t: TimeIndexEntry, - node_id: MaybeNew, + node_id: VID, props: &[(usize, Prop)], ) -> Result<(), GraphError> { - let node = graph - .node(VID(node_id.inner().as_u64() as usize)) - .expect("Node for internal id should exist.") - .at(t.t()); - let vid_u64 = node_id.inner().as_u64(); - - // Check if the node document is already in the index, - // if it does skip adding a new doc for same node - node_id - .if_new(|_| { - let mut writer = self.entity_index.index.writer(100_000_000)?; - let node_doc = self.create_document(vid_u64, node.name(), node.node_type()); - writer.add_document(node_doc)?; - writer.commit()?; - Ok::<(), GraphError>(()) - }) - .transpose()?; - + let vid_u64 = node_id.as_u64(); let indexes = self.entity_index.temporal_property_indexes.read(); for (prop_id, prop_value) in indexed_props(props, &indexes) { if let Some(index) = &indexes[prop_id] { From a794e0a02776db1a4036f37679097e1b2ef9d01a Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Tue, 19 Aug 2025 13:17:43 +0200 Subject: [PATCH 119/321] make sure secondary index is consistent between properties and exploded for persisted update --- .../view/internal/time_semantics/persistent_semantics.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/raphtory/src/db/api/view/internal/time_semantics/persistent_semantics.rs b/raphtory/src/db/api/view/internal/time_semantics/persistent_semantics.rs index 3607b44c9e..50857a7168 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/persistent_semantics.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/persistent_semantics.rs @@ -152,11 +152,12 @@ fn persisted_prop_value_at<'a, 'b>( t: i64, props: impl TPropOps<'a>, deletions: impl TimeIndexOps<'b, IndexType = TimeIndexEntry>, -) -> Option { +) -> Option<(TimeIndexEntry, Prop)> { if props.active_t(t..t.saturating_add(1)) || deletions.active_t(t..t.saturating_add(1)) { None } else { - last_prop_value_before(TimeIndexEntry::start(t), props, deletions).map(|(_, v)| v) + last_prop_value_before(TimeIndexEntry::start(t), props, deletions) + .map(|(update_t, v)| (TimeIndexEntry(t, update_t.i()), v)) } } @@ -1154,7 +1155,7 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { .filtered_deletions(layer, &view) .merge(e.filtered_additions(layer, &view).invert()); let first_prop = persisted_prop_value_at(w.start, props.clone(), &deletions) - .map(|v| (TimeIndexEntry::start(w.start), layer, v)); + .map(|(t, v)| (t, layer, v)); first_prop.into_iter().chain( props .iter_window(interior_window(w.clone(), &deletions)) @@ -1176,7 +1177,7 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { .map(|(layer, props)| { let deletions = merged_deletions(e, &view, layer); let first_prop = persisted_prop_value_at(w.start, props.clone(), &deletions) - .map(|v| (TimeIndexEntry::start(w.start), layer, v)); + .map(|(t, v)| (t, layer, v)); props .iter_inner_rev(Some(interior_window(w.clone(), &deletions))) .map(move |(t, v)| (t, layer, v)) From e45a412b6c4dac49cab36b2ed67fb7763cd23b95 Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Tue, 19 Aug 2025 14:13:30 +0200 Subject: [PATCH 120/321] edge deletions need to be marked correctly in node additions --- raphtory-storage/src/mutation/addition_ops_ext.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/raphtory-storage/src/mutation/addition_ops_ext.rs b/raphtory-storage/src/mutation/addition_ops_ext.rs index 05b22709d0..8991f91101 100644 --- a/raphtory-storage/src/mutation/addition_ops_ext.rs +++ b/raphtory-storage/src/mutation/addition_ops_ext.rs @@ -74,7 +74,7 @@ impl<'a, EXT: PersistentStrategy, ES = ES>> EdgeWriteLock for let eid = self .static_session .add_static_edge(src, dst, lsn) - .map(|eid| eid.with_layer(layer)); + .map(|eid| eid.with_layer_deletion(layer)); self.static_session .delete_edge_from_layer(t, src, dst, eid, lsn); From 799846f6723ecfa4060f6d260079878446b16b26 Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Tue, 19 Aug 2025 14:28:54 +0200 Subject: [PATCH 121/321] initialise metadata in materialize correctly --- raphtory/src/db/api/view/graph.rs | 4 ++-- raphtory/src/db/graph/views/deletion_graph.rs | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/raphtory/src/db/api/view/graph.rs b/raphtory/src/db/api/view/graph.rs index 52e6d1d80a..33546f94d4 100644 --- a/raphtory/src/db/api/view/graph.rs +++ b/raphtory/src/db/api/view/graph.rs @@ -222,8 +222,8 @@ impl<'graph, G: GraphView + 'graph> GraphViewOps<'graph> for G { let storage = self.core_graph().lock(); // preserve all property mappings - let mut node_meta = Meta::default(); - let mut edge_meta = Meta::default(); + let mut node_meta = Meta::new_for_nodes(); + let mut edge_meta = Meta::new_for_edges(); node_meta.set_metadata_mapper(self.node_meta().metadata_mapper().deep_clone()); node_meta.set_temporal_prop_meta(self.node_meta().temporal_prop_mapper().deep_clone()); diff --git a/raphtory/src/db/graph/views/deletion_graph.rs b/raphtory/src/db/graph/views/deletion_graph.rs index 57d0bc6631..3deb62e51d 100644 --- a/raphtory/src/db/graph/views/deletion_graph.rs +++ b/raphtory/src/db/graph/views/deletion_graph.rs @@ -1006,6 +1006,14 @@ mod test_deletions { check_valid(&e_layer_2.at(10)); } + #[test] + fn test_materialize_node_type() { + let g = PersistentGraph::new(); + g.delete_edge(0, 0, 0, None).unwrap(); + g.node(0).unwrap().set_node_type("test").unwrap(); + assert_graph_equal(&g, &g.materialize().unwrap()); + } + #[test] fn test_edge_is_valid() { let g = PersistentGraph::new(); @@ -2046,4 +2054,6 @@ mod test_edge_history_filter_persistent_graph { // let bool = g.is_edge_prop_update_latest_window(prop_id, edge_id, TimeIndexEntry::end(3), w.clone()); // assert!(!bool); } + + } From fb2d578bdcdd94411713b87320d692ce0203ef89 Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Tue, 19 Aug 2025 15:08:37 +0200 Subject: [PATCH 122/321] tidy up repeated resolves --- raphtory/src/db/api/view/graph.rs | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/raphtory/src/db/api/view/graph.rs b/raphtory/src/db/api/view/graph.rs index 33546f94d4..e736bb2cc6 100644 --- a/raphtory/src/db/api/view/graph.rs +++ b/raphtory/src/db/api/view/graph.rs @@ -408,20 +408,22 @@ impl<'graph, G: GraphView + 'graph> GraphViewOps<'graph> for G { let eid = EID(eid); let src_id = node_map[edge.edge.src().index()]; let dst_id = node_map[edge.edge.dst().index()]; + let maybe_src_pos = shard.resolve_pos(src_id); + let maybe_dst_pos = shard.resolve_pos(dst_id); - if let Some(node_pos) = shard.resolve_pos(src_id) { + if let Some(node_pos) = maybe_src_pos { let mut writer = shard.writer(); writer.add_static_outbound_edge(node_pos, dst_id, eid, 0); } - if let Some(node_pos) = shard.resolve_pos(dst_id) { + if let Some(node_pos) = maybe_dst_pos { let mut writer = shard.writer(); writer.add_static_inbound_edge(node_pos, src_id, eid, 0); } for e in edge.explode_layers() { let layer = layer_map[e.edge.layer().unwrap()]; - if let Some(node_pos) = shard.resolve_pos(src_id) { + if let Some(node_pos) = maybe_src_pos { let mut writer = shard.writer(); writer.add_outbound_edge::( None, @@ -431,7 +433,7 @@ impl<'graph, G: GraphView + 'graph> GraphViewOps<'graph> for G { 0, ); } - if let Some(node_pos) = shard.resolve_pos(dst_id) { + if let Some(node_pos) = maybe_dst_pos { let mut writer = shard.writer(); writer.add_inbound_edge::( None, @@ -444,14 +446,14 @@ impl<'graph, G: GraphView + 'graph> GraphViewOps<'graph> for G { } for e in edge.explode() { - if let Some(node_pos) = shard.resolve_pos(src_id) { + if let Some(node_pos) = maybe_src_pos { let mut writer = shard.writer(); let t = e.time_and_index().expect("exploded edge should have time"); let l = layer_map[e.edge.layer().unwrap()]; writer.update_timestamp(t, node_pos, eid.with_layer(l), 0); } - if let Some(node_pos) = shard.resolve_pos(dst_id) { + if let Some(node_pos) = maybe_dst_pos { let mut writer = shard.writer(); let t = e.time_and_index().expect("exploded edge should have time"); @@ -467,10 +469,7 @@ impl<'graph, G: GraphView + 'graph> GraphViewOps<'graph> for G { self, self.layer_ids(), ) { - let src = node_map[edge.edge.src().index()]; - let dst = node_map[edge.edge.dst().index()]; - - if let Some(node_pos) = shard.resolve_pos(src) { + if let Some(node_pos) = maybe_src_pos { let mut writer = shard.writer(); writer.update_deletion_time( t, @@ -479,7 +478,7 @@ impl<'graph, G: GraphView + 'graph> GraphViewOps<'graph> for G { 0, ); } - if let Some(node_pos) = shard.resolve_pos(dst) { + if let Some(node_pos) = maybe_dst_pos { let mut writer = shard.writer(); writer.update_deletion_time( t, From c6ddb2abe6241a7ec6715860c3738984d4000ef2 Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Tue, 19 Aug 2025 17:09:47 +0200 Subject: [PATCH 123/321] refactor double type reference because it is confusing --- .../src/graph/edges/edge_entry.rs | 6 +- raphtory-storage/src/graph/edges/edge_ref.rs | 4 +- raphtory-storage/src/graph/edges/edges.rs | 9 +- .../storage/graph/storage_ops/edge_filter.rs | 6 +- .../db/api/view/internal/edge_filter_ops.rs | 10 +- .../src/db/api/view/internal/filter_ops.rs | 24 ++--- .../time_semantics/base_time_semantics.rs | 91 +++++++++--------- .../time_semantics/event_semantics.rs | 92 +++++++++--------- .../internal/time_semantics/filtered_edge.rs | 11 +-- .../time_semantics/persistent_semantics.rs | 96 +++++++++---------- .../internal/time_semantics/time_semantics.rs | 89 ++++++++--------- .../time_semantics/time_semantics_ops.rs | 94 +++++++++--------- .../time_semantics/window_time_semantics.rs | 89 ++++++++--------- raphtory/src/db/graph/views/cached_view.rs | 7 +- .../views/filter/edge_and_filtered_graph.rs | 7 +- .../views/filter/edge_field_filtered_graph.rs | 5 +- .../views/filter/edge_not_filtered_graph.rs | 6 +- .../views/filter/edge_or_filtered_graph.rs | 7 +- .../filter/edge_property_filtered_graph.rs | 5 +- .../src/db/graph/views/filter/model/mod.rs | 5 +- .../views/filter/model/property_filter.rs | 5 +- raphtory/src/db/graph/views/layer_graph.rs | 4 +- raphtory/src/db/graph/views/node_subgraph.rs | 6 +- raphtory/src/db/graph/views/valid_graph.rs | 4 +- raphtory/src/db/graph/views/window_graph.rs | 6 +- 25 files changed, 350 insertions(+), 338 deletions(-) diff --git a/raphtory-storage/src/graph/edges/edge_entry.rs b/raphtory-storage/src/graph/edges/edge_entry.rs index 2790488e6a..af38d9cb70 100644 --- a/raphtory-storage/src/graph/edges/edge_entry.rs +++ b/raphtory-storage/src/graph/edges/edge_entry.rs @@ -1,6 +1,6 @@ use std::ops::Range; -use crate::graph::edges::{edge_ref::EdgeStorageRef, edge_storage_ops::EdgeStorageOps}; +use crate::graph::edges::{edge_storage_ops::EdgeStorageOps}; use raphtory_api::core::entities::properties::{prop::Prop, tprop::TPropOps}; use raphtory_core::entities::{LayerIds, EID, VID}; use storage::{api::edges::EdgeEntryOps, EdgeEntry, EdgeEntryRef}; @@ -18,12 +18,12 @@ pub enum EdgeStorageEntry<'a> { impl<'a> EdgeStorageEntry<'a> { #[inline] - pub fn as_ref(&self) -> EdgeStorageRef { + pub fn as_ref(&self) -> EdgeEntryRef { match self { EdgeStorageEntry::Mem(edge) => *edge, EdgeStorageEntry::Unlocked(edge) => edge.as_ref(), #[cfg(feature = "storage")] - EdgeStorageEntry::Disk(edge) => EdgeStorageRef::Disk(*edge), + EdgeStorageEntry::Disk(edge) => EdgeEntryRef::Disk(*edge), } } } diff --git a/raphtory-storage/src/graph/edges/edge_ref.rs b/raphtory-storage/src/graph/edges/edge_ref.rs index 3569cff334..05f844d60f 100644 --- a/raphtory-storage/src/graph/edges/edge_ref.rs +++ b/raphtory-storage/src/graph/edges/edge_ref.rs @@ -1,3 +1 @@ -use storage::EdgeEntryRef; - -pub type EdgeStorageRef<'a> = EdgeEntryRef<'a>; +pub use storage::EdgeEntryRef; diff --git a/raphtory-storage/src/graph/edges/edges.rs b/raphtory-storage/src/graph/edges/edges.rs index a3c6b6f36d..72a738ae21 100644 --- a/raphtory-storage/src/graph/edges/edges.rs +++ b/raphtory-storage/src/graph/edges/edges.rs @@ -1,9 +1,8 @@ use super::{edge_entry::EdgeStorageEntry, unlocked::UnlockedEdges}; -use crate::graph::edges::edge_ref::EdgeStorageRef; use raphtory_api::core::entities::{LayerIds, EID}; use rayon::iter::ParallelIterator; use std::sync::Arc; -use storage::{utils::Iter2, Extension, ReadLockedEdges}; +use storage::{utils::Iter2, EdgeEntryRef, Extension, ReadLockedEdges}; pub struct EdgesStorage { storage: Arc>, @@ -19,21 +18,21 @@ impl EdgesStorage { EdgesStorageRef::Mem(self.storage.as_ref()) } - pub fn edge(&self, eid: EID) -> EdgeStorageRef { + pub fn edge(&self, eid: EID) -> EdgeEntryRef { self.storage.edge_ref(eid) } pub fn iter<'a>( &'a self, layers: &'a LayerIds, - ) -> impl Iterator> + Send + Sync + 'a { + ) -> impl Iterator> + Send + Sync + 'a { self.storage.iter(layers) } pub fn par_iter<'a>( &'a self, layers: &'a LayerIds, - ) -> impl ParallelIterator> + Sync + 'a { + ) -> impl ParallelIterator> + Sync + 'a { self.storage.par_iter(layers) } } diff --git a/raphtory/src/db/api/storage/graph/storage_ops/edge_filter.rs b/raphtory/src/db/api/storage/graph/storage_ops/edge_filter.rs index 94e31b4c58..4cd05af793 100644 --- a/raphtory/src/db/api/storage/graph/storage_ops/edge_filter.rs +++ b/raphtory/src/db/api/storage/graph/storage_ops/edge_filter.rs @@ -6,7 +6,7 @@ use crate::{ }, }; use raphtory_api::core::{entities::ELID, storage::timeindex::TimeIndexEntry}; -use raphtory_storage::graph::edges::edge_ref::EdgeStorageRef; +use storage::EdgeEntryRef; impl InternalEdgeFilterOps for GraphStorage { #[inline] @@ -20,7 +20,7 @@ impl InternalEdgeFilterOps for GraphStorage { } #[inline] - fn internal_filter_edge(&self, _edge: EdgeStorageRef, _layer_ids: &LayerIds) -> bool { + fn internal_filter_edge(&self, _edge: EdgeEntryRef, _layer_ids: &LayerIds) -> bool { true } @@ -66,7 +66,7 @@ impl InternalEdgeLayerFilterOps for GraphStorage { } #[inline] - fn internal_filter_edge_layer(&self, _edge: EdgeStorageRef, _layer: usize) -> bool { + fn internal_filter_edge_layer(&self, _edge: EdgeEntryRef, _layer: usize) -> bool { true } diff --git a/raphtory/src/db/api/view/internal/edge_filter_ops.rs b/raphtory/src/db/api/view/internal/edge_filter_ops.rs index 6c15af6fd8..17accda5bd 100644 --- a/raphtory/src/db/api/view/internal/edge_filter_ops.rs +++ b/raphtory/src/db/api/view/internal/edge_filter_ops.rs @@ -3,7 +3,7 @@ use raphtory_api::{ core::{entities::ELID, storage::timeindex::TimeIndexEntry}, inherit::Base, }; -use raphtory_storage::graph::edges::edge_ref::EdgeStorageRef; +use storage::EdgeEntryRef; pub trait InternalEdgeLayerFilterOps { /// Set to true when filtering, used for optimisations @@ -13,7 +13,7 @@ pub trait InternalEdgeLayerFilterOps { fn internal_layer_filter_edge_list_trusted(&self) -> bool; /// Filter a layer for an edge - fn internal_filter_edge_layer(&self, edge: EdgeStorageRef, layer: usize) -> bool; + fn internal_filter_edge_layer(&self, edge: EdgeEntryRef, layer: usize) -> bool; fn node_filter_includes_edge_layer_filter(&self) -> bool { false @@ -62,7 +62,7 @@ pub trait InternalEdgeFilterOps { /// If true, all edges returned by `self.edge_list()` exist, otherwise it needs further filtering fn internal_edge_list_trusted(&self) -> bool; - fn internal_filter_edge(&self, edge: EdgeStorageRef, layer_ids: &LayerIds) -> bool; + fn internal_filter_edge(&self, edge: EdgeEntryRef, layer_ids: &LayerIds) -> bool; fn node_filter_includes_edge_filter(&self) -> bool { false @@ -94,7 +94,7 @@ impl> InternalEdgeFilterOps self.base().internal_edge_list_trusted() } #[inline] - fn internal_filter_edge(&self, edge: EdgeStorageRef, layer_ids: &LayerIds) -> bool { + fn internal_filter_edge(&self, edge: EdgeEntryRef, layer_ids: &LayerIds) -> bool { self.base().internal_filter_edge(edge, layer_ids) } @@ -122,7 +122,7 @@ impl> InternalEdg } #[inline] - fn internal_filter_edge_layer(&self, edge: EdgeStorageRef, layer: usize) -> bool { + fn internal_filter_edge_layer(&self, edge: EdgeEntryRef, layer: usize) -> bool { self.base().internal_filter_edge_layer(edge, layer) } diff --git a/raphtory/src/db/api/view/internal/filter_ops.rs b/raphtory/src/db/api/view/internal/filter_ops.rs index 09fc69e074..332e02db22 100644 --- a/raphtory/src/db/api/view/internal/filter_ops.rs +++ b/raphtory/src/db/api/view/internal/filter_ops.rs @@ -8,7 +8,7 @@ use raphtory_api::core::{ storage::timeindex::{TimeIndexEntry, TimeIndexOps}, }; use raphtory_storage::graph::{ - edges::{edge_ref::EdgeStorageRef, edge_storage_ops::EdgeStorageOps}, + edges::{edge_ref::EdgeEntryRef, edge_storage_ops::EdgeStorageOps}, nodes::node_ref::NodeStorageRef, }; @@ -45,16 +45,16 @@ pub trait FilterOps { fn node_list_trusted(&self) -> bool; - fn filter_edge(&self, edge: EdgeStorageRef) -> bool; + fn filter_edge(&self, edge: EdgeEntryRef) -> bool; - fn filter_edge_layer(&self, edge: EdgeStorageRef, layer: usize) -> bool; + fn filter_edge_layer(&self, edge: EdgeEntryRef, layer: usize) -> bool; fn filter_exploded_edge(&self, eid: ELID, t: TimeIndexEntry) -> bool; fn edge_list_trusted(&self) -> bool; fn exploded_filter_independent(&self) -> bool; - fn filter_edge_from_nodes(&self, edge: EdgeStorageRef) -> bool; + fn filter_edge_from_nodes(&self, edge: EdgeEntryRef) -> bool; } /// Implements all the filtering except for time semantics as it is used to define the time semantics @@ -63,10 +63,10 @@ pub trait InnerFilterOps { fn filtered_inner(&self) -> bool; - fn filter_edge_inner(&self, edge: EdgeStorageRef) -> bool; + fn filter_edge_inner(&self, edge: EdgeEntryRef) -> bool; /// handles edge and edge layer filter (not exploded edge filter or windows) - fn filter_edge_layer_inner(&self, edge: EdgeStorageRef, layer: usize) -> bool; + fn filter_edge_layer_inner(&self, edge: EdgeEntryRef, layer: usize) -> bool; fn filter_exploded_edge_inner(&self, eid: ELID, t: TimeIndexEntry) -> bool; } @@ -83,7 +83,7 @@ impl InnerFilterOps for G { || self.internal_exploded_edge_filtered() } - fn filter_edge_inner(&self, edge: EdgeStorageRef) -> bool { + fn filter_edge_inner(&self, edge: EdgeEntryRef) -> bool { self.internal_filter_edge(edge, self.layer_ids()) && (self.edge_filter_includes_edge_layer_filter() || edge @@ -93,7 +93,7 @@ impl InnerFilterOps for G { && self.filter_edge_from_nodes(edge) } - fn filter_edge_layer_inner(&self, edge: EdgeStorageRef, layer: usize) -> bool { + fn filter_edge_layer_inner(&self, edge: EdgeEntryRef, layer: usize) -> bool { self.layer_ids().contains(&layer) && self.internal_filter_edge_layer(edge, layer) && (self.edge_layer_filter_includes_edge_filter() @@ -174,7 +174,7 @@ impl FilterOps for G { && self.node_filter_includes_exploded_edge_filter() } - fn filter_edge(&self, edge: EdgeStorageRef) -> bool { + fn filter_edge(&self, edge: EdgeEntryRef) -> bool { self.internal_filter_edge(edge, self.layer_ids()) && self.filter_edge_from_nodes(edge) && { let time_semantics = self.edge_time_semantics(); edge.layer_ids_iter(self.layer_ids()).any(|layer_id| { @@ -184,7 +184,7 @@ impl FilterOps for G { } } - fn filter_edge_layer(&self, edge: EdgeStorageRef, layer: usize) -> bool { + fn filter_edge_layer(&self, edge: EdgeEntryRef, layer: usize) -> bool { self.internal_filter_edge_layer(edge, layer) && (self.edge_layer_filter_includes_edge_filter() || self.internal_filter_edge(edge, self.layer_ids())) @@ -207,7 +207,7 @@ impl FilterOps for G { && self.exploded_edge_filter_includes_edge_layer_filter() } - fn filter_edge_from_nodes(&self, edge: EdgeStorageRef) -> bool { + fn filter_edge_from_nodes(&self, edge: EdgeEntryRef) -> bool { self.exploded_edge_filter_includes_node_filter() || self.edge_layer_filter_includes_node_filter() || self.edge_filter_includes_node_filter() @@ -216,7 +216,7 @@ impl FilterOps for G { } } -fn filter_edge_from_exploded_filter(view: &G, edge: EdgeStorageRef) -> bool { +fn filter_edge_from_exploded_filter(view: &G, edge: EdgeEntryRef) -> bool { view.edge_filter_includes_exploded_edge_filter() || view.edge_layer_filter_includes_exploded_edge_filter() || { diff --git a/raphtory/src/db/api/view/internal/time_semantics/base_time_semantics.rs b/raphtory/src/db/api/view/internal/time_semantics/base_time_semantics.rs index 05cce313a6..b2c044b496 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/base_time_semantics.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/base_time_semantics.rs @@ -10,8 +10,9 @@ use raphtory_api::core::{ entities::{properties::prop::Prop, LayerIds, ELID}, storage::timeindex::TimeIndexEntry, }; -use raphtory_storage::graph::{edges::edge_ref::EdgeStorageRef, nodes::node_ref::NodeStorageRef}; +use raphtory_storage::graph::{ nodes::node_ref::NodeStorageRef}; use std::ops::Range; +use storage::EdgeEntryRef; #[derive(Copy, Clone, Debug)] pub enum BaseTimeSemantics { @@ -236,14 +237,14 @@ impl EdgeTimeSemanticsOps for BaseTimeSemantics { for_all!(self, semantics => semantics.handle_edge_update_filter(t, eid, view)) } - fn include_edge(&self, edge: EdgeStorageRef, view: G, layer_id: usize) -> bool { + fn include_edge(&self, edge: EdgeEntryRef, view: G, layer_id: usize) -> bool { for_all!(self, semantics => semantics.include_edge(edge, view, layer_id)) } #[inline] fn include_edge_window( &self, - edge: EdgeStorageRef, + edge: EdgeEntryRef, view: G, layer_id: usize, w: Range, @@ -268,7 +269,7 @@ impl EdgeTimeSemanticsOps for BaseTimeSemantics { #[inline] fn edge_history<'graph, G: GraphView + 'graph>( self, - edge: EdgeStorageRef<'graph>, + edge: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, ) -> impl Iterator + Send + Sync + 'graph { @@ -278,7 +279,7 @@ impl EdgeTimeSemanticsOps for BaseTimeSemantics { #[inline] fn edge_history_window<'graph, G: GraphView + 'graph>( self, - edge: EdgeStorageRef<'graph>, + edge: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, w: Range, @@ -286,10 +287,12 @@ impl EdgeTimeSemanticsOps for BaseTimeSemantics { for_all_iter!(self, semantics => semantics.edge_history_window(edge, view, layer_ids, w)) } + + #[inline] fn edge_exploded_count<'graph, G: GraphView + 'graph>( &self, - edge: EdgeStorageRef, + edge: EdgeEntryRef, view: G, ) -> usize { for_all!(self, semantics => semantics.edge_exploded_count(edge, view)) @@ -298,7 +301,7 @@ impl EdgeTimeSemanticsOps for BaseTimeSemantics { #[inline] fn edge_exploded_count_window<'graph, G: GraphView + 'graph>( &self, - edge: EdgeStorageRef, + edge: EdgeEntryRef, view: G, w: Range, ) -> usize { @@ -308,7 +311,7 @@ impl EdgeTimeSemanticsOps for BaseTimeSemantics { #[inline] fn edge_exploded<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, ) -> impl Iterator + Send + Sync + 'graph { @@ -318,7 +321,7 @@ impl EdgeTimeSemanticsOps for BaseTimeSemantics { #[inline] fn edge_layers<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, ) -> impl Iterator + Send + Sync + 'graph { @@ -328,7 +331,7 @@ impl EdgeTimeSemanticsOps for BaseTimeSemantics { #[inline] fn edge_window_exploded<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, w: Range, @@ -339,7 +342,7 @@ impl EdgeTimeSemanticsOps for BaseTimeSemantics { #[inline] fn edge_window_layers<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, w: Range, @@ -350,7 +353,7 @@ impl EdgeTimeSemanticsOps for BaseTimeSemantics { #[inline] fn edge_earliest_time<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, ) -> Option { for_all!(self, semantics => semantics.edge_earliest_time(e, view)) @@ -359,7 +362,7 @@ impl EdgeTimeSemanticsOps for BaseTimeSemantics { #[inline] fn edge_earliest_time_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, w: Range, ) -> Option { @@ -369,7 +372,7 @@ impl EdgeTimeSemanticsOps for BaseTimeSemantics { #[inline] fn edge_exploded_earliest_time<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, t: TimeIndexEntry, layer: usize, @@ -380,7 +383,7 @@ impl EdgeTimeSemanticsOps for BaseTimeSemantics { #[inline] fn edge_exploded_earliest_time_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, t: TimeIndexEntry, layer: usize, @@ -392,7 +395,7 @@ impl EdgeTimeSemanticsOps for BaseTimeSemantics { #[inline] fn edge_latest_time<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, ) -> Option { for_all!(self, semantics => semantics.edge_latest_time(e, view)) @@ -401,7 +404,7 @@ impl EdgeTimeSemanticsOps for BaseTimeSemantics { #[inline] fn edge_latest_time_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, w: Range, ) -> Option { @@ -411,7 +414,7 @@ impl EdgeTimeSemanticsOps for BaseTimeSemantics { #[inline] fn edge_exploded_latest_time<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, t: TimeIndexEntry, layer: usize, @@ -422,7 +425,7 @@ impl EdgeTimeSemanticsOps for BaseTimeSemantics { #[inline] fn edge_exploded_latest_time_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, t: TimeIndexEntry, layer: usize, @@ -434,7 +437,7 @@ impl EdgeTimeSemanticsOps for BaseTimeSemantics { #[inline] fn edge_deletion_history<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, ) -> impl Iterator + Send + Sync + 'graph { @@ -444,7 +447,7 @@ impl EdgeTimeSemanticsOps for BaseTimeSemantics { #[inline] fn edge_deletion_history_window<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, w: Range, @@ -455,7 +458,7 @@ impl EdgeTimeSemanticsOps for BaseTimeSemantics { #[inline] fn edge_is_valid<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, ) -> bool { for_all!(self, semantics => semantics.edge_is_valid(e, view)) @@ -464,7 +467,7 @@ impl EdgeTimeSemanticsOps for BaseTimeSemantics { #[inline] fn edge_is_valid_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, r: Range, ) -> bool { @@ -474,7 +477,7 @@ impl EdgeTimeSemanticsOps for BaseTimeSemantics { #[inline] fn edge_is_deleted<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, ) -> bool { for_all!(self, semantics => semantics.edge_is_deleted(e, view)) @@ -483,7 +486,7 @@ impl EdgeTimeSemanticsOps for BaseTimeSemantics { #[inline] fn edge_is_deleted_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, w: Range, ) -> bool { @@ -493,7 +496,7 @@ impl EdgeTimeSemanticsOps for BaseTimeSemantics { #[inline] fn edge_is_active<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, ) -> bool { for_all!(self, semantics => semantics.edge_is_active(e, view)) @@ -502,7 +505,7 @@ impl EdgeTimeSemanticsOps for BaseTimeSemantics { #[inline] fn edge_is_active_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, w: Range, ) -> bool { @@ -512,7 +515,7 @@ impl EdgeTimeSemanticsOps for BaseTimeSemantics { #[inline] fn edge_is_active_exploded<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, t: TimeIndexEntry, layer: usize, @@ -523,7 +526,7 @@ impl EdgeTimeSemanticsOps for BaseTimeSemantics { #[inline] fn edge_is_active_exploded_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, t: TimeIndexEntry, layer: usize, @@ -535,7 +538,7 @@ impl EdgeTimeSemanticsOps for BaseTimeSemantics { #[inline] fn edge_is_valid_exploded<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, t: TimeIndexEntry, layer: usize, @@ -546,7 +549,7 @@ impl EdgeTimeSemanticsOps for BaseTimeSemantics { #[inline] fn edge_is_valid_exploded_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, t: TimeIndexEntry, layer: usize, @@ -558,7 +561,7 @@ impl EdgeTimeSemanticsOps for BaseTimeSemantics { #[inline] fn edge_exploded_deletion<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, t: TimeIndexEntry, layer: usize, @@ -569,7 +572,7 @@ impl EdgeTimeSemanticsOps for BaseTimeSemantics { #[inline] fn edge_exploded_deletion_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, t: TimeIndexEntry, layer: usize, @@ -581,7 +584,7 @@ impl EdgeTimeSemanticsOps for BaseTimeSemantics { #[inline] fn temporal_edge_prop_exploded<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, prop_id: usize, t: TimeIndexEntry, @@ -593,7 +596,7 @@ impl EdgeTimeSemanticsOps for BaseTimeSemantics { #[inline] fn temporal_edge_prop_exploded_last_at<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, edge_time: TimeIndexEntry, layer_id: usize, @@ -606,7 +609,7 @@ impl EdgeTimeSemanticsOps for BaseTimeSemantics { #[inline] fn temporal_edge_prop_exploded_last_at_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, edge_time: TimeIndexEntry, layer_id: usize, @@ -620,7 +623,7 @@ impl EdgeTimeSemanticsOps for BaseTimeSemantics { #[inline] fn temporal_edge_prop_last_at<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, prop_id: usize, t: TimeIndexEntry, @@ -631,7 +634,7 @@ impl EdgeTimeSemanticsOps for BaseTimeSemantics { #[inline] fn temporal_edge_prop_last_at_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, prop_id: usize, t: TimeIndexEntry, @@ -643,7 +646,7 @@ impl EdgeTimeSemanticsOps for BaseTimeSemantics { #[inline] fn temporal_edge_prop_hist<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, prop_id: usize, @@ -654,7 +657,7 @@ impl EdgeTimeSemanticsOps for BaseTimeSemantics { #[inline] fn temporal_edge_prop_hist_rev<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, prop_id: usize, @@ -665,7 +668,7 @@ impl EdgeTimeSemanticsOps for BaseTimeSemantics { #[inline] fn temporal_edge_prop_hist_window<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, prop_id: usize, @@ -677,7 +680,7 @@ impl EdgeTimeSemanticsOps for BaseTimeSemantics { #[inline] fn temporal_edge_prop_hist_window_rev<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, prop_id: usize, @@ -689,7 +692,7 @@ impl EdgeTimeSemanticsOps for BaseTimeSemantics { #[inline] fn edge_metadata<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, prop_id: usize, ) -> Option { @@ -699,7 +702,7 @@ impl EdgeTimeSemanticsOps for BaseTimeSemantics { #[inline] fn edge_metadata_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, prop_id: usize, w: Range, diff --git a/raphtory/src/db/api/view/internal/time_semantics/event_semantics.rs b/raphtory/src/db/api/view/internal/time_semantics/event_semantics.rs index 913be15c98..9bf065c41f 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/event_semantics.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/event_semantics.rs @@ -1,3 +1,4 @@ +use std::iter::Peekable; use crate::db::api::view::internal::{ time_semantics::{ filtered_edge::FilteredEdgeStorageOps, filtered_node::FilteredNodeStorageOps, @@ -15,10 +16,11 @@ use raphtory_api::core::{ storage::timeindex::{AsTime, TimeIndexEntry, TimeIndexOps}, }; use raphtory_storage::graph::{ - edges::{edge_ref::EdgeStorageRef, edge_storage_ops::EdgeStorageOps}, + edges::{edge_storage_ops::EdgeStorageOps}, nodes::{node_ref::NodeStorageRef, node_storage_ops::NodeStorageOps}, }; use std::ops::Range; +use storage::EdgeEntryRef; #[derive(Debug, Copy, Clone)] pub struct EventSemantics; @@ -205,14 +207,14 @@ impl EdgeTimeSemanticsOps for EventSemantics { view.filter_exploded_edge_inner(eid, t).then_some((t, eid)) } - fn include_edge(&self, edge: EdgeStorageRef, view: G, layer_id: usize) -> bool { + fn include_edge(&self, edge: EdgeEntryRef, view: G, layer_id: usize) -> bool { !edge.filtered_additions(layer_id, &view).is_empty() || !edge.filtered_deletions(layer_id, &view).is_empty() } fn include_edge_window( &self, - edge: EdgeStorageRef, + edge: EdgeEntryRef, view: G, layer_id: usize, w: Range, @@ -247,7 +249,7 @@ impl EdgeTimeSemanticsOps for EventSemantics { fn edge_history<'graph, G: GraphView + 'graph>( self, - edge: EdgeStorageRef<'graph>, + edge: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, ) -> impl Iterator + Send + Sync + 'graph { @@ -258,7 +260,7 @@ impl EdgeTimeSemanticsOps for EventSemantics { fn edge_history_window<'graph, G: GraphView + 'graph>( self, - edge: EdgeStorageRef<'graph>, + edge: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, w: Range, @@ -275,7 +277,7 @@ impl EdgeTimeSemanticsOps for EventSemantics { fn edge_exploded_count<'graph, G: GraphView + 'graph>( &self, - edge: EdgeStorageRef, + edge: EdgeEntryRef, view: G, ) -> usize { edge.filtered_additions_iter(&view, view.layer_ids()) @@ -285,7 +287,7 @@ impl EdgeTimeSemanticsOps for EventSemantics { fn edge_exploded_count_window<'graph, G: GraphView + 'graph>( &self, - edge: EdgeStorageRef, + edge: EdgeEntryRef, view: G, w: Range, ) -> usize { @@ -296,7 +298,7 @@ impl EdgeTimeSemanticsOps for EventSemantics { fn edge_exploded<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, ) -> impl Iterator + Send + Sync + 'graph { @@ -305,7 +307,7 @@ impl EdgeTimeSemanticsOps for EventSemantics { fn edge_layers<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, ) -> impl Iterator + Send + Sync + 'graph { @@ -326,7 +328,7 @@ impl EdgeTimeSemanticsOps for EventSemantics { fn edge_window_exploded<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, w: Range, @@ -336,7 +338,7 @@ impl EdgeTimeSemanticsOps for EventSemantics { fn edge_window_layers<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, w: Range, @@ -350,7 +352,7 @@ impl EdgeTimeSemanticsOps for EventSemantics { fn edge_earliest_time<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, ) -> Option { e.filtered_additions_iter(&view, view.layer_ids()) @@ -364,7 +366,7 @@ impl EdgeTimeSemanticsOps for EventSemantics { fn edge_earliest_time_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, w: Range, ) -> Option { @@ -379,7 +381,7 @@ impl EdgeTimeSemanticsOps for EventSemantics { fn edge_exploded_earliest_time<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, t: TimeIndexEntry, layer: usize, @@ -390,7 +392,7 @@ impl EdgeTimeSemanticsOps for EventSemantics { fn edge_exploded_earliest_time_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, t: TimeIndexEntry, layer: usize, @@ -404,7 +406,7 @@ impl EdgeTimeSemanticsOps for EventSemantics { fn edge_latest_time<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, ) -> Option { e.filtered_additions_iter(&view, view.layer_ids()) @@ -418,7 +420,7 @@ impl EdgeTimeSemanticsOps for EventSemantics { fn edge_latest_time_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, w: Range, ) -> Option { @@ -433,7 +435,7 @@ impl EdgeTimeSemanticsOps for EventSemantics { fn edge_exploded_latest_time<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, t: TimeIndexEntry, layer: usize, @@ -443,7 +445,7 @@ impl EdgeTimeSemanticsOps for EventSemantics { fn edge_exploded_latest_time_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, t: TimeIndexEntry, layer: usize, @@ -454,7 +456,7 @@ impl EdgeTimeSemanticsOps for EventSemantics { fn edge_deletion_history<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, ) -> impl Iterator + Send + Sync + 'graph { @@ -465,7 +467,7 @@ impl EdgeTimeSemanticsOps for EventSemantics { fn edge_deletion_history_window<'graph, G: GraphView + 'graph>( self, - edge: EdgeStorageRef<'graph>, + edge: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, w: Range, @@ -483,7 +485,7 @@ impl EdgeTimeSemanticsOps for EventSemantics { /// An edge is valid with event semantics if it has at least one addition event in the current view fn edge_is_valid<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, ) -> bool { e.filtered_additions_iter(&view, view.layer_ids()) @@ -493,7 +495,7 @@ impl EdgeTimeSemanticsOps for EventSemantics { /// An edge is valid in a window with event semantics if it has at least one addition event in the current view in the window fn edge_is_valid_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, w: Range, ) -> bool { @@ -504,7 +506,7 @@ impl EdgeTimeSemanticsOps for EventSemantics { /// An edge is deleted with event semantics if it has at least one deletion event in the current view fn edge_is_deleted<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, ) -> bool { e.filtered_deletions_iter(&view, view.layer_ids()) @@ -514,7 +516,7 @@ impl EdgeTimeSemanticsOps for EventSemantics { /// An edge is deleted in a window with event semantics if it has at least one deletion event in the current view in the window fn edge_is_deleted_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, w: Range, ) -> bool { @@ -525,7 +527,7 @@ impl EdgeTimeSemanticsOps for EventSemantics { /// An edge is valid with event semantics if it has at least one event in the current view fn edge_is_active<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, ) -> bool { self.edge_is_valid(e, &view) || self.edge_is_deleted(e, &view) @@ -534,7 +536,7 @@ impl EdgeTimeSemanticsOps for EventSemantics { /// An edge is active in a window with event semantics if it has at least one event in the current view in the window fn edge_is_active_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, w: Range, ) -> bool { @@ -543,7 +545,7 @@ impl EdgeTimeSemanticsOps for EventSemantics { fn edge_is_active_exploded<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, t: TimeIndexEntry, layer: usize, @@ -553,7 +555,7 @@ impl EdgeTimeSemanticsOps for EventSemantics { fn edge_is_active_exploded_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, t: TimeIndexEntry, layer: usize, @@ -567,7 +569,7 @@ impl EdgeTimeSemanticsOps for EventSemantics { /// (i.e., it's corresponding event is part of the view) fn edge_is_valid_exploded<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, t: TimeIndexEntry, layer: usize, @@ -579,7 +581,7 @@ impl EdgeTimeSemanticsOps for EventSemantics { /// (i.e., it's corresponding event is part of the view) fn edge_is_valid_exploded_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, t: TimeIndexEntry, layer: usize, @@ -590,7 +592,7 @@ impl EdgeTimeSemanticsOps for EventSemantics { fn edge_exploded_deletion<'graph, G: GraphView + 'graph>( &self, - _e: EdgeStorageRef<'graph>, + _e: EdgeEntryRef<'graph>, _view: G, _t: TimeIndexEntry, _layer: usize, @@ -600,7 +602,7 @@ impl EdgeTimeSemanticsOps for EventSemantics { fn edge_exploded_deletion_window<'graph, G: GraphView + 'graph>( &self, - _e: EdgeStorageRef<'graph>, + _e: EdgeEntryRef<'graph>, _view: G, _t: TimeIndexEntry, _layer: usize, @@ -611,7 +613,7 @@ impl EdgeTimeSemanticsOps for EventSemantics { fn temporal_edge_prop_exploded<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, prop_id: usize, t: TimeIndexEntry, @@ -627,7 +629,7 @@ impl EdgeTimeSemanticsOps for EventSemantics { fn temporal_edge_prop_exploded_last_at<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, edge_time: TimeIndexEntry, layer_id: usize, @@ -643,7 +645,7 @@ impl EdgeTimeSemanticsOps for EventSemantics { fn temporal_edge_prop_exploded_last_at_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, edge_time: TimeIndexEntry, layer_id: usize, @@ -660,7 +662,7 @@ impl EdgeTimeSemanticsOps for EventSemantics { fn temporal_edge_prop_last_at<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, prop_id: usize, t: TimeIndexEntry, @@ -673,7 +675,7 @@ impl EdgeTimeSemanticsOps for EventSemantics { fn temporal_edge_prop_last_at_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, prop_id: usize, t: TimeIndexEntry, @@ -694,7 +696,7 @@ impl EdgeTimeSemanticsOps for EventSemantics { fn temporal_edge_prop_hist<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, prop_id: usize, @@ -706,7 +708,7 @@ impl EdgeTimeSemanticsOps for EventSemantics { fn temporal_edge_prop_hist_rev<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, prop_id: usize, @@ -721,7 +723,7 @@ impl EdgeTimeSemanticsOps for EventSemantics { fn temporal_edge_prop_hist_window<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, prop_id: usize, @@ -737,7 +739,7 @@ impl EdgeTimeSemanticsOps for EventSemantics { fn temporal_edge_prop_hist_window_rev<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, prop_id: usize, @@ -753,7 +755,7 @@ impl EdgeTimeSemanticsOps for EventSemantics { fn edge_metadata<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, prop_id: usize, ) -> Option { @@ -766,7 +768,7 @@ impl EdgeTimeSemanticsOps for EventSemantics { fn edge_metadata_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, prop_id: usize, w: Range, @@ -778,4 +780,4 @@ impl EdgeTimeSemanticsOps for EventSemantics { }; e.filtered_edge_metadata(&view, prop_id, layer_filter) } -} +} \ No newline at end of file diff --git a/raphtory/src/db/api/view/internal/time_semantics/filtered_edge.rs b/raphtory/src/db/api/view/internal/time_semantics/filtered_edge.rs index b9706540ce..aef20991e1 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/filtered_edge.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/filtered_edge.rs @@ -11,13 +11,12 @@ use raphtory_api::core::{ storage::timeindex::{TimeIndexEntry, TimeIndexOps}, }; use raphtory_storage::graph::edges::{ - edge_ref::EdgeStorageRef, - edge_storage_ops::{EdgeStorageOps, TimeIndexRef}, + edge_storage_ops::{EdgeStorageOps}, edges::EdgesStorage, }; use rayon::iter::ParallelIterator; use std::{iter, marker::PhantomData, ops::Range}; -use storage::{EdgeAdditions, EdgeDeletions}; +use storage::{EdgeAdditions, EdgeDeletions, EdgeEntryRef}; #[derive(Clone)] pub struct FilteredEdgeTimeIndex<'graph, G, TS> { @@ -313,7 +312,7 @@ pub trait FilteredEdgeStorageOps<'a> { ) -> Option; } -impl<'a> FilteredEdgeStorageOps<'a> for EdgeStorageRef<'a> { +impl<'a> FilteredEdgeStorageOps<'a> for EdgeEntryRef<'a> { fn filtered_additions_iter( self, view: G, @@ -449,7 +448,7 @@ pub trait FilteredEdgesStorageOps { &'a self, view: G, layer_ids: &'a LayerIds, - ) -> impl ParallelIterator> + 'a; + ) -> impl ParallelIterator> + 'a; } impl FilteredEdgesStorageOps for EdgesStorage { @@ -457,7 +456,7 @@ impl FilteredEdgesStorageOps for EdgesStorage { &'a self, view: G, layer_ids: &'a LayerIds, - ) -> impl ParallelIterator> + 'a { + ) -> impl ParallelIterator> + 'a { let par_iter = self.par_iter(layer_ids); match view.filter_state() { FilterState::Neither => FilterVariants::Neither(par_iter), diff --git a/raphtory/src/db/api/view/internal/time_semantics/persistent_semantics.rs b/raphtory/src/db/api/view/internal/time_semantics/persistent_semantics.rs index 50857a7168..be8c188871 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/persistent_semantics.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/persistent_semantics.rs @@ -21,11 +21,11 @@ use raphtory_api::core::{ storage::timeindex::{AsTime, MergedTimeIndex, TimeIndexEntry, TimeIndexOps}, }; use raphtory_storage::graph::{ - edges::{edge_ref::EdgeStorageRef, edge_storage_ops::EdgeStorageOps}, + edges::edge_storage_ops::EdgeStorageOps, nodes::{node_ref::NodeStorageRef, node_storage_ops::NodeStorageOps}, }; use std::{iter, ops::Range}; -use storage::{EdgeAdditions, EdgeDeletions}; +use storage::{EdgeAdditions, EdgeDeletions, EdgeEntryRef}; fn alive_before< 'a, @@ -82,7 +82,7 @@ fn persisted_event< } fn edge_alive_at_end<'graph, G: GraphViewOps<'graph>>( - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, t: i64, view: G, ) -> bool { @@ -91,7 +91,7 @@ fn edge_alive_at_end<'graph, G: GraphViewOps<'graph>>( } fn edge_alive_at_start<'graph, G: GraphViewOps<'graph>>( - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, t: i64, view: G, ) -> bool { @@ -122,7 +122,7 @@ fn node_has_valid_edges<'graph, G: GraphView>( } fn merged_deletions<'a, G: GraphView + 'a>( - e: EdgeStorageRef<'a>, + e: EdgeEntryRef<'a>, view: G, layer: usize, ) -> MergedTimeIndex< @@ -473,7 +473,7 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { fn include_edge( &self, - _edge: EdgeStorageRef, + _edge: EdgeEntryRef, _view: G, _layer_id: usize, ) -> bool { @@ -483,7 +483,7 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { fn include_edge_window( &self, - edge: EdgeStorageRef, + edge: EdgeEntryRef, view: G, layer_id: usize, w: Range, @@ -539,7 +539,7 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { fn edge_history<'graph, G: GraphViewOps<'graph>>( self, - edge: EdgeStorageRef<'graph>, + edge: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, ) -> impl Iterator + Send + Sync + 'graph { @@ -548,7 +548,7 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { fn edge_history_window<'graph, G: GraphViewOps<'graph>>( self, - edge: EdgeStorageRef<'graph>, + edge: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, w: Range, @@ -563,7 +563,7 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { fn edge_exploded_count<'graph, G: GraphViewOps<'graph>>( &self, - edge: EdgeStorageRef, + edge: EdgeEntryRef, view: G, ) -> usize { EventSemantics.edge_exploded_count(edge, view) @@ -571,7 +571,7 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { fn edge_exploded_count_window<'graph, G: GraphViewOps<'graph>>( &self, - edge: EdgeStorageRef, + edge: EdgeEntryRef, view: G, w: Range, ) -> usize { @@ -589,7 +589,7 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { fn edge_exploded<'graph, G: GraphViewOps<'graph>>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, ) -> impl Iterator + Send + Sync + 'graph { @@ -598,7 +598,7 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { fn edge_layers<'graph, G: GraphViewOps<'graph>>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, _view: G, layer_ids: &'graph LayerIds, ) -> impl Iterator + Send + Sync + 'graph { @@ -607,7 +607,7 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { fn edge_window_exploded<'graph, G: GraphViewOps<'graph>>( self, - edge: EdgeStorageRef<'graph>, + edge: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, w: Range, @@ -632,7 +632,7 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { fn edge_window_layers<'graph, G: GraphViewOps<'graph>>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, w: Range, @@ -653,7 +653,7 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { fn edge_earliest_time<'graph, G: GraphViewOps<'graph>>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, ) -> Option { e.filtered_additions_iter(&view, view.layer_ids()) @@ -667,7 +667,7 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { fn edge_earliest_time_window<'graph, G: GraphViewOps<'graph>>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, w: Range, ) -> Option { @@ -691,7 +691,7 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { fn edge_exploded_earliest_time<'graph, G: GraphViewOps<'graph>>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, t: TimeIndexEntry, layer: usize, @@ -701,7 +701,7 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { fn edge_exploded_earliest_time_window<'graph, G: GraphViewOps<'graph>>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, t: TimeIndexEntry, layer: usize, @@ -732,7 +732,7 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { fn edge_latest_time<'graph, G: GraphViewOps<'graph>>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, ) -> Option { e.filtered_additions_iter(&view, view.layer_ids()) @@ -746,7 +746,7 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { fn edge_latest_time_window<'graph, G: GraphViewOps<'graph>>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, w: Range, ) -> Option { @@ -774,7 +774,7 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { fn edge_exploded_latest_time<'graph, G: GraphViewOps<'graph>>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, t: TimeIndexEntry, layer: usize, @@ -792,7 +792,7 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { fn edge_exploded_latest_time_window<'graph, G: GraphViewOps<'graph>>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, t: TimeIndexEntry, layer: usize, @@ -824,7 +824,7 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { fn edge_deletion_history<'graph, G: GraphViewOps<'graph>>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, ) -> impl Iterator + Send + Sync + 'graph { @@ -840,7 +840,7 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { fn edge_deletion_history_window<'graph, G: GraphViewOps<'graph>>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, w: Range, @@ -860,7 +860,7 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { fn edge_is_valid<'graph, G: GraphViewOps<'graph>>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, ) -> bool { edge_alive_at_end(e, i64::MAX, view) @@ -868,7 +868,7 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { fn edge_is_valid_window<'graph, G: GraphViewOps<'graph>>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, r: Range, ) -> bool { @@ -877,7 +877,7 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { fn edge_is_deleted<'graph, G: GraphViewOps<'graph>>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, ) -> bool { !edge_alive_at_end(e, i64::MAX, view) @@ -885,7 +885,7 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { fn edge_is_deleted_window<'graph, G: GraphViewOps<'graph>>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, w: Range, ) -> bool { @@ -894,7 +894,7 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { fn edge_is_active<'graph, G: GraphViewOps<'graph>>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, ) -> bool { e.additions_iter(view.layer_ids()) @@ -905,7 +905,7 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { fn edge_is_active_window<'graph, G: GraphViewOps<'graph>>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, w: Range, ) -> bool { @@ -921,7 +921,7 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { fn edge_is_active_exploded<'graph, G: GraphViewOps<'graph>>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, t: TimeIndexEntry, layer: usize, @@ -931,7 +931,7 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { fn edge_is_active_exploded_window<'graph, G: GraphViewOps<'graph>>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, t: TimeIndexEntry, layer: usize, @@ -943,7 +943,7 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { /// An exploded edge is valid if it is the last exploded view and the edge is not deleted (i.e., there are no additions or deletions for the edge after t in the layer) fn edge_is_valid_exploded<'graph, G: GraphViewOps<'graph>>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, t: TimeIndexEntry, layer: usize, @@ -960,7 +960,7 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { /// (i.e., there are no additions or deletions for the edge after t in the layer in the window) fn edge_is_valid_exploded_window<'graph, G: GraphViewOps<'graph>>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, t: TimeIndexEntry, layer: usize, @@ -975,7 +975,7 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { fn edge_exploded_deletion<'graph, G: GraphViewOps<'graph>>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, t: TimeIndexEntry, layer: usize, @@ -999,7 +999,7 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { fn edge_exploded_deletion_window<'graph, G: GraphViewOps<'graph>>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, t: TimeIndexEntry, layer: usize, @@ -1026,7 +1026,7 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { fn temporal_edge_prop_exploded<'graph, G: GraphViewOps<'graph>>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, prop_id: usize, t: TimeIndexEntry, @@ -1044,7 +1044,7 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { fn temporal_edge_prop_exploded_last_at<'graph, G: GraphViewOps<'graph>>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, edge_time: TimeIndexEntry, layer_id: usize, @@ -1067,7 +1067,7 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { fn temporal_edge_prop_exploded_last_at_window<'graph, G: GraphViewOps<'graph>>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, edge_time: TimeIndexEntry, layer_id: usize, @@ -1084,7 +1084,7 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { fn temporal_edge_prop_last_at<'graph, G: GraphViewOps<'graph>>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, prop_id: usize, t: TimeIndexEntry, @@ -1094,7 +1094,7 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { fn temporal_edge_prop_last_at_window<'graph, G: GraphViewOps<'graph>>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, prop_id: usize, t: TimeIndexEntry, @@ -1123,7 +1123,7 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { fn temporal_edge_prop_hist<'graph, G: GraphViewOps<'graph>>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, prop_id: usize, @@ -1133,7 +1133,7 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { fn temporal_edge_prop_hist_rev<'graph, G: GraphViewOps<'graph>>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, prop_id: usize, @@ -1143,7 +1143,7 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { fn temporal_edge_prop_hist_window<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, prop_id: usize, @@ -1167,7 +1167,7 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { fn temporal_edge_prop_hist_window_rev<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, prop_id: usize, @@ -1188,7 +1188,7 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { fn edge_metadata<'graph, G: GraphViewOps<'graph>>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, prop_id: usize, ) -> Option { @@ -1202,7 +1202,7 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { fn edge_metadata_window<'graph, G: GraphViewOps<'graph>>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, prop_id: usize, w: Range, diff --git a/raphtory/src/db/api/view/internal/time_semantics/time_semantics.rs b/raphtory/src/db/api/view/internal/time_semantics/time_semantics.rs index ce58279863..2812e24e06 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/time_semantics.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/time_semantics.rs @@ -11,8 +11,9 @@ use raphtory_api::core::{ entities::{properties::prop::Prop, LayerIds, ELID}, storage::timeindex::TimeIndexEntry, }; -use raphtory_storage::graph::{edges::edge_ref::EdgeStorageRef, nodes::node_ref::NodeStorageRef}; +use raphtory_storage::graph::{nodes::node_ref::NodeStorageRef}; use std::ops::Range; +use storage::EdgeEntryRef; #[derive(Clone, Debug)] pub enum TimeSemantics { @@ -217,13 +218,13 @@ impl EdgeTimeSemanticsOps for TimeSemantics { for_all!(self, semantics => semantics.handle_edge_update_filter(t, eid, view)) } - fn include_edge(&self, edge: EdgeStorageRef, view: G, layer_id: usize) -> bool { + fn include_edge(&self, edge: EdgeEntryRef, view: G, layer_id: usize) -> bool { for_all!(self, semantics => semantics.include_edge(edge, view, layer_id)) } fn include_edge_window( &self, - edge: EdgeStorageRef, + edge: EdgeEntryRef, view: G, layer_id: usize, w: Range, @@ -247,7 +248,7 @@ impl EdgeTimeSemanticsOps for TimeSemantics { fn edge_history<'graph, G: GraphView + 'graph>( self, - edge: EdgeStorageRef<'graph>, + edge: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, ) -> impl Iterator + Send + Sync + 'graph { @@ -256,7 +257,7 @@ impl EdgeTimeSemanticsOps for TimeSemantics { fn edge_history_window<'graph, G: GraphView + 'graph>( self, - edge: EdgeStorageRef<'graph>, + edge: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, w: Range, @@ -266,7 +267,7 @@ impl EdgeTimeSemanticsOps for TimeSemantics { fn edge_exploded_count<'graph, G: GraphView + 'graph>( &self, - edge: EdgeStorageRef, + edge: EdgeEntryRef, view: G, ) -> usize { for_all!(self, semantics => semantics.edge_exploded_count(edge, view)) @@ -274,7 +275,7 @@ impl EdgeTimeSemanticsOps for TimeSemantics { fn edge_exploded_count_window<'graph, G: GraphView + 'graph>( &self, - edge: EdgeStorageRef, + edge: EdgeEntryRef, view: G, w: Range, ) -> usize { @@ -283,7 +284,7 @@ impl EdgeTimeSemanticsOps for TimeSemantics { fn edge_exploded<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, ) -> impl Iterator + Send + Sync + 'graph { @@ -292,7 +293,7 @@ impl EdgeTimeSemanticsOps for TimeSemantics { fn edge_layers<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, ) -> impl Iterator + Send + Sync + 'graph { @@ -301,7 +302,7 @@ impl EdgeTimeSemanticsOps for TimeSemantics { fn edge_window_exploded<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, w: Range, @@ -311,7 +312,7 @@ impl EdgeTimeSemanticsOps for TimeSemantics { fn edge_window_layers<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, w: Range, @@ -321,7 +322,7 @@ impl EdgeTimeSemanticsOps for TimeSemantics { fn edge_earliest_time<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, ) -> Option { for_all!(self, semantics => semantics.edge_earliest_time(e, view)) @@ -329,7 +330,7 @@ impl EdgeTimeSemanticsOps for TimeSemantics { fn edge_earliest_time_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, w: Range, ) -> Option { @@ -338,7 +339,7 @@ impl EdgeTimeSemanticsOps for TimeSemantics { fn edge_exploded_earliest_time<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, t: TimeIndexEntry, layer: usize, @@ -348,7 +349,7 @@ impl EdgeTimeSemanticsOps for TimeSemantics { fn edge_exploded_earliest_time_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, t: TimeIndexEntry, layer: usize, @@ -359,7 +360,7 @@ impl EdgeTimeSemanticsOps for TimeSemantics { fn edge_latest_time<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, ) -> Option { for_all!(self, semantics => semantics.edge_latest_time(e, view)) @@ -367,7 +368,7 @@ impl EdgeTimeSemanticsOps for TimeSemantics { fn edge_latest_time_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, w: Range, ) -> Option { @@ -376,7 +377,7 @@ impl EdgeTimeSemanticsOps for TimeSemantics { fn edge_exploded_latest_time<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, t: TimeIndexEntry, layer: usize, @@ -386,7 +387,7 @@ impl EdgeTimeSemanticsOps for TimeSemantics { fn edge_exploded_latest_time_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, t: TimeIndexEntry, layer: usize, @@ -397,7 +398,7 @@ impl EdgeTimeSemanticsOps for TimeSemantics { fn edge_deletion_history<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, ) -> impl Iterator + Send + Sync + 'graph { @@ -406,7 +407,7 @@ impl EdgeTimeSemanticsOps for TimeSemantics { fn edge_deletion_history_window<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, w: Range, @@ -416,7 +417,7 @@ impl EdgeTimeSemanticsOps for TimeSemantics { fn edge_is_valid<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, ) -> bool { for_all!(self, semantics => semantics.edge_is_valid(e, view)) @@ -424,7 +425,7 @@ impl EdgeTimeSemanticsOps for TimeSemantics { fn edge_is_valid_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, r: Range, ) -> bool { @@ -433,7 +434,7 @@ impl EdgeTimeSemanticsOps for TimeSemantics { fn edge_is_deleted<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, ) -> bool { for_all!(self, semantics => semantics.edge_is_deleted(e, view)) @@ -441,7 +442,7 @@ impl EdgeTimeSemanticsOps for TimeSemantics { fn edge_is_deleted_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, w: Range, ) -> bool { @@ -450,7 +451,7 @@ impl EdgeTimeSemanticsOps for TimeSemantics { fn edge_is_active<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, ) -> bool { for_all!(self, semantics => semantics.edge_is_active(e, view)) @@ -458,7 +459,7 @@ impl EdgeTimeSemanticsOps for TimeSemantics { fn edge_is_active_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, w: Range, ) -> bool { @@ -467,7 +468,7 @@ impl EdgeTimeSemanticsOps for TimeSemantics { fn edge_is_active_exploded<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, t: TimeIndexEntry, layer: usize, @@ -477,7 +478,7 @@ impl EdgeTimeSemanticsOps for TimeSemantics { fn edge_is_active_exploded_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, t: TimeIndexEntry, layer: usize, @@ -488,7 +489,7 @@ impl EdgeTimeSemanticsOps for TimeSemantics { fn edge_is_valid_exploded<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, t: TimeIndexEntry, layer: usize, @@ -498,7 +499,7 @@ impl EdgeTimeSemanticsOps for TimeSemantics { fn edge_is_valid_exploded_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, t: TimeIndexEntry, layer: usize, @@ -509,7 +510,7 @@ impl EdgeTimeSemanticsOps for TimeSemantics { fn edge_exploded_deletion<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, t: TimeIndexEntry, layer: usize, @@ -519,7 +520,7 @@ impl EdgeTimeSemanticsOps for TimeSemantics { fn edge_exploded_deletion_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, t: TimeIndexEntry, layer: usize, @@ -530,7 +531,7 @@ impl EdgeTimeSemanticsOps for TimeSemantics { fn temporal_edge_prop_exploded<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, prop_id: usize, t: TimeIndexEntry, @@ -541,7 +542,7 @@ impl EdgeTimeSemanticsOps for TimeSemantics { fn temporal_edge_prop_exploded_last_at<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, edge_time: TimeIndexEntry, layer_id: usize, @@ -553,7 +554,7 @@ impl EdgeTimeSemanticsOps for TimeSemantics { fn temporal_edge_prop_exploded_last_at_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, edge_time: TimeIndexEntry, layer_id: usize, @@ -566,7 +567,7 @@ impl EdgeTimeSemanticsOps for TimeSemantics { fn temporal_edge_prop_last_at<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, prop_id: usize, t: TimeIndexEntry, @@ -576,7 +577,7 @@ impl EdgeTimeSemanticsOps for TimeSemantics { fn temporal_edge_prop_last_at_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, prop_id: usize, t: TimeIndexEntry, @@ -587,7 +588,7 @@ impl EdgeTimeSemanticsOps for TimeSemantics { fn temporal_edge_prop_hist<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, prop_id: usize, @@ -597,7 +598,7 @@ impl EdgeTimeSemanticsOps for TimeSemantics { fn temporal_edge_prop_hist_rev<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, prop_id: usize, @@ -607,7 +608,7 @@ impl EdgeTimeSemanticsOps for TimeSemantics { fn temporal_edge_prop_hist_window<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, prop_id: usize, @@ -618,7 +619,7 @@ impl EdgeTimeSemanticsOps for TimeSemantics { fn temporal_edge_prop_hist_window_rev<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, prop_id: usize, @@ -629,7 +630,7 @@ impl EdgeTimeSemanticsOps for TimeSemantics { fn edge_metadata<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, prop_id: usize, ) -> Option { @@ -638,7 +639,7 @@ impl EdgeTimeSemanticsOps for TimeSemantics { fn edge_metadata_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, prop_id: usize, w: Range, diff --git a/raphtory/src/db/api/view/internal/time_semantics/time_semantics_ops.rs b/raphtory/src/db/api/view/internal/time_semantics/time_semantics_ops.rs index ccb3e3c344..e11a5d0c57 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/time_semantics_ops.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/time_semantics_ops.rs @@ -3,8 +3,9 @@ use raphtory_api::core::{ entities::{properties::prop::Prop, LayerIds, ELID}, storage::timeindex::TimeIndexEntry, }; -use raphtory_storage::graph::{edges::edge_ref::EdgeStorageRef, nodes::node_ref::NodeStorageRef}; +use raphtory_storage::graph::{nodes::node_ref::NodeStorageRef}; use std::ops::Range; +use storage::EdgeEntryRef; pub trait NodeTimeSemanticsOps { fn node_earliest_time<'graph, G: GraphView + 'graph>( @@ -142,12 +143,12 @@ pub trait EdgeTimeSemanticsOps { view: G, ) -> Option<(TimeIndexEntry, ELID)>; - fn include_edge(&self, edge: EdgeStorageRef, view: G, layer_id: usize) -> bool; + fn include_edge(&self, edge: EdgeEntryRef, view: G, layer_id: usize) -> bool; /// check if edge `e` should be included in window `w` fn include_edge_window( &self, - edge: EdgeStorageRef, + edge: EdgeEntryRef, view: G, layer_id: usize, w: Range, @@ -172,7 +173,7 @@ pub trait EdgeTimeSemanticsOps { /// An iterator over timestamp and layer pairs fn edge_history<'graph, G: GraphView + 'graph>( self, - edge: EdgeStorageRef<'graph>, + edge: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, ) -> impl Iterator + Send + Sync + 'graph; @@ -184,23 +185,24 @@ pub trait EdgeTimeSemanticsOps { /// An iterator over timestamp and layer pairs fn edge_history_window<'graph, G: GraphView + 'graph>( self, - edge: EdgeStorageRef<'graph>, + edge: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, w: Range, ) -> impl Iterator + Send + Sync + 'graph; + /// The number of exploded edge events for the `edge` fn edge_exploded_count<'graph, G: GraphView + 'graph>( &self, - edge: EdgeStorageRef, + edge: EdgeEntryRef, view: G, ) -> usize; /// The number of exploded edge events for the edge in the window `w` fn edge_exploded_count_window<'graph, G: GraphView + 'graph>( &self, - edge: EdgeStorageRef, + edge: EdgeEntryRef, view: G, w: Range, ) -> usize; @@ -208,7 +210,7 @@ pub trait EdgeTimeSemanticsOps { /// Exploded edge iterator for edge `e` fn edge_exploded<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, ) -> impl Iterator + Send + Sync + 'graph; @@ -216,7 +218,7 @@ pub trait EdgeTimeSemanticsOps { /// Explode edge iterator for edge `e` for every layer fn edge_layers<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, ) -> impl Iterator + Send + Sync + 'graph; @@ -224,7 +226,7 @@ pub trait EdgeTimeSemanticsOps { /// Exploded edge iterator for edge`e` over window `w` fn edge_window_exploded<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, w: Range, @@ -233,7 +235,7 @@ pub trait EdgeTimeSemanticsOps { /// Exploded edge iterator for edge `e` over window `w` for every layer fn edge_window_layers<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, w: Range, @@ -242,21 +244,21 @@ pub trait EdgeTimeSemanticsOps { /// Get the time of the earliest activity of an edge fn edge_earliest_time<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, ) -> Option; /// Get the time of the earliest activity of an edge `e` in window `w` fn edge_earliest_time_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, w: Range, ) -> Option; fn edge_exploded_earliest_time<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, t: TimeIndexEntry, layer: usize, @@ -264,7 +266,7 @@ pub trait EdgeTimeSemanticsOps { fn edge_exploded_earliest_time_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, t: TimeIndexEntry, layer: usize, @@ -274,21 +276,21 @@ pub trait EdgeTimeSemanticsOps { /// Get the time of the latest activity of an edge fn edge_latest_time<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, ) -> Option; /// Get the time of the latest activity of an edge `e` in window `w` fn edge_latest_time_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, w: Range, ) -> Option; fn edge_exploded_latest_time<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, t: TimeIndexEntry, layer: usize, @@ -296,7 +298,7 @@ pub trait EdgeTimeSemanticsOps { fn edge_exploded_latest_time_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, t: TimeIndexEntry, layer: usize, @@ -306,7 +308,7 @@ pub trait EdgeTimeSemanticsOps { /// Get the edge deletions for use with materialize fn edge_deletion_history<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, ) -> impl Iterator + Send + Sync + 'graph; @@ -314,7 +316,7 @@ pub trait EdgeTimeSemanticsOps { /// Get the edge deletions for use with materialize restricted to window `w` fn edge_deletion_history_window<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, w: Range, @@ -323,7 +325,7 @@ pub trait EdgeTimeSemanticsOps { /// Check if edge `e` is currently valid in any layer included in the view fn edge_is_valid<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, ) -> bool; @@ -331,40 +333,40 @@ pub trait EdgeTimeSemanticsOps { /// in any layer included in the view fn edge_is_valid_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, r: Range, ) -> bool; fn edge_is_deleted<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, ) -> bool; fn edge_is_deleted_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, w: Range, ) -> bool; fn edge_is_active<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, ) -> bool; fn edge_is_active_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, w: Range, ) -> bool; fn edge_is_active_exploded<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, t: TimeIndexEntry, layer: usize, @@ -372,7 +374,7 @@ pub trait EdgeTimeSemanticsOps { fn edge_is_active_exploded_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, t: TimeIndexEntry, layer: usize, @@ -381,7 +383,7 @@ pub trait EdgeTimeSemanticsOps { fn edge_is_valid_exploded<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, t: TimeIndexEntry, layer: usize, @@ -389,7 +391,7 @@ pub trait EdgeTimeSemanticsOps { fn edge_is_valid_exploded_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, t: TimeIndexEntry, layer: usize, @@ -398,7 +400,7 @@ pub trait EdgeTimeSemanticsOps { fn edge_exploded_deletion<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, t: TimeIndexEntry, layer: usize, @@ -406,7 +408,7 @@ pub trait EdgeTimeSemanticsOps { fn edge_exploded_deletion_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, t: TimeIndexEntry, layer: usize, @@ -415,7 +417,7 @@ pub trait EdgeTimeSemanticsOps { fn edge_is_deleted_exploded<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, t: TimeIndexEntry, layer: usize, @@ -425,7 +427,7 @@ pub trait EdgeTimeSemanticsOps { fn edge_is_deleted_exploded_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, t: TimeIndexEntry, layer: usize, @@ -438,7 +440,7 @@ pub trait EdgeTimeSemanticsOps { /// Return the value of an edge temporal property at a given point in time and layer if it exists fn temporal_edge_prop_exploded<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, prop_id: usize, t: TimeIndexEntry, @@ -447,7 +449,7 @@ pub trait EdgeTimeSemanticsOps { fn temporal_edge_prop_exploded_last_at<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, edge_time: TimeIndexEntry, layer_id: usize, @@ -457,7 +459,7 @@ pub trait EdgeTimeSemanticsOps { fn temporal_edge_prop_exploded_last_at_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, edge_time: TimeIndexEntry, layer_id: usize, @@ -469,7 +471,7 @@ pub trait EdgeTimeSemanticsOps { /// Return the last value of a temporal edge property at or before a given point in time fn temporal_edge_prop_last_at<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, prop_id: usize, t: TimeIndexEntry, @@ -477,7 +479,7 @@ pub trait EdgeTimeSemanticsOps { fn temporal_edge_prop_last_at_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, prop_id: usize, t: TimeIndexEntry, @@ -489,7 +491,7 @@ pub trait EdgeTimeSemanticsOps { /// Items are (timestamp, layer_id, property value) fn temporal_edge_prop_hist<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, prop_id: usize, @@ -500,7 +502,7 @@ pub trait EdgeTimeSemanticsOps { /// Items are (timestamp, layer_id, property value) fn temporal_edge_prop_hist_rev<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, prop_id: usize, @@ -519,7 +521,7 @@ pub trait EdgeTimeSemanticsOps { /// Items are (timestamp, layer_id, property value) fn temporal_edge_prop_hist_window<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, prop_id: usize, @@ -531,7 +533,7 @@ pub trait EdgeTimeSemanticsOps { /// Items are (timestamp, layer_id, property value) fn temporal_edge_prop_hist_window_rev<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, prop_id: usize, @@ -541,7 +543,7 @@ pub trait EdgeTimeSemanticsOps { /// Get metadata edge property fn edge_metadata<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, prop_id: usize, ) -> Option; @@ -551,7 +553,7 @@ pub trait EdgeTimeSemanticsOps { /// Should only return the property for a layer if the edge exists in the window in that layer fn edge_metadata_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, prop_id: usize, w: Range, diff --git a/raphtory/src/db/api/view/internal/time_semantics/window_time_semantics.rs b/raphtory/src/db/api/view/internal/time_semantics/window_time_semantics.rs index 096fb62470..8d7ee75930 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/window_time_semantics.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/window_time_semantics.rs @@ -8,8 +8,9 @@ use raphtory_api::core::{ entities::{properties::prop::Prop, LayerIds, ELID}, storage::timeindex::TimeIndexEntry, }; -use raphtory_storage::graph::{edges::edge_ref::EdgeStorageRef, nodes::node_ref::NodeStorageRef}; +use raphtory_storage::graph::{nodes::node_ref::NodeStorageRef}; use std::ops::Range; +use storage::EdgeEntryRef; #[derive(Clone, Debug)] pub struct WindowTimeSemantics { @@ -231,7 +232,7 @@ impl EdgeTimeSemanticsOps for WindowTimeSemantics { self.semantics.handle_edge_update_filter(t, eid, view) } - fn include_edge(&self, edge: EdgeStorageRef, view: G, layer_id: usize) -> bool { + fn include_edge(&self, edge: EdgeEntryRef, view: G, layer_id: usize) -> bool { self.semantics .include_edge_window(edge, view, layer_id, self.window.clone()) } @@ -239,7 +240,7 @@ impl EdgeTimeSemanticsOps for WindowTimeSemantics { #[inline] fn include_edge_window( &self, - edge: EdgeStorageRef, + edge: EdgeEntryRef, view: G, layer_id: usize, w: Range, @@ -266,7 +267,7 @@ impl EdgeTimeSemanticsOps for WindowTimeSemantics { #[inline] fn edge_history<'graph, G: GraphView + 'graph>( self, - edge: EdgeStorageRef<'graph>, + edge: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, ) -> impl Iterator + Send + Sync + 'graph { @@ -277,7 +278,7 @@ impl EdgeTimeSemanticsOps for WindowTimeSemantics { #[inline] fn edge_history_window<'graph, G: GraphView + 'graph>( self, - edge: EdgeStorageRef<'graph>, + edge: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, w: Range, @@ -288,7 +289,7 @@ impl EdgeTimeSemanticsOps for WindowTimeSemantics { #[inline] fn edge_exploded_count<'graph, G: GraphView + 'graph>( &self, - edge: EdgeStorageRef, + edge: EdgeEntryRef, view: G, ) -> usize { self.semantics @@ -298,7 +299,7 @@ impl EdgeTimeSemanticsOps for WindowTimeSemantics { #[inline] fn edge_exploded_count_window<'graph, G: GraphView + 'graph>( &self, - edge: EdgeStorageRef, + edge: EdgeEntryRef, view: G, w: Range, ) -> usize { @@ -308,7 +309,7 @@ impl EdgeTimeSemanticsOps for WindowTimeSemantics { #[inline] fn edge_exploded<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, ) -> impl Iterator + Send + Sync + 'graph { @@ -319,7 +320,7 @@ impl EdgeTimeSemanticsOps for WindowTimeSemantics { #[inline] fn edge_layers<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, ) -> impl Iterator + Send + Sync + 'graph { @@ -330,7 +331,7 @@ impl EdgeTimeSemanticsOps for WindowTimeSemantics { #[inline] fn edge_window_exploded<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, w: Range, @@ -341,7 +342,7 @@ impl EdgeTimeSemanticsOps for WindowTimeSemantics { #[inline] fn edge_window_layers<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, w: Range, @@ -352,7 +353,7 @@ impl EdgeTimeSemanticsOps for WindowTimeSemantics { #[inline] fn edge_earliest_time<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, ) -> Option { self.semantics @@ -362,7 +363,7 @@ impl EdgeTimeSemanticsOps for WindowTimeSemantics { #[inline] fn edge_earliest_time_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, w: Range, ) -> Option { @@ -372,7 +373,7 @@ impl EdgeTimeSemanticsOps for WindowTimeSemantics { #[inline] fn edge_exploded_earliest_time<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, t: TimeIndexEntry, layer: usize, @@ -384,7 +385,7 @@ impl EdgeTimeSemanticsOps for WindowTimeSemantics { #[inline] fn edge_exploded_earliest_time_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, t: TimeIndexEntry, layer: usize, @@ -397,7 +398,7 @@ impl EdgeTimeSemanticsOps for WindowTimeSemantics { #[inline] fn edge_latest_time<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, ) -> Option { self.semantics @@ -407,7 +408,7 @@ impl EdgeTimeSemanticsOps for WindowTimeSemantics { #[inline] fn edge_latest_time_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, w: Range, ) -> Option { @@ -417,7 +418,7 @@ impl EdgeTimeSemanticsOps for WindowTimeSemantics { #[inline] fn edge_exploded_latest_time<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, t: TimeIndexEntry, layer: usize, @@ -429,7 +430,7 @@ impl EdgeTimeSemanticsOps for WindowTimeSemantics { #[inline] fn edge_exploded_latest_time_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef, + e: EdgeEntryRef, view: G, t: TimeIndexEntry, layer: usize, @@ -442,7 +443,7 @@ impl EdgeTimeSemanticsOps for WindowTimeSemantics { #[inline] fn edge_deletion_history<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, ) -> impl Iterator + Send + Sync + 'graph { @@ -453,7 +454,7 @@ impl EdgeTimeSemanticsOps for WindowTimeSemantics { #[inline] fn edge_deletion_history_window<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, w: Range, @@ -465,7 +466,7 @@ impl EdgeTimeSemanticsOps for WindowTimeSemantics { #[inline] fn edge_is_valid<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, ) -> bool { self.semantics @@ -475,7 +476,7 @@ impl EdgeTimeSemanticsOps for WindowTimeSemantics { #[inline] fn edge_is_valid_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, r: Range, ) -> bool { @@ -485,7 +486,7 @@ impl EdgeTimeSemanticsOps for WindowTimeSemantics { #[inline] fn edge_is_deleted<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, ) -> bool { self.semantics @@ -495,7 +496,7 @@ impl EdgeTimeSemanticsOps for WindowTimeSemantics { #[inline] fn edge_is_deleted_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, w: Range, ) -> bool { @@ -505,7 +506,7 @@ impl EdgeTimeSemanticsOps for WindowTimeSemantics { #[inline] fn edge_is_active<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, ) -> bool { self.semantics @@ -515,7 +516,7 @@ impl EdgeTimeSemanticsOps for WindowTimeSemantics { #[inline] fn edge_is_active_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, w: Range, ) -> bool { @@ -525,7 +526,7 @@ impl EdgeTimeSemanticsOps for WindowTimeSemantics { #[inline] fn edge_is_active_exploded<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, t: TimeIndexEntry, layer: usize, @@ -537,7 +538,7 @@ impl EdgeTimeSemanticsOps for WindowTimeSemantics { #[inline] fn edge_is_active_exploded_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, t: TimeIndexEntry, layer: usize, @@ -550,7 +551,7 @@ impl EdgeTimeSemanticsOps for WindowTimeSemantics { #[inline] fn edge_is_valid_exploded<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, t: TimeIndexEntry, layer: usize, @@ -562,7 +563,7 @@ impl EdgeTimeSemanticsOps for WindowTimeSemantics { #[inline] fn edge_is_valid_exploded_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, t: TimeIndexEntry, layer: usize, @@ -575,7 +576,7 @@ impl EdgeTimeSemanticsOps for WindowTimeSemantics { #[inline] fn edge_exploded_deletion<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, t: TimeIndexEntry, layer: usize, @@ -587,7 +588,7 @@ impl EdgeTimeSemanticsOps for WindowTimeSemantics { #[inline] fn edge_exploded_deletion_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, t: TimeIndexEntry, layer: usize, @@ -600,7 +601,7 @@ impl EdgeTimeSemanticsOps for WindowTimeSemantics { #[inline] fn temporal_edge_prop_exploded<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, prop_id: usize, t: TimeIndexEntry, @@ -613,7 +614,7 @@ impl EdgeTimeSemanticsOps for WindowTimeSemantics { #[inline] fn temporal_edge_prop_exploded_last_at<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, edge_time: TimeIndexEntry, layer_id: usize, @@ -634,7 +635,7 @@ impl EdgeTimeSemanticsOps for WindowTimeSemantics { #[inline] fn temporal_edge_prop_exploded_last_at_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, edge_time: TimeIndexEntry, layer_id: usize, @@ -650,7 +651,7 @@ impl EdgeTimeSemanticsOps for WindowTimeSemantics { #[inline] fn temporal_edge_prop_last_at<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, prop_id: usize, t: TimeIndexEntry, @@ -662,7 +663,7 @@ impl EdgeTimeSemanticsOps for WindowTimeSemantics { #[inline] fn temporal_edge_prop_last_at_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, prop_id: usize, t: TimeIndexEntry, @@ -675,7 +676,7 @@ impl EdgeTimeSemanticsOps for WindowTimeSemantics { #[inline] fn temporal_edge_prop_hist<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, prop_id: usize, @@ -687,7 +688,7 @@ impl EdgeTimeSemanticsOps for WindowTimeSemantics { #[inline] fn temporal_edge_prop_hist_rev<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, prop_id: usize, @@ -699,7 +700,7 @@ impl EdgeTimeSemanticsOps for WindowTimeSemantics { #[inline] fn temporal_edge_prop_hist_window<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, prop_id: usize, @@ -712,7 +713,7 @@ impl EdgeTimeSemanticsOps for WindowTimeSemantics { #[inline] fn temporal_edge_prop_hist_window_rev<'graph, G: GraphView + 'graph>( self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, layer_ids: &'graph LayerIds, prop_id: usize, @@ -725,7 +726,7 @@ impl EdgeTimeSemanticsOps for WindowTimeSemantics { #[inline] fn edge_metadata<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, prop_id: usize, ) -> Option { @@ -736,7 +737,7 @@ impl EdgeTimeSemanticsOps for WindowTimeSemantics { #[inline] fn edge_metadata_window<'graph, G: GraphView + 'graph>( &self, - e: EdgeStorageRef<'graph>, + e: EdgeEntryRef<'graph>, view: G, prop_id: usize, w: Range, diff --git a/raphtory/src/db/graph/views/cached_view.rs b/raphtory/src/db/graph/views/cached_view.rs index b3bda46554..54ce914f7a 100644 --- a/raphtory/src/db/graph/views/cached_view.rs +++ b/raphtory/src/db/graph/views/cached_view.rs @@ -22,7 +22,7 @@ use raphtory_api::{ use raphtory_storage::{ core_ops::CoreGraphOps, graph::{ - edges::{edge_ref::EdgeStorageRef, edge_storage_ops::EdgeStorageOps}, + edges::{edge_storage_ops::EdgeStorageOps}, nodes::{node_ref::NodeStorageRef, node_storage_ops::NodeStorageOps}, }, }; @@ -32,6 +32,7 @@ use std::{ fmt::{Debug, Formatter}, sync::Arc, }; +use storage::EdgeEntryRef; #[derive(Clone)] pub struct CachedView { @@ -194,7 +195,7 @@ impl<'graph, G: GraphViewOps<'graph>> InternalEdgeLayerFilterOps for CachedView< self.graph.internal_layer_filter_edge_list_trusted() } - fn internal_filter_edge_layer(&self, edge: EdgeStorageRef, layer: usize) -> bool { + fn internal_filter_edge_layer(&self, edge: EdgeEntryRef, layer: usize) -> bool { self.layered_mask .get(layer) .is_some_and(|(_, edge_filter, _)| edge_filter.contains(edge.eid().as_u64())) @@ -216,7 +217,7 @@ impl<'graph, G: GraphViewOps<'graph>> InternalEdgeFilterOps for CachedView { } #[inline] - fn internal_filter_edge(&self, edge: EdgeStorageRef, layer_ids: &LayerIds) -> bool { + fn internal_filter_edge(&self, edge: EdgeEntryRef, layer_ids: &LayerIds) -> bool { let filter_fn = |(_, edges, _): &(RoaringTreemap, RoaringTreemap, Option)| { edges.contains(edge.eid().as_u64()) diff --git a/raphtory/src/db/graph/views/filter/edge_and_filtered_graph.rs b/raphtory/src/db/graph/views/filter/edge_and_filtered_graph.rs index 497324692c..e8802ce1d1 100644 --- a/raphtory/src/db/graph/views/filter/edge_and_filtered_graph.rs +++ b/raphtory/src/db/graph/views/filter/edge_and_filtered_graph.rs @@ -24,8 +24,9 @@ use raphtory_api::{ }, inherit::Base, }; -use raphtory_storage::{core_ops::InheritCoreGraphOps, graph::edges::edge_ref::EdgeStorageRef}; +use raphtory_storage::{core_ops::InheritCoreGraphOps}; use std::ops::Range; +use storage::EdgeEntryRef; #[derive(Debug, Clone)] pub struct EdgeAndFilteredGraph { @@ -217,7 +218,7 @@ impl InternalEd && self.right.internal_layer_filter_edge_list_trusted() } - fn internal_filter_edge_layer(&self, edge: EdgeStorageRef, layer: usize) -> bool { + fn internal_filter_edge_layer(&self, edge: EdgeEntryRef, layer: usize) -> bool { self.left.internal_filter_edge_layer(edge, layer) && self.right.internal_filter_edge_layer(edge, layer) } @@ -260,7 +261,7 @@ impl InternalEdgeFilterOp } #[inline] - fn internal_filter_edge(&self, edge: EdgeStorageRef, layer_ids: &LayerIds) -> bool { + fn internal_filter_edge(&self, edge: EdgeEntryRef, layer_ids: &LayerIds) -> bool { self.left.internal_filter_edge(edge, layer_ids) && self.right.internal_filter_edge(edge, layer_ids) } diff --git a/raphtory/src/db/graph/views/filter/edge_field_filtered_graph.rs b/raphtory/src/db/graph/views/filter/edge_field_filtered_graph.rs index e84d7a7e78..b91f352d98 100644 --- a/raphtory/src/db/graph/views/filter/edge_field_filtered_graph.rs +++ b/raphtory/src/db/graph/views/filter/edge_field_filtered_graph.rs @@ -1,3 +1,4 @@ +use raphtory_storage::graph::edges::edge_ref::EdgeEntryRef; use crate::{ db::{ api::{ @@ -15,7 +16,7 @@ use crate::{ prelude::GraphViewOps, }; use raphtory_api::{core::entities::LayerIds, inherit::Base}; -use raphtory_storage::{core_ops::InheritCoreGraphOps, graph::edges::edge_ref::EdgeStorageRef}; +use raphtory_storage::{core_ops::InheritCoreGraphOps}; #[derive(Debug, Clone)] pub struct EdgeFieldFilteredGraph { @@ -78,7 +79,7 @@ impl<'graph, G: GraphViewOps<'graph>> InternalEdgeFilterOps for EdgeFieldFiltere } #[inline] - fn internal_filter_edge(&self, edge: EdgeStorageRef, layer_ids: &LayerIds) -> bool { + fn internal_filter_edge(&self, edge: EdgeEntryRef, layer_ids: &LayerIds) -> bool { if self.graph.internal_filter_edge(edge, layer_ids) { self.filter.matches_edge(&self.graph, edge) } else { diff --git a/raphtory/src/db/graph/views/filter/edge_not_filtered_graph.rs b/raphtory/src/db/graph/views/filter/edge_not_filtered_graph.rs index e6e17c2c58..bc46c31d67 100644 --- a/raphtory/src/db/graph/views/filter/edge_not_filtered_graph.rs +++ b/raphtory/src/db/graph/views/filter/edge_not_filtered_graph.rs @@ -24,7 +24,7 @@ use raphtory_api::{ }, inherit::Base, }; -use raphtory_storage::{core_ops::InheritCoreGraphOps, graph::edges::edge_ref::EdgeStorageRef}; +use raphtory_storage::{core_ops::InheritCoreGraphOps, graph::edges::edge_ref::EdgeEntryRef}; #[derive(Debug, Clone)] pub struct EdgeNotFilteredGraph { @@ -95,7 +95,7 @@ impl<'graph, G: GraphViewOps<'graph>, T: FilterOps> InternalEdgeLayerFilterOps false } - fn internal_filter_edge_layer(&self, edge: EdgeStorageRef, layer: usize) -> bool { + fn internal_filter_edge_layer(&self, edge: EdgeEntryRef, layer: usize) -> bool { self.graph.filter_edge_layer(edge, layer) && !self.filter.filter_edge_layer(edge, layer) } } @@ -135,7 +135,7 @@ impl<'graph, G: GraphViewOps<'graph>, T: FilterOps> InternalEdgeFilterOps } #[inline] - fn internal_filter_edge(&self, edge: EdgeStorageRef, _layer_ids: &LayerIds) -> bool { + fn internal_filter_edge(&self, edge: EdgeEntryRef, _layer_ids: &LayerIds) -> bool { self.graph.filter_edge(edge) && !self.filter.filter_edge(edge) } } diff --git a/raphtory/src/db/graph/views/filter/edge_or_filtered_graph.rs b/raphtory/src/db/graph/views/filter/edge_or_filtered_graph.rs index c5b78f9ed1..2c16d1e100 100644 --- a/raphtory/src/db/graph/views/filter/edge_or_filtered_graph.rs +++ b/raphtory/src/db/graph/views/filter/edge_or_filtered_graph.rs @@ -24,8 +24,9 @@ use raphtory_api::{ }, inherit::Base, }; -use raphtory_storage::{core_ops::InheritCoreGraphOps, graph::edges::edge_ref::EdgeStorageRef}; +use raphtory_storage::{core_ops::InheritCoreGraphOps}; use std::ops::Range; +use storage::EdgeEntryRef; #[derive(Debug, Clone)] pub struct EdgeOrFilteredGraph { @@ -179,7 +180,7 @@ impl InternalEd && self.right.internal_layer_filter_edge_list_trusted() } - fn internal_filter_edge_layer(&self, edge: EdgeStorageRef, layer: usize) -> bool { + fn internal_filter_edge_layer(&self, edge: EdgeEntryRef, layer: usize) -> bool { self.left.internal_filter_edge_layer(edge, layer) || self.right.internal_filter_edge_layer(edge, layer) } @@ -222,7 +223,7 @@ impl InternalEdgeFilterOp } #[inline] - fn internal_filter_edge(&self, edge: EdgeStorageRef, layer_ids: &LayerIds) -> bool { + fn internal_filter_edge(&self, edge: EdgeEntryRef, layer_ids: &LayerIds) -> bool { self.left.internal_filter_edge(edge, layer_ids) || self.right.internal_filter_edge(edge, layer_ids) } diff --git a/raphtory/src/db/graph/views/filter/edge_property_filtered_graph.rs b/raphtory/src/db/graph/views/filter/edge_property_filtered_graph.rs index 2c34d44d28..5ea0d120cd 100644 --- a/raphtory/src/db/graph/views/filter/edge_property_filtered_graph.rs +++ b/raphtory/src/db/graph/views/filter/edge_property_filtered_graph.rs @@ -16,7 +16,8 @@ use crate::{ prelude::{GraphViewOps, LayerOps}, }; use raphtory_api::inherit::Base; -use raphtory_storage::{core_ops::InheritCoreGraphOps, graph::edges::edge_ref::EdgeStorageRef}; +use raphtory_storage::{core_ops::InheritCoreGraphOps}; +use storage::EdgeEntryRef; #[derive(Debug, Clone)] pub struct EdgePropertyFilteredGraph { @@ -88,7 +89,7 @@ impl<'graph, G: GraphViewOps<'graph>> InternalEdgeFilterOps for EdgePropertyFilt } #[inline] - fn internal_filter_edge(&self, edge: EdgeStorageRef, layer_ids: &LayerIds) -> bool { + fn internal_filter_edge(&self, edge: EdgeEntryRef, layer_ids: &LayerIds) -> bool { if self.graph.internal_filter_edge(edge, layer_ids) { self.filter.matches_edge(&self.graph, self.prop_id, edge) } else { diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index b65195cf35..aa9afe6807 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -11,8 +11,9 @@ use crate::{ prelude::{GraphViewOps, NodeViewOps}, }; use raphtory_api::core::entities::properties::prop::Prop; -use raphtory_storage::graph::edges::{edge_ref::EdgeStorageRef, edge_storage_ops::EdgeStorageOps}; +use raphtory_storage::graph::edges::{edge_storage_ops::EdgeStorageOps}; use std::{collections::HashSet, fmt, fmt::Display, ops::Deref, sync::Arc}; +use storage::EdgeEntryRef; pub mod edge_filter; pub mod filter_operator; @@ -130,7 +131,7 @@ impl Filter { pub fn matches_edge<'graph, G: GraphViewOps<'graph>>( &self, graph: &G, - edge: EdgeStorageRef, + edge: EdgeEntryRef, ) -> bool { match self.field_name.as_str() { "src" => self.matches(graph.node(edge.src()).map(|n| n.name()).as_deref()), diff --git a/raphtory/src/db/graph/views/filter/model/property_filter.rs b/raphtory/src/db/graph/views/filter/model/property_filter.rs index d3bd37fd6f..06db4747ad 100644 --- a/raphtory/src/db/graph/views/filter/model/property_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/property_filter.rs @@ -23,10 +23,11 @@ use raphtory_api::core::{ storage::{arc_str::ArcStr, timeindex::TimeIndexEntry}, }; use raphtory_storage::graph::{ - edges::{edge_ref::EdgeStorageRef, edge_storage_ops::EdgeStorageOps}, + edges::{edge_storage_ops::EdgeStorageOps}, nodes::{node_ref::NodeStorageRef, node_storage_ops::NodeStorageOps}, }; use std::{collections::HashSet, fmt, fmt::Display, sync::Arc}; +use storage::EdgeEntryRef; #[derive(Debug, Clone, PartialEq, Eq)] pub enum Temporal { @@ -378,7 +379,7 @@ impl PropertyFilter { &self, graph: &G, prop_id: Option, - edge: EdgeStorageRef, + edge: EdgeEntryRef, ) -> bool { let edge = EdgeView::new(graph, edge.out_ref()); match self.prop_ref { diff --git a/raphtory/src/db/graph/views/layer_graph.rs b/raphtory/src/db/graph/views/layer_graph.rs index 3eab0567a4..c7fe3ed5f5 100644 --- a/raphtory/src/db/graph/views/layer_graph.rs +++ b/raphtory/src/db/graph/views/layer_graph.rs @@ -12,7 +12,7 @@ use crate::{ prelude::GraphViewOps, }; use raphtory_api::inherit::Base; -use raphtory_storage::{core_ops::InheritCoreGraphOps, graph::edges::edge_ref::EdgeStorageRef}; +use raphtory_storage::{core_ops::InheritCoreGraphOps, graph::edges::edge_ref::EdgeEntryRef}; use std::fmt::{Debug, Formatter}; #[derive(Clone)] @@ -82,7 +82,7 @@ impl InternalEdgeLayerFilterOps for LayeredGraph { matches!(self.layers, LayerIds::All) && self.graph.internal_layer_filter_edge_list_trusted() } - fn internal_filter_edge_layer(&self, edge: EdgeStorageRef, layer: usize) -> bool { + fn internal_filter_edge_layer(&self, edge: EdgeEntryRef, layer: usize) -> bool { self.graph.internal_filter_edge_layer(edge, layer) // actual layer filter handled upstream for optimisation } } diff --git a/raphtory/src/db/graph/views/node_subgraph.rs b/raphtory/src/db/graph/views/node_subgraph.rs index 38d00e364c..473c634772 100644 --- a/raphtory/src/db/graph/views/node_subgraph.rs +++ b/raphtory/src/db/graph/views/node_subgraph.rs @@ -19,7 +19,7 @@ use raphtory_api::{ use raphtory_storage::{ core_ops::{CoreGraphOps, InheritCoreGraphOps}, graph::{ - edges::{edge_ref::EdgeStorageRef, edge_storage_ops::EdgeStorageOps}, + edges::{edge_ref::EdgeEntryRef, edge_storage_ops::EdgeStorageOps}, nodes::{node_ref::NodeStorageRef, node_storage_ops::NodeStorageOps}, }, }; @@ -120,7 +120,7 @@ impl<'graph, G: GraphViewOps<'graph>> InternalEdgeLayerFilterOps for NodeSubgrap false } - fn internal_filter_edge_layer(&self, edge: EdgeStorageRef, layer: usize) -> bool { + fn internal_filter_edge_layer(&self, edge: EdgeEntryRef, layer: usize) -> bool { self.graph.internal_filter_edge_layer(edge, layer) } @@ -153,7 +153,7 @@ impl<'graph, G: GraphViewOps<'graph>> InternalEdgeFilterOps for NodeSubgraph } #[inline] - fn internal_filter_edge(&self, edge: EdgeStorageRef, layer_ids: &LayerIds) -> bool { + fn internal_filter_edge(&self, edge: EdgeEntryRef, layer_ids: &LayerIds) -> bool { self.nodes.contains(&edge.src()) && self.nodes.contains(&edge.dst()) && self.graph.internal_filter_edge(edge, layer_ids) diff --git a/raphtory/src/db/graph/views/valid_graph.rs b/raphtory/src/db/graph/views/valid_graph.rs index b0b6d98d6b..532b2cb8ea 100644 --- a/raphtory/src/db/graph/views/valid_graph.rs +++ b/raphtory/src/db/graph/views/valid_graph.rs @@ -14,7 +14,7 @@ use crate::{ prelude::GraphViewOps, }; use raphtory_api::{core::entities::LayerIds, inherit::Base}; -use raphtory_storage::{core_ops::InheritCoreGraphOps, graph::edges::edge_ref::EdgeStorageRef}; +use raphtory_storage::{core_ops::InheritCoreGraphOps, graph::edges::edge_ref::EdgeEntryRef}; #[derive(Copy, Clone, Debug)] pub struct ValidGraph { @@ -63,7 +63,7 @@ impl<'graph, G: GraphViewOps<'graph>> InternalEdgeLayerFilterOps for ValidGraph< false } - fn internal_filter_edge_layer(&self, edge: EdgeStorageRef, layer: usize) -> bool { + fn internal_filter_edge_layer(&self, edge: EdgeEntryRef, layer: usize) -> bool { let time_semantics = self.graph.edge_time_semantics(); time_semantics.edge_is_valid(edge, LayeredGraph::new(&self.graph, LayerIds::One(layer))) && self.graph.internal_filter_edge_layer(edge, layer) diff --git a/raphtory/src/db/graph/views/window_graph.rs b/raphtory/src/db/graph/views/window_graph.rs index c17ca5a428..414f86f17c 100644 --- a/raphtory/src/db/graph/views/window_graph.rs +++ b/raphtory/src/db/graph/views/window_graph.rs @@ -74,7 +74,7 @@ use raphtory_api::{ }; use raphtory_storage::{ core_ops::{CoreGraphOps, InheritCoreGraphOps}, - graph::{edges::edge_ref::EdgeStorageRef, nodes::node_ref::NodeStorageRef}, + graph::{edges::edge_ref::EdgeEntryRef, nodes::node_ref::NodeStorageRef}, }; use std::{ fmt::{Debug, Formatter}, @@ -517,7 +517,7 @@ impl InternalEdgeFilterOps for WindowedGraph { || (!self.window_is_bounding() && self.graph.internal_edge_list_trusted()) } - fn internal_filter_edge(&self, edge: EdgeStorageRef, layer_ids: &LayerIds) -> bool { + fn internal_filter_edge(&self, edge: EdgeEntryRef, layer_ids: &LayerIds) -> bool { self.graph.internal_filter_edge(edge, layer_ids) } @@ -535,7 +535,7 @@ impl InternalEdgeLayerFilterOps for WindowedGraph { || (!self.window_is_bounding() && self.graph.internal_layer_filter_edge_list_trusted()) } - fn internal_filter_edge_layer(&self, edge: EdgeStorageRef, layer: usize) -> bool { + fn internal_filter_edge_layer(&self, edge: EdgeEntryRef, layer: usize) -> bool { self.graph.internal_filter_edge_layer(edge, layer) } From 6b7077902b372d6417301db2106cb229cd82dae6 Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Tue, 19 Aug 2025 17:10:25 +0200 Subject: [PATCH 124/321] keep TimeIndexEntry consistent for persisted property updates --- .../time_semantics/persistent_semantics.rs | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/raphtory/src/db/api/view/internal/time_semantics/persistent_semantics.rs b/raphtory/src/db/api/view/internal/time_semantics/persistent_semantics.rs index be8c188871..fcaa7f6571 100644 --- a/raphtory/src/db/api/view/internal/time_semantics/persistent_semantics.rs +++ b/raphtory/src/db/api/view/internal/time_semantics/persistent_semantics.rs @@ -152,12 +152,11 @@ fn persisted_prop_value_at<'a, 'b>( t: i64, props: impl TPropOps<'a>, deletions: impl TimeIndexOps<'b, IndexType = TimeIndexEntry>, -) -> Option<(TimeIndexEntry, Prop)> { +) -> Option { if props.active_t(t..t.saturating_add(1)) || deletions.active_t(t..t.saturating_add(1)) { None } else { - last_prop_value_before(TimeIndexEntry::start(t), props, deletions) - .map(|(update_t, v)| (TimeIndexEntry(t, update_t.i()), v)) + last_prop_value_before(TimeIndexEntry::start(t), props, deletions).map(|(_, v)| v) } } @@ -471,12 +470,7 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { } } - fn include_edge( - &self, - _edge: EdgeEntryRef, - _view: G, - _layer_id: usize, - ) -> bool { + fn include_edge(&self, _edge: EdgeEntryRef, _view: G, _layer_id: usize) -> bool { // history filtering only maps additions to deletions and thus doesn't filter edges true } @@ -1151,14 +1145,17 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { ) -> impl Iterator + Send + Sync + 'graph { e.filtered_temporal_prop_iter(prop_id, view.clone(), layer_ids) .map(|(layer, props)| { - let deletions = e - .filtered_deletions(layer, &view) - .merge(e.filtered_additions(layer, &view).invert()); - let first_prop = persisted_prop_value_at(w.start, props.clone(), &deletions) - .map(|(t, v)| (t, layer, v)); + let additions = e.filtered_additions(layer, &view); + let deletions = e.filtered_deletions(layer, &view); + let merged_deletions = deletions.clone().merge(additions.clone().invert()); + let persisted_ts = persisted_event(additions, deletions, w.start); + let first_prop = persisted_ts.and_then(|ts| { + persisted_prop_value_at(w.start, props.clone(), &merged_deletions) + .map(|v| (TimeIndexEntry(w.start, ts.i()), layer, v)) + }); first_prop.into_iter().chain( props - .iter_window(interior_window(w.clone(), &deletions)) + .iter_window(interior_window(w.clone(), &merged_deletions)) .map(move |(t, v)| (t, layer, v)), ) }) @@ -1175,11 +1172,16 @@ impl EdgeTimeSemanticsOps for PersistentSemantics { ) -> impl Iterator + Send + Sync + 'graph { e.filtered_temporal_prop_iter(prop_id, view.clone(), layer_ids) .map(|(layer, props)| { - let deletions = merged_deletions(e, &view, layer); - let first_prop = persisted_prop_value_at(w.start, props.clone(), &deletions) - .map(|(t, v)| (t, layer, v)); + let additions = e.filtered_additions(layer, &view); + let deletions = e.filtered_deletions(layer, &view); + let merged_deletions = deletions.clone().merge(additions.clone().invert()); + let persisted_ts = persisted_event(additions, deletions, w.start); + let first_prop = persisted_ts.and_then(|ts| { + persisted_prop_value_at(w.start, props.clone(), &merged_deletions) + .map(|v| (TimeIndexEntry(w.start, ts.i()), layer, v)) + }); props - .iter_inner_rev(Some(interior_window(w.clone(), &deletions))) + .iter_inner_rev(Some(interior_window(w.clone(), &merged_deletions))) .map(move |(t, v)| (t, layer, v)) .chain(first_prop.into_iter()) }) From e76da90cc94470507c9bcf015af980445397eb09 Mon Sep 17 00:00:00 2001 From: Lucas Jeub Date: Wed, 20 Aug 2025 10:41:03 +0200 Subject: [PATCH 125/321] fix the "confusing lifetime" warnings --- db4-storage/src/api/edges.rs | 6 +- db4-storage/src/api/nodes.rs | 4 +- db4-storage/src/pages/flush_thread.rs | 13 +-- db4-storage/src/pages/mod.rs | 34 ++++---- db4-storage/src/properties/mod.rs | 4 +- db4-storage/src/segments/edge.rs | 6 +- db4-storage/src/segments/mod.rs | 8 +- db4-storage/src/segments/node.rs | 6 +- .../src/core/entities/properties/meta.rs | 24 +++--- .../src/entities/properties/graph_meta.rs | 2 +- .../src/entities/properties/props.rs | 6 +- raphtory-core/src/storage/lazy_vec.rs | 8 +- raphtory-core/src/storage/mod.rs | 20 ++--- raphtory-core/src/storage/timeindex.rs | 6 +- raphtory-graphql/src/python/pymodule.rs | 2 +- raphtory-storage/src/core_ops.rs | 6 +- .../src/graph/edges/edge_entry.rs | 4 +- raphtory-storage/src/graph/edges/edges.rs | 4 +- raphtory-storage/src/graph/graph.rs | 6 +- .../src/graph/nodes/node_entry.rs | 2 +- raphtory-storage/src/graph/nodes/nodes.rs | 4 +- raphtory-storage/src/mutation/addition_ops.rs | 6 +- .../src/mutation/addition_ops_ext.rs | 2 +- .../src/mutation/property_addition_ops.rs | 32 +++---- raphtory/src/db/api/properties/internal.rs | 46 +++++----- .../src/db/api/properties/temporal_props.rs | 6 +- raphtory/src/db/api/state/lazy_node_state.rs | 5 +- raphtory/src/db/api/state/node_state.rs | 5 +- raphtory/src/db/api/state/node_state_ops.rs | 20 ++++- .../src/db/api/state/node_state_ord_ops.rs | 84 ++++++++++++++++--- .../api/storage/graph/storage_ops/metadata.rs | 4 +- .../storage/graph/storage_ops/time_props.rs | 8 +- .../graph/storage_ops/time_semantics.rs | 6 +- raphtory/src/db/api/storage/storage.rs | 39 ++++----- .../src/db/api/view/internal/materialize.rs | 11 +-- .../time_semantics/event_semantics.rs | 5 +- .../internal/time_semantics/filtered_node.rs | 5 +- .../api/view/internal/time_semantics/mod.rs | 12 +-- raphtory/src/db/graph/edge.rs | 14 ++-- raphtory/src/db/graph/node.rs | 10 +-- raphtory/src/db/graph/nodes.rs | 4 +- raphtory/src/db/graph/views/deletion_graph.rs | 8 +- raphtory/src/db/graph/views/node_subgraph.rs | 2 +- raphtory/src/db/graph/views/window_graph.rs | 12 +-- raphtory/src/db/replay/mod.rs | 2 +- raphtory/src/io/arrow/dataframe.rs | 2 +- raphtory/src/io/arrow/df_loaders.rs | 10 +-- raphtory/src/io/arrow/layer_col.rs | 1 - raphtory/src/io/arrow/node_col.rs | 30 +++---- raphtory/src/python/filter/mod.rs | 2 +- raphtory/src/python/graph/node.rs | 2 +- raphtory/src/python/graph/node_state/mod.rs | 2 +- raphtory/src/python/packages/base_modules.rs | 8 +- .../python/types/macros/borrowing_iterator.rs | 2 +- .../src/python/types/wrappers/iterators.rs | 2 +- raphtory/src/python/utils/mod.rs | 2 +- raphtory/src/search/edge_index.rs | 2 +- raphtory/src/search/graph_index.rs | 4 +- raphtory/src/search/node_index.rs | 12 +-- 59 files changed, 314 insertions(+), 290 deletions(-) diff --git a/db4-storage/src/api/edges.rs b/db4-storage/src/api/edges.rs index c0a328fb77..4d4a98da01 100644 --- a/db4-storage/src/api/edges.rs +++ b/db4-storage/src/api/edges.rs @@ -52,13 +52,13 @@ pub trait EdgeSegmentOps: Send + Sync + std::fmt::Debug + 'static { fn num_edges(&self) -> usize; - fn head(&self) -> RwLockReadGuard; + fn head(&self) -> RwLockReadGuard<'_, MemEdgeSegment>; fn head_arc(&self) -> ArcRwLockReadGuard; - fn head_mut(&self) -> RwLockWriteGuard; + fn head_mut(&self) -> RwLockWriteGuard<'_, MemEdgeSegment>; - fn try_head_mut(&self) -> Option>; + fn try_head_mut(&self) -> Option>; fn notify_write( &self, diff --git a/db4-storage/src/api/nodes.rs b/db4-storage/src/api/nodes.rs index 51cd6387f9..f066ba5bdf 100644 --- a/db4-storage/src/api/nodes.rs +++ b/db4-storage/src/api/nodes.rs @@ -72,9 +72,9 @@ pub trait NodeSegmentOps: Send + Sync + std::fmt::Debug + 'static { fn segment_id(&self) -> usize; fn head_arc(&self) -> ArcRwLockReadGuard; - fn head(&self) -> RwLockReadGuard; + fn head(&self) -> RwLockReadGuard<'_, MemNodeSegment>; - fn head_mut(&self) -> RwLockWriteGuard; + fn head_mut(&self) -> RwLockWriteGuard<'_, MemNodeSegment>; fn num_nodes(&self) -> usize { self.layer_count(0) diff --git a/db4-storage/src/pages/flush_thread.rs b/db4-storage/src/pages/flush_thread.rs index fe394f91a8..6d32536550 100644 --- a/db4-storage/src/pages/flush_thread.rs +++ b/db4-storage/src/pages/flush_thread.rs @@ -1,18 +1,13 @@ +use crate::{ + api::{edges::EdgeSegmentOps, nodes::NodeSegmentOps}, + pages::node_store::NodeStorageInner, +}; use std::{ collections::BinaryHeap, sync::{Arc, atomic::AtomicBool}, thread::JoinHandle, }; -use itertools::Itertools; - -use crate::{ - NS, - api::{edges::EdgeSegmentOps, nodes::NodeSegmentOps}, - pages::node_store::NodeStorageInner, - persist::strategy::PersistentStrategy, -}; - // This should be a rayon thread with a reference to Arc that will flush the data to disk periodically. #[derive(Debug)] pub(crate) struct FlushThread { diff --git a/db4-storage/src/pages/mod.rs b/db4-storage/src/pages/mod.rs index 17c1d58d10..8468ab4b21 100644 --- a/db4-storage/src/pages/mod.rs +++ b/db4-storage/src/pages/mod.rs @@ -1,19 +1,8 @@ -use std::{ - path::Path, - sync::{ - Arc, - atomic::{self, AtomicUsize}, - }, -}; - use crate::{ LocalPOS, api::{edges::EdgeSegmentOps, nodes::NodeSegmentOps}, error::StorageError, - pages::{ - edge_store::ReadLockedEdgeStorage, flush_thread::FlushThread, - node_store::ReadLockedNodeStorage, - }, + pages::{edge_store::ReadLockedEdgeStorage, node_store::ReadLockedNodeStorage}, persist::strategy::PersistentStrategy, properties::props_meta_writer::PropsMetaWriter, segments::{edge::MemEdgeSegment, node::MemNodeSegment}, @@ -24,10 +13,7 @@ use node_page::writer::{NodeWriter, WriterPair}; use node_store::NodeStorageInner; use parking_lot::RwLockWriteGuard; use raphtory_api::core::{ - entities::properties::{ - meta::Meta, - prop::{Prop, PropType}, - }, + entities::properties::{meta::Meta, prop::Prop}, storage::dict_mapper::MaybeNew, }; use raphtory_core::{ @@ -37,6 +23,13 @@ use raphtory_core::{ }; use serde::{Deserialize, Serialize}; use session::WriteSession; +use std::{ + path::Path, + sync::{ + Arc, + atomic::{self, AtomicUsize}, + }, +}; pub mod edge_page; pub mod edge_store; @@ -369,15 +362,18 @@ impl< pub fn node_writer( &self, node_segment: usize, - ) -> NodeWriter, NS> { + ) -> NodeWriter<'_, RwLockWriteGuard<'_, MemNodeSegment>, NS> { self.nodes().writer(node_segment) } - pub fn edge_writer(&self, eid: EID) -> EdgeWriter, ES> { + pub fn edge_writer( + &self, + eid: EID, + ) -> EdgeWriter<'_, RwLockWriteGuard<'_, MemEdgeSegment>, ES> { self.edges().get_writer(eid) } - pub fn get_free_writer(&self) -> EdgeWriter, ES> { + pub fn get_free_writer(&self) -> EdgeWriter<'_, RwLockWriteGuard<'_, MemEdgeSegment>, ES> { self.edges().get_free_writer() } } diff --git a/db4-storage/src/properties/mod.rs b/db4-storage/src/properties/mod.rs index 3ba887acb7..40e0bce58e 100644 --- a/db4-storage/src/properties/mod.rs +++ b/db4-storage/src/properties/mod.rs @@ -49,14 +49,14 @@ impl Properties { self.t_properties.len() + self.c_properties.len() } - pub(crate) fn get_mut_entry(&mut self, row: usize) -> PropMutEntry { + pub(crate) fn get_mut_entry(&mut self, row: usize) -> PropMutEntry<'_> { PropMutEntry { row, properties: self, } } - pub(crate) fn get_entry(&self, row: usize) -> RowEntry { + pub(crate) fn get_entry(&self, row: usize) -> RowEntry<'_> { RowEntry { row, properties: self, diff --git a/db4-storage/src/segments/edge.rs b/db4-storage/src/segments/edge.rs index e3ba37601c..2403f8de53 100644 --- a/db4-storage/src/segments/edge.rs +++ b/db4-storage/src/segments/edge.rs @@ -455,7 +455,7 @@ impl>> EdgeSegmentOps for EdgeSegm self.num_edges.load(atomic::Ordering::Relaxed) } - fn head(&self) -> parking_lot::RwLockReadGuard { + fn head(&self) -> parking_lot::RwLockReadGuard<'_, MemEdgeSegment> { self.segment.read_recursive() } @@ -463,11 +463,11 @@ impl>> EdgeSegmentOps for EdgeSegm self.segment.read_arc_recursive() } - fn head_mut(&self) -> parking_lot::RwLockWriteGuard { + fn head_mut(&self) -> parking_lot::RwLockWriteGuard<'_, MemEdgeSegment> { self.segment.write() } - fn try_head_mut(&self) -> Option> { + fn try_head_mut(&self) -> Option> { self.segment.try_write() } diff --git a/db4-storage/src/segments/mod.rs b/db4-storage/src/segments/mod.rs index ba5bffd252..3b2445389f 100644 --- a/db4-storage/src/segments/mod.rs +++ b/db4-storage/src/segments/mod.rs @@ -173,7 +173,7 @@ impl SegmentContainer { self.data.is_empty() } - pub fn row_entries(&self) -> impl Iterator { + pub fn row_entries(&self) -> impl Iterator)> { self.items.iter_ones().filter_map(move |l_pos| { let entry = self.data.get(&LocalPOS(l_pos))?; Some(( @@ -184,7 +184,9 @@ impl SegmentContainer { }) } - pub fn all_entries(&self) -> impl ExactSizeIterator)> { + pub fn all_entries( + &self, + ) -> impl ExactSizeIterator)>)> { self.items.iter().enumerate().map(move |(l_pos, exists)| { let l_pos = LocalPOS(l_pos); let entry = (*exists).then(|| { @@ -197,7 +199,7 @@ impl SegmentContainer { pub fn all_entries_par( &self, - ) -> impl ParallelIterator)> + '_ { + ) -> impl ParallelIterator)>)> + '_ { (0..self.items.len()).into_par_iter().map(move |l_pos| { let exists = unsafe { self.items.get_unchecked(l_pos) }; let l_pos = LocalPOS(l_pos); diff --git a/db4-storage/src/segments/node.rs b/db4-storage/src/segments/node.rs index db76840464..9320feac5e 100644 --- a/db4-storage/src/segments/node.rs +++ b/db4-storage/src/segments/node.rs @@ -361,7 +361,7 @@ impl MemNodeSegment { self.layers.iter().map(|seg| seg.t_len()).sum() } - pub fn node_ref(&self, pos: LocalPOS) -> MemNodeRef { + pub fn node_ref(&self, pos: LocalPOS) -> MemNodeRef<'_> { MemNodeRef::new(pos, self) } } @@ -460,7 +460,7 @@ impl>> NodeSegmentOps for NodeSegm self.segment_id } - fn head(&self) -> parking_lot::RwLockReadGuard { + fn head(&self) -> parking_lot::RwLockReadGuard<'_, MemNodeSegment> { self.inner.read() } @@ -468,7 +468,7 @@ impl>> NodeSegmentOps for NodeSegm self.inner.read_arc_recursive() } - fn head_mut(&self) -> parking_lot::RwLockWriteGuard { + fn head_mut(&self) -> parking_lot::RwLockWriteGuard<'_, MemNodeSegment> { self.inner.write() } diff --git a/raphtory-api/src/core/entities/properties/meta.rs b/raphtory-api/src/core/entities/properties/meta.rs index d76ac2a324..63b7ebfe4e 100644 --- a/raphtory-api/src/core/entities/properties/meta.rs +++ b/raphtory-api/src/core/entities/properties/meta.rs @@ -1,23 +1,19 @@ -use std::{ - ops::{Deref, DerefMut}, - sync::{ - atomic::{self, AtomicUsize}, - Arc, +use crate::core::{ + entities::properties::prop::{check_for_unification, unify_types, PropError, PropType}, + storage::{ + arc_str::ArcStr, + dict_mapper::{DictMapper, LockedDictMapper, MaybeNew, PublicKeys, WriteLockedDictMapper}, }, }; - use itertools::Either; use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard}; use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; - -use crate::core::{ - entities::properties::prop::{check_for_unification, unify_types, PropError, PropType}, - storage::{ - arc_str::ArcStr, - dict_mapper::{ - AllKeys, DictMapper, LockedDictMapper, MaybeNew, PublicKeys, WriteLockedDictMapper, - }, +use std::{ + ops::{Deref, DerefMut}, + sync::{ + atomic::{self, AtomicUsize}, + Arc, }, }; diff --git a/raphtory-core/src/entities/properties/graph_meta.rs b/raphtory-core/src/entities/properties/graph_meta.rs index cf4cd14770..49b5b193e6 100644 --- a/raphtory-core/src/entities/properties/graph_meta.rs +++ b/raphtory-core/src/entities/properties/graph_meta.rs @@ -12,7 +12,7 @@ use raphtory_api::core::{ }, storage::{ arc_str::ArcStr, - dict_mapper::{AllKeys, MaybeNew, PublicKeys}, + dict_mapper::{MaybeNew, PublicKeys}, FxDashMap, }, }; diff --git a/raphtory-core/src/entities/properties/props.rs b/raphtory-core/src/entities/properties/props.rs index 69b43c6a68..d99d30521d 100644 --- a/raphtory-core/src/entities/properties/props.rs +++ b/raphtory-core/src/entities/properties/props.rs @@ -1,12 +1,8 @@ use crate::{ entities::properties::tprop::{IllegalPropType, TProp}, - storage::{ - lazy_vec::{IllegalSet, LazyVec}, - timeindex::TimeIndexEntry, - }, + storage::lazy_vec::IllegalSet, }; use raphtory_api::core::entities::properties::prop::Prop; -use serde::{Deserialize, Serialize}; use std::fmt::Debug; use thiserror::Error; diff --git a/raphtory-core/src/storage/lazy_vec.rs b/raphtory-core/src/storage/lazy_vec.rs index e0b24cd578..11aa949a5f 100644 --- a/raphtory-core/src/storage/lazy_vec.rs +++ b/raphtory-core/src/storage/lazy_vec.rs @@ -168,7 +168,7 @@ where A: PartialEq + Default + Debug + Sync + Send + Clone, { // fails if there is already a value set for the given id to a different value - pub(crate) fn set(&mut self, id: usize, value: A) -> Result<(), IllegalSet> { + pub fn set(&mut self, id: usize, value: A) -> Result<(), IllegalSet> { match self { LazyVec::Empty => { *self = Self::from(id, value); @@ -199,7 +199,7 @@ where } } - pub(crate) fn update(&mut self, id: usize, updater: F) -> Result + pub fn update(&mut self, id: usize, updater: F) -> Result where F: FnOnce(&mut A) -> Result, E: From>, @@ -241,7 +241,7 @@ where LazyVec::LazyVec1(A::default(), TupleCol::from(inner)) } - pub(crate) fn filled_ids(&self) -> BoxedLIter { + pub fn filled_ids(&self) -> BoxedLIter<'_, usize> { match self { LazyVec::Empty => Box::new(iter::empty()), LazyVec::LazyVec1(_, tuples) => Box::new( @@ -281,7 +281,7 @@ where } } - pub(crate) fn get(&self, id: usize) -> Option<&A> { + pub fn get(&self, id: usize) -> Option<&A> { match self { LazyVec::LazyVec1(default, tuples) => tuples .get(id) diff --git a/raphtory-core/src/storage/mod.rs b/raphtory-core/src/storage/mod.rs index 822b63b172..b914e48df5 100644 --- a/raphtory-core/src/storage/mod.rs +++ b/raphtory-core/src/storage/mod.rs @@ -3,32 +3,23 @@ use crate::{ storage::lazy_vec::IllegalSet, }; use bigdecimal::BigDecimal; -use itertools::Itertools; use lazy_vec::LazyVec; -use lock_api; -#[cfg(feature = "arrow")] -use raphtory_api::core::entities::properties::prop::PropArray; use raphtory_api::core::{ entities::properties::prop::{Prop, PropRef, PropType}, storage::arc_str::ArcStr, }; -use rayon::prelude::*; use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; -use std::{ - collections::HashMap, - fmt::Debug, - ops::{Deref, DerefMut, Index, IndexMut}, - sync::Arc, -}; +use std::{collections::HashMap, fmt::Debug, sync::Arc}; use thiserror::Error; +#[cfg(feature = "arrow")] +use raphtory_api::core::entities::properties::prop::PropArray; + pub mod lazy_vec; pub mod locked_view; pub mod timeindex; -type ArcRwLockReadGuard = lock_api::ArcRwLockReadGuard; - #[derive(Debug, Serialize, Deserialize, PartialEq, Default)] pub struct TColumns { t_props_log: Vec, @@ -288,7 +279,7 @@ impl PropColumn { } } - fn is_empty(&self) -> bool { + pub fn is_empty(&self) -> bool { matches!(self, PropColumn::Empty(_)) } @@ -391,7 +382,6 @@ impl PropColumn { mod test { use super::TColumns; use raphtory_api::core::entities::properties::prop::Prop; - use rayon::prelude::*; #[test] fn tcolumns_append_1() { diff --git a/raphtory-core/src/storage/timeindex.rs b/raphtory-core/src/storage/timeindex.rs index 226402872d..9bbc76876a 100644 --- a/raphtory-core/src/storage/timeindex.rs +++ b/raphtory-core/src/storage/timeindex.rs @@ -380,13 +380,9 @@ where #[cfg(test)] mod test { + use crate::{entities::properties::tcell::TCell, storage::timeindex::TimeIndexOps}; use raphtory_api::core::storage::timeindex::TimeIndexEntry; - use crate::{ - entities::properties::tcell::TCell, - storage::timeindex::{TimeIndex, TimeIndexOps, TimeIndexWindow}, - }; - #[test] fn window_of_window_not_empty() { let mut cell: TCell<()> = TCell::default(); diff --git a/raphtory-graphql/src/python/pymodule.rs b/raphtory-graphql/src/python/pymodule.rs index 7088a3d18b..d1953f602a 100644 --- a/raphtory-graphql/src/python/pymodule.rs +++ b/raphtory-graphql/src/python/pymodule.rs @@ -9,7 +9,7 @@ use crate::python::{ }; use pyo3::prelude::*; -pub fn base_graphql_module(py: Python<'_>) -> Result, PyErr> { +pub fn base_graphql_module(py: Python) -> Result, PyErr> { let graphql_module = PyModule::new(py, "graphql")?; graphql_module.add_class::()?; graphql_module.add_class::()?; diff --git a/raphtory-storage/src/core_ops.rs b/raphtory-storage/src/core_ops.rs index a8019576b0..3bab456c23 100644 --- a/raphtory-storage/src/core_ops.rs +++ b/raphtory-storage/src/core_ops.rs @@ -100,7 +100,7 @@ pub trait CoreGraphOps: Send + Sync { self.core_graph().owned_edges() } #[inline] - fn core_edge(&self, eid: EID) -> EdgeStorageEntry { + fn core_edge(&self, eid: EID) -> EdgeStorageEntry<'_> { self.core_graph().edge_entry(eid) } @@ -110,7 +110,7 @@ pub trait CoreGraphOps: Send + Sync { } #[inline] - fn core_node(&self, vid: VID) -> NodeStorageEntry { + fn core_node(&self, vid: VID) -> NodeStorageEntry<'_> { self.core_graph().core_node(vid) } @@ -229,7 +229,7 @@ pub trait CoreGraphOps: Send + Sync { /// /// # Returns /// The keys of the metadata. - fn node_metadata_ids(&self, _v: VID) -> BoxedLIter { + fn node_metadata_ids(&self, _v: VID) -> BoxedLIter<'_, usize> { self.node_meta().metadata_mapper().ids().into_dyn_boxed() } diff --git a/raphtory-storage/src/graph/edges/edge_entry.rs b/raphtory-storage/src/graph/edges/edge_entry.rs index af38d9cb70..f750458626 100644 --- a/raphtory-storage/src/graph/edges/edge_entry.rs +++ b/raphtory-storage/src/graph/edges/edge_entry.rs @@ -1,6 +1,6 @@ use std::ops::Range; -use crate::graph::edges::{edge_storage_ops::EdgeStorageOps}; +use crate::graph::edges::edge_storage_ops::EdgeStorageOps; use raphtory_api::core::entities::properties::{prop::Prop, tprop::TPropOps}; use raphtory_core::entities::{LayerIds, EID, VID}; use storage::{api::edges::EdgeEntryOps, EdgeEntry, EdgeEntryRef}; @@ -18,7 +18,7 @@ pub enum EdgeStorageEntry<'a> { impl<'a> EdgeStorageEntry<'a> { #[inline] - pub fn as_ref(&self) -> EdgeEntryRef { + pub fn as_ref(&self) -> EdgeEntryRef<'_> { match self { EdgeStorageEntry::Mem(edge) => *edge, EdgeStorageEntry::Unlocked(edge) => edge.as_ref(), diff --git a/raphtory-storage/src/graph/edges/edges.rs b/raphtory-storage/src/graph/edges/edges.rs index 72a738ae21..2648517b2a 100644 --- a/raphtory-storage/src/graph/edges/edges.rs +++ b/raphtory-storage/src/graph/edges/edges.rs @@ -14,11 +14,11 @@ impl EdgesStorage { } #[inline] - pub fn as_ref(&self) -> EdgesStorageRef { + pub fn as_ref(&self) -> EdgesStorageRef<'_> { EdgesStorageRef::Mem(self.storage.as_ref()) } - pub fn edge(&self, eid: EID) -> EdgeEntryRef { + pub fn edge(&self, eid: EID) -> EdgeEntryRef<'_> { self.storage.edge_ref(eid) } diff --git a/raphtory-storage/src/graph/graph.rs b/raphtory-storage/src/graph/graph.rs index 3432b0ee55..99e371134c 100644 --- a/raphtory-storage/src/graph/graph.rs +++ b/raphtory-storage/src/graph/graph.rs @@ -117,7 +117,7 @@ impl GraphStorage { } #[inline(always)] - pub fn nodes(&self) -> NodesStorageEntry { + pub fn nodes(&self) -> NodesStorageEntry<'_> { match self { GraphStorage::Mem(storage) => NodesStorageEntry::Mem(&storage.nodes), GraphStorage::Unlocked(storage) => { @@ -205,7 +205,7 @@ impl GraphStorage { } #[inline(always)] - pub fn edges(&self) -> EdgesStorageRef { + pub fn edges(&self) -> EdgesStorageRef<'_> { match self { GraphStorage::Mem(storage) => EdgesStorageRef::Mem(&storage.edges), GraphStorage::Unlocked(storage) => { @@ -229,7 +229,7 @@ impl GraphStorage { } #[inline(always)] - pub fn edge_entry(&self, eid: EID) -> EdgeStorageEntry { + pub fn edge_entry(&self, eid: EID) -> EdgeStorageEntry<'_> { match self { GraphStorage::Mem(storage) => EdgeStorageEntry::Mem(storage.edges.edge_ref(eid)), GraphStorage::Unlocked(storage) => { diff --git a/raphtory-storage/src/graph/nodes/node_entry.rs b/raphtory-storage/src/graph/nodes/node_entry.rs index 462818820b..80860d890e 100644 --- a/raphtory-storage/src/graph/nodes/node_entry.rs +++ b/raphtory-storage/src/graph/nodes/node_entry.rs @@ -32,7 +32,7 @@ impl<'a> From> for NodeStorageEntry<'a> { impl<'a> NodeStorageEntry<'a> { #[inline] - pub fn as_ref(&self) -> NodeStorageRef { + pub fn as_ref(&self) -> NodeStorageRef<'_> { match self { NodeStorageEntry::Mem(entry) => *entry, NodeStorageEntry::Unlocked(entry) => entry.as_ref(), diff --git a/raphtory-storage/src/graph/nodes/nodes.rs b/raphtory-storage/src/graph/nodes/nodes.rs index d0cc4963bb..a6c56ef02c 100644 --- a/raphtory-storage/src/graph/nodes/nodes.rs +++ b/raphtory-storage/src/graph/nodes/nodes.rs @@ -19,12 +19,12 @@ impl NodesStorage { } #[inline] - pub fn as_ref(&self) -> NodesStorageEntry { + pub fn as_ref(&self) -> NodesStorageEntry<'_> { NodesStorageEntry::Mem(self.storage.as_ref()) } #[inline] - pub fn node_entry(&self, vid: VID) -> NodeStorageRef { + pub fn node_entry(&self, vid: VID) -> NodeStorageRef<'_> { self.storage.node_ref(vid) } diff --git a/raphtory-storage/src/mutation/addition_ops.rs b/raphtory-storage/src/mutation/addition_ops.rs index e8bcda0460..c8e176e320 100644 --- a/raphtory-storage/src/mutation/addition_ops.rs +++ b/raphtory-storage/src/mutation/addition_ops.rs @@ -32,7 +32,7 @@ pub trait InternalAdditionOps { where Self: 'a; - fn write_lock(&self) -> Result, Self::Error>; + fn write_lock(&self) -> Result, Self::Error>; /// map layer name to id and allocate a new layer if needed fn resolve_layer(&self, layer: Option<&str>) -> Result, Self::Error>; @@ -200,7 +200,7 @@ impl InternalAdditionOps for GraphStorage { type AtomicAddEdge<'a> = WriteS<'a, Extension>; - fn write_lock(&self) -> Result, Self::Error> { + fn write_lock(&self) -> Result, Self::Error> { self.mutable()?.write_lock() } @@ -313,7 +313,7 @@ where G: 'a; #[inline] - fn write_lock(&self) -> Result, Self::Error> { + fn write_lock(&self) -> Result, Self::Error> { self.base().write_lock() } diff --git a/raphtory-storage/src/mutation/addition_ops_ext.rs b/raphtory-storage/src/mutation/addition_ops_ext.rs index 8991f91101..8ed9554680 100644 --- a/raphtory-storage/src/mutation/addition_ops_ext.rs +++ b/raphtory-storage/src/mutation/addition_ops_ext.rs @@ -196,7 +196,7 @@ impl InternalAdditionOps for TemporalGraph { type AtomicAddEdge<'a> = WriteS<'a, Extension>; - fn write_lock(&self) -> Result, Self::Error> { + fn write_lock(&self) -> Result, Self::Error> { let locked_g = self.write_locked_graph(); Ok(locked_g) } diff --git a/raphtory-storage/src/mutation/property_addition_ops.rs b/raphtory-storage/src/mutation/property_addition_ops.rs index 0d5291dc96..101958a514 100644 --- a/raphtory-storage/src/mutation/property_addition_ops.rs +++ b/raphtory-storage/src/mutation/property_addition_ops.rs @@ -24,24 +24,24 @@ pub trait InternalPropertyAdditionOps { &self, vid: VID, props: &[(usize, Prop)], - ) -> Result; + ) -> Result, Self::Error>; fn internal_update_node_metadata( &self, vid: VID, props: &[(usize, Prop)], - ) -> Result; + ) -> Result, Self::Error>; fn internal_add_edge_metadata( &self, eid: EID, layer: usize, props: &[(usize, Prop)], - ) -> Result; + ) -> Result, Self::Error>; fn internal_update_edge_metadata( &self, eid: EID, layer: usize, props: &[(usize, Prop)], - ) -> Result; + ) -> Result, Self::Error>; } impl InternalPropertyAdditionOps for db4_graph::TemporalGraph { @@ -79,7 +79,7 @@ impl InternalPropertyAdditionOps for db4_graph::TemporalGraph { &self, vid: VID, props: &[(usize, Prop)], - ) -> Result { + ) -> Result, Self::Error> { let (segment_id, node_pos) = self.storage().nodes().resolve_pos(vid); let mut writer = self.storage().nodes().writer(segment_id); writer.update_c_props(node_pos, 0, props.iter().cloned(), 0); @@ -90,7 +90,7 @@ impl InternalPropertyAdditionOps for db4_graph::TemporalGraph { &self, vid: VID, props: &[(usize, Prop)], - ) -> Result { + ) -> Result, Self::Error> { todo!() } @@ -99,7 +99,7 @@ impl InternalPropertyAdditionOps for db4_graph::TemporalGraph { eid: EID, layer: usize, props: &[(usize, Prop)], - ) -> Result { + ) -> Result, Self::Error> { let (_, edge_pos) = self.storage().edges().resolve_pos(eid); let mut writer = self.storage().edge_writer(eid); let (src, dst) = writer.get_edge(layer, edge_pos).unwrap_or_else(|| { @@ -114,7 +114,7 @@ impl InternalPropertyAdditionOps for db4_graph::TemporalGraph { eid: EID, layer: usize, props: &[(usize, Prop)], - ) -> Result { + ) -> Result, Self::Error> { let (_, edge_pos) = self.storage().edges().resolve_pos(eid); let mut writer = self.storage().edge_writer(eid); let (src, dst) = writer.get_edge(layer, edge_pos).unwrap_or_else(|| { @@ -148,7 +148,7 @@ impl InternalPropertyAdditionOps for GraphStorage { &self, vid: VID, props: &[(usize, Prop)], - ) -> Result { + ) -> Result, Self::Error> { self.mutable()?.internal_add_node_metadata(vid, props) } @@ -156,7 +156,7 @@ impl InternalPropertyAdditionOps for GraphStorage { &self, vid: VID, props: &[(usize, Prop)], - ) -> Result { + ) -> Result, Self::Error> { self.mutable()?.internal_update_node_metadata(vid, props) } @@ -165,7 +165,7 @@ impl InternalPropertyAdditionOps for GraphStorage { eid: EID, layer: usize, props: &[(usize, Prop)], - ) -> Result { + ) -> Result, Self::Error> { self.mutable()? .internal_add_edge_metadata(eid, layer, props) } @@ -175,7 +175,7 @@ impl InternalPropertyAdditionOps for GraphStorage { eid: EID, layer: usize, props: &[(usize, Prop)], - ) -> Result { + ) -> Result, Self::Error> { self.mutable()? .internal_update_edge_metadata(eid, layer, props) } @@ -213,7 +213,7 @@ where &self, vid: VID, props: &[(usize, Prop)], - ) -> Result { + ) -> Result, Self::Error> { self.base().internal_add_node_metadata(vid, props) } @@ -222,7 +222,7 @@ where &self, vid: VID, props: &[(usize, Prop)], - ) -> Result { + ) -> Result, Self::Error> { self.base().internal_update_node_metadata(vid, props) } @@ -232,7 +232,7 @@ where eid: EID, layer: usize, props: &[(usize, Prop)], - ) -> Result { + ) -> Result, Self::Error> { self.base().internal_add_edge_metadata(eid, layer, props) } @@ -242,7 +242,7 @@ where eid: EID, layer: usize, props: &[(usize, Prop)], - ) -> Result { + ) -> Result, Self::Error> { self.base().internal_update_edge_metadata(eid, layer, props) } } diff --git a/raphtory/src/db/api/properties/internal.rs b/raphtory/src/db/api/properties/internal.rs index 8c5622fd11..267a62fc92 100644 --- a/raphtory/src/db/api/properties/internal.rs +++ b/raphtory/src/db/api/properties/internal.rs @@ -13,14 +13,14 @@ pub trait InternalTemporalPropertyViewOps { fn dtype(&self, id: usize) -> PropType; fn temporal_value(&self, id: usize) -> Option; - fn temporal_iter(&self, id: usize) -> BoxedLIter<(TimeIndexEntry, Prop)>; + fn temporal_iter(&self, id: usize) -> BoxedLIter<'_, (TimeIndexEntry, Prop)>; - fn temporal_iter_rev(&self, id: usize) -> BoxedLIter<(TimeIndexEntry, Prop)>; - fn temporal_history_iter(&self, id: usize) -> BoxedLIter { + fn temporal_iter_rev(&self, id: usize) -> BoxedLIter<'_, (TimeIndexEntry, Prop)>; + fn temporal_history_iter(&self, id: usize) -> BoxedLIter<'_, i64> { self.temporal_iter(id).map(|(t, _)| t.t()).into_dyn_boxed() } - fn temporal_history_iter_rev(&self, id: usize) -> BoxedLIter { + fn temporal_history_iter_rev(&self, id: usize) -> BoxedLIter<'_, i64> { self.temporal_iter_rev(id) .map(|(t, _)| t.t()) .into_dyn_boxed() @@ -32,11 +32,11 @@ pub trait InternalTemporalPropertyViewOps { .collect::>>() } - fn temporal_values_iter(&self, id: usize) -> BoxedLIter { + fn temporal_values_iter(&self, id: usize) -> BoxedLIter<'_, Prop> { self.temporal_iter(id).map(|(_, v)| v).into_dyn_boxed() } - fn temporal_values_iter_rev(&self, id: usize) -> BoxedLIter { + fn temporal_values_iter_rev(&self, id: usize) -> BoxedLIter<'_, Prop> { self.temporal_iter_rev(id).map(|(_, v)| v).into_dyn_boxed() } @@ -44,18 +44,18 @@ pub trait InternalTemporalPropertyViewOps { } pub trait TemporalPropertiesRowView { - fn rows(&self) -> BoxedLIter<(TimeIndexEntry, Vec<(usize, Prop)>)>; + fn rows(&self) -> BoxedLIter<'_, (TimeIndexEntry, Vec<(usize, Prop)>)>; } pub trait InternalMetadataOps: Send + Sync { /// Find id for property name (note this only checks the meta-data, not if the property actually exists for the entity) fn get_metadata_id(&self, name: &str) -> Option; fn get_metadata_name(&self, id: usize) -> ArcStr; - fn metadata_ids(&self) -> BoxedLIter; - fn metadata_keys(&self) -> BoxedLIter { + fn metadata_ids(&self) -> BoxedLIter<'_, usize>; + fn metadata_keys(&self) -> BoxedLIter<'_, ArcStr> { Box::new(self.metadata_ids().map(|id| self.get_metadata_name(id))) } - fn metadata_values(&self) -> BoxedLIter> { + fn metadata_values(&self) -> BoxedLIter<'_, Option> { Box::new(self.metadata_ids().map(|k| self.get_metadata(k))) } fn get_metadata(&self, id: usize) -> Option; @@ -65,8 +65,8 @@ pub trait InternalTemporalPropertiesOps: Send + Sync { fn get_temporal_prop_id(&self, name: &str) -> Option; fn get_temporal_prop_name(&self, id: usize) -> ArcStr; - fn temporal_prop_ids(&self) -> BoxedLIter; - fn temporal_prop_keys(&self) -> BoxedLIter { + fn temporal_prop_ids(&self) -> BoxedLIter<'_, usize>; + fn temporal_prop_keys(&self) -> BoxedLIter<'_, ArcStr> { Box::new( self.temporal_prop_ids() .map(|id| self.get_temporal_prop_name(id)), @@ -106,22 +106,22 @@ where } #[inline] - fn temporal_iter(&self, id: usize) -> BoxedLIter<(TimeIndexEntry, Prop)> { + fn temporal_iter(&self, id: usize) -> BoxedLIter<'_, (TimeIndexEntry, Prop)> { self.base().temporal_iter(id) } #[inline] - fn temporal_iter_rev(&self, id: usize) -> BoxedLIter<(TimeIndexEntry, Prop)> { + fn temporal_iter_rev(&self, id: usize) -> BoxedLIter<'_, (TimeIndexEntry, Prop)> { self.base().temporal_iter_rev(id) } #[inline] - fn temporal_history_iter(&self, id: usize) -> BoxedLIter { + fn temporal_history_iter(&self, id: usize) -> BoxedLIter<'_, i64> { self.base().temporal_history_iter(id) } #[inline] - fn temporal_history_iter_rev(&self, id: usize) -> BoxedLIter { + fn temporal_history_iter_rev(&self, id: usize) -> BoxedLIter<'_, i64> { self.base().temporal_history_iter_rev(id) } @@ -131,12 +131,12 @@ where } #[inline] - fn temporal_values_iter(&self, id: usize) -> BoxedLIter { + fn temporal_values_iter(&self, id: usize) -> BoxedLIter<'_, Prop> { self.base().temporal_values_iter(id) } #[inline] - fn temporal_values_iter_rev(&self, id: usize) -> BoxedLIter { + fn temporal_values_iter_rev(&self, id: usize) -> BoxedLIter<'_, Prop> { self.base().temporal_values_iter_rev(id) } @@ -163,12 +163,12 @@ where } #[inline] - fn temporal_prop_ids(&self) -> BoxedLIter { + fn temporal_prop_ids(&self) -> BoxedLIter<'_, usize> { self.base().temporal_prop_ids() } #[inline] - fn temporal_prop_keys(&self) -> BoxedLIter { + fn temporal_prop_keys(&self) -> BoxedLIter<'_, ArcStr> { self.base().temporal_prop_keys() } } @@ -188,17 +188,17 @@ where } #[inline] - fn metadata_ids(&self) -> BoxedLIter { + fn metadata_ids(&self) -> BoxedLIter<'_, usize> { self.base().metadata_ids() } #[inline] - fn metadata_keys(&self) -> BoxedLIter { + fn metadata_keys(&self) -> BoxedLIter<'_, ArcStr> { self.base().metadata_keys() } #[inline] - fn metadata_values(&self) -> BoxedLIter> { + fn metadata_values(&self) -> BoxedLIter<'_, Option> { self.base().metadata_values() } diff --git a/raphtory/src/db/api/properties/temporal_props.rs b/raphtory/src/db/api/properties/temporal_props.rs index b98b2cb0a7..02020c2782 100644 --- a/raphtory/src/db/api/properties/temporal_props.rs +++ b/raphtory/src/db/api/properties/temporal_props.rs @@ -64,18 +64,18 @@ impl TemporalPropertyView