diff --git a/Cargo.lock b/Cargo.lock index 34ace980..4ca192b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -42,9 +42,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" dependencies = [ "memchr", ] @@ -131,9 +131,18 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.103" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "approx" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] [[package]] name = "arbitrary" @@ -146,9 +155,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.42" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac" +checksum = "3976abdc8fe7d1133d43d304afd42abdf5bc3e1319d263d223bde07b5efc4be8" dependencies = [ "compression-codecs", "compression-core", @@ -158,13 +167,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.89" +version = "0.1.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.3", ] [[package]] @@ -184,31 +193,31 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-rs" -version = "1.16.3" +version = "1.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f" +checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1" dependencies = [ "aws-lc-sys", - "untrusted 0.7.1", "zeroize", ] [[package]] name = "aws-lc-sys" -version = "0.40.0" +version = "0.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7" +checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c" dependencies = [ "cc", "cmake", "dunce", "fs_extra", + "pkg-config", ] [[package]] @@ -243,6 +252,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "base64ct" version = "1.8.3" @@ -278,15 +293,15 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.13.0" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" [[package]] name = "bitvec" -version = "1.0.1" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" dependencies = [ "funty", "radium", @@ -303,6 +318,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + [[package]] name = "block-padding" version = "0.3.3" @@ -314,9 +338,9 @@ dependencies = [ [[package]] name = "bon" -version = "3.9.1" +version = "3.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f47dbe92550676ee653353c310dfb9cf6ba17ee70396e1f7cf0a2020ad49b2fe" +checksum = "a602c73c7b0148ec6d12af6fd5cc7a46e2eacc8878271a999abac56eed12f561" dependencies = [ "bon-macros", "rustversion", @@ -324,9 +348,9 @@ dependencies = [ [[package]] name = "bon-macros" -version = "3.9.1" +version = "3.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "519bd3116aeeb42d5372c29d982d16d0170d3d4a5ed85fc7dd91642ffff3c67c" +checksum = "6dee98b0db6a962de883bf5d20362dee4d7ca0d12fe39a7c6c73c844e1cd7c1f" dependencies = [ "darling 0.23.0", "ident_case", @@ -334,14 +358,14 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] name = "borsh" -version = "1.6.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" +checksum = "a88b7ea17d208c4193f2c1e6de3c35fe71f98c96982d5ced308bdcc749ff6e1f" dependencies = [ "bytes", "cfg_aliases", @@ -349,13 +373,13 @@ dependencies = [ [[package]] name = "bstr" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +checksum = "1f7dc094d718f2e1c1559ad110e27eeaae14a5465d3d56dd6dbd793079fbd530" dependencies = [ "memchr", "regex-automata", - "serde", + "serde_core", ] [[package]] @@ -364,11 +388,17 @@ version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +[[package]] +name = "by_address" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" + [[package]] name = "bytemuck" -version = "1.25.0" +version = "1.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" [[package]] name = "byteorder" @@ -378,9 +408,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "bzip2" @@ -421,7 +451,7 @@ dependencies = [ "cached_proc_macro_types", "hashbrown 0.14.5", "once_cell", - "thiserror 2.0.18", + "thiserror 2.0.19", "web-time", ] @@ -434,7 +464,7 @@ dependencies = [ "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -445,9 +475,9 @@ checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0" [[package]] name = "camino" -version = "1.2.2" +version = "1.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +checksum = "bb1307f12aa967b5a58416e87b3653360e0fd614a016b6e970db08fecbb1b80d" dependencies = [ "serde_core", ] @@ -472,9 +502,9 @@ dependencies = [ [[package]] name = "cargo-config2" -version = "0.1.44" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ada53f7339c78084fb37d7e17f34e76537541c4fbb02fa3a2baa14b8faad37" +checksum = "855d1b74d8faf6c56e02ddbb8a60f6eae479bba5ee6ee29cddee73fe9484f261" dependencies = [ "serde", "serde_derive", @@ -483,9 +513,9 @@ dependencies = [ [[package]] name = "cargo-deny" -version = "0.19.4" +version = "0.19.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4664ab61c5370e9bc86928dec044499866db0d830ef7b17d81bc668eb4f74c06" +checksum = "24bb0f6e6660ac1585169cb9a24ebd6ad77944561604e27f7e87ce2a6c70b6b7" dependencies = [ "anyhow", "bitvec", @@ -514,7 +544,7 @@ dependencies = [ "serde_json", "smallvec", "spdx", - "strum 0.28.0", + "strum", "tame-index", "toml-span", "twox-hash", @@ -524,9 +554,9 @@ dependencies = [ [[package]] name = "cargo-llvm-cov" -version = "0.8.5" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e40b72db9c18b7e128730f67135c065d73d014c34d59e107351b98214eca1034" +checksum = "dbb60793c145d8ef09b5cfa49c2f2890b93bde5a13885a9369654ca87dddfe7f" dependencies = [ "anyhow", "camino", @@ -569,9 +599,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.65" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" dependencies = [ "find-msvc-tools", "jobserver", @@ -581,9 +611,9 @@ dependencies = [ [[package]] name = "cfg-expr" -version = "0.20.7" +version = "0.20.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6b04e07d8080154ed4ac03546d9a2b303cc2fe1901ba0b35b301516e289368" +checksum = "fb693542bcafa528e198be0ebd9d3632ca5b7c93dbe7237460e199910835997c" dependencies = [ "smallvec", "target-lexicon", @@ -597,15 +627,15 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" [[package]] name = "chacha20" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ "cfg-if", "cpufeatures 0.3.0", @@ -614,9 +644,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "js-sys", @@ -632,7 +662,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "inout", ] @@ -644,9 +674,9 @@ checksum = "b0fc239e0f6cb375d2402d48afb92f76f5404fd1df208a41930ec81eda078bea" [[package]] name = "clap" -version = "4.6.1" +version = "4.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +checksum = "301b56658598e48f3648647ac6fc887be7e7108eddfa4e9b63fcf3ec58c0cadf" dependencies = [ "clap_builder", "clap_derive", @@ -654,9 +684,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.0" +version = "4.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +checksum = "94a65403d1a1bd28f7dc68eb8506e8874808ee5eecb59298de588e2e1407a078" dependencies = [ "anstream", "anstyle", @@ -666,14 +696,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.1" +version = "4.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.3", ] [[package]] @@ -691,13 +721,19 @@ dependencies = [ "cc", ] +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + [[package]] name = "cobs" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" dependencies = [ - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -739,9 +775,9 @@ dependencies = [ [[package]] name = "compact_str" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" +checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab" dependencies = [ "castaway", "cfg-if", @@ -770,9 +806,9 @@ checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" [[package]] name = "console" -version = "0.16.3" +version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" +checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c" dependencies = [ "encode_unicode", "libc", @@ -786,6 +822,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "const-random" version = "0.1.18" @@ -906,6 +948,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + [[package]] name = "crossbeam" version = "0.8.4" @@ -921,18 +969,18 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.15" +version = "0.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -949,18 +997,18 @@ dependencies = [ [[package]] name = "crossbeam-queue" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.21" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" [[package]] name = "crossterm" @@ -968,7 +1016,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "crossterm_winapi", "derive_more", "document-features", @@ -1005,6 +1053,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + [[package]] name = "csscolorparser" version = "0.6.2" @@ -1052,6 +1109,15 @@ version = "0.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + [[package]] name = "darling" version = "0.20.11" @@ -1072,6 +1138,16 @@ dependencies = [ "darling_macro 0.23.0", ] +[[package]] +name = "darling" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88490bf1b990d87eaaa7ac8aa887f629a08e7359765b4911faf63c3763347d23" +dependencies = [ + "darling_core 0.24.0", + "darling_macro 0.24.0", +] + [[package]] name = "darling_core" version = "0.20.11" @@ -1083,7 +1159,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1096,7 +1172,20 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.118", + "syn 2.0.119", +] + +[[package]] +name = "darling_core" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "084e274f91c482280130e1e34e0b8d6e66776a060d7b6de7b84289ca778868c4" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 3.0.3", ] [[package]] @@ -1107,7 +1196,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core 0.20.11", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1118,7 +1207,18 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core 0.23.0", "quote", - "syn 2.0.118", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f5792fa0d41cd2325ce0ffa64f0a340eaebd4971a3a0c5e1ffd2cc488a355e" +dependencies = [ + "darling_core 0.24.0", + "quote", + "syn 3.0.3", ] [[package]] @@ -1136,6 +1236,37 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2" +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.19", +] + [[package]] name = "deltae" version = "0.3.2" @@ -1148,7 +1279,7 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "const-oid", + "const-oid 0.9.6", "pem-rfc7468", "zeroize", ] @@ -1158,9 +1289,6 @@ name = "deranged" version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -dependencies = [ - "powerfmt", -] [[package]] name = "derive_arbitrary" @@ -1170,7 +1298,7 @@ checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1192,7 +1320,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1201,12 +1329,24 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "const-oid", - "crypto-common", + "block-buffer 0.10.4", + "const-oid 0.9.6", + "crypto-common 0.1.7", "subtle", ] +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid 0.10.2", + "crypto-common 0.2.2", + "ctutils", +] + [[package]] name = "directories" version = "6.0.0" @@ -1228,15 +1368,25 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.1", + "objc2", +] + [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.3", ] [[package]] @@ -1298,9 +1448,9 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "either" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" [[package]] name = "embedded-io" @@ -1328,9 +1478,9 @@ checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] name = "env_filter" -version = "1.0.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" +checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217" dependencies = [ "log", "regex", @@ -1351,9 +1501,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.10" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" +checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6" dependencies = [ "anstream", "anstyle", @@ -1379,7 +1529,7 @@ checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1431,9 +1581,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] name = "fern" @@ -1457,13 +1607,12 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.27" +version = "0.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" dependencies = [ "cfg-if", "libc", - "libredox", ] [[package]] @@ -1553,9 +1702,9 @@ dependencies = [ [[package]] name = "fs-err" -version = "3.3.0" +version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73fde052dbfc920003cfd2c8e2c6e6d4cc7c1091538c3a24226cec0665ab08c0" +checksum = "b91aa448ca50d7e79433bdf3ee8d99215430d2ec02ade5aefab2a073a1822e8a" dependencies = [ "autocfg", ] @@ -1580,9 +1729,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" dependencies = [ "futures-channel", "futures-core", @@ -1595,9 +1744,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" dependencies = [ "futures-core", "futures-sink", @@ -1605,15 +1754,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" [[package]] name = "futures-executor" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" dependencies = [ "futures-core", "futures-task", @@ -1622,38 +1771,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" [[package]] name = "futures-macro" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] name = "futures-sink" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" dependencies = [ "futures-channel", "futures-core", @@ -1685,7 +1834,7 @@ dependencies = [ "itertools", "log", "nom 8.0.0", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -1693,7 +1842,7 @@ name = "gen" version = "0.2.1" dependencies = [ "anyhow", - "base64", + "base64 0.22.1", "capnp", "cargo-deny", "cargo-llvm-cov", @@ -1702,7 +1851,7 @@ dependencies = [ "crossterm", "csv", "directories", - "env_logger 0.11.10", + "env_logger 0.11.11", "fallible-streaming-iterator", "flate2", "gb-io", @@ -1712,6 +1861,7 @@ dependencies = [ "gen-diff", "gen-graph", "gen-models", + "gen-models-graph-tests", "gen-tui", "getrandom 0.3.4", "html-escape", @@ -1730,7 +1880,7 @@ dependencies = [ "petgraph 0.6.5", "postcard", "pprof", - "rand 0.10.1", + "rand 0.10.2", "rat-cursor", "rat-text", "ratatui", @@ -1740,9 +1890,9 @@ dependencies = [ "rusqdoltlite_migration", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "tempfile", - "thiserror 2.0.18", + "thiserror 2.0.19", "tracing", "tracing-subscriber", "tui-widget-list", @@ -1763,7 +1913,7 @@ dependencies = [ "noodles", "rusqdoltlite", "tempfile", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -1783,12 +1933,13 @@ dependencies = [ "hex", "itertools", "noodles", + "petgraph 0.6.5", "pyo3", "pyo3-macros", - "rand 0.10.1", + "rand 0.10.2", "rusqdoltlite", "serde", - "sha2", + "sha2 0.10.9", "tempfile", "thiserror 1.0.69", "uuid", @@ -1806,7 +1957,7 @@ dependencies = [ "petgraph 0.6.5", "rusqdoltlite", "tempfile", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -1814,11 +1965,13 @@ name = "gen-graph" version = "0.2.1" dependencies = [ "gen-core", + "gen-models", "interavl", "intervaltree", + "itertools", "petgraph 0.6.5", "serde", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -1832,7 +1985,6 @@ dependencies = [ "fallible-streaming-iterator", "gen-capnp-schemas", "gen-core", - "gen-graph", "include_dir", "indexmap", "intervaltree", @@ -1841,19 +1993,31 @@ dependencies = [ "opendal", "petgraph 0.6.5", "postcard", - "rand 0.10.1", + "rand 0.10.2", "rusqdoltlite", "rusqdoltlite_migration", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "tempfile", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tracing", "url", ] +[[package]] +name = "gen-models-graph-tests" +version = "0.2.1" +dependencies = [ + "gen-core", + "gen-graph", + "gen-models", + "intervaltree", + "rusqdoltlite", + "tempfile", +] + [[package]] name = "gen-sugiyama" version = "0.2.1" @@ -1876,7 +2040,7 @@ dependencies = [ "log", "more-asserts", "petgraph 0.6.5", - "rand 0.10.1", + "rand 0.10.2", "ratatui", "rstar", "serde", @@ -1884,7 +2048,7 @@ dependencies = [ "serial_test", "tachyonfx", "tempfile", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -1926,16 +2090,16 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 6.0.0", "rand_core 0.10.1", - "wasip2", - "wasip3", + "wasm-bindgen", ] [[package]] @@ -1946,15 +2110,15 @@ checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" [[package]] name = "glob" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" [[package]] name = "globset" -version = "0.4.18" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +checksum = "07c34a9410465b45bd9787443bc7370f37735bad04b0f0cd57ff1a3186c98988" dependencies = [ "aho-corasick", "bstr", @@ -1977,9 +2141,9 @@ dependencies = [ [[package]] name = "goblin" -version = "0.10.5" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "983a6aafb3b12d4c41ea78d39e189af4298ce747353945ff5105b54a056e5cd9" +checksum = "17582616a7718cca54cec18e534a76c7c4aec11a8b9a85695712f262fd15a4c8" dependencies = [ "log", "plain", @@ -2033,14 +2197,16 @@ version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" dependencies = [ + "allocator-api2", + "equivalent", "foldhash 0.2.0", ] [[package]] name = "hashlink" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5081f264ed7adee96ea4b4778b6bb9da0a7228b084587aa3bd3ff05da7c5a3b" +checksum = "32069d97bb81e38fa67eab65e3393bf804bb85969f2bc06bf13f64aef5aba248" dependencies = [ "hashbrown 0.17.1", ] @@ -2079,7 +2245,16 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", ] [[package]] @@ -2093,18 +2268,15 @@ dependencies = [ [[package]] name = "html-escape" -version = "0.2.13" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" -dependencies = [ - "utf8-width", -] +checksum = "c9356095b4b41197bba32173600e1582792cda618f65d12f68e2e77d273413c5" [[package]] name = "http" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" dependencies = [ "bytes", "itoa", @@ -2112,9 +2284,9 @@ dependencies = [ [[package]] name = "http-body" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" dependencies = [ "bytes", "http", @@ -2122,9 +2294,9 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" dependencies = [ "bytes", "futures-core", @@ -2141,15 +2313,24 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "humantime" -version = "2.3.0" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15cdd26707701c53297e2fa6afb323d55fbc1d0810c3aec078ae3ef0424c3c15" + +[[package]] +name = "hybrid-array" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" +dependencies = [ + "typenum", +] [[package]] name = "hyper" -version = "1.9.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" dependencies = [ "atomic-waker", "bytes", @@ -2187,7 +2368,7 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-util", @@ -2310,12 +2491,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "ident_case" version = "1.0.1" @@ -2370,15 +2545,13 @@ checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", "hashbrown 0.17.1", - "serde", - "serde_core", ] [[package]] name = "indicatif" -version = "0.18.4" +version = "0.18.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb" +checksum = "9433806cd6b4ec1aba79c021c7e4c58fb4c3b9977c085062e611ac929998fb0c" dependencies = [ "console", "portable-atomic", @@ -2408,9 +2581,9 @@ dependencies = [ [[package]] name = "insta" -version = "1.47.2" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4a6248eb93a4401ed2f37dfe8ea592d3cf05b7cf4f8efa867b6895af7e094e" +checksum = "86f0f8fee8c926415c58d6ae43a08523a26faccb2323f5e6b644fe7dd4ef6b82" dependencies = [ "console", "once_cell", @@ -2420,15 +2593,15 @@ dependencies = [ [[package]] name = "instability" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971" +checksum = "2bf84e73fa6f27f299dec58e13223cf70db80da872eb921d4f6138342a0eabc8" dependencies = [ - "darling 0.23.0", + "darling 0.24.0", "indoc", "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.3", ] [[package]] @@ -2448,9 +2621,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" [[package]] name = "is-terminal" @@ -2471,9 +2644,9 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "iset" -version = "0.3.1" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "855de4169757ca6b92b396d7a0380ef234e9a1ec2ec603c80779453ed0ab45e4" +checksum = "a4d5f8699985a95af72fe8d67484b0b036df7500e0d68e769adc198180da2011" [[package]] name = "itertools" @@ -2492,10 +2665,12 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" -version = "0.2.24" +version = "0.2.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d" +checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" dependencies = [ + "defmt", + "jiff-core", "jiff-static", "jiff-tzdb-platform", "js-sys", @@ -2504,25 +2679,35 @@ dependencies = [ "portable-atomic-util", "serde_core", "wasm-bindgen", - "windows-sys 0.61.2", + "windows-link", +] + +[[package]] +name = "jiff-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" +dependencies = [ + "defmt", ] [[package]] name = "jiff-static" -version = "0.2.24" +version = "0.2.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7" +checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" dependencies = [ + "jiff-core", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] name = "jiff-tzdb" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c900ef84826f1338a557697dc8fc601df9ca9af4ac137c7fb61d4c6f2dfd3076" +checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" [[package]] name = "jiff-tzdb-platform" @@ -2545,7 +2730,7 @@ dependencies = [ "jni-sys", "log", "simd_cesu8", - "thiserror 2.0.18", + "thiserror 2.0.19", "walkdir", "windows-link", ] @@ -2560,7 +2745,7 @@ dependencies = [ "quote", "rustc_version", "simd_cesu8", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -2579,48 +2764,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" dependencies = [ "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] name = "jobserver" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.3", "libc", ] [[package]] name = "js-sys" -version = "0.3.97" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ "cfg-if", "futures-util", - "once_cell", "wasm-bindgen", ] -[[package]] -name = "jsonwebtoken" -version = "10.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" -dependencies = [ - "aws-lc-rs", - "base64", - "getrandom 0.2.17", - "js-sys", - "pem", - "serde", - "serde_json", - "signature", - "simple_asn1", -] - [[package]] name = "kasuari" version = "0.4.12" @@ -2629,14 +2796,14 @@ checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899" dependencies = [ "hashbrown 0.16.1", "portable-atomic", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] name = "krates" -version = "0.21.1" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cca2a52229bf275e08b7ebfa7d1339668d5f9ec33e27ceb3a762bc9edd437418" +checksum = "990a061f1ba398ba5fa7c573bbe6bbcc99f1dece48e8a4434abbe255f1888faf" dependencies = [ "camino", "cfg-expr", @@ -2658,7 +2825,7 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" dependencies = [ - "spin 0.9.8", + "spin 0.9.9", ] [[package]] @@ -2673,12 +2840,6 @@ dependencies = [ "rustc-demangle", ] -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - [[package]] name = "lexical-core" version = "1.0.6" @@ -2744,15 +2905,15 @@ checksum = "803ec87c9cfb29b9d2633f20cba1f488db3fd53f2158b1024cbefb47ba05d413" [[package]] name = "libbz2-rs-sys" -version = "0.2.3" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3a6a8c165077efc8f3a971534c50ea6a1a18b329ef4a66e897a7e3a1494565f" +checksum = "34b357333733e8260735ba5894eb928c02ecc69c78715f01a8019e7fa7f2db4c" [[package]] name = "libc" -version = "0.2.186" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libdoltlite-sys" @@ -2773,23 +2934,20 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.16" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +checksum = "2026a5056764a10b2bf5d56488cba40da507f5493a6a429340e2004d9ed085fa" dependencies = [ - "bitflags 2.13.0", "libc", - "plain", - "redox_syscall 0.7.5", ] [[package]] name = "line-clipping" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f50e8f47623268b5407192d26876c4d7f89d686ca130fdc53bced4814cd29f8" +checksum = "e752191d037c44ad111a8caa762921926658402f01cc1253f7bef2020ece4f5e" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", ] [[package]] @@ -2821,17 +2979,17 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "lru" -version = "0.16.4" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" +checksum = "5d2f2f9b4ba7e6b24d95e7e899329d35be83bcded72c8540cdd5368932d1d90a" dependencies = [ - "hashbrown 0.16.1", + "hashbrown 0.17.1", ] [[package]] @@ -2878,29 +3036,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ "cfg-if", - "digest", + "digest 0.10.7", ] [[package]] name = "mea" -version = "0.6.3" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6747f54621d156e1b47eb6b25f39a941b9fc347f98f67d25d8881ff99e8ed832" +checksum = "31fc7d159de0085ab6dd7ff145a9819442cfd3d098f783263120503c3f3e58b0" dependencies = [ "slab", ] [[package]] name = "memchr" -version = "2.8.2" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "memmap2" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" dependencies = [ "libc", ] @@ -2960,9 +3118,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.0" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" dependencies = [ "libc", "log", @@ -2999,7 +3157,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "cfg-if", "cfg_aliases", "libc", @@ -3128,7 +3286,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a97398fd9d03cf1b773408c0227dc1214877d990ca8babd6dd19fbbea1c061d6" dependencies = [ "async-compression", - "bitflags 2.13.0", + "bitflags 2.13.1", "bstr", "bzip2 0.6.1", "flate2", @@ -3222,7 +3380,7 @@ version = "0.79.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c64ab4f1c784a44cef043f2c630b0e08fffc7757d5ccb2a0a698369041b987" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "bstr", "futures", "indexmap", @@ -3285,16 +3443,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - [[package]] name = "num-bigint-dig" version = "0.8.6" @@ -3306,16 +3454,16 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand 0.8.6", + "rand 0.8.7", "smallvec", "zeroize", ] [[package]] name = "num-conv" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-derive" @@ -3325,7 +3473,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -3339,11 +3487,10 @@ dependencies = [ [[package]] name = "num-iter" -version = "0.1.45" +version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" dependencies = [ - "autocfg", "num-integer", "num-traits", ] @@ -3376,6 +3523,28 @@ dependencies = [ "objc2-encode", ] +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.1", + "dispatch2", + "objc2", +] + [[package]] name = "objc2-encode" version = "4.1.0" @@ -3388,8 +3557,9 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "objc2", + "objc2-core-foundation", ] [[package]] @@ -3439,7 +3609,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1849dd2687e173e776d3af5fce1ba3ae47b9dd37a09d1c4deba850ef45fe00ca" dependencies = [ "anyhow", - "base64", + "base64 0.22.1", "bytes", "futures", "http", @@ -3451,7 +3621,7 @@ dependencies = [ "percent-encoding", "quick-xml 0.38.4", "reqsign-core", - "reqwest 0.13.3", + "reqwest 0.13.4", "serde", "serde_json", "tokio", @@ -3509,7 +3679,7 @@ version = "0.56.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7452bf3ec61cfd81ac9ad9ada17825931e9e371d44a045c6bfab9596c0a2ac3b" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "http", "log", @@ -3520,7 +3690,7 @@ dependencies = [ "reqsign-core", "reqsign-file-read-tokio", "serde", - "sha2", + "sha2 0.10.9", "uuid", ] @@ -3587,7 +3757,7 @@ version = "0.56.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9dadddeb9bb50b0d30927dd914c298c4ddca47e4c1cfa7674d311f0cf9b051c8" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "crc32c", "http", @@ -3604,9 +3774,9 @@ dependencies = [ [[package]] name = "opener" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2fa337e0cf13357c13ef1dc108df1333eb192f75fc170bea03fcf1fd404c2ee" +checksum = "b2b03ff07a220d0d0ec9a1f0f238951b7967a5a2e96aefcd21a117b1083415e9" dependencies = [ "bstr", "normpath", @@ -3654,6 +3824,39 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "palette" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddeed8580d347d2abf3dcf06a5f0b3dc020258338526b277847cd4248a70fc64" +dependencies = [ + "approx", + "libm", + "palette_derive", + "palette_math", +] + +[[package]] +name = "palette_derive" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88537020289b719d81be994ccf1bbf4990f477e2f69ee52fe3e45f43a02e56be" +dependencies = [ + "by_address", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "palette_math" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e6eb142958d64335fb0e345c5b9ead2ecd6fc438c307e9d7d3c4fd428dbaf12" +dependencies = [ + "libm", +] + [[package]] name = "parking_lot" version = "0.12.5" @@ -3672,7 +3875,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.18", + "redox_syscall", "smallvec", "windows-link", ] @@ -3683,8 +3886,8 @@ version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ - "digest", - "hmac", + "digest 0.10.7", + "hmac 0.12.1", ] [[package]] @@ -3693,7 +3896,7 @@ version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" dependencies = [ - "base64", + "base64 0.22.1", "serde_core", ] @@ -3714,9 +3917,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.6" +version = "2.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +checksum = "7df728be843c7070fab6ab7c328c4e9e9d78e23bf749c0669c86ee7ebfa050a2" dependencies = [ "memchr", "ucd-trie", @@ -3724,9 +3927,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.6" +version = "2.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +checksum = "9e2dd6fc3b26b3462ee188aac870f5a41d398f1cd5e2408d16531bd71c9591fd" dependencies = [ "pest", "pest_generator", @@ -3734,25 +3937,24 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.6" +version = "2.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +checksum = "6a7a9205cfb6f596a9e8b689c0a15f9ceb7a1aafae7aaf788150ac65b29975b6" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] name = "pest_meta" -version = "2.8.6" +version = "2.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +checksum = "85abd351c0de1e8384fc791a0737111a350394937e92b956b743dac12429f57c" dependencies = [ "pest", - "sha2", ] [[package]] @@ -3805,7 +4007,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", - "rand 0.8.6", + "rand 0.8.7", ] [[package]] @@ -3818,7 +4020,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -3858,7 +4060,7 @@ dependencies = [ "der", "pbkdf2", "scrypt", - "sha2", + "sha2 0.10.9", "spki", ] @@ -3888,9 +4090,9 @@ checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" [[package]] name = "portable-atomic" -version = "1.13.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" [[package]] name = "portable-atomic-util" @@ -3943,10 +4145,10 @@ dependencies = [ "nix 0.26.4", "once_cell", "smallvec", - "spin 0.10.0", + "spin 0.10.1", "symbolic-demangle", "tempfile", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -3965,14 +4167,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] @@ -4029,7 +4231,7 @@ dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -4042,7 +4244,7 @@ dependencies = [ "proc-macro2", "pyo3-build-config", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -4060,6 +4262,15 @@ name = "quick-xml" version = "0.39.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +dependencies = [ + "memchr", +] + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" dependencies = [ "memchr", "serde", @@ -4067,9 +4278,9 @@ dependencies = [ [[package]] name = "quinn" -version = "0.11.9" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" dependencies = [ "bytes", "cfg_aliases", @@ -4079,7 +4290,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tracing", "web-time", @@ -4087,21 +4298,22 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.14" +version = "0.11.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" dependencies = [ "aws-lc-rs", "bytes", - "getrandom 0.3.4", + "getrandom 0.4.3", "lru-slab", - "rand 0.9.4", + "rand 0.10.2", + "rand_pcg", "ring", "rustc-hash", "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.18", + "thiserror 2.0.19", "tinyvec", "tracing", "web-time", @@ -4109,23 +4321,23 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.14" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" dependencies = [ "cfg_aliases", "libc", "once_cell", "socket2", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "quote" -version = "1.0.46" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] @@ -4150,32 +4362,22 @@ checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" [[package]] name = "rand" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" dependencies = [ - "rand_chacha 0.3.1", + "rand_chacha", "rand_core 0.6.4", ] [[package]] name = "rand" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.5", -] - -[[package]] -name = "rand" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ "chacha20", - "getrandom 0.4.2", + "getrandom 0.4.3", "rand_core 0.10.1", ] @@ -4189,16 +4391,6 @@ dependencies = [ "rand_core 0.6.4", ] -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.5", -] - [[package]] name = "rand_core" version = "0.6.4" @@ -4210,18 +4402,18 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.9.5" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] -name = "rand_core" -version = "0.10.1" +name = "rand_pcg" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] [[package]] name = "rat-cursor" @@ -4305,33 +4497,37 @@ dependencies = [ [[package]] name = "ratatui" -version = "0.30.0" +version = "0.30.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1ce67fb8ba4446454d1c8dbaeda0557ff5e94d39d5e5ed7f10a65eb4c8266bc" +checksum = "3274ba0a2c5e1bcad2a2005d20f4dc59dad26b2eb0940fb094500dba4099d57d" dependencies = [ "instability", "ratatui-core", "ratatui-crossterm", "ratatui-macros", + "ratatui-termina", "ratatui-termwiz", "ratatui-widgets", + "serde", ] [[package]] name = "ratatui-core" -version = "0.1.0" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef8dea09a92caaf73bff7adb70b76162e5937524058a7e5bff37869cbbec293" +checksum = "cbb175c433c8e28a809d1f5773a2ae96e68c0ce40db865cbab1020bf33ae479c" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "compact_str", - "hashbrown 0.16.1", - "indoc", + "critical-section", + "hashbrown 0.17.1", "itertools", "kasuari", "lru", - "strum 0.27.2", - "thiserror 2.0.18", + "palette", + "serde", + "strum", + "thiserror 2.0.19", "unicode-segmentation", "unicode-truncate", "unicode-width", @@ -4339,9 +4535,9 @@ dependencies = [ [[package]] name = "ratatui-crossterm" -version = "0.1.0" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "577c9b9f652b4c121fb25c6a391dd06406d3b092ba68827e6d2f09550edc54b3" +checksum = "567584a3b0e6a8203c23de40b4861497266725eb5363dbfd18a1edd603cca9f0" dependencies = [ "cfg-if", "crossterm", @@ -4351,19 +4547,30 @@ dependencies = [ [[package]] name = "ratatui-macros" -version = "0.7.0" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7f1342a13e83e4bb9d0b793d0ea762be633f9582048c892ae9041ef39c936f4" +checksum = "ed7dc68daa7498a43e4d68e0eb078427e10c38fbcfbb1e42d955f1fa2140d814" dependencies = [ "ratatui-core", "ratatui-widgets", ] [[package]] -name = "ratatui-termwiz" +name = "ratatui-termina" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f76fe0bd0ed4295f0321b1676732e2454024c15a35d01904ddb315afd3d545c" +checksum = "c0bf912d9e66f057a759d92e386a280ea886b352ab757d6ac4d653c7ed2c43c2" +dependencies = [ + "instability", + "ratatui-core", + "termina", +] + +[[package]] +name = "ratatui-termwiz" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf03e0380b7744054d6cb74224fe3adf062a029754933f575ca1e3b4c2ce977" dependencies = [ "ratatui-core", "termwiz", @@ -4371,18 +4578,19 @@ dependencies = [ [[package]] name = "ratatui-widgets" -version = "0.3.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7dbfa023cd4e604c2553483820c5fe8aa9d71a42eea5aa77c6e7f35756612db" +checksum = "66e3d19bcc9130ca376277d93b60767ff121ace3be06f5f95f81dd68956407d1" dependencies = [ - "bitflags 2.13.0", - "hashbrown 0.16.1", + "bitflags 2.13.1", + "hashbrown 0.17.1", "indoc", "instability", "itertools", "line-clipping", "ratatui-core", - "strum 0.27.2", + "serde", + "strum", "time", "unicode-segmentation", "unicode-width", @@ -4414,16 +4622,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.13.0", -] - -[[package]] -name = "redox_syscall" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4666a1a60d8412eab19d94f6d13dcc9cea0a5ef4fdf6a5db306537413c661b1b" -dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", ] [[package]] @@ -4434,14 +4633,14 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.17", "libredox", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] name = "regex" -version = "1.12.4" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" dependencies = [ "aho-corasick", "memchr", @@ -4451,9 +4650,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.14" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" dependencies = [ "aho-corasick", "memchr", @@ -4480,38 +4679,52 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] -name = "reqsign-aws-v4" -version = "3.0.0" +name = "reqsign-aws-core" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44eaca382e94505a49f1a4849658d153aebf79d9c1a58e5dd3b10361511e9f43" +checksum = "e4af084e1f3cbf3e67e0c972765399bce54ecec804cceba46b39a8331f3c1bff" dependencies = [ - "anyhow", "bytes", "form_urlencoded", + "hex", "http", "log", "percent-encoding", - "quick-xml 0.39.4", + "quick-xml 0.41.0", "reqsign-core", "rust-ini", "serde", "serde_json", "serde_urlencoded", - "sha1", + "sha1 0.11.0", +] + +[[package]] +name = "reqsign-aws-v4" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ac5b3b7cefa28933792b439186459f77f19f9b6edbeab41b8b187150361a206" +dependencies = [ + "bytes", + "http", + "log", + "quick-xml 0.41.0", + "reqsign-aws-core", + "reqsign-core", + "serde", ] [[package]] name = "reqsign-azure-storage" -version = "3.0.0" +version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a321980405d596bd34aaf95c4722a3de4128a67fd19e74a81a83aa3fdf082e6" +checksum = "2824e7da3c2cc42ac3406c674eb57c89127fdcd97f3a73c608cfc680505ea134" dependencies = [ "anyhow", - "base64", + "base64 0.23.1", "bytes", "form_urlencoded", "http", - "jsonwebtoken", "log", "pem", "percent-encoding", @@ -4519,36 +4732,38 @@ dependencies = [ "rsa", "serde", "serde_json", - "sha1", + "sha1 0.11.0", ] [[package]] name = "reqsign-core" -version = "3.0.0" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b10302cf0a7d7e7352ba211fc92c3c5bebf1286153e49cc5aa87348078a8e102" +checksum = "c07dd510b1e1b9b241883e483358147fb2ed2d497a7b39b065ba61eb93deceb0" dependencies = [ "anyhow", - "base64", + "base64 0.23.1", "bytes", - "form_urlencoded", "futures", "hex", - "hmac", + "hmac 0.13.0", "http", "jiff", "log", "percent-encoding", - "sha1", - "sha2", + "rsa", + "serde", + "serde_json", + "sha1 0.11.0", + "sha2 0.11.0", "windows-sys 0.61.2", ] [[package]] name = "reqsign-file-read-tokio" -version = "3.0.0" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d89295b3d17abea31851cc8de55d843d89c52132c864963c38d41920613dc5" +checksum = "663d9d55abd0df0830ef0ae43708297cc1371cf4e8ca91f3ac813c309cca8c98" dependencies = [ "anyhow", "reqsign-core", @@ -4557,13 +4772,12 @@ dependencies = [ [[package]] name = "reqsign-google" -version = "3.0.0" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35cc609b49c69e76ecaceb775a03f792d1ed3e7755ab3548d4534fd801e3242e" +checksum = "4080a227f82a09f68540ecd028622065d7ac4c0bcb8727a25bdcfc0526235792" dependencies = [ "form_urlencoded", "http", - "jsonwebtoken", "log", "percent-encoding", "reqsign-aws-v4", @@ -4571,7 +4785,6 @@ dependencies = [ "rsa", "serde", "serde_json", - "sha2", "tokio", ] @@ -4581,7 +4794,7 @@ version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-core", @@ -4620,11 +4833,11 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-core", "futures-util", @@ -4666,7 +4879,7 @@ dependencies = [ "cfg-if", "getrandom 0.2.17", "libc", - "untrusted 0.9.0", + "untrusted", "windows-sys 0.52.0", ] @@ -4686,15 +4899,15 @@ version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" dependencies = [ - "const-oid", - "digest", + "const-oid 0.9.6", + "digest 0.10.7", "num-bigint-dig", "num-integer", "num-traits", "pkcs1", "pkcs8", "rand_core 0.6.4", - "sha2", + "sha2 0.10.9", "signature", "spki", "subtle", @@ -4708,7 +4921,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c" dependencies = [ "hashbrown 0.16.1", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -4729,7 +4942,7 @@ version = "0.40.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6de2ed3a86b99876639d7e7af3ed30992a2f46a73131b5c9339c1c28d8aebea" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -4761,15 +4974,15 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" [[package]] name = "rustc-hash" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustc-stable-hash" @@ -4792,7 +5005,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "errno", "libc", "linux-raw-sys", @@ -4801,9 +5014,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.40" +version = "0.23.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" dependencies = [ "aws-lc-rs", "once_cell", @@ -4816,9 +5029,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -4828,9 +5041,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.1" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" dependencies = [ "web-time", "zeroize", @@ -4872,20 +5085,20 @@ dependencies = [ "aws-lc-rs", "ring", "rustls-pki-types", - "untrusted 0.9.0", + "untrusted", ] [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "ruzstd" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ff0cc5e135c8870a775d3320910cd9b564ec036b4dc0b8741629020be63f01" +checksum = "a7c1c839d570d835527c9a5e4db7cb2198683a988cb9d7293fc8674e6bd58fc8" [[package]] name = "ryu" @@ -4911,15 +5124,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "scc" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc" -dependencies = [ - "sdd", -] - [[package]] name = "schannel" version = "0.1.29" @@ -4952,7 +5156,7 @@ checksum = "ed76efe62313ab6610570951494bdaa81568026e0318eaa55f167de70eeea67d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -4963,22 +5167,16 @@ checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" dependencies = [ "pbkdf2", "salsa20", - "sha2", + "sha2 0.10.9", ] -[[package]] -name = "sdd" -version = "3.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" - [[package]] name = "security-framework" version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "core-foundation", "core-foundation-sys", "libc", @@ -5007,9 +5205,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -5017,29 +5215,29 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.3", ] [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", "memchr", @@ -5071,39 +5269,49 @@ dependencies = [ [[package]] name = "serial_test" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "911bd979bf1070a3f3aa7b691a3b3e9968f339ceeec89e08c280a8a22207a32f" +checksum = "699f4197115b8a7e7ff19c9a315a4bd6fffec26cc4626ef45ecaea389e081c6d" dependencies = [ "futures-executor", "futures-util", "log", "once_cell", "parking_lot", - "scc", "serial_test_derive", ] [[package]] name = "serial_test_derive" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a7d91949b85b0d2fb687445e448b40d322b6b3e4af6b44a29b21d9a5f33e6d9" +checksum = "94e153fc76e1c6a068703d6d29c508a0b15c061c4b7e43da59cc097bc342673c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] name = "sha1" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest", + "digest 0.10.7", +] + +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] @@ -5114,7 +5322,18 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] @@ -5191,21 +5410,21 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest", + "digest 0.10.7", "rand_core 0.6.4", ] [[package]] name = "simd-adler32" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" [[package]] name = "simd_cesu8" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" dependencies = [ "rustc_version", "simdutf8", @@ -5223,18 +5442,6 @@ version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" -[[package]] -name = "simple_asn1" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" -dependencies = [ - "num-bigint", - "num-traits", - "thiserror 2.0.18", - "time", -] - [[package]] name = "siphasher" version = "1.0.3" @@ -5265,9 +5472,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.3" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", "windows-sys 0.61.2", @@ -5288,15 +5495,15 @@ dependencies = [ [[package]] name = "spin" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" [[package]] name = "spin" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" +checksum = "023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3" dependencies = [ "lock_api", ] @@ -5347,34 +5554,13 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" -[[package]] -name = "strum" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" -dependencies = [ - "strum_macros 0.27.2", -] - [[package]] name = "strum" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" dependencies = [ - "strum_macros 0.28.0", -] - -[[package]] -name = "strum_macros" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.118", + "strum_macros", ] [[package]] @@ -5386,7 +5572,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -5431,9 +5617,20 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.118" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" dependencies = [ "proc-macro2", "quote", @@ -5457,7 +5654,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -5471,7 +5668,7 @@ dependencies = [ "compact_str", "micromath", "ratatui-core", - "thiserror 2.0.18", + "thiserror 2.0.19", "unicode-width", ] @@ -5490,9 +5687,9 @@ dependencies = [ "semver", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "smol_str", - "thiserror 2.0.18", + "thiserror 2.0.19", "toml-span", "twox-hash", ] @@ -5505,9 +5702,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tar" -version = "0.4.45" +version = "0.4.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" dependencies = [ "filetime", "libc", @@ -5516,9 +5713,9 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.13.3" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" [[package]] name = "tempfile" @@ -5527,7 +5724,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.2", + "getrandom 0.4.3", "once_cell", "rustix", "windows-sys 0.61.2", @@ -5542,6 +5739,19 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "termina" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9048a889effe34a5cddee0af7f53285198b16dca3be510858d38dfdb3e62a04e" +dependencies = [ + "bitflags 2.13.1", + "parking_lot", + "rustix", + "signal-hook", + "windows-sys 0.61.2", +] + [[package]] name = "terminfo" version = "0.9.0" @@ -5570,8 +5780,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" dependencies = [ "anyhow", - "base64", - "bitflags 2.13.0", + "base64 0.22.1", + "bitflags 2.13.1", "fancy-regex", "filedescriptor", "finl_unicode", @@ -5588,7 +5798,7 @@ dependencies = [ "pest", "pest_derive", "phf", - "sha2", + "sha2 0.10.9", "signal-hook", "siphasher", "terminfo", @@ -5616,11 +5826,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" dependencies = [ - "thiserror-impl 2.0.18", + "thiserror-impl 2.0.19", ] [[package]] @@ -5631,61 +5841,49 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.3", ] [[package]] name = "thread_local" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" dependencies = [ "cfg-if", ] [[package]] name = "time" -version = "0.3.47" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" dependencies = [ "deranged", - "itoa", "libc", "num-conv", "num_threads", "powerfmt", "serde_core", "time-core", - "time-macros", ] [[package]] name = "time-core" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" - -[[package]] -name = "time-macros" -version = "0.2.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" -dependencies = [ - "num-conv", - "time-core", -] +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "tiny-keccak" @@ -5708,9 +5906,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" dependencies = [ "tinyvec_macros", ] @@ -5723,9 +5921,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.2" +version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "110a78583f19d5cdb2c5ccf321d1290344e71313c6c37d43520d386027d18386" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ "bytes", "libc", @@ -5738,13 +5936,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.0" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.3", ] [[package]] @@ -5759,22 +5957,23 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.18" +version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" dependencies = [ "bytes", "futures-core", "futures-sink", + "libc", "pin-project-lite", "tokio", ] [[package]] name = "toml" -version = "1.1.2+spec-1.1.0" +version = "1.1.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" dependencies = [ "serde_core", "serde_spanned", @@ -5804,9 +6003,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" dependencies = [ "winnow", ] @@ -5828,11 +6027,11 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.9" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28f0d049ccfaa566e14e9663d304d8577427b368cb4710a20528690287a738b" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "bytes", "futures-util", "http", @@ -5875,7 +6074,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -5921,9 +6120,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tui-widget-list" -version = "0.15.2" +version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fbf7f6c7d04beb3602630d83db44e81d7a1de1fea11ca3435d82e094c5c0192" +checksum = "39fc4e2d4f0673ad55213e1db3203b2f5962d8e99acbc54faa59318c435b010a" dependencies = [ "ratatui-core", "ratatui-widgets", @@ -5931,15 +6130,15 @@ dependencies = [ [[package]] name = "twox-hash" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" +checksum = "8464ec13c3691491391d9fce00f6416c9a48e46972f72d7865688be2080192c9" [[package]] name = "typenum" -version = "1.20.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "ucd-trie" @@ -5979,9 +6178,9 @@ dependencies = [ [[package]] name = "unicode-segmentation" -version = "1.13.2" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-truncate" @@ -6000,12 +6199,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - [[package]] name = "unindent" version = "0.2.4" @@ -6018,12 +6211,6 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" -[[package]] -name = "untrusted" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" - [[package]] name = "untrusted" version = "0.9.0" @@ -6051,12 +6238,6 @@ dependencies = [ "regex", ] -[[package]] -name = "utf8-width" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -6071,12 +6252,12 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.1" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" dependencies = [ "atomic", - "getrandom 0.4.2", + "getrandom 0.4.3", "js-sys", "serde_core", "wasm-bindgen", @@ -6140,23 +6321,14 @@ version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ - "wit-bindgen 0.57.1", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen 0.51.0", + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.120" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -6167,9 +6339,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.70" +version = "0.4.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" dependencies = [ "js-sys", "wasm-bindgen", @@ -6177,9 +6349,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.120" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6187,48 +6359,26 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.120" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.120" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - [[package]] name = "wasm-streams" version = "0.4.2" @@ -6255,23 +6405,11 @@ dependencies = [ "web-sys", ] -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags 2.13.0", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - [[package]] name = "web-sys" -version = "0.3.97" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" dependencies = [ "js-sys", "wasm-bindgen", @@ -6289,15 +6427,15 @@ dependencies = [ [[package]] name = "webbrowser" -version = "1.2.1" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fc95580916af1e68ff6a7be07446fc5db73ebf71cf092de939bbf5f7e189f72" +checksum = "3d6eb50cc7b133f0c7720a64661fbd8e520b882f8ab9015deea5e00d5cd809c4" dependencies = [ - "core-foundation", "jni", "log", "ndk-context", "objc2", + "objc2-app-kit", "objc2-foundation", "url", "web-sys", @@ -6305,18 +6443,18 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.7" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" dependencies = [ "rustls-pki-types", ] [[package]] name = "webpki-roots" -version = "1.0.7" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" dependencies = [ "rustls-pki-types", ] @@ -6339,7 +6477,7 @@ checksum = "692daff6d93d94e29e4114544ef6d5c942a7ed998b37abdc19b17136ea428eb7" dependencies = [ "getrandom 0.3.4", "mac_address", - "sha2", + "sha2 0.10.9", "thiserror 1.0.69", "uuid", ] @@ -6445,7 +6583,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -6456,7 +6594,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -6641,18 +6779,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" - -[[package]] -name = "wit-bindgen" -version = "0.51.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" [[package]] name = "wit-bindgen" @@ -6660,85 +6789,6 @@ version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn 2.0.118", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn 2.0.118", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags 2.13.0", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - [[package]] name = "writeable" version = "0.6.3" @@ -6766,9 +6816,9 @@ dependencies = [ [[package]] name = "xxhash-rust" -version = "0.8.16" +version = "0.8.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d93c89cdc2d3a63c3ec48ffe926931bdc069eafa8e4402fe6d8f790c9d1e576" +checksum = "aee1b19627c7c60102ab80d3a9cbe18de90bfe03bfa6c3715447681f0e8c8af6" [[package]] name = "xz2" @@ -6781,9 +6831,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -6798,35 +6848,35 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.48" +version = "0.8.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.48" +version = "0.8.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] name = "zerofrom" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] @@ -6839,28 +6889,28 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", "synstructure", ] [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -6893,7 +6943,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -6912,13 +6962,13 @@ dependencies = [ "displaydoc", "flate2", "getrandom 0.3.4", - "hmac", + "hmac 0.12.1", "indexmap", "lzma-rs", "memchr", "pbkdf2", - "sha1", - "thiserror 2.0.18", + "sha1 0.10.7", + "thiserror 2.0.19", "time", "xz2", "zeroize", @@ -6928,15 +6978,15 @@ dependencies = [ [[package]] name = "zlib-rs" -version = "0.6.3" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513" +checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" [[package]] name = "zmij" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" [[package]] name = "zopfli" diff --git a/Cargo.toml b/Cargo.toml index ca0e48c1..d2da7741 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,8 +21,8 @@ name = "gen" path = "src/main.rs" [workspace] -members = [".", "gen-core", "gen-models", "gen-graph", "gen-diff", "gen-tui", "gen-sugiyama", "gen-annotations", "gen-capnp-schemas"] -default-members = [".", "gen-core", "gen-models", "gen-graph", "gen-diff", "gen-capnp-schemas", "gen-annotations"] +members = [".", "gen-core", "gen-models", "gen-models/graph-tests", "gen-graph", "gen-diff", "gen-tui", "gen-sugiyama", "gen-annotations", "gen-capnp-schemas"] +default-members = [".", "gen-core", "gen-models", "gen-models/graph-tests", "gen-graph", "gen-diff", "gen-capnp-schemas", "gen-annotations"] exclude = ["gen-python", "gen-r/src/rust"] [features] @@ -93,6 +93,7 @@ pprof = { version = "0.15.0", optional = true } [dev-dependencies] cargo-llvm-cov = "0.8.5" cargo-deny = "0.19.0" +gen-models-graph-tests = { path = "gen-models/graph-tests" } more-asserts = "0.3.1" insta = "1.43.1" diff --git a/gen-annotations/src/gff.rs b/gen-annotations/src/gff.rs index 6cc35972..c172e7af 100644 --- a/gen-annotations/src/gff.rs +++ b/gen-annotations/src/gff.rs @@ -304,7 +304,8 @@ mod tests { preserve_edge: true, }; - BlockGroup::insert_change(conn, &change).expect("should apply AA update to child sample"); + gen_graph::models::insert_change(conn, &change) + .expect("should apply AA update to child sample"); let edge_to_insert = Edge::query( conn, diff --git a/gen-annotations/src/test_helpers.rs b/gen-annotations/src/test_helpers.rs index f9736029..b49a1c2a 100644 --- a/gen-annotations/src/test_helpers.rs +++ b/gen-annotations/src/test_helpers.rs @@ -190,5 +190,5 @@ pub fn setup_test_data(conn: &GraphConnection) { preserve_edge: false, }; - BlockGroup::insert_change(conn, &change).expect("should apply variant change"); + gen_graph::models::insert_change(conn, &change).expect("should apply variant change"); } diff --git a/gen-annotations/src/translate/bed.rs b/gen-annotations/src/translate/bed.rs index cdb9ab2f..c865f066 100644 --- a/gen-annotations/src/translate/bed.rs +++ b/gen-annotations/src/translate/bed.rs @@ -77,7 +77,8 @@ where Entry::Occupied(entry) => entry.into_mut(), Entry::Vacant(entry) => { let path = BlockGroup::get_current_path(conn, &bg.id, history_ref)?; - let graph = BlockGroup::get_graph(conn, &bg.id, history_ref)?; + let graph = + gen_graph::models::load_block_group_graph(conn, &bg.id, history_ref)?; let mut tree = IntervalTree::default(); let mut position: i64 = 0; for (node, strand) in project_path(&graph, &path.blocks(conn, history_ref)?) { diff --git a/gen-annotations/src/translate/gff.rs b/gen-annotations/src/translate/gff.rs index b8606d88..72443945 100644 --- a/gen-annotations/src/translate/gff.rs +++ b/gen-annotations/src/translate/gff.rs @@ -72,7 +72,8 @@ where Entry::Occupied(entry) => entry.into_mut(), Entry::Vacant(entry) => { let path = BlockGroup::get_current_path(conn, &bg.id, history_ref)?; - let graph = BlockGroup::get_graph(conn, &bg.id, history_ref)?; + let graph = + gen_graph::models::load_block_group_graph(conn, &bg.id, history_ref)?; let mut tree = IntervalTree::default(); let mut position: i64 = 0; for (node, strand) in project_path(&graph, &path.blocks(conn, history_ref)?) { diff --git a/gen-core/Cargo.toml b/gen-core/Cargo.toml index ee37463d..9259ad38 100644 --- a/gen-core/Cargo.toml +++ b/gen-core/Cargo.toml @@ -16,6 +16,7 @@ gen-capnp-schemas = { path = "../gen-capnp-schemas", version = "0.2.1" } itertools = "0.14.0" capnp = { version = "0.24" } noodles = { version = "0.101.0", features = ["gff"] } +petgraph = "0.6.5" rusqlite = { package = "rusqdoltlite", version = "0.40.14", features = ["bundled", "fallible_uint"] } serde = { version = "1.0.219", features = ["derive"] } thiserror = "1.0" diff --git a/gen-core/src/graph.rs b/gen-core/src/graph.rs new file mode 100644 index 00000000..f8de6527 --- /dev/null +++ b/gen-core/src/graph.rs @@ -0,0 +1,101 @@ +use core::fmt; + +use petgraph::graphmap::DiGraphMap; +use serde::{Deserialize, Serialize}; + +use crate::{HashId, Strand}; + +pub type GenGraph = DiGraphMap>; + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd, Deserialize, Serialize)] +pub struct GraphEdge { + pub edge_id: HashId, + pub source_strand: Strand, + pub target_strand: Strand, + pub chromosome_index: i64, + pub phased: i64, + pub created_on: i64, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct GraphLoadBlock { + pub id: i64, + pub node_id: HashId, + pub start: i64, + pub end: i64, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct GraphLoadEdge { + pub edge_id: HashId, + pub source_node_id: HashId, + pub source_coordinate: i64, + pub source_strand: Strand, + pub target_node_id: HashId, + pub target_coordinate: i64, + pub target_strand: Strand, + pub chromosome_index: i64, + pub phased: i64, + pub created_on: i64, +} + +/// A contiguous slice of a stored sequence represented as a node in graph space. +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd, Deserialize, Serialize)] +pub struct GraphNode { + pub node_id: HashId, + pub sequence_start: i64, + pub sequence_end: i64, +} + +impl fmt::Display for GraphNode { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + formatter, + "{}[{}-{}]", + self.node_id, self.sequence_start, self.sequence_end + ) + } +} + +impl GraphNode { + pub const fn length(&self) -> i64 { + self.sequence_end - self.sequence_start + } +} + +/// A local sequence slice within a graph node, including its traversal orientation. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct GraphNodeSlice { + pub block: GraphNode, + /// Local start offset within the block's sequence slice (`0..block.length()`). + pub start: usize, + /// Local end offset, exclusive (`start..=block.length()`). + pub end: usize, + /// 5'→3' orientation of this slice. + pub strand: Strand, +} + +impl GraphNodeSlice { + pub fn full(block: GraphNode, strand: Strand) -> Self { + Self { + block, + start: 0, + end: block.length() as usize, + strand, + } + } +} + +/// A cursor within a graph node. +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd, Deserialize, Serialize)] +pub struct GraphNodePosition { + pub graph_node: GraphNode, + /// Distance from `sequence_start` of `graph_node`. + pub offset: i64, +} + +impl GraphNodePosition { + pub const fn coordinate(&self) -> i64 { + self.graph_node.sequence_start + self.offset + } +} diff --git a/gen-core/src/lib.rs b/gen-core/src/lib.rs index 7bfc2f57..d601aad2 100644 --- a/gen-core/src/lib.rs +++ b/gen-core/src/lib.rs @@ -8,6 +8,7 @@ pub mod config; pub mod errors; #[allow(clippy::all)] pub mod generated; +pub mod graph; pub mod path; pub mod range; pub mod region; @@ -17,6 +18,10 @@ pub mod traits; pub use config::Workspace; use errors::HashError; pub use generated::gen_core_capnp; +pub use graph::{ + GenGraph, GraphEdge, GraphLoadBlock, GraphLoadEdge, GraphNode, GraphNodePosition, + GraphNodeSlice, +}; pub use path::PathBlock; #[cfg(feature = "python-bindings")] use pyo3::pyclass; diff --git a/gen-graph/Cargo.toml b/gen-graph/Cargo.toml index a24938fc..96356ad7 100644 --- a/gen-graph/Cargo.toml +++ b/gen-graph/Cargo.toml @@ -9,8 +9,10 @@ license = "Apache-2.0" [dependencies] gen-core = { path = "../gen-core", version = "0.2.1" } +gen-models = { path = "../gen-models", version = "0.2.1" } interavl = "0.3.0" intervaltree = "0.2.7" +itertools = "0.14.0" petgraph = "0.6.5" serde = { version = "1.0.219", features = ["derive"] } thiserror = "2.0.12" diff --git a/gen-graph/src/graph_loader.rs b/gen-graph/src/graph_loader.rs new file mode 100644 index 00000000..e401de44 --- /dev/null +++ b/gen-graph/src/graph_loader.rs @@ -0,0 +1,659 @@ +use std::collections::{HashMap, HashSet, VecDeque}; + +pub use gen_core::{GraphLoadBlock, GraphLoadEdge}; +use gen_core::{ + GraphNodePosition, HashId, INDETERMINATE_CHROMOSOME_INDEX, NO_CHROMOSOME_INDEX, + NodeIntervalBlock, PATH_END_NODE_ID, PATH_START_NODE_ID, PRESERVE_EDIT_SITE_CHROMOSOME_INDEX, + PathBlock, Strand, is_end_node, is_start_node, is_terminal, +}; +use intervaltree::IntervalTree; +use itertools::Itertools; +use petgraph::Direction; + +use crate::{ + GenGraph, GraphEdge, GraphError, GraphNode, MergeGraph, all_reachable_nodes, all_simple_paths, +}; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct RegionPositionQuery { + pub start: i64, + pub end: i64, + pub start_offset: i64, + pub end_offset: i64, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct PlannedEdge { + pub source_node_id: HashId, + pub source_coordinate: i64, + pub source_strand: Strand, + pub target_node_id: HashId, + pub target_coordinate: i64, + pub target_strand: Strand, + pub chromosome_index: i64, + pub phased: i64, +} + +/// Resolves a linear region and its offsets to positions in an expandable graph. +/// +/// Callers retain responsibility for persistence by supplying node lengths and graph expansion. +pub fn resolve_region_positions( + interval_tree: &IntervalTree, + query: RegionPositionQuery, + mut node_length: impl FnMut(HashId) -> Option, + mut expand: impl FnMut(&mut GenGraph, HashId) -> bool, +) -> Result<(Vec, Vec), GraphError> { + let filtered_tree = interval_tree + .iter() + .filter(|item| !is_terminal(item.value.node_id)) + .map(|item| (item.range.clone(), item.value)) + .collect::>(); + let mut graph = crate::graph_from_interval_tree(&filtered_tree); + let start_anchor = resolve_anchor( + &graph, + &filtered_tree, + query.start, + &mut node_length, + &mut expand, + )?; + let end_anchor = resolve_anchor( + &graph, + &filtered_tree, + query.end, + &mut node_length, + &mut expand, + )?; + let start_positions = find_offset(&mut graph, &start_anchor, query.start_offset, &mut expand)?; + let end_positions = find_offset(&mut graph, &end_anchor, query.end_offset, &mut expand)?; + Ok((start_positions, end_positions)) +} + +/// Returns every non-terminal graph position covering a linear coordinate. +pub fn positions_at_coordinate( + interval_tree: &IntervalTree, + coordinate: i64, +) -> Vec { + let mut positions = interval_tree + .query_point(coordinate) + .map(|entry| entry.value) + .filter(|block| !is_terminal(block.node_id)) + .map(|block| GraphNodePosition { + graph_node: GraphNode { + node_id: block.node_id, + sequence_start: block.sequence_start, + sequence_end: block.sequence_end, + }, + offset: coordinate - block.start, + }) + .collect::>(); + positions.sort(); + positions.dedup(); + positions +} + +/// Plans graph edges that preserve an edit site and connect a replacement block. +pub fn plan_region_edges( + start_positions: &[GraphNodePosition], + end_positions: &[GraphNodePosition], + block: &PathBlock, + preserve_edge: bool, + chromosome_index: i64, + phased: i64, +) -> Vec { + let preserve_chromosome_index = if preserve_edge { + 0 + } else { + PRESERVE_EDIT_SITE_CHROMOSOME_INDEX + }; + let mut edges = Vec::new(); + + for position in start_positions.iter().chain(end_positions) { + if is_terminal(position.graph_node.node_id) { + continue; + } + let coordinate = position.coordinate(); + edges.push(PlannedEdge { + source_node_id: position.graph_node.node_id, + source_coordinate: coordinate, + source_strand: Strand::Forward, + target_node_id: position.graph_node.node_id, + target_coordinate: coordinate, + target_strand: Strand::Forward, + chromosome_index: preserve_chromosome_index, + phased: 0, + }); + } + + if block.sequence_start == block.sequence_end { + for start_position in start_positions { + for end_position in end_positions { + edges.push(PlannedEdge { + source_node_id: start_position.graph_node.node_id, + source_coordinate: start_position.coordinate(), + source_strand: Strand::Forward, + target_node_id: end_position.graph_node.node_id, + target_coordinate: end_position.coordinate(), + target_strand: Strand::Forward, + chromosome_index, + phased, + }); + } + } + } else { + for start_position in start_positions { + edges.push(PlannedEdge { + source_node_id: start_position.graph_node.node_id, + source_coordinate: start_position.coordinate(), + source_strand: Strand::Forward, + target_node_id: block.node_id, + target_coordinate: block.sequence_start, + target_strand: Strand::Forward, + chromosome_index, + phased, + }); + } + for end_position in end_positions { + edges.push(PlannedEdge { + source_node_id: block.node_id, + source_coordinate: block.sequence_end, + source_strand: Strand::Forward, + target_node_id: end_position.graph_node.node_id, + target_coordinate: end_position.coordinate(), + target_strand: Strand::Forward, + chromosome_index, + phased, + }); + } + } + + edges +} + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +struct BlockKey { + node_id: HashId, + coordinate: i64, +} + +/// Finds every graph position a signed sequence distance from an anchor. +/// +/// The callback can add neighboring nodes when traversal reaches the boundary of the currently +/// loaded graph. It is first used at dead ends and then along existing paths if no result was +/// found. +pub fn find_offset( + graph: &mut GenGraph, + anchor: &GraphNodePosition, + distance: i64, + mut expand: impl FnMut(&mut GenGraph, HashId) -> bool, +) -> Result, GraphError> { + if distance == 0 { + return Ok(vec![*anchor]); + } + + match find_offset_with_optional_expansion(graph, anchor, distance, false, &mut expand) { + Ok(results) => Ok(results), + Err(GraphError::OutOfBounds(_)) => { + find_offset_with_optional_expansion(graph, anchor, distance, true, &mut expand) + } + Err(error) => Err(error), + } +} + +/// Resolves a linear coordinate to a graph position, expanding beyond the loaded interval when +/// necessary. +/// +/// Persistence stays outside this function: callers provide node lengths and graph fragments +/// through callbacks. +pub fn resolve_anchor( + graph: &GenGraph, + interval_tree: &IntervalTree, + coordinate: i64, + mut node_length: impl FnMut(HashId) -> Option, + mut expand: impl FnMut(&mut GenGraph, HashId) -> bool, +) -> Result { + let block = interval_tree + .query_point(coordinate) + .map(|item| item.value) + .find(|block| !is_terminal(block.node_id)); + if let Some(block) = block { + return Ok(GraphNodePosition { + graph_node: GraphNode { + node_id: block.node_id, + sequence_start: block.sequence_start, + sequence_end: block.sequence_end, + }, + offset: coordinate - block.start, + }); + } + + let last_block = interval_tree + .iter() + .map(|item| item.value) + .filter(|block| !is_terminal(block.node_id)) + .max_by_key(|block| block.end); + let before_tree = coordinate < 0; + let after_tree = last_block + .map(|block| coordinate >= block.end) + .unwrap_or(false); + let boundary = if before_tree { + interval_tree + .iter() + .map(|item| item.value) + .filter(|block| !is_terminal(block.node_id)) + .min_by_key(|block| block.start) + } else if after_tree { + last_block + } else { + None + } + .ok_or(GraphError::NoPath)?; + + let boundary_node = GraphNode { + node_id: boundary.node_id, + sequence_start: boundary.sequence_start, + sequence_end: boundary.sequence_end, + }; + let boundary_anchor = GraphNodePosition { + graph_node: boundary_node, + offset: if before_tree { + 0 + } else { + boundary_node.length() + }, + }; + let boundary_distance = if before_tree { + coordinate - boundary.start + } else { + coordinate - boundary.end + }; + let same_node_coordinate = if before_tree { + boundary.sequence_start + boundary_distance + } else { + boundary.sequence_end + boundary_distance + }; + if same_node_coordinate >= 0 + && node_length(boundary.node_id).is_some_and(|length| same_node_coordinate <= length) + { + return Ok(GraphNodePosition { + graph_node: boundary_node, + offset: boundary_anchor.offset + boundary_distance, + }); + } + + let mut expanded_graph = graph.clone(); + expand(&mut expanded_graph, boundary_node.node_id); + let anchors = find_offset( + &mut expanded_graph, + &boundary_anchor, + boundary_distance, + expand, + )?; + + anchors.into_iter().next().ok_or(GraphError::NoPath) +} + +/// Returns candidate edge identifiers that are not already present in the loaded graph. +pub fn unloaded_edge_ids( + graph: &GenGraph, + candidate_edge_ids: impl IntoIterator, +) -> HashSet { + let loaded_edge_ids = graph + .all_edges() + .flat_map(|(_, _, edges)| edges.iter().map(|edge| edge.edge_id)) + .collect::>(); + candidate_edge_ids + .into_iter() + .filter(|edge_id| !loaded_edge_ids.contains(edge_id)) + .collect() +} + +/// Merges a newly constructed local fragment into a loaded graph. +pub fn merge_fragment(graph: &mut GenGraph, fragment: &GenGraph) { + graph.merge_graph(fragment); +} + +fn find_offset_with_optional_expansion( + graph: &mut GenGraph, + anchor: &GraphNodePosition, + distance: i64, + expand_through_existing_paths: bool, + expand: &mut impl FnMut(&mut GenGraph, HashId) -> bool, +) -> Result, GraphError> { + let forward = distance > 0; + let mut queue = VecDeque::new(); + let mut visited = HashSet::new(); + let mut results = Vec::new(); + let mut result_seen = HashSet::new(); + + queue.push_back((*anchor, distance)); + visited.insert((*anchor, distance)); + + while let Some((position, remaining)) = queue.pop_front() { + let node = position.graph_node; + let node_length = node.length(); + + if remaining == 0 { + if result_seen.insert(position) { + results.push(position); + } + continue; + } + + if forward { + let remaining_in_node = node_length - position.offset; + if remaining <= remaining_in_node { + let result = GraphNodePosition { + graph_node: node, + offset: position.offset + remaining, + }; + if result_seen.insert(result) { + results.push(result); + } + continue; + } + + let mut neighbors = graph + .neighbors_directed(node, Direction::Outgoing) + .collect::>(); + if (neighbors.is_empty() || expand_through_existing_paths) + && expand(graph, node.node_id) + { + neighbors = graph + .neighbors_directed(node, Direction::Outgoing) + .collect(); + } + + for neighbor in neighbors { + let next_position = GraphNodePosition { + graph_node: neighbor, + offset: 0, + }; + let next_remaining = remaining - remaining_in_node; + if visited.insert((next_position, next_remaining)) { + queue.push_back((next_position, next_remaining)); + } + } + } else { + let remaining_in_node = position.offset; + if -remaining <= remaining_in_node { + let result = GraphNodePosition { + graph_node: node, + offset: position.offset + remaining, + }; + if result_seen.insert(result) { + results.push(result); + } + continue; + } + + let mut neighbors = graph + .neighbors_directed(node, Direction::Incoming) + .collect::>(); + if (neighbors.is_empty() || expand_through_existing_paths) + && expand(graph, node.node_id) + { + neighbors = graph + .neighbors_directed(node, Direction::Incoming) + .collect(); + } + + for neighbor in neighbors { + let next_position = GraphNodePosition { + graph_node: neighbor, + offset: neighbor.length(), + }; + let next_remaining = remaining + remaining_in_node; + if visited.insert((next_position, next_remaining)) { + queue.push_back((next_position, next_remaining)); + } + } + } + } + + if results.is_empty() { + return Err(GraphError::OutOfBounds(distance)); + } + + Ok(results) +} + +fn graph_node(block: &GraphLoadBlock) -> GraphNode { + GraphNode { + node_id: block.node_id, + sequence_start: block.start, + sequence_end: block.end, + } +} + +fn select_junction_or_sequence_blocks<'a>( + blocks: &[&'a GraphLoadBlock], +) -> Vec<&'a GraphLoadBlock> { + let junctions = blocks + .iter() + .copied() + .filter(|block| block.start == block.end) + .collect::>(); + if junctions.is_empty() { + blocks.to_vec() + } else { + junctions + } +} + +fn block_connections<'a>( + edge: &GraphLoadEdge, + source_blocks: &[&'a GraphLoadBlock], + target_blocks: &[&'a GraphLoadBlock], +) -> Vec<(&'a GraphLoadBlock, &'a GraphLoadBlock)> { + if edge.source_node_id == edge.target_node_id + && edge.source_coordinate == edge.target_coordinate + { + let source_sequence_blocks = source_blocks + .iter() + .copied() + .filter(|block| block.start != block.end) + .collect::>(); + let target_sequence_blocks = target_blocks + .iter() + .copied() + .filter(|block| block.start != block.end) + .collect::>(); + let source_junctions = source_blocks + .iter() + .copied() + .filter(|block| block.start == block.end) + .collect::>(); + let target_junctions = target_blocks + .iter() + .copied() + .filter(|block| block.start == block.end) + .collect::>(); + + if source_junctions.is_empty() && target_junctions.is_empty() { + return source_blocks + .iter() + .copied() + .cartesian_product(target_blocks.iter().copied()) + .collect(); + } + + return source_sequence_blocks + .into_iter() + .cartesian_product(target_junctions) + .chain( + source_junctions + .into_iter() + .cartesian_product(target_sequence_blocks), + ) + .collect(); + } + + select_junction_or_sequence_blocks(source_blocks) + .into_iter() + .cartesian_product(select_junction_or_sequence_blocks(target_blocks)) + .collect() +} + +/// Builds an in-memory sequence graph from records already loaded by the model layer. +pub fn build_graph( + edges: &[GraphLoadEdge], + blocks: &[GraphLoadBlock], +) -> (GenGraph, HashMap<(i64, i64), HashId>) { + let blocks_by_start = blocks + .iter() + .map(|block| { + ( + BlockKey { + node_id: block.node_id, + coordinate: block.start, + }, + block, + ) + }) + .into_group_map(); + let blocks_by_end = blocks + .iter() + .map(|block| { + ( + BlockKey { + node_id: block.node_id, + coordinate: block.end, + }, + block, + ) + }) + .into_group_map(); + + let mut graph = GenGraph::new(); + let mut edges_by_node_pair = HashMap::new(); + for block in blocks { + graph.add_node(graph_node(block)); + } + for edge in edges { + let source_key = BlockKey { + node_id: edge.source_node_id, + coordinate: edge.source_coordinate, + }; + let target_key = BlockKey { + node_id: edge.target_node_id, + coordinate: edge.target_coordinate, + }; + if let Some(source_blocks) = blocks_by_end.get(&source_key) + && let Some(target_blocks) = blocks_by_start.get(&target_key) + { + for (source_block, target_block) in + block_connections(edge, source_blocks, target_blocks) + { + let source_node = graph_node(source_block); + let target_node = graph_node(target_block); + let graph_edge = GraphEdge { + edge_id: edge.edge_id, + source_strand: edge.source_strand, + target_strand: edge.target_strand, + chromosome_index: edge.chromosome_index, + phased: edge.phased, + created_on: edge.created_on, + }; + if let Some(existing_edges) = graph.edge_weight_mut(source_node, target_node) { + existing_edges.push(graph_edge); + } else { + graph.add_edge(source_node, target_node, vec![graph_edge]); + } + edges_by_node_pair.insert((source_block.id, target_block.id), edge.edge_id); + } + } + } + + (graph, edges_by_node_pair) +} + +/// Removes superseded chromosome paths and nodes that are no longer reachable from a graph root. +pub fn prune_graph(graph: &mut GenGraph) { + let mut root_nodes = HashSet::new(); + let mut edges_to_remove = Vec::new(); + for node in graph.nodes() { + if node.node_id == PATH_START_NODE_ID { + root_nodes.insert(node); + } + let mut edges_by_chromosome = HashMap::new(); + for (source_node, target_node, edge_weights) in graph.edges(node) { + for edge_weight in edge_weights { + if edge_weight.chromosome_index == NO_CHROMOSOME_INDEX + || edge_weight.chromosome_index == INDETERMINATE_CHROMOSOME_INDEX + { + continue; + } + if edge_weight.chromosome_index == PRESERVE_EDIT_SITE_CHROMOSOME_INDEX { + edges_to_remove.push((source_node, target_node)); + continue; + } + edges_by_chromosome + .entry(edge_weight.chromosome_index) + .and_modify( + |(source, target, created_on): &mut (GraphNode, GraphNode, i64)| { + if edge_weight.created_on > *created_on { + edges_to_remove.push((*source, *target)); + *source = source_node; + *target = target_node; + *created_on = edge_weight.created_on; + } else { + edges_to_remove.push((source_node, target_node)); + } + }, + ) + .or_insert((source_node, target_node, edge_weight.created_on)); + } + } + } + + for (source, target) in edges_to_remove { + graph.remove_edge(source, target); + } + + let reachable_nodes = all_reachable_nodes(&*graph, &Vec::from_iter(root_nodes)); + let nodes_to_remove = graph + .nodes() + .filter(|node| !reachable_nodes.contains(node)) + .collect::>(); + for node in nodes_to_remove { + graph.remove_node(node); + } +} + +/// Enumerates the sequence spelled by every simple start-to-end path in a loaded graph. +pub fn get_all_sequences( + graph: &GenGraph, + sequences_by_node: &HashMap, +) -> HashSet { + let start_nodes = graph + .nodes() + .filter(|node| is_start_node(node.node_id)) + .collect::>(); + let end_nodes = graph + .nodes() + .filter(|node| is_end_node(node.node_id)) + .collect::>(); + let mut sequences = HashSet::new(); + + for start_node in start_nodes { + for end_node in &end_nodes { + if start_node == *end_node { + if start_node.node_id != PATH_START_NODE_ID + && start_node.node_id != PATH_END_NODE_ID + { + sequences.insert(sequences_by_node[&start_node].clone()); + } + continue; + } + for path in all_simple_paths(graph, start_node, *end_node) { + let mut sequence = String::new(); + for node in path { + sequence.push_str(&sequences_by_node[&node]); + } + sequences.insert(sequence); + } + } + } + + sequences +} diff --git a/gen-graph/src/lib.rs b/gen-graph/src/lib.rs index fda08f19..75e1f30c 100644 --- a/gen-graph/src/lib.rs +++ b/gen-graph/src/lib.rs @@ -1,6 +1,6 @@ use std::{ collections::{HashMap, HashSet, VecDeque}, - fmt::{self, Debug}, + fmt::Debug, hash::Hash, iter::from_fn, rc::Rc, @@ -21,87 +21,16 @@ use petgraph::{ NodeCount, Reversed, }, }; -use serde::{Deserialize, Serialize}; use thiserror::Error; +pub mod graph_loader; +pub mod models; pub mod traits; +pub use gen_core::{GenGraph, GraphEdge, GraphNode, GraphNodePosition, GraphNodeSlice}; pub use traits::MergeGraph; -#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd, Deserialize, Serialize)] -pub struct GraphNode { - pub node_id: HashId, - pub sequence_start: i64, - pub sequence_end: i64, -} - -impl fmt::Display for GraphNode { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}[{}-{}]", - self.node_id, self.sequence_start, self.sequence_end - ) - } -} - -impl GraphNode { - pub fn length(&self) -> i64 { - self.sequence_end - self.sequence_start - } -} - -/// A slice of a single graph block: the block itself plus local start/end byte -/// offsets within that block's sequence. Middle blocks in a multi-block locus -/// span the full block (`start = 0`, `end = block.length()`). -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct GraphNodeSlice { - pub block: GraphNode, - /// Local start offset within the block's sequence slice (`0..block.length()`). - pub start: usize, - /// Local end offset, exclusive (`start..=block.length()`). - pub end: usize, - /// 5'→3' orientation of this slice. - pub strand: Strand, -} - -impl GraphNodeSlice { - pub fn full(block: GraphNode, strand: Strand) -> Self { - Self { - block, - start: 0, - end: block.length() as usize, - strand, - } - } -} - -pub type GenGraph = DiGraphMap>; pub type OperationGraph = DiGraphMap; -#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd, Deserialize, Serialize)] -pub struct GraphEdge { - pub edge_id: HashId, - pub source_strand: Strand, - pub target_strand: Strand, - pub chromosome_index: i64, - pub phased: i64, - pub created_on: i64, -} - -/// A cursor within a GraphNode -#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd, Deserialize, Serialize)] -pub struct GraphNodePosition { - pub graph_node: GraphNode, - /// Distance from sequence_start of graph_node - pub offset: i64, -} - -impl GraphNodePosition { - pub fn coordinate(&self) -> i64 { - self.graph_node.sequence_start + self.offset - } -} - #[derive(Debug, Error)] pub enum GraphError { #[error("Distance {0} exceeds graph boundaries")] diff --git a/gen-graph/src/models.rs b/gen-graph/src/models.rs new file mode 100644 index 00000000..0fe8b5af --- /dev/null +++ b/gen-graph/src/models.rs @@ -0,0 +1,435 @@ +use std::collections::{HashMap, HashSet}; + +use gen_core::{ + GenGraph, GraphNode, GraphNodePosition, HashId, NodeIntervalBlock, + PRESERVE_EDIT_SITE_CHROMOSOME_INDEX, PathBlock, +}; +use gen_models::{ + accession::AccessionSpan, + annotations::persist_annotation, + block_group::{ + BlockGroup, BlockGroupChange, BlockGroupError, IntervalTreeCache, IntervalTreeSource, + SubgraphBoundary, + }, + block_group_edge::{AugmentedEdgeData, BlockGroupEdge}, + db::GraphConnection, + edge::{Edge, EdgeData}, + node::Node, + operations::OperationSummary, + region::{ResolvedGenRegion, ResolvedRegionKind}, + sample::{Sample, SampleError}, +}; +use intervaltree::IntervalTree; + +use crate::{GraphError, all_intermediate_edges, flatten_to_interval_tree, graph_loader}; + +pub fn add_annotation( + context: &gen_models::db::DbContext, + collection: &str, + name: &str, + group: Option<&str>, + sample: &str, + region: &str, +) -> Result> { + let conn = context.graph().conn(); + let parsed_region = gen_core::region::Region::parse(region)?; + let resolved_region = gen_models::region::resolve(&parsed_region, conn, collection, sample)?; + let tree = region_intervaltree(&resolved_region, conn)?; + let range = resolved_region.start..resolved_region.end; + let spans = AccessionSpan::from_intervaltree_ranges(&tree, core::slice::from_ref(&range))?; + persist_annotation(context, name, group, sample, &resolved_region, spans) +} + +pub fn prune_graph(graph: &mut GenGraph) { + graph_loader::prune_graph(graph); +} + +#[expect( + clippy::too_many_arguments, + reason = "subgraph persistence requires both graph boundaries" +)] +pub fn derive_subgraph( + conn: &GraphConnection, + source_block_group_id: &HashId, + start_block: &NodeIntervalBlock, + end_block: &NodeIntervalBlock, + start_node_coordinate: i64, + end_node_coordinate: i64, + target_block_group_id: &HashId, + create_terminal_edges: bool, +) -> Result<(), BlockGroupError> { + let graph = load_block_group_graph(conn, source_block_group_id, None)?; + let start_node = graph + .nodes() + .find(|node| { + node.node_id == start_block.node_id + && node.sequence_start <= start_node_coordinate + && node.sequence_end >= start_node_coordinate + }) + .expect("should find the start boundary in the source graph"); + let end_node = graph + .nodes() + .find(|node| { + node.node_id == end_block.node_id + && node.sequence_start <= end_node_coordinate + && node.sequence_end >= end_node_coordinate + }) + .expect("should find the end boundary in the source graph"); + let edge_ids = all_intermediate_edges(&graph, start_node, end_node) + .iter() + .map(|(_source, _target, edge_info)| edge_info[0].edge_id) + .collect::>(); + BlockGroup::persist_subgraph( + conn, + source_block_group_id, + &edge_ids, + &SubgraphBoundary { + block: *start_block, + node_coordinate: start_node_coordinate, + }, + &SubgraphBoundary { + block: *end_block, + node_coordinate: end_node_coordinate, + }, + target_block_group_id, + create_terminal_edges, + ) +} + +pub fn get_all_sequences( + conn: &GraphConnection, + block_group_id: &HashId, +) -> Result, BlockGroupError> { + get_all_sequences_with_pruning(conn, block_group_id, true) +} + +pub fn get_all_sequences_with_pruning( + conn: &GraphConnection, + block_group_id: &HashId, + prune: bool, +) -> Result, BlockGroupError> { + let edges = BlockGroupEdge::edges_for_block_group(conn, block_group_id, None) + .into_iter() + .filter(|edge| edge.chromosome_index != PRESERVE_EDIT_SITE_CHROMOSOME_INDEX) + .collect::>(); + let blocks = Edge::blocks_from_edges(conn, block_group_id, &edges, None)?; + let (load_edges, load_blocks) = Edge::graph_load_data(&edges, &blocks); + let (mut graph, _) = graph_loader::build_graph(&load_edges, &load_blocks); + if prune { + graph_loader::prune_graph(&mut graph); + } + let sequences_by_node = blocks + .iter() + .map(|block| { + ( + GraphNode { + node_id: block.node_id, + sequence_start: block.start, + sequence_end: block.end, + }, + block.sequence(), + ) + }) + .collect::>(); + Ok(graph_loader::get_all_sequences(&graph, &sequences_by_node)) +} + +pub fn get_sample_all_sequences( + conn: &GraphConnection, + collection_name: &str, + sample_name: &str, + history_ref: Option<&str>, +) -> Result, SampleError> { + let mut sequences = HashSet::new(); + for block_group in Sample::get_block_groups(conn, collection_name, sample_name, history_ref) { + sequences.extend(get_all_sequences(conn, &block_group.id)?); + } + Ok(sequences) +} + +/// Loads persisted block-group records and constructs their graph representation. +pub fn load_block_group_graph( + conn: &GraphConnection, + block_group_id: &HashId, + history_ref: Option<&str>, +) -> Result { + let edges = BlockGroupEdge::edges_for_block_group(conn, block_group_id, history_ref); + let blocks = Edge::blocks_from_edges(conn, block_group_id, &edges, history_ref)?; + let (load_edges, load_blocks) = Edge::graph_load_data(&edges, &blocks); + let (graph, _) = graph_loader::build_graph(&load_edges, &load_blocks); + Ok(graph) +} + +/// Loads a block group and projects its graph nodes into linear intervals. +pub fn load_block_group_intervaltree( + conn: &GraphConnection, + block_group_id: &HashId, + remove_ambiguous_positions: bool, +) -> Result, BlockGroupError> { + let mut graph = load_block_group_graph(conn, block_group_id, None)?; + prune_graph(&mut graph); + Ok(flatten_to_interval_tree(&graph, remove_ambiguous_positions)) +} + +pub fn load_sample_graph( + conn: &GraphConnection, + collection: &str, + name: &str, + history_ref: Option<&str>, +) -> Result { + let block_groups = Sample::get_block_groups(conn, collection, name, history_ref); + let mut sample_graph = GenGraph::new(); + for block_group in block_groups { + let block_group_graph = load_block_group_graph(conn, &block_group.id, history_ref)?; + for node in block_group_graph.nodes() { + sample_graph.add_node(node); + } + for (source, target, edges) in block_group_graph.all_edges() { + if let Some(existing_edges) = sample_graph.edge_weight_mut(source, target) { + existing_edges.extend(edges.clone()); + } else { + sample_graph.add_edge(source, target, edges.clone()); + } + } + } + Ok(sample_graph) +} + +fn region_intervaltree( + region: &ResolvedGenRegion, + conn: &GraphConnection, +) -> Result, BlockGroupError> { + if region.kind == ResolvedRegionKind::BlockGroup { + load_block_group_intervaltree( + conn, + ®ion.block_group.id, + region.remove_ambiguous_positions, + ) + } else { + IntervalTreeSource::intervaltree(region, conn) + } +} + +pub fn resolve_region_positions( + conn: &GraphConnection, + block_group_id: &HashId, + interval_tree: &IntervalTree, + start: i64, + end: i64, + start_offset: i64, + end_offset: i64, +) -> Result<(Vec, Vec), GraphError> { + graph_loader::resolve_region_positions( + interval_tree, + graph_loader::RegionPositionQuery { + start, + end, + start_offset, + end_offset, + }, + |node_id| { + Node::query_nodes_length(conn, &[node_id]) + .ok() + .and_then(|lengths| lengths.get(&node_id).copied()) + }, + |graph, node_id| expand(conn, graph, block_group_id, node_id), + ) +} + +pub fn positions_at_coordinate( + interval_tree: &IntervalTree, + coordinate: i64, +) -> Vec { + graph_loader::positions_at_coordinate(interval_tree, coordinate) +} + +pub fn plan_region_edges( + start_positions: &[GraphNodePosition], + end_positions: &[GraphNodePosition], + block: &PathBlock, + preserve_edge: bool, + chromosome_index: i64, + phased: i64, +) -> Vec { + graph_loader::plan_region_edges( + start_positions, + end_positions, + block, + preserve_edge, + chromosome_index, + phased, + ) + .into_iter() + .map(|edge| AugmentedEdgeData { + edge_data: EdgeData { + source_node_id: edge.source_node_id, + source_coordinate: edge.source_coordinate, + source_strand: edge.source_strand, + target_node_id: edge.target_node_id, + target_coordinate: edge.target_coordinate, + target_strand: edge.target_strand, + }, + chromosome_index: edge.chromosome_index, + phased: edge.phased, + }) + .collect() +} + +pub fn find_region_graph_positions( + region: &ResolvedGenRegion, + conn: &GraphConnection, + start_offset: i64, + end_offset: i64, +) -> Result { + let interval_tree = region_intervaltree(region, conn).map_err(|_| GraphError::NoPath)?; + let (start_positions, end_positions) = resolve_region_positions( + conn, + ®ion.block_group.id, + &interval_tree, + region.start, + region.end, + start_offset, + end_offset, + )?; + let mut resolved = region.clone(); + resolved.start_anchors = Some(start_positions); + resolved.end_anchors = Some(end_positions); + Ok(resolved) +} + +pub fn plan_region_change( + region: &ResolvedGenRegion, + conn: &GraphConnection, + change: &BlockGroupChange, + tree: Option<&IntervalTree>, +) -> Result, BlockGroupError> { + match region.kind { + ResolvedRegionKind::Path | ResolvedRegionKind::BlockGroup => { + let local_tree; + let tree = match tree { + Some(tree) => tree, + None => { + local_tree = region_intervaltree(region, conn)?; + &local_tree + } + }; + return BlockGroup::set_up_new_edges(change, tree); + } + ResolvedRegionKind::Annotation | ResolvedRegionKind::Accession => {} + } + + let graph_positions_from_tree = |coordinate| { + let positions = positions_at_coordinate(tree?, coordinate); + (!positions.is_empty()).then_some(positions) + }; + let (start_positions, end_positions) = + if let (Some(start), Some(end)) = (®ion.start_anchors, ®ion.end_anchors) { + (start.clone(), end.clone()) + } else if let Some(start_positions) = graph_positions_from_tree(region.start) + && let Some(end_positions) = graph_positions_from_tree(region.end) + { + (start_positions, end_positions) + } else { + let resolved = find_region_graph_positions(region, conn, 0, 0) + .map_err(|err| BlockGroupError::ChangeOutOfBounds(err.to_string()))?; + ( + resolved.start_anchors.expect("should have start anchors"), + resolved.end_anchors.expect("should have end anchors"), + ) + }; + Ok(plan_region_edges( + &start_positions, + &end_positions, + &change.block, + change.preserve_edge, + change.chromosome_index, + change.phased, + )) +} + +pub fn insert_changes( + conn: &GraphConnection, + changes: &[BlockGroupChange], + tree_map: Option<&mut IntervalTreeCache>, +) -> Result<(), BlockGroupError> { + let mut new_augmented_edges_by_block_group = HashMap::new(); + let mut new_accession_edges = HashMap::new(); + let mut local_tree_map = HashMap::new(); + let tree_map = tree_map.unwrap_or(&mut local_tree_map); + for change in changes { + let cache_key = change.region.intervaltree_cache_key(); + if let std::collections::hash_map::Entry::Vacant(entry) = tree_map.entry(cache_key) { + entry.insert(region_intervaltree(&change.region, conn)?); + } + let new_augmented_edges = + plan_region_change(&change.region, conn, change, tree_map.get(&cache_key))?; + new_augmented_edges_by_block_group + .entry(change.region.block_group.id) + .or_insert_with(Vec::new) + .extend(new_augmented_edges.iter().cloned()); + if let Some(accession) = &change.path_accession { + new_accession_edges + .entry((change.region.block_group.id, accession.clone())) + .or_insert_with(Vec::new) + .extend(new_augmented_edges); + } + } + BlockGroup::persist_insert_changes( + conn, + new_augmented_edges_by_block_group, + new_accession_edges, + ) +} + +pub fn insert_change( + conn: &GraphConnection, + change: &BlockGroupChange, +) -> Result<(), BlockGroupError> { + let new_augmented_edges = plan_region_change(&change.region, conn, change, None)?; + let mut new_augmented_edges_by_block_group = HashMap::new(); + new_augmented_edges_by_block_group + .insert(change.region.block_group.id, new_augmented_edges.clone()); + let mut new_accession_edges = HashMap::new(); + if let Some(accession) = &change.path_accession { + new_accession_edges.insert( + (change.region.block_group.id, accession.clone()), + new_augmented_edges, + ); + } + BlockGroup::persist_insert_changes( + conn, + new_augmented_edges_by_block_group, + new_accession_edges, + ) +} + +/// Merges edges adjacent to a node into an existing graph. +pub fn expand( + conn: &GraphConnection, + graph: &mut GenGraph, + block_group_id: &HashId, + node_id: HashId, +) -> bool { + let candidate_edges = + match Edge::edges_for_block_group_node_neighborhood(conn, block_group_id, node_id, None) { + Ok(edges) => edges, + Err(_) => return false, + }; + let unloaded_edge_ids = + graph_loader::unloaded_edge_ids(graph, candidate_edges.iter().map(|edge| edge.edge.id)); + let unloaded_edges = candidate_edges + .into_iter() + .filter(|edge| unloaded_edge_ids.contains(&edge.edge.id)) + .collect::>(); + if unloaded_edges.is_empty() { + return false; + } + let blocks = match Edge::blocks_from_edges(conn, block_group_id, &unloaded_edges, None) { + Ok(blocks) => blocks, + Err(_) => return false, + }; + let (load_edges, load_blocks) = Edge::graph_load_data(&unloaded_edges, &blocks); + let (fragment, _) = graph_loader::build_graph(&load_edges, &load_blocks); + graph_loader::merge_fragment(graph, &fragment); + true +} diff --git a/gen-models/Cargo.toml b/gen-models/Cargo.toml index 62d2627b..60b1633c 100644 --- a/gen-models/Cargo.toml +++ b/gen-models/Cargo.toml @@ -18,7 +18,6 @@ rand = "0.10.1" [dependencies] gen-capnp-schemas = { path = "../gen-capnp-schemas", version = "0.2.1" } gen-core = { path = "../gen-core", version = "0.2.1" } -gen-graph = { path = "../gen-graph", version = "0.2.1" } cached = "0.55.1" capnp = { version = "0.24" } include_dir = "0.7.4" diff --git a/gen-models/graph-tests/Cargo.toml b/gen-models/graph-tests/Cargo.toml new file mode 100644 index 00000000..3438b5dd --- /dev/null +++ b/gen-models/graph-tests/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "gen-models-graph-tests" +description = "Graph-backed integration tests for gen-models workflows. They are unit tests that rely on functions from gen-graph, so we keep them separate from the regular gen-models tests to avoid circular dependencies between gen-models and gen-graph" +version = "0.2.1" +edition = "2024" +license = "Apache-2.0" +publish = false + +[dependencies] +gen-core = { path = "../../gen-core", version = "0.2.1" } +gen-graph = { path = "../../gen-graph", version = "0.2.1" } +gen-models = { path = "..", version = "0.2.1" } +intervaltree = "0.2.7" +rusqlite = { package = "rusqdoltlite", version = "0.40.14", features = ["bundled", "array", "limits", "fallible_uint", "backup"] } +tempfile = "3.20.0" diff --git a/gen-models/graph-tests/src/lib.rs b/gen-models/graph-tests/src/lib.rs new file mode 100644 index 00000000..c80e0625 --- /dev/null +++ b/gen-models/graph-tests/src/lib.rs @@ -0,0 +1,162 @@ +use std::fs; + +use gen_core::{ + HashId, PATH_END_NODE_ID, PATH_START_NODE_ID, Strand, config::Workspace, + errors::ConnectionError, +}; +use gen_models::{ + block_group::{BlockGroup, NewBlockGroup}, + block_group_edge::{BlockGroupEdge, BlockGroupEdgeData}, + collection::Collection, + db::{ConfigConnection, DbContext, GraphConnection}, + edge::Edge, + migrations::{run_config_migrations, run_migrations}, + node::Node, + path::Path, + sample::{NewSample, Sample}, + sequence::Sequence, +}; +use rusqlite::Connection; +use tempfile::tempdir; + +pub fn get_connection<'a>( + database_path: impl Into>, +) -> Result { + let database_path = database_path.into(); + if let Some(database_path) = database_path + && fs::metadata(database_path).is_ok() + { + fs::remove_file(database_path).expect("should remove the existing test database"); + } + let mut conn = if let Some(database_path) = database_path { + Connection::open(database_path).map_err(ConnectionError::OpenFailed)? + } else { + Connection::open_in_memory().map_err(ConnectionError::OpenFailed)? + }; + rusqlite::vtab::array::load_module(&conn)?; + run_migrations(&mut conn); + Ok(GraphConnection(conn)) +} + +pub fn setup_gen() -> DbContext { + let directory = tempdir() + .expect("should create a temporary repository") + .keep(); + let workspace = Workspace::new(directory); + workspace.ensure_gen_dir(); + let graph_conn = get_connection(None).expect("should create the graph database"); + let mut config_conn = Connection::open_in_memory().expect("should create the config database"); + run_config_migrations(&mut config_conn); + DbContext::new(workspace, graph_conn, ConfigConnection(config_conn)) + .expect("should create the database context") +} + +pub fn create_block_group( + conn: &GraphConnection, + collection_name: &str, + sample_name: &str, + name: &str, +) -> BlockGroup { + Sample::get_or_create( + conn, + NewSample { + name: sample_name, + ..Default::default() + }, + ) + .expect("should create the test sample"); + BlockGroup::create( + conn, + NewBlockGroup { + collection_name, + sample_name, + name, + ..Default::default() + }, + ) + .expect("should create the test block group") +} + +pub fn setup_block_group(conn: &GraphConnection) -> (HashId, Path) { + let sequences = [ + ("AAAAAAAAAA", "test-a-node"), + ("TTTTTTTTTT", "test-t-node"), + ("CCCCCCCCCC", "test-c-node"), + ("GGGGGGGGGG", "test-g-node"), + ]; + let node_ids = sequences + .into_iter() + .map(|(sequence, node_name)| { + let sequence = Sequence::new() + .sequence_type("DNA") + .sequence(sequence) + .save(conn) + .expect("should save the test sequence"); + Node::create(conn, &sequence.hash, &HashId::convert_str(node_name)) + .expect("should create the test node") + }) + .collect::>(); + + Collection::get_or_create(conn, "test").expect("should create the test collection"); + let block_group = create_block_group(conn, "test", "test", "chr1"); + let node_path = [ + (PATH_START_NODE_ID, 0, node_ids[0], 0), + (node_ids[0], 10, node_ids[1], 0), + (node_ids[1], 10, node_ids[2], 0), + (node_ids[2], 10, node_ids[3], 0), + (node_ids[3], 10, PATH_END_NODE_ID, 0), + ]; + let edges = node_path + .into_iter() + .map( + |(source_node_id, source_coordinate, target_node_id, target_coordinate)| { + Edge::create( + conn, + source_node_id, + source_coordinate, + Strand::Forward, + target_node_id, + target_coordinate, + Strand::Forward, + ) + .expect("should create the test edge") + }, + ) + .collect::>(); + let block_group_edges = edges + .iter() + .map(|edge| BlockGroupEdgeData { + block_group_id: block_group.id, + edge_id: edge.id, + chromosome_index: 0, + phased: 0, + }) + .collect::>(); + BlockGroupEdge::bulk_create(conn, &block_group_edges); + + let edge_ids = edges.iter().map(|edge| edge.id).collect::>(); + let path = Path::create(conn, "chr1", &block_group.id, &edge_ids) + .expect("should create the test path"); + (block_group.id, path) +} + +pub fn get_single_block_group_id( + conn: &GraphConnection, + collection_name: &str, + sample_name: &str, + group_name: &str, + parent_samples: Vec, +) -> HashId { + BlockGroup::get_or_create_sample_block_groups( + conn, + collection_name, + sample_name, + group_name, + parent_samples, + ) + .expect("should create the sample block group") + .into_iter() + .next() + .expect("should return one block group") + .id +} diff --git a/gen-models/graph-tests/tests/annotations.rs b/gen-models/graph-tests/tests/annotations.rs new file mode 100644 index 00000000..c8479198 --- /dev/null +++ b/gen-models/graph-tests/tests/annotations.rs @@ -0,0 +1,66 @@ +use gen_models::{ + annotations::Annotation, + assets::{OperationKind, OperationLog}, + errors::OperationError, + history::{HistoryStore, dolt::DoltHistoryStore}, + operations::commit_operation_summary, + traits::Query, +}; +use gen_models_graph_tests::{setup_block_group, setup_gen}; + +#[test] +fn test_add_annotation_creates_annotation() { + let context = setup_gen(); + let conn = context.graph().conn(); + let history_store = DoltHistoryStore::new(conn); + setup_block_group(conn); + + let summary = gen_graph::models::add_annotation( + &context, + "test", + "gene-a", + Some("track-1"), + "test", + "chr1:1-5", + ) + .expect("should add the annotation"); + let commit_hash = + commit_operation_summary(&context, &summary).expect("should commit the annotation"); + assert_eq!( + history_store.current_head().expect("should read history"), + Some(commit_hash) + ); + let mut operation_logs = OperationLog::all(conn); + operation_logs.sort_by_key(|operation_log| core::cmp::Reverse(operation_log.created_on)); + assert_eq!( + operation_logs[0].operation_kind, + OperationKind::Other("add annotation gene-a".to_string()) + ); + let annotations = + Annotation::query_by_group(conn, "track-1", None).expect("should query annotations"); + assert_eq!(annotations.len(), 1); + assert_eq!(annotations[0].name, "gene-a"); +} + +#[test] +fn test_add_annotation_detects_no_changes() { + let context = setup_gen(); + let conn = context.graph().conn(); + setup_block_group(conn); + + let add = || { + gen_graph::models::add_annotation( + &context, + "test", + "gene-a", + Some("track-1"), + "test", + "chr1:1-5", + ) + .expect("should add the annotation") + }; + commit_operation_summary(&context, &add()).expect("should commit the first annotation"); + let error = commit_operation_summary(&context, &add()) + .expect_err("should detect that the annotation already exists"); + assert_eq!(error, OperationError::NoChanges); +} diff --git a/gen-models/graph-tests/tests/block_group.rs b/gen-models/graph-tests/tests/block_group.rs new file mode 100644 index 00000000..df04f972 --- /dev/null +++ b/gen-models/graph-tests/tests/block_group.rs @@ -0,0 +1,99 @@ +use std::collections::HashSet; + +use gen_core::{HashId, PathBlock, Strand}; +use gen_graph::models::get_all_sequences; +use gen_models::{ + block_group::BlockGroupChange, node::Node, region::ResolvedGenRegion, sequence::Sequence, +}; +use gen_models_graph_tests::{get_connection, setup_block_group}; + +#[test] +fn test_insert_and_deletion_sequences() { + let conn = get_connection(None).expect("should create an in-memory graph database"); + let (block_group_id, path) = setup_block_group(&conn); + let insert_sequence = Sequence::new() + .sequence_type("DNA") + .sequence("NNNN") + .save(&conn) + .expect("should save the inserted sequence"); + let insert_node_id = Node::create(&conn, &insert_sequence.hash, &HashId::convert_str("1")) + .expect("should create the inserted node"); + let insert = PathBlock { + node_id: insert_node_id, + block_sequence: insert_sequence + .get_sequence(0, 4) + .expect("should load the inserted sequence"), + sequence_start: 0, + sequence_end: 4, + path_start: 7, + path_end: 15, + strand: Strand::Forward, + }; + let region = ResolvedGenRegion::from_path(&conn, block_group_id, &path, 7, 15) + .expect("should resolve the insertion region"); + gen_graph::models::insert_change( + &conn, + &BlockGroupChange { + region, + path_accession: None, + block: insert, + chromosome_index: 1, + phased: 0, + preserve_edge: true, + }, + ) + .expect("should insert the sequence change"); + + assert_eq!( + get_all_sequences(&conn, &block_group_id) + .expect("should enumerate sequences after insertion"), + HashSet::from([ + "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), + "AAAAAAANNNNTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), + ]) + ); + + let deletion_sequence = Sequence::new() + .sequence_type("DNA") + .sequence("") + .save(&conn) + .expect("should save the deletion sequence"); + let deletion_node_id = Node::create(&conn, &deletion_sequence.hash, &HashId::convert_str("2")) + .expect("should create the deletion node"); + let deletion = PathBlock { + node_id: deletion_node_id, + block_sequence: deletion_sequence + .get_sequence(None, None) + .expect("should load the deletion sequence"), + sequence_start: 0, + sequence_end: 0, + path_start: 19, + path_end: 31, + strand: Strand::Forward, + }; + let region = ResolvedGenRegion::from_path(&conn, block_group_id, &path, 19, 31) + .expect("should resolve the deletion region"); + gen_graph::models::insert_change( + &conn, + &BlockGroupChange { + region, + path_accession: None, + block: deletion, + chromosome_index: 1, + phased: 0, + preserve_edge: true, + }, + ) + .expect("should insert the deletion change"); + + assert_eq!( + get_all_sequences(&conn, &block_group_id) + .expect("should enumerate sequences after deletion"), + HashSet::from([ + "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), + "AAAAAAANNNNTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), + "AAAAAAAAAATTTTTTTTTGGGGGGGGG".to_string(), + "AAAAAAANNNNTTTTGGGGGGGGG".to_string(), + ]) + ); +} diff --git a/gen-models/graph-tests/tests/block_group_changes.rs b/gen-models/graph-tests/tests/block_group_changes.rs new file mode 100644 index 00000000..e807a3ae --- /dev/null +++ b/gen-models/graph-tests/tests/block_group_changes.rs @@ -0,0 +1,906 @@ +use std::collections::HashSet; + +use gen_core::{HashId, PathBlock, Strand}; +use gen_graph::models::get_all_sequences; +use gen_models::{ + accession::Accession, + annotations::Annotation as ModelAnnotation, + block_group::{BlockGroup, BlockGroupChange, PathCache}, + node::Node, + region::ResolvedGenRegion, + sequence::Sequence, + traits::Query as _, +}; +use gen_models_graph_tests::{get_connection, setup_block_group}; + +#[test] +fn test_insert_accession_change_get_all() { + let conn = get_connection(None).unwrap(); + let (block_group_id, path) = setup_block_group(&conn); + let mut path_cache = PathCache::new(&conn); + let accession = + BlockGroup::add_accession(&conn, &path, "test-accession", 10, 30, &mut path_cache).unwrap(); + let insert_sequence = Sequence::new() + .sequence_type("DNA") + .sequence("NNNN") + .save(&conn) + .unwrap(); + let insert_node_id = Node::create( + &conn, + &insert_sequence.hash, + &HashId::convert_str("acc-insert-node"), + ) + .unwrap(); + let insert = PathBlock { + node_id: insert_node_id, + block_sequence: insert_sequence.get_sequence(0, 4).unwrap(), + sequence_start: 0, + sequence_end: 4, + path_start: 5, + path_end: 15, + strand: Strand::Forward, + }; + let region = ResolvedGenRegion::from_accession(&conn, &accession, 5, 15).unwrap(); + let change = BlockGroupChange { + region, + path_accession: None, + block: insert, + chromosome_index: 1, + phased: 0, + preserve_edge: true, + }; + + gen_graph::models::insert_change(&conn, &change).unwrap(); + let all_sequences = get_all_sequences(&conn, &block_group_id).unwrap(); + assert_eq!( + all_sequences, + HashSet::from_iter(vec![ + "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), + "AAAAAAAAAATTTTTNNNNCCCCCGGGGGGGGGG".to_string(), + ]) + ); +} + +#[test] +fn test_insert_annotation_change_get_all() { + let conn = get_connection(None).unwrap(); + let (block_group_id, path) = setup_block_group(&conn); + let mut path_cache = PathCache::new(&conn); + let accession = + BlockGroup::add_accession(&conn, &path, "test-accession", 10, 30, &mut path_cache).unwrap(); + let annotation = + ModelAnnotation::get_or_create(&conn, "gene-1", "track-1", &accession.id, None).unwrap(); + let deletion_sequence = Sequence::new() + .sequence_type("DNA") + .sequence("") + .save(&conn) + .unwrap(); + let deletion_node_id = Node::create( + &conn, + &deletion_sequence.hash, + &HashId::convert_str("annotation-delete-node"), + ) + .unwrap(); + let deletion = PathBlock { + node_id: deletion_node_id, + block_sequence: deletion_sequence.get_sequence(None, None).unwrap(), + sequence_start: 0, + sequence_end: 0, + path_start: 5, + path_end: 15, + strand: Strand::Forward, + }; + let annotation_accession = Accession::get_by_id(&conn, &annotation.accession_id, None).unwrap(); + let region = + ResolvedGenRegion::from_annotation(&conn, &annotation, &annotation_accession, 5, 15) + .unwrap(); + let change = BlockGroupChange { + region, + path_accession: None, + block: deletion, + chromosome_index: 1, + phased: 0, + preserve_edge: true, + }; + + gen_graph::models::insert_change(&conn, &change).unwrap(); + let all_sequences = get_all_sequences(&conn, &block_group_id).unwrap(); + assert_eq!( + all_sequences, + HashSet::from_iter(vec![ + "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), + "AAAAAAAAAATTTTTCCCCCGGGGGGGGGG".to_string(), + ]) + ); +} + +#[test] +fn test_simple_insert_get_all() { + let conn = get_connection(None).unwrap(); + let (block_group_id, path) = setup_block_group(&conn); + let insert_sequence = Sequence::new() + .sequence_type("DNA") + .sequence("NNNN") + .save(&conn) + .unwrap(); + let insert_node_id = + Node::create(&conn, &insert_sequence.hash, &HashId::convert_str("1")).unwrap(); + let insert = PathBlock { + node_id: insert_node_id, + block_sequence: insert_sequence.get_sequence(0, 4).unwrap(), + sequence_start: 0, + sequence_end: 4, + path_start: 7, + path_end: 15, + strand: Strand::Forward, + }; + let region = ResolvedGenRegion::from_path(&conn, block_group_id, &path, 7, 15).unwrap(); + let change = BlockGroupChange { + region, + path_accession: None, + block: insert, + chromosome_index: 1, + phased: 0, + preserve_edge: true, + }; + gen_graph::models::insert_change(&conn, &change).unwrap(); + + let all_sequences = get_all_sequences(&conn, &block_group_id).unwrap(); + assert_eq!( + all_sequences, + HashSet::from_iter(vec![ + "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), + "AAAAAAANNNNTTTTTCCCCCCCCCCGGGGGGGGGG".to_string() + ]) + ); +} + +#[test] +fn test_insert_on_block_boundary_middle() { + let conn = get_connection(None).unwrap(); + let (block_group_id, path) = setup_block_group(&conn); + let insert_sequence = Sequence::new() + .sequence_type("DNA") + .sequence("NNNN") + .save(&conn) + .unwrap(); + let insert_node_id = + Node::create(&conn, &insert_sequence.hash, &HashId::convert_str("1")).unwrap(); + let insert = PathBlock { + node_id: insert_node_id, + block_sequence: insert_sequence.get_sequence(0, 4).unwrap(), + sequence_start: 0, + sequence_end: 4, + path_start: 15, + path_end: 15, + strand: Strand::Forward, + }; + let region = ResolvedGenRegion::from_path(&conn, block_group_id, &path, 15, 15).unwrap(); + let change = BlockGroupChange { + region, + path_accession: None, + block: insert, + chromosome_index: 1, + phased: 0, + preserve_edge: true, + }; + gen_graph::models::insert_change(&conn, &change).unwrap(); + + let all_sequences = get_all_sequences(&conn, &block_group_id).unwrap(); + assert_eq!( + all_sequences, + HashSet::from_iter(vec![ + "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), + "AAAAAAAAAATTTTTNNNNTTTTTCCCCCCCCCCGGGGGGGGGG".to_string() + ]) + ); +} + +#[test] +fn test_insert_within_block() { + let conn = get_connection(None).unwrap(); + let (block_group_id, path) = setup_block_group(&conn); + let insert_sequence = Sequence::new() + .sequence_type("DNA") + .sequence("NNNN") + .save(&conn) + .unwrap(); + let insert_node_id = + Node::create(&conn, &insert_sequence.hash, &HashId::convert_str("1")).unwrap(); + let insert = PathBlock { + node_id: insert_node_id, + block_sequence: insert_sequence.get_sequence(0, 4).unwrap(), + sequence_start: 0, + sequence_end: 4, + path_start: 12, + path_end: 17, + strand: Strand::Forward, + }; + let region = ResolvedGenRegion::from_path(&conn, block_group_id, &path, 12, 17).unwrap(); + let change = BlockGroupChange { + region, + path_accession: None, + block: insert, + chromosome_index: 1, + phased: 0, + preserve_edge: true, + }; + gen_graph::models::insert_change(&conn, &change).unwrap(); + + let all_sequences = get_all_sequences(&conn, &block_group_id).unwrap(); + assert_eq!( + all_sequences, + HashSet::from_iter(vec![ + "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), + "AAAAAAAAAATTNNNNTTTCCCCCCCCCCGGGGGGGGGG".to_string() + ]) + ); +} + +#[test] +fn test_insert_on_block_boundary_start() { + let conn = get_connection(None).unwrap(); + let (block_group_id, path) = setup_block_group(&conn); + let insert_sequence = Sequence::new() + .sequence_type("DNA") + .sequence("NNNN") + .save(&conn) + .unwrap(); + let insert_node_id = + Node::create(&conn, &insert_sequence.hash, &HashId::convert_str("1")).unwrap(); + let insert = PathBlock { + node_id: insert_node_id, + block_sequence: insert_sequence.get_sequence(0, 4).unwrap(), + sequence_start: 0, + sequence_end: 4, + path_start: 10, + path_end: 10, + strand: Strand::Forward, + }; + let region = ResolvedGenRegion::from_path(&conn, block_group_id, &path, 10, 10).unwrap(); + let change = BlockGroupChange { + region, + path_accession: None, + block: insert, + chromosome_index: 1, + phased: 0, + preserve_edge: true, + }; + gen_graph::models::insert_change(&conn, &change).unwrap(); + + let all_sequences = get_all_sequences(&conn, &block_group_id).unwrap(); + assert_eq!( + all_sequences, + HashSet::from_iter(vec![ + "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), + "AAAAAAAAAANNNNTTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string() + ]) + ); +} + +#[test] +fn test_insert_on_block_boundary_end() { + let conn = get_connection(None).unwrap(); + let (block_group_id, path) = setup_block_group(&conn); + let insert_sequence = Sequence::new() + .sequence_type("DNA") + .sequence("NNNN") + .save(&conn) + .unwrap(); + let insert_node_id = + Node::create(&conn, &insert_sequence.hash, &HashId::convert_str("1")).unwrap(); + let insert = PathBlock { + node_id: insert_node_id, + block_sequence: insert_sequence.get_sequence(0, 4).unwrap(), + sequence_start: 0, + sequence_end: 4, + path_start: 9, + path_end: 9, + strand: Strand::Forward, + }; + let region = ResolvedGenRegion::from_path(&conn, block_group_id, &path, 9, 9).unwrap(); + let change = BlockGroupChange { + region, + path_accession: None, + block: insert, + chromosome_index: 1, + phased: 0, + preserve_edge: true, + }; + gen_graph::models::insert_change(&conn, &change).unwrap(); + + let all_sequences = get_all_sequences(&conn, &block_group_id).unwrap(); + assert_eq!( + all_sequences, + HashSet::from_iter(vec![ + "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), + "AAAAAAAAANNNNATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string() + ]) + ); +} + +#[test] +fn test_insert_across_entire_block_boundary() { + let conn = get_connection(None).unwrap(); + let (block_group_id, path) = setup_block_group(&conn); + let insert_sequence = Sequence::new() + .sequence_type("DNA") + .sequence("NNNN") + .save(&conn) + .unwrap(); + let insert_node_id = + Node::create(&conn, &insert_sequence.hash, &HashId::convert_str("1")).unwrap(); + let insert = PathBlock { + node_id: insert_node_id, + block_sequence: insert_sequence.get_sequence(0, 4).unwrap(), + sequence_start: 0, + sequence_end: 4, + path_start: 10, + path_end: 20, + strand: Strand::Forward, + }; + let region = ResolvedGenRegion::from_path(&conn, block_group_id, &path, 10, 20).unwrap(); + let change = BlockGroupChange { + region, + path_accession: None, + block: insert, + chromosome_index: 1, + phased: 0, + preserve_edge: true, + }; + gen_graph::models::insert_change(&conn, &change).unwrap(); + + let all_sequences = get_all_sequences(&conn, &block_group_id).unwrap(); + assert_eq!( + all_sequences, + HashSet::from_iter(vec![ + "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), + "AAAAAAAAAANNNNCCCCCCCCCCGGGGGGGGGG".to_string() + ]) + ); +} + +#[test] +fn test_insert_across_two_blocks() { + let conn = get_connection(None).unwrap(); + let (block_group_id, path) = setup_block_group(&conn); + let insert_sequence = Sequence::new() + .sequence_type("DNA") + .sequence("NNNN") + .save(&conn) + .unwrap(); + let insert_node_id = + Node::create(&conn, &insert_sequence.hash, &HashId::convert_str("1")).unwrap(); + let insert = PathBlock { + node_id: insert_node_id, + block_sequence: insert_sequence.get_sequence(0, 4).unwrap(), + sequence_start: 0, + sequence_end: 4, + path_start: 15, + path_end: 25, + strand: Strand::Forward, + }; + let region = ResolvedGenRegion::from_path(&conn, block_group_id, &path, 15, 25).unwrap(); + let change = BlockGroupChange { + region, + path_accession: None, + block: insert, + chromosome_index: 1, + phased: 0, + preserve_edge: true, + }; + gen_graph::models::insert_change(&conn, &change).unwrap(); + + let all_sequences = get_all_sequences(&conn, &block_group_id).unwrap(); + assert_eq!( + all_sequences, + HashSet::from_iter(vec![ + "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), + "AAAAAAAAAATTTTTNNNNCCCCCGGGGGGGGGG".to_string() + ]) + ); +} + +#[test] +fn test_insert_spanning_blocks() { + let conn = get_connection(None).unwrap(); + let (block_group_id, path) = setup_block_group(&conn); + let insert_sequence = Sequence::new() + .sequence_type("DNA") + .sequence("NNNN") + .save(&conn) + .unwrap(); + let insert_node_id = + Node::create(&conn, &insert_sequence.hash, &HashId::convert_str("1")).unwrap(); + let insert = PathBlock { + node_id: insert_node_id, + block_sequence: insert_sequence.get_sequence(0, 4).unwrap(), + sequence_start: 0, + sequence_end: 4, + path_start: 5, + path_end: 35, + strand: Strand::Forward, + }; + let region = ResolvedGenRegion::from_path(&conn, block_group_id, &path, 5, 35).unwrap(); + let change = BlockGroupChange { + region, + path_accession: None, + block: insert, + chromosome_index: 1, + phased: 0, + preserve_edge: true, + }; + gen_graph::models::insert_change(&conn, &change).unwrap(); + + let all_sequences = get_all_sequences(&conn, &block_group_id).unwrap(); + assert_eq!( + all_sequences, + HashSet::from_iter(vec![ + "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), + "AAAAANNNNGGGGG".to_string() + ]) + ); +} + +#[test] +fn test_simple_deletion() { + let conn = get_connection(None).unwrap(); + let (block_group_id, path) = setup_block_group(&conn); + let deletion_sequence = Sequence::new() + .sequence_type("DNA") + .sequence("") + .save(&conn) + .unwrap(); + let deletion_node_id = + Node::create(&conn, &deletion_sequence.hash, &HashId::convert_str("1")).unwrap(); + let deletion = PathBlock { + node_id: deletion_node_id, + block_sequence: deletion_sequence.get_sequence(None, None).unwrap(), + sequence_start: 0, + sequence_end: 0, + path_start: 19, + path_end: 31, + strand: Strand::Forward, + }; + + let region = ResolvedGenRegion::from_path(&conn, block_group_id, &path, 19, 31).unwrap(); + let change = BlockGroupChange { + region, + path_accession: None, + block: deletion, + chromosome_index: 1, + phased: 0, + preserve_edge: true, + }; + + // take out an entire block + gen_graph::models::insert_change(&conn, &change).unwrap(); + let all_sequences = get_all_sequences(&conn, &block_group_id).unwrap(); + assert_eq!( + all_sequences, + HashSet::from_iter(vec![ + "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), + "AAAAAAAAAATTTTTTTTTGGGGGGGGG".to_string(), + ]) + ); +} + +#[test] +fn test_doesnt_apply_same_insert_twice() { + let conn = get_connection(None).unwrap(); + let (block_group_id, path) = setup_block_group(&conn); + let insert_sequence = Sequence::new() + .sequence_type("DNA") + .sequence("NNNN") + .save(&conn) + .unwrap(); + let insert_node_id = + Node::create(&conn, &insert_sequence.hash, &HashId::convert_str("1")).unwrap(); + let insert = PathBlock { + node_id: insert_node_id, + block_sequence: insert_sequence.get_sequence(0, 4).unwrap(), + sequence_start: 0, + sequence_end: 4, + path_start: 7, + path_end: 15, + strand: Strand::Forward, + }; + let region = ResolvedGenRegion::from_path(&conn, block_group_id, &path, 7, 15).unwrap(); + let change = BlockGroupChange { + region, + path_accession: None, + block: insert, + chromosome_index: 1, + phased: 0, + preserve_edge: true, + }; + gen_graph::models::insert_change(&conn, &change).unwrap(); + + let all_sequences = get_all_sequences(&conn, &block_group_id).unwrap(); + assert_eq!( + all_sequences, + HashSet::from_iter(vec![ + "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), + "AAAAAAANNNNTTTTTCCCCCCCCCCGGGGGGGGGG".to_string() + ]) + ); + gen_graph::models::insert_change(&conn, &change).unwrap(); + + let all_sequences = get_all_sequences(&conn, &block_group_id).unwrap(); + assert_eq!( + all_sequences, + HashSet::from_iter(vec![ + "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), + "AAAAAAANNNNTTTTTCCCCCCCCCCGGGGGGGGGG".to_string() + ]) + ); +} + +#[test] +fn test_insert_at_beginning_of_path() { + let conn = get_connection(None).unwrap(); + let (block_group_id, path) = setup_block_group(&conn); + let insert_sequence = Sequence::new() + .sequence_type("DNA") + .sequence("NNNN") + .save(&conn) + .unwrap(); + let insert_node_id = + Node::create(&conn, &insert_sequence.hash, &HashId::convert_str("1")).unwrap(); + let insert = PathBlock { + node_id: insert_node_id, + block_sequence: insert_sequence.get_sequence(0, 4).unwrap(), + sequence_start: 0, + sequence_end: 4, + path_start: 0, + path_end: 0, + strand: Strand::Forward, + }; + let region = ResolvedGenRegion::from_path(&conn, block_group_id, &path, 0, 0).unwrap(); + let change = BlockGroupChange { + region, + path_accession: None, + block: insert, + chromosome_index: 1, + phased: 0, + preserve_edge: true, + }; + gen_graph::models::insert_change(&conn, &change).unwrap(); + + let all_sequences = get_all_sequences(&conn, &block_group_id).unwrap(); + assert_eq!( + all_sequences, + HashSet::from_iter(vec![ + "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), + "NNNNAAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), + ]) + ); +} + +#[test] +fn test_homozygous_insert_at_beginning_of_path() { + let conn = get_connection(None).unwrap(); + let (block_group_id, path) = setup_block_group(&conn); + let insert_sequence = Sequence::new() + .sequence_type("DNA") + .sequence("NNNN") + .save(&conn) + .unwrap(); + let insert_node_id = + Node::create(&conn, &insert_sequence.hash, &HashId::convert_str("1")).unwrap(); + let insert = PathBlock { + node_id: insert_node_id, + block_sequence: insert_sequence.get_sequence(0, 4).unwrap(), + sequence_start: 0, + sequence_end: 4, + path_start: 0, + path_end: 0, + strand: Strand::Forward, + }; + let region = ResolvedGenRegion::from_path(&conn, block_group_id, &path, 0, 0).unwrap(); + let change = BlockGroupChange { + region, + path_accession: None, + block: insert, + chromosome_index: 0, + phased: 0, + preserve_edge: true, + }; + gen_graph::models::insert_change(&conn, &change).unwrap(); + + let all_sequences = get_all_sequences(&conn, &block_group_id).unwrap(); + assert_eq!( + all_sequences, + HashSet::from_iter(vec![ + "NNNNAAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), + ]) + ); +} + +#[test] +fn test_insert_at_end_of_path() { + let conn = get_connection(None).unwrap(); + let (block_group_id, path) = setup_block_group(&conn); + + let insert_sequence = Sequence::new() + .sequence_type("DNA") + .sequence("NNNN") + .save(&conn) + .unwrap(); + let insert_node_id = + Node::create(&conn, &insert_sequence.hash, &HashId::convert_str("1")).unwrap(); + let insert = PathBlock { + node_id: insert_node_id, + block_sequence: insert_sequence.get_sequence(0, 4).unwrap(), + sequence_start: 0, + sequence_end: 4, + path_start: 40, + path_end: 40, + strand: Strand::Forward, + }; + let region = ResolvedGenRegion::from_path(&conn, block_group_id, &path, 40, 40).unwrap(); + let change = BlockGroupChange { + region, + path_accession: None, + block: insert, + chromosome_index: 1, + phased: 0, + preserve_edge: true, + }; + gen_graph::models::insert_change(&conn, &change).unwrap(); + + let all_sequences = get_all_sequences(&conn, &block_group_id).unwrap(); + assert_eq!( + all_sequences, + HashSet::from_iter(vec![ + "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), + "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGGNNNN".to_string(), + ]) + ); +} + +#[test] +fn test_insert_at_one_bp_into_block() { + let conn = get_connection(None).unwrap(); + let (block_group_id, path) = setup_block_group(&conn); + let insert_sequence = Sequence::new() + .sequence_type("DNA") + .sequence("NNNN") + .save(&conn) + .unwrap(); + let insert_node_id = + Node::create(&conn, &insert_sequence.hash, &HashId::convert_str("1")).unwrap(); + let insert = PathBlock { + node_id: insert_node_id, + block_sequence: insert_sequence.get_sequence(0, 4).unwrap(), + sequence_start: 0, + sequence_end: 4, + path_start: 10, + path_end: 11, + strand: Strand::Forward, + }; + let region = ResolvedGenRegion::from_path(&conn, block_group_id, &path, 10, 11).unwrap(); + let change = BlockGroupChange { + region, + path_accession: None, + block: insert, + chromosome_index: 1, + phased: 0, + preserve_edge: true, + }; + gen_graph::models::insert_change(&conn, &change).unwrap(); + + let all_sequences = get_all_sequences(&conn, &block_group_id).unwrap(); + assert_eq!( + all_sequences, + HashSet::from_iter(vec![ + "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), + "AAAAAAAAAANNNNTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), + ]) + ); +} + +#[test] +fn test_insert_at_one_bp_from_end_of_block() { + let conn = get_connection(None).unwrap(); + let (block_group_id, path) = setup_block_group(&conn); + let insert_sequence = Sequence::new() + .sequence_type("DNA") + .sequence("NNNN") + .save(&conn) + .unwrap(); + let insert_node_id = + Node::create(&conn, &insert_sequence.hash, &HashId::convert_str("1")).unwrap(); + let insert = PathBlock { + node_id: insert_node_id, + block_sequence: insert_sequence.get_sequence(0, 4).unwrap(), + sequence_start: 0, + sequence_end: 4, + path_start: 19, + path_end: 20, + strand: Strand::Forward, + }; + let region = ResolvedGenRegion::from_path(&conn, block_group_id, &path, 19, 20).unwrap(); + let change = BlockGroupChange { + region, + path_accession: None, + block: insert, + chromosome_index: 1, + phased: 0, + preserve_edge: true, + }; + gen_graph::models::insert_change(&conn, &change).unwrap(); + + let all_sequences = get_all_sequences(&conn, &block_group_id).unwrap(); + assert_eq!( + all_sequences, + HashSet::from_iter(vec![ + "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), + "AAAAAAAAAATTTTTTTTTNNNNCCCCCCCCCCGGGGGGGGGG".to_string(), + ]) + ); +} + +#[test] +fn test_delete_at_beginning_of_path() { + let conn = get_connection(None).unwrap(); + let (block_group_id, path) = setup_block_group(&conn); + let deletion_sequence = Sequence::new() + .sequence_type("DNA") + .sequence("") + .save(&conn) + .unwrap(); + let deletion_node_id = + Node::create(&conn, &deletion_sequence.hash, &HashId::convert_str("1")).unwrap(); + let deletion = PathBlock { + node_id: deletion_node_id, + block_sequence: deletion_sequence.get_sequence(None, None).unwrap(), + sequence_start: 0, + sequence_end: 0, + path_start: 0, + path_end: 1, + strand: Strand::Forward, + }; + let region = ResolvedGenRegion::from_path(&conn, block_group_id, &path, 0, 1).unwrap(); + let change = BlockGroupChange { + region, + path_accession: None, + block: deletion, + chromosome_index: 1, + phased: 0, + preserve_edge: true, + }; + gen_graph::models::insert_change(&conn, &change).unwrap(); + + let all_sequences = get_all_sequences(&conn, &block_group_id).unwrap(); + assert_eq!( + all_sequences, + HashSet::from_iter(vec![ + "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), + "AAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), + ]) + ); +} + +#[test] +fn test_delete_at_end_of_path() { + let conn = get_connection(None).unwrap(); + let (block_group_id, path) = setup_block_group(&conn); + let deletion_sequence = Sequence::new() + .sequence_type("DNA") + .sequence("") + .save(&conn) + .unwrap(); + let deletion_node_id = + Node::create(&conn, &deletion_sequence.hash, &HashId::convert_str("1")).unwrap(); + let deletion = PathBlock { + node_id: deletion_node_id, + block_sequence: deletion_sequence.get_sequence(None, None).unwrap(), + sequence_start: 0, + sequence_end: 0, + path_start: 35, + path_end: 40, + strand: Strand::Forward, + }; + let region = ResolvedGenRegion::from_path(&conn, block_group_id, &path, 35, 40).unwrap(); + let change = BlockGroupChange { + region, + path_accession: None, + block: deletion, + chromosome_index: 1, + phased: 0, + preserve_edge: true, + }; + gen_graph::models::insert_change(&conn, &change).unwrap(); + + let all_sequences = get_all_sequences(&conn, &block_group_id).unwrap(); + assert_eq!( + all_sequences, + HashSet::from_iter(vec![ + "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGG".to_string(), + "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), + ]) + ); +} + +#[test] +fn test_deletion_starting_at_block_boundary() { + let conn = get_connection(None).unwrap(); + let (block_group_id, path) = setup_block_group(&conn); + let deletion_sequence = Sequence::new() + .sequence_type("DNA") + .sequence("") + .save(&conn) + .unwrap(); + let deletion_node_id = + Node::create(&conn, &deletion_sequence.hash, &HashId::convert_str("1")).unwrap(); + let deletion = PathBlock { + node_id: deletion_node_id, + block_sequence: deletion_sequence.get_sequence(None, None).unwrap(), + sequence_start: 0, + sequence_end: 0, + path_start: 10, + path_end: 12, + strand: Strand::Forward, + }; + let region = ResolvedGenRegion::from_path(&conn, block_group_id, &path, 10, 12).unwrap(); + let change = BlockGroupChange { + region, + path_accession: None, + block: deletion, + chromosome_index: 1, + phased: 0, + preserve_edge: true, + }; + gen_graph::models::insert_change(&conn, &change).unwrap(); + + let all_sequences = get_all_sequences(&conn, &block_group_id).unwrap(); + assert_eq!( + all_sequences, + HashSet::from_iter(vec![ + "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), + "AAAAAAAAAATTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), + ]) + ); +} + +#[test] +fn test_deletion_ending_at_block_boundary() { + let conn = get_connection(None).unwrap(); + let (block_group_id, path) = setup_block_group(&conn); + let deletion_sequence = Sequence::new() + .sequence_type("DNA") + .sequence("") + .save(&conn) + .unwrap(); + let deletion_node_id = + Node::create(&conn, &deletion_sequence.hash, &HashId::convert_str("1")).unwrap(); + let deletion = PathBlock { + node_id: deletion_node_id, + block_sequence: deletion_sequence.get_sequence(None, None).unwrap(), + sequence_start: 0, + sequence_end: 0, + path_start: 18, + path_end: 20, + strand: Strand::Forward, + }; + let region = ResolvedGenRegion::from_path(&conn, block_group_id, &path, 18, 20).unwrap(); + let change = BlockGroupChange { + region, + path_accession: None, + block: deletion, + chromosome_index: 1, + phased: 0, + preserve_edge: true, + }; + gen_graph::models::insert_change(&conn, &change).unwrap(); + + let all_sequences = get_all_sequences(&conn, &block_group_id).unwrap(); + assert_eq!( + all_sequences, + HashSet::from_iter(vec![ + "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), + "AAAAAAAAAATTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), + ]) + ); +} diff --git a/gen-models/graph-tests/tests/block_group_lineage.rs b/gen-models/graph-tests/tests/block_group_lineage.rs new file mode 100644 index 00000000..e7afbff3 --- /dev/null +++ b/gen-models/graph-tests/tests/block_group_lineage.rs @@ -0,0 +1,606 @@ +use std::collections::{HashMap, HashSet}; + +use gen_core::{HashId, PATH_END_NODE_ID, PATH_START_NODE_ID, PathBlock, Strand}; +use gen_graph::models::{get_all_sequences, get_sample_all_sequences}; +use gen_models::{ + block_group::{BlockGroup, BlockGroupChange}, + block_group_edge::{BlockGroupEdge, BlockGroupEdgeData}, + collection::Collection, + edge::Edge, + node::Node, + path::Path, + region::{ResolvedGenRegion, ResolvedRegionKind}, + sample::{NewSample, Sample}, + sequence::Sequence, + traits::Query as _, +}; +use gen_models_graph_tests::{ + create_block_group, get_connection, get_single_block_group_id, setup_block_group, +}; +use rusqlite::{params, types::Value as SQLValue}; + +#[test] +fn test_blockgroup_copies_immediate_parent_block_groups() { + let conn = &get_connection(None).unwrap(); + Collection::create(conn, "test").unwrap(); + Sample::get_or_create( + conn, + NewSample { + name: "parent_a", + ..Default::default() + }, + ) + .unwrap(); + Sample::get_or_create( + conn, + NewSample { + name: "parent_b", + ..Default::default() + }, + ) + .unwrap(); + Sample::get_or_create( + conn, + NewSample { + name: "child", + ..Default::default() + }, + ) + .unwrap(); + + let parent_a_bg = create_block_group(conn, "test", "parent_a", "chr1"); + let parent_b_bg = create_block_group(conn, "test", "parent_b", "chr1"); + + let seq_a = Sequence::new() + .sequence_type("DNA") + .sequence("AAAA") + .save(conn) + .unwrap(); + let seq_b = Sequence::new() + .sequence_type("DNA") + .sequence("CCCC") + .save(conn) + .unwrap(); + let node_a = Node::create(conn, &seq_a.hash, &HashId::convert_str("merge-parent-a")).unwrap(); + let node_b = Node::create(conn, &seq_b.hash, &HashId::convert_str("merge-parent-b")).unwrap(); + + let parent_a_edges = [ + Edge::create( + conn, + PATH_START_NODE_ID, + 0, + Strand::Forward, + node_a, + 0, + Strand::Forward, + ) + .unwrap(), + Edge::create( + conn, + node_a, + 4, + Strand::Forward, + PATH_END_NODE_ID, + 0, + Strand::Forward, + ) + .unwrap(), + ]; + let parent_b_edges = [ + Edge::create( + conn, + PATH_START_NODE_ID, + 0, + Strand::Forward, + node_b, + 0, + Strand::Forward, + ) + .unwrap(), + Edge::create( + conn, + node_b, + 4, + Strand::Forward, + PATH_END_NODE_ID, + 0, + Strand::Forward, + ) + .unwrap(), + ]; + + BlockGroupEdge::bulk_create( + conn, + &parent_a_edges + .iter() + .map(|edge| BlockGroupEdgeData { + block_group_id: parent_a_bg.id, + edge_id: edge.id, + chromosome_index: 0, + phased: 0, + }) + .collect::>(), + ); + BlockGroupEdge::bulk_create( + conn, + &parent_b_edges + .iter() + .map(|edge| BlockGroupEdgeData { + block_group_id: parent_b_bg.id, + edge_id: edge.id, + chromosome_index: 0, + phased: 0, + }) + .collect::>(), + ); + + let child_block_groups = BlockGroup::get_or_create_sample_block_groups( + conn, + "test", + "child", + "chr1", + vec!["parent_a".to_string(), "parent_b".to_string()], + ) + .unwrap(); + assert_eq!(child_block_groups.len(), 2); + + let child_by_parent = child_block_groups + .iter() + .map(|block_group| (block_group.parent_block_group_id.unwrap(), block_group)) + .collect::>(); + + let child_a = child_by_parent.get(&parent_a_bg.id).unwrap(); + let child_b = child_by_parent.get(&parent_b_bg.id).unwrap(); + + let child_a_edges = BlockGroupEdge::query( + conn, + "select * from block_group_edges where block_group_id = ?1", + params![child_a.id], + ); + let child_b_edges = BlockGroupEdge::query( + conn, + "select * from block_group_edges where block_group_id = ?1", + params![child_b.id], + ); + assert_eq!( + child_a_edges + .iter() + .map(|edge| edge.edge_id) + .collect::>(), + parent_a_edges + .iter() + .map(|edge| edge.id) + .collect::>() + ); + assert_eq!( + child_b_edges + .iter() + .map(|edge| edge.edge_id) + .collect::>(), + parent_b_edges + .iter() + .map(|edge| edge.id) + .collect::>() + ); + + assert_eq!( + get_all_sequences(conn, &child_a.id).unwrap(), + HashSet::from_iter(vec!["AAAA".to_string()]) + ); + assert_eq!( + get_all_sequences(conn, &child_b.id).unwrap(), + HashSet::from_iter(vec!["CCCC".to_string()]) + ); + assert_eq!( + get_sample_all_sequences(conn, "test", "child", None).unwrap(), + HashSet::from_iter(vec!["AAAA".to_string(), "CCCC".to_string()]) + ); +} + +#[test] +fn test_changes_against_derivative_blockgroups() { + let conn = &get_connection(None).unwrap(); + let (_block_group_id, _path) = setup_block_group(conn); + let _new_sample = Sample::get_or_create( + conn, + NewSample { + name: "child", + ..Default::default() + }, + ) + .unwrap(); + let new_bg_id = + get_single_block_group_id(conn, "test", "child", "chr1", vec!["test".to_string()]); + let new_path = Path::query( + conn, + "select * from paths where block_group_id = ?1", + rusqlite::params!(SQLValue::from(new_bg_id)), + ); + let insert_sequence = Sequence::new() + .sequence_type("DNA") + .sequence("NNNN") + .save(conn) + .unwrap(); + let insert_node_id = + Node::create(conn, &insert_sequence.hash, &HashId::convert_str("1")).unwrap(); + let insert = PathBlock { + node_id: insert_node_id, + block_sequence: insert_sequence.get_sequence(0, 4).unwrap(), + sequence_start: 0, + sequence_end: 4, + path_start: 7, + path_end: 15, + strand: Strand::Forward, + }; + let region = ResolvedGenRegion::from_path(conn, new_bg_id, &new_path[0], 7, 15).unwrap(); + let change = BlockGroupChange { + region, + path_accession: None, + block: insert, + chromosome_index: 1, + phased: 0, + preserve_edge: false, + }; + + // note we are making our change against the new blockgroup, and not the parent blockgroup + gen_graph::models::insert_change(conn, &change).unwrap(); + let all_sequences = get_all_sequences(conn, &new_bg_id).unwrap(); + assert_eq!( + all_sequences, + HashSet::from_iter(vec!["AAAAAAANNNNTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(),]) + ); + + // Now, we make a change against another descendant + let _new_sample = Sample::get_or_create( + conn, + NewSample { + name: "grandchild", + ..Default::default() + }, + ) + .unwrap(); + let gc_bg_id = get_single_block_group_id( + conn, + "test", + "grandchild", + "chr1", + vec!["child".to_string()], + ); + let _new_path = Path::query( + conn, + "select * from paths where block_group_id = ?1", + rusqlite::params!(SQLValue::from(gc_bg_id)), + ); + + let insert = PathBlock { + node_id: insert_node_id, + block_sequence: insert_sequence.get_sequence(0, 4).unwrap(), + sequence_start: 0, + sequence_end: 4, + path_start: 7, + path_end: 15, + strand: Strand::Forward, + }; + let gc_bg = BlockGroup::get_by_id(conn, &gc_bg_id, None).unwrap(); + let gc_region = ResolvedGenRegion { + block_group: gc_bg, + path: None, + accession: None, + annotation: None, + kind: ResolvedRegionKind::BlockGroup, + anchor_start: 0, + anchor_end: 0, + feature_length: 0, + start: 7, + end: 15, + start_anchors: None, + end_anchors: None, + remove_ambiguous_positions: true, + }; + let change = BlockGroupChange { + region: gc_region, + path_accession: None, + block: insert, + chromosome_index: 1, + phased: 0, + preserve_edge: false, + }; + gen_graph::models::insert_change(conn, &change).unwrap(); + let all_sequences = get_all_sequences(conn, &gc_bg_id).unwrap(); + assert_eq!( + all_sequences, + HashSet::from_iter(vec!["AAAAAAANNNNTCCCCCCCCCCGGGGGGGGGG".to_string(),]) + ); +} + +#[test] +fn test_changes_against_derivative_diploid_blockgroups() { + // This test ensures that if we have heterozygous changes that do not introduce frameshifts, + // we can modify regions downstream of them. + let conn = &get_connection(None).unwrap(); + let (_block_group_id, _path) = setup_block_group(conn); + let _new_sample = Sample::get_or_create( + conn, + NewSample { + name: "child", + ..Default::default() + }, + ) + .unwrap(); + let new_bg_id = + get_single_block_group_id(conn, "test", "child", "chr1", vec!["test".to_string()]); + let _new_path = Path::query( + conn, + "select * from paths where block_group_id = ?1", + params![new_bg_id], + ); + let insert_sequence = Sequence::new() + .sequence_type("DNA") + .sequence("NNNN") + .save(conn) + .unwrap(); + let insert_node_id = + Node::create(conn, &insert_sequence.hash, &HashId::convert_str("1")).unwrap(); + let insert = PathBlock { + node_id: insert_node_id, + block_sequence: insert_sequence.get_sequence(0, 4).unwrap(), + sequence_start: 0, + sequence_end: 4, + path_start: 7, + path_end: 11, + strand: Strand::Forward, + }; + let bg = BlockGroup::get_by_id(conn, &new_bg_id, None).unwrap(); + let region = ResolvedGenRegion { + block_group: bg, + path: None, + accession: None, + annotation: None, + kind: ResolvedRegionKind::BlockGroup, + anchor_start: 0, + anchor_end: 0, + feature_length: 0, + start: 7, + end: 11, + start_anchors: None, + end_anchors: None, + remove_ambiguous_positions: true, + }; + let change = BlockGroupChange { + region, + path_accession: None, + block: insert, + chromosome_index: 1, + phased: 0, + preserve_edge: true, + }; + gen_graph::models::insert_change(conn, &change).unwrap(); + let all_sequences = get_all_sequences(conn, &new_bg_id).unwrap(); + assert_eq!( + all_sequences, + HashSet::from_iter(vec![ + "AAAAAAANNNNTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), + "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), + ]) + ); + + // Now, we make a change against another descendant + let _new_sample = Sample::get_or_create( + conn, + NewSample { + name: "grandchild", + ..Default::default() + }, + ) + .unwrap(); + let gc_bg_id = get_single_block_group_id( + conn, + "test", + "grandchild", + "chr1", + vec!["child".to_string()], + ); + let _new_path = Path::query( + conn, + "select * from paths where block_group_id = ?1", + params![gc_bg_id], + ); + + let insert_sequence = Sequence::new() + .sequence_type("DNA") + .sequence("NNNN") + .save(conn) + .unwrap(); + let insert_node_id = Node::create( + conn, + &insert_sequence.hash, + &HashId::convert_str("new-hash"), + ) + .unwrap(); + + let insert = PathBlock { + node_id: insert_node_id, + block_sequence: insert_sequence.get_sequence(0, 4).unwrap(), + sequence_start: 0, + sequence_end: 4, + path_start: 20, + path_end: 24, + strand: Strand::Forward, + }; + let gc_bg = BlockGroup::get_by_id(conn, &gc_bg_id, None).unwrap(); + let gc_region = ResolvedGenRegion { + block_group: gc_bg, + path: None, + accession: None, + annotation: None, + kind: ResolvedRegionKind::BlockGroup, + anchor_start: 0, + anchor_end: 0, + feature_length: 0, + start: 20, + end: 24, + start_anchors: None, + end_anchors: None, + remove_ambiguous_positions: true, + }; + let change = BlockGroupChange { + region: gc_region, + path_accession: None, + block: insert, + chromosome_index: 1, + phased: 0, + preserve_edge: true, + }; + gen_graph::models::insert_change(conn, &change).unwrap(); + let all_sequences = get_all_sequences(conn, &gc_bg_id).unwrap(); + assert_eq!( + all_sequences, + HashSet::from_iter(vec![ + "AAAAAAANNNNTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), + "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), + "AAAAAAAAAATTTTTTTTTTNNNNCCCCCCGGGGGGGGGG".to_string(), + "AAAAAAANNNNTTTTTTTTTNNNNCCCCCCGGGGGGGGGG".to_string() + ]) + ); +} + +#[test] +#[should_panic] +fn test_prohibits_out_of_frame_changes_against_derivative_diploid_blockgroups() { + // This test ensures that we do not allow ambiguous changes by coordinates + let conn = &get_connection(None).unwrap(); + let (_block_group_id, _path) = setup_block_group(conn); + let _new_sample = Sample::get_or_create( + conn, + NewSample { + name: "child", + ..Default::default() + }, + ) + .unwrap(); + let new_bg_id = + get_single_block_group_id(conn, "test", "child", "chr1", vec!["test".to_string()]); + let _new_path = Path::query( + conn, + "select * from paths where block_group_id = ?1", + rusqlite::params!(SQLValue::from(new_bg_id)), + ); + // This is a heterozygous replacement of 5 bases with 4 bases, so positions + // downstream of this are not addressable. + let insert_sequence = Sequence::new() + .sequence_type("DNA") + .sequence("NNNN") + .save(conn) + .unwrap(); + let insert_node_id = + Node::create(conn, &insert_sequence.hash, &HashId::convert_str("1")).unwrap(); + let insert = PathBlock { + node_id: insert_node_id, + block_sequence: insert_sequence.get_sequence(0, 4).unwrap(), + sequence_start: 0, + sequence_end: 4, + path_start: 7, + path_end: 12, + strand: Strand::Forward, + }; + let bg = BlockGroup::get_by_id(conn, &new_bg_id, None).unwrap(); + let region = ResolvedGenRegion { + block_group: bg, + path: None, + accession: None, + annotation: None, + kind: ResolvedRegionKind::BlockGroup, + anchor_start: 0, + anchor_end: 0, + feature_length: 0, + start: 7, + end: 12, + start_anchors: None, + end_anchors: None, + remove_ambiguous_positions: true, + }; + let change = BlockGroupChange { + region, + path_accession: None, + block: insert, + chromosome_index: 1, + phased: 0, + preserve_edge: true, + }; + + // note we are making our change against the new blockgroup, and not the parent blockgroup + gen_graph::models::insert_change(conn, &change).unwrap(); + let all_sequences = get_all_sequences(conn, &new_bg_id).unwrap(); + assert_eq!( + all_sequences, + HashSet::from_iter(vec![ + "AAAAAAANNNNTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), + "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), + ]) + ); + + // Now, we make a change against another descendant and get an error + let _new_sample = Sample::get_or_create( + conn, + NewSample { + name: "grandchild", + ..Default::default() + }, + ) + .unwrap(); + let gc_bg_id = get_single_block_group_id( + conn, + "test", + "grandchild", + "chr1", + vec!["child".to_string()], + ); + let _new_path = Path::query( + conn, + "select * from paths where block_group_id = ?1", + rusqlite::params!(SQLValue::from(gc_bg_id)), + ); + + let insert_sequence = Sequence::new() + .sequence_type("DNA") + .sequence("NNNN") + .save(conn) + .unwrap(); + let insert_node_id = + Node::create(conn, &insert_sequence.hash, &HashId::pad_str("new-hash")).unwrap(); + + let insert = PathBlock { + node_id: insert_node_id, + block_sequence: insert_sequence.get_sequence(0, 4).unwrap(), + sequence_start: 0, + sequence_end: 4, + path_start: 20, + path_end: 24, + strand: Strand::Forward, + }; + let gc_bg = BlockGroup::get_by_id(conn, &gc_bg_id, None).unwrap(); + let gc_region = ResolvedGenRegion { + block_group: gc_bg, + path: None, + accession: None, + annotation: None, + kind: ResolvedRegionKind::BlockGroup, + anchor_start: 0, + anchor_end: 0, + feature_length: 0, + start: 20, + end: 24, + start_anchors: None, + end_anchors: None, + remove_ambiguous_positions: true, + }; + let change = BlockGroupChange { + region: gc_region, + path_accession: None, + block: insert, + chromosome_index: 1, + phased: 0, + preserve_edge: true, + }; + gen_graph::models::insert_change(conn, &change).unwrap(); +} diff --git a/gen-models/graph-tests/tests/derive_subgraph.rs b/gen-models/graph-tests/tests/derive_subgraph.rs new file mode 100644 index 00000000..0fafd630 --- /dev/null +++ b/gen-models/graph-tests/tests/derive_subgraph.rs @@ -0,0 +1,656 @@ +use core::ops::Range; +use std::collections::HashSet; + +use gen_core::{HashId, NO_CHROMOSOME_INDEX, Strand, calculate_hash}; +use gen_graph::models::{derive_subgraph, get_all_sequences}; +use gen_models::{ + block_group_edge::{BlockGroupEdge, BlockGroupEdgeData}, + edge::Edge, + node::Node, + sequence::Sequence, + traits::Query as _, +}; +use gen_models_graph_tests::{create_block_group, get_connection, setup_block_group}; +use rusqlite::params; + +#[test] +fn test_derive_subgraph_one_insertion() { + /* + AAAAAAAAAA -> TTTTTTTTTT -> CCCCCCCCCC -> GGGGGGGGGG + \-> AAAAAAAA ->/ + Subgraph range: |-----------------| + Sequences of the subgraph are TAAAAAAAAC, TTTTTCCCCC + */ + let conn = &get_connection(None).unwrap(); + let (block_group1_id, original_path) = setup_block_group(conn); + + let intervaltree = original_path.intervaltree(conn).unwrap(); + let insert_start_node_id = intervaltree.query_point(16).next().unwrap().value.node_id; + let insert_end_node_id = intervaltree.query_point(24).next().unwrap().value.node_id; + + let insert_sequence = Sequence::new() + .sequence_type("DNA") + .sequence("AAAAAAAA") + .save(conn) + .unwrap(); + let insert_node_id = Node::create( + conn, + &insert_sequence.hash, + &HashId(calculate_hash(&format!( + "test-insert-a-node.{}", + insert_sequence.hash + ))), + ) + .unwrap(); + let edge_into_insert = Edge::create( + conn, + insert_start_node_id, + 6, + Strand::Forward, + insert_node_id, + 0, + Strand::Forward, + ) + .unwrap(); + let edge_out_of_insert = Edge::create( + conn, + insert_node_id, + 8, + Strand::Forward, + insert_end_node_id, + 4, + Strand::Forward, + ) + .unwrap(); + let ref_heal_1 = Edge::create( + conn, + insert_start_node_id, + 6, + Strand::Forward, + insert_start_node_id, + 6, + Strand::Forward, + ) + .unwrap(); + let ref_heal_2 = Edge::create( + conn, + insert_end_node_id, + 4, + Strand::Forward, + insert_end_node_id, + 4, + Strand::Forward, + ) + .unwrap(); + + let edge_ids = [ + &edge_into_insert.id, + &edge_out_of_insert.id, + &ref_heal_1.id, + &ref_heal_2.id, + ]; + let block_group_edges = edge_ids + .iter() + .enumerate() + .map(|(i, edge_id)| BlockGroupEdgeData { + block_group_id: block_group1_id, + edge_id: *(*edge_id), + chromosome_index: if i < 2 { 1 } else { 0 }, + phased: 0, + }) + .collect::>(); + BlockGroupEdge::bulk_create(conn, &block_group_edges); + + let insert_path = original_path + .new_path_with(conn, 16, 24, &edge_into_insert, &edge_out_of_insert) + .unwrap(); + assert_eq!( + insert_path.sequence(conn, None).unwrap(), + "AAAAAAAAAATTTTTTAAAAAAAACCCCCCGGGGGGGGGG" + ); + + let all_sequences = get_all_sequences(conn, &block_group1_id).unwrap(); + assert_eq!( + all_sequences, + HashSet::from_iter(vec![ + "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), + "AAAAAAAAAATTTTTTAAAAAAAACCCCCCGGGGGGGGGG".to_string(), + ]) + ); + + let mut blocks = intervaltree + .query(Range { start: 15, end: 25 }) + .map(|x| x.value) + .collect::>(); + blocks.sort_by_key(|a| a.start); + let start_block = blocks[0]; + let start_node_coordinate = 15 - start_block.start + start_block.sequence_start; + let end_block = blocks[blocks.len() - 1]; + let end_node_coordinate = 25 - end_block.start + end_block.sequence_start; + + let block_group2 = create_block_group(conn, "test", "test", "chr1.1"); + let node_count_before = Node::query(conn, "SELECT * FROM nodes", params![]).len(); + derive_subgraph( + conn, + &block_group1_id, + &start_block, + &end_block, + start_node_coordinate, + end_node_coordinate, + &block_group2.id, + true, + ) + .unwrap(); + let node_count_after = Node::query(conn, "SELECT * FROM nodes", params![]).len(); + assert_eq!(node_count_after, node_count_before); + let all_sequences2 = get_all_sequences(conn, &block_group2.id).unwrap(); + assert_eq!( + all_sequences2, + HashSet::from_iter(vec!["TTTTTCCCCC".to_string(), "TAAAAAAAAC".to_string(),]) + ); +} + +#[test] +fn test_derive_subgraph_two_independent_insertions() { + /* + AAAAAAAAAA -> TTTTTTTTTT -> CCCCCCCCCC -----> GGGGGGGGGG + \-> AAAAAAAA ->/ \->TTTTTTTT -/ + Subgraph range: |----------------------------------| + */ + let conn = &get_connection(None).unwrap(); + let (block_group1_id, original_path) = setup_block_group(conn); + + let intervaltree = original_path.intervaltree(conn).unwrap(); + let insert_start_node_id = intervaltree.query_point(16).next().unwrap().value.node_id; + let insert_end_node_id = intervaltree.query_point(24).next().unwrap().value.node_id; + + let insert_sequence = Sequence::new() + .sequence_type("DNA") + .sequence("AAAAAAAA") + .save(conn) + .unwrap(); + let insert_node_id = Node::create( + conn, + &insert_sequence.hash, + &HashId(calculate_hash(&format!( + "test-insert-a-node.{}", + insert_sequence.hash + ))), + ) + .unwrap(); + let edge_into_insert = Edge::create( + conn, + insert_start_node_id, + 6, + Strand::Forward, + insert_node_id, + 0, + Strand::Forward, + ) + .unwrap(); + let edge_out_of_insert = Edge::create( + conn, + insert_node_id, + 8, + Strand::Forward, + insert_end_node_id, + 4, + Strand::Forward, + ) + .unwrap(); + let ref_heal_1 = Edge::create( + conn, + insert_start_node_id, + 6, + Strand::Forward, + insert_start_node_id, + 6, + Strand::Forward, + ) + .unwrap(); + let ref_heal_2 = Edge::create( + conn, + insert_end_node_id, + 4, + Strand::Forward, + insert_end_node_id, + 4, + Strand::Forward, + ) + .unwrap(); + + let edge_ids = [ + &edge_into_insert.id, + &edge_out_of_insert.id, + &ref_heal_1.id, + &ref_heal_2.id, + ]; + let block_group_edges = edge_ids + .iter() + .enumerate() + .map(|(i, edge_id)| BlockGroupEdgeData { + block_group_id: block_group1_id, + edge_id: *(*edge_id), + chromosome_index: if i < 2 { 1 } else { 0 }, + phased: 0, + }) + .collect::>(); + BlockGroupEdge::bulk_create(conn, &block_group_edges); + + let insert_path = original_path + .new_path_with(conn, 16, 24, &edge_into_insert, &edge_out_of_insert) + .unwrap(); + assert_eq!( + insert_path.sequence(conn, None).unwrap(), + "AAAAAAAAAATTTTTTAAAAAAAACCCCCCGGGGGGGGGG" + ); + + let insert2_start_node_id = intervaltree.query_point(28).next().unwrap().value.node_id; + let insert2_end_node_id = intervaltree.query_point(32).next().unwrap().value.node_id; + + let insert2_sequence = Sequence::new() + .sequence_type("DNA") + .sequence("TTTTTTTT") + .save(conn) + .unwrap(); + let insert2_node_id = Node::create( + conn, + &insert2_sequence.hash, + &HashId(calculate_hash(&format!( + "test-insert-t-node.{}", + insert2_sequence.hash + ))), + ) + .unwrap(); + let edge_into_insert2 = Edge::create( + conn, + insert2_start_node_id, + 6, + Strand::Forward, + insert2_node_id, + 0, + Strand::Forward, + ) + .unwrap(); + let edge_out_of_insert2 = Edge::create( + conn, + insert2_node_id, + 8, + Strand::Forward, + insert2_end_node_id, + 4, + Strand::Forward, + ) + .unwrap(); + let ref_heal_1 = Edge::create( + conn, + insert2_start_node_id, + 6, + Strand::Forward, + insert2_start_node_id, + 6, + Strand::Forward, + ) + .unwrap(); + let ref_heal_2 = Edge::create( + conn, + insert2_end_node_id, + 4, + Strand::Forward, + insert2_end_node_id, + 4, + Strand::Forward, + ) + .unwrap(); + + let edge_ids = [ + &edge_into_insert2.id, + &edge_out_of_insert2.id, + &ref_heal_1.id, + &ref_heal_2.id, + ]; + let block_group_edges = edge_ids + .iter() + .enumerate() + .map(|(i, edge_id)| BlockGroupEdgeData { + block_group_id: block_group1_id, + edge_id: *(*edge_id), + chromosome_index: if i < 2 { 1 } else { 0 }, + phased: 0, + }) + .collect::>(); + BlockGroupEdge::bulk_create(conn, &block_group_edges); + + let insert2_path = insert_path + .new_path_with(conn, 28, 32, &edge_into_insert2, &edge_out_of_insert2) + .unwrap(); + assert_eq!( + insert2_path.sequence(conn, None).unwrap(), + "AAAAAAAAAATTTTTTAAAAAAAACCTTTTTTTTGGGGGG" + ); + + let all_sequences = get_all_sequences(conn, &block_group1_id).unwrap(); + assert_eq!( + all_sequences, + HashSet::from_iter(vec![ + "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), + "AAAAAAAAAATTTTTTAAAAAAAACCCCCCGGGGGGGGGG".to_string(), + "AAAAAAAAAATTTTTTTTTTCCCCCCTTTTTTTTGGGGGG".to_string(), + "AAAAAAAAAATTTTTTAAAAAAAACCTTTTTTTTGGGGGG".to_string(), + ]) + ); + + let mut blocks = intervaltree + .query(Range { start: 15, end: 36 }) + .map(|x| x.value) + .collect::>(); + blocks.sort_by_key(|a| a.start); + let start_block = blocks[0]; + let start_node_coordinate = 15 - start_block.start + start_block.sequence_start; + let end_block = blocks[blocks.len() - 1]; + let end_node_coordinate = 36 - end_block.start + end_block.sequence_start; + + let block_group2 = create_block_group(conn, "test", "test", "chr1.1"); + derive_subgraph( + conn, + &block_group1_id, + &start_block, + &end_block, + start_node_coordinate, + end_node_coordinate, + &block_group2.id, + true, + ) + .unwrap(); + let all_sequences2 = get_all_sequences(conn, &block_group2.id).unwrap(); + assert_eq!( + all_sequences2, + HashSet::from_iter(vec![ + "TTTTTCCCCCCCCCCGGGGGG".to_string(), + "TAAAAAAAACCCCCCGGGGGG".to_string(), + "TTTTTCCCCCCTTTTTTTTGG".to_string(), + "TAAAAAAAACCTTTTTTTTGG".to_string(), + ]) + ); +} + +#[test] +fn test_derive_subgraph_two_independent_insertions_and_one_deletion() { + /* + /--------------------------------------------\ (<-- Deletion edge) + AAAAAAAAAA -> TTTTTTTTTT -> CCCCCCCCCC -----> GGGGGGGGGG + \-> AAAAAAAA ->/ \->TTTTTTTT -/ + Subgraph range: |----------------------------------| + + Confirms that deletion edge is ignored and not added to subgraph + */ + let conn = &get_connection(None).unwrap(); + let (block_group1_id, original_path) = setup_block_group(conn); + + let intervaltree = original_path.intervaltree(conn).unwrap(); + let insert_start_node_id = intervaltree.query_point(16).next().unwrap().value.node_id; + let insert_end_node_id = intervaltree.query_point(24).next().unwrap().value.node_id; + + let insert_sequence = Sequence::new() + .sequence_type("DNA") + .sequence("AAAAAAAA") + .save(conn) + .unwrap(); + let insert_node_id = Node::create( + conn, + &insert_sequence.hash, + &HashId(calculate_hash(&format!( + "test-insert-a-node.{}", + insert_sequence.hash + ))), + ) + .unwrap(); + let edge_into_insert = Edge::create( + conn, + insert_start_node_id, + 6, + Strand::Forward, + insert_node_id, + 0, + Strand::Forward, + ) + .unwrap(); + let edge_out_of_insert = Edge::create( + conn, + insert_node_id, + 8, + Strand::Forward, + insert_end_node_id, + 4, + Strand::Forward, + ) + .unwrap(); + let ref_heal_1 = Edge::create( + conn, + insert_start_node_id, + 6, + Strand::Forward, + insert_start_node_id, + 6, + Strand::Forward, + ) + .unwrap(); + let ref_heal_2 = Edge::create( + conn, + insert_end_node_id, + 4, + Strand::Forward, + insert_end_node_id, + 4, + Strand::Forward, + ) + .unwrap(); + + let edge_ids = [ + &edge_into_insert.id, + &edge_out_of_insert.id, + &ref_heal_1.id, + &ref_heal_2.id, + ]; + let block_group_edges = edge_ids + .iter() + .map(|edge_id| BlockGroupEdgeData { + block_group_id: block_group1_id, + edge_id: *(*edge_id), + chromosome_index: NO_CHROMOSOME_INDEX, + phased: 0, + }) + .collect::>(); + BlockGroupEdge::bulk_create(conn, &block_group_edges); + + let insert_path = original_path + .new_path_with(conn, 16, 24, &edge_into_insert, &edge_out_of_insert) + .unwrap(); + assert_eq!( + insert_path.sequence(conn, None).unwrap(), + "AAAAAAAAAATTTTTTAAAAAAAACCCCCCGGGGGGGGGG" + ); + + let insert2_start_node_id = intervaltree.query_point(28).next().unwrap().value.node_id; + let insert2_end_node_id = intervaltree.query_point(32).next().unwrap().value.node_id; + + let insert2_sequence = Sequence::new() + .sequence_type("DNA") + .sequence("TTTTTTTT") + .save(conn) + .unwrap(); + let insert2_node_id = Node::create( + conn, + &insert2_sequence.hash, + &HashId(calculate_hash(&format!( + "test-insert-t-node.{}", + insert2_sequence.hash + ))), + ) + .unwrap(); + let edge_into_insert2 = Edge::create( + conn, + insert2_start_node_id, + 6, + Strand::Forward, + insert2_node_id, + 0, + Strand::Forward, + ) + .unwrap(); + let edge_out_of_insert2 = Edge::create( + conn, + insert2_node_id, + 8, + Strand::Forward, + insert2_end_node_id, + 4, + Strand::Forward, + ) + .unwrap(); + let ref_heal_1 = Edge::create( + conn, + insert2_start_node_id, + 6, + Strand::Forward, + insert2_start_node_id, + 6, + Strand::Forward, + ) + .unwrap(); + let ref_heal_2 = Edge::create( + conn, + insert2_end_node_id, + 4, + Strand::Forward, + insert2_end_node_id, + 4, + Strand::Forward, + ) + .unwrap(); + + let edge_ids = [ + &edge_into_insert2.id, + &edge_out_of_insert2.id, + &ref_heal_1.id, + &ref_heal_2.id, + ]; + let block_group_edges = edge_ids + .iter() + .map(|edge_id| BlockGroupEdgeData { + block_group_id: block_group1_id, + edge_id: *(*edge_id), + chromosome_index: NO_CHROMOSOME_INDEX, + phased: 0, + }) + .collect::>(); + BlockGroupEdge::bulk_create(conn, &block_group_edges); + + let insert2_path = insert_path + .new_path_with(conn, 28, 32, &edge_into_insert2, &edge_out_of_insert2) + .unwrap(); + assert_eq!( + insert2_path.sequence(conn, None).unwrap(), + "AAAAAAAAAATTTTTTAAAAAAAACCTTTTTTTTGGGGGG" + ); + + let deletion_end_node_id = intervaltree.query_point(38).next().unwrap().value.node_id; + let deletion_edge = Edge::create( + conn, + insert_node_id, + 8, + Strand::Forward, + deletion_end_node_id, + 8, + Strand::Forward, + ) + .unwrap(); + let ref_heal_1 = Edge::create( + conn, + insert_node_id, + 8, + Strand::Forward, + insert_node_id, + 8, + Strand::Forward, + ) + .unwrap(); + let ref_heal_2 = Edge::create( + conn, + deletion_end_node_id, + 8, + Strand::Forward, + deletion_end_node_id, + 8, + Strand::Forward, + ) + .unwrap(); + let block_group_edges = [ + BlockGroupEdgeData { + block_group_id: block_group1_id, + edge_id: deletion_edge.id, + chromosome_index: NO_CHROMOSOME_INDEX, + phased: 0, + }, + BlockGroupEdgeData { + block_group_id: block_group1_id, + edge_id: ref_heal_1.id, + chromosome_index: NO_CHROMOSOME_INDEX, + phased: 0, + }, + BlockGroupEdgeData { + block_group_id: block_group1_id, + edge_id: ref_heal_2.id, + chromosome_index: NO_CHROMOSOME_INDEX, + phased: 0, + }, + ]; + BlockGroupEdge::bulk_create(conn, &block_group_edges); + + let all_sequences = get_all_sequences(conn, &block_group1_id).unwrap(); + assert_eq!( + all_sequences, + HashSet::from_iter(vec![ + "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), + "AAAAAAAAAATTTTTTAAAAAAAACCCCCCGGGGGGGGGG".to_string(), + "AAAAAAAAAATTTTTTTTTTCCCCCCTTTTTTTTGGGGGG".to_string(), + "AAAAAAAAAATTTTTTAAAAAAAACCTTTTTTTTGGGGGG".to_string(), + "AAAAAAAAAATTTTTTAAAAAAAAGG".to_string(), // Sequence including deletion + ]) + ); + + let mut blocks = intervaltree + .query(Range { start: 15, end: 36 }) + .map(|x| x.value) + .collect::>(); + blocks.sort_by_key(|a| a.start); + let start_block = blocks[0]; + let start_node_coordinate = 15 - start_block.start + start_block.sequence_start; + let end_block = blocks[blocks.len() - 1]; + let end_node_coordinate = 36 - end_block.start + end_block.sequence_start; + + let block_group2 = create_block_group(conn, "test", "test", "chr1.1"); + derive_subgraph( + conn, + &block_group1_id, + &start_block, + &end_block, + start_node_coordinate, + end_node_coordinate, + &block_group2.id, + true, + ) + .unwrap(); + let all_sequences2 = get_all_sequences(conn, &block_group2.id).unwrap(); + assert_eq!( + all_sequences2, + // The deletion is not included in the cloned subgraph since one end of it is + // outside the specified range + HashSet::from_iter(vec![ + "TTTTTCCCCCCCCCCGGGGGG".to_string(), + "TAAAAAAAACCCCCCGGGGGG".to_string(), + "TTTTTCCCCCCTTTTTTTTGG".to_string(), + "TAAAAAAAACCTTTTTTTTGG".to_string(), + ]) + ); +} diff --git a/gen-models/graph-tests/tests/edge_graph.rs b/gen-models/graph-tests/tests/edge_graph.rs new file mode 100644 index 00000000..2eb62ca0 --- /dev/null +++ b/gen-models/graph-tests/tests/edge_graph.rs @@ -0,0 +1,184 @@ +use gen_core::{GraphNode, HashId, Strand}; +use gen_graph::graph_loader::{GraphLoadBlock, GraphLoadEdge, build_graph}; + +fn graph_block(id: i64, node_id: HashId, start: i64, end: i64) -> GraphLoadBlock { + GraphLoadBlock { + id, + node_id, + start, + end, + } +} + +fn graph_edge( + id: &str, + source_node_id: HashId, + source_coordinate: i64, + target_node_id: HashId, + target_coordinate: i64, +) -> GraphLoadEdge { + GraphLoadEdge { + edge_id: HashId::convert_str(id), + source_node_id, + source_coordinate, + source_strand: Strand::Forward, + target_node_id, + target_coordinate, + target_strand: Strand::Forward, + chromosome_index: 0, + phased: 0, + created_on: 0, + } +} + +fn graph_node(block: &GraphLoadBlock) -> GraphNode { + GraphNode { + node_id: block.node_id, + sequence_start: block.start, + sequence_end: block.end, + } +} + +#[test] +fn test_build_graph_routes_incoming_edge_to_junction() { + let source_node_id = HashId::convert_str("incoming-source"); + let target_node_id = HashId::convert_str("incoming-target"); + let blocks = vec![ + graph_block(0, source_node_id, 0, 3), + graph_block(1, target_node_id, 0, 0), + graph_block(2, target_node_id, 0, 1), + ]; + let edges = vec![graph_edge( + "incoming-edge", + source_node_id, + 3, + target_node_id, + 0, + )]; + + let (graph, _) = build_graph(&edges, &blocks); + + assert!( + graph.contains_edge(graph_node(&blocks[0]), graph_node(&blocks[1])), + "incoming edge should terminate at the junction" + ); + assert!( + !graph.contains_edge(graph_node(&blocks[0]), graph_node(&blocks[2])), + "incoming edge should not bypass the junction" + ); + assert_eq!(graph.edge_count(), 1, "should project one block edge"); +} + +#[test] +fn test_build_graph_routes_outgoing_edge_from_junction() { + let source_node_id = HashId::convert_str("outgoing-source"); + let target_node_id = HashId::convert_str("outgoing-target"); + let blocks = vec![ + graph_block(0, source_node_id, 0, 1), + graph_block(1, source_node_id, 1, 1), + graph_block(2, target_node_id, 0, 2), + ]; + let edges = vec![graph_edge( + "outgoing-edge", + source_node_id, + 1, + target_node_id, + 0, + )]; + + let (graph, _) = build_graph(&edges, &blocks); + + assert!( + graph.contains_edge(graph_node(&blocks[1]), graph_node(&blocks[2])), + "outgoing edge should originate at the junction" + ); + assert!( + !graph.contains_edge(graph_node(&blocks[0]), graph_node(&blocks[2])), + "outgoing edge should not bypass the junction" + ); + assert_eq!(graph.edge_count(), 1, "should create one block edge"); +} + +#[test] +fn test_build_graph_creates_same_coordinate_edge_from_start_junction() { + let node_id = HashId::convert_str("same-coordinate-node"); + let blocks = vec![graph_block(0, node_id, 0, 0), graph_block(1, node_id, 0, 1)]; + let edges = vec![graph_edge("same-coordinate-edge", node_id, 0, node_id, 0)]; + + let (graph, _) = build_graph(&edges, &blocks); + + assert!( + graph.contains_edge(graph_node(&blocks[0]), graph_node(&blocks[1])), + "same-coordinate edge should connect the junction to adjacent sequence" + ); + assert!( + !graph.contains_edge(graph_node(&blocks[0]), graph_node(&blocks[0])), + "same-coordinate edge should not add a redundant junction self-loop" + ); + assert_eq!(graph.edge_count(), 1, "should create one block edge"); +} + +#[test] +fn test_build_graph_creates_same_coordinate_edge_into_end_junction() { + let node_id = HashId::convert_str("ending-same-coordinate-node"); + let blocks = vec![graph_block(0, node_id, 0, 1), graph_block(1, node_id, 1, 1)]; + let edges = vec![graph_edge( + "ending-same-coordinate-edge", + node_id, + 1, + node_id, + 1, + )]; + + let (graph, _) = build_graph(&edges, &blocks); + + assert!( + graph.contains_edge(graph_node(&blocks[0]), graph_node(&blocks[1])), + "same-coordinate edge should connect adjacent sequence into the junction" + ); + assert!( + !graph.contains_edge(graph_node(&blocks[1]), graph_node(&blocks[1])), + "same-coordinate edge should not add a redundant junction self-loop" + ); + assert_eq!(graph.edge_count(), 1, "should create one block edge"); +} + +#[test] +fn test_build_graph_creates_same_coordinate_edge_without_junction_directly() { + let node_id = HashId::convert_str("interior-same-coordinate-node"); + let blocks = vec![graph_block(0, node_id, 0, 1), graph_block(1, node_id, 1, 2)]; + let edges = vec![graph_edge( + "interior-same-coordinate-edge", + node_id, + 1, + node_id, + 1, + )]; + + let (graph, _) = build_graph(&edges, &blocks); + + assert!( + graph.contains_edge(graph_node(&blocks[0]), graph_node(&blocks[1])), + "same-coordinate edge should directly connect sequence blocks without a junction" + ); + assert_eq!(graph.edge_count(), 1, "should create one block edge"); +} + +#[test] +fn test_build_graph_omits_same_coordinate_edge_without_adjacent_sequence() { + let node_id = HashId::convert_str("isolated-junction"); + let blocks = vec![graph_block(0, node_id, 0, 0)]; + let edges = vec![graph_edge("isolated-edge", node_id, 0, node_id, 0)]; + + let (graph, edges_by_node_pair) = build_graph(&edges, &blocks); + + assert!( + !graph.contains_edge(graph_node(&blocks[0]), graph_node(&blocks[0])), + "a junction without adjacent sequence should not create a graph self-loop" + ); + assert_eq!(graph.edge_count(), 0, "should not create a block edge"); + assert!( + edges_by_node_pair.is_empty(), + "omitted self-loop should not have a block-pair mapping" + ); +} diff --git a/gen-models/graph-tests/tests/graph_loader.rs b/gen-models/graph-tests/tests/graph_loader.rs new file mode 100644 index 00000000..71fae0cc --- /dev/null +++ b/gen-models/graph-tests/tests/graph_loader.rs @@ -0,0 +1,556 @@ +use std::collections::HashSet; + +use gen_core::{ + GenGraph, GraphNode, GraphNodePosition, HashId, NodeIntervalBlock, PATH_END_NODE_ID, + PATH_START_NODE_ID, Strand, +}; +use gen_graph::{GraphEdge, graph_from_interval_tree, graph_loader, models::expand}; +use gen_models::{ + block_group::{BlockGroup, NewBlockGroup}, + block_group_edge::{BlockGroupEdge, BlockGroupEdgeData}, + collection::Collection, + db::GraphConnection, + edge::Edge, + node::Node, + sample::{NewSample, Sample}, + sequence::Sequence, +}; +use gen_models_graph_tests::get_connection; +use intervaltree::IntervalTree; + +fn setup_subset_graph() -> (GraphConnection, HashId) { + let conn = get_connection(None).unwrap(); + Collection::get_or_create(&conn, "test").unwrap(); + Sample::get_or_create( + &conn, + NewSample { + name: "test", + ..Default::default() + }, + ) + .unwrap(); + let block_group = BlockGroup::create( + &conn, + NewBlockGroup { + collection_name: "test", + sample_name: "test", + name: "chr1", + ..Default::default() + }, + ) + .unwrap(); + + let seq_x = Sequence::new() + .sequence_type("DNA") + .sequence("XXXXX") + .save(&conn) + .unwrap(); + let seq_y = Sequence::new() + .sequence_type("DNA") + .sequence("YYYYY") + .save(&conn) + .unwrap(); + let seq_z = Sequence::new() + .sequence_type("DNA") + .sequence("ZZZZZ") + .save(&conn) + .unwrap(); + + let node_x = Node::create(&conn, &seq_x.hash, &HashId::convert_str("node-x")).unwrap(); + let node_y = Node::create(&conn, &seq_y.hash, &HashId::convert_str("node-y")).unwrap(); + let node_z = Node::create(&conn, &seq_z.hash, &HashId::convert_str("node-z")).unwrap(); + + let e_start = Edge::create( + &conn, + PATH_START_NODE_ID, + -1, + Strand::Forward, + node_x, + 0, + Strand::Forward, + ) + .unwrap(); + let e_xy = Edge::create( + &conn, + node_x, + 5, + Strand::Forward, + node_y, + 0, + Strand::Forward, + ) + .unwrap(); + let e_yz = Edge::create( + &conn, + node_y, + 5, + Strand::Forward, + node_z, + 0, + Strand::Forward, + ) + .unwrap(); + let e_end = Edge::create( + &conn, + node_z, + 5, + Strand::Forward, + PATH_END_NODE_ID, + 0, + Strand::Forward, + ) + .unwrap(); + + BlockGroupEdge::bulk_create( + &conn, + &[ + BlockGroupEdgeData { + block_group_id: block_group.id, + edge_id: e_start.id, + chromosome_index: 0, + phased: 0, + }, + BlockGroupEdgeData { + block_group_id: block_group.id, + edge_id: e_xy.id, + chromosome_index: 0, + phased: 0, + }, + BlockGroupEdgeData { + block_group_id: block_group.id, + edge_id: e_yz.id, + chromosome_index: 0, + phased: 0, + }, + BlockGroupEdgeData { + block_group_id: block_group.id, + edge_id: e_end.id, + chromosome_index: 0, + phased: 0, + }, + ], + ); + + (conn, block_group.id) +} + +fn subset_interval_tree() -> IntervalTree { + vec![( + 0..5, + NodeIntervalBlock { + node_id: HashId::convert_str("node-y"), + start: 0, + end: 5, + sequence_start: 0, + sequence_end: 5, + strand: Strand::Forward, + }, + )] + .into_iter() + .collect() +} + +fn test_edge(edge_id: &str) -> Vec { + vec![GraphEdge { + edge_id: HashId::convert_str(edge_id), + source_strand: Strand::Forward, + target_strand: Strand::Forward, + chromosome_index: 0, + phased: 0, + created_on: 0, + }] +} + +fn variable_length_branched_graph() -> GenGraph { + let node_aaa = GraphNode { + node_id: HashId::convert_str("node-aaa"), + sequence_start: 0, + sequence_end: 3, + }; + let node_cc = GraphNode { + node_id: HashId::convert_str("node-cc"), + sequence_start: 0, + sequence_end: 2, + }; + let node_gggg = GraphNode { + node_id: HashId::convert_str("node-gggg"), + sequence_start: 0, + sequence_end: 4, + }; + let node_ttt = GraphNode { + node_id: HashId::convert_str("node-ttt"), + sequence_start: 0, + sequence_end: 3, + }; + + let mut graph = GenGraph::new(); + graph.add_edge(node_aaa, node_cc, test_edge("edge-aaa-cc")); + graph.add_edge(node_aaa, node_gggg, test_edge("edge-aaa-gggg")); + graph.add_edge(node_cc, node_ttt, test_edge("edge-cc-ttt")); + graph.add_edge(node_gggg, node_ttt, test_edge("edge-gggg-ttt")); + graph +} + +fn position_set(positions: &[GraphNodePosition]) -> HashSet<(HashId, i64)> { + positions + .iter() + .map(|pos| (pos.graph_node.node_id, pos.offset)) + .collect() +} + +#[test] +fn test_find_offset_within_subset_node() { + let (conn, bg_id) = setup_subset_graph(); + let tree = subset_interval_tree(); + let mut graph = graph_from_interval_tree(&tree); + + let y_node = GraphNode { + node_id: HashId::convert_str("node-y"), + sequence_start: 0, + sequence_end: 5, + }; + let anchor = GraphNodePosition { + graph_node: y_node, + offset: 0, + }; + + let result = graph_loader::find_offset(&mut graph, &anchor, 3, |g, nid| { + expand(&conn, g, &bg_id, nid) + }); + assert!( + result.is_ok(), + "graph_loader::find_offset(3) should succeed: {:?}", + result.err() + ); + let positions = result.unwrap(); + assert_eq!(positions.len(), 1); + assert_eq!(positions[0].graph_node, y_node); + assert_eq!(positions[0].offset, 3); +} + +#[test] +fn test_find_offset_forward_expands_beyond_subset() { + let (conn, bg_id) = setup_subset_graph(); + let tree = subset_interval_tree(); + let mut graph = graph_from_interval_tree(&tree); + + let y_node = GraphNode { + node_id: HashId::convert_str("node-y"), + sequence_start: 0, + sequence_end: 5, + }; + let anchor = GraphNodePosition { + graph_node: y_node, + offset: 0, + }; + + let result = graph_loader::find_offset(&mut graph, &anchor, 7, |g, nid| { + expand(&conn, g, &bg_id, nid) + }); + assert!( + result.is_ok(), + "graph_loader::find_offset(7) should expand to Z: {:?}", + result.err() + ); + let positions = result.unwrap(); + assert_eq!(positions.len(), 1); + assert_eq!( + positions[0].graph_node.node_id, + HashId::convert_str("node-z") + ); + assert_eq!(positions[0].offset, 2); +} + +#[test] +fn test_find_offset_forward_expands_after_existing_paths_fail() { + let (conn, bg_id) = setup_subset_graph(); + let mut graph = GenGraph::new(); + + let y_node = GraphNode { + node_id: HashId::convert_str("node-y"), + sequence_start: 0, + sequence_end: 5, + }; + let dead_end = GraphNode { + node_id: HashId::convert_str("dead-end"), + sequence_start: 0, + sequence_end: 1, + }; + graph.add_edge( + y_node, + dead_end, + vec![GraphEdge { + edge_id: HashId::convert_str("edge-y-dead"), + source_strand: Strand::Forward, + target_strand: Strand::Forward, + chromosome_index: 0, + phased: 0, + created_on: 0, + }], + ); + let anchor = GraphNodePosition { + graph_node: y_node, + offset: 0, + }; + + let result = graph_loader::find_offset(&mut graph, &anchor, 7, |g, nid| { + expand(&conn, g, &bg_id, nid) + }); + assert!( + result.is_ok(), + "graph_loader::find_offset(7) should expand from Y after the existing path fails: {:?}", + result.err() + ); + let positions = result.unwrap(); + assert_eq!(positions.len(), 1); + assert_eq!( + positions[0].graph_node.node_id, + HashId::convert_str("node-z") + ); + assert_eq!(positions[0].offset, 2); +} + +#[test] +fn test_find_offset_backward_expands_beyond_subset() { + let (conn, bg_id) = setup_subset_graph(); + let tree = subset_interval_tree(); + let mut graph = graph_from_interval_tree(&tree); + + let y_node = GraphNode { + node_id: HashId::convert_str("node-y"), + sequence_start: 0, + sequence_end: 5, + }; + let anchor = GraphNodePosition { + graph_node: y_node, + offset: 0, + }; + + let result = graph_loader::find_offset(&mut graph, &anchor, -3, |g, nid| { + expand(&conn, g, &bg_id, nid) + }); + assert!( + result.is_ok(), + "graph_loader::find_offset(-3) should expand to X: {:?}", + result.err() + ); + let positions = result.unwrap(); + assert_eq!(positions.len(), 1); + assert_eq!( + positions[0].graph_node.node_id, + HashId::convert_str("node-x") + ); + assert_eq!(positions[0].offset, 2); +} + +#[test] +fn test_find_offset_out_of_bounds_with_expansion() { + let (conn, bg_id) = setup_subset_graph(); + let tree = subset_interval_tree(); + let mut graph = graph_from_interval_tree(&tree); + + let y_node = GraphNode { + node_id: HashId::convert_str("node-y"), + sequence_start: 0, + sequence_end: 5, + }; + let anchor = GraphNodePosition { + graph_node: y_node, + offset: 0, + }; + + let result = graph_loader::find_offset(&mut graph, &anchor, 100, |g, nid| { + expand(&conn, g, &bg_id, nid) + }); + assert!(result.is_err()); +} + +#[test] +fn test_find_offset_with_fragment_node() { + let (conn, bg_id) = setup_subset_graph(); + + let tree: IntervalTree = vec![( + 0..3, + NodeIntervalBlock { + node_id: HashId::convert_str("node-y"), + start: 0, + end: 3, + sequence_start: 0, + sequence_end: 3, + strand: Strand::Forward, + }, + )] + .into_iter() + .collect(); + let mut graph = graph_from_interval_tree(&tree); + + let y_frag = GraphNode { + node_id: HashId::convert_str("node-y"), + sequence_start: 0, + sequence_end: 3, + }; + let anchor = GraphNodePosition { + graph_node: y_frag, + offset: 0, + }; + + let result = graph_loader::find_offset(&mut graph, &anchor, 2, |g, nid| { + expand(&conn, g, &bg_id, nid) + }); + assert!(result.is_ok()); + let positions = result.unwrap(); + assert_eq!(positions[0].graph_node, y_frag); + assert_eq!(positions[0].offset, 2); +} + +#[test] +fn resolve_anchor_before_fragment_stays_on_same_backing_node() { + // The case is when we have a node fragment + // at the beginning of an accession. We don't know how far back to expand + // the fragment as there are no edges. + let (conn, bg_id) = setup_subset_graph(); + + let tree: IntervalTree = vec![( + 0..3, + NodeIntervalBlock { + node_id: HashId::convert_str("node-y"), + start: 0, + end: 3, + sequence_start: 2, + sequence_end: 5, + strand: Strand::Forward, + }, + )] + .into_iter() + .collect(); + let graph = graph_from_interval_tree(&tree); + let position = graph_loader::resolve_anchor( + &graph, + &tree, + -2, + |node_id| { + Node::query_nodes_length(&conn, &[node_id]) + .ok() + .and_then(|lengths| lengths.get(&node_id).copied()) + }, + |graph, node_id| expand(&conn, graph, &bg_id, node_id), + ) + .unwrap(); + + assert_eq!( + position.graph_node, + GraphNode { + node_id: HashId::convert_str("node-y"), + sequence_start: 2, + sequence_end: 5, + } + ); + assert_eq!(position.offset, -2); + assert_eq!(position.coordinate(), 0); +} + +#[test] +fn test_expand_adds_neighbors() { + let (conn, bg_id) = setup_subset_graph(); + let tree = subset_interval_tree(); + let mut graph = graph_from_interval_tree(&tree); + + assert_eq!(graph.node_count(), 1); + + let expanded = expand(&conn, &mut graph, &bg_id, HashId::convert_str("node-y")); + assert!(expanded, "expand should add new nodes"); + assert!( + graph.node_count() > 1, + "graph should have more nodes after expansion" + ); + + let node_ids: Vec = graph.nodes().map(|n| n.node_id).collect(); + assert!( + node_ids.contains(&HashId::convert_str("node-x")), + "X should be added after expanding Y" + ); + assert!( + node_ids.contains(&HashId::convert_str("node-z")), + "Z should be added after expanding Y" + ); +} + +#[test] +fn test_find_offset_in_variable_length_branch_finds_middle_nodes() { + let mut graph = variable_length_branched_graph(); + let aaa_anchor = GraphNodePosition { + graph_node: GraphNode { + node_id: HashId::convert_str("node-aaa"), + sequence_start: 0, + sequence_end: 3, + }, + offset: 2, + }; + + let from_aaa = graph_loader::find_offset(&mut graph, &aaa_anchor, 2, |_, _| false).unwrap(); + assert_eq!( + position_set(&from_aaa), + HashSet::from([ + (HashId::convert_str("node-cc"), 1), + (HashId::convert_str("node-gggg"), 1) + ]) + ); + + let ttt_anchor = GraphNodePosition { + graph_node: GraphNode { + node_id: HashId::convert_str("node-ttt"), + sequence_start: 0, + sequence_end: 3, + }, + offset: 0, + }; + let from_ttt = graph_loader::find_offset(&mut graph, &ttt_anchor, -2, |_, _| false).unwrap(); + assert_eq!( + position_set(&from_ttt), + HashSet::from([ + (HashId::convert_str("node-cc"), 0), + (HashId::convert_str("node-gggg"), 2) + ]) + ); +} + +#[test] +fn test_find_offset_in_variable_length_branch_returns_single_position_within_node() { + let mut graph = variable_length_branched_graph(); + let anchor = GraphNodePosition { + graph_node: GraphNode { + node_id: HashId::convert_str("node-aaa"), + sequence_start: 0, + sequence_end: 3, + }, + offset: 1, + }; + + let positions = graph_loader::find_offset(&mut graph, &anchor, 1, |_, _| false).unwrap(); + assert_eq!( + position_set(&positions), + HashSet::from([(HashId::convert_str("node-aaa"), 2)]) + ); +} + +#[test] +fn test_find_offset_in_variable_length_branch_finds_different_ttt_offsets() { + let mut graph = variable_length_branched_graph(); + let anchor = GraphNodePosition { + graph_node: GraphNode { + node_id: HashId::convert_str("node-aaa"), + sequence_start: 0, + sequence_end: 3, + }, + offset: 2, + }; + + let positions = graph_loader::find_offset(&mut graph, &anchor, 6, |_, _| false).unwrap(); + assert_eq!( + position_set(&positions), + HashSet::from([ + (HashId::convert_str("node-ttt"), 1), + (HashId::convert_str("node-ttt"), 3) + ]) + ); +} diff --git a/gen-models/graph-tests/tests/migrated_model_graph.rs b/gen-models/graph-tests/tests/migrated_model_graph.rs new file mode 100644 index 00000000..b656b265 --- /dev/null +++ b/gen-models/graph-tests/tests/migrated_model_graph.rs @@ -0,0 +1,313 @@ +use gen_core::{GraphNode, HashId, NodeIntervalBlock, PathBlock, Strand}; +use gen_models::{ + block_group::{BlockGroup, BlockGroupChange, BlockGroupError, NewBlockGroup}, + block_group_edge::{BlockGroupEdge, BlockGroupEdgeData}, + collection::Collection, + edge::Edge, + node::Node, + region::{ResolvedGenRegion, ResolvedRegionKind}, + sample::{NewSample, Sample}, + sequence::Sequence, +}; +use gen_models_graph_tests::{get_connection, setup_block_group}; + +#[test] +fn test_get_graph_branched_graph() { + let conn = get_connection(None).expect("should create an in-memory graph database"); + Collection::get_or_create(&conn, "test").expect("should create the collection"); + Sample::get_or_create( + &conn, + NewSample { + name: "test", + ..Default::default() + }, + ) + .expect("should create the sample"); + let block_group = BlockGroup::create( + &conn, + NewBlockGroup { + collection_name: "test", + sample_name: "test", + name: "branched", + ..Default::default() + }, + ) + .expect("should create the block group"); + + let node_ids = [ + ("AAA", "node-aaa"), + ("GGG", "node-ggg"), + ("TTT", "node-ttt"), + ("CCC", "node-ccc"), + ("ATC", "node-atc"), + ] + .map(|(bases, name)| { + let sequence = Sequence::new() + .sequence_type("DNA") + .sequence(bases) + .save(&conn) + .expect("should save the sequence"); + Node::create(&conn, &sequence.hash, &HashId::convert_str(name)) + .expect("should create the node") + }); + let [node_aaa, node_ggg, node_ttt, node_ccc, node_atc] = node_ids; + let edges = [ + (node_aaa, 3, node_ttt, 0), + (node_ggg, 3, node_ttt, 0), + (node_ttt, 3, node_ccc, 0), + (node_ttt, 3, node_atc, 0), + ] + .map(|(source, source_coordinate, target, target_coordinate)| { + Edge::create( + &conn, + source, + source_coordinate, + Strand::Forward, + target, + target_coordinate, + Strand::Forward, + ) + .expect("should create the edge") + }); + BlockGroupEdge::bulk_create( + &conn, + &edges + .iter() + .map(|edge| BlockGroupEdgeData { + block_group_id: block_group.id, + edge_id: edge.id, + chromosome_index: 0, + phased: 0, + }) + .collect::>(), + ); + + let graph = gen_graph::models::load_block_group_graph(&conn, &block_group.id, None) + .expect("should load the branched graph"); + assert_eq!(graph.nodes().len(), 7); + assert_eq!(graph.all_edges().count(), 4); + let expected_edges = [ + (node_aaa, 3, 3, node_ttt, 0, 3, edges[0].id), + (node_ggg, 3, 3, node_ttt, 0, 3, edges[1].id), + (node_ttt, 0, 3, node_ccc, 0, 0, edges[2].id), + (node_ttt, 0, 3, node_atc, 0, 0, edges[3].id), + ]; + for (source, source_start, source_end, target, target_start, target_end, edge_id) in + expected_edges + { + let source = GraphNode { + node_id: source, + sequence_start: source_start, + sequence_end: source_end, + }; + let target = GraphNode { + node_id: target, + sequence_start: target_start, + sequence_end: target_end, + }; + let weights = graph + .edge_weight(source, target) + .unwrap_or_else(|| panic!("should contain graph edge {source:?} -> {target:?}")); + assert_eq!(weights.len(), 1); + assert_eq!(weights[0].edge_id, edge_id); + } +} + +#[test] +fn test_error_on_out_of_bounds_change() { + let conn = get_connection(None).expect("should create an in-memory graph database"); + let (block_group_id, path) = setup_block_group(&conn); + let sequence = Sequence::new() + .sequence_type("DNA") + .sequence("") + .save(&conn) + .expect("should save the deletion sequence"); + let node_id = Node::create(&conn, &sequence.hash, &HashId::convert_str("1")) + .expect("should create the deletion node"); + let deletion = PathBlock { + node_id, + block_sequence: sequence + .get_sequence(None, None) + .expect("should load the deletion sequence"), + sequence_start: 0, + sequence_end: 0, + path_start: 350, + path_end: 400, + strand: Strand::Forward, + }; + for (start, end) in [(350, 400), (-300, 400)] { + let region = ResolvedGenRegion::from_path(&conn, block_group_id, &path, start, end) + .expect("should resolve the requested path region"); + let result = gen_graph::models::insert_change( + &conn, + &BlockGroupChange { + region, + path_accession: None, + block: deletion.clone(), + chromosome_index: 1, + phased: 0, + preserve_edge: true, + }, + ); + assert!(matches!(result, Err(BlockGroupError::ChangeOutOfBounds(_)))); + } +} + +fn values_at( + tree: &intervaltree::IntervalTree, + coordinate: i64, +) -> Vec { + let mut values = tree + .query_point(coordinate) + .map(|entry| entry.value) + .collect::>(); + values.sort(); + values +} + +#[test] +fn test_blockgroup_interval_tree() { + let conn = get_connection(None).expect("should create an in-memory graph database"); + let (block_group_id, _path) = setup_block_group(&conn); + Sample::get_or_create( + &conn, + NewSample { + name: "child", + ..Default::default() + }, + ) + .expect("should create the child sample"); + let child_id = BlockGroup::get_or_create_sample_block_groups( + &conn, + "test", + "child", + "chr1", + vec!["test".to_string()], + ) + .expect("should create the child block group")[0] + .id; + let sequence = Sequence::new() + .sequence_type("DNA") + .sequence("NNNN") + .save(&conn) + .expect("should save the inserted sequence"); + let node_id = Node::create(&conn, &sequence.hash, &HashId::convert_str("insert-node")) + .expect("should create the inserted node"); + let child = + BlockGroup::get_by_id(&conn, &child_id, None).expect("should load the child block group"); + gen_graph::models::insert_change( + &conn, + &BlockGroupChange { + region: ResolvedGenRegion { + block_group: child, + path: None, + accession: None, + annotation: None, + kind: ResolvedRegionKind::BlockGroup, + anchor_start: 0, + anchor_end: 0, + feature_length: 0, + start: 7, + end: 15, + start_anchors: None, + end_anchors: None, + remove_ambiguous_positions: true, + }, + path_accession: None, + block: PathBlock { + node_id, + block_sequence: sequence + .get_sequence(0, 4) + .expect("should load the inserted sequence"), + sequence_start: 0, + sequence_end: 4, + path_start: 7, + path_end: 15, + strand: Strand::Forward, + }, + chromosome_index: 1, + phased: 0, + preserve_edge: true, + }, + ) + .expect("should insert the block-group change"); + + let original = gen_graph::models::load_block_group_intervaltree(&conn, &block_group_id, false) + .expect("should load the original interval tree"); + let original_unambiguous = + gen_graph::models::load_block_group_intervaltree(&conn, &block_group_id, true) + .expect("should load the original unambiguous interval tree"); + assert_eq!(values_at(&original, 3), values_at(&original_unambiguous, 3)); + assert_eq!( + values_at(&original, 35), + values_at(&original_unambiguous, 35) + ); + + let child_tree = gen_graph::models::load_block_group_intervaltree(&conn, &child_id, false) + .expect("should load the child interval tree"); + let child_unambiguous = + gen_graph::models::load_block_group_intervaltree(&conn, &child_id, true) + .expect("should load the child unambiguous interval tree"); + assert_eq!(values_at(&child_tree, 3), values_at(&child_unambiguous, 3)); + assert_eq!(values_at(&child_tree, 30).len(), 2); + assert!(values_at(&child_unambiguous, 30).is_empty()); + assert_eq!(values_at(&child_unambiguous, 9).len(), 2); +} + +#[test] +fn test_blocks_from_edges_after_change() { + let conn = get_connection(None).expect("should create an in-memory graph database"); + let (block_group_id, path) = setup_block_group(&conn); + let edges = BlockGroupEdge::edges_for_block_group(&conn, &block_group_id, None); + assert_eq!( + Edge::blocks_from_edges(&conn, &block_group_id, &edges, None) + .expect("should derive the original blocks") + .len(), + 6 + ); + let sequence = Sequence::new() + .sequence_type("DNA") + .sequence("NNNN") + .save(&conn) + .expect("should save the inserted sequence"); + let node_id = Node::create(&conn, &sequence.hash, &HashId::convert_str("1")) + .expect("should create the inserted node"); + let region = ResolvedGenRegion::from_path(&conn, block_group_id, &path, 7, 15) + .expect("should resolve the changed region"); + gen_graph::models::insert_change( + &conn, + &BlockGroupChange { + region, + path_accession: None, + block: PathBlock { + node_id, + block_sequence: sequence + .get_sequence(0, 4) + .expect("should load the inserted sequence"), + sequence_start: 0, + sequence_end: 4, + path_start: 7, + path_end: 15, + strand: Strand::Forward, + }, + chromosome_index: 0, + phased: 0, + preserve_edge: true, + }, + ) + .expect("should insert the change"); + let mut edges = BlockGroupEdge::edges_for_block_group(&conn, &block_group_id, None); + assert_eq!( + Edge::blocks_from_edges(&conn, &block_group_id, &edges, None) + .expect("should derive the changed blocks") + .len(), + 9 + ); + edges.reverse(); + assert_eq!( + Edge::blocks_from_edges(&conn, &block_group_id, &edges, None) + .expect("should derive the changed blocks independent of edge ordering") + .len(), + 9 + ); +} diff --git a/gen-models/graph-tests/tests/region_graph_positions.rs b/gen-models/graph-tests/tests/region_graph_positions.rs new file mode 100644 index 00000000..eea430d2 --- /dev/null +++ b/gen-models/graph-tests/tests/region_graph_positions.rs @@ -0,0 +1,862 @@ +use std::collections::HashSet; + +use gen_core::{GraphNodePosition, HashId, PATH_END_NODE_ID, PATH_START_NODE_ID, Strand}; +use gen_models::{ + accession::Accession, + block_group::{BlockGroup, NewBlockGroup, PathCache}, + block_group_edge::{AugmentedEdge, BlockGroupEdge, BlockGroupEdgeData}, + collection::Collection, + db::GraphConnection, + edge::Edge, + node::Node, + path::Path, + region::{ResolvedGenRegion, ResolvedRegionKind}, + sample::{NewSample, Sample}, + sequence::Sequence, +}; +use gen_models_graph_tests::get_connection; + +trait FindGraphPositions { + fn find_graph_positions( + &self, + conn: &GraphConnection, + start_offset: i64, + end_offset: i64, + ) -> Result; +} + +impl FindGraphPositions for ResolvedGenRegion { + fn find_graph_positions( + &self, + conn: &GraphConnection, + start_offset: i64, + end_offset: i64, + ) -> Result { + gen_graph::models::find_region_graph_positions(self, conn, start_offset, end_offset) + } +} + +fn setup_graph() -> (GraphConnection, HashId) { + let conn = get_connection(None).unwrap(); + Collection::get_or_create(&conn, "test").unwrap(); + Sample::get_or_create( + &conn, + NewSample { + name: "test", + ..Default::default() + }, + ) + .unwrap(); + let block_group = BlockGroup::create( + &conn, + NewBlockGroup { + collection_name: "test", + sample_name: "test", + name: "chr1", + ..Default::default() + }, + ) + .unwrap(); + + let seq_x = Sequence::new() + .sequence_type("DNA") + .sequence("XXXXX") + .save(&conn) + .unwrap(); + let seq_y = Sequence::new() + .sequence_type("DNA") + .sequence("YYYYY") + .save(&conn) + .unwrap(); + let seq_z = Sequence::new() + .sequence_type("DNA") + .sequence("ZZZZZ") + .save(&conn) + .unwrap(); + + let node_x = Node::create(&conn, &seq_x.hash, &HashId::convert_str("node-x")).unwrap(); + let node_y = Node::create(&conn, &seq_y.hash, &HashId::convert_str("node-y")).unwrap(); + let node_z = Node::create(&conn, &seq_z.hash, &HashId::convert_str("node-z")).unwrap(); + + let e_start = Edge::create( + &conn, + PATH_START_NODE_ID, + -1, + Strand::Forward, + node_x, + 0, + Strand::Forward, + ) + .unwrap(); + let e_xy = Edge::create( + &conn, + node_x, + 5, + Strand::Forward, + node_y, + 0, + Strand::Forward, + ) + .unwrap(); + let e_yz = Edge::create( + &conn, + node_y, + 5, + Strand::Forward, + node_z, + 0, + Strand::Forward, + ) + .unwrap(); + let e_end = Edge::create( + &conn, + node_z, + 5, + Strand::Forward, + PATH_END_NODE_ID, + 0, + Strand::Forward, + ) + .unwrap(); + + BlockGroupEdge::bulk_create( + &conn, + &[ + BlockGroupEdgeData { + block_group_id: block_group.id, + edge_id: e_start.id, + chromosome_index: 0, + phased: 0, + }, + BlockGroupEdgeData { + block_group_id: block_group.id, + edge_id: e_xy.id, + chromosome_index: 0, + phased: 0, + }, + BlockGroupEdgeData { + block_group_id: block_group.id, + edge_id: e_yz.id, + chromosome_index: 0, + phased: 0, + }, + BlockGroupEdgeData { + block_group_id: block_group.id, + edge_id: e_end.id, + chromosome_index: 0, + phased: 0, + }, + ], + ); + + (conn, block_group.id) +} + +fn create_accession( + conn: &GraphConnection, + block_group_id: HashId, + name: &str, + start: i64, + end: i64, +) -> Accession { + let edges = BlockGroupEdge::edges_for_block_group(conn, &block_group_id, None); + let mut by_source: std::collections::HashMap = + std::collections::HashMap::new(); + for ae in &edges { + by_source.insert(ae.edge.source_node_id, ae); + } + let mut ordered = vec![]; + let mut current = Some(PATH_START_NODE_ID); + while let Some(src) = current { + if let Some(ae) = by_source.get(&src) { + ordered.push(ae.edge.id); + current = if ae.edge.target_node_id == PATH_END_NODE_ID { + None + } else { + Some(ae.edge.target_node_id) + }; + } else { + break; + } + } + let path = Path::create(conn, name, &block_group_id, &ordered).unwrap(); + let mut path_cache = PathCache::new(conn); + let accession = + BlockGroup::add_accession(conn, &path, name, start, end, &mut path_cache).unwrap(); + Path::delete(conn, name, &block_group_id); + accession +} + +fn create_accession_from_edges( + conn: &GraphConnection, + block_group_id: HashId, + name: &str, + edge_ids: &[HashId], + start: i64, + end: i64, +) -> Accession { + let path = Path::create(conn, name, &block_group_id, edge_ids).unwrap(); + let mut path_cache = PathCache::new(conn); + let accession = + BlockGroup::add_accession(conn, &path, name, start, end, &mut path_cache).unwrap(); + Path::delete(conn, name, &block_group_id); + accession +} + +fn make_region( + bg: BlockGroup, + accession: Accession, + anchor_start: i64, + anchor_end: i64, + feature_length: i64, + start: i64, + end: i64, +) -> ResolvedGenRegion { + ResolvedGenRegion { + block_group: bg, + path: None, + accession: Some(accession), + annotation: None, + kind: ResolvedRegionKind::Accession, + anchor_start, + anchor_end, + feature_length, + start, + end, + start_anchors: None, + end_anchors: None, + remove_ambiguous_positions: false, + } +} + +/// Creates a branched graph: {AAA,GGG} → TTT → {CCC,ATC} +/// Path: AAA→TTT→CCC (positions 0..9) +fn setup_branched_graph() -> (GraphConnection, HashId) { + let conn = get_connection(None).unwrap(); + Collection::get_or_create(&conn, "test").unwrap(); + Sample::get_or_create( + &conn, + NewSample { + name: "test", + ..Default::default() + }, + ) + .unwrap(); + let bg = BlockGroup::create( + &conn, + NewBlockGroup { + collection_name: "test", + sample_name: "test", + name: "branched", + ..Default::default() + }, + ) + .unwrap(); + + let seq_aaa = Sequence::new() + .sequence_type("DNA") + .sequence("AAA") + .save(&conn) + .unwrap(); + let seq_ggg = Sequence::new() + .sequence_type("DNA") + .sequence("GGG") + .save(&conn) + .unwrap(); + let seq_ttt = Sequence::new() + .sequence_type("DNA") + .sequence("TTT") + .save(&conn) + .unwrap(); + let seq_ccc = Sequence::new() + .sequence_type("DNA") + .sequence("CCC") + .save(&conn) + .unwrap(); + let seq_atc = Sequence::new() + .sequence_type("DNA") + .sequence("ATC") + .save(&conn) + .unwrap(); + + let n_aaa = Node::create(&conn, &seq_aaa.hash, &HashId::convert_str("node-aaa")).unwrap(); + let n_ggg = Node::create(&conn, &seq_ggg.hash, &HashId::convert_str("node-ggg")).unwrap(); + let n_ttt = Node::create(&conn, &seq_ttt.hash, &HashId::convert_str("node-ttt")).unwrap(); + let n_ccc = Node::create(&conn, &seq_ccc.hash, &HashId::convert_str("node-ccc")).unwrap(); + let n_atc = Node::create(&conn, &seq_atc.hash, &HashId::convert_str("node-atc")).unwrap(); + + let e_start = Edge::create( + &conn, + PATH_START_NODE_ID, + -1, + Strand::Forward, + n_aaa, + 0, + Strand::Forward, + ) + .unwrap(); + let e_ggg_start = Edge::create( + &conn, + PATH_START_NODE_ID, + -1, + Strand::Forward, + n_ggg, + 0, + Strand::Forward, + ) + .unwrap(); + let e_aaa_ttt = + Edge::create(&conn, n_aaa, 3, Strand::Forward, n_ttt, 0, Strand::Forward).unwrap(); + let e_ttt_ccc = + Edge::create(&conn, n_ttt, 3, Strand::Forward, n_ccc, 0, Strand::Forward).unwrap(); + let e_ccc_end = Edge::create( + &conn, + n_ccc, + 3, + Strand::Forward, + PATH_END_NODE_ID, + 0, + Strand::Forward, + ) + .unwrap(); + let e_atc_end = Edge::create( + &conn, + n_atc, + 3, + Strand::Forward, + PATH_END_NODE_ID, + 0, + Strand::Forward, + ) + .unwrap(); + let e_ggg_ttt = + Edge::create(&conn, n_ggg, 3, Strand::Forward, n_ttt, 0, Strand::Forward).unwrap(); + let e_ttt_atc = + Edge::create(&conn, n_ttt, 3, Strand::Forward, n_atc, 0, Strand::Forward).unwrap(); + + BlockGroupEdge::bulk_create( + &conn, + &[ + BlockGroupEdgeData { + block_group_id: bg.id, + edge_id: e_start.id, + chromosome_index: 0, + phased: 0, + }, + BlockGroupEdgeData { + block_group_id: bg.id, + edge_id: e_ggg_start.id, + chromosome_index: 0, + phased: 0, + }, + BlockGroupEdgeData { + block_group_id: bg.id, + edge_id: e_aaa_ttt.id, + chromosome_index: 0, + phased: 0, + }, + BlockGroupEdgeData { + block_group_id: bg.id, + edge_id: e_ttt_ccc.id, + chromosome_index: 0, + phased: 0, + }, + BlockGroupEdgeData { + block_group_id: bg.id, + edge_id: e_ccc_end.id, + chromosome_index: 0, + phased: 0, + }, + BlockGroupEdgeData { + block_group_id: bg.id, + edge_id: e_atc_end.id, + chromosome_index: 0, + phased: 0, + }, + BlockGroupEdgeData { + block_group_id: bg.id, + edge_id: e_ggg_ttt.id, + chromosome_index: 0, + phased: 0, + }, + BlockGroupEdgeData { + block_group_id: bg.id, + edge_id: e_ttt_atc.id, + chromosome_index: 0, + phased: 0, + }, + ], + ); + + (conn, bg.id) +} + +struct GraphFixture { + conn: GraphConnection, + block_group_id: HashId, + path: Vec, +} + +/// Creates a branched graph: AAA -> {CC, GGGG} -> TTT. +fn setup_variable_length_branched_graph() -> GraphFixture { + let conn = get_connection(None).unwrap(); + Collection::get_or_create(&conn, "test").unwrap(); + Sample::get_or_create( + &conn, + NewSample { + name: "test", + ..Default::default() + }, + ) + .unwrap(); + let bg = BlockGroup::create( + &conn, + NewBlockGroup { + collection_name: "test", + sample_name: "test", + name: "variable-length-branched", + ..Default::default() + }, + ) + .unwrap(); + + let seq_aaa = Sequence::new() + .sequence_type("DNA") + .sequence("AAA") + .save(&conn) + .unwrap(); + let seq_cc = Sequence::new() + .sequence_type("DNA") + .sequence("CC") + .save(&conn) + .unwrap(); + let seq_gggg = Sequence::new() + .sequence_type("DNA") + .sequence("GGGG") + .save(&conn) + .unwrap(); + let seq_ttt = Sequence::new() + .sequence_type("DNA") + .sequence("TTT") + .save(&conn) + .unwrap(); + + let n_aaa = Node::create(&conn, &seq_aaa.hash, &HashId::convert_str("node-aaa")).unwrap(); + let n_cc = Node::create(&conn, &seq_cc.hash, &HashId::convert_str("node-cc")).unwrap(); + let n_gggg = Node::create(&conn, &seq_gggg.hash, &HashId::convert_str("node-gggg")).unwrap(); + let n_ttt = Node::create(&conn, &seq_ttt.hash, &HashId::convert_str("node-ttt")).unwrap(); + + let e_start = Edge::create( + &conn, + PATH_START_NODE_ID, + -1, + Strand::Forward, + n_aaa, + 0, + Strand::Forward, + ) + .unwrap(); + let e_aaa_cc = + Edge::create(&conn, n_aaa, 3, Strand::Forward, n_cc, 0, Strand::Forward).unwrap(); + let e_aaa_gggg = + Edge::create(&conn, n_aaa, 3, Strand::Forward, n_gggg, 0, Strand::Forward).unwrap(); + let e_cc_ttt = + Edge::create(&conn, n_cc, 2, Strand::Forward, n_ttt, 0, Strand::Forward).unwrap(); + let e_gggg_ttt = + Edge::create(&conn, n_gggg, 4, Strand::Forward, n_ttt, 0, Strand::Forward).unwrap(); + let e_end = Edge::create( + &conn, + n_ttt, + 3, + Strand::Forward, + PATH_END_NODE_ID, + 0, + Strand::Forward, + ) + .unwrap(); + + BlockGroupEdge::bulk_create( + &conn, + &[ + BlockGroupEdgeData { + block_group_id: bg.id, + edge_id: e_start.id, + chromosome_index: 0, + phased: 0, + }, + BlockGroupEdgeData { + block_group_id: bg.id, + edge_id: e_aaa_cc.id, + chromosome_index: 0, + phased: 0, + }, + BlockGroupEdgeData { + block_group_id: bg.id, + edge_id: e_aaa_gggg.id, + chromosome_index: 0, + phased: 0, + }, + BlockGroupEdgeData { + block_group_id: bg.id, + edge_id: e_cc_ttt.id, + chromosome_index: 0, + phased: 0, + }, + BlockGroupEdgeData { + block_group_id: bg.id, + edge_id: e_gggg_ttt.id, + chromosome_index: 0, + phased: 0, + }, + BlockGroupEdgeData { + block_group_id: bg.id, + edge_id: e_end.id, + chromosome_index: 0, + phased: 0, + }, + ], + ); + + GraphFixture { + conn, + block_group_id: bg.id, + path: vec![e_start.id, e_aaa_cc.id, e_cc_ttt.id, e_end.id], + } +} + +fn position_set(positions: &[GraphNodePosition]) -> HashSet<(HashId, i64)> { + positions + .iter() + .map(|pos| (pos.graph_node.node_id, pos.offset)) + .collect() +} + +#[test] +fn test_finds_graph_positions_within_node() { + let (conn, bg_id) = setup_graph(); + let bg = BlockGroup::get_by_id(&conn, &bg_id, None).unwrap(); + let acc = create_accession(&conn, bg_id, "within", 0, 15); + let region = make_region(bg, acc, 0, 15, 15, 7, 7); + + let resolved = region.find_graph_positions(&conn, 2, 2).unwrap(); + let start_pos = resolved.start_anchors.as_ref().unwrap(); + let end_pos = resolved.end_anchors.as_ref().unwrap(); + assert_eq!(start_pos.len(), 1); + assert_eq!( + start_pos[0].graph_node.node_id, + HashId::convert_str("node-y") + ); + assert_eq!(start_pos[0].offset, 4); + assert_eq!(end_pos.len(), 1); + assert_eq!(end_pos[0].graph_node.node_id, HashId::convert_str("node-y")); + assert_eq!(end_pos[0].offset, 4); +} + +#[test] +fn test_finds_graph_positions_forward_across_nodes() { + let (conn, bg_id) = setup_graph(); + let bg = BlockGroup::get_by_id(&conn, &bg_id, None).unwrap(); + let acc = create_accession(&conn, bg_id, "fwd", 0, 15); + let region = make_region(bg, acc, 0, 15, 15, 7, 7); + + let resolved = region.find_graph_positions(&conn, 5, 5).unwrap(); + let start_pos = resolved.start_anchors.as_ref().unwrap(); + let end_pos = resolved.end_anchors.as_ref().unwrap(); + assert_eq!(start_pos.len(), 1); + assert_eq!( + start_pos[0].graph_node.node_id, + HashId::convert_str("node-z") + ); + assert_eq!(start_pos[0].offset, 2); + assert_eq!(end_pos.len(), 1); + assert_eq!(end_pos[0].graph_node.node_id, HashId::convert_str("node-z")); + assert_eq!(end_pos[0].offset, 2); +} + +#[test] +fn test_finds_graph_positions_backwards_across_nodes() { + let (conn, bg_id) = setup_graph(); + let bg = BlockGroup::get_by_id(&conn, &bg_id, None).unwrap(); + let acc = create_accession(&conn, bg_id, "bwd", 0, 15); + let region = make_region(bg, acc, 0, 15, 15, 7, 7); + + let resolved = region.find_graph_positions(&conn, -5, -5).unwrap(); + let start_pos = resolved.start_anchors.as_ref().unwrap(); + let end_pos = resolved.end_anchors.as_ref().unwrap(); + assert_eq!(start_pos.len(), 1); + assert_eq!( + start_pos[0].graph_node.node_id, + HashId::convert_str("node-x") + ); + assert_eq!(start_pos[0].offset, 2); + assert_eq!(end_pos.len(), 1); + assert_eq!(end_pos[0].graph_node.node_id, HashId::convert_str("node-x")); + assert_eq!(end_pos[0].offset, 2); +} + +#[test] +fn test_reports_out_of_bounds() { + let (conn, bg_id) = setup_graph(); + let bg = BlockGroup::get_by_id(&conn, &bg_id, None).unwrap(); + let acc = create_accession(&conn, bg_id, "oob", 0, 15); + let region = make_region(bg, acc, 0, 15, 15, 7, 7); + + assert!(region.find_graph_positions(&conn, 100, 100).is_err()); +} + +#[test] +fn test_finds_graph_positions_from_start() { + let (conn, bg_id) = setup_graph(); + let bg = BlockGroup::get_by_id(&conn, &bg_id, None).unwrap(); + let acc = create_accession(&conn, bg_id, "start", 0, 15); + let region = make_region(bg, acc, 0, 15, 15, 0, 0); + + let resolved = region.find_graph_positions(&conn, 12, 12).unwrap(); + let start_pos = resolved.start_anchors.as_ref().unwrap(); + let end_pos = resolved.end_anchors.as_ref().unwrap(); + assert_eq!(start_pos.len(), 1); + assert_eq!( + start_pos[0].graph_node.node_id, + HashId::convert_str("node-z") + ); + assert_eq!(start_pos[0].offset, 2); + assert_eq!(end_pos.len(), 1); + assert_eq!(end_pos[0].graph_node.node_id, HashId::convert_str("node-z")); + assert_eq!(end_pos[0].offset, 2); +} + +#[test] +fn test_finds_graph_positions_from_end() { + let (conn, bg_id) = setup_graph(); + let bg = BlockGroup::get_by_id(&conn, &bg_id, None).unwrap(); + let acc = create_accession(&conn, bg_id, "end", 0, 15); + let region = make_region(bg, acc, 0, 15, 15, 14, 14); + + let resolved = region.find_graph_positions(&conn, -14, -14).unwrap(); + let start_pos = resolved.start_anchors.as_ref().unwrap(); + let end_pos = resolved.end_anchors.as_ref().unwrap(); + assert_eq!(start_pos.len(), 1); + assert_eq!( + start_pos[0].graph_node.node_id, + HashId::convert_str("node-x") + ); + assert_eq!(start_pos[0].offset, 0); + assert_eq!(end_pos.len(), 1); + assert_eq!(end_pos[0].graph_node.node_id, HashId::convert_str("node-x")); + assert_eq!(end_pos[0].offset, 0); +} + +#[test] +fn test_finds_graph_positions_within_accessions() { + let (conn, bg_id) = setup_graph(); + let bg = BlockGroup::get_by_id(&conn, &bg_id, None).unwrap(); + let acc = create_accession(&conn, bg_id, "acc-within", 5, 10); + let region = make_region(bg, acc, 5, 10, 5, 2, 2); + + let resolved = region.find_graph_positions(&conn, 1, 1).unwrap(); + let start_pos = resolved.start_anchors.as_ref().unwrap(); + let end_pos = resolved.end_anchors.as_ref().unwrap(); + assert_eq!(start_pos.len(), 1); + assert_eq!( + start_pos[0].graph_node.node_id, + HashId::convert_str("node-y") + ); + assert_eq!(start_pos[0].offset, 3); + assert_eq!(end_pos.len(), 1); + assert_eq!(end_pos[0].graph_node.node_id, HashId::convert_str("node-y")); + assert_eq!(end_pos[0].offset, 3); +} + +#[test] +fn test_finds_graph_positions_expands_accession_forward() { + let (conn, bg_id) = setup_graph(); + let bg = BlockGroup::get_by_id(&conn, &bg_id, None).unwrap(); + let acc = create_accession(&conn, bg_id, "acc-fwd", 5, 10); + let region = make_region(bg, acc, 5, 10, 5, 3, 3); + + let resolved = region.find_graph_positions(&conn, 5, 5).unwrap(); + let start_pos = resolved.start_anchors.as_ref().unwrap(); + let end_pos = resolved.end_anchors.as_ref().unwrap(); + assert_eq!(start_pos.len(), 1); + assert_eq!( + start_pos[0].graph_node.node_id, + HashId::convert_str("node-z") + ); + assert_eq!(start_pos[0].offset, 3); + assert_eq!(end_pos.len(), 1); + assert_eq!(end_pos[0].graph_node.node_id, HashId::convert_str("node-z")); + assert_eq!(end_pos[0].offset, 3); +} + +#[test] +fn test_finds_graph_positions_expands_accession_backward() { + let (conn, bg_id) = setup_graph(); + let bg = BlockGroup::get_by_id(&conn, &bg_id, None).unwrap(); + let acc = create_accession(&conn, bg_id, "acc-bwd", 5, 10); + let region = make_region(bg, acc, 5, 10, 5, 1, 1); + + let resolved = region.find_graph_positions(&conn, -4, -4).unwrap(); + let start_pos = resolved.start_anchors.as_ref().unwrap(); + let end_pos = resolved.end_anchors.as_ref().unwrap(); + assert_eq!(start_pos.len(), 1); + assert_eq!( + start_pos[0].graph_node.node_id, + HashId::convert_str("node-x") + ); + assert_eq!(start_pos[0].offset, 2); + assert_eq!(end_pos.len(), 1); + assert_eq!(end_pos[0].graph_node.node_id, HashId::convert_str("node-x")); + assert_eq!(end_pos[0].offset, 2); +} + +#[test] +fn test_finds_graph_positions_reports_accession_out_of_bounds() { + let (conn, bg_id) = setup_graph(); + let bg = BlockGroup::get_by_id(&conn, &bg_id, None).unwrap(); + let acc = create_accession(&conn, bg_id, "acc-oob", 5, 10); + let region = make_region(bg, acc, 5, 10, 5, 2, 2); + + assert!(region.find_graph_positions(&conn, 100, 100).is_err()); +} + +#[test] +fn test_finds_graph_positions_in_branched_graph_backwards_returns_all_positions() { + let (conn, bg_id) = setup_branched_graph(); + let bg = BlockGroup::get_by_id(&conn, &bg_id, None).unwrap(); + // Accession on TTT: path positions 3..6, accession-relative 0..3 + let acc = create_accession(&conn, bg_id, "branched-bwd", 3, 6); + let region = make_region(bg, acc, 3, 6, 3, 0, 0); + + // Backward 3 from TTT offset 0 → should find AAA and GGG at offset 0 + let resolved = region.find_graph_positions(&conn, -3, -3).unwrap(); + let start_pos = resolved.start_anchors.as_ref().unwrap(); + let end_pos = resolved.end_anchors.as_ref().unwrap(); + assert_eq!(start_pos.len(), 2); + let start_ids: Vec = start_pos.iter().map(|p| p.graph_node.node_id).collect(); + assert!(start_ids.contains(&HashId::convert_str("node-aaa"))); + assert!(start_ids.contains(&HashId::convert_str("node-ggg"))); + for pos in start_pos { + assert_eq!(pos.offset, 0); + } + assert_eq!(end_pos.len(), 2); + let end_ids: Vec = end_pos.iter().map(|p| p.graph_node.node_id).collect(); + assert!(end_ids.contains(&HashId::convert_str("node-aaa"))); + assert!(end_ids.contains(&HashId::convert_str("node-ggg"))); +} + +#[test] +fn test_finds_graph_positions_in_branched_graph_forwardsgr_returns_all_positions() { + let (conn, bg_id) = setup_branched_graph(); + let bg = BlockGroup::get_by_id(&conn, &bg_id, None).unwrap(); + // Accession on TTT: path positions 3..6, accession-relative 0..3 + let acc = create_accession(&conn, bg_id, "branched-fwd", 3, 6); + let region = make_region(bg, acc, 3, 6, 3, 2, 2); + + // Forward 3 from TTT offset 2 → should find CCC and ATC at offset 2 + let resolved = region.find_graph_positions(&conn, 3, 3).unwrap(); + let start_pos = resolved.start_anchors.as_ref().unwrap(); + let end_pos = resolved.end_anchors.as_ref().unwrap(); + assert_eq!(start_pos.len(), 2); + let start_ids: Vec = start_pos.iter().map(|p| p.graph_node.node_id).collect(); + assert!(start_ids.contains(&HashId::convert_str("node-ccc"))); + assert!(start_ids.contains(&HashId::convert_str("node-atc"))); + for pos in start_pos { + assert_eq!(pos.offset, 2); + } + assert_eq!(end_pos.len(), 2); + let end_ids: Vec = end_pos.iter().map(|p| p.graph_node.node_id).collect(); + assert!(end_ids.contains(&HashId::convert_str("node-ccc"))); + assert!(end_ids.contains(&HashId::convert_str("node-atc"))); +} + +#[test] +fn test_finds_graph_positions_in_variable_length_branch_finds_middle_nodes() { + let fixture = setup_variable_length_branched_graph(); + let bg = BlockGroup::get_by_id(&fixture.conn, &fixture.block_group_id, None).unwrap(); + let aaa_acc = create_accession_from_edges( + &fixture.conn, + fixture.block_group_id, + "variable-aaa", + &fixture.path, + 0, + 3, + ); + let aaa_region = make_region(bg.clone(), aaa_acc, 0, 3, 3, 2, 2); + + let from_aaa = aaa_region + .find_graph_positions(&fixture.conn, 2, 2) + .unwrap(); + assert_eq!( + position_set(&from_aaa.start_anchors.unwrap()), + HashSet::from([ + (HashId::convert_str("node-cc"), 1), + (HashId::convert_str("node-gggg"), 1) + ]) + ); + + let ttt_acc = create_accession_from_edges( + &fixture.conn, + fixture.block_group_id, + "variable-ttt", + &fixture.path, + 5, + 8, + ); + let ttt_region = make_region(bg, ttt_acc, 5, 8, 3, 0, 0); + + let from_ttt = ttt_region + .find_graph_positions(&fixture.conn, -2, -2) + .unwrap(); + assert_eq!( + position_set(&from_ttt.start_anchors.unwrap()), + HashSet::from([ + (HashId::convert_str("node-cc"), 0), + (HashId::convert_str("node-gggg"), 2) + ]) + ); +} + +#[test] +fn test_finds_graph_positions_in_variable_length_branch_returns_single_position() { + let fixture = setup_variable_length_branched_graph(); + let bg = BlockGroup::get_by_id(&fixture.conn, &fixture.block_group_id, None).unwrap(); + let acc = create_accession_from_edges( + &fixture.conn, + fixture.block_group_id, + "variable-single", + &fixture.path, + 0, + 3, + ); + let region = make_region(bg, acc, 0, 3, 3, 1, 1); + + let positions = region.find_graph_positions(&fixture.conn, 1, 1).unwrap(); + assert_eq!( + position_set(&positions.start_anchors.unwrap()), + HashSet::from([(HashId::convert_str("node-aaa"), 2)]) + ); +} + +#[test] +fn test_finds_graph_positions_in_variable_length_branch_finds_different_ttt_offsets() { + let fixture = setup_variable_length_branched_graph(); + let bg = BlockGroup::get_by_id(&fixture.conn, &fixture.block_group_id, None).unwrap(); + let acc = create_accession_from_edges( + &fixture.conn, + fixture.block_group_id, + "variable-ttt-offsets", + &fixture.path, + 0, + 3, + ); + let region = make_region(bg, acc, 0, 3, 3, 2, 2); + + let positions = region.find_graph_positions(&fixture.conn, 6, 6).unwrap(); + assert_eq!( + position_set(&positions.start_anchors.unwrap()), + HashSet::from([ + (HashId::convert_str("node-ttt"), 1), + (HashId::convert_str("node-ttt"), 3) + ]) + ); +} diff --git a/gen-models/src/annotations.rs b/gen-models/src/annotations.rs index db05a564..95b1f97d 100644 --- a/gen-models/src/annotations.rs +++ b/gen-models/src/annotations.rs @@ -24,6 +24,7 @@ use crate::{ FileAddition, OperationAssetRecord, OperationFile, OperationInfo, OperationSummary, track_operation_assets, }, + region::ResolvedGenRegion, traits::Query, }; #[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] @@ -705,19 +706,15 @@ pub fn annotation_index_file_path( None } -pub fn add_annotation( +pub fn persist_annotation( context: &DbContext, - collection: &str, name: &str, group: Option<&str>, sample: &str, - region: &str, + resolved_region: &ResolvedGenRegion, + spans: Vec, ) -> Result> { let graph_conn = context.graph().conn(); - let parsed_region = Region::parse(region)?; - let resolved_region = crate::region::resolve(&parsed_region, graph_conn, collection, sample)?; - let spans = AccessionSpan::from_resolved_region(graph_conn, &resolved_region, None)?; - let accession = Accession::get_or_create( graph_conn, &NewAccession { @@ -860,7 +857,7 @@ mod tests { block_group::{BlockGroup, PathCache}, block_group_edge::{BlockGroupEdge, BlockGroupEdgeData}, errors::OperationError, - operations::{calculate_reader_checksum, commit_operation_summary}, + operations::calculate_reader_checksum, path::Path, path_edge::PathEdge, sample::Sample, @@ -1378,66 +1375,6 @@ mod tests { assert!(matches!(err, AnnotationFileError::UnsupportedFileType(_))); } - #[test] - fn add_annotation_creates_annotation() { - let context = setup_gen(); - let graph_conn = context.graph().conn(); - let history_store = DoltHistoryStore::new(graph_conn); - let _ = setup_block_group(graph_conn); - - let operation_summary = add_annotation( - &context, - "test", - "gene-a", - Some("track-1"), - "test", - "chr1:1-5", - ) - .unwrap(); - let commit_hash = commit_operation_summary(&context, &operation_summary).unwrap(); - assert_eq!(history_store.current_head().unwrap(), Some(commit_hash)); - let mut operation_logs = OperationLog::all(graph_conn); - operation_logs.sort_by_key(|operation_log| std::cmp::Reverse(operation_log.created_on)); - assert_eq!( - operation_logs[0].operation_kind, - OperationKind::Other("add annotation gene-a".to_string()) - ); - - let annotations = Annotation::query_by_group(graph_conn, "track-1", None).unwrap(); - assert_eq!(annotations.len(), 1); - assert_eq!(annotations[0].name, "gene-a"); - } - - #[test] - fn test_add_annotation_detects_no_changes() { - let context = setup_gen(); - let graph_conn = context.graph().conn(); - let _ = setup_block_group(graph_conn); - - let operation_summary = add_annotation( - &context, - "test", - "gene-a", - Some("track-1"), - "test", - "chr1:1-5", - ) - .unwrap(); - commit_operation_summary(&context, &operation_summary).unwrap(); - - let operation_summary = add_annotation( - &context, - "test", - "gene-a", - Some("track-1"), - "test", - "chr1:1-5", - ) - .unwrap(); - let err = commit_operation_summary(&context, &operation_summary).unwrap_err(); - assert_eq!(err, OperationError::NoChanges); - } - #[test] fn add_annotation_file_creates_operation() { let context = setup_gen(); diff --git a/gen-models/src/block_group.rs b/gen-models/src/block_group.rs index 405b4dcd..92ecfaf4 100644 --- a/gen-models/src/block_group.rs +++ b/gen-models/src/block_group.rs @@ -5,17 +5,13 @@ use std::{ }; use gen_core::{ - HashId, INDETERMINATE_CHROMOSOME_INDEX, NO_CHROMOSOME_INDEX, NodeIntervalBlock, - PATH_END_NODE_ID, PATH_START_NODE_ID, PRESERVE_EDIT_SITE_CHROMOSOME_INDEX, PathBlock, Strand, - calculate_hash, is_end_node, is_start_node, is_terminal, + HashId, NodeIntervalBlock, PATH_END_NODE_ID, PATH_START_NODE_ID, + PRESERVE_EDIT_SITE_CHROMOSOME_INDEX, PathBlock, Strand, calculate_hash, is_end_node, + is_start_node, is_terminal, range::Range, region::{Region, RegionResolutionError, RegionResolver}, traits::Capnp, }; -use gen_graph::{ - GenGraph, GraphNode, all_intermediate_edges, all_reachable_nodes, all_simple_paths, - flatten_to_interval_tree, -}; use indexmap::IndexSet; use intervaltree::IntervalTree; use rusqlite::{Row, params, types::Value as SQLValue}; @@ -25,9 +21,9 @@ use thiserror::Error; use crate::{ accession::{Accession, AccessionSpan, NewAccession}, annotations::AnnotationError, - block_group_edge::{AugmentedEdge, AugmentedEdgeData, BlockGroupEdge, BlockGroupEdgeData}, + block_group_edge::{AugmentedEdgeData, BlockGroupEdge, BlockGroupEdgeData}, db::GraphConnection, - edge::{Edge, EdgeData, GroupBlock}, + edge::{Edge, EdgeData}, errors::{ AccessionError, AccessionNodeError, EdgeError, NodeError, PathError, QueryError, SequenceError, @@ -161,6 +157,13 @@ pub struct NewBlockGroup<'a> { pub is_default: bool, } +#[derive(Clone, Copy, Debug)] +pub struct SubgraphBoundary { + pub block: NodeIntervalBlock, + pub node_coordinate: i64, +} + +/// A requested sequence-graph change paired with its resolved model region. #[derive(Clone, Debug)] pub struct BlockGroupChange { pub region: ResolvedGenRegion, @@ -551,148 +554,6 @@ impl BlockGroup { ))) } - pub fn get_graph( - conn: &GraphConnection, - block_group_id: &HashId, - history_ref: Option<&str>, - ) -> Result { - let edges = BlockGroupEdge::edges_for_block_group(conn, block_group_id, history_ref); - let blocks = Edge::blocks_from_edges(conn, block_group_id, &edges, history_ref)?; - let (graph, _) = Edge::build_graph(&edges, &blocks); - Ok(graph) - } - - /// Build a graph from a set of known edges. If a source or target node is unknown, the graph - /// will expand out automatically to cover it. - pub fn get_graph_from_edges( - conn: &GraphConnection, - block_group_id: &HashId, - edges: &[AugmentedEdge], - ) -> Result { - let blocks = Edge::blocks_from_edges(conn, block_group_id, edges, None)?; - let edges_vec = edges.to_vec(); - let (graph, _) = Edge::build_graph(&edges_vec, &blocks); - Ok(graph) - } - - pub fn prune_graph(graph: &mut GenGraph) { - // Prunes a graph by removing edges on the same chromosome_index. This means if 2 edges are - // both "chromosome index 0", we keep the newer one. - let mut root_nodes = HashSet::new(); - let mut edges_to_remove: Vec<(GraphNode, GraphNode)> = vec![]; - for node in graph.nodes() { - if node.node_id == PATH_START_NODE_ID { - root_nodes.insert(node); - } - let mut edges_by_ci: HashMap = HashMap::new(); - for (source_node, target_node, edge_weights) in graph.edges(node) { - for edge_weight in edge_weights { - if edge_weight.chromosome_index == NO_CHROMOSOME_INDEX { - continue; - } - if edge_weight.chromosome_index == INDETERMINATE_CHROMOSOME_INDEX { - continue; - } - if edge_weight.chromosome_index == PRESERVE_EDIT_SITE_CHROMOSOME_INDEX { - edges_to_remove.push((source_node, target_node)); - continue; - } - edges_by_ci - .entry(edge_weight.chromosome_index) - .and_modify(|(source, target, created_on)| { - if edge_weight.created_on > *created_on { - edges_to_remove.push((*source, *target)); - *source = source_node; - *target = target_node; - *created_on = edge_weight.created_on; - } else { - edges_to_remove.push((source_node, target_node)); - } - }) - .or_insert((source_node, target_node, edge_weight.created_on)); - } - } - } - - for (source, target) in edges_to_remove.iter() { - graph.remove_edge(*source, *target); - } - - let reachable_nodes = all_reachable_nodes(&*graph, &Vec::from_iter(root_nodes)); - let mut to_remove = vec![]; - for node in graph.nodes() { - if !reachable_nodes.contains(&node) { - to_remove.push(node); - } - } - for node in to_remove { - graph.remove_node(node); - } - } - - pub fn get_all_sequences( - conn: &GraphConnection, - block_group_id: &HashId, - _prune: bool, - ) -> Result, BlockGroupError> { - let edges = BlockGroupEdge::edges_for_block_group(conn, block_group_id, None) - .into_iter() - .filter(|edge| edge.chromosome_index != PRESERVE_EDIT_SITE_CHROMOSOME_INDEX) - .collect::>(); - let blocks = Edge::blocks_from_edges(conn, block_group_id, &edges, None)?; - - let (mut graph, _) = Edge::build_graph(&edges, &blocks); - BlockGroup::prune_graph(&mut graph); - - let mut start_nodes = vec![]; - let mut end_nodes = vec![]; - for node in graph.nodes() { - if is_start_node(node.node_id) { - start_nodes.push(node); - } else if is_end_node(node.node_id) { - end_nodes.push(node); - } - } - let blocks_by_node = blocks - .iter() - .map(|block| { - ( - GraphNode { - node_id: block.node_id, - sequence_start: block.start, - sequence_end: block.end, - }, - block, - ) - }) - .collect::>(); - let mut sequences = HashSet::::new(); - - for start_node in start_nodes { - for end_node in &end_nodes { - // TODO: maybe make all_simple_paths return a single path id where start == end - if start_node == *end_node { - let block = blocks_by_node.get(&start_node).unwrap(); - if block.node_id != PATH_START_NODE_ID && block.node_id != PATH_END_NODE_ID { - sequences.insert(block.sequence()); - } - } else { - for path in all_simple_paths(&graph, start_node, *end_node) { - let mut current_sequence = "".to_string(); - for node in path { - let block = blocks_by_node.get(&node).unwrap(); - let block_sequence = block.sequence(); - current_sequence.push_str(&block_sequence); - } - sequences.insert(current_sequence); - } - } - } - } - - Ok(sequences) - } - pub fn add_accession( conn: &GraphConnection, path: &Path, @@ -737,84 +598,11 @@ impl BlockGroup { Ok(accession) } - #[cfg_attr( - feature = "profiling", - tracing::instrument(skip(conn, changes, tree_map)) - )] - pub fn insert_changes( - conn: &GraphConnection, - changes: &[BlockGroupChange], - tree_map: Option<&mut IntervalTreeCache>, - ) -> Result<(), BlockGroupError> { - let mut new_augmented_edges_by_block_group = - HashMap::>::new(); - let mut new_accession_edges = HashMap::<(HashId, String), Vec>::new(); - let mut local_tree_map = HashMap::new(); - let tree_map = match tree_map { - Some(tree_map) => tree_map, - None => &mut local_tree_map, - }; - for change in changes { - let cache_key = change.region.intervaltree_cache_key(); - #[expect( - clippy::map_entry, - reason = "entry API doesn't work with ? error propagation" - )] - if !tree_map.contains_key(&cache_key) { - tree_map.insert( - cache_key, - IntervalTreeSource::intervaltree(&change.region, conn)?, - ); - } - let tree = tree_map.get(&cache_key); - let new_augmented_edges = change.region.plan_edges(conn, change, tree)?; - new_augmented_edges_by_block_group - .entry(change.region.block_group.id) - .and_modify(|new_edge_data| new_edge_data.extend(new_augmented_edges.clone())) - .or_insert_with(|| new_augmented_edges.clone()); - if let Some(accession) = &change.path_accession { - new_accession_edges - .entry((change.region.block_group.id, accession.clone())) - .and_modify(|new_edge_data: &mut Vec| { - new_edge_data.extend(new_augmented_edges.clone()) - }) - .or_insert_with(|| new_augmented_edges.clone()); - } - } - Self::persist_insert_changes( - conn, - new_augmented_edges_by_block_group, - new_accession_edges, - ) - } - - pub fn insert_change( - conn: &GraphConnection, - change: &BlockGroupChange, - ) -> Result<(), BlockGroupError> { - let new_augmented_edges = change.region.plan_edges(conn, change, None)?; - let mut new_augmented_edges_by_block_group = HashMap::new(); - new_augmented_edges_by_block_group - .insert(change.region.block_group.id, new_augmented_edges.clone()); - let mut new_accession_edges = HashMap::new(); - if let Some(accession) = &change.path_accession { - new_accession_edges.insert( - (change.region.block_group.id, accession.clone()), - new_augmented_edges, - ); - } - Self::persist_insert_changes( - conn, - new_augmented_edges_by_block_group, - new_accession_edges, - ) - } - #[cfg_attr( feature = "profiling", tracing::instrument(skip(conn, new_augmented_edges_by_block_group, new_accession_edges)) )] - fn persist_insert_changes( + pub fn persist_insert_changes( conn: &GraphConnection, new_augmented_edges_by_block_group: HashMap>, new_accession_edges: HashMap<(HashId, String), Vec>, @@ -1121,17 +909,6 @@ impl BlockGroup { Ok(new_edges) } - pub fn intervaltree_for( - conn: &GraphConnection, - block_group_id: &HashId, - remove_ambiguous_positions: bool, - ) -> Result, BlockGroupError> { - // make a tree where every node has a span in the graph. - let mut graph = BlockGroup::get_graph(conn, block_group_id, None)?; - BlockGroup::prune_graph(&mut graph); - Ok(flatten_to_interval_tree(&graph, remove_ambiguous_positions)) - } - pub fn get_current_path( conn: &GraphConnection, block_group_id: &HashId, @@ -1174,47 +951,21 @@ impl BlockGroup { Ok(None) } - #[allow(clippy::too_many_arguments)] - pub fn derive_subgraph( + pub fn persist_subgraph( conn: &GraphConnection, source_block_group_id: &HashId, - start_block: &NodeIntervalBlock, - end_block: &NodeIntervalBlock, - start_node_coordinate: i64, - end_node_coordinate: i64, + subgraph_edge_ids: &[HashId], + start: &SubgraphBoundary, + end: &SubgraphBoundary, target_block_group_id: &HashId, create_terminal_edges: bool, ) -> Result<(), BlockGroupError> { - let current_graph = BlockGroup::get_graph(conn, source_block_group_id, None)?; - let start_node = current_graph - .nodes() - .find(|node| { - node.node_id == start_block.node_id - && node.sequence_start <= start_node_coordinate - && node.sequence_end >= start_node_coordinate - }) - .unwrap(); - let end_node = current_graph - .nodes() - .find(|node| { - node.node_id == end_block.node_id - && node.sequence_start <= end_node_coordinate - && node.sequence_end >= end_node_coordinate - }) - .unwrap(); - let subgraph_edges = all_intermediate_edges(¤t_graph, start_node, end_node); - - // Filter out internal edges (boundary edges) that don't exist in the database - let subgraph_edge_ids = subgraph_edges - .iter() - .map(|(_to, _from, edge_info)| edge_info[0].edge_id) - .collect::>(); - let source_edges = Edge::query_by_ids(conn, &subgraph_edge_ids, None); + let source_edges = Edge::query_by_ids(conn, subgraph_edge_ids, None); let source_block_group_edges = BlockGroupEdge::specific_edges_for_block_group( conn, source_block_group_id, - &subgraph_edge_ids, + subgraph_edge_ids, ); let source_edge_ids = source_edges .iter() @@ -1252,9 +1003,9 @@ impl BlockGroup { PATH_START_NODE_ID, 0, Strand::Forward, - start_block.node_id, - start_node_coordinate, - start_block.strand, + start.block.node_id, + start.node_coordinate, + start.block.strand, )?; let new_start_edge_data = BlockGroupEdgeData { block_group_id: *target_block_group_id, @@ -1264,9 +1015,9 @@ impl BlockGroup { }; let new_end_edge = Edge::create( conn, - end_block.node_id, - end_node_coordinate, - end_block.strand, + end.block.node_id, + end.node_coordinate, + end.block.strand, PATH_END_NODE_ID, 0, Strand::Forward, @@ -1338,22 +1089,17 @@ impl Query for BlockGroup { #[cfg(test)] mod tests { - use core::ops::Range; - use std::collections::HashSet; - use capnp::message::TypedBuilder; use chrono::Utc; - use gen_core::{NO_CHROMOSOME_INDEX, region::RegionResolutionError}; + use gen_core::region::RegionResolutionError; use super::*; use crate::{ - annotations::Annotation as ModelAnnotation, collection::Collection, node::Node, - region::{ResolvedGenRegion, ResolvedRegionKind}, sample::{NewSample, Sample}, sequence::Sequence, - test_helpers::{create_bg, get_connection, interval_tree_verify, setup_block_group}, + test_helpers::{create_bg, get_connection, setup_block_group}, }; mod region_resolver { @@ -1553,7 +1299,74 @@ mod tests { } #[test] - fn test_blockgroup_copies_immediate_parent_block_groups() { + fn test_get_or_create_sample_block_groups_creates_root_block_group_if_no_parents() { + let conn = &get_connection(None).unwrap(); + Collection::create(conn, "test").unwrap(); + Sample::get_or_create( + conn, + NewSample { + name: "root_sample", + ..Default::default() + }, + ) + .unwrap(); + + let block_groups = BlockGroup::get_or_create_sample_block_groups( + conn, + "test", + "root_sample", + "chr1", + vec![], + ) + .unwrap(); + + assert_eq!(block_groups.len(), 1); + assert_eq!(block_groups[0].collection_name, "test"); + assert_eq!(block_groups[0].sample_name, "root_sample"); + assert_eq!(block_groups[0].name, "chr1"); + assert!(block_groups[0].parent_block_group_id.is_none()); + } + + #[test] + fn test_get_or_create_sample_block_groups_seeds_from_parents_without_block_groups() { + let conn = &get_connection(None).unwrap(); + Collection::create(conn, "test").unwrap(); + Sample::get_or_create( + conn, + NewSample { + name: "parent_sample", + ..Default::default() + }, + ) + .unwrap(); + Sample::get_or_create( + conn, + NewSample { + name: "child_sample", + ..Default::default() + }, + ) + .unwrap(); + + let block_groups = BlockGroup::get_or_create_sample_block_groups( + conn, + "test", + "child_sample", + "chr1", + vec!["parent_sample".to_string()], + ) + .unwrap(); + + assert_eq!(block_groups.len(), 1); + assert_eq!(block_groups[0].collection_name, "test"); + assert_eq!(block_groups[0].sample_name, "child_sample"); + assert_eq!(block_groups[0].name, "chr1"); + assert!(block_groups[0].parent_block_group_id.is_none()); + assert!(BlockGroupEdge::edges_for_block_group(conn, &block_groups[0].id, None).is_empty()); + } + + #[test] + fn test_blockgroup_merge_from_multiple_parents_preserves_paths_and_accessions() { let conn = &get_connection(None).unwrap(); Collection::create(conn, "test").unwrap(); Sample::get_or_create( @@ -1595,9 +1408,9 @@ mod tests { .save(conn) .unwrap(); let node_a = - Node::create(conn, &seq_a.hash, &HashId::convert_str("merge-parent-a")).unwrap(); + Node::create(conn, &seq_a.hash, &HashId::convert_str("metadata-parent-a")).unwrap(); let node_b = - Node::create(conn, &seq_b.hash, &HashId::convert_str("merge-parent-b")).unwrap(); + Node::create(conn, &seq_b.hash, &HashId::convert_str("metadata-parent-b")).unwrap(); let parent_a_edges = [ Edge::create( @@ -1669,6 +1482,69 @@ mod tests { .collect::>(), ); + let parent_a_path = Path::create( + conn, + "chr1", + &parent_a_bg.id, + &parent_a_edges + .iter() + .map(|edge| edge.id) + .collect::>(), + ) + .unwrap(); + let parent_b_path = Path::create( + conn, + "chr1", + &parent_b_bg.id, + &parent_b_edges + .iter() + .map(|edge| edge.id) + .collect::>(), + ) + .unwrap(); + let parent_b_alt_path = Path::create( + conn, + "chr1-alt", + &parent_b_bg.id, + &parent_b_edges + .iter() + .map(|edge| edge.id) + .collect::>(), + ) + .unwrap(); + + let mut path_cache = PathCache::new(conn); + let parent_a_path_len = parent_a_path.length(conn, None).unwrap(); + let parent_b_path_len = parent_b_path.length(conn, None).unwrap(); + let parent_b_alt_path_len = parent_b_alt_path.length(conn, None).unwrap(); + BlockGroup::add_accession( + conn, + &parent_a_path, + "parent-a-acc", + 0, + parent_a_path_len, + &mut path_cache, + ) + .unwrap(); + BlockGroup::add_accession( + conn, + &parent_b_path, + "parent-b-acc", + 0, + parent_b_path_len, + &mut path_cache, + ) + .unwrap(); + BlockGroup::add_accession( + conn, + &parent_b_alt_path, + "parent-b-alt-acc", + 0, + parent_b_alt_path_len, + &mut path_cache, + ) + .unwrap(); + let child_block_groups = BlockGroup::get_or_create_sample_block_groups( conn, "test", @@ -1677,594 +1553,115 @@ mod tests { vec!["parent_a".to_string(), "parent_b".to_string()], ) .unwrap(); - assert_eq!(child_block_groups.len(), 2); - let child_by_parent = child_block_groups .iter() .map(|block_group| (block_group.parent_block_group_id.unwrap(), block_group)) .collect::>(); - let child_a = child_by_parent.get(&parent_a_bg.id).unwrap(); let child_b = child_by_parent.get(&parent_b_bg.id).unwrap(); - let child_a_edges = BlockGroupEdge::query( + let child_a_paths = Path::query( conn, - "select * from block_group_edges where block_group_id = ?1", + "select * from paths where block_group_id = ?1 order by name", params![child_a.id], ); - let child_b_edges = BlockGroupEdge::query( + assert_eq!( + child_a_paths + .iter() + .map(|path| path.name.as_str()) + .collect::>(), + vec!["chr1"] + ); + + let child_b_paths = Path::query( conn, - "select * from block_group_edges where block_group_id = ?1", + "select * from paths where block_group_id = ?1 order by name", params![child_b.id], ); assert_eq!( - child_a_edges - .iter() - .map(|edge| edge.edge_id) - .collect::>(), - parent_a_edges + child_b_paths .iter() - .map(|edge| edge.id) - .collect::>() + .map(|path| path.name.as_str()) + .collect::>(), + vec!["chr1", "chr1-alt"] + ); + + let child_a_accessions = Accession::query( + conn, + "select * from accessions where block_group_id = ?1 order by name", + params![child_a.id], ); assert_eq!( - child_b_edges - .iter() - .map(|edge| edge.edge_id) - .collect::>(), - parent_b_edges + child_a_accessions .iter() - .map(|edge| edge.id) - .collect::>() + .map(|accession| accession.name.as_str()) + .collect::>(), + Vec::<&str>::new() ); - assert_eq!( - BlockGroup::get_all_sequences(conn, &child_a.id, false).unwrap(), - HashSet::from_iter(vec!["AAAA".to_string()]) - ); - assert_eq!( - BlockGroup::get_all_sequences(conn, &child_b.id, false).unwrap(), - HashSet::from_iter(vec!["CCCC".to_string()]) + let child_b_accessions = Accession::query( + conn, + "select * from accessions where block_group_id = ?1 order by name", + params![child_b.id], ); assert_eq!( - Sample::get_all_sequences(conn, "test", "child", false, None).unwrap(), - HashSet::from_iter(vec!["AAAA".to_string(), "CCCC".to_string()]) + child_b_accessions + .iter() + .map(|accession| accession.name.as_str()) + .collect::>(), + Vec::<&str>::new() ); } #[test] - fn test_get_or_create_sample_block_groups_creates_root_block_group_if_no_parents() { + fn test_blockgroup_clone_does_not_copy_accessions() { let conn = &get_connection(None).unwrap(); - Collection::create(conn, "test").unwrap(); - Sample::get_or_create( - conn, - NewSample { - name: "root_sample", - ..Default::default() - }, - ) - .unwrap(); - - let block_groups = BlockGroup::get_or_create_sample_block_groups( - conn, - "test", - "root_sample", - "chr1", - vec![], - ) - .unwrap(); - - assert_eq!(block_groups.len(), 1); - assert_eq!(block_groups[0].collection_name, "test"); - assert_eq!(block_groups[0].sample_name, "root_sample"); - assert_eq!(block_groups[0].name, "chr1"); - assert!(block_groups[0].parent_block_group_id.is_none()); - } + let (_bg_1, path) = setup_block_group(conn); + let mut path_cache = PathCache::new(conn); + let acc_1 = BlockGroup::add_accession(conn, &path, "test", 3, 7, &mut path_cache).unwrap(); + assert_eq!( + Accession::query( + conn, + "select * from accessions where name = ?1", + rusqlite::params!(SQLValue::from("test".to_string())), + ), + vec![Accession { + id: acc_1.id, + name: "test".to_string(), + block_group_id: path.block_group_id, + parent_accession_id: None, + }] + ); - #[test] - fn test_get_or_create_sample_block_groups_seeds_from_parents_without_block_groups() { - let conn = &get_connection(None).unwrap(); - Collection::create(conn, "test").unwrap(); - Sample::get_or_create( - conn, - NewSample { - name: "parent_sample", - ..Default::default() - }, - ) - .unwrap(); Sample::get_or_create( conn, NewSample { - name: "child_sample", - ..Default::default() - }, - ) - .unwrap(); - - let block_groups = BlockGroup::get_or_create_sample_block_groups( - conn, - "test", - "child_sample", - "chr1", - vec!["parent_sample".to_string()], - ) - .unwrap(); - - assert_eq!(block_groups.len(), 1); - assert_eq!(block_groups[0].collection_name, "test"); - assert_eq!(block_groups[0].sample_name, "child_sample"); - assert_eq!(block_groups[0].name, "chr1"); - assert!(block_groups[0].parent_block_group_id.is_none()); - assert!(BlockGroupEdge::edges_for_block_group(conn, &block_groups[0].id, None).is_empty()); - } - - #[test] - fn test_get_graph_branched_graph() { - // Branched graph: {AAA,GGG} → TTT → {CCC,ATC} - // TTT has 2 incoming edges and 2 outgoing edges. - // There are explicitly no start/end nodes to ensure we can build graphs purely from coordinates - let conn = get_connection(None).unwrap(); - Collection::get_or_create(&conn, "test").unwrap(); - crate::sample::Sample::get_or_create( - &conn, - crate::sample::NewSample { - name: "test", - ..Default::default() - }, - ) - .unwrap(); - let bg = BlockGroup::create( - &conn, - crate::block_group::NewBlockGroup { - collection_name: "test", - sample_name: "test", - name: "branched", + name: "sample2", ..Default::default() }, ) .unwrap(); - - let seq_aaa = Sequence::new() - .sequence_type("DNA") - .sequence("AAA") - .save(&conn) - .unwrap(); - let seq_ggg = Sequence::new() - .sequence_type("DNA") - .sequence("GGG") - .save(&conn) - .unwrap(); - let seq_ttt = Sequence::new() - .sequence_type("DNA") - .sequence("TTT") - .save(&conn) - .unwrap(); - let seq_ccc = Sequence::new() - .sequence_type("DNA") - .sequence("CCC") - .save(&conn) - .unwrap(); - let seq_atc = Sequence::new() - .sequence_type("DNA") - .sequence("ATC") - .save(&conn) - .unwrap(); - - let n_aaa = Node::create(&conn, &seq_aaa.hash, &HashId::convert_str("node-aaa")).unwrap(); - let n_ggg = Node::create(&conn, &seq_ggg.hash, &HashId::convert_str("node-ggg")).unwrap(); - let n_ttt = Node::create(&conn, &seq_ttt.hash, &HashId::convert_str("node-ttt")).unwrap(); - let n_ccc = Node::create(&conn, &seq_ccc.hash, &HashId::convert_str("node-ccc")).unwrap(); - let n_atc = Node::create(&conn, &seq_atc.hash, &HashId::convert_str("node-atc")).unwrap(); - - // Edges: AAA→TTT, GGG→TTT, TTT→CCC, TTT→ATC - let e_aaa_ttt = - Edge::create(&conn, n_aaa, 3, Strand::Forward, n_ttt, 0, Strand::Forward).unwrap(); - let e_ggg_ttt = - Edge::create(&conn, n_ggg, 3, Strand::Forward, n_ttt, 0, Strand::Forward).unwrap(); - let e_ttt_ccc = - Edge::create(&conn, n_ttt, 3, Strand::Forward, n_ccc, 0, Strand::Forward).unwrap(); - let e_ttt_atc = - Edge::create(&conn, n_ttt, 3, Strand::Forward, n_atc, 0, Strand::Forward).unwrap(); - - let expected_edges = [ - ( - GraphNode { - node_id: n_aaa, - sequence_start: 3, - sequence_end: 3, - }, - GraphNode { - node_id: n_ttt, - sequence_start: 0, - sequence_end: 3, - }, - e_aaa_ttt.id, - ), - ( - GraphNode { - node_id: n_ggg, - sequence_start: 3, - sequence_end: 3, - }, - GraphNode { - node_id: n_ttt, - sequence_start: 0, - sequence_end: 3, - }, - e_ggg_ttt.id, - ), - ( - GraphNode { - node_id: n_ttt, - sequence_start: 0, - sequence_end: 3, - }, - GraphNode { - node_id: n_ccc, - sequence_start: 0, - sequence_end: 0, - }, - e_ttt_ccc.id, - ), - ( - GraphNode { - node_id: n_ttt, - sequence_start: 0, - sequence_end: 3, - }, - GraphNode { - node_id: n_atc, - sequence_start: 0, - sequence_end: 0, - }, - e_ttt_atc.id, - ), - ]; - - let block_group_edges = [ - e_aaa_ttt.clone(), - e_ggg_ttt.clone(), - e_ttt_ccc.clone(), - e_ttt_atc.clone(), - ] - .iter() - .map(|edge_id| BlockGroupEdgeData { - block_group_id: bg.id, - edge_id: edge_id.id, - chromosome_index: 0, - phased: 0, - }) - .collect::>(); - - BlockGroupEdge::bulk_create(&conn, &block_group_edges); - let graph = BlockGroup::get_graph(&conn, &bg.id, None).unwrap(); - - // 5 non-terminal nodes: AAA, GGG, TTT, CCC, ATC - // 2 terminal blocks: START, END - // Total: 7 - assert_eq!( - graph.nodes().len(), - 7, - "expected 7 blocks (5 nodes + 2 terminals), got {}", - graph.nodes().len() - ); - + let _bg2 = get_single_bg_id(conn, "test", "sample2", "chr1", vec!["test".to_string()]); assert_eq!( - graph.all_edges().count(), - expected_edges.len(), - "expected exactly the 4 branch edges" + Accession::query( + conn, + "select * from accessions where name = ?1", + rusqlite::params!(SQLValue::from("test".to_string())), + ) + .len(), + 1 ); - for (source, target, edge_id) in expected_edges { - let weights = graph - .edge_weight(source, target) - .unwrap_or_else(|| panic!("missing graph edge {source:?} -> {target:?}")); - assert_eq!( - weights.len(), - 1, - "expected a single edge weight for {source:?} -> {target:?}" - ); - assert_eq!(weights[0].edge_id, edge_id); - } } #[test] - fn test_blockgroup_merge_from_multiple_parents_preserves_paths_and_accessions() { + fn test_accession_end_coordinate_is_not_included() { let conn = &get_connection(None).unwrap(); - Collection::create(conn, "test").unwrap(); - Sample::get_or_create( - conn, - NewSample { - name: "parent_a", - ..Default::default() - }, - ) - .unwrap(); - Sample::get_or_create( - conn, - NewSample { - name: "parent_b", - ..Default::default() - }, - ) - .unwrap(); - Sample::get_or_create( - conn, - NewSample { - name: "child", - ..Default::default() - }, - ) - .unwrap(); - - let parent_a_bg = create_bg(conn, "test", "parent_a", "chr1"); - let parent_b_bg = create_bg(conn, "test", "parent_b", "chr1"); - - let seq_a = Sequence::new() - .sequence_type("DNA") - .sequence("AAAA") - .save(conn) - .unwrap(); - let seq_b = Sequence::new() - .sequence_type("DNA") - .sequence("CCCC") - .save(conn) - .unwrap(); - let node_a = - Node::create(conn, &seq_a.hash, &HashId::convert_str("metadata-parent-a")).unwrap(); - let node_b = - Node::create(conn, &seq_b.hash, &HashId::convert_str("metadata-parent-b")).unwrap(); - - let parent_a_edges = [ - Edge::create( - conn, - PATH_START_NODE_ID, - 0, - Strand::Forward, - node_a, - 0, - Strand::Forward, - ) - .unwrap(), - Edge::create( - conn, - node_a, - 4, - Strand::Forward, - PATH_END_NODE_ID, - 0, - Strand::Forward, - ) - .unwrap(), - ]; - let parent_b_edges = [ - Edge::create( - conn, - PATH_START_NODE_ID, - 0, - Strand::Forward, - node_b, - 0, - Strand::Forward, - ) - .unwrap(), - Edge::create( - conn, - node_b, - 4, - Strand::Forward, - PATH_END_NODE_ID, - 0, - Strand::Forward, - ) - .unwrap(), - ]; + let (_block_group_id, path) = setup_block_group(conn); + let mut path_cache = PathCache::new(conn); - BlockGroupEdge::bulk_create( - conn, - &parent_a_edges - .iter() - .map(|edge| BlockGroupEdgeData { - block_group_id: parent_a_bg.id, - edge_id: edge.id, - chromosome_index: 0, - phased: 0, - }) - .collect::>(), - ); - BlockGroupEdge::bulk_create( - conn, - &parent_b_edges - .iter() - .map(|edge| BlockGroupEdgeData { - block_group_id: parent_b_bg.id, - edge_id: edge.id, - chromosome_index: 0, - phased: 0, - }) - .collect::>(), - ); - - let parent_a_path = Path::create( - conn, - "chr1", - &parent_a_bg.id, - &parent_a_edges - .iter() - .map(|edge| edge.id) - .collect::>(), - ) - .unwrap(); - let parent_b_path = Path::create( - conn, - "chr1", - &parent_b_bg.id, - &parent_b_edges - .iter() - .map(|edge| edge.id) - .collect::>(), - ) - .unwrap(); - let parent_b_alt_path = Path::create( - conn, - "chr1-alt", - &parent_b_bg.id, - &parent_b_edges - .iter() - .map(|edge| edge.id) - .collect::>(), - ) - .unwrap(); - - let mut path_cache = PathCache::new(conn); - let parent_a_path_len = parent_a_path.length(conn, None).unwrap(); - let parent_b_path_len = parent_b_path.length(conn, None).unwrap(); - let parent_b_alt_path_len = parent_b_alt_path.length(conn, None).unwrap(); - BlockGroup::add_accession( - conn, - &parent_a_path, - "parent-a-acc", - 0, - parent_a_path_len, - &mut path_cache, - ) - .unwrap(); - BlockGroup::add_accession( - conn, - &parent_b_path, - "parent-b-acc", - 0, - parent_b_path_len, - &mut path_cache, - ) - .unwrap(); - BlockGroup::add_accession( - conn, - &parent_b_alt_path, - "parent-b-alt-acc", - 0, - parent_b_alt_path_len, - &mut path_cache, - ) - .unwrap(); - - let child_block_groups = BlockGroup::get_or_create_sample_block_groups( - conn, - "test", - "child", - "chr1", - vec!["parent_a".to_string(), "parent_b".to_string()], - ) - .unwrap(); - let child_by_parent = child_block_groups - .iter() - .map(|block_group| (block_group.parent_block_group_id.unwrap(), block_group)) - .collect::>(); - let child_a = child_by_parent.get(&parent_a_bg.id).unwrap(); - let child_b = child_by_parent.get(&parent_b_bg.id).unwrap(); - - let child_a_paths = Path::query( - conn, - "select * from paths where block_group_id = ?1 order by name", - params![child_a.id], - ); - assert_eq!( - child_a_paths - .iter() - .map(|path| path.name.as_str()) - .collect::>(), - vec!["chr1"] - ); - - let child_b_paths = Path::query( - conn, - "select * from paths where block_group_id = ?1 order by name", - params![child_b.id], - ); - assert_eq!( - child_b_paths - .iter() - .map(|path| path.name.as_str()) - .collect::>(), - vec!["chr1", "chr1-alt"] - ); - - let child_a_accessions = Accession::query( - conn, - "select * from accessions where block_group_id = ?1 order by name", - params![child_a.id], - ); - assert_eq!( - child_a_accessions - .iter() - .map(|accession| accession.name.as_str()) - .collect::>(), - Vec::<&str>::new() - ); - - let child_b_accessions = Accession::query( - conn, - "select * from accessions where block_group_id = ?1 order by name", - params![child_b.id], - ); - assert_eq!( - child_b_accessions - .iter() - .map(|accession| accession.name.as_str()) - .collect::>(), - Vec::<&str>::new() - ); - } - - #[test] - fn test_blockgroup_clone_does_not_copy_accessions() { - let conn = &get_connection(None).unwrap(); - let (_bg_1, path) = setup_block_group(conn); - let mut path_cache = PathCache::new(conn); - let acc_1 = BlockGroup::add_accession(conn, &path, "test", 3, 7, &mut path_cache).unwrap(); - assert_eq!( - Accession::query( - conn, - "select * from accessions where name = ?1", - rusqlite::params!(SQLValue::from("test".to_string())), - ), - vec![Accession { - id: acc_1.id, - name: "test".to_string(), - block_group_id: path.block_group_id, - parent_accession_id: None, - }] - ); - - Sample::get_or_create( - conn, - NewSample { - name: "sample2", - ..Default::default() - }, - ) - .unwrap(); - let _bg2 = get_single_bg_id(conn, "test", "sample2", "chr1", vec!["test".to_string()]); - assert_eq!( - Accession::query( - conn, - "select * from accessions where name = ?1", - rusqlite::params!(SQLValue::from("test".to_string())), - ) - .len(), - 1 - ); - } - - #[test] - fn test_accession_end_coordinate_is_not_included() { - let conn = &get_connection(None).unwrap(); - let (_block_group_id, path) = setup_block_group(conn); - let mut path_cache = PathCache::new(conn); - - let accession = - BlockGroup::add_accession(conn, &path, "test", 3, 10, &mut path_cache).unwrap(); - let nodes = Accession::get_nodes_by_id(conn, &accession.id, None); + let accession = + BlockGroup::add_accession(conn, &path, "test", 3, 10, &mut path_cache).unwrap(); + let nodes = Accession::get_nodes_by_id(conn, &accession.id, None); assert_eq!(accession.length(conn).unwrap(), 7); assert_eq!(nodes.len(), 1); @@ -2272,2279 +1669,4 @@ mod tests { assert_eq!(nodes[0].sequence_start, 3); assert_eq!(nodes[0].sequence_end, 10); } - - #[test] - fn insert_accession_change_get_all() { - let conn = get_connection(None).unwrap(); - let (block_group_id, path) = setup_block_group(&conn); - let mut path_cache = PathCache::new(&conn); - let accession = - BlockGroup::add_accession(&conn, &path, "test-accession", 10, 30, &mut path_cache) - .unwrap(); - let insert_sequence = Sequence::new() - .sequence_type("DNA") - .sequence("NNNN") - .save(&conn) - .unwrap(); - let insert_node_id = Node::create( - &conn, - &insert_sequence.hash, - &HashId::convert_str("acc-insert-node"), - ) - .unwrap(); - let insert = PathBlock { - node_id: insert_node_id, - block_sequence: insert_sequence.get_sequence(0, 4).unwrap(), - sequence_start: 0, - sequence_end: 4, - path_start: 5, - path_end: 15, - strand: Strand::Forward, - }; - let region = ResolvedGenRegion::from_accession(&conn, &accession, 5, 15).unwrap(); - let change = BlockGroupChange { - region, - path_accession: None, - block: insert, - chromosome_index: 1, - phased: 0, - preserve_edge: true, - }; - - BlockGroup::insert_change(&conn, &change).unwrap(); - let all_sequences = BlockGroup::get_all_sequences(&conn, &block_group_id, false).unwrap(); - assert_eq!( - all_sequences, - HashSet::from_iter(vec![ - "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), - "AAAAAAAAAATTTTTNNNNCCCCCGGGGGGGGGG".to_string(), - ]) - ); - } - - #[test] - fn insert_annotation_change_get_all() { - let conn = get_connection(None).unwrap(); - let (block_group_id, path) = setup_block_group(&conn); - let mut path_cache = PathCache::new(&conn); - let accession = - BlockGroup::add_accession(&conn, &path, "test-accession", 10, 30, &mut path_cache) - .unwrap(); - let annotation = - ModelAnnotation::get_or_create(&conn, "gene-1", "track-1", &accession.id, None) - .unwrap(); - let deletion_sequence = Sequence::new() - .sequence_type("DNA") - .sequence("") - .save(&conn) - .unwrap(); - let deletion_node_id = Node::create( - &conn, - &deletion_sequence.hash, - &HashId::convert_str("annotation-delete-node"), - ) - .unwrap(); - let deletion = PathBlock { - node_id: deletion_node_id, - block_sequence: deletion_sequence.get_sequence(None, None).unwrap(), - sequence_start: 0, - sequence_end: 0, - path_start: 5, - path_end: 15, - strand: Strand::Forward, - }; - let annotation_accession = - Accession::get_by_id(&conn, &annotation.accession_id, None).unwrap(); - let region = - ResolvedGenRegion::from_annotation(&conn, &annotation, &annotation_accession, 5, 15) - .unwrap(); - let change = BlockGroupChange { - region, - path_accession: None, - block: deletion, - chromosome_index: 1, - phased: 0, - preserve_edge: true, - }; - - BlockGroup::insert_change(&conn, &change).unwrap(); - let all_sequences = BlockGroup::get_all_sequences(&conn, &block_group_id, false).unwrap(); - assert_eq!( - all_sequences, - HashSet::from_iter(vec![ - "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), - "AAAAAAAAAATTTTTCCCCCGGGGGGGGGG".to_string(), - ]) - ); - } - - #[test] - fn insert_and_deletion_get_all() { - let conn = get_connection(None).unwrap(); - let (block_group_id, path) = setup_block_group(&conn); - let insert_sequence = Sequence::new() - .sequence_type("DNA") - .sequence("NNNN") - .save(&conn) - .unwrap(); - let insert_node_id = - Node::create(&conn, &insert_sequence.hash, &HashId::convert_str("1")).unwrap(); - let insert = PathBlock { - node_id: insert_node_id, - block_sequence: insert_sequence.get_sequence(0, 4).unwrap(), - sequence_start: 0, - sequence_end: 4, - path_start: 7, - path_end: 15, - strand: Strand::Forward, - }; - let region = ResolvedGenRegion::from_path(&conn, block_group_id, &path, 7, 15).unwrap(); - let change = BlockGroupChange { - region, - path_accession: None, - block: insert, - chromosome_index: 1, - phased: 0, - preserve_edge: true, - }; - BlockGroup::insert_change(&conn, &change).unwrap(); - - let all_sequences = BlockGroup::get_all_sequences(&conn, &block_group_id, false).unwrap(); - assert_eq!( - all_sequences, - HashSet::from_iter(vec![ - "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), - "AAAAAAANNNNTTTTTCCCCCCCCCCGGGGGGGGGG".to_string() - ]) - ); - - let deletion_sequence = Sequence::new() - .sequence_type("DNA") - .sequence("") - .save(&conn) - .unwrap(); - let deletion_node_id = - Node::create(&conn, &deletion_sequence.hash, &HashId::convert_str("2")).unwrap(); - let deletion = PathBlock { - node_id: deletion_node_id, - block_sequence: deletion_sequence.get_sequence(None, None).unwrap(), - sequence_start: 0, - sequence_end: 0, - path_start: 19, - path_end: 31, - strand: Strand::Forward, - }; - - let region = ResolvedGenRegion::from_path(&conn, block_group_id, &path, 19, 31).unwrap(); - let change = BlockGroupChange { - region, - path_accession: None, - block: deletion, - chromosome_index: 1, - phased: 0, - preserve_edge: true, - }; - BlockGroup::insert_change(&conn, &change).unwrap(); - let all_sequences = BlockGroup::get_all_sequences(&conn, &block_group_id, false).unwrap(); - assert_eq!( - all_sequences, - HashSet::from_iter(vec![ - "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), - "AAAAAAANNNNTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), - "AAAAAAAAAATTTTTTTTTGGGGGGGGG".to_string(), - "AAAAAAANNNNTTTTGGGGGGGGG".to_string(), - ]) - ); - } - - #[test] - fn simple_insert_get_all() { - let conn = get_connection(None).unwrap(); - let (block_group_id, path) = setup_block_group(&conn); - let insert_sequence = Sequence::new() - .sequence_type("DNA") - .sequence("NNNN") - .save(&conn) - .unwrap(); - let insert_node_id = - Node::create(&conn, &insert_sequence.hash, &HashId::convert_str("1")).unwrap(); - let insert = PathBlock { - node_id: insert_node_id, - block_sequence: insert_sequence.get_sequence(0, 4).unwrap(), - sequence_start: 0, - sequence_end: 4, - path_start: 7, - path_end: 15, - strand: Strand::Forward, - }; - let region = ResolvedGenRegion::from_path(&conn, block_group_id, &path, 7, 15).unwrap(); - let change = BlockGroupChange { - region, - path_accession: None, - block: insert, - chromosome_index: 1, - phased: 0, - preserve_edge: true, - }; - BlockGroup::insert_change(&conn, &change).unwrap(); - - let all_sequences = BlockGroup::get_all_sequences(&conn, &block_group_id, false).unwrap(); - assert_eq!( - all_sequences, - HashSet::from_iter(vec![ - "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), - "AAAAAAANNNNTTTTTCCCCCCCCCCGGGGGGGGGG".to_string() - ]) - ); - } - - #[test] - fn insert_on_block_boundary_middle() { - let conn = get_connection(None).unwrap(); - let (block_group_id, path) = setup_block_group(&conn); - let insert_sequence = Sequence::new() - .sequence_type("DNA") - .sequence("NNNN") - .save(&conn) - .unwrap(); - let insert_node_id = - Node::create(&conn, &insert_sequence.hash, &HashId::convert_str("1")).unwrap(); - let insert = PathBlock { - node_id: insert_node_id, - block_sequence: insert_sequence.get_sequence(0, 4).unwrap(), - sequence_start: 0, - sequence_end: 4, - path_start: 15, - path_end: 15, - strand: Strand::Forward, - }; - let region = ResolvedGenRegion::from_path(&conn, block_group_id, &path, 15, 15).unwrap(); - let change = BlockGroupChange { - region, - path_accession: None, - block: insert, - chromosome_index: 1, - phased: 0, - preserve_edge: true, - }; - BlockGroup::insert_change(&conn, &change).unwrap(); - - let all_sequences = BlockGroup::get_all_sequences(&conn, &block_group_id, false).unwrap(); - assert_eq!( - all_sequences, - HashSet::from_iter(vec![ - "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), - "AAAAAAAAAATTTTTNNNNTTTTTCCCCCCCCCCGGGGGGGGGG".to_string() - ]) - ); - } - - #[test] - fn insert_within_block() { - let conn = get_connection(None).unwrap(); - let (block_group_id, path) = setup_block_group(&conn); - let insert_sequence = Sequence::new() - .sequence_type("DNA") - .sequence("NNNN") - .save(&conn) - .unwrap(); - let insert_node_id = - Node::create(&conn, &insert_sequence.hash, &HashId::convert_str("1")).unwrap(); - let insert = PathBlock { - node_id: insert_node_id, - block_sequence: insert_sequence.get_sequence(0, 4).unwrap(), - sequence_start: 0, - sequence_end: 4, - path_start: 12, - path_end: 17, - strand: Strand::Forward, - }; - let region = ResolvedGenRegion::from_path(&conn, block_group_id, &path, 12, 17).unwrap(); - let change = BlockGroupChange { - region, - path_accession: None, - block: insert, - chromosome_index: 1, - phased: 0, - preserve_edge: true, - }; - BlockGroup::insert_change(&conn, &change).unwrap(); - - let all_sequences = BlockGroup::get_all_sequences(&conn, &block_group_id, false).unwrap(); - assert_eq!( - all_sequences, - HashSet::from_iter(vec![ - "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), - "AAAAAAAAAATTNNNNTTTCCCCCCCCCCGGGGGGGGGG".to_string() - ]) - ); - } - - #[test] - fn insert_on_block_boundary_start() { - let conn = get_connection(None).unwrap(); - let (block_group_id, path) = setup_block_group(&conn); - let insert_sequence = Sequence::new() - .sequence_type("DNA") - .sequence("NNNN") - .save(&conn) - .unwrap(); - let insert_node_id = - Node::create(&conn, &insert_sequence.hash, &HashId::convert_str("1")).unwrap(); - let insert = PathBlock { - node_id: insert_node_id, - block_sequence: insert_sequence.get_sequence(0, 4).unwrap(), - sequence_start: 0, - sequence_end: 4, - path_start: 10, - path_end: 10, - strand: Strand::Forward, - }; - let region = ResolvedGenRegion::from_path(&conn, block_group_id, &path, 10, 10).unwrap(); - let change = BlockGroupChange { - region, - path_accession: None, - block: insert, - chromosome_index: 1, - phased: 0, - preserve_edge: true, - }; - BlockGroup::insert_change(&conn, &change).unwrap(); - - let all_sequences = BlockGroup::get_all_sequences(&conn, &block_group_id, false).unwrap(); - assert_eq!( - all_sequences, - HashSet::from_iter(vec![ - "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), - "AAAAAAAAAANNNNTTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string() - ]) - ); - } - - #[test] - fn insert_on_block_boundary_end() { - let conn = get_connection(None).unwrap(); - let (block_group_id, path) = setup_block_group(&conn); - let insert_sequence = Sequence::new() - .sequence_type("DNA") - .sequence("NNNN") - .save(&conn) - .unwrap(); - let insert_node_id = - Node::create(&conn, &insert_sequence.hash, &HashId::convert_str("1")).unwrap(); - let insert = PathBlock { - node_id: insert_node_id, - block_sequence: insert_sequence.get_sequence(0, 4).unwrap(), - sequence_start: 0, - sequence_end: 4, - path_start: 9, - path_end: 9, - strand: Strand::Forward, - }; - let region = ResolvedGenRegion::from_path(&conn, block_group_id, &path, 9, 9).unwrap(); - let change = BlockGroupChange { - region, - path_accession: None, - block: insert, - chromosome_index: 1, - phased: 0, - preserve_edge: true, - }; - BlockGroup::insert_change(&conn, &change).unwrap(); - - let all_sequences = BlockGroup::get_all_sequences(&conn, &block_group_id, false).unwrap(); - assert_eq!( - all_sequences, - HashSet::from_iter(vec![ - "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), - "AAAAAAAAANNNNATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string() - ]) - ); - } - - #[test] - fn insert_across_entire_block_boundary() { - let conn = get_connection(None).unwrap(); - let (block_group_id, path) = setup_block_group(&conn); - let insert_sequence = Sequence::new() - .sequence_type("DNA") - .sequence("NNNN") - .save(&conn) - .unwrap(); - let insert_node_id = - Node::create(&conn, &insert_sequence.hash, &HashId::convert_str("1")).unwrap(); - let insert = PathBlock { - node_id: insert_node_id, - block_sequence: insert_sequence.get_sequence(0, 4).unwrap(), - sequence_start: 0, - sequence_end: 4, - path_start: 10, - path_end: 20, - strand: Strand::Forward, - }; - let region = ResolvedGenRegion::from_path(&conn, block_group_id, &path, 10, 20).unwrap(); - let change = BlockGroupChange { - region, - path_accession: None, - block: insert, - chromosome_index: 1, - phased: 0, - preserve_edge: true, - }; - BlockGroup::insert_change(&conn, &change).unwrap(); - - let all_sequences = BlockGroup::get_all_sequences(&conn, &block_group_id, false).unwrap(); - assert_eq!( - all_sequences, - HashSet::from_iter(vec![ - "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), - "AAAAAAAAAANNNNCCCCCCCCCCGGGGGGGGGG".to_string() - ]) - ); - } - - #[test] - fn insert_across_two_blocks() { - let conn = get_connection(None).unwrap(); - let (block_group_id, path) = setup_block_group(&conn); - let insert_sequence = Sequence::new() - .sequence_type("DNA") - .sequence("NNNN") - .save(&conn) - .unwrap(); - let insert_node_id = - Node::create(&conn, &insert_sequence.hash, &HashId::convert_str("1")).unwrap(); - let insert = PathBlock { - node_id: insert_node_id, - block_sequence: insert_sequence.get_sequence(0, 4).unwrap(), - sequence_start: 0, - sequence_end: 4, - path_start: 15, - path_end: 25, - strand: Strand::Forward, - }; - let region = ResolvedGenRegion::from_path(&conn, block_group_id, &path, 15, 25).unwrap(); - let change = BlockGroupChange { - region, - path_accession: None, - block: insert, - chromosome_index: 1, - phased: 0, - preserve_edge: true, - }; - BlockGroup::insert_change(&conn, &change).unwrap(); - - let all_sequences = BlockGroup::get_all_sequences(&conn, &block_group_id, false).unwrap(); - assert_eq!( - all_sequences, - HashSet::from_iter(vec![ - "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), - "AAAAAAAAAATTTTTNNNNCCCCCGGGGGGGGGG".to_string() - ]) - ); - } - - #[test] - fn insert_spanning_blocks() { - let conn = get_connection(None).unwrap(); - let (block_group_id, path) = setup_block_group(&conn); - let insert_sequence = Sequence::new() - .sequence_type("DNA") - .sequence("NNNN") - .save(&conn) - .unwrap(); - let insert_node_id = - Node::create(&conn, &insert_sequence.hash, &HashId::convert_str("1")).unwrap(); - let insert = PathBlock { - node_id: insert_node_id, - block_sequence: insert_sequence.get_sequence(0, 4).unwrap(), - sequence_start: 0, - sequence_end: 4, - path_start: 5, - path_end: 35, - strand: Strand::Forward, - }; - let region = ResolvedGenRegion::from_path(&conn, block_group_id, &path, 5, 35).unwrap(); - let change = BlockGroupChange { - region, - path_accession: None, - block: insert, - chromosome_index: 1, - phased: 0, - preserve_edge: true, - }; - BlockGroup::insert_change(&conn, &change).unwrap(); - - let all_sequences = BlockGroup::get_all_sequences(&conn, &block_group_id, false).unwrap(); - assert_eq!( - all_sequences, - HashSet::from_iter(vec![ - "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), - "AAAAANNNNGGGGG".to_string() - ]) - ); - } - - #[test] - fn simple_deletion() { - let conn = get_connection(None).unwrap(); - let (block_group_id, path) = setup_block_group(&conn); - let deletion_sequence = Sequence::new() - .sequence_type("DNA") - .sequence("") - .save(&conn) - .unwrap(); - let deletion_node_id = - Node::create(&conn, &deletion_sequence.hash, &HashId::convert_str("1")).unwrap(); - let deletion = PathBlock { - node_id: deletion_node_id, - block_sequence: deletion_sequence.get_sequence(None, None).unwrap(), - sequence_start: 0, - sequence_end: 0, - path_start: 19, - path_end: 31, - strand: Strand::Forward, - }; - - let region = ResolvedGenRegion::from_path(&conn, block_group_id, &path, 19, 31).unwrap(); - let change = BlockGroupChange { - region, - path_accession: None, - block: deletion, - chromosome_index: 1, - phased: 0, - preserve_edge: true, - }; - - // take out an entire block - BlockGroup::insert_change(&conn, &change).unwrap(); - let all_sequences = BlockGroup::get_all_sequences(&conn, &block_group_id, false).unwrap(); - assert_eq!( - all_sequences, - HashSet::from_iter(vec![ - "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), - "AAAAAAAAAATTTTTTTTTGGGGGGGGG".to_string(), - ]) - ); - } - - #[test] - fn doesnt_apply_same_insert_twice() { - let conn = get_connection(None).unwrap(); - let (block_group_id, path) = setup_block_group(&conn); - let insert_sequence = Sequence::new() - .sequence_type("DNA") - .sequence("NNNN") - .save(&conn) - .unwrap(); - let insert_node_id = - Node::create(&conn, &insert_sequence.hash, &HashId::convert_str("1")).unwrap(); - let insert = PathBlock { - node_id: insert_node_id, - block_sequence: insert_sequence.get_sequence(0, 4).unwrap(), - sequence_start: 0, - sequence_end: 4, - path_start: 7, - path_end: 15, - strand: Strand::Forward, - }; - let region = ResolvedGenRegion::from_path(&conn, block_group_id, &path, 7, 15).unwrap(); - let change = BlockGroupChange { - region, - path_accession: None, - block: insert, - chromosome_index: 1, - phased: 0, - preserve_edge: true, - }; - BlockGroup::insert_change(&conn, &change).unwrap(); - - let all_sequences = BlockGroup::get_all_sequences(&conn, &block_group_id, false).unwrap(); - assert_eq!( - all_sequences, - HashSet::from_iter(vec![ - "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), - "AAAAAAANNNNTTTTTCCCCCCCCCCGGGGGGGGGG".to_string() - ]) - ); - BlockGroup::insert_change(&conn, &change).unwrap(); - - let all_sequences = BlockGroup::get_all_sequences(&conn, &block_group_id, false).unwrap(); - assert_eq!( - all_sequences, - HashSet::from_iter(vec![ - "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), - "AAAAAAANNNNTTTTTCCCCCCCCCCGGGGGGGGGG".to_string() - ]) - ); - } - - #[test] - fn insert_at_beginning_of_path() { - let conn = get_connection(None).unwrap(); - let (block_group_id, path) = setup_block_group(&conn); - let insert_sequence = Sequence::new() - .sequence_type("DNA") - .sequence("NNNN") - .save(&conn) - .unwrap(); - let insert_node_id = - Node::create(&conn, &insert_sequence.hash, &HashId::convert_str("1")).unwrap(); - let insert = PathBlock { - node_id: insert_node_id, - block_sequence: insert_sequence.get_sequence(0, 4).unwrap(), - sequence_start: 0, - sequence_end: 4, - path_start: 0, - path_end: 0, - strand: Strand::Forward, - }; - let region = ResolvedGenRegion::from_path(&conn, block_group_id, &path, 0, 0).unwrap(); - let change = BlockGroupChange { - region, - path_accession: None, - block: insert, - chromosome_index: 1, - phased: 0, - preserve_edge: true, - }; - BlockGroup::insert_change(&conn, &change).unwrap(); - - let all_sequences = BlockGroup::get_all_sequences(&conn, &block_group_id, false).unwrap(); - assert_eq!( - all_sequences, - HashSet::from_iter(vec![ - "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), - "NNNNAAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), - ]) - ); - } - - #[test] - fn homozygous_insert_at_beginning_of_path() { - let conn = get_connection(None).unwrap(); - let (block_group_id, path) = setup_block_group(&conn); - let insert_sequence = Sequence::new() - .sequence_type("DNA") - .sequence("NNNN") - .save(&conn) - .unwrap(); - let insert_node_id = - Node::create(&conn, &insert_sequence.hash, &HashId::convert_str("1")).unwrap(); - let insert = PathBlock { - node_id: insert_node_id, - block_sequence: insert_sequence.get_sequence(0, 4).unwrap(), - sequence_start: 0, - sequence_end: 4, - path_start: 0, - path_end: 0, - strand: Strand::Forward, - }; - let region = ResolvedGenRegion::from_path(&conn, block_group_id, &path, 0, 0).unwrap(); - let change = BlockGroupChange { - region, - path_accession: None, - block: insert, - chromosome_index: 0, - phased: 0, - preserve_edge: true, - }; - BlockGroup::insert_change(&conn, &change).unwrap(); - - let all_sequences = BlockGroup::get_all_sequences(&conn, &block_group_id, false).unwrap(); - assert_eq!( - all_sequences, - HashSet::from_iter(vec![ - "NNNNAAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), - ]) - ); - } - - #[test] - fn insert_at_end_of_path() { - let conn = get_connection(None).unwrap(); - let (block_group_id, path) = setup_block_group(&conn); - - let insert_sequence = Sequence::new() - .sequence_type("DNA") - .sequence("NNNN") - .save(&conn) - .unwrap(); - let insert_node_id = - Node::create(&conn, &insert_sequence.hash, &HashId::convert_str("1")).unwrap(); - let insert = PathBlock { - node_id: insert_node_id, - block_sequence: insert_sequence.get_sequence(0, 4).unwrap(), - sequence_start: 0, - sequence_end: 4, - path_start: 40, - path_end: 40, - strand: Strand::Forward, - }; - let region = ResolvedGenRegion::from_path(&conn, block_group_id, &path, 40, 40).unwrap(); - let change = BlockGroupChange { - region, - path_accession: None, - block: insert, - chromosome_index: 1, - phased: 0, - preserve_edge: true, - }; - BlockGroup::insert_change(&conn, &change).unwrap(); - - let all_sequences = BlockGroup::get_all_sequences(&conn, &block_group_id, false).unwrap(); - assert_eq!( - all_sequences, - HashSet::from_iter(vec![ - "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), - "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGGNNNN".to_string(), - ]) - ); - } - - #[test] - fn insert_at_one_bp_into_block() { - let conn = get_connection(None).unwrap(); - let (block_group_id, path) = setup_block_group(&conn); - let insert_sequence = Sequence::new() - .sequence_type("DNA") - .sequence("NNNN") - .save(&conn) - .unwrap(); - let insert_node_id = - Node::create(&conn, &insert_sequence.hash, &HashId::convert_str("1")).unwrap(); - let insert = PathBlock { - node_id: insert_node_id, - block_sequence: insert_sequence.get_sequence(0, 4).unwrap(), - sequence_start: 0, - sequence_end: 4, - path_start: 10, - path_end: 11, - strand: Strand::Forward, - }; - let region = ResolvedGenRegion::from_path(&conn, block_group_id, &path, 10, 11).unwrap(); - let change = BlockGroupChange { - region, - path_accession: None, - block: insert, - chromosome_index: 1, - phased: 0, - preserve_edge: true, - }; - BlockGroup::insert_change(&conn, &change).unwrap(); - - let all_sequences = BlockGroup::get_all_sequences(&conn, &block_group_id, false).unwrap(); - assert_eq!( - all_sequences, - HashSet::from_iter(vec![ - "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), - "AAAAAAAAAANNNNTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), - ]) - ); - } - - #[test] - fn insert_at_one_bp_from_end_of_block() { - let conn = get_connection(None).unwrap(); - let (block_group_id, path) = setup_block_group(&conn); - let insert_sequence = Sequence::new() - .sequence_type("DNA") - .sequence("NNNN") - .save(&conn) - .unwrap(); - let insert_node_id = - Node::create(&conn, &insert_sequence.hash, &HashId::convert_str("1")).unwrap(); - let insert = PathBlock { - node_id: insert_node_id, - block_sequence: insert_sequence.get_sequence(0, 4).unwrap(), - sequence_start: 0, - sequence_end: 4, - path_start: 19, - path_end: 20, - strand: Strand::Forward, - }; - let region = ResolvedGenRegion::from_path(&conn, block_group_id, &path, 19, 20).unwrap(); - let change = BlockGroupChange { - region, - path_accession: None, - block: insert, - chromosome_index: 1, - phased: 0, - preserve_edge: true, - }; - BlockGroup::insert_change(&conn, &change).unwrap(); - - let all_sequences = BlockGroup::get_all_sequences(&conn, &block_group_id, false).unwrap(); - assert_eq!( - all_sequences, - HashSet::from_iter(vec![ - "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), - "AAAAAAAAAATTTTTTTTTNNNNCCCCCCCCCCGGGGGGGGGG".to_string(), - ]) - ); - } - - #[test] - fn delete_at_beginning_of_path() { - let conn = get_connection(None).unwrap(); - let (block_group_id, path) = setup_block_group(&conn); - let deletion_sequence = Sequence::new() - .sequence_type("DNA") - .sequence("") - .save(&conn) - .unwrap(); - let deletion_node_id = - Node::create(&conn, &deletion_sequence.hash, &HashId::convert_str("1")).unwrap(); - let deletion = PathBlock { - node_id: deletion_node_id, - block_sequence: deletion_sequence.get_sequence(None, None).unwrap(), - sequence_start: 0, - sequence_end: 0, - path_start: 0, - path_end: 1, - strand: Strand::Forward, - }; - let region = ResolvedGenRegion::from_path(&conn, block_group_id, &path, 0, 1).unwrap(); - let change = BlockGroupChange { - region, - path_accession: None, - block: deletion, - chromosome_index: 1, - phased: 0, - preserve_edge: true, - }; - BlockGroup::insert_change(&conn, &change).unwrap(); - - let all_sequences = BlockGroup::get_all_sequences(&conn, &block_group_id, false).unwrap(); - assert_eq!( - all_sequences, - HashSet::from_iter(vec![ - "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), - "AAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), - ]) - ); - } - - #[test] - fn delete_at_end_of_path() { - let conn = get_connection(None).unwrap(); - let (block_group_id, path) = setup_block_group(&conn); - let deletion_sequence = Sequence::new() - .sequence_type("DNA") - .sequence("") - .save(&conn) - .unwrap(); - let deletion_node_id = - Node::create(&conn, &deletion_sequence.hash, &HashId::convert_str("1")).unwrap(); - let deletion = PathBlock { - node_id: deletion_node_id, - block_sequence: deletion_sequence.get_sequence(None, None).unwrap(), - sequence_start: 0, - sequence_end: 0, - path_start: 35, - path_end: 40, - strand: Strand::Forward, - }; - let region = ResolvedGenRegion::from_path(&conn, block_group_id, &path, 35, 40).unwrap(); - let change = BlockGroupChange { - region, - path_accession: None, - block: deletion, - chromosome_index: 1, - phased: 0, - preserve_edge: true, - }; - BlockGroup::insert_change(&conn, &change).unwrap(); - - let all_sequences = BlockGroup::get_all_sequences(&conn, &block_group_id, false).unwrap(); - assert_eq!( - all_sequences, - HashSet::from_iter(vec![ - "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGG".to_string(), - "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), - ]) - ); - } - - #[test] - fn error_on_out_of_bounds_change() { - let conn = get_connection(None).unwrap(); - let (block_group_id, path) = setup_block_group(&conn); - let deletion_sequence = Sequence::new() - .sequence_type("DNA") - .sequence("") - .save(&conn) - .unwrap(); - let deletion_node_id = - Node::create(&conn, &deletion_sequence.hash, &HashId::convert_str("1")).unwrap(); - let deletion = PathBlock { - node_id: deletion_node_id, - block_sequence: deletion_sequence.get_sequence(None, None).unwrap(), - sequence_start: 0, - sequence_end: 0, - path_start: 350, - path_end: 400, - strand: Strand::Forward, - }; - let after_end_region = - ResolvedGenRegion::from_path(&conn, block_group_id, &path, 350, 400).unwrap(); - let after_end_change = BlockGroupChange { - region: after_end_region, - path_accession: None, - block: deletion.clone(), - chromosome_index: 1, - phased: 0, - preserve_edge: true, - }; - let before_start_region = - ResolvedGenRegion::from_path(&conn, block_group_id, &path, -300, 400).unwrap(); - let before_start_change = BlockGroupChange { - region: before_start_region, - path_accession: None, - block: deletion, - chromosome_index: 1, - phased: 0, - preserve_edge: true, - }; - let res = BlockGroup::insert_change(&conn, &after_end_change); - assert!(matches!(res, Err(BlockGroupError::ChangeOutOfBounds(_)))); - let res = BlockGroup::insert_change(&conn, &before_start_change); - assert!(matches!(res, Err(BlockGroupError::ChangeOutOfBounds(_)))); - } - - #[test] - fn deletion_starting_at_block_boundary() { - let conn = get_connection(None).unwrap(); - let (block_group_id, path) = setup_block_group(&conn); - let deletion_sequence = Sequence::new() - .sequence_type("DNA") - .sequence("") - .save(&conn) - .unwrap(); - let deletion_node_id = - Node::create(&conn, &deletion_sequence.hash, &HashId::convert_str("1")).unwrap(); - let deletion = PathBlock { - node_id: deletion_node_id, - block_sequence: deletion_sequence.get_sequence(None, None).unwrap(), - sequence_start: 0, - sequence_end: 0, - path_start: 10, - path_end: 12, - strand: Strand::Forward, - }; - let region = ResolvedGenRegion::from_path(&conn, block_group_id, &path, 10, 12).unwrap(); - let change = BlockGroupChange { - region, - path_accession: None, - block: deletion, - chromosome_index: 1, - phased: 0, - preserve_edge: true, - }; - BlockGroup::insert_change(&conn, &change).unwrap(); - - let all_sequences = BlockGroup::get_all_sequences(&conn, &block_group_id, false).unwrap(); - assert_eq!( - all_sequences, - HashSet::from_iter(vec![ - "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), - "AAAAAAAAAATTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), - ]) - ); - } - - #[test] - fn deletion_ending_at_block_boundary() { - let conn = get_connection(None).unwrap(); - let (block_group_id, path) = setup_block_group(&conn); - let deletion_sequence = Sequence::new() - .sequence_type("DNA") - .sequence("") - .save(&conn) - .unwrap(); - let deletion_node_id = - Node::create(&conn, &deletion_sequence.hash, &HashId::convert_str("1")).unwrap(); - let deletion = PathBlock { - node_id: deletion_node_id, - block_sequence: deletion_sequence.get_sequence(None, None).unwrap(), - sequence_start: 0, - sequence_end: 0, - path_start: 18, - path_end: 20, - strand: Strand::Forward, - }; - let region = ResolvedGenRegion::from_path(&conn, block_group_id, &path, 18, 20).unwrap(); - let change = BlockGroupChange { - region, - path_accession: None, - block: deletion, - chromosome_index: 1, - phased: 0, - preserve_edge: true, - }; - BlockGroup::insert_change(&conn, &change).unwrap(); - - let all_sequences = BlockGroup::get_all_sequences(&conn, &block_group_id, false).unwrap(); - assert_eq!( - all_sequences, - HashSet::from_iter(vec![ - "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), - "AAAAAAAAAATTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), - ]) - ); - } - - #[test] - fn test_blockgroup_interval_tree() { - let conn = &get_connection(None).unwrap(); - let (block_group_id, _path) = setup_block_group(conn); - let _new_sample = Sample::get_or_create( - conn, - NewSample { - name: "child", - ..Default::default() - }, - ) - .unwrap(); - let new_bg_id = get_single_bg_id(conn, "test", "child", "chr1", vec!["test".to_string()]); - let _new_path = Path::query( - conn, - "select * from paths where block_group_id = ?1", - params![new_bg_id], - ); - let insert_sequence = Sequence::new() - .sequence_type("DNA") - .sequence("NNNN") - .save(conn) - .unwrap(); - let insert_node_id = Node::create( - conn, - &insert_sequence.hash, - &HashId::convert_str("insert-node"), - ) - .unwrap(); - let insert = PathBlock { - node_id: insert_node_id, - block_sequence: insert_sequence.get_sequence(0, 4).unwrap(), - sequence_start: 0, - sequence_end: 4, - path_start: 7, - path_end: 15, - strand: Strand::Forward, - }; - let bg = BlockGroup::get_by_id(conn, &new_bg_id, None).unwrap(); - let region = ResolvedGenRegion { - block_group: bg, - path: None, - accession: None, - annotation: None, - kind: ResolvedRegionKind::BlockGroup, - anchor_start: 0, - anchor_end: 0, - feature_length: 0, - start: 7, - end: 15, - start_anchors: None, - end_anchors: None, - remove_ambiguous_positions: true, - }; - let change = BlockGroupChange { - region, - path_accession: None, - block: insert, - chromosome_index: 1, - phased: 0, - preserve_edge: true, - }; - BlockGroup::insert_change(conn, &change).unwrap(); - - let tree = BlockGroup::intervaltree_for(conn, &block_group_id, false).unwrap(); - let tree2 = BlockGroup::intervaltree_for(conn, &block_group_id, true).unwrap(); - interval_tree_verify( - &tree, - 3, - &[NodeIntervalBlock { - node_id: HashId::convert_str("test-a-node"), - start: 0, - end: 10, - sequence_start: 0, - sequence_end: 10, - strand: Strand::Forward, - }], - ); - interval_tree_verify( - &tree2, - 3, - &[NodeIntervalBlock { - node_id: HashId::convert_str("test-a-node"), - start: 0, - end: 10, - sequence_start: 0, - sequence_end: 10, - strand: Strand::Forward, - }], - ); - interval_tree_verify( - &tree, - 35, - &[NodeIntervalBlock { - node_id: HashId::convert_str("test-g-node"), - start: 30, - end: 40, - sequence_start: 0, - sequence_end: 10, - strand: Strand::Forward, - }], - ); - interval_tree_verify( - &tree2, - 35, - &[NodeIntervalBlock { - node_id: HashId::convert_str("test-g-node"), - start: 30, - end: 40, - sequence_start: 0, - sequence_end: 10, - strand: Strand::Forward, - }], - ); - - // This blockgroup has a change from positions 7-15 of 4 base pairs -- so any changes after this will be ambiguous - let tree = BlockGroup::intervaltree_for(conn, &new_bg_id, false).unwrap(); - let tree2 = BlockGroup::intervaltree_for(conn, &new_bg_id, true).unwrap(); - interval_tree_verify( - &tree, - 3, - &[NodeIntervalBlock { - node_id: HashId::convert_str("test-a-node"), - start: 0, - end: 7, - sequence_start: 0, - sequence_end: 7, - strand: Strand::Forward, - }], - ); - interval_tree_verify( - &tree2, - 3, - &[NodeIntervalBlock { - node_id: HashId::convert_str("test-a-node"), - start: 0, - end: 7, - sequence_start: 0, - sequence_end: 7, - strand: Strand::Forward, - }], - ); - interval_tree_verify( - &tree, - 30, - &[ - NodeIntervalBlock { - node_id: HashId::convert_str("test-g-node"), - start: 26, - end: 36, - sequence_start: 0, - sequence_end: 10, - strand: Strand::Forward, - }, - NodeIntervalBlock { - node_id: HashId::convert_str("test-g-node"), - start: 30, - end: 40, - sequence_start: 0, - sequence_end: 10, - strand: Strand::Forward, - }, - ], - ); - interval_tree_verify(&tree2, 30, &[]); - // TODO: This case should return [] because there are 2 distinct nodes at this position and thus it is ambiguous. - // currently, the caller needs to filter these out. - interval_tree_verify( - &tree2, - 9, - &[ - NodeIntervalBlock { - node_id: HashId::convert_str("insert-node"), - start: 7, - end: 11, - sequence_start: 0, - sequence_end: 4, - strand: Strand::Forward, - }, - NodeIntervalBlock { - node_id: HashId::convert_str("test-a-node"), - start: 7, - end: 10, - sequence_start: 7, - sequence_end: 10, - strand: Strand::Forward, - }, - ], - ); - } - - #[test] - fn test_changes_against_derivative_blockgroups() { - let conn = &get_connection(None).unwrap(); - let (_block_group_id, _path) = setup_block_group(conn); - let _new_sample = Sample::get_or_create( - conn, - NewSample { - name: "child", - ..Default::default() - }, - ) - .unwrap(); - let new_bg_id = get_single_bg_id(conn, "test", "child", "chr1", vec!["test".to_string()]); - let new_path = Path::query( - conn, - "select * from paths where block_group_id = ?1", - rusqlite::params!(SQLValue::from(new_bg_id)), - ); - let insert_sequence = Sequence::new() - .sequence_type("DNA") - .sequence("NNNN") - .save(conn) - .unwrap(); - let insert_node_id = - Node::create(conn, &insert_sequence.hash, &HashId::convert_str("1")).unwrap(); - let insert = PathBlock { - node_id: insert_node_id, - block_sequence: insert_sequence.get_sequence(0, 4).unwrap(), - sequence_start: 0, - sequence_end: 4, - path_start: 7, - path_end: 15, - strand: Strand::Forward, - }; - let region = ResolvedGenRegion::from_path(conn, new_bg_id, &new_path[0], 7, 15).unwrap(); - let change = BlockGroupChange { - region, - path_accession: None, - block: insert, - chromosome_index: 1, - phased: 0, - preserve_edge: false, - }; - - // note we are making our change against the new blockgroup, and not the parent blockgroup - BlockGroup::insert_change(conn, &change).unwrap(); - let all_sequences = BlockGroup::get_all_sequences(conn, &new_bg_id, true).unwrap(); - assert_eq!( - all_sequences, - HashSet::from_iter(vec!["AAAAAAANNNNTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(),]) - ); - - // Now, we make a change against another descendant - let _new_sample = Sample::get_or_create( - conn, - NewSample { - name: "grandchild", - ..Default::default() - }, - ) - .unwrap(); - let gc_bg_id = get_single_bg_id( - conn, - "test", - "grandchild", - "chr1", - vec!["child".to_string()], - ); - let _new_path = Path::query( - conn, - "select * from paths where block_group_id = ?1", - rusqlite::params!(SQLValue::from(gc_bg_id)), - ); - - let insert = PathBlock { - node_id: insert_node_id, - block_sequence: insert_sequence.get_sequence(0, 4).unwrap(), - sequence_start: 0, - sequence_end: 4, - path_start: 7, - path_end: 15, - strand: Strand::Forward, - }; - let gc_bg = BlockGroup::get_by_id(conn, &gc_bg_id, None).unwrap(); - let gc_region = ResolvedGenRegion { - block_group: gc_bg, - path: None, - accession: None, - annotation: None, - kind: ResolvedRegionKind::BlockGroup, - anchor_start: 0, - anchor_end: 0, - feature_length: 0, - start: 7, - end: 15, - start_anchors: None, - end_anchors: None, - remove_ambiguous_positions: true, - }; - let change = BlockGroupChange { - region: gc_region, - path_accession: None, - block: insert, - chromosome_index: 1, - phased: 0, - preserve_edge: false, - }; - BlockGroup::insert_change(conn, &change).unwrap(); - let all_sequences = BlockGroup::get_all_sequences(conn, &gc_bg_id, true).unwrap(); - assert_eq!( - all_sequences, - HashSet::from_iter(vec!["AAAAAAANNNNTCCCCCCCCCCGGGGGGGGGG".to_string(),]) - ); - } - - #[test] - fn test_changes_against_derivative_diploid_blockgroups() { - // This test ensures that if we have heterozygous changes that do not introduce frameshifts, - // we can modify regions downstream of them. - let conn = &get_connection(None).unwrap(); - let (_block_group_id, _path) = setup_block_group(conn); - let _new_sample = Sample::get_or_create( - conn, - NewSample { - name: "child", - ..Default::default() - }, - ) - .unwrap(); - let new_bg_id = get_single_bg_id(conn, "test", "child", "chr1", vec!["test".to_string()]); - let _new_path = Path::query( - conn, - "select * from paths where block_group_id = ?1", - params![new_bg_id], - ); - let insert_sequence = Sequence::new() - .sequence_type("DNA") - .sequence("NNNN") - .save(conn) - .unwrap(); - let insert_node_id = - Node::create(conn, &insert_sequence.hash, &HashId::convert_str("1")).unwrap(); - let insert = PathBlock { - node_id: insert_node_id, - block_sequence: insert_sequence.get_sequence(0, 4).unwrap(), - sequence_start: 0, - sequence_end: 4, - path_start: 7, - path_end: 11, - strand: Strand::Forward, - }; - let bg = BlockGroup::get_by_id(conn, &new_bg_id, None).unwrap(); - let region = ResolvedGenRegion { - block_group: bg, - path: None, - accession: None, - annotation: None, - kind: ResolvedRegionKind::BlockGroup, - anchor_start: 0, - anchor_end: 0, - feature_length: 0, - start: 7, - end: 11, - start_anchors: None, - end_anchors: None, - remove_ambiguous_positions: true, - }; - let change = BlockGroupChange { - region, - path_accession: None, - block: insert, - chromosome_index: 1, - phased: 0, - preserve_edge: true, - }; - BlockGroup::insert_change(conn, &change).unwrap(); - let all_sequences = BlockGroup::get_all_sequences(conn, &new_bg_id, true).unwrap(); - assert_eq!( - all_sequences, - HashSet::from_iter(vec![ - "AAAAAAANNNNTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), - "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), - ]) - ); - - // Now, we make a change against another descendant - let _new_sample = Sample::get_or_create( - conn, - NewSample { - name: "grandchild", - ..Default::default() - }, - ) - .unwrap(); - let gc_bg_id = get_single_bg_id( - conn, - "test", - "grandchild", - "chr1", - vec!["child".to_string()], - ); - let _new_path = Path::query( - conn, - "select * from paths where block_group_id = ?1", - params![gc_bg_id], - ); - - let insert_sequence = Sequence::new() - .sequence_type("DNA") - .sequence("NNNN") - .save(conn) - .unwrap(); - let insert_node_id = Node::create( - conn, - &insert_sequence.hash, - &HashId::convert_str("new-hash"), - ) - .unwrap(); - - let insert = PathBlock { - node_id: insert_node_id, - block_sequence: insert_sequence.get_sequence(0, 4).unwrap(), - sequence_start: 0, - sequence_end: 4, - path_start: 20, - path_end: 24, - strand: Strand::Forward, - }; - let gc_bg = BlockGroup::get_by_id(conn, &gc_bg_id, None).unwrap(); - let gc_region = ResolvedGenRegion { - block_group: gc_bg, - path: None, - accession: None, - annotation: None, - kind: ResolvedRegionKind::BlockGroup, - anchor_start: 0, - anchor_end: 0, - feature_length: 0, - start: 20, - end: 24, - start_anchors: None, - end_anchors: None, - remove_ambiguous_positions: true, - }; - let change = BlockGroupChange { - region: gc_region, - path_accession: None, - block: insert, - chromosome_index: 1, - phased: 0, - preserve_edge: true, - }; - BlockGroup::insert_change(conn, &change).unwrap(); - let all_sequences = BlockGroup::get_all_sequences(conn, &gc_bg_id, true).unwrap(); - assert_eq!( - all_sequences, - HashSet::from_iter(vec![ - "AAAAAAANNNNTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), - "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), - "AAAAAAAAAATTTTTTTTTTNNNNCCCCCCGGGGGGGGGG".to_string(), - "AAAAAAANNNNTTTTTTTTTNNNNCCCCCCGGGGGGGGGG".to_string() - ]) - ); - } - - #[test] - #[should_panic] - fn test_prohibits_out_of_frame_changes_against_derivative_diploid_blockgroups() { - // This test ensures that we do not allow ambiguous changes by coordinates - let conn = &get_connection(None).unwrap(); - let (_block_group_id, _path) = setup_block_group(conn); - let _new_sample = Sample::get_or_create( - conn, - NewSample { - name: "child", - ..Default::default() - }, - ) - .unwrap(); - let new_bg_id = get_single_bg_id(conn, "test", "child", "chr1", vec!["test".to_string()]); - let _new_path = Path::query( - conn, - "select * from paths where block_group_id = ?1", - rusqlite::params!(SQLValue::from(new_bg_id)), - ); - // This is a heterozygous replacement of 5 bases with 4 bases, so positions - // downstream of this are not addressable. - let insert_sequence = Sequence::new() - .sequence_type("DNA") - .sequence("NNNN") - .save(conn) - .unwrap(); - let insert_node_id = - Node::create(conn, &insert_sequence.hash, &HashId::convert_str("1")).unwrap(); - let insert = PathBlock { - node_id: insert_node_id, - block_sequence: insert_sequence.get_sequence(0, 4).unwrap(), - sequence_start: 0, - sequence_end: 4, - path_start: 7, - path_end: 12, - strand: Strand::Forward, - }; - let bg = BlockGroup::get_by_id(conn, &new_bg_id, None).unwrap(); - let region = ResolvedGenRegion { - block_group: bg, - path: None, - accession: None, - annotation: None, - kind: ResolvedRegionKind::BlockGroup, - anchor_start: 0, - anchor_end: 0, - feature_length: 0, - start: 7, - end: 12, - start_anchors: None, - end_anchors: None, - remove_ambiguous_positions: true, - }; - let change = BlockGroupChange { - region, - path_accession: None, - block: insert, - chromosome_index: 1, - phased: 0, - preserve_edge: true, - }; - - // note we are making our change against the new blockgroup, and not the parent blockgroup - BlockGroup::insert_change(conn, &change).unwrap(); - let all_sequences = BlockGroup::get_all_sequences(conn, &new_bg_id, true).unwrap(); - assert_eq!( - all_sequences, - HashSet::from_iter(vec![ - "AAAAAAANNNNTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), - "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), - ]) - ); - - // Now, we make a change against another descendant and get an error - let _new_sample = Sample::get_or_create( - conn, - NewSample { - name: "grandchild", - ..Default::default() - }, - ) - .unwrap(); - let gc_bg_id = get_single_bg_id( - conn, - "test", - "grandchild", - "chr1", - vec!["child".to_string()], - ); - let _new_path = Path::query( - conn, - "select * from paths where block_group_id = ?1", - rusqlite::params!(SQLValue::from(gc_bg_id)), - ); - - let insert_sequence = Sequence::new() - .sequence_type("DNA") - .sequence("NNNN") - .save(conn) - .unwrap(); - let insert_node_id = - Node::create(conn, &insert_sequence.hash, &HashId::pad_str("new-hash")).unwrap(); - - let insert = PathBlock { - node_id: insert_node_id, - block_sequence: insert_sequence.get_sequence(0, 4).unwrap(), - sequence_start: 0, - sequence_end: 4, - path_start: 20, - path_end: 24, - strand: Strand::Forward, - }; - let gc_bg = BlockGroup::get_by_id(conn, &gc_bg_id, None).unwrap(); - let gc_region = ResolvedGenRegion { - block_group: gc_bg, - path: None, - accession: None, - annotation: None, - kind: ResolvedRegionKind::BlockGroup, - anchor_start: 0, - anchor_end: 0, - feature_length: 0, - start: 20, - end: 24, - start_anchors: None, - end_anchors: None, - remove_ambiguous_positions: true, - }; - let change = BlockGroupChange { - region: gc_region, - path_accession: None, - block: insert, - chromosome_index: 1, - phased: 0, - preserve_edge: true, - }; - BlockGroup::insert_change(conn, &change).unwrap(); - } - - mod test_derive_subgraph { - - use super::*; - use crate::{ - node::Node, - sequence::Sequence, - test_helpers::{get_connection, setup_block_group}, - }; - - #[test] - fn test_derive_subgraph_one_insertion() { - /* - AAAAAAAAAA -> TTTTTTTTTT -> CCCCCCCCCC -> GGGGGGGGGG - \-> AAAAAAAA ->/ - Subgraph range: |-----------------| - Sequences of the subgraph are TAAAAAAAAC, TTTTTCCCCC - */ - let conn = &get_connection(None).unwrap(); - let (block_group1_id, original_path) = setup_block_group(conn); - - let intervaltree = original_path.intervaltree(conn).unwrap(); - let insert_start_node_id = intervaltree.query_point(16).next().unwrap().value.node_id; - let insert_end_node_id = intervaltree.query_point(24).next().unwrap().value.node_id; - - let insert_sequence = Sequence::new() - .sequence_type("DNA") - .sequence("AAAAAAAA") - .save(conn) - .unwrap(); - let insert_node_id = Node::create( - conn, - &insert_sequence.hash, - &HashId(calculate_hash(&format!( - "test-insert-a-node.{}", - insert_sequence.hash - ))), - ) - .unwrap(); - let edge_into_insert = Edge::create( - conn, - insert_start_node_id, - 6, - Strand::Forward, - insert_node_id, - 0, - Strand::Forward, - ) - .unwrap(); - let edge_out_of_insert = Edge::create( - conn, - insert_node_id, - 8, - Strand::Forward, - insert_end_node_id, - 4, - Strand::Forward, - ) - .unwrap(); - let ref_heal_1 = Edge::create( - conn, - insert_start_node_id, - 6, - Strand::Forward, - insert_start_node_id, - 6, - Strand::Forward, - ) - .unwrap(); - let ref_heal_2 = Edge::create( - conn, - insert_end_node_id, - 4, - Strand::Forward, - insert_end_node_id, - 4, - Strand::Forward, - ) - .unwrap(); - - let edge_ids = [ - &edge_into_insert.id, - &edge_out_of_insert.id, - &ref_heal_1.id, - &ref_heal_2.id, - ]; - let block_group_edges = edge_ids - .iter() - .enumerate() - .map(|(i, edge_id)| BlockGroupEdgeData { - block_group_id: block_group1_id, - edge_id: *(*edge_id), - chromosome_index: if i < 2 { 1 } else { 0 }, - phased: 0, - }) - .collect::>(); - BlockGroupEdge::bulk_create(conn, &block_group_edges); - - let insert_path = original_path - .new_path_with(conn, 16, 24, &edge_into_insert, &edge_out_of_insert) - .unwrap(); - assert_eq!( - insert_path.sequence(conn, None).unwrap(), - "AAAAAAAAAATTTTTTAAAAAAAACCCCCCGGGGGGGGGG" - ); - - let all_sequences = - BlockGroup::get_all_sequences(conn, &block_group1_id, false).unwrap(); - assert_eq!( - all_sequences, - HashSet::from_iter(vec![ - "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), - "AAAAAAAAAATTTTTTAAAAAAAACCCCCCGGGGGGGGGG".to_string(), - ]) - ); - - let mut blocks = intervaltree - .query(Range { start: 15, end: 25 }) - .map(|x| x.value) - .collect::>(); - blocks.sort_by_key(|a| a.start); - let start_block = blocks[0]; - let start_node_coordinate = 15 - start_block.start + start_block.sequence_start; - let end_block = blocks[blocks.len() - 1]; - let end_node_coordinate = 25 - end_block.start + end_block.sequence_start; - - let block_group2 = create_bg(conn, "test", "test", "chr1.1"); - let node_count_before = Node::query(conn, "SELECT * FROM nodes", params![]).len(); - BlockGroup::derive_subgraph( - conn, - &block_group1_id, - &start_block, - &end_block, - start_node_coordinate, - end_node_coordinate, - &block_group2.id, - true, - ) - .unwrap(); - let node_count_after = Node::query(conn, "SELECT * FROM nodes", params![]).len(); - assert_eq!(node_count_after, node_count_before); - let all_sequences2 = - BlockGroup::get_all_sequences(conn, &block_group2.id, false).unwrap(); - assert_eq!( - all_sequences2, - HashSet::from_iter(vec!["TTTTTCCCCC".to_string(), "TAAAAAAAAC".to_string(),]) - ); - } - - #[test] - fn test_derive_subgraph_two_independent_insertions() { - /* - AAAAAAAAAA -> TTTTTTTTTT -> CCCCCCCCCC -----> GGGGGGGGGG - \-> AAAAAAAA ->/ \->TTTTTTTT -/ - Subgraph range: |----------------------------------| - */ - let conn = &get_connection(None).unwrap(); - let (block_group1_id, original_path) = setup_block_group(conn); - - let intervaltree = original_path.intervaltree(conn).unwrap(); - let insert_start_node_id = intervaltree.query_point(16).next().unwrap().value.node_id; - let insert_end_node_id = intervaltree.query_point(24).next().unwrap().value.node_id; - - let insert_sequence = Sequence::new() - .sequence_type("DNA") - .sequence("AAAAAAAA") - .save(conn) - .unwrap(); - let insert_node_id = Node::create( - conn, - &insert_sequence.hash, - &HashId(calculate_hash(&format!( - "test-insert-a-node.{}", - insert_sequence.hash - ))), - ) - .unwrap(); - let edge_into_insert = Edge::create( - conn, - insert_start_node_id, - 6, - Strand::Forward, - insert_node_id, - 0, - Strand::Forward, - ) - .unwrap(); - let edge_out_of_insert = Edge::create( - conn, - insert_node_id, - 8, - Strand::Forward, - insert_end_node_id, - 4, - Strand::Forward, - ) - .unwrap(); - let ref_heal_1 = Edge::create( - conn, - insert_start_node_id, - 6, - Strand::Forward, - insert_start_node_id, - 6, - Strand::Forward, - ) - .unwrap(); - let ref_heal_2 = Edge::create( - conn, - insert_end_node_id, - 4, - Strand::Forward, - insert_end_node_id, - 4, - Strand::Forward, - ) - .unwrap(); - - let edge_ids = [ - &edge_into_insert.id, - &edge_out_of_insert.id, - &ref_heal_1.id, - &ref_heal_2.id, - ]; - let block_group_edges = edge_ids - .iter() - .enumerate() - .map(|(i, edge_id)| BlockGroupEdgeData { - block_group_id: block_group1_id, - edge_id: *(*edge_id), - chromosome_index: if i < 2 { 1 } else { 0 }, - phased: 0, - }) - .collect::>(); - BlockGroupEdge::bulk_create(conn, &block_group_edges); - - let insert_path = original_path - .new_path_with(conn, 16, 24, &edge_into_insert, &edge_out_of_insert) - .unwrap(); - assert_eq!( - insert_path.sequence(conn, None).unwrap(), - "AAAAAAAAAATTTTTTAAAAAAAACCCCCCGGGGGGGGGG" - ); - - let insert2_start_node_id = intervaltree.query_point(28).next().unwrap().value.node_id; - let insert2_end_node_id = intervaltree.query_point(32).next().unwrap().value.node_id; - - let insert2_sequence = Sequence::new() - .sequence_type("DNA") - .sequence("TTTTTTTT") - .save(conn) - .unwrap(); - let insert2_node_id = Node::create( - conn, - &insert2_sequence.hash, - &HashId(calculate_hash(&format!( - "test-insert-t-node.{}", - insert2_sequence.hash - ))), - ) - .unwrap(); - let edge_into_insert2 = Edge::create( - conn, - insert2_start_node_id, - 6, - Strand::Forward, - insert2_node_id, - 0, - Strand::Forward, - ) - .unwrap(); - let edge_out_of_insert2 = Edge::create( - conn, - insert2_node_id, - 8, - Strand::Forward, - insert2_end_node_id, - 4, - Strand::Forward, - ) - .unwrap(); - let ref_heal_1 = Edge::create( - conn, - insert2_start_node_id, - 6, - Strand::Forward, - insert2_start_node_id, - 6, - Strand::Forward, - ) - .unwrap(); - let ref_heal_2 = Edge::create( - conn, - insert2_end_node_id, - 4, - Strand::Forward, - insert2_end_node_id, - 4, - Strand::Forward, - ) - .unwrap(); - - let edge_ids = [ - &edge_into_insert2.id, - &edge_out_of_insert2.id, - &ref_heal_1.id, - &ref_heal_2.id, - ]; - let block_group_edges = edge_ids - .iter() - .enumerate() - .map(|(i, edge_id)| BlockGroupEdgeData { - block_group_id: block_group1_id, - edge_id: *(*edge_id), - chromosome_index: if i < 2 { 1 } else { 0 }, - phased: 0, - }) - .collect::>(); - BlockGroupEdge::bulk_create(conn, &block_group_edges); - - let insert2_path = insert_path - .new_path_with(conn, 28, 32, &edge_into_insert2, &edge_out_of_insert2) - .unwrap(); - assert_eq!( - insert2_path.sequence(conn, None).unwrap(), - "AAAAAAAAAATTTTTTAAAAAAAACCTTTTTTTTGGGGGG" - ); - - let all_sequences = - BlockGroup::get_all_sequences(conn, &block_group1_id, false).unwrap(); - assert_eq!( - all_sequences, - HashSet::from_iter(vec![ - "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), - "AAAAAAAAAATTTTTTAAAAAAAACCCCCCGGGGGGGGGG".to_string(), - "AAAAAAAAAATTTTTTTTTTCCCCCCTTTTTTTTGGGGGG".to_string(), - "AAAAAAAAAATTTTTTAAAAAAAACCTTTTTTTTGGGGGG".to_string(), - ]) - ); - - let mut blocks = intervaltree - .query(Range { start: 15, end: 36 }) - .map(|x| x.value) - .collect::>(); - blocks.sort_by_key(|a| a.start); - let start_block = blocks[0]; - let start_node_coordinate = 15 - start_block.start + start_block.sequence_start; - let end_block = blocks[blocks.len() - 1]; - let end_node_coordinate = 36 - end_block.start + end_block.sequence_start; - - let block_group2 = create_bg(conn, "test", "test", "chr1.1"); - BlockGroup::derive_subgraph( - conn, - &block_group1_id, - &start_block, - &end_block, - start_node_coordinate, - end_node_coordinate, - &block_group2.id, - true, - ) - .unwrap(); - let all_sequences2 = - BlockGroup::get_all_sequences(conn, &block_group2.id, false).unwrap(); - assert_eq!( - all_sequences2, - HashSet::from_iter(vec![ - "TTTTTCCCCCCCCCCGGGGGG".to_string(), - "TAAAAAAAACCCCCCGGGGGG".to_string(), - "TTTTTCCCCCCTTTTTTTTGG".to_string(), - "TAAAAAAAACCTTTTTTTTGG".to_string(), - ]) - ); - } - - #[test] - fn test_derive_subgraph_two_independent_insertions_and_one_deletion() { - /* - /--------------------------------------------\ (<-- Deletion edge) - AAAAAAAAAA -> TTTTTTTTTT -> CCCCCCCCCC -----> GGGGGGGGGG - \-> AAAAAAAA ->/ \->TTTTTTTT -/ - Subgraph range: |----------------------------------| - - Confirms that deletion edge is ignored and not added to subgraph - */ - let conn = &get_connection(None).unwrap(); - let (block_group1_id, original_path) = setup_block_group(conn); - - let intervaltree = original_path.intervaltree(conn).unwrap(); - let insert_start_node_id = intervaltree.query_point(16).next().unwrap().value.node_id; - let insert_end_node_id = intervaltree.query_point(24).next().unwrap().value.node_id; - - let insert_sequence = Sequence::new() - .sequence_type("DNA") - .sequence("AAAAAAAA") - .save(conn) - .unwrap(); - let insert_node_id = Node::create( - conn, - &insert_sequence.hash, - &HashId(calculate_hash(&format!( - "test-insert-a-node.{}", - insert_sequence.hash - ))), - ) - .unwrap(); - let edge_into_insert = Edge::create( - conn, - insert_start_node_id, - 6, - Strand::Forward, - insert_node_id, - 0, - Strand::Forward, - ) - .unwrap(); - let edge_out_of_insert = Edge::create( - conn, - insert_node_id, - 8, - Strand::Forward, - insert_end_node_id, - 4, - Strand::Forward, - ) - .unwrap(); - let ref_heal_1 = Edge::create( - conn, - insert_start_node_id, - 6, - Strand::Forward, - insert_start_node_id, - 6, - Strand::Forward, - ) - .unwrap(); - let ref_heal_2 = Edge::create( - conn, - insert_end_node_id, - 4, - Strand::Forward, - insert_end_node_id, - 4, - Strand::Forward, - ) - .unwrap(); - - let edge_ids = [ - &edge_into_insert.id, - &edge_out_of_insert.id, - &ref_heal_1.id, - &ref_heal_2.id, - ]; - let block_group_edges = edge_ids - .iter() - .map(|edge_id| BlockGroupEdgeData { - block_group_id: block_group1_id, - edge_id: *(*edge_id), - chromosome_index: NO_CHROMOSOME_INDEX, - phased: 0, - }) - .collect::>(); - BlockGroupEdge::bulk_create(conn, &block_group_edges); - - let insert_path = original_path - .new_path_with(conn, 16, 24, &edge_into_insert, &edge_out_of_insert) - .unwrap(); - assert_eq!( - insert_path.sequence(conn, None).unwrap(), - "AAAAAAAAAATTTTTTAAAAAAAACCCCCCGGGGGGGGGG" - ); - - let insert2_start_node_id = intervaltree.query_point(28).next().unwrap().value.node_id; - let insert2_end_node_id = intervaltree.query_point(32).next().unwrap().value.node_id; - - let insert2_sequence = Sequence::new() - .sequence_type("DNA") - .sequence("TTTTTTTT") - .save(conn) - .unwrap(); - let insert2_node_id = Node::create( - conn, - &insert2_sequence.hash, - &HashId(calculate_hash(&format!( - "test-insert-t-node.{}", - insert2_sequence.hash - ))), - ) - .unwrap(); - let edge_into_insert2 = Edge::create( - conn, - insert2_start_node_id, - 6, - Strand::Forward, - insert2_node_id, - 0, - Strand::Forward, - ) - .unwrap(); - let edge_out_of_insert2 = Edge::create( - conn, - insert2_node_id, - 8, - Strand::Forward, - insert2_end_node_id, - 4, - Strand::Forward, - ) - .unwrap(); - let ref_heal_1 = Edge::create( - conn, - insert2_start_node_id, - 6, - Strand::Forward, - insert2_start_node_id, - 6, - Strand::Forward, - ) - .unwrap(); - let ref_heal_2 = Edge::create( - conn, - insert2_end_node_id, - 4, - Strand::Forward, - insert2_end_node_id, - 4, - Strand::Forward, - ) - .unwrap(); - - let edge_ids = [ - &edge_into_insert2.id, - &edge_out_of_insert2.id, - &ref_heal_1.id, - &ref_heal_2.id, - ]; - let block_group_edges = edge_ids - .iter() - .map(|edge_id| BlockGroupEdgeData { - block_group_id: block_group1_id, - edge_id: *(*edge_id), - chromosome_index: NO_CHROMOSOME_INDEX, - phased: 0, - }) - .collect::>(); - BlockGroupEdge::bulk_create(conn, &block_group_edges); - - let insert2_path = insert_path - .new_path_with(conn, 28, 32, &edge_into_insert2, &edge_out_of_insert2) - .unwrap(); - assert_eq!( - insert2_path.sequence(conn, None).unwrap(), - "AAAAAAAAAATTTTTTAAAAAAAACCTTTTTTTTGGGGGG" - ); - - let deletion_end_node_id = intervaltree.query_point(38).next().unwrap().value.node_id; - let deletion_edge = Edge::create( - conn, - insert_node_id, - 8, - Strand::Forward, - deletion_end_node_id, - 8, - Strand::Forward, - ) - .unwrap(); - let ref_heal_1 = Edge::create( - conn, - insert_node_id, - 8, - Strand::Forward, - insert_node_id, - 8, - Strand::Forward, - ) - .unwrap(); - let ref_heal_2 = Edge::create( - conn, - deletion_end_node_id, - 8, - Strand::Forward, - deletion_end_node_id, - 8, - Strand::Forward, - ) - .unwrap(); - let block_group_edges = [ - BlockGroupEdgeData { - block_group_id: block_group1_id, - edge_id: deletion_edge.id, - chromosome_index: NO_CHROMOSOME_INDEX, - phased: 0, - }, - BlockGroupEdgeData { - block_group_id: block_group1_id, - edge_id: ref_heal_1.id, - chromosome_index: NO_CHROMOSOME_INDEX, - phased: 0, - }, - BlockGroupEdgeData { - block_group_id: block_group1_id, - edge_id: ref_heal_2.id, - chromosome_index: NO_CHROMOSOME_INDEX, - phased: 0, - }, - ]; - BlockGroupEdge::bulk_create(conn, &block_group_edges); - - let all_sequences = - BlockGroup::get_all_sequences(conn, &block_group1_id, false).unwrap(); - assert_eq!( - all_sequences, - HashSet::from_iter(vec![ - "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), - "AAAAAAAAAATTTTTTAAAAAAAACCCCCCGGGGGGGGGG".to_string(), - "AAAAAAAAAATTTTTTTTTTCCCCCCTTTTTTTTGGGGGG".to_string(), - "AAAAAAAAAATTTTTTAAAAAAAACCTTTTTTTTGGGGGG".to_string(), - "AAAAAAAAAATTTTTTAAAAAAAAGG".to_string(), // Sequence including deletion - ]) - ); - - let mut blocks = intervaltree - .query(Range { start: 15, end: 36 }) - .map(|x| x.value) - .collect::>(); - blocks.sort_by_key(|a| a.start); - let start_block = blocks[0]; - let start_node_coordinate = 15 - start_block.start + start_block.sequence_start; - let end_block = blocks[blocks.len() - 1]; - let end_node_coordinate = 36 - end_block.start + end_block.sequence_start; - - let block_group2 = create_bg(conn, "test", "test", "chr1.1"); - BlockGroup::derive_subgraph( - conn, - &block_group1_id, - &start_block, - &end_block, - start_node_coordinate, - end_node_coordinate, - &block_group2.id, - true, - ) - .unwrap(); - let all_sequences2 = - BlockGroup::get_all_sequences(conn, &block_group2.id, false).unwrap(); - assert_eq!( - all_sequences2, - // The deletion is not included in the cloned subgraph since one end of it is - // outside the specified range - HashSet::from_iter(vec![ - "TTTTTCCCCCCCCCCGGGGGG".to_string(), - "TAAAAAAAACCCCCCGGGGGG".to_string(), - "TTTTTCCCCCCTTTTTTTTGG".to_string(), - "TAAAAAAAACCTTTTTTTTGG".to_string(), - ]) - ); - } - } } diff --git a/gen-models/src/edge.rs b/gen-models/src/edge.rs index c1686bf5..8931d15e 100644 --- a/gen-models/src/edge.rs +++ b/gen-models/src/edge.rs @@ -5,10 +5,9 @@ use std::{ }; use gen_core::{ - HashId, PATH_END_NODE_ID, PATH_START_NODE_ID, Strand, calculate_hash, is_terminal, - traits::Capnp, + GraphLoadBlock, GraphLoadEdge, HashId, PATH_END_NODE_ID, PATH_START_NODE_ID, Strand, + calculate_hash, is_terminal, traits::Capnp, }; -use gen_graph::{GenGraph, GraphEdge, GraphNode}; use indexmap::IndexSet; use itertools::Itertools; use rusqlite::{Row, ToSql, params, types::Value}; @@ -126,12 +125,6 @@ impl From<&Edge> for EdgeData { } } -#[derive(Eq, Hash, PartialEq)] -pub struct BlockKey { - pub node_id: HashId, - pub coordinate: i64, -} - #[derive(Clone, Debug)] pub struct GroupBlock { pub id: i64, @@ -411,6 +404,40 @@ impl Edge { Ok(edges) } + /// Loads the edges touching a node and the edges touching those immediate neighbors. + /// + /// Graph expansion uses this two-hop record set to add complete local connections without + /// putting database queries in the graph traversal layer. + pub fn edges_for_block_group_node_neighborhood( + conn: &GraphConnection, + block_group_id: &HashId, + node_id: HashId, + history_ref: Option<&str>, + ) -> Result, EdgeError> { + let immediate_edges = + Self::edges_for_block_group_nodes(conn, block_group_id, &[node_id], history_ref)?; + let mut neighbor_ids = immediate_edges + .iter() + .flat_map(|edge| [edge.edge.source_node_id, edge.edge.target_node_id]) + .filter(|neighbor_id| *neighbor_id != node_id) + .collect::>(); + neighbor_ids.sort(); + neighbor_ids.dedup(); + + let mut edges_by_id = immediate_edges + .into_iter() + .map(|edge| (edge.edge.id, edge)) + .collect::>(); + for edge in + Self::edges_for_block_group_nodes(conn, block_group_id, &neighbor_ids, history_ref) + .unwrap_or_default() + { + edges_by_id.entry(edge.edge.id).or_insert(edge); + } + + Ok(edges_by_id.into_values().collect()) + } + /// Converts input edge coordinates for one backing node into the sequence slices represented /// by `GroupBlock`s. /// @@ -481,10 +508,10 @@ impl Edge { /// Computes the backing-node slices from the stored block group edges. /// /// Graph construction, sequence enumeration, GFA export, and diff reconstruction use this as - /// the first half of the edge-to-`GenGraph` pipeline, the second half being the build_graph - /// method below. This method gathers coordinates, expands incomplete node descriptions from - /// the block group, and calls `get_block_intervals` to construct both blocks with sequences - /// and zero-width junction blocks. + /// the first half of the edge-to-`GenGraph` pipeline, followed by conversion to graph load + /// records and construction in `gen-graph`. This method gathers coordinates, expands + /// incomplete node descriptions from the block group, and calls `get_block_intervals` to + /// construct both blocks with sequences and zero-width junction blocks. /// /// For example, two adjacent deletions retain each original base while also exposing the path /// that skips both. Parenthesized nodes are zero-width junctions and bracketed nodes contain @@ -687,122 +714,10 @@ impl Edge { Ok(blocks) } - /// Checks whether both endpoints use the same node and coordinate. - /// - /// `block_connections` uses this structural property when connecting blocks around a junction. - /// Whether the edge is a real path choice or a reference-healing edit-site marker remains - /// separate chromosome-index metadata on `AugmentedEdge`. - fn is_same_coordinate_edge(&self) -> bool { - self.source_node_id == self.target_node_id - && self.source_coordinate == self.target_coordinate - } - - /// Selects the blocks that an edge endpoint should connect to. - /// - /// `block_connections` calls this when an input edge enters or leaves a coordinate. If a - /// junction exists, the edge connects to it and traversal continues from there. Otherwise the - /// edge connects directly to the sequence block. - fn select_junction_or_sequence_blocks<'a>(blocks: &[&'a GroupBlock]) -> Vec<&'a GroupBlock> { - let junctions = blocks - .iter() - .copied() - .filter(|block| block.start == block.end) - .collect::>(); - if junctions.is_empty() { - blocks.to_vec() - } else { - junctions - } - } - - /// Builds connections for a same-coordinate edge. - /// - /// At a junction, the source lookup contains the sequence block ending at the coordinate and - /// the junction, while the target lookup contains the junction and the sequence block starting - /// there. `block_connections` calls this to produce the deliberate `sequence -> junction` and - /// `junction -> sequence` connections without adding a junction self-loop. With no junction, - /// the Cartesian product keeps the direct connection between sequence blocks. - /// - /// ```text - /// without a junction: [sequence ending at k] ---> [sequence starting at k] - /// - /// with a junction: [sequence ending at k] ---> (k,k) ---> [sequence starting at k] - /// ``` - /// - /// The same-coordinate edge therefore preserves the same route after the junction is - /// introduced without creating `(k,k) -> (k,k)`. - fn same_coordinate_block_connections<'a>( - source_blocks: &[&'a GroupBlock], - target_blocks: &[&'a GroupBlock], - ) -> Vec<(&'a GroupBlock, &'a GroupBlock)> { - let source_junctions = source_blocks - .iter() - .copied() - .filter(|block| block.start == block.end) - .collect::>(); - let target_junctions = target_blocks - .iter() - .copied() - .filter(|block| block.start == block.end) - .collect::>(); - - if source_junctions.is_empty() && target_junctions.is_empty() { - return source_blocks - .iter() - .copied() - .cartesian_product(target_blocks.iter().copied()) - .collect(); - } - - let source_sequence_blocks = source_blocks - .iter() - .copied() - .filter(|block| block.start != block.end); - let target_sequence_blocks = target_blocks - .iter() - .copied() - .filter(|block| block.start != block.end); - - // Sequence ending at the coordinate enters the junction, and sequence starting there - // leaves it. If neither side has real sequence, there is no connection to add. - source_sequence_blocks - .cartesian_product(target_junctions) - .chain( - source_junctions - .into_iter() - .cartesian_product(target_sequence_blocks), - ) - .collect() - } - - /// Returns the in-memory block connections represented by one input edge. - /// - /// `build_graph` calls this after coordinate lookup may have returned both a sequence block and - /// a junction. Same-coordinate edges connect the surrounding sequence through that junction; - /// all other edges connect to the junction and let the next input edge continue from it. These - /// returned connections are a graph projection and are not additional database edges. - fn block_connections<'a>( - &self, - source_blocks: &[&'a GroupBlock], - target_blocks: &[&'a GroupBlock], - ) -> Vec<(&'a GroupBlock, &'a GroupBlock)> { - if self.is_same_coordinate_edge() { - return Self::same_coordinate_block_connections(source_blocks, target_blocks); - } - - let source_blocks = Self::select_junction_or_sequence_blocks(source_blocks); - let target_blocks = Self::select_junction_or_sequence_blocks(target_blocks); - source_blocks - .into_iter() - .cartesian_product(target_blocks) - .collect() - } - - /// Computes a `GenGraph` from augmented edges and their computed blocks. + /// Converts persisted edges and computed blocks into graph construction records. /// - /// `BlockGroup::get_graph`, sequence enumeration, and graph export call this after - /// `blocks_from_edges`. The returned node-pair map retains the input `Edge` responsible for - /// each projected graph edge so downstream consumers can recover edge provenance. + /// `gen_graph::models::load_block_group_graph`, sequence enumeration, and graph export call this after + /// `blocks_from_edges`, then pass the records to the graph layer for construction. /// /// Junctions and `PRESERVE_EDIT_SITE_CHROMOSOME_INDEX` have independent jobs: /// @@ -823,91 +738,35 @@ impl Edge { /// /// The junction is the generated node. The two labels represent copied chromosome-index /// metadata on the projected connections; neither connection is added to the database. - pub fn build_graph( + pub fn graph_load_data( edges: &[AugmentedEdge], blocks: &[GroupBlock], - ) -> (GenGraph, HashMap<(i64, i64), Edge>) { - let graph_node_for_block = |block: &GroupBlock| GraphNode { - node_id: block.node_id, - sequence_start: block.start, - sequence_end: block.end, - }; - // Input edge sources resolve to blocks ending at their coordinate, and targets resolve to - // blocks starting there. A junction belongs to both indexes, which lets one edge arrive at - // it and the next edge leave from it. - let blocks_by_start = blocks + ) -> (Vec, Vec) { + let load_edges = edges .iter() - .map(|block| { - ( - BlockKey { - node_id: block.node_id, - coordinate: block.start, - }, - block, - ) + .map(|augmented_edge| GraphLoadEdge { + edge_id: augmented_edge.edge.id, + source_node_id: augmented_edge.edge.source_node_id, + source_coordinate: augmented_edge.edge.source_coordinate, + source_strand: augmented_edge.edge.source_strand, + target_node_id: augmented_edge.edge.target_node_id, + target_coordinate: augmented_edge.edge.target_coordinate, + target_strand: augmented_edge.edge.target_strand, + chromosome_index: augmented_edge.chromosome_index, + phased: augmented_edge.phased, + created_on: augmented_edge.created_on, }) - .into_group_map(); - let blocks_by_end = blocks + .collect::>(); + let load_blocks = blocks .iter() - .map(|block| { - ( - BlockKey { - node_id: block.node_id, - coordinate: block.end, - }, - block, - ) + .map(|block| GraphLoadBlock { + id: block.id, + node_id: block.node_id, + start: block.start, + end: block.end, }) - .into_group_map(); - - let mut graph = GenGraph::new(); - let mut edges_by_node_pair = HashMap::new(); - for block in blocks { - graph.add_node(graph_node_for_block(block)); - } - for augmented_edge in edges { - let edge = &augmented_edge.edge; - let source_key = BlockKey { - node_id: edge.source_node_id, - coordinate: edge.source_coordinate, - }; - let source_blocks = blocks_by_end.get(&source_key); - let target_key = BlockKey { - node_id: edge.target_node_id, - coordinate: edge.target_coordinate, - }; - let target_blocks = blocks_by_start.get(&target_key); - - if let Some(source_blocks) = source_blocks - && let Some(target_blocks) = target_blocks - { - // A coordinate may match both a sequence block and a junction. When connecting - // to junctions, multiple edges may be needed to fully represent the graph. - // `block_connections` orchestrates this. - for (source_block, target_block) in - edge.block_connections(source_blocks, target_blocks) - { - let source_node = graph_node_for_block(source_block); - let target_node = graph_node_for_block(target_block); - let graph_edge = GraphEdge { - edge_id: edge.id, - source_strand: edge.source_strand, - target_strand: edge.target_strand, - chromosome_index: augmented_edge.chromosome_index, - phased: augmented_edge.phased, - created_on: augmented_edge.created_on, - }; - if let Some(existing_edges) = graph.edge_weight_mut(source_node, target_node) { - existing_edges.push(graph_edge); - } else { - graph.add_edge(source_node, target_node, vec![graph_edge]); - } - edges_by_node_pair.insert((source_block.id, target_block.id), edge.clone()); - } - } - } - - (graph, edges_by_node_pair) + .collect::>(); + (load_edges, load_blocks) } pub fn is_start_edge(&self) -> bool { @@ -922,16 +781,13 @@ impl Edge { #[cfg(test)] mod tests { // Note this useful idiom: importing names from outer (for mod tests) scope. - use gen_core::PathBlock; - use super::*; use crate::{ - block_group::{BlockGroup, BlockGroupChange}, + block_group::BlockGroup, block_group_edge::{BlockGroupEdge, BlockGroupEdgeData}, collection::Collection, - region::ResolvedGenRegion, sequence::Sequence, - test_helpers::{get_connection, setup_block_group}, + test_helpers::get_connection, }; fn get_block_boundaries( @@ -955,49 +811,6 @@ mod tests { .sorted_by(|c1, c2| Ord::cmp(&c1, &c2)) .collect::>() } - - fn group_block(id: i64, node_id: HashId, start: i64, end: i64) -> GroupBlock { - GroupBlock { - id, - node_id, - sequence: Some(String::new()), - external_sequence: None, - start, - end, - } - } - - fn augmented_edge( - id: &str, - source_node_id: HashId, - source_coordinate: i64, - target_node_id: HashId, - target_coordinate: i64, - ) -> AugmentedEdge { - AugmentedEdge { - edge: Edge { - id: HashId::convert_str(id), - source_node_id, - source_coordinate, - source_strand: Strand::Forward, - target_node_id, - target_coordinate, - target_strand: Strand::Forward, - }, - chromosome_index: 0, - phased: 0, - created_on: 0, - } - } - - fn graph_node(block: &GroupBlock) -> GraphNode { - GraphNode { - node_id: block.node_id, - sequence_start: block.start, - sequence_end: block.end, - } - } - #[test] fn test_get_block_intervals_splits_sorted_unique_coordinates() { let starts = HashSet::from([5, 0, 5]); @@ -1047,156 +860,6 @@ mod tests { )); } - #[test] - fn test_build_graph_routes_incoming_edge_to_junction() { - let source_node_id = HashId::convert_str("incoming-source"); - let target_node_id = HashId::convert_str("incoming-target"); - let blocks = vec![ - group_block(0, source_node_id, 0, 3), - group_block(1, target_node_id, 0, 0), - group_block(2, target_node_id, 0, 1), - ]; - let edges = vec![augmented_edge( - "incoming-edge", - source_node_id, - 3, - target_node_id, - 0, - )]; - - let (graph, _) = Edge::build_graph(&edges, &blocks); - - assert!( - graph.contains_edge(graph_node(&blocks[0]), graph_node(&blocks[1])), - "incoming edge should terminate at the junction" - ); - assert!( - !graph.contains_edge(graph_node(&blocks[0]), graph_node(&blocks[2])), - "incoming edge should not bypass the junction" - ); - assert_eq!(graph.edge_count(), 1, "should project one block edge"); - } - - #[test] - fn test_build_graph_routes_outgoing_edge_from_junction() { - let source_node_id = HashId::convert_str("outgoing-source"); - let target_node_id = HashId::convert_str("outgoing-target"); - let blocks = vec![ - group_block(0, source_node_id, 0, 1), - group_block(1, source_node_id, 1, 1), - group_block(2, target_node_id, 0, 2), - ]; - let edges = vec![augmented_edge( - "outgoing-edge", - source_node_id, - 1, - target_node_id, - 0, - )]; - - let (graph, _) = Edge::build_graph(&edges, &blocks); - - assert!( - graph.contains_edge(graph_node(&blocks[1]), graph_node(&blocks[2])), - "outgoing edge should originate at the junction" - ); - assert!( - !graph.contains_edge(graph_node(&blocks[0]), graph_node(&blocks[2])), - "outgoing edge should not bypass the junction" - ); - assert_eq!(graph.edge_count(), 1, "should create one block edge"); - } - - #[test] - fn test_build_graph_creates_same_coordinate_edge_from_start_junction() { - let node_id = HashId::convert_str("same-coordinate-node"); - let blocks = vec![group_block(0, node_id, 0, 0), group_block(1, node_id, 0, 1)]; - let edges = vec![augmented_edge( - "same-coordinate-edge", - node_id, - 0, - node_id, - 0, - )]; - - let (graph, _) = Edge::build_graph(&edges, &blocks); - - assert!( - graph.contains_edge(graph_node(&blocks[0]), graph_node(&blocks[1])), - "same-coordinate edge should connect the junction to adjacent sequence" - ); - assert!( - !graph.contains_edge(graph_node(&blocks[0]), graph_node(&blocks[0])), - "same-coordinate edge should not add a redundant junction self-loop" - ); - assert_eq!(graph.edge_count(), 1, "should create one block edge"); - } - - #[test] - fn test_build_graph_creates_same_coordinate_edge_into_end_junction() { - let node_id = HashId::convert_str("ending-same-coordinate-node"); - let blocks = vec![group_block(0, node_id, 0, 1), group_block(1, node_id, 1, 1)]; - let edges = vec![augmented_edge( - "ending-same-coordinate-edge", - node_id, - 1, - node_id, - 1, - )]; - - let (graph, _) = Edge::build_graph(&edges, &blocks); - - assert!( - graph.contains_edge(graph_node(&blocks[0]), graph_node(&blocks[1])), - "same-coordinate edge should connect adjacent sequence into the junction" - ); - assert!( - !graph.contains_edge(graph_node(&blocks[1]), graph_node(&blocks[1])), - "same-coordinate edge should not add a redundant junction self-loop" - ); - assert_eq!(graph.edge_count(), 1, "should create one block edge"); - } - - #[test] - fn test_build_graph_creates_same_coordinate_edge_without_junction_directly() { - let node_id = HashId::convert_str("interior-same-coordinate-node"); - let blocks = vec![group_block(0, node_id, 0, 1), group_block(1, node_id, 1, 2)]; - let edges = vec![augmented_edge( - "interior-same-coordinate-edge", - node_id, - 1, - node_id, - 1, - )]; - - let (graph, _) = Edge::build_graph(&edges, &blocks); - - assert!( - graph.contains_edge(graph_node(&blocks[0]), graph_node(&blocks[1])), - "same-coordinate edge should directly connect sequence blocks without a junction" - ); - assert_eq!(graph.edge_count(), 1, "should create one block edge"); - } - - #[test] - fn test_build_graph_omits_same_coordinate_edge_without_adjacent_sequence() { - let node_id = HashId::convert_str("isolated-junction"); - let blocks = vec![group_block(0, node_id, 0, 0)]; - let edges = vec![augmented_edge("isolated-edge", node_id, 0, node_id, 0)]; - - let (graph, edges_by_node_pair) = Edge::build_graph(&edges, &blocks); - - assert!( - !graph.contains_edge(graph_node(&blocks[0]), graph_node(&blocks[0])), - "a junction without adjacent sequence should not create a graph self-loop" - ); - assert_eq!(graph.edge_count(), 0, "should not create a block edge"); - assert!( - edges_by_node_pair.is_empty(), - "omitted self-loop should not have a block-pair mapping" - ); - } - #[test] fn test_bulk_create() { let conn = &mut get_connection(None).unwrap(); @@ -2048,68 +1711,6 @@ mod tests { assert_eq!(edge.id_hash(), created_edge.id); } - #[test] - fn test_blocks_from_edges() { - let conn = get_connection(None).unwrap(); - let (block_group_id, path) = setup_block_group(&conn); - - let edges = BlockGroupEdge::edges_for_block_group(&conn, &block_group_id, None); - let blocks = Edge::blocks_from_edges(&conn, &block_group_id, &edges, None).unwrap(); - - // 4 actual sequences: 10-length ones of all A, all T, all C, all G - // 2 terminal node blocks (start/end) - // 6 total - assert_eq!(blocks.len(), 6); - - let insert_sequence = Sequence::new() - .sequence_type("DNA") - .sequence("NNNN") - .save(&conn) - .unwrap(); - let insert_node_id = - Node::create(&conn, &insert_sequence.hash, &HashId::convert_str("1")).unwrap(); - let insert = PathBlock { - node_id: insert_node_id, - block_sequence: insert_sequence.get_sequence(0, 4).unwrap(), - sequence_start: 0, - sequence_end: 4, - path_start: 7, - path_end: 15, - strand: Strand::Forward, - }; - let region = ResolvedGenRegion::from_path(&conn, block_group_id, &path, 7, 15).unwrap(); - let change = BlockGroupChange { - region, - path_accession: None, - block: insert, - chromosome_index: 0, - phased: 0, - preserve_edge: true, - }; - BlockGroup::insert_change(&conn, &change).unwrap(); - let mut edges = BlockGroupEdge::edges_for_block_group(&conn, &block_group_id, None); - - let blocks = Edge::blocks_from_edges(&conn, &block_group_id, &edges, None).unwrap(); - - // 2 10-length sequences of all C, all G - // 1 inserted NNNN sequence - // 4 split blocks (A and T sequences were split) resulting from the inserted sequence - // 2 terminal node blocks (start/end) - // 9 total - assert_eq!(blocks.len(), 9); - - // Confirm that ordering doesn't matter - edges.reverse(); - let blocks = Edge::blocks_from_edges(&conn, &block_group_id, &edges, None).unwrap(); - - // 2 10-length sequences of all C, all G - // 1 inserted NNNN sequence - // 4 split blocks (A and T sequences were split) resulting from the inserted sequence - // 2 terminal node blocks (start/end) - // 9 total - assert_eq!(blocks.len(), 9); - } - #[test] fn test_get_block_boundaries() { let conn = get_connection(None).unwrap(); diff --git a/gen-models/src/graph.rs b/gen-models/src/graph.rs deleted file mode 100644 index 9c6866f5..00000000 --- a/gen-models/src/graph.rs +++ /dev/null @@ -1,868 +0,0 @@ -use std::collections::{HashMap, HashSet, VecDeque}; - -use gen_core::{HashId, NodeIntervalBlock, is_terminal}; -use gen_graph::{GenGraph, GraphError, GraphNode, GraphNodePosition, MergeGraph}; -use intervaltree::IntervalTree; -use petgraph::Direction; - -use crate::{ - block_group::BlockGroup, block_group_edge::AugmentedEdge, db::GraphConnection, edge::Edge, - node::Node, -}; - -pub struct ResolvedGraph { - pub graph: GenGraph, - pub interval_tree: IntervalTree, - pub block_group_id: HashId, -} - -/// Identify all edges leading to and from a provided node_id in a block_group and merge them into an existing GenGraph. -/// Returns true if the graph was expanded, false if no new edges were added. -pub fn expand( - conn: &GraphConnection, - graph: &mut GenGraph, - block_group_id: &HashId, - node_id: HashId, -) -> bool { - let edges_1hop = Edge::edges_for_block_group_nodes(conn, block_group_id, &[node_id], None) - .unwrap_or_default(); - - let mut neighbor_ids: Vec = edges_1hop - .iter() - .flat_map(|ae| [ae.edge.source_node_id, ae.edge.target_node_id]) - .filter(|id| *id != node_id) - .collect(); - neighbor_ids.sort(); - neighbor_ids.dedup(); - - let mut all_edges: HashMap = - edges_1hop.into_iter().map(|ae| (ae.edge.id, ae)).collect(); - if !neighbor_ids.is_empty() { - let neighbor_edges = - Edge::edges_for_block_group_nodes(conn, block_group_id, &neighbor_ids, None) - .unwrap_or_default(); - for ae in neighbor_edges { - all_edges.entry(ae.edge.id).or_insert(ae); - } - } - - let existing_edge_ids: HashSet = graph - .all_edges() - .flat_map(|(_, _, edges)| edges.iter().map(|e| e.edge_id)) - .collect(); - - let new_edges: Vec = all_edges - .into_values() - .filter(|ae| !existing_edge_ids.contains(&ae.edge.id)) - .collect(); - - if new_edges.is_empty() { - return false; - } - - let fragment = match BlockGroup::get_graph_from_edges(conn, block_group_id, &new_edges) { - Ok(g) => g, - Err(_) => return false, - }; - graph.merge_graph(&fragment); - true -} - -/// From a given position in a graph, find positions a given number of characters away. An -/// expand function can be given that specifies how to expand the input graph when it is exhausted. -/// By default, this function searches along the known graph, and if a matching position cannot be found -/// it will expand until it is not possible to grow the graph anymore. -/// Returns a Result>, given a list of matching GraphNodePositions at the requested distance. -pub fn find_offset( - graph: &mut GenGraph, - // The position to begin the search from - anchor: &GraphNodePosition, - // How far from the anchor the position of interst is. negative means search upstream of the graph position. - // positive means search downstream. - distance: i64, - mut expand: impl FnMut(&mut GenGraph, HashId) -> bool, -) -> Result, GraphError> { - if distance == 0 { - return Ok(vec![*anchor]); - } - - match find_offset_with_optional_expansion(graph, anchor, distance, false, &mut expand) { - Ok(results) => Ok(results), - Err(GraphError::OutOfBounds(_)) => { - find_offset_with_optional_expansion(graph, anchor, distance, true, &mut expand) - } - Err(err) => Err(err), - } -} - -/// The actual function doing the work from `find_offset`. See docs in `find_offset` for explanation. -fn find_offset_with_optional_expansion( - graph: &mut GenGraph, - anchor: &GraphNodePosition, - distance: i64, - // If true, expand the graph if search leads to the end of a graph. - expand_through_existing_paths: bool, - expand: &mut impl FnMut(&mut GenGraph, HashId) -> bool, -) -> Result, GraphError> { - let forward = distance > 0; - let mut queue = VecDeque::new(); - let mut visited = HashSet::new(); - let mut results = Vec::new(); - let mut result_seen = HashSet::new(); - - queue.push_back((*anchor, distance)); - visited.insert((*anchor, distance)); - - while let Some((pos, remaining)) = queue.pop_front() { - let node = pos.graph_node; - let node_len = node.length(); - - if remaining == 0 { - if result_seen.insert(pos) { - results.push(pos); - } - continue; - } - - if forward { - let in_node = node_len - pos.offset; - if remaining <= in_node { - let result = GraphNodePosition { - graph_node: node, - offset: pos.offset + remaining, - }; - if result_seen.insert(result) { - results.push(result); - } - continue; - } - - // Outgoing nodes means downstream nodes to search for. - let mut neighbors: Vec = graph - .neighbors_directed(node, Direction::Outgoing) - .collect(); - - if (neighbors.is_empty() || expand_through_existing_paths) - && expand(graph, node.node_id) - { - neighbors = graph - .neighbors_directed(node, Direction::Outgoing) - .collect(); - } - - for neighbor in neighbors { - let next_pos = GraphNodePosition { - graph_node: neighbor, - offset: 0, - }; - let next_remaining = remaining - in_node; - if visited.insert((next_pos, next_remaining)) { - queue.push_back((next_pos, next_remaining)); - } - } - } else { - let in_node = pos.offset; - let abs_remaining = -remaining; - if abs_remaining <= in_node { - let result = GraphNodePosition { - graph_node: node, - offset: pos.offset + remaining, - }; - if result_seen.insert(result) { - results.push(result); - } - continue; - } - - // Incoming edges means upstream nodes to traverse - let mut neighbors: Vec = graph - .neighbors_directed(node, Direction::Incoming) - .collect(); - - if (neighbors.is_empty() || expand_through_existing_paths) - && expand(graph, node.node_id) - { - neighbors = graph - .neighbors_directed(node, Direction::Incoming) - .collect(); - } - - for neighbor in neighbors { - let next_pos = GraphNodePosition { - graph_node: neighbor, - offset: neighbor.length(), - }; - let next_remaining = remaining + in_node; - if visited.insert((next_pos, next_remaining)) { - queue.push_back((next_pos, next_remaining)); - } - } - } - } - - if results.is_empty() { - return Err(GraphError::OutOfBounds(distance)); - } - - Ok(results) -} - -impl ResolvedGraph { - /// Find a postion in a graph according to a provided coordinate. This is generally utilized by the GenRegion machinery to identify the - /// positions a user requested. - pub fn resolve_anchor( - &self, - coord: i64, - conn: &GraphConnection, - ) -> Result { - let mut block = None; - for item in self.interval_tree.query_point(coord) { - let b = &item.value; - if !is_terminal(b.node_id) { - block = Some(*b); - break; - } - } - - if let Some(b) = block { - let offset = coord - b.start; - return Ok(GraphNodePosition { - graph_node: GraphNode { - node_id: b.node_id, - sequence_start: b.sequence_start, - sequence_end: b.sequence_end, - }, - offset, - }); - } - - let last_block = self - .interval_tree - .iter() - .map(|item| &item.value) - .filter(|b| !is_terminal(b.node_id)) - .max_by_key(|b| b.end) - .copied(); - - let before_tree = coord < 0; - let after_tree = last_block.map(|b| coord >= b.end).unwrap_or(false); - - let boundary_block = if before_tree { - self.interval_tree - .iter() - .map(|item| &item.value) - .filter(|b| !is_terminal(b.node_id)) - .min_by_key(|b| b.start) - .copied() - } else if after_tree { - last_block - } else { - None - }; - - let boundary = boundary_block.ok_or(GraphError::NoPath)?; - let boundary_node = GraphNode { - node_id: boundary.node_id, - sequence_start: boundary.sequence_start, - sequence_end: boundary.sequence_end, - }; - - let boundary_anchor = GraphNodePosition { - graph_node: boundary_node, - offset: if before_tree { - 0 - } else { - boundary.sequence_end - boundary.sequence_start - }, - }; - - let boundary_distance = if before_tree { - coord - boundary.start - } else { - coord - boundary.end - }; - - let same_node_coordinate = if before_tree { - boundary.sequence_start + boundary_distance - } else { - boundary.sequence_end + boundary_distance - }; - if same_node_coordinate >= 0 - && let Ok(node_lengths) = Node::query_nodes_length(conn, &[boundary.node_id]) - && same_node_coordinate <= *node_lengths.get(&boundary.node_id).unwrap_or(&0) - { - return Ok(GraphNodePosition { - graph_node: boundary_node, - offset: boundary_anchor.offset + boundary_distance, - }); - } - - let mut expanded_graph = self.graph.clone(); - expand( - conn, - &mut expanded_graph, - &self.block_group_id, - boundary_node.node_id, - ); - - let anchors = find_offset( - &mut expanded_graph, - &boundary_anchor, - boundary_distance, - |g, nid| expand(conn, g, &self.block_group_id, nid), - )?; - - anchors.into_iter().next().ok_or(GraphError::NoPath) - } -} - -#[cfg(test)] -mod tests { - use std::collections::HashSet; - - use gen_core::{PATH_END_NODE_ID, PATH_START_NODE_ID, Strand}; - use gen_graph::{GraphEdge, graph_from_interval_tree}; - - use super::*; - use crate::{ - block_group::{BlockGroup, NewBlockGroup}, - block_group_edge::{BlockGroupEdge, BlockGroupEdgeData}, - collection::Collection, - node::Node, - sample::{NewSample, Sample}, - sequence::Sequence, - test_helpers::get_connection, - }; - - fn setup_subset_graph() -> (crate::db::GraphConnection, HashId) { - let conn = get_connection(None).unwrap(); - Collection::get_or_create(&conn, "test").unwrap(); - Sample::get_or_create( - &conn, - NewSample { - name: "test", - ..Default::default() - }, - ) - .unwrap(); - let block_group = BlockGroup::create( - &conn, - NewBlockGroup { - collection_name: "test", - sample_name: "test", - name: "chr1", - ..Default::default() - }, - ) - .unwrap(); - - let seq_x = Sequence::new() - .sequence_type("DNA") - .sequence("XXXXX") - .save(&conn) - .unwrap(); - let seq_y = Sequence::new() - .sequence_type("DNA") - .sequence("YYYYY") - .save(&conn) - .unwrap(); - let seq_z = Sequence::new() - .sequence_type("DNA") - .sequence("ZZZZZ") - .save(&conn) - .unwrap(); - - let node_x = Node::create(&conn, &seq_x.hash, &HashId::convert_str("node-x")).unwrap(); - let node_y = Node::create(&conn, &seq_y.hash, &HashId::convert_str("node-y")).unwrap(); - let node_z = Node::create(&conn, &seq_z.hash, &HashId::convert_str("node-z")).unwrap(); - - let e_start = Edge::create( - &conn, - PATH_START_NODE_ID, - -1, - Strand::Forward, - node_x, - 0, - Strand::Forward, - ) - .unwrap(); - let e_xy = Edge::create( - &conn, - node_x, - 5, - Strand::Forward, - node_y, - 0, - Strand::Forward, - ) - .unwrap(); - let e_yz = Edge::create( - &conn, - node_y, - 5, - Strand::Forward, - node_z, - 0, - Strand::Forward, - ) - .unwrap(); - let e_end = Edge::create( - &conn, - node_z, - 5, - Strand::Forward, - PATH_END_NODE_ID, - 0, - Strand::Forward, - ) - .unwrap(); - - BlockGroupEdge::bulk_create( - &conn, - &[ - BlockGroupEdgeData { - block_group_id: block_group.id, - edge_id: e_start.id, - chromosome_index: 0, - phased: 0, - }, - BlockGroupEdgeData { - block_group_id: block_group.id, - edge_id: e_xy.id, - chromosome_index: 0, - phased: 0, - }, - BlockGroupEdgeData { - block_group_id: block_group.id, - edge_id: e_yz.id, - chromosome_index: 0, - phased: 0, - }, - BlockGroupEdgeData { - block_group_id: block_group.id, - edge_id: e_end.id, - chromosome_index: 0, - phased: 0, - }, - ], - ); - - (conn, block_group.id) - } - - fn subset_interval_tree() -> IntervalTree { - vec![( - 0..5, - NodeIntervalBlock { - node_id: HashId::convert_str("node-y"), - start: 0, - end: 5, - sequence_start: 0, - sequence_end: 5, - strand: Strand::Forward, - }, - )] - .into_iter() - .collect() - } - - fn test_edge(edge_id: &str) -> Vec { - vec![GraphEdge { - edge_id: HashId::convert_str(edge_id), - source_strand: Strand::Forward, - target_strand: Strand::Forward, - chromosome_index: 0, - phased: 0, - created_on: 0, - }] - } - - fn variable_length_branched_graph() -> GenGraph { - let node_aaa = GraphNode { - node_id: HashId::convert_str("node-aaa"), - sequence_start: 0, - sequence_end: 3, - }; - let node_cc = GraphNode { - node_id: HashId::convert_str("node-cc"), - sequence_start: 0, - sequence_end: 2, - }; - let node_gggg = GraphNode { - node_id: HashId::convert_str("node-gggg"), - sequence_start: 0, - sequence_end: 4, - }; - let node_ttt = GraphNode { - node_id: HashId::convert_str("node-ttt"), - sequence_start: 0, - sequence_end: 3, - }; - - let mut graph = GenGraph::new(); - graph.add_edge(node_aaa, node_cc, test_edge("edge-aaa-cc")); - graph.add_edge(node_aaa, node_gggg, test_edge("edge-aaa-gggg")); - graph.add_edge(node_cc, node_ttt, test_edge("edge-cc-ttt")); - graph.add_edge(node_gggg, node_ttt, test_edge("edge-gggg-ttt")); - graph - } - - fn position_set(positions: &[GraphNodePosition]) -> HashSet<(HashId, i64)> { - positions - .iter() - .map(|pos| (pos.graph_node.node_id, pos.offset)) - .collect() - } - - #[test] - fn test_find_offset_within_subset_node() { - let (conn, bg_id) = setup_subset_graph(); - let tree = subset_interval_tree(); - let mut graph = graph_from_interval_tree(&tree); - - let y_node = GraphNode { - node_id: HashId::convert_str("node-y"), - sequence_start: 0, - sequence_end: 5, - }; - let anchor = GraphNodePosition { - graph_node: y_node, - offset: 0, - }; - - let result = find_offset(&mut graph, &anchor, 3, |g, nid| { - expand(&conn, g, &bg_id, nid) - }); - assert!( - result.is_ok(), - "find_offset(3) should succeed: {:?}", - result.err() - ); - let positions = result.unwrap(); - assert_eq!(positions.len(), 1); - assert_eq!(positions[0].graph_node, y_node); - assert_eq!(positions[0].offset, 3); - } - - #[test] - fn test_find_offset_forward_expands_beyond_subset() { - let (conn, bg_id) = setup_subset_graph(); - let tree = subset_interval_tree(); - let mut graph = graph_from_interval_tree(&tree); - - let y_node = GraphNode { - node_id: HashId::convert_str("node-y"), - sequence_start: 0, - sequence_end: 5, - }; - let anchor = GraphNodePosition { - graph_node: y_node, - offset: 0, - }; - - let result = find_offset(&mut graph, &anchor, 7, |g, nid| { - expand(&conn, g, &bg_id, nid) - }); - assert!( - result.is_ok(), - "find_offset(7) should expand to Z: {:?}", - result.err() - ); - let positions = result.unwrap(); - assert_eq!(positions.len(), 1); - assert_eq!( - positions[0].graph_node.node_id, - HashId::convert_str("node-z") - ); - assert_eq!(positions[0].offset, 2); - } - - #[test] - fn test_find_offset_forward_expands_after_existing_paths_fail() { - let (conn, bg_id) = setup_subset_graph(); - let mut graph = GenGraph::new(); - - let y_node = GraphNode { - node_id: HashId::convert_str("node-y"), - sequence_start: 0, - sequence_end: 5, - }; - let dead_end = GraphNode { - node_id: HashId::convert_str("dead-end"), - sequence_start: 0, - sequence_end: 1, - }; - graph.add_edge( - y_node, - dead_end, - vec![GraphEdge { - edge_id: HashId::convert_str("edge-y-dead"), - source_strand: Strand::Forward, - target_strand: Strand::Forward, - chromosome_index: 0, - phased: 0, - created_on: 0, - }], - ); - let anchor = GraphNodePosition { - graph_node: y_node, - offset: 0, - }; - - let result = find_offset(&mut graph, &anchor, 7, |g, nid| { - expand(&conn, g, &bg_id, nid) - }); - assert!( - result.is_ok(), - "find_offset(7) should expand from Y after the existing path fails: {:?}", - result.err() - ); - let positions = result.unwrap(); - assert_eq!(positions.len(), 1); - assert_eq!( - positions[0].graph_node.node_id, - HashId::convert_str("node-z") - ); - assert_eq!(positions[0].offset, 2); - } - - #[test] - fn test_find_offset_backward_expands_beyond_subset() { - let (conn, bg_id) = setup_subset_graph(); - let tree = subset_interval_tree(); - let mut graph = graph_from_interval_tree(&tree); - - let y_node = GraphNode { - node_id: HashId::convert_str("node-y"), - sequence_start: 0, - sequence_end: 5, - }; - let anchor = GraphNodePosition { - graph_node: y_node, - offset: 0, - }; - - let result = find_offset(&mut graph, &anchor, -3, |g, nid| { - expand(&conn, g, &bg_id, nid) - }); - assert!( - result.is_ok(), - "find_offset(-3) should expand to X: {:?}", - result.err() - ); - let positions = result.unwrap(); - assert_eq!(positions.len(), 1); - assert_eq!( - positions[0].graph_node.node_id, - HashId::convert_str("node-x") - ); - assert_eq!(positions[0].offset, 2); - } - - #[test] - fn test_find_offset_out_of_bounds_with_expansion() { - let (conn, bg_id) = setup_subset_graph(); - let tree = subset_interval_tree(); - let mut graph = graph_from_interval_tree(&tree); - - let y_node = GraphNode { - node_id: HashId::convert_str("node-y"), - sequence_start: 0, - sequence_end: 5, - }; - let anchor = GraphNodePosition { - graph_node: y_node, - offset: 0, - }; - - let result = find_offset(&mut graph, &anchor, 100, |g, nid| { - expand(&conn, g, &bg_id, nid) - }); - assert!(result.is_err()); - } - - #[test] - fn test_find_offset_with_fragment_node() { - let (conn, bg_id) = setup_subset_graph(); - - let tree: IntervalTree = vec![( - 0..3, - NodeIntervalBlock { - node_id: HashId::convert_str("node-y"), - start: 0, - end: 3, - sequence_start: 0, - sequence_end: 3, - strand: Strand::Forward, - }, - )] - .into_iter() - .collect(); - let mut graph = graph_from_interval_tree(&tree); - - let y_frag = GraphNode { - node_id: HashId::convert_str("node-y"), - sequence_start: 0, - sequence_end: 3, - }; - let anchor = GraphNodePosition { - graph_node: y_frag, - offset: 0, - }; - - let result = find_offset(&mut graph, &anchor, 2, |g, nid| { - expand(&conn, g, &bg_id, nid) - }); - assert!(result.is_ok()); - let positions = result.unwrap(); - assert_eq!(positions[0].graph_node, y_frag); - assert_eq!(positions[0].offset, 2); - } - - #[test] - fn resolve_anchor_before_fragment_stays_on_same_backing_node() { - // The case is when we have a node fragment - // at the beginning of an accession. We don't know how far back to expand - // the fragment as there are no edges. - let (conn, bg_id) = setup_subset_graph(); - - let tree: IntervalTree = vec![( - 0..3, - NodeIntervalBlock { - node_id: HashId::convert_str("node-y"), - start: 0, - end: 3, - sequence_start: 2, - sequence_end: 5, - strand: Strand::Forward, - }, - )] - .into_iter() - .collect(); - let graph = graph_from_interval_tree(&tree); - let resolved = ResolvedGraph { - graph, - interval_tree: tree, - block_group_id: bg_id, - }; - - let position = resolved.resolve_anchor(-2, &conn).unwrap(); - - assert_eq!( - position.graph_node, - GraphNode { - node_id: HashId::convert_str("node-y"), - sequence_start: 2, - sequence_end: 5, - } - ); - assert_eq!(position.offset, -2); - assert_eq!(position.coordinate(), 0); - } - - #[test] - fn test_expand_adds_neighbors() { - let (conn, bg_id) = setup_subset_graph(); - let tree = subset_interval_tree(); - let mut graph = graph_from_interval_tree(&tree); - - assert_eq!(graph.node_count(), 1); - - let expanded = expand(&conn, &mut graph, &bg_id, HashId::convert_str("node-y")); - assert!(expanded, "expand should add new nodes"); - assert!( - graph.node_count() > 1, - "graph should have more nodes after expansion" - ); - - let node_ids: Vec = graph.nodes().map(|n| n.node_id).collect(); - assert!( - node_ids.contains(&HashId::convert_str("node-x")), - "X should be added after expanding Y" - ); - assert!( - node_ids.contains(&HashId::convert_str("node-z")), - "Z should be added after expanding Y" - ); - } - - #[test] - fn test_find_offset_in_variable_length_branch_finds_middle_nodes() { - let mut graph = variable_length_branched_graph(); - let aaa_anchor = GraphNodePosition { - graph_node: GraphNode { - node_id: HashId::convert_str("node-aaa"), - sequence_start: 0, - sequence_end: 3, - }, - offset: 2, - }; - - let from_aaa = find_offset(&mut graph, &aaa_anchor, 2, |_, _| false).unwrap(); - assert_eq!( - position_set(&from_aaa), - HashSet::from([ - (HashId::convert_str("node-cc"), 1), - (HashId::convert_str("node-gggg"), 1) - ]) - ); - - let ttt_anchor = GraphNodePosition { - graph_node: GraphNode { - node_id: HashId::convert_str("node-ttt"), - sequence_start: 0, - sequence_end: 3, - }, - offset: 0, - }; - let from_ttt = find_offset(&mut graph, &ttt_anchor, -2, |_, _| false).unwrap(); - assert_eq!( - position_set(&from_ttt), - HashSet::from([ - (HashId::convert_str("node-cc"), 0), - (HashId::convert_str("node-gggg"), 2) - ]) - ); - } - - #[test] - fn test_find_offset_in_variable_length_branch_returns_single_position_within_node() { - let mut graph = variable_length_branched_graph(); - let anchor = GraphNodePosition { - graph_node: GraphNode { - node_id: HashId::convert_str("node-aaa"), - sequence_start: 0, - sequence_end: 3, - }, - offset: 1, - }; - - let positions = find_offset(&mut graph, &anchor, 1, |_, _| false).unwrap(); - assert_eq!( - position_set(&positions), - HashSet::from([(HashId::convert_str("node-aaa"), 2)]) - ); - } - - #[test] - fn test_find_offset_in_variable_length_branch_finds_different_ttt_offsets() { - let mut graph = variable_length_branched_graph(); - let anchor = GraphNodePosition { - graph_node: GraphNode { - node_id: HashId::convert_str("node-aaa"), - sequence_start: 0, - sequence_end: 3, - }, - offset: 2, - }; - - let positions = find_offset(&mut graph, &anchor, 6, |_, _| false).unwrap(); - assert_eq!( - position_set(&positions), - HashSet::from([ - (HashId::convert_str("node-ttt"), 1), - (HashId::convert_str("node-ttt"), 3) - ]) - ); - } -} diff --git a/gen-models/src/lib.rs b/gen-models/src/lib.rs index 83c42e66..17ef22e8 100644 --- a/gen-models/src/lib.rs +++ b/gen-models/src/lib.rs @@ -11,7 +11,6 @@ pub mod errors; pub mod file_types; #[allow(clippy::all)] pub mod generated; -pub mod graph; pub mod history; pub use generated::gen_models_capnp; pub mod lineage; diff --git a/gen-models/src/locus.rs b/gen-models/src/locus.rs index 624e8dbd..c44c3cfd 100644 --- a/gen-models/src/locus.rs +++ b/gen-models/src/locus.rs @@ -15,8 +15,8 @@ //! coordinate reference frame (left side = 0). But to store graph changes in //! the additive model in the database we must convert back to the Node format. +pub use gen_core::GraphNodeSlice; use gen_core::{HashId, Strand}; -pub use gen_graph::GraphNodeSlice; use crate::{db::GraphConnection, node::Node, sequence::reverse_complement}; diff --git a/gen-models/src/region.rs b/gen-models/src/region.rs index b3dfd90d..801f439a 100644 --- a/gen-models/src/region.rs +++ b/gen-models/src/region.rs @@ -1,19 +1,16 @@ pub use gen_core::region::Region; use gen_core::{ - HashId, NodeIntervalBlock, PRESERVE_EDIT_SITE_CHROMOSOME_INDEX, Strand, is_terminal, + GraphNodePosition, HashId, NodeIntervalBlock, region::{RegionParseError, RegionResolutionError, RegionResolver}, }; -use gen_graph::{GraphNode, GraphNodePosition}; use intervaltree::IntervalTree; use thiserror::Error; use crate::{ accession::{Accession, AccessionError}, annotations::{Annotation, AnnotationError}, - block_group::{BlockGroup, BlockGroupChange, BlockGroupError, IntervalTreeSource}, - block_group_edge::AugmentedEdgeData, + block_group::{BlockGroup, BlockGroupError, IntervalTreeSource}, db::GraphConnection, - edge::EdgeData, errors::PathError, path::Path, traits::Query, @@ -488,213 +485,11 @@ impl ResolvedGenRegion { })?; Ok(accession.intervaltree(conn)?) } - ResolvedRegionKind::BlockGroup => Ok(BlockGroup::intervaltree_for( - conn, - &self.block_group.id, - self.remove_ambiguous_positions, - )?), + ResolvedRegionKind::BlockGroup => Err(GenRegionError::Unmappable( + "block-group regions require graph projection".to_string(), + )), } } - - pub fn find_graph_positions( - &self, - conn: &GraphConnection, - start_offset: i64, - end_offset: i64, - ) -> Result { - let (start_positions, end_positions) = - self.compute_graph_positions(conn, start_offset, end_offset)?; - let mut region = self.clone(); - region.start_anchors = Some(start_positions); - region.end_anchors = Some(end_positions); - Ok(region) - } - - fn compute_graph_positions( - &self, - conn: &GraphConnection, - start_offset: i64, - end_offset: i64, - ) -> Result< - ( - Vec, - Vec, - ), - gen_graph::GraphError, - > { - let interval_tree = self - .intervaltree(conn) - .map_err(|_| gen_graph::GraphError::NoPath)?; - let filtered: Vec<(std::ops::Range, gen_core::NodeIntervalBlock)> = interval_tree - .iter() - .filter(|item| !gen_core::is_terminal(item.value.node_id)) - .map(|item| (item.range.clone(), item.value)) - .collect(); - let tree: intervaltree::IntervalTree = - filtered.into_iter().collect(); - - let mut graph = gen_graph::graph_from_interval_tree(&tree); - - let resolved = crate::graph::ResolvedGraph { - graph: graph.clone(), - interval_tree: tree, - block_group_id: self.block_group.id, - }; - let start_anchor = resolved.resolve_anchor(self.start, conn)?; - let end_anchor = resolved.resolve_anchor(self.end, conn)?; - - let start_positions = - crate::graph::find_offset(&mut graph, &start_anchor, start_offset, |g, nid| { - crate::graph::expand(conn, g, &self.block_group.id, nid) - })?; - let end_positions = - crate::graph::find_offset(&mut graph, &end_anchor, end_offset, |g, nid| { - crate::graph::expand(conn, g, &self.block_group.id, nid) - })?; - - Ok((start_positions, end_positions)) - } - - #[cfg_attr( - feature = "profiling", - tracing::instrument(skip(self, conn, change, tree)) - )] - pub fn plan_edges( - &self, - conn: &GraphConnection, - change: &BlockGroupChange, - tree: Option<&IntervalTree>, - ) -> Result, BlockGroupError> { - match self.kind { - ResolvedRegionKind::Path | ResolvedRegionKind::BlockGroup => { - let local_tree; - let tree = match tree { - Some(tree) => tree, - None => { - local_tree = IntervalTreeSource::intervaltree(self, conn)?; - &local_tree - } - }; - return BlockGroup::set_up_new_edges(change, tree); - } - ResolvedRegionKind::Annotation | ResolvedRegionKind::Accession => {} - }; - - let graph_positions_from_tree = |coordinate| { - let mut positions = tree? - .query_point(coordinate) - .map(|entry| entry.value) - .filter(|block| !is_terminal(block.node_id)) - .map(|block| GraphNodePosition { - graph_node: GraphNode { - node_id: block.node_id, - sequence_start: block.sequence_start, - sequence_end: block.sequence_end, - }, - offset: coordinate - block.start, - }) - .collect::>(); - - if positions.is_empty() { - return None; - } - - positions.sort(); - positions.dedup(); - Some(positions) - }; - - let (start_positions, end_positions) = - if let (Some(start), Some(end)) = (&self.start_anchors, &self.end_anchors) { - (start.clone(), end.clone()) - } else if let Some(start_positions) = graph_positions_from_tree(self.start) - && let Some(end_positions) = graph_positions_from_tree(self.end) - { - (start_positions, end_positions) - } else { - let resolved = self - .find_graph_positions(conn, 0, 0) - .map_err(|err| BlockGroupError::ChangeOutOfBounds(err.to_string()))?; - ( - resolved.start_anchors.expect("should have start anchors"), - resolved.end_anchors.expect("should have end anchors"), - ) - }; - let preserve_chromosome_index = if change.preserve_edge { - 0 - } else { - PRESERVE_EDIT_SITE_CHROMOSOME_INDEX - }; - let mut new_edges = vec![]; - - for position in start_positions.iter().chain(end_positions.iter()) { - if !is_terminal(position.graph_node.node_id) { - let coordinate = position.coordinate(); - new_edges.push(AugmentedEdgeData { - edge_data: EdgeData { - source_node_id: position.graph_node.node_id, - source_coordinate: coordinate, - source_strand: Strand::Forward, - target_node_id: position.graph_node.node_id, - target_coordinate: coordinate, - target_strand: Strand::Forward, - }, - chromosome_index: preserve_chromosome_index, - phased: 0, - }); - } - } - - if change.block.sequence_start == change.block.sequence_end { - for start_position in &start_positions { - for end_position in &end_positions { - new_edges.push(AugmentedEdgeData { - edge_data: EdgeData { - source_node_id: start_position.graph_node.node_id, - source_coordinate: start_position.coordinate(), - source_strand: Strand::Forward, - target_node_id: end_position.graph_node.node_id, - target_coordinate: end_position.coordinate(), - target_strand: Strand::Forward, - }, - chromosome_index: change.chromosome_index, - phased: change.phased, - }); - } - } - } else { - for start_position in &start_positions { - new_edges.push(AugmentedEdgeData { - edge_data: EdgeData { - source_node_id: start_position.graph_node.node_id, - source_coordinate: start_position.coordinate(), - source_strand: Strand::Forward, - target_node_id: change.block.node_id, - target_coordinate: change.block.sequence_start, - target_strand: Strand::Forward, - }, - chromosome_index: change.chromosome_index, - phased: change.phased, - }); - } - for end_position in &end_positions { - new_edges.push(AugmentedEdgeData { - edge_data: EdgeData { - source_node_id: change.block.node_id, - source_coordinate: change.block.sequence_end, - source_strand: Strand::Forward, - target_node_id: end_position.graph_node.node_id, - target_coordinate: end_position.coordinate(), - target_strand: Strand::Forward, - }, - chromosome_index: change.chromosome_index, - phased: change.phased, - }); - } - } - - Ok(new_edges) - } } impl IntervalTreeSource for ResolvedGenRegion { @@ -937,857 +732,4 @@ mod tests { assert_eq!((wrap.start, wrap.end), (15, 5)); } } - - mod find_graph_positions { - use std::collections::HashSet; - - use gen_core::{HashId, PATH_END_NODE_ID, PATH_START_NODE_ID, Strand}; - - use super::*; - use crate::{ - block_group::{BlockGroup, NewBlockGroup, PathCache}, - block_group_edge::{BlockGroupEdge, BlockGroupEdgeData}, - collection::Collection, - edge::Edge, - node::Node, - path::Path, - sample::{NewSample, Sample}, - sequence::Sequence, - }; - - fn setup_graph() -> (crate::db::GraphConnection, HashId) { - let conn = get_connection(None).unwrap(); - Collection::get_or_create(&conn, "test").unwrap(); - Sample::get_or_create( - &conn, - NewSample { - name: "test", - ..Default::default() - }, - ) - .unwrap(); - let block_group = BlockGroup::create( - &conn, - NewBlockGroup { - collection_name: "test", - sample_name: "test", - name: "chr1", - ..Default::default() - }, - ) - .unwrap(); - - let seq_x = Sequence::new() - .sequence_type("DNA") - .sequence("XXXXX") - .save(&conn) - .unwrap(); - let seq_y = Sequence::new() - .sequence_type("DNA") - .sequence("YYYYY") - .save(&conn) - .unwrap(); - let seq_z = Sequence::new() - .sequence_type("DNA") - .sequence("ZZZZZ") - .save(&conn) - .unwrap(); - - let node_x = Node::create(&conn, &seq_x.hash, &HashId::convert_str("node-x")).unwrap(); - let node_y = Node::create(&conn, &seq_y.hash, &HashId::convert_str("node-y")).unwrap(); - let node_z = Node::create(&conn, &seq_z.hash, &HashId::convert_str("node-z")).unwrap(); - - let e_start = Edge::create( - &conn, - PATH_START_NODE_ID, - -1, - Strand::Forward, - node_x, - 0, - Strand::Forward, - ) - .unwrap(); - let e_xy = Edge::create( - &conn, - node_x, - 5, - Strand::Forward, - node_y, - 0, - Strand::Forward, - ) - .unwrap(); - let e_yz = Edge::create( - &conn, - node_y, - 5, - Strand::Forward, - node_z, - 0, - Strand::Forward, - ) - .unwrap(); - let e_end = Edge::create( - &conn, - node_z, - 5, - Strand::Forward, - PATH_END_NODE_ID, - 0, - Strand::Forward, - ) - .unwrap(); - - BlockGroupEdge::bulk_create( - &conn, - &[ - BlockGroupEdgeData { - block_group_id: block_group.id, - edge_id: e_start.id, - chromosome_index: 0, - phased: 0, - }, - BlockGroupEdgeData { - block_group_id: block_group.id, - edge_id: e_xy.id, - chromosome_index: 0, - phased: 0, - }, - BlockGroupEdgeData { - block_group_id: block_group.id, - edge_id: e_yz.id, - chromosome_index: 0, - phased: 0, - }, - BlockGroupEdgeData { - block_group_id: block_group.id, - edge_id: e_end.id, - chromosome_index: 0, - phased: 0, - }, - ], - ); - - (conn, block_group.id) - } - - fn create_accession( - conn: &crate::db::GraphConnection, - block_group_id: HashId, - name: &str, - start: i64, - end: i64, - ) -> Accession { - let edges = BlockGroupEdge::edges_for_block_group(conn, &block_group_id, None); - let mut by_source: std::collections::HashMap< - HashId, - &crate::block_group_edge::AugmentedEdge, - > = std::collections::HashMap::new(); - for ae in &edges { - by_source.insert(ae.edge.source_node_id, ae); - } - let mut ordered = vec![]; - let mut current = Some(PATH_START_NODE_ID); - while let Some(src) = current { - if let Some(ae) = by_source.get(&src) { - ordered.push(ae.edge.id); - current = if ae.edge.target_node_id == PATH_END_NODE_ID { - None - } else { - Some(ae.edge.target_node_id) - }; - } else { - break; - } - } - let path = Path::create(conn, name, &block_group_id, &ordered).unwrap(); - let mut path_cache = PathCache::new(conn); - let accession = - BlockGroup::add_accession(conn, &path, name, start, end, &mut path_cache).unwrap(); - Path::delete(conn, name, &block_group_id); - accession - } - - fn create_accession_from_edges( - conn: &crate::db::GraphConnection, - block_group_id: HashId, - name: &str, - edge_ids: &[HashId], - start: i64, - end: i64, - ) -> Accession { - let path = Path::create(conn, name, &block_group_id, edge_ids).unwrap(); - let mut path_cache = PathCache::new(conn); - let accession = - BlockGroup::add_accession(conn, &path, name, start, end, &mut path_cache).unwrap(); - Path::delete(conn, name, &block_group_id); - accession - } - - fn make_region( - bg: BlockGroup, - accession: Accession, - anchor_start: i64, - anchor_end: i64, - feature_length: i64, - start: i64, - end: i64, - ) -> ResolvedGenRegion { - ResolvedGenRegion { - block_group: bg, - path: None, - accession: Some(accession), - annotation: None, - kind: ResolvedRegionKind::Accession, - anchor_start, - anchor_end, - feature_length, - start, - end, - start_anchors: None, - end_anchors: None, - remove_ambiguous_positions: false, - } - } - - /// Creates a branched graph: {AAA,GGG} → TTT → {CCC,ATC} - /// Path: AAA→TTT→CCC (positions 0..9) - fn setup_branched_graph() -> (crate::db::GraphConnection, HashId) { - let conn = get_connection(None).unwrap(); - Collection::get_or_create(&conn, "test").unwrap(); - Sample::get_or_create( - &conn, - NewSample { - name: "test", - ..Default::default() - }, - ) - .unwrap(); - let bg = BlockGroup::create( - &conn, - NewBlockGroup { - collection_name: "test", - sample_name: "test", - name: "branched", - ..Default::default() - }, - ) - .unwrap(); - - let seq_aaa = Sequence::new() - .sequence_type("DNA") - .sequence("AAA") - .save(&conn) - .unwrap(); - let seq_ggg = Sequence::new() - .sequence_type("DNA") - .sequence("GGG") - .save(&conn) - .unwrap(); - let seq_ttt = Sequence::new() - .sequence_type("DNA") - .sequence("TTT") - .save(&conn) - .unwrap(); - let seq_ccc = Sequence::new() - .sequence_type("DNA") - .sequence("CCC") - .save(&conn) - .unwrap(); - let seq_atc = Sequence::new() - .sequence_type("DNA") - .sequence("ATC") - .save(&conn) - .unwrap(); - - let n_aaa = - Node::create(&conn, &seq_aaa.hash, &HashId::convert_str("node-aaa")).unwrap(); - let n_ggg = - Node::create(&conn, &seq_ggg.hash, &HashId::convert_str("node-ggg")).unwrap(); - let n_ttt = - Node::create(&conn, &seq_ttt.hash, &HashId::convert_str("node-ttt")).unwrap(); - let n_ccc = - Node::create(&conn, &seq_ccc.hash, &HashId::convert_str("node-ccc")).unwrap(); - let n_atc = - Node::create(&conn, &seq_atc.hash, &HashId::convert_str("node-atc")).unwrap(); - - let e_start = Edge::create( - &conn, - PATH_START_NODE_ID, - -1, - Strand::Forward, - n_aaa, - 0, - Strand::Forward, - ) - .unwrap(); - let e_ggg_start = Edge::create( - &conn, - PATH_START_NODE_ID, - -1, - Strand::Forward, - n_ggg, - 0, - Strand::Forward, - ) - .unwrap(); - let e_aaa_ttt = - Edge::create(&conn, n_aaa, 3, Strand::Forward, n_ttt, 0, Strand::Forward).unwrap(); - let e_ttt_ccc = - Edge::create(&conn, n_ttt, 3, Strand::Forward, n_ccc, 0, Strand::Forward).unwrap(); - let e_ccc_end = Edge::create( - &conn, - n_ccc, - 3, - Strand::Forward, - PATH_END_NODE_ID, - 0, - Strand::Forward, - ) - .unwrap(); - let e_atc_end = Edge::create( - &conn, - n_atc, - 3, - Strand::Forward, - PATH_END_NODE_ID, - 0, - Strand::Forward, - ) - .unwrap(); - let e_ggg_ttt = - Edge::create(&conn, n_ggg, 3, Strand::Forward, n_ttt, 0, Strand::Forward).unwrap(); - let e_ttt_atc = - Edge::create(&conn, n_ttt, 3, Strand::Forward, n_atc, 0, Strand::Forward).unwrap(); - - BlockGroupEdge::bulk_create( - &conn, - &[ - BlockGroupEdgeData { - block_group_id: bg.id, - edge_id: e_start.id, - chromosome_index: 0, - phased: 0, - }, - BlockGroupEdgeData { - block_group_id: bg.id, - edge_id: e_ggg_start.id, - chromosome_index: 0, - phased: 0, - }, - BlockGroupEdgeData { - block_group_id: bg.id, - edge_id: e_aaa_ttt.id, - chromosome_index: 0, - phased: 0, - }, - BlockGroupEdgeData { - block_group_id: bg.id, - edge_id: e_ttt_ccc.id, - chromosome_index: 0, - phased: 0, - }, - BlockGroupEdgeData { - block_group_id: bg.id, - edge_id: e_ccc_end.id, - chromosome_index: 0, - phased: 0, - }, - BlockGroupEdgeData { - block_group_id: bg.id, - edge_id: e_atc_end.id, - chromosome_index: 0, - phased: 0, - }, - BlockGroupEdgeData { - block_group_id: bg.id, - edge_id: e_ggg_ttt.id, - chromosome_index: 0, - phased: 0, - }, - BlockGroupEdgeData { - block_group_id: bg.id, - edge_id: e_ttt_atc.id, - chromosome_index: 0, - phased: 0, - }, - ], - ); - - (conn, bg.id) - } - - struct GraphFixture { - conn: crate::db::GraphConnection, - block_group_id: HashId, - path: Vec, - } - - /// Creates a branched graph: AAA -> {CC, GGGG} -> TTT. - fn setup_variable_length_branched_graph() -> GraphFixture { - let conn = get_connection(None).unwrap(); - Collection::get_or_create(&conn, "test").unwrap(); - Sample::get_or_create( - &conn, - NewSample { - name: "test", - ..Default::default() - }, - ) - .unwrap(); - let bg = BlockGroup::create( - &conn, - NewBlockGroup { - collection_name: "test", - sample_name: "test", - name: "variable-length-branched", - ..Default::default() - }, - ) - .unwrap(); - - let seq_aaa = Sequence::new() - .sequence_type("DNA") - .sequence("AAA") - .save(&conn) - .unwrap(); - let seq_cc = Sequence::new() - .sequence_type("DNA") - .sequence("CC") - .save(&conn) - .unwrap(); - let seq_gggg = Sequence::new() - .sequence_type("DNA") - .sequence("GGGG") - .save(&conn) - .unwrap(); - let seq_ttt = Sequence::new() - .sequence_type("DNA") - .sequence("TTT") - .save(&conn) - .unwrap(); - - let n_aaa = - Node::create(&conn, &seq_aaa.hash, &HashId::convert_str("node-aaa")).unwrap(); - let n_cc = Node::create(&conn, &seq_cc.hash, &HashId::convert_str("node-cc")).unwrap(); - let n_gggg = - Node::create(&conn, &seq_gggg.hash, &HashId::convert_str("node-gggg")).unwrap(); - let n_ttt = - Node::create(&conn, &seq_ttt.hash, &HashId::convert_str("node-ttt")).unwrap(); - - let e_start = Edge::create( - &conn, - PATH_START_NODE_ID, - -1, - Strand::Forward, - n_aaa, - 0, - Strand::Forward, - ) - .unwrap(); - let e_aaa_cc = - Edge::create(&conn, n_aaa, 3, Strand::Forward, n_cc, 0, Strand::Forward).unwrap(); - let e_aaa_gggg = - Edge::create(&conn, n_aaa, 3, Strand::Forward, n_gggg, 0, Strand::Forward).unwrap(); - let e_cc_ttt = - Edge::create(&conn, n_cc, 2, Strand::Forward, n_ttt, 0, Strand::Forward).unwrap(); - let e_gggg_ttt = - Edge::create(&conn, n_gggg, 4, Strand::Forward, n_ttt, 0, Strand::Forward).unwrap(); - let e_end = Edge::create( - &conn, - n_ttt, - 3, - Strand::Forward, - PATH_END_NODE_ID, - 0, - Strand::Forward, - ) - .unwrap(); - - BlockGroupEdge::bulk_create( - &conn, - &[ - BlockGroupEdgeData { - block_group_id: bg.id, - edge_id: e_start.id, - chromosome_index: 0, - phased: 0, - }, - BlockGroupEdgeData { - block_group_id: bg.id, - edge_id: e_aaa_cc.id, - chromosome_index: 0, - phased: 0, - }, - BlockGroupEdgeData { - block_group_id: bg.id, - edge_id: e_aaa_gggg.id, - chromosome_index: 0, - phased: 0, - }, - BlockGroupEdgeData { - block_group_id: bg.id, - edge_id: e_cc_ttt.id, - chromosome_index: 0, - phased: 0, - }, - BlockGroupEdgeData { - block_group_id: bg.id, - edge_id: e_gggg_ttt.id, - chromosome_index: 0, - phased: 0, - }, - BlockGroupEdgeData { - block_group_id: bg.id, - edge_id: e_end.id, - chromosome_index: 0, - phased: 0, - }, - ], - ); - - GraphFixture { - conn, - block_group_id: bg.id, - path: vec![e_start.id, e_aaa_cc.id, e_cc_ttt.id, e_end.id], - } - } - - fn position_set(positions: &[gen_graph::GraphNodePosition]) -> HashSet<(HashId, i64)> { - positions - .iter() - .map(|pos| (pos.graph_node.node_id, pos.offset)) - .collect() - } - - #[test] - fn test_finds_graph_positions_within_node() { - let (conn, bg_id) = setup_graph(); - let bg = BlockGroup::get_by_id(&conn, &bg_id, None).unwrap(); - let acc = create_accession(&conn, bg_id, "within", 0, 15); - let region = make_region(bg, acc, 0, 15, 15, 7, 7); - - let resolved = region.find_graph_positions(&conn, 2, 2).unwrap(); - let start_pos = resolved.start_anchors.as_ref().unwrap(); - let end_pos = resolved.end_anchors.as_ref().unwrap(); - assert_eq!(start_pos.len(), 1); - assert_eq!( - start_pos[0].graph_node.node_id, - HashId::convert_str("node-y") - ); - assert_eq!(start_pos[0].offset, 4); - assert_eq!(end_pos.len(), 1); - assert_eq!(end_pos[0].graph_node.node_id, HashId::convert_str("node-y")); - assert_eq!(end_pos[0].offset, 4); - } - - #[test] - fn test_finds_graph_positions_forward_across_nodes() { - let (conn, bg_id) = setup_graph(); - let bg = BlockGroup::get_by_id(&conn, &bg_id, None).unwrap(); - let acc = create_accession(&conn, bg_id, "fwd", 0, 15); - let region = make_region(bg, acc, 0, 15, 15, 7, 7); - - let resolved = region.find_graph_positions(&conn, 5, 5).unwrap(); - let start_pos = resolved.start_anchors.as_ref().unwrap(); - let end_pos = resolved.end_anchors.as_ref().unwrap(); - assert_eq!(start_pos.len(), 1); - assert_eq!( - start_pos[0].graph_node.node_id, - HashId::convert_str("node-z") - ); - assert_eq!(start_pos[0].offset, 2); - assert_eq!(end_pos.len(), 1); - assert_eq!(end_pos[0].graph_node.node_id, HashId::convert_str("node-z")); - assert_eq!(end_pos[0].offset, 2); - } - - #[test] - fn test_finds_graph_positions_backwards_across_nodes() { - let (conn, bg_id) = setup_graph(); - let bg = BlockGroup::get_by_id(&conn, &bg_id, None).unwrap(); - let acc = create_accession(&conn, bg_id, "bwd", 0, 15); - let region = make_region(bg, acc, 0, 15, 15, 7, 7); - - let resolved = region.find_graph_positions(&conn, -5, -5).unwrap(); - let start_pos = resolved.start_anchors.as_ref().unwrap(); - let end_pos = resolved.end_anchors.as_ref().unwrap(); - assert_eq!(start_pos.len(), 1); - assert_eq!( - start_pos[0].graph_node.node_id, - HashId::convert_str("node-x") - ); - assert_eq!(start_pos[0].offset, 2); - assert_eq!(end_pos.len(), 1); - assert_eq!(end_pos[0].graph_node.node_id, HashId::convert_str("node-x")); - assert_eq!(end_pos[0].offset, 2); - } - - #[test] - fn test_reports_out_of_bounds() { - let (conn, bg_id) = setup_graph(); - let bg = BlockGroup::get_by_id(&conn, &bg_id, None).unwrap(); - let acc = create_accession(&conn, bg_id, "oob", 0, 15); - let region = make_region(bg, acc, 0, 15, 15, 7, 7); - - assert!(region.find_graph_positions(&conn, 100, 100).is_err()); - } - - #[test] - fn test_finds_graph_positions_from_start() { - let (conn, bg_id) = setup_graph(); - let bg = BlockGroup::get_by_id(&conn, &bg_id, None).unwrap(); - let acc = create_accession(&conn, bg_id, "start", 0, 15); - let region = make_region(bg, acc, 0, 15, 15, 0, 0); - - let resolved = region.find_graph_positions(&conn, 12, 12).unwrap(); - let start_pos = resolved.start_anchors.as_ref().unwrap(); - let end_pos = resolved.end_anchors.as_ref().unwrap(); - assert_eq!(start_pos.len(), 1); - assert_eq!( - start_pos[0].graph_node.node_id, - HashId::convert_str("node-z") - ); - assert_eq!(start_pos[0].offset, 2); - assert_eq!(end_pos.len(), 1); - assert_eq!(end_pos[0].graph_node.node_id, HashId::convert_str("node-z")); - assert_eq!(end_pos[0].offset, 2); - } - - #[test] - fn test_finds_graph_positions_from_end() { - let (conn, bg_id) = setup_graph(); - let bg = BlockGroup::get_by_id(&conn, &bg_id, None).unwrap(); - let acc = create_accession(&conn, bg_id, "end", 0, 15); - let region = make_region(bg, acc, 0, 15, 15, 14, 14); - - let resolved = region.find_graph_positions(&conn, -14, -14).unwrap(); - let start_pos = resolved.start_anchors.as_ref().unwrap(); - let end_pos = resolved.end_anchors.as_ref().unwrap(); - assert_eq!(start_pos.len(), 1); - assert_eq!( - start_pos[0].graph_node.node_id, - HashId::convert_str("node-x") - ); - assert_eq!(start_pos[0].offset, 0); - assert_eq!(end_pos.len(), 1); - assert_eq!(end_pos[0].graph_node.node_id, HashId::convert_str("node-x")); - assert_eq!(end_pos[0].offset, 0); - } - - #[test] - fn test_finds_graph_positions_within_accessions() { - let (conn, bg_id) = setup_graph(); - let bg = BlockGroup::get_by_id(&conn, &bg_id, None).unwrap(); - let acc = create_accession(&conn, bg_id, "acc-within", 5, 10); - let region = make_region(bg, acc, 5, 10, 5, 2, 2); - - let resolved = region.find_graph_positions(&conn, 1, 1).unwrap(); - let start_pos = resolved.start_anchors.as_ref().unwrap(); - let end_pos = resolved.end_anchors.as_ref().unwrap(); - assert_eq!(start_pos.len(), 1); - assert_eq!( - start_pos[0].graph_node.node_id, - HashId::convert_str("node-y") - ); - assert_eq!(start_pos[0].offset, 3); - assert_eq!(end_pos.len(), 1); - assert_eq!(end_pos[0].graph_node.node_id, HashId::convert_str("node-y")); - assert_eq!(end_pos[0].offset, 3); - } - - #[test] - fn test_finds_graph_positions_expands_accession_forward() { - let (conn, bg_id) = setup_graph(); - let bg = BlockGroup::get_by_id(&conn, &bg_id, None).unwrap(); - let acc = create_accession(&conn, bg_id, "acc-fwd", 5, 10); - let region = make_region(bg, acc, 5, 10, 5, 3, 3); - - let resolved = region.find_graph_positions(&conn, 5, 5).unwrap(); - let start_pos = resolved.start_anchors.as_ref().unwrap(); - let end_pos = resolved.end_anchors.as_ref().unwrap(); - assert_eq!(start_pos.len(), 1); - assert_eq!( - start_pos[0].graph_node.node_id, - HashId::convert_str("node-z") - ); - assert_eq!(start_pos[0].offset, 3); - assert_eq!(end_pos.len(), 1); - assert_eq!(end_pos[0].graph_node.node_id, HashId::convert_str("node-z")); - assert_eq!(end_pos[0].offset, 3); - } - - #[test] - fn test_finds_graph_positions_expands_accession_backward() { - let (conn, bg_id) = setup_graph(); - let bg = BlockGroup::get_by_id(&conn, &bg_id, None).unwrap(); - let acc = create_accession(&conn, bg_id, "acc-bwd", 5, 10); - let region = make_region(bg, acc, 5, 10, 5, 1, 1); - - let resolved = region.find_graph_positions(&conn, -4, -4).unwrap(); - let start_pos = resolved.start_anchors.as_ref().unwrap(); - let end_pos = resolved.end_anchors.as_ref().unwrap(); - assert_eq!(start_pos.len(), 1); - assert_eq!( - start_pos[0].graph_node.node_id, - HashId::convert_str("node-x") - ); - assert_eq!(start_pos[0].offset, 2); - assert_eq!(end_pos.len(), 1); - assert_eq!(end_pos[0].graph_node.node_id, HashId::convert_str("node-x")); - assert_eq!(end_pos[0].offset, 2); - } - - #[test] - fn test_finds_graph_positions_reports_accession_out_of_bounds() { - let (conn, bg_id) = setup_graph(); - let bg = BlockGroup::get_by_id(&conn, &bg_id, None).unwrap(); - let acc = create_accession(&conn, bg_id, "acc-oob", 5, 10); - let region = make_region(bg, acc, 5, 10, 5, 2, 2); - - assert!(region.find_graph_positions(&conn, 100, 100).is_err()); - } - - #[test] - fn test_finds_graph_positions_in_branched_graph_backwards_returns_all_positions() { - let (conn, bg_id) = setup_branched_graph(); - let bg = BlockGroup::get_by_id(&conn, &bg_id, None).unwrap(); - // Accession on TTT: path positions 3..6, accession-relative 0..3 - let acc = create_accession(&conn, bg_id, "branched-bwd", 3, 6); - let region = make_region(bg, acc, 3, 6, 3, 0, 0); - - // Backward 3 from TTT offset 0 → should find AAA and GGG at offset 0 - let resolved = region.find_graph_positions(&conn, -3, -3).unwrap(); - let start_pos = resolved.start_anchors.as_ref().unwrap(); - let end_pos = resolved.end_anchors.as_ref().unwrap(); - assert_eq!(start_pos.len(), 2); - let start_ids: Vec = start_pos.iter().map(|p| p.graph_node.node_id).collect(); - assert!(start_ids.contains(&HashId::convert_str("node-aaa"))); - assert!(start_ids.contains(&HashId::convert_str("node-ggg"))); - for pos in start_pos { - assert_eq!(pos.offset, 0); - } - assert_eq!(end_pos.len(), 2); - let end_ids: Vec = end_pos.iter().map(|p| p.graph_node.node_id).collect(); - assert!(end_ids.contains(&HashId::convert_str("node-aaa"))); - assert!(end_ids.contains(&HashId::convert_str("node-ggg"))); - } - - #[test] - fn test_finds_graph_positions_in_branched_graph_forwardsgr_returns_all_positions() { - let (conn, bg_id) = setup_branched_graph(); - let bg = BlockGroup::get_by_id(&conn, &bg_id, None).unwrap(); - // Accession on TTT: path positions 3..6, accession-relative 0..3 - let acc = create_accession(&conn, bg_id, "branched-fwd", 3, 6); - let region = make_region(bg, acc, 3, 6, 3, 2, 2); - - // Forward 3 from TTT offset 2 → should find CCC and ATC at offset 2 - let resolved = region.find_graph_positions(&conn, 3, 3).unwrap(); - let start_pos = resolved.start_anchors.as_ref().unwrap(); - let end_pos = resolved.end_anchors.as_ref().unwrap(); - assert_eq!(start_pos.len(), 2); - let start_ids: Vec = start_pos.iter().map(|p| p.graph_node.node_id).collect(); - assert!(start_ids.contains(&HashId::convert_str("node-ccc"))); - assert!(start_ids.contains(&HashId::convert_str("node-atc"))); - for pos in start_pos { - assert_eq!(pos.offset, 2); - } - assert_eq!(end_pos.len(), 2); - let end_ids: Vec = end_pos.iter().map(|p| p.graph_node.node_id).collect(); - assert!(end_ids.contains(&HashId::convert_str("node-ccc"))); - assert!(end_ids.contains(&HashId::convert_str("node-atc"))); - } - - #[test] - fn test_finds_graph_positions_in_variable_length_branch_finds_middle_nodes() { - let fixture = setup_variable_length_branched_graph(); - let bg = BlockGroup::get_by_id(&fixture.conn, &fixture.block_group_id, None).unwrap(); - let aaa_acc = create_accession_from_edges( - &fixture.conn, - fixture.block_group_id, - "variable-aaa", - &fixture.path, - 0, - 3, - ); - let aaa_region = make_region(bg.clone(), aaa_acc, 0, 3, 3, 2, 2); - - let from_aaa = aaa_region - .find_graph_positions(&fixture.conn, 2, 2) - .unwrap(); - assert_eq!( - position_set(&from_aaa.start_anchors.unwrap()), - HashSet::from([ - (HashId::convert_str("node-cc"), 1), - (HashId::convert_str("node-gggg"), 1) - ]) - ); - - let ttt_acc = create_accession_from_edges( - &fixture.conn, - fixture.block_group_id, - "variable-ttt", - &fixture.path, - 5, - 8, - ); - let ttt_region = make_region(bg, ttt_acc, 5, 8, 3, 0, 0); - - let from_ttt = ttt_region - .find_graph_positions(&fixture.conn, -2, -2) - .unwrap(); - assert_eq!( - position_set(&from_ttt.start_anchors.unwrap()), - HashSet::from([ - (HashId::convert_str("node-cc"), 0), - (HashId::convert_str("node-gggg"), 2) - ]) - ); - } - - #[test] - fn test_finds_graph_positions_in_variable_length_branch_returns_single_position() { - let fixture = setup_variable_length_branched_graph(); - let bg = BlockGroup::get_by_id(&fixture.conn, &fixture.block_group_id, None).unwrap(); - let acc = create_accession_from_edges( - &fixture.conn, - fixture.block_group_id, - "variable-single", - &fixture.path, - 0, - 3, - ); - let region = make_region(bg, acc, 0, 3, 3, 1, 1); - - let positions = region.find_graph_positions(&fixture.conn, 1, 1).unwrap(); - assert_eq!( - position_set(&positions.start_anchors.unwrap()), - HashSet::from([(HashId::convert_str("node-aaa"), 2)]) - ); - } - - #[test] - fn test_finds_graph_positions_in_variable_length_branch_finds_different_ttt_offsets() { - let fixture = setup_variable_length_branched_graph(); - let bg = BlockGroup::get_by_id(&fixture.conn, &fixture.block_group_id, None).unwrap(); - let acc = create_accession_from_edges( - &fixture.conn, - fixture.block_group_id, - "variable-ttt-offsets", - &fixture.path, - 0, - 3, - ); - let region = make_region(bg, acc, 0, 3, 3, 2, 2); - - let positions = region.find_graph_positions(&fixture.conn, 6, 6).unwrap(); - assert_eq!( - position_set(&positions.start_anchors.unwrap()), - HashSet::from([ - (HashId::convert_str("node-ttt"), 1), - (HashId::convert_str("node-ttt"), 3) - ]) - ); - } - } } diff --git a/gen-models/src/sample.rs b/gen-models/src/sample.rs index 39e81e00..35643a12 100644 --- a/gen-models/src/sample.rs +++ b/gen-models/src/sample.rs @@ -1,7 +1,6 @@ use std::{collections::HashSet, rc::Rc}; use gen_core::traits::Capnp; -use gen_graph::GenGraph; use rusqlite::{Result as SQLResult, Row, params, types::Value as SQLValue}; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -179,46 +178,6 @@ impl Sample { stmt.execute([name]).unwrap(); } - pub fn get_graph( - conn: &GraphConnection, - collection: &str, - name: &str, - history_ref: Option<&str>, - ) -> Result { - let block_groups = Sample::get_block_groups(conn, collection, name, history_ref); - let mut sample_graph = GenGraph::new(); - for bg in block_groups { - let bg_graph = BlockGroup::get_graph(conn, &bg.id, history_ref)?; - // Add nodes and edges from block group graph to sample graph - for node in bg_graph.nodes() { - sample_graph.add_node(node); - } - for (source, dest, edges) in bg_graph.all_edges() { - if let Some(existing_edges) = sample_graph.edge_weight_mut(source, dest) { - existing_edges.extend(edges.clone()); - } else { - sample_graph.add_edge(source, dest, edges.clone()); - } - } - } - Ok(sample_graph) - } - - pub fn get_all_sequences( - conn: &GraphConnection, - collection_name: &str, - sample_name: &str, - prune: bool, - history_ref: Option<&str>, - ) -> Result, SampleError> { - let mut sequences = HashSet::new(); - for block_group in Sample::get_block_groups(conn, collection_name, sample_name, history_ref) - { - sequences.extend(BlockGroup::get_all_sequences(conn, &block_group.id, prune)?); - } - Ok(sequences) - } - pub fn get_or_create_child( conn: &GraphConnection, collection_name: &str, diff --git a/gen-models/src/traits.rs b/gen-models/src/traits.rs index d74f4dab..1b4428a4 100644 --- a/gen-models/src/traits.rs +++ b/gen-models/src/traits.rs @@ -1,6 +1,5 @@ use std::rc::Rc; -use itertools::Itertools; use rusqlite::{ Connection, Params, Result as SQLResult, Row, ToSql, limits::Limit, params, types::Value, }; @@ -93,21 +92,17 @@ pub trait Query { rows.next().unwrap().map(|row| Self::process_row(row)) } - fn query_by_ids<'a, I: ?Sized, T>( - conn: &Connection, - ids: &'a I, - history_ref: Option<&str>, - ) -> Vec + fn query_by_ids(conn: &Connection, ids: &[T], history_ref: Option<&str>) -> Vec where - &'a I: IntoIterator, - T: Clone + 'a, + T: Clone, Value: From, { let mut results = vec![]; let batch_size = max_rows_per_batch(conn, 1); - for chunk in &ids.into_iter().chunks(batch_size) { + for chunk in ids.chunks(batch_size) { let values: Vec = chunk - .map(|value: &'a T| Value::from(value.clone())) + .iter() + .map(|value| Value::from(value.clone())) .collect(); let query = format!( " @@ -139,17 +134,17 @@ pub trait Query { results } - fn delete_by_ids<'a, I: ?Sized, T>(conn: &Connection, ids: &'a I) -> Vec + fn delete_by_ids(conn: &Connection, ids: &[T]) -> Vec where - &'a I: IntoIterator, - T: Clone + 'a, + T: Clone, Value: From, { let mut results = vec![]; let batch_size = max_rows_per_batch(conn, 1); - for chunk in &ids.into_iter().chunks(batch_size) { + for chunk in ids.chunks(batch_size) { let values: Vec = chunk - .map(|value: &'a T| Value::from(value.clone())) + .iter() + .map(|value| Value::from(value.clone())) .collect(); results.append(&mut Self::query( conn, diff --git a/gen-python/Cargo.lock b/gen-python/Cargo.lock index beeda016..ac15cc87 100644 --- a/gen-python/Cargo.lock +++ b/gen-python/Cargo.lock @@ -1442,6 +1442,7 @@ dependencies = [ "hex", "itertools", "noodles", + "petgraph", "pyo3", "pyo3-macros", "rand 0.10.1", @@ -1472,8 +1473,10 @@ name = "gen-graph" version = "0.2.1" dependencies = [ "gen-core", + "gen-models", "interavl", "intervaltree", + "itertools", "petgraph", "serde", "thiserror 2.0.18", @@ -1490,7 +1493,6 @@ dependencies = [ "fallible-streaming-iterator", "gen-capnp-schemas", "gen-core", - "gen-graph", "include_dir", "indexmap", "intervaltree", @@ -1504,6 +1506,7 @@ dependencies = [ "serde", "serde_json", "sha2", + "tempfile", "thiserror 2.0.18", "tokio", "url", diff --git a/gen-python/src/python_api/block_group.rs b/gen-python/src/python_api/block_group.rs index cb71ac5b..e050cdad 100644 --- a/gen-python/src/python_api/block_group.rs +++ b/gen-python/src/python_api/block_group.rs @@ -233,8 +233,8 @@ impl PySequenceGraph { ) })?; let conn = context.graph().conn(); - let graph = - BlockGroup::get_graph(conn, &self.id, None).map_err(block_group_err_to_pyerr)?; + let graph = gen_graph::models::load_block_group_graph(conn, &self.id, None) + .map_err(block_group_err_to_pyerr)?; let matcher = GenGraphMatcher::new_with_sequence_kind(conn, graph, kind); let gen_dir = context.workspace().ensure_gen_dir(); @@ -295,8 +295,8 @@ impl PySequenceGraph { fs::create_dir_all(&index_dir) .map_err(|e| PyRuntimeError::new_err(format!("Failed to create index dir: {e}")))?; let conn = context.graph().conn(); - let graph = - BlockGroup::get_graph(conn, &self.id, None).map_err(block_group_err_to_pyerr)?; + let graph = gen_graph::models::load_block_group_graph(conn, &self.id, None) + .map_err(block_group_err_to_pyerr)?; let matcher = GenGraphMatcher::new_with_sequence_kind(conn, graph, kind); let normalized = kind != SequenceKind::Exact; let index = SeedIndex::build(&matcher, k, normalized); @@ -357,8 +357,8 @@ impl PySequenceGraph { fn to_dict(&self, py: Python<'_>) -> PyResult { let conn = self.require_context("to_dict()")?.graph().conn(); - let graph = - BlockGroup::get_graph(conn, &self.id, None).map_err(block_group_err_to_pyerr)?; + let graph = gen_graph::models::load_block_group_graph(conn, &self.id, None) + .map_err(block_group_err_to_pyerr)?; let dict = PyDict::new(py); let nodes: Vec = graph .nodes() @@ -393,8 +393,8 @@ impl PySequenceGraph { fn to_rustworkx(&self, py: Python<'_>) -> PyResult { let conn = self.require_context("to_rustworkx()")?.graph().conn(); - let graph = - BlockGroup::get_graph(conn, &self.id, None).map_err(block_group_err_to_pyerr)?; + let graph = gen_graph::models::load_block_group_graph(conn, &self.id, None) + .map_err(block_group_err_to_pyerr)?; { let rustworkx = PyModule::import(py, "rustworkx").map_err(|_| { pyo3::exceptions::PyModuleNotFoundError::new_err( @@ -435,8 +435,8 @@ impl PySequenceGraph { fn to_networkx(&self, py: Python<'_>) -> PyResult { let conn = self.require_context("to_networkx()")?.graph().conn(); - let graph = - BlockGroup::get_graph(conn, &self.id, None).map_err(block_group_err_to_pyerr)?; + let graph = gen_graph::models::load_block_group_graph(conn, &self.id, None) + .map_err(block_group_err_to_pyerr)?; { let networkx = PyModule::import(py, "networkx").map_err(|_| { pyo3::exceptions::PyModuleNotFoundError::new_err( diff --git a/gen-python/src/python_api/jupyter_widget.rs b/gen-python/src/python_api/jupyter_widget.rs index 50c28b9f..e01c7566 100644 --- a/gen-python/src/python_api/jupyter_widget.rs +++ b/gen-python/src/python_api/jupyter_widget.rs @@ -1089,8 +1089,8 @@ fn loaded_page_for_sequence_graph(sg: &PySequenceGraph) -> PyResult { .path() .map(PathBuf::from) .ok_or_else(|| PyRuntimeError::new_err("graph DB has no file path"))?; - let graph = - BlockGroup::get_graph(graph_conn, &sg.id, None).map_err(block_group_err_to_pyerr)?; + let graph = gen_graph::models::load_block_group_graph(graph_conn, &sg.id, None) + .map_err(block_group_err_to_pyerr)?; let mut page = GraphPage::new(sg.name.clone(), db_path, graph); page.block_group_id = Some(sg.id); Ok(page) @@ -1190,8 +1190,9 @@ impl PyGraphController { if let Page::Pending(page_ref) = page { let conn = get_connection(&page_ref.db_path) .map_err(|e| PyRuntimeError::new_err(e.to_string()))?; - let graph = BlockGroup::get_graph(&conn, &page_ref.block_group_id, None) - .map_err(block_group_err_to_pyerr)?; + let graph = + gen_graph::models::load_block_group_graph(&conn, &page_ref.block_group_id, None) + .map_err(block_group_err_to_pyerr)?; let mut loaded = GraphPage::new(page_ref.name.clone(), page_ref.db_path.clone(), graph); loaded.block_group_id = Some(page_ref.block_group_id); *page = Page::Loaded(Box::new(loaded)); @@ -1499,7 +1500,6 @@ impl PyGraphController { #[cfg(test)] mod tests { use r#gen::test_helpers::{setup_block_group, setup_gen_on_disk}; - use gen_models::block_group::BlockGroup; use pyo3::{exceptions::PyValueError, prelude::*}; use serde_json::Value; @@ -1514,7 +1514,7 @@ mod tests { .map(std::path::PathBuf::from) .expect("test DB must be file-backed"); let (bg_id, _) = setup_block_group(graph_handle.conn()); - let graph = BlockGroup::get_graph(graph_handle.conn(), &bg_id, None) + let graph = gen_graph::models::load_block_group_graph(graph_handle.conn(), &bg_id, None) .map_err(crate::python_api::utils::block_group_err_to_pyerr)?; let mut ctrl = PyGraphController::new(db_path, graph); if let Some(node_detail) = detail { diff --git a/gen-python/src/python_api/repository/search.rs b/gen-python/src/python_api/repository/search.rs index b3315ed0..f84bbc77 100644 --- a/gen-python/src/python_api/repository/search.rs +++ b/gen-python/src/python_api/repository/search.rs @@ -45,8 +45,8 @@ impl PyRepository { }; for bg in bgs { - let graph = - BlockGroup::get_graph(conn, &bg.id, None).map_err(block_group_err_to_pyerr)?; + let graph = gen_graph::models::load_block_group_graph(conn, &bg.id, None) + .map_err(block_group_err_to_pyerr)?; let matcher = GenGraphMatcher::new_with_sequence_kind(conn, graph, kind); let index = SeedIndex::build(&matcher, k, normalized); let path = index_dir.join(format!("{}.bin", bg.id)); @@ -93,8 +93,8 @@ impl PyRepository { let query_bytes = query.as_bytes(); let mut results = Vec::new(); for bg in bgs { - let graph = - BlockGroup::get_graph(conn, &bg.id, None).map_err(block_group_err_to_pyerr)?; + let graph = gen_graph::models::load_block_group_graph(conn, &bg.id, None) + .map_err(block_group_err_to_pyerr)?; let matcher = GenGraphMatcher::new_with_sequence_kind(conn, graph, kind); let index_path = self diff --git a/gen-r/src/rust/src/lib.rs b/gen-r/src/rust/src/lib.rs index 01c105bf..d8de6b57 100644 --- a/gen-r/src/rust/src/lib.rs +++ b/gen-r/src/rust/src/lib.rs @@ -269,7 +269,7 @@ fn list_annotation_records( sample_name: &str, name: &str, ) -> std::result::Result { - let graph = BlockGroup::get_graph(conn, block_group_id, None) + let graph = gen_graph::models::load_block_group_graph(conn, block_group_id, None) .map_err(|e| Error::Other(e.to_string()))?; let annotations = Annotation::query_with_lineage(conn, collection_name, sample_name, name) .map_err(|e| Error::Other(e.to_string()))?; @@ -1875,7 +1875,7 @@ impl Repository { .collect() }; for bg in bgs { - let graph = BlockGroup::get_graph(conn, &bg.id, None) + let graph = gen_graph::models::load_block_group_graph(conn, &bg.id, None) .map_err(|e| Error::Other(e.to_string()))?; let matcher = GenGraphMatcher::new_with_sequence_kind(conn, graph, kind); let index = SeedIndex::build(&matcher, k as usize, normalized); @@ -1914,7 +1914,7 @@ impl Repository { .join("search_index"); let mut results = Vec::new(); for bg in bgs { - let graph = BlockGroup::get_graph(conn, &bg.id, None) + let graph = gen_graph::models::load_block_group_graph(conn, &bg.id, None) .map_err(|e| Error::Other(e.to_string()))?; let matcher = GenGraphMatcher::new_with_sequence_kind(conn, graph, kind); let index_path = index_dir.join(format!("{}.bin", bg.id)); @@ -2067,8 +2067,8 @@ impl Repository { ) -> std::result::Result { let conn = self.context.graph().conn(); let bg_id = hash_id_from_string(&sequence_graph_id).map_err(Error::Other)?; - let graph = - BlockGroup::get_graph(conn, &bg_id, None).map_err(|e| Error::Other(e.to_string()))?; + let graph = gen_graph::models::load_block_group_graph(conn, &bg_id, None) + .map_err(|e| Error::Other(e.to_string()))?; let node_sizer = GenGraphNodeSizer; let mut controller = GraphController::new(graph, node_sizer); controller.set_detail_level(visual_detail(&detail).map_err(Error::Other)?); @@ -2154,8 +2154,8 @@ impl Repository { ) -> std::result::Result { let conn = self.context.graph().conn(); let bg_id = hash_id_from_string(&sequence_graph_id).map_err(Error::Other)?; - let graph = - BlockGroup::get_graph(conn, &bg_id, None).map_err(|e| Error::Other(e.to_string()))?; + let graph = gen_graph::models::load_block_group_graph(conn, &bg_id, None) + .map_err(|e| Error::Other(e.to_string()))?; let node_sizer = GenGraphNodeSizer; let mut controller = GraphController::new(graph, node_sizer); controller.set_detail_level(visual_detail(&detail).map_err(Error::Other)?); @@ -2314,8 +2314,8 @@ impl SequenceGraph { .join("search_index"); fs::create_dir_all(&index_dir) .map_err(|e| Error::Other(format!("Failed to create index dir: {e}")))?; - let graph = - BlockGroup::get_graph(conn, &self.id, None).map_err(|e| Error::Other(e.to_string()))?; + let graph = gen_graph::models::load_block_group_graph(conn, &self.id, None) + .map_err(|e| Error::Other(e.to_string()))?; let matcher = GenGraphMatcher::new_with_sequence_kind(conn, graph, kind); let index = SeedIndex::build(&matcher, k as usize, normalized); let path = index_dir.join(format!("{}.bin", self.id)); @@ -2328,8 +2328,8 @@ impl SequenceGraph { fn search(&self, query: String, sequence_kind: String) -> std::result::Result { let kind = parse_sequence_kind_r(&sequence_kind).map_err(Error::Other)?; let conn = self.context.graph().conn(); - let graph = - BlockGroup::get_graph(conn, &self.id, None).map_err(|e| Error::Other(e.to_string()))?; + let graph = gen_graph::models::load_block_group_graph(conn, &self.id, None) + .map_err(|e| Error::Other(e.to_string()))?; let matcher = GenGraphMatcher::new_with_sequence_kind(conn, graph, kind); let index_dir = self .context @@ -2462,8 +2462,8 @@ impl SequenceGraph { fn to_dict(&self) -> std::result::Result { let conn = self.context.graph().conn(); - let graph = - BlockGroup::get_graph(conn, &self.id, None).map_err(|e| Error::Other(e.to_string()))?; + let graph = gen_graph::models::load_block_group_graph(conn, &self.id, None) + .map_err(|e| Error::Other(e.to_string()))?; let nodes = graph .nodes() diff --git a/src/diffs/gfa.rs b/src/diffs/gfa.rs index bbf5e1ff..a3c5aae8 100644 --- a/src/diffs/gfa.rs +++ b/src/diffs/gfa.rs @@ -419,8 +419,12 @@ mod tests { let new_child_block_group = Collection::get_block_groups(conn, "test collection 2", None) .pop() .unwrap(); - let all_child_sequences = - BlockGroup::get_all_sequences(conn, &new_child_block_group.id, false).unwrap(); + let all_child_sequences = gen_graph::models::get_all_sequences_with_pruning( + conn, + &new_child_block_group.id, + false, + ) + .unwrap(); // We've replaced the middle AAAA with CCCC, so expect that as the child sequence assert_eq!( @@ -504,8 +508,12 @@ mod tests { Collection::get_block_groups(conn, "test collection 3", None) .pop() .unwrap(); - let all_grandchild_sequences = - BlockGroup::get_all_sequences(conn, &new_grandchild_block_group.id, false).unwrap(); + let all_grandchild_sequences = gen_graph::models::get_all_sequences_with_pruning( + conn, + &new_grandchild_block_group.id, + false, + ) + .unwrap(); // We've replaced the middle AAAA with CCCC and the middle TTTT with GGGG, so four possible sequences assert_eq!( @@ -535,8 +543,12 @@ mod tests { Collection::get_block_groups(conn, "test collection 4", None) .pop() .unwrap(); - let all_grandchild_sequences = - BlockGroup::get_all_sequences(conn, &new_grandchild_block_group.id, false).unwrap(); + let all_grandchild_sequences = gen_graph::models::get_all_sequences_with_pruning( + conn, + &new_grandchild_block_group.id, + false, + ) + .unwrap(); assert_eq!( all_grandchild_sequences, @@ -643,7 +655,8 @@ mod tests { .pop() .unwrap(); let all_sequences = - BlockGroup::get_all_sequences(conn, &new_block_group.id, false).unwrap(); + gen_graph::models::get_all_sequences_with_pruning(conn, &new_block_group.id, false) + .unwrap(); assert_eq!( all_sequences, @@ -750,7 +763,8 @@ mod tests { .pop() .unwrap(); let all_sequences = - BlockGroup::get_all_sequences(conn, &new_block_group.id, false).unwrap(); + gen_graph::models::get_all_sequences_with_pruning(conn, &new_block_group.id, false) + .unwrap(); assert_eq!( all_sequences, @@ -916,7 +930,8 @@ mod tests { .pop() .unwrap(); let all_sequences = - BlockGroup::get_all_sequences(conn, &new_block_group.id, false).unwrap(); + gen_graph::models::get_all_sequences_with_pruning(conn, &new_block_group.id, false) + .unwrap(); assert_eq!( all_sequences, @@ -1083,7 +1098,8 @@ mod tests { .pop() .unwrap(); let all_sequences = - BlockGroup::get_all_sequences(conn, &new_block_group.id, false).unwrap(); + gen_graph::models::get_all_sequences_with_pruning(conn, &new_block_group.id, false) + .unwrap(); assert_eq!( all_sequences, @@ -1225,8 +1241,12 @@ mod tests { let new_child_block_group = Collection::get_block_groups(conn, "test collection 2", None) .pop() .unwrap(); - let all_child_sequences = - BlockGroup::get_all_sequences(conn, &new_child_block_group.id, false).unwrap(); + let all_child_sequences = gen_graph::models::get_all_sequences_with_pruning( + conn, + &new_child_block_group.id, + false, + ) + .unwrap(); // We've replaced [2, 6) of AAAA with CCCC assert_eq!( @@ -1311,8 +1331,12 @@ mod tests { Collection::get_block_groups(conn, "test collection 3", None) .pop() .unwrap(); - let all_grandchild_sequences = - BlockGroup::get_all_sequences(conn, &new_grandchild_block_group.id, false).unwrap(); + let all_grandchild_sequences = gen_graph::models::get_all_sequences_with_pruning( + conn, + &new_grandchild_block_group.id, + false, + ) + .unwrap(); // Original is AAAAAAAAAAAAAAAA // Grandchild is AACCGGGGAAAAAA @@ -1339,8 +1363,12 @@ mod tests { Collection::get_block_groups(conn, "test collection 4", None) .pop() .unwrap(); - let all_grandchild_sequences = - BlockGroup::get_all_sequences(conn, &new_grandchild_block_group.id, false).unwrap(); + let all_grandchild_sequences = gen_graph::models::get_all_sequences_with_pruning( + conn, + &new_grandchild_block_group.id, + false, + ) + .unwrap(); // Child is AACCCCAAAAAAAAAA // Grandchild is AACCGGGGAAAAAA diff --git a/src/exports/genbank.rs b/src/exports/genbank.rs index ce81bf03..2b50f5af 100644 --- a/src/exports/genbank.rs +++ b/src/exports/genbank.rs @@ -297,7 +297,7 @@ pub fn export_genbank( )?; // Identify the node traversal corresponding to our path. - let graph = BlockGroup::get_graph(conn, &block_group.id, history_ref)?; + let graph = gen_graph::models::load_block_group_graph(conn, &block_group.id, history_ref)?; let path_nodes = get_path_nodes(&graph, &path_blocks); let path_node_set: HashSet<&GraphNode> = HashSet::from_iter(&path_nodes); let mut node_it = path_nodes.iter().peekable(); diff --git a/src/exports/gfa.rs b/src/exports/gfa.rs index a592ad96..c1093f5f 100644 --- a/src/exports/gfa.rs +++ b/src/exports/gfa.rs @@ -89,7 +89,9 @@ pub fn export_gfa( blocks.sort_by_key(|a| a.node_id); - let (gen_graph, _edges_by_node_pair) = Edge::build_graph(&edges, &blocks); + let (load_edges, load_blocks) = Edge::graph_load_data(&edges, &blocks); + let (gen_graph, _edges_by_node_pair) = + gen_graph::graph_loader::build_graph(&load_edges, &load_blocks); // Create GenGraph from the built graph let mut graph = GenGraph::new(); @@ -337,7 +339,6 @@ mod tests { use gen_core::{PATH_END_NODE_ID, PATH_START_NODE_ID, Strand, path::PathBlock}; use gen_graph::GraphNode; use gen_models::{ - annotations::add_annotation, block_group::{BlockGroup, BlockGroupChange}, block_group_edge::BlockGroupEdgeData, collection::Collection, @@ -502,7 +503,9 @@ mod tests { ) .unwrap(); - let all_sequences = BlockGroup::get_all_sequences(conn, &block_group.id, false).unwrap(); + let all_sequences = + gen_graph::models::get_all_sequences_with_pruning(conn, &block_group.id, false) + .unwrap(); let temp_dir = tempdir().expect("Couldn't get handle to temp directory"); let mut gfa_path = PathBuf::from(temp_dir.path()); @@ -528,7 +531,9 @@ mod tests { let block_group2 = Collection::get_block_groups(conn, "test collection 2", None) .pop() .unwrap(); - let all_sequences2 = BlockGroup::get_all_sequences(conn, &block_group2.id, false).unwrap(); + let all_sequences2 = + gen_graph::models::get_all_sequences_with_pruning(conn, &block_group2.id, false) + .unwrap(); assert_eq!(all_sequences, all_sequences2); @@ -567,7 +572,7 @@ mod tests { false, ) .unwrap(); - add_annotation( + gen_graph::models::add_annotation( &context, collection, "SITE", @@ -593,7 +598,7 @@ mod tests { .unwrap(); let block_group = get_sample_bg(conn, collection, "deleted"); - let graph = BlockGroup::get_graph(conn, &block_group.id, None).unwrap(); + let graph = gen_graph::models::load_block_group_graph(conn, &block_group.id, None).unwrap(); let node_ids = graph.nodes().map(|node| node.node_id).collect::>(); let sequences = Node::get_sequences_by_node_ids(conn, &node_ids, None); let rendered_sequence = |node: GraphNode| { @@ -697,7 +702,8 @@ mod tests { let conn = context.graph().conn(); let (bg_id, _path) = setup_block_group(conn); - let all_sequences = BlockGroup::get_all_sequences(conn, &bg_id, false).unwrap(); + let all_sequences = + gen_graph::models::get_all_sequences_with_pruning(conn, &bg_id, false).unwrap(); let temp_dir = tempdir().expect("Couldn't get handle to temp directory"); let gfa_path = PathBuf::from(temp_dir.path()).join("split.gfa"); @@ -714,11 +720,14 @@ mod tests { let block_group2 = Collection::get_block_groups(conn, "test collection 2", None) .pop() .unwrap(); - let all_sequences2 = BlockGroup::get_all_sequences(conn, &block_group2.id, false).unwrap(); + let all_sequences2 = + gen_graph::models::get_all_sequences_with_pruning(conn, &block_group2.id, false) + .unwrap(); assert_eq!(all_sequences, all_sequences2); - let graph = BlockGroup::get_graph(conn, &block_group2.id, None).unwrap(); + let graph = + gen_graph::models::load_block_group_graph(conn, &block_group2.id, None).unwrap(); let graph_nodes = graph .nodes() .filter_map(|node| { @@ -751,7 +760,9 @@ mod tests { let _ = import_gfa(&context, &gfa_path, &collection_name, Sample::DEFAULT_NAME); let block_group_id = BlockGroup::get_id(&collection_name, Sample::DEFAULT_NAME, "", None); - let all_sequences = BlockGroup::get_all_sequences(conn, &block_group_id, false).unwrap(); + let all_sequences = + gen_graph::models::get_all_sequences_with_pruning(conn, &block_group_id, false) + .unwrap(); let temp_dir = tempdir().expect("Couldn't get handle to temp directory"); let mut gfa_path = PathBuf::from(temp_dir.path()); @@ -776,7 +787,9 @@ mod tests { let block_group2 = Collection::get_block_groups(conn, "test collection 2", None) .pop() .unwrap(); - let all_sequences2 = BlockGroup::get_all_sequences(conn, &block_group2.id, false).unwrap(); + let all_sequences2 = + gen_graph::models::get_all_sequences_with_pruning(conn, &block_group2.id, false) + .unwrap(); assert_eq!(all_sequences, all_sequences2); } @@ -792,7 +805,9 @@ mod tests { let _ = import_gfa(&context, &gfa_path, &collection_name, Sample::DEFAULT_NAME); let block_group_id = BlockGroup::get_id(&collection_name, Sample::DEFAULT_NAME, "", None); - let all_sequences = BlockGroup::get_all_sequences(conn, &block_group_id, false).unwrap(); + let all_sequences = + gen_graph::models::get_all_sequences_with_pruning(conn, &block_group_id, false) + .unwrap(); let temp_dir = tempdir().expect("Couldn't get handle to temp directory"); let mut gfa_path = PathBuf::from(temp_dir.path()); @@ -817,7 +832,9 @@ mod tests { let block_group2 = Collection::get_block_groups(conn, "anderson promoters 2", None) .pop() .unwrap(); - let all_sequences2 = BlockGroup::get_all_sequences(conn, &block_group2.id, false).unwrap(); + let all_sequences2 = + gen_graph::models::get_all_sequences_with_pruning(conn, &block_group2.id, false) + .unwrap(); assert_eq!(all_sequences, all_sequences2); } @@ -833,7 +850,9 @@ mod tests { let _ = import_gfa(&context, &gfa_path, &collection_name, Sample::DEFAULT_NAME); let block_group_id = BlockGroup::get_id(&collection_name, Sample::DEFAULT_NAME, "", None); - let all_sequences = BlockGroup::get_all_sequences(conn, &block_group_id, false).unwrap(); + let all_sequences = + gen_graph::models::get_all_sequences_with_pruning(conn, &block_group_id, false) + .unwrap(); let temp_dir = tempdir().expect("Couldn't get handle to temp directory"); let mut gfa_path = PathBuf::from(temp_dir.path()); @@ -858,7 +877,9 @@ mod tests { let block_group2 = Collection::get_block_groups(conn, "test collection 2", None) .pop() .unwrap(); - let all_sequences2 = BlockGroup::get_all_sequences(conn, &block_group2.id, false).unwrap(); + let all_sequences2 = + gen_graph::models::get_all_sequences_with_pruning(conn, &block_group2.id, false) + .unwrap(); assert_eq!(all_sequences, all_sequences2); } @@ -897,7 +918,7 @@ mod tests { phased: 0, preserve_edge: true, }; - BlockGroup::insert_change(conn, &change).unwrap(); + gen_graph::models::insert_change(conn, &change).unwrap(); let augmented_edges = BlockGroupEdge::edges_for_block_group(conn, &block_group_id, None); let mut node_ids = HashSet::new(); @@ -923,6 +944,7 @@ mod tests { // 7 total assert_eq!(edge_ids.len(), 7); + let node_ids = node_ids.into_iter().collect::>(); let nodes = Node::query_by_ids(conn, &node_ids, None); let mut node_hashes = HashSet::new(); for node in nodes { @@ -974,6 +996,7 @@ mod tests { // 7 total assert_eq!(edge_ids2.len(), 7); + let node_ids2 = node_ids2.into_iter().collect::>(); let nodes2 = Node::query_by_ids(conn, &node_ids2, None); let mut node_hashes2 = HashSet::new(); for node in nodes2 { diff --git a/src/graphs/graph_search.rs b/src/graphs/graph_search.rs index 52acb956..2771b231 100644 --- a/src/graphs/graph_search.rs +++ b/src/graphs/graph_search.rs @@ -771,7 +771,7 @@ fn validate_seed_index_header_version(header: &SeedIndexHeader) -> Result<(), Se #[cfg(test)] mod tests { - use gen_models::{block_group::BlockGroup, collection::Collection}; + use gen_models::collection::Collection; use super::*; use crate::test_helpers::{setup_block_group, setup_gen}; @@ -784,7 +784,7 @@ mod tests { let conn = ctx.graph().conn(); Collection::create(conn, "test").unwrap(); let (block_group_id, _path) = setup_block_group(conn); - let graph = BlockGroup::get_graph(conn, &block_group_id, None).unwrap(); + let graph = gen_graph::models::load_block_group_graph(conn, &block_group_id, None).unwrap(); GenGraphMatcher::new(conn, graph) } @@ -793,7 +793,7 @@ mod tests { let conn = ctx.graph().conn(); let _ = Collection::create(conn, "test"); let (block_group_id, _path) = setup_block_group(conn); - let graph = BlockGroup::get_graph(conn, &block_group_id, None).unwrap(); + let graph = gen_graph::models::load_block_group_graph(conn, &block_group_id, None).unwrap(); GenGraphMatcher::new_ssdna(conn, graph) } @@ -802,7 +802,7 @@ mod tests { let conn = ctx.graph().conn(); Collection::create(conn, "test").unwrap(); let (block_group_id, _path) = setup_block_group(conn); - let graph = BlockGroup::get_graph(conn, &block_group_id, None).unwrap(); + let graph = gen_graph::models::load_block_group_graph(conn, &block_group_id, None).unwrap(); GenGraphMatcher::new_protein(conn, graph) } @@ -811,7 +811,7 @@ mod tests { let conn = ctx.graph().conn(); Collection::create(conn, "test").unwrap(); let (block_group_id, _path) = setup_block_group(conn); - let graph = BlockGroup::get_graph(conn, &block_group_id, None).unwrap(); + let graph = gen_graph::models::load_block_group_graph(conn, &block_group_id, None).unwrap(); GenGraphMatcher::new_with_sequence_kind(conn, graph, SequenceKind::Exact) } diff --git a/src/graphs/operators.rs b/src/graphs/operators.rs index f0440945..0ad055dd 100644 --- a/src/graphs/operators.rs +++ b/src/graphs/operators.rs @@ -167,7 +167,7 @@ pub fn derive_chunks( let end_block = blocks[blocks.len() - 1]; let end_node_coordinate = end_coordinate - end_block.start + end_block.sequence_start; - BlockGroup::derive_subgraph( + gen_graph::models::derive_subgraph( conn, &parent_block_group_id, &start_block, @@ -469,7 +469,7 @@ fn validate_stitched_block_group_is_acyclic( conn: &GraphConnection, block_group_id: &HashId, ) -> Result<(), GraphOperationError> { - let graph = BlockGroup::get_graph(conn, block_group_id, None)?; + let graph = gen_graph::models::load_block_group_graph(conn, block_group_id, None)?; if is_cyclic_directed(&graph) { return Err(GraphOperationError::StitchedGraphCycle(format!( "block group {block_group_id} is cyclic" @@ -656,7 +656,9 @@ mod tests { "AAAAAAAAAATTTTTTAAAAAAAACCCCCCGGGGGGGGGG" ); - let all_sequences = BlockGroup::get_all_sequences(conn, &block_group1_id, false).unwrap(); + let all_sequences = + gen_graph::models::get_all_sequences_with_pruning(conn, &block_group1_id, false) + .unwrap(); assert_eq!( all_sequences, HashSet::from_iter(vec![ @@ -681,7 +683,9 @@ mod tests { let block_groups = Sample::get_block_groups(conn, "test", Sample::DEFAULT_NAME, None); let block_group2 = block_groups.iter().find(|x| x.name == "chr1").unwrap(); - let all_sequences2 = BlockGroup::get_all_sequences(conn, &block_group2.id, false).unwrap(); + let all_sequences2 = + gen_graph::models::get_all_sequences_with_pruning(conn, &block_group2.id, false) + .unwrap(); assert_eq!( all_sequences2, HashSet::from_iter(vec!["TTTTTCCCCC".to_string(), "TAAAAAAAAC".to_string(),]) @@ -738,7 +742,8 @@ mod tests { Sample::get_block_groups(conn, collection, Sample::DEFAULT_NAME, None); let original_block_group_id = &original_block_groups[0].id; let all_original_sequences = - BlockGroup::get_all_sequences(conn, original_block_group_id, false).unwrap(); + gen_graph::models::get_all_sequences_with_pruning(conn, original_block_group_id, false) + .unwrap(); assert_eq!( all_original_sequences, HashSet::from_iter(vec!["ATCGATCGATCGATCGATCGGGAACACACAGAGA".to_string(),]) @@ -746,8 +751,12 @@ mod tests { let grandchild_block_groups = Sample::get_block_groups(conn, collection, "test2", None); let grandchild_block_group_id = &grandchild_block_groups[0].id; - let all_grandchild_sequences = - BlockGroup::get_all_sequences(conn, grandchild_block_group_id, false).unwrap(); + let all_grandchild_sequences = gen_graph::models::get_all_sequences_with_pruning( + conn, + grandchild_block_group_id, + false, + ) + .unwrap(); assert_eq!( all_grandchild_sequences, HashSet::from_iter(vec![ @@ -779,7 +788,9 @@ mod tests { let block_groups = Sample::get_block_groups(conn, collection, "test3", None); let block_group2 = block_groups.iter().find(|x| x.name == "m123.2").unwrap(); - let all_sequences2 = BlockGroup::get_all_sequences(conn, &block_group2.id, false).unwrap(); + let all_sequences2 = + gen_graph::models::get_all_sequences_with_pruning(conn, &block_group2.id, false) + .unwrap(); assert_eq!( all_sequences2, HashSet::from_iter(vec!["TCAATCG".to_string(), "TCGATCG".to_string(),]) @@ -789,7 +800,9 @@ mod tests { assert_eq!(path2.sequence(conn, None).unwrap(), "TCAATCG"); let block_group3 = block_groups.iter().find(|x| x.name == "m123.3").unwrap(); - let all_sequences3 = BlockGroup::get_all_sequences(conn, &block_group3.id, false).unwrap(); + let all_sequences3 = + gen_graph::models::get_all_sequences_with_pruning(conn, &block_group3.id, false) + .unwrap(); assert_eq!( all_sequences3, HashSet::from_iter(vec![ @@ -849,7 +862,8 @@ mod tests { Sample::get_block_groups(conn, collection, Sample::DEFAULT_NAME, None); let original_block_group_id = &original_block_groups[0].id; let all_original_sequences = - BlockGroup::get_all_sequences(conn, original_block_group_id, false).unwrap(); + gen_graph::models::get_all_sequences_with_pruning(conn, original_block_group_id, false) + .unwrap(); assert_eq!( all_original_sequences, HashSet::from_iter(vec!["ATCGATCGATCGATCGATCGGGAACACACAGAGA".to_string(),]) @@ -857,8 +871,12 @@ mod tests { let grandchild_block_groups = Sample::get_block_groups(conn, collection, "test2", None); let grandchild_block_group_id = &grandchild_block_groups[0].id; - let all_grandchild_sequences = - BlockGroup::get_all_sequences(conn, grandchild_block_group_id, false).unwrap(); + let all_grandchild_sequences = gen_graph::models::get_all_sequences_with_pruning( + conn, + grandchild_block_group_id, + false, + ) + .unwrap(); assert_eq!( all_grandchild_sequences, HashSet::from_iter(vec![ @@ -890,7 +908,9 @@ mod tests { let block_groups = Sample::get_block_groups(conn, collection, "test3", None); let block_group2 = block_groups.iter().find(|x| x.name == "m123.2").unwrap(); - let all_sequences2 = BlockGroup::get_all_sequences(conn, &block_group2.id, false).unwrap(); + let all_sequences2 = + gen_graph::models::get_all_sequences_with_pruning(conn, &block_group2.id, false) + .unwrap(); assert_eq!( all_sequences2, HashSet::from_iter(vec!["TCAATCG".to_string(), "TCGATCG".to_string(),]) @@ -900,7 +920,9 @@ mod tests { assert_eq!(path2.sequence(conn, None).unwrap(), "TCAATCG"); let block_group3 = block_groups.iter().find(|x| x.name == "m123.3").unwrap(); - let all_sequences3 = BlockGroup::get_all_sequences(conn, &block_group3.id, false).unwrap(); + let all_sequences3 = + gen_graph::models::get_all_sequences_with_pruning(conn, &block_group3.id, false) + .unwrap(); assert_eq!( all_sequences3, HashSet::from_iter(vec![ @@ -929,7 +951,9 @@ mod tests { .find(|x| x.name == "m123.stitched") .unwrap(); - let all_sequences4 = BlockGroup::get_all_sequences(conn, &block_group4.id, false).unwrap(); + let all_sequences4 = + gen_graph::models::get_all_sequences_with_pruning(conn, &block_group4.id, false) + .unwrap(); assert_eq!( all_sequences4, HashSet::from_iter(vec![ @@ -964,7 +988,9 @@ mod tests { .find(|x| x.name == "m123.reverse-stitched") .unwrap(); - let all_sequences5 = BlockGroup::get_all_sequences(conn, &block_group5.id, false).unwrap(); + let all_sequences5 = + gen_graph::models::get_all_sequences_with_pruning(conn, &block_group5.id, false) + .unwrap(); assert_eq!( all_sequences5, HashSet::from_iter(vec![ diff --git a/src/graphs/translation.rs b/src/graphs/translation.rs index 4eec64b4..5698d253 100644 --- a/src/graphs/translation.rs +++ b/src/graphs/translation.rs @@ -607,7 +607,7 @@ fn extract_full_graph( conn: &GraphConnection, block_group_id: &HashId, ) -> Result { - let gen_graph = BlockGroup::get_graph(conn, block_group_id, None) + let gen_graph = gen_graph::models::load_block_group_graph(conn, block_group_id, None) .map_err(|e| TranslationError::BlockGroupError(e.to_string()))?; let start_node = gen_graph .nodes() @@ -694,7 +694,7 @@ fn extract_from_entry( entry_node_id: HashId, entry_coord: i64, ) -> Result { - let gen_graph = BlockGroup::get_graph(conn, block_group_id, None) + let gen_graph = gen_graph::models::load_block_group_graph(conn, block_group_id, None) .map_err(|e| TranslationError::BlockGroupError(e.to_string()))?; let entry_node = gen_graph @@ -1846,8 +1846,8 @@ ncbieaa "FFLLSSSSYY**CC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG" /// Whether PATH_END is reachable from PATH_START in the protein graph. fn start_reaches_end(conn: &GraphConnection, block_group_id: &HashId) -> bool { - let graph = - BlockGroup::get_graph(conn, block_group_id, None).expect("should load protein graph"); + let graph = gen_graph::models::load_block_group_graph(conn, block_group_id, None) + .expect("should load protein graph"); let start = graph.nodes().find(|n| n.node_id == PATH_START_NODE_ID); let end = graph.nodes().find(|n| n.node_id == PATH_END_NODE_ID); match (start, end) { @@ -1864,7 +1864,7 @@ ncbieaa "FFLLSSSSYY**CC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG" let protein = translate_annotation(&conn, &annotation, Some(&block_group_id), params).unwrap(); assert_eq!( - BlockGroup::get_all_sequences(&conn, &protein.id, true).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(&conn, &protein.id, true).unwrap(), HashSet::from(["ME*".to_string()]) ); } @@ -1912,7 +1912,7 @@ ncbieaa "FFLLSSSSYY**CC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG" let protein = translate_annotation(&conn, &annotation, Some(&block_group_id), params).unwrap(); assert_eq!( - BlockGroup::get_all_sequences(&conn, &protein.id, true).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(&conn, &protein.id, true).unwrap(), HashSet::from(["GM".to_string()]) ); } @@ -1926,7 +1926,7 @@ ncbieaa "FFLLSSSSYY**CC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG" let protein = translate_annotation(&conn, &annotation, Some(&block_group_id), params).unwrap(); assert_eq!( - BlockGroup::get_all_sequences(&conn, &protein.id, true).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(&conn, &protein.id, true).unwrap(), HashSet::from(["ME*".to_string()]) ); } @@ -1940,7 +1940,7 @@ ncbieaa "FFLLSSSSYY**CC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG" let protein = translate_annotation(&conn, &annotation, Some(&block_group_id), params).unwrap(); assert_eq!( - BlockGroup::get_all_sequences(&conn, &protein.id, true).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(&conn, &protein.id, true).unwrap(), HashSet::from(["ME*".to_string()]) ); } @@ -1954,7 +1954,7 @@ ncbieaa "FFLLSSSSYY**CC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG" let protein = translate_annotation(&conn, &annotation, Some(&block_group_id), params).unwrap(); assert_eq!( - BlockGroup::get_all_sequences(&conn, &protein.id, true).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(&conn, &protein.id, true).unwrap(), HashSet::from(["WE*".to_string()]) ); } @@ -1967,7 +1967,7 @@ ncbieaa "FFLLSSSSYY**CC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG" let protein = translate_annotation(&conn, &annotation, Some(&block_group_id), params).unwrap(); assert_eq!( - BlockGroup::get_all_sequences(&conn, &protein.id, true).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(&conn, &protein.id, true).unwrap(), HashSet::from(["MW".to_string()]) ); } @@ -2059,7 +2059,7 @@ ncbieaa "FFLLSSSSYY**CC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG" translate_annotation(&conn, &annotation, Some(&block_group.id), params).unwrap(); assert_eq!( - BlockGroup::get_all_sequences(&conn, &protein.id, true).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(&conn, &protein.id, true).unwrap(), HashSet::from([expected.to_string()]), "reverse-strand protein should read N → C left to right" ); @@ -2217,8 +2217,8 @@ ncbieaa "FFLLSSSSYY**CC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG" start_reaches_end(&conn, &protein.id), "protein graph has no PATH_START → PATH_END path (disconnected)" ); - let protein_graph = - BlockGroup::get_graph(&conn, &protein.id, None).expect("should load protein graph"); + let protein_graph = gen_graph::models::load_block_group_graph(&conn, &protein.id, None) + .expect("should load protein graph"); assert_eq!( connected_components(&protein_graph), 1, @@ -2236,7 +2236,7 @@ ncbieaa "FFLLSSSSYY**CC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG" let protein = translate_annotation(&conn, &annotation, Some(&block_group_id), params).unwrap(); assert_eq!( - BlockGroup::get_all_sequences(&conn, &protein.id, true).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(&conn, &protein.id, true).unwrap(), HashSet::from(["M*".to_string()]) ); } @@ -2297,7 +2297,7 @@ ncbieaa "FFLLSSSSYY**CC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG" ) .unwrap(); assert_eq!( - BlockGroup::get_all_sequences(&conn, &protein.id, true).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(&conn, &protein.id, true).unwrap(), HashSet::from(["ME*".to_string()]), "translation should only follow the annotation's own entry node" ); @@ -2313,7 +2313,7 @@ ncbieaa "FFLLSSSSYY**CC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG" let protein = translate_annotation(&conn, &annotation, Some(&block_group_id), params).unwrap(); assert_eq!( - BlockGroup::get_all_sequences(&conn, &protein.id, true).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(&conn, &protein.id, true).unwrap(), HashSet::from(["*".to_string()]) ); } @@ -2334,7 +2334,7 @@ ncbieaa "FFLLSSSSYY**CC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG" let protein = translate_annotation(&conn, &annotation, Some(&block_group_id), params).unwrap(); assert_eq!( - BlockGroup::get_all_sequences(&conn, &protein.id, true).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(&conn, &protein.id, true).unwrap(), HashSet::from(["MP*".to_string(), "MPVG*".to_string()]), "frameshift deletion should truncate the variant protein at the premature stop" ); @@ -2355,7 +2355,7 @@ ncbieaa "FFLLSSSSYY**CC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG" let protein = translate_annotation(&conn, &annotation, Some(&block_group_id), params).unwrap(); assert_eq!( - BlockGroup::get_all_sequences(&conn, &protein.id, true).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(&conn, &protein.id, true).unwrap(), HashSet::from(["MK*".to_string(), "MND*".to_string()]), "frameshift deletion should read through the wild-type stop to a later one" ); @@ -2547,12 +2547,15 @@ ncbieaa "FFLLSSSSYY**CC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG" .unwrap(); assert_eq!( - BlockGroup::get_all_sequences(&conn, &parent_protein.id, true).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(&conn, &parent_protein.id, true) + .unwrap(), HashSet::from(["ME*".to_string()]) ); assert_eq!( - BlockGroup::get_all_sequences(&conn, &child_protein.id, true).unwrap(), - BlockGroup::get_all_sequences(&conn, &parent_protein.id, true).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(&conn, &child_protein.id, true) + .unwrap(), + gen_graph::models::get_all_sequences_with_pruning(&conn, &parent_protein.id, true) + .unwrap(), "upstream deletion changed the annotated protein", ); } @@ -2629,16 +2632,20 @@ ncbieaa "FFLLSSSSYY**CC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG" .unwrap(); assert_eq!( - BlockGroup::get_all_sequences(&conn, &parent_protein.id, true).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(&conn, &parent_protein.id, true) + .unwrap(), HashSet::from(["ME*".to_string()]) ); assert_eq!( - BlockGroup::get_all_sequences(&conn, &child_protein.id, true).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(&conn, &child_protein.id, true) + .unwrap(), HashSet::from(["MQ*".to_string()]) ); assert_ne!( - BlockGroup::get_all_sequences(&conn, &child_protein.id, true).unwrap(), - BlockGroup::get_all_sequences(&conn, &parent_protein.id, true).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(&conn, &child_protein.id, true) + .unwrap(), + gen_graph::models::get_all_sequences_with_pruning(&conn, &parent_protein.id, true) + .unwrap(), "point mutation inside the CDS did not change the protein", ); } @@ -2704,7 +2711,7 @@ ncbieaa "FFLLSSSSYY**CC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG" .unwrap(); assert_eq!( - BlockGroup::get_all_sequences(&conn, &protein.id, true).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(&conn, &protein.id, true).unwrap(), HashSet::from(["ME*".to_string()]), "translation should only follow the annotation's own entry node" ); @@ -2769,7 +2776,7 @@ ncbieaa "FFLLSSSSYY**CC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG" .unwrap(); assert_eq!( - BlockGroup::get_all_sequences(&conn, &protein.id, true).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(&conn, &protein.id, true).unwrap(), HashSet::from(["ME*".to_string(), "MEW".to_string()]), "last-base variant was dropped from the extracted subgraph", ); @@ -2822,7 +2829,7 @@ ncbieaa "FFLLSSSSYY**CC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG" translate_from_path(&conn, &block_group.id, 0, TranslationParams::new("test")).unwrap(); assert_eq!( - BlockGroup::get_all_sequences(&conn, &from_path.id, true).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(&conn, &from_path.id, true).unwrap(), HashSet::from(["*".to_string()]), "translate_from_path(0) should only translate the literal entry node (A), \ not the unrelated D/P/Z branch reachable from PATH_START" @@ -2844,7 +2851,7 @@ ncbieaa "FFLLSSSSYY**CC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG" let protein = translate_block_group(&conn, &block_group_id, params).unwrap(); assert_eq!( - BlockGroup::get_all_sequences(&conn, &protein.id, true).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(&conn, &protein.id, true).unwrap(), HashSet::from(["MKPEGF*".to_string(), "MKPQGF*".to_string()]) ); } @@ -2870,7 +2877,7 @@ ncbieaa "FFLLSSSSYY**CC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG" translate_annotation(&conn, &annotation, Some(&block_group_id), params).unwrap(); assert_eq!( - BlockGroup::get_all_sequences(&conn, &protein.id, true).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(&conn, &protein.id, true).unwrap(), HashSet::from(["MKPEGF*".to_string(), "MKPQGF*".to_string()]) ); } @@ -2887,7 +2894,7 @@ ncbieaa "FFLLSSSSYY**CC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG" let protein = translate_from_path(&conn, &block_group_id, 0, params).unwrap(); assert_eq!( - BlockGroup::get_all_sequences(&conn, &protein.id, true).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(&conn, &protein.id, true).unwrap(), HashSet::from(["MKPEGF*".to_string(), "MKPQGF*".to_string()]) ); } diff --git a/src/imports/fasta.rs b/src/imports/fasta.rs index 05df8727..f0b5a117 100644 --- a/src/imports/fasta.rs +++ b/src/imports/fasta.rs @@ -243,7 +243,8 @@ mod tests { let block_group_id = BlockGroup::get_id("test", Sample::DEFAULT_NAME, "m123", None); assert_eq!( - BlockGroup::get_all_sequences(conn, &block_group_id, false).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(conn, &block_group_id, false) + .unwrap(), HashSet::from_iter(vec!["ATCGATCGATCGATCGATCGGGAACACACAGAGA".to_string()]) ); @@ -272,7 +273,8 @@ mod tests { .unwrap(); let block_group_id = BlockGroup::get_id("test", Sample::DEFAULT_NAME, "m123", None); assert_eq!( - BlockGroup::get_all_sequences(conn, &block_group_id, false).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(conn, &block_group_id, false) + .unwrap(), HashSet::from_iter(vec!["ATCGATCGATCGATCGATCGGGAACACACAGAGA".to_string()]) ); } @@ -319,7 +321,8 @@ mod tests { .unwrap(); let block_group_id = BlockGroup::get_id("test", Sample::DEFAULT_NAME, "m123", None); assert_eq!( - BlockGroup::get_all_sequences(conn, &block_group_id, false).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(conn, &block_group_id, false) + .unwrap(), HashSet::from_iter(vec!["ATCGATCGATCGATCGATCGGGAACACACAGAGA".to_string()]) ); } @@ -342,7 +345,8 @@ mod tests { .unwrap(); let block_group_id = BlockGroup::get_id("test", "new-sample", "m123", None); assert_eq!( - BlockGroup::get_all_sequences(conn, &block_group_id, false).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(conn, &block_group_id, false) + .unwrap(), HashSet::from_iter(vec!["ATCGATCGATCGATCGATCGGGAACACACAGAGA".to_string()]) ); @@ -375,7 +379,8 @@ mod tests { .unwrap(); let block_group_id = BlockGroup::get_id("test", Sample::DEFAULT_NAME, "m123", None); assert_eq!( - BlockGroup::get_all_sequences(conn, &block_group_id, false).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(conn, &block_group_id, false) + .unwrap(), HashSet::from_iter(vec!["ATCGATCGATCGATCGATCGGGAACACACAGAGA".to_string()]) ); diff --git a/src/imports/genbank.rs b/src/imports/genbank.rs index 05bf68a6..e89a32ba 100644 --- a/src/imports/genbank.rs +++ b/src/imports/genbank.rs @@ -630,7 +630,7 @@ where None, ), }; - BlockGroup::insert_change(conn, &change).unwrap(); + gen_graph::models::insert_change(conn, &change).unwrap(); applied_changes.push((edit, change_node_id)); } @@ -1199,7 +1199,9 @@ mod tests { let f = reader::parse_file(&path).unwrap(); let seq = str::from_utf8(&f[0].seq).unwrap().to_string(); let block_group_id = BlockGroup::get_id("", Sample::DEFAULT_NAME, "insertion", None); - let seqs = BlockGroup::get_all_sequences(conn, &block_group_id, false).unwrap(); + let seqs = + gen_graph::models::get_all_sequences_with_pruning(conn, &block_group_id, false) + .unwrap(); assert_eq!( seqs, HashSet::from_iter([ @@ -1247,7 +1249,9 @@ mod tests { GATGCCATTGGGATATATCAACGGTGGTATATCCAGTGATTTTTTTCTCCAT", ); let block_group_id = BlockGroup::get_id("", Sample::DEFAULT_NAME, "deletion", None); - let seqs = BlockGroup::get_all_sequences(conn, &block_group_id, false).unwrap(); + let seqs = + gen_graph::models::get_all_sequences_with_pruning(conn, &block_group_id, false) + .unwrap(); assert_eq!( seqs, HashSet::from_iter([ @@ -1294,7 +1298,7 @@ mod tests { GGTACATTGAGCAACTGACTGAAATGCCTCAAAATGTTCTTTACGATGCCATTGGGAT ATATCAACGGTGGTATATCCAGTGATTTTTTTCTC", ); - let seqs = BlockGroup::get_all_sequences( + let seqs = gen_graph::models::get_all_sequences_with_pruning( conn, &BlockGroup::get_id("", Sample::DEFAULT_NAME, "deletion_and_insertion", None), false, @@ -1348,7 +1352,7 @@ mod tests { GGTACATTGAGCAACTGACTGAAATGCCTCAAAATGTTCTTTACGATGCCATTGGGAT ATATCAACGGTGGTATATCCAGTGATTTTTTTCTC", ); - let seqs = BlockGroup::get_all_sequences( + let seqs = gen_graph::models::get_all_sequences_with_pruning( conn, &BlockGroup::get_id("", Sample::DEFAULT_NAME, "substitution", None), false, @@ -1387,7 +1391,7 @@ mod tests { // there would be 4! sequences so we just check we have the fully changed and unchanged sequence let f = reader::parse_file(&path).unwrap(); let mod_seq = str::from_utf8(&f[0].seq).unwrap().to_string(); - let sequences: HashSet = BlockGroup::get_all_sequences( + let sequences: HashSet = gen_graph::models::get_all_sequences_with_pruning( conn, &BlockGroup::get_id("", Sample::DEFAULT_NAME, "insertion", None), false, diff --git a/src/imports/gfa.rs b/src/imports/gfa.rs index 3894a4c1..04263068 100644 --- a/src/imports/gfa.rs +++ b/src/imports/gfa.rs @@ -370,7 +370,7 @@ pub fn import_gfa( let bar = progress_bar.add(get_progress_bar(None)); bar.set_message("Breaking cycles"); let message_bar = progress_bar.add(get_message_bar()); - let graph = BlockGroup::get_graph(conn, &block_group.id, None)?; + let graph = gen_graph::models::load_block_group_graph(conn, &block_group.id, None)?; let mut undirected_graph: UnGraphMap = UnGraphMap::new(); for node in graph.nodes() { undirected_graph.add_node(node); @@ -558,7 +558,9 @@ mod tests { let _ = import_gfa(&context, &gfa_path, &collection_name, Sample::DEFAULT_NAME); let block_group_id = BlockGroup::get_id(&collection_name, Sample::DEFAULT_NAME, "", None); - let all_sequences = BlockGroup::get_all_sequences(conn, &block_group_id, false).unwrap(); + let all_sequences = + gen_graph::models::get_all_sequences_with_pruning(conn, &block_group_id, false) + .unwrap(); assert_eq!( all_sequences, HashSet::from_iter(vec!["AAAATTTTGGGGCCCC".to_string()]) @@ -711,7 +713,9 @@ mod tests { } } } - let all_sequences = BlockGroup::get_all_sequences(conn, &block_group_id, false).unwrap(); + let all_sequences = + gen_graph::models::get_all_sequences_with_pruning(conn, &block_group_id, false) + .unwrap(); assert_eq!(all_sequences.len(), 1024); assert_eq!(all_sequences, expected_sequences); @@ -739,7 +743,9 @@ mod tests { let result = path.sequence(conn, None); assert_eq!(result.unwrap(), "AA"); - let all_sequences = BlockGroup::get_all_sequences(conn, &block_group_id, false).unwrap(); + let all_sequences = + gen_graph::models::get_all_sequences_with_pruning(conn, &block_group_id, false) + .unwrap(); assert_eq!(all_sequences, HashSet::from_iter(vec!["AA".to_string()])); let node_count = Node::query(conn, "select * from nodes", rusqlite::params!()).len() as i64; @@ -757,7 +763,9 @@ mod tests { let block_group_id = BlockGroup::get_id(&collection_name, Sample::DEFAULT_NAME, "", None); - let all_sequences = BlockGroup::get_all_sequences(conn, &block_group_id, false).unwrap(); + let all_sequences = + gen_graph::models::get_all_sequences_with_pruning(conn, &block_group_id, false) + .unwrap(); assert_eq!( all_sequences, HashSet::from_iter(vec!["AAACCCTTTGGGACTCTA".to_string()]) @@ -777,7 +785,9 @@ mod tests { let block_group_id = BlockGroup::get_id(&collection_name, Sample::DEFAULT_NAME, "", None); - let all_sequences = BlockGroup::get_all_sequences(conn, &block_group_id, false).unwrap(); + let all_sequences = + gen_graph::models::get_all_sequences_with_pruning(conn, &block_group_id, false) + .unwrap(); assert_eq!( all_sequences, HashSet::from_iter(vec!["TTTGGGACTCTAAAACCC".to_string()]) diff --git a/src/imports/library.rs b/src/imports/library.rs index 7dff8eb6..7c5e6417 100644 --- a/src/imports/library.rs +++ b/src/imports/library.rs @@ -184,7 +184,9 @@ mod tests { } } - let actual_sequences = BlockGroup::get_all_sequences(conn, &block_group.id, false).unwrap(); + let actual_sequences = + gen_graph::models::get_all_sequences_with_pruning(conn, &block_group.id, false) + .unwrap(); assert_eq!(actual_sequences, expected_sequences); let current_path = BlockGroup::get_current_path(conn, &block_group.id, None).unwrap(); @@ -224,7 +226,9 @@ mod tests { let block_groups = Sample::get_block_groups(conn, collection, Sample::DEFAULT_NAME, None); let block_group = &block_groups[0]; - let all_sequences = BlockGroup::get_all_sequences(conn, &block_group.id, false).unwrap(); + let all_sequences = + gen_graph::models::get_all_sequences_with_pruning(conn, &block_group.id, false) + .unwrap(); assert_eq!( all_sequences, HashSet::from_iter(vec![ @@ -271,7 +275,9 @@ mod tests { expected_sequences.push(part1.to_string() + part2); } } - let all_sequences = BlockGroup::get_all_sequences(conn, &block_group.id, false).unwrap(); + let all_sequences = + gen_graph::models::get_all_sequences_with_pruning(conn, &block_group.id, false) + .unwrap(); assert_eq!( all_sequences, expected_sequences diff --git a/src/imports/sequences.rs b/src/imports/sequences.rs index 9850f815..b9d5ce02 100644 --- a/src/imports/sequences.rs +++ b/src/imports/sequences.rs @@ -285,7 +285,8 @@ mod tests { let block_group_id = BlockGroup::get_id("test", Sample::DEFAULT_NAME, "chr1", None); assert_eq!( - BlockGroup::get_all_sequences(conn, &block_group_id, false).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(conn, &block_group_id, false) + .unwrap(), HashSet::from_iter(vec!["ATCG".to_string()]) ); @@ -322,7 +323,8 @@ mod tests { let block_group_id = BlockGroup::get_id("test", Sample::DEFAULT_NAME, "region-a", None); assert_eq!( - BlockGroup::get_all_sequences(conn, &block_group_id, false).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(conn, &block_group_id, false) + .unwrap(), HashSet::from_iter(vec!["TCGA".to_string()]) ); } diff --git a/src/main.rs b/src/main.rs index a57be3c3..243835c9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -30,7 +30,7 @@ use gen_annotations::translate; use gen_core::{BranchName, CommitRef, config::Workspace, range::Range, region::Region}; use gen_diff::operations::collect_operation_diff; use gen_models::{ - annotations::{AnnotationFileChecksumOverrides, add_annotation, add_annotation_file}, + annotations::{AnnotationFileChecksumOverrides, add_annotation_file}, block_group::BlockGroup, collection::Collection, db::{ConfigConnection, DbContext, GraphConnection}, @@ -620,7 +620,7 @@ fn call_cli() -> Result<(), Box> { }) => { let collection_name = get_default_collection(config_conn)?; graph_conn.execute("BEGIN TRANSACTION", [])?; - let operation_summary = match add_annotation( + let operation_summary = match gen_graph::models::add_annotation( &db_context, &collection_name, &name, @@ -692,7 +692,7 @@ fn call_cli() -> Result<(), Box> { .ensure_search_index() .map_err(|_| "No .gen directory found. Run 'gen init' first.")?; for bg in block_groups { - let graph = BlockGroup::get_graph(graph_conn, &bg.id, None)?; + let graph = gen_graph::models::load_block_group_graph(graph_conn, &bg.id, None)?; let matcher = GenGraphMatcher::new(graph_conn, graph); let index = SeedIndex::build(&matcher, kmer_size, true); let path = index_dir.join(format!("{}.bin", bg.id)); @@ -744,7 +744,7 @@ fn call_cli() -> Result<(), Box> { let query_bytes = query.as_bytes(); println!("sample\tgraph\tblocks\toffset"); for bg in block_groups { - let graph = BlockGroup::get_graph(graph_conn, &bg.id, None)?; + let graph = gen_graph::models::load_block_group_graph(graph_conn, &bg.id, None)?; let matcher = GenGraphMatcher::new(graph_conn, graph); let matches = index_dir .as_ref() diff --git a/src/updates.rs b/src/updates.rs index ae0ffbeb..f73acc0b 100644 --- a/src/updates.rs +++ b/src/updates.rs @@ -92,5 +92,5 @@ pub(crate) fn insert_update_change( phased: data.phased, preserve_edge: data.preserve_edge, }; - BlockGroup::insert_change(conn, &change) + gen_graph::models::insert_change(conn, &change) } diff --git a/src/updates/fasta.rs b/src/updates/fasta.rs index bf45de03..f118fb36 100644 --- a/src/updates/fasta.rs +++ b/src/updates/fasta.rs @@ -240,7 +240,6 @@ mod tests { use std::{collections::HashSet, io::Write, path::PathBuf}; use gen_models::{ - annotations::add_annotation, assets::{OperationKind, OperationLog}, history::{HistoryStore, dolt::DoltHistoryStore}, operations::commit_operation_summary, @@ -313,7 +312,8 @@ mod tests { ); assert_eq!(block_groups.len(), 1); assert_eq!( - BlockGroup::get_all_sequences(conn, &block_groups[0].id, false).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(conn, &block_groups[0].id, false) + .unwrap(), HashSet::from_iter(expected_sequences), ); } @@ -422,7 +422,8 @@ mod tests { ); assert_eq!(block_groups.len(), 1); assert_eq!( - BlockGroup::get_all_sequences(conn, &block_groups[0].id, false).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(conn, &block_groups[0].id, false) + .unwrap(), HashSet::from_iter(expected_sequences), ); } @@ -489,7 +490,8 @@ mod tests { ); assert_eq!(block_groups.len(), 1); assert_eq!( - BlockGroup::get_all_sequences(conn, &block_groups[0].id, false).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(conn, &block_groups[0].id, false) + .unwrap(), HashSet::from_iter(expected_sequences), ); } @@ -556,7 +558,8 @@ mod tests { ); assert_eq!(block_groups.len(), 1); assert_eq!( - BlockGroup::get_all_sequences(conn, &block_groups[0].id, false).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(conn, &block_groups[0].id, false) + .unwrap(), HashSet::from_iter(expected_sequences), ); } @@ -629,7 +632,8 @@ mod tests { ); assert_eq!(block_groups.len(), 1); assert_eq!( - BlockGroup::get_all_sequences(conn, &block_groups[0].id, false).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(conn, &block_groups[0].id, false) + .unwrap(), HashSet::from_iter(expected_sequences), ); } @@ -696,7 +700,8 @@ mod tests { ); assert_eq!(block_groups.len(), 1); assert_eq!( - BlockGroup::get_all_sequences(conn, &block_groups[0].id, false).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(conn, &block_groups[0].id, false) + .unwrap(), HashSet::from_iter(expected_sequences), ); } @@ -761,7 +766,8 @@ mod tests { ); assert_eq!(block_groups.len(), 1); assert_eq!( - BlockGroup::get_all_sequences(conn, &block_groups[0].id, false).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(conn, &block_groups[0].id, false) + .unwrap(), HashSet::from_iter(expected_sequences), ); } @@ -782,7 +788,15 @@ mod tests { false, ) .unwrap(); - add_annotation(&context, &collection, "foobar", None, "simple", "m123:5-20").unwrap(); + gen_graph::models::add_annotation( + &context, + &collection, + "foobar", + None, + "simple", + "m123:5-20", + ) + .unwrap(); Sample::get_or_create_child(conn, &collection, "derived", vec!["simple".to_string()]) .unwrap(); @@ -805,7 +819,8 @@ mod tests { assert!(result.is_ok(), "{result:?}"); let block_group = get_sample_bg(conn, &collection, "derived"); assert_eq!( - BlockGroup::get_all_sequences(conn, &block_group.id, false).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(conn, &block_group.id, false) + .unwrap(), HashSet::from_iter([ "ATCGATCGATCGATCGATCGGGAACACACAGAGA".to_string(), "ATAAACGATCGATCGGGAACACACAGAGA".to_string(), @@ -859,7 +874,8 @@ mod tests { ); assert_eq!(block_groups.len(), 1); assert_eq!( - BlockGroup::get_all_sequences(conn, &block_groups[0].id, false).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(conn, &block_groups[0].id, false) + .unwrap(), HashSet::from_iter(expected_sequences), ); @@ -919,7 +935,8 @@ mod tests { ); assert_eq!(block_groups.len(), 1); assert_eq!( - BlockGroup::get_all_sequences(conn, &block_groups[0].id, false).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(conn, &block_groups[0].id, false) + .unwrap(), HashSet::from_iter(expected_sequences), ); } @@ -973,7 +990,8 @@ mod tests { ); assert_eq!(block_groups.len(), 1); assert_eq!( - BlockGroup::get_all_sequences(conn, &block_groups[0].id, false).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(conn, &block_groups[0].id, false) + .unwrap(), HashSet::from_iter(expected_sequences), ); } diff --git a/src/updates/gaf.rs b/src/updates/gaf.rs index a302b72c..07b56d0b 100644 --- a/src/updates/gaf.rs +++ b/src/updates/gaf.rs @@ -500,7 +500,7 @@ mod tests { let _ = import_gfa(&context, &gfa_path, &collection, ""); let gaf_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("fixtures/chr22_het.gaf"); update_with_gaf(&context, gaf_path, csv_path, "test", "child", None).unwrap(); - let graph = Sample::get_graph(conn, "test", "child", None).unwrap(); + let graph = gen_graph::models::load_sample_graph(conn, "test", "child", None).unwrap(); let query = Node::query( conn, @@ -565,7 +565,7 @@ mod tests { let _ = import_gfa(&context, &gfa_path, &collection, ""); let gaf_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("fixtures/chr22_het.gaf"); update_with_gaf(&context, gaf_path, csv_path, "test", "child", None).unwrap(); - let graph = Sample::get_graph(conn, "test", "child", None).unwrap(); + let graph = gen_graph::models::load_sample_graph(conn, "test", "child", None).unwrap(); // we should end up with a new edge putting our insert to the beginning of the graph, which is node 3. let query = Node::query( diff --git a/src/updates/genbank.rs b/src/updates/genbank.rs index d92dc286..c969c8bf 100644 --- a/src/updates/genbank.rs +++ b/src/updates/genbank.rs @@ -214,7 +214,7 @@ where preserve_edge: true, }, }; - BlockGroup::insert_change(conn, &change).unwrap(); + gen_graph::models::insert_change(conn, &change).unwrap(); } } Err(e) => return Err(GenBankError::ParseError(format!("Failed to parse {e}"))), @@ -380,7 +380,7 @@ mod tests { let mod_seq = str::from_utf8(&f[0].seq).unwrap().to_string(); let block_group_id = BlockGroup::get_id("", Sample::DEFAULT_NAME, "insertion", None); let sequences: HashSet = - BlockGroup::get_all_sequences(conn, &block_group_id, false) + gen_graph::models::get_all_sequences_with_pruning(conn, &block_group_id, false) .unwrap() .iter() .map(|s| s.to_lowercase()) @@ -433,7 +433,7 @@ mod tests { let f = reader::parse_file(&path).unwrap(); let block_group_id = BlockGroup::get_id("", Sample::DEFAULT_NAME, "insertion", None); let sequences: HashSet = - BlockGroup::get_all_sequences(conn, &block_group_id, false) + gen_graph::models::get_all_sequences_with_pruning(conn, &block_group_id, false) .unwrap() .iter() .map(|s| s.to_lowercase()) @@ -447,7 +447,7 @@ mod tests { // has a deletion in it. let block_group_id = BlockGroup::get_id("", Sample::DEFAULT_NAME, "deletion", None); let sequences: HashSet = - BlockGroup::get_all_sequences(conn, &block_group_id, false) + gen_graph::models::get_all_sequences_with_pruning(conn, &block_group_id, false) .unwrap() .iter() .map(|s| s.to_lowercase()) diff --git a/src/updates/gfa.rs b/src/updates/gfa.rs index d1216da6..362eec9b 100644 --- a/src/updates/gfa.rs +++ b/src/updates/gfa.rs @@ -546,7 +546,8 @@ mod tests { ); assert_eq!(block_groups.len(), 1); assert_eq!( - BlockGroup::get_all_sequences(conn, &block_groups[0].id, false).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(conn, &block_groups[0].id, false) + .unwrap(), HashSet::from_iter(expected_sequences), ); } @@ -596,7 +597,8 @@ mod tests { ); assert_eq!(block_groups.len(), 1); assert_eq!( - BlockGroup::get_all_sequences(conn, &block_groups[0].id, false).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(conn, &block_groups[0].id, false) + .unwrap(), HashSet::from_iter(expected_sequences), ); } diff --git a/src/updates/library.rs b/src/updates/library.rs index 7c31bbc8..7122d9b4 100644 --- a/src/updates/library.rs +++ b/src/updates/library.rs @@ -310,9 +310,9 @@ fn update_path_library( false, )?; - let resolved_with_positions = resolved_region - .find_graph_positions(conn, 0, 0) - .map_err(UpdateWithLibraryError::from)?; + let resolved_with_positions = + gen_graph::models::find_region_graph_positions(resolved_region, conn, 0, 0) + .map_err(UpdateWithLibraryError::from)?; let splice_point = &resolved_with_positions.start_anchors.unwrap()[0]; let location_accession = create_location_accession(conn, target_block_group.id, splice_point)?; create_part_annotations( @@ -388,8 +388,7 @@ fn update_graph_native_library( target_block_group: &BlockGroup, parts_list: Vec>, ) -> Result<(), UpdateWithLibraryError> { - let resolved = resolved_region - .find_graph_positions(conn, 0, 0) + let resolved = gen_graph::models::find_region_graph_positions(resolved_region, conn, 0, 0) .map_err(UpdateWithLibraryError::from)?; let start_positions = resolved.start_anchors.as_ref().unwrap(); let end_positions = resolved.end_anchors.as_ref().unwrap(); @@ -500,10 +499,7 @@ mod tests { use anyhow::Result; use gen_models::{ - annotations::{Annotation, add_annotation}, - block_group::BlockGroup, - path::Path, - sample_lineage::SampleLineage, + annotations::Annotation, block_group::BlockGroup, path::Path, sample_lineage::SampleLineage, }; use super::*; @@ -552,7 +548,9 @@ mod tests { let block_groups = Sample::get_block_groups(conn, "test", "new sample", None); let block_group = &block_groups[0]; - let all_sequences = BlockGroup::get_all_sequences(conn, &block_group.id, false).unwrap(); + let all_sequences = + gen_graph::models::get_all_sequences_with_pruning(conn, &block_group.id, false) + .unwrap(); assert_eq!( all_sequences, HashSet::from_iter(vec![ @@ -610,7 +608,9 @@ mod tests { let block_groups = Sample::get_block_groups(conn, "test", "new sample", None); let block_group = &block_groups[0]; - let all_sequences = BlockGroup::get_all_sequences(conn, &block_group.id, false).unwrap(); + let all_sequences = + gen_graph::models::get_all_sequences_with_pruning(conn, &block_group.id, false) + .unwrap(); assert_eq!( all_sequences, HashSet::from_iter(vec![ @@ -650,7 +650,15 @@ mod tests { false, ) .unwrap(); - add_annotation(&context, &collection, "foobar", None, "simple", "m123:5-20").unwrap(); + gen_graph::models::add_annotation( + &context, + &collection, + "foobar", + None, + "simple", + "m123:5-20", + ) + .unwrap(); Sample::get_or_create_child(conn, &collection, "derived", vec!["simple".to_string()]) .unwrap(); @@ -677,7 +685,8 @@ mod tests { let block_group = crate::test_helpers::get_sample_bg(conn, &collection, "derived"); assert_eq!( - BlockGroup::get_all_sequences(conn, &block_group.id, false).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(conn, &block_group.id, false) + .unwrap(), HashSet::from_iter(vec![ "ATCGATCGATCGATCGATCGGGAACACACAGAGA".to_string(), "ATAAAACGATCGATCGGGAACACACAGAGA".to_string(), @@ -705,7 +714,15 @@ mod tests { false, ) .unwrap(); - add_annotation(&context, &collection, "foobar", None, "simple", "m123:5-20").unwrap(); + gen_graph::models::add_annotation( + &context, + &collection, + "foobar", + None, + "simple", + "m123:5-20", + ) + .unwrap(); let binding = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("fixtures/parts.fa"); let parts_path = binding.to_str().unwrap(); @@ -727,7 +744,8 @@ mod tests { let block_group = crate::test_helpers::get_sample_bg(conn, &collection, "derived"); assert_eq!( - BlockGroup::get_all_sequences(conn, &block_group.id, false).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(conn, &block_group.id, false) + .unwrap(), HashSet::from_iter(vec![ "ATCGATCGATCGATCGATCGGGAACACACAGAGA".to_string(), "ATCGAAAAAGGAACACACAGAGA".to_string(), @@ -784,7 +802,9 @@ mod tests { expected_sequences.push(seq); } } - let all_sequences = BlockGroup::get_all_sequences(conn, &block_group.id, false).unwrap(); + let all_sequences = + gen_graph::models::get_all_sequences_with_pruning(conn, &block_group.id, false) + .unwrap(); assert_eq!( all_sequences, expected_sequences @@ -834,7 +854,9 @@ mod tests { let block_groups = Sample::get_block_groups(conn, "test", "new sample", None); let block_group = &block_groups[0]; - let all_sequences = BlockGroup::get_all_sequences(conn, &block_group.id, false).unwrap(); + let all_sequences = + gen_graph::models::get_all_sequences_with_pruning(conn, &block_group.id, false) + .unwrap(); assert_eq!( all_sequences, HashSet::from_iter(vec![ @@ -893,7 +915,9 @@ mod tests { expected_sequences.push(seq); } } - let all_sequences = BlockGroup::get_all_sequences(conn, &block_group.id, false).unwrap(); + let all_sequences = + gen_graph::models::get_all_sequences_with_pruning(conn, &block_group.id, false) + .unwrap(); assert_eq!( all_sequences, expected_sequences diff --git a/src/updates/sequence.rs b/src/updates/sequence.rs index 1c04ba60..378872d2 100644 --- a/src/updates/sequence.rs +++ b/src/updates/sequence.rs @@ -188,7 +188,7 @@ mod tests { use gen_core::NO_CHROMOSOME_INDEX; use gen_graph::GraphNode; use gen_models::{ - annotations::{Annotation, add_annotation}, + annotations::Annotation, assets::{OperationKind, OperationLog}, block_group::{BlockGroup, BlockGroupChange, PathCache}, history::{HistoryStore, dolt::DoltHistoryStore}, @@ -249,10 +249,11 @@ mod tests { preserve_edge: true, }; - BlockGroup::insert_change(&conn, &change).unwrap(); + gen_graph::models::insert_change(&conn, &change).unwrap(); assert_eq!( - BlockGroup::get_all_sequences(&conn, &block_group_id, false).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(&conn, &block_group_id, false) + .unwrap(), HashSet::from_iter([ "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), "AAAAAAAAAATTTTTNNNNCCCCCGGGGGGGGGG".to_string(), @@ -283,10 +284,11 @@ mod tests { preserve_edge: true, }; - BlockGroup::insert_change(&conn, &change).unwrap(); + gen_graph::models::insert_change(&conn, &change).unwrap(); assert_eq!( - BlockGroup::get_all_sequences(&conn, &block_group_id, false).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(&conn, &block_group_id, false) + .unwrap(), HashSet::from_iter([ "AAAAAAAAAATTTTTTTTTTCCCCCCCCCCGGGGGGGGGG".to_string(), "AAAAANNNNGGGGG".to_string(), @@ -310,7 +312,15 @@ mod tests { false, ) .unwrap(); - add_annotation(&context, &collection, "foobar", None, "simple", "m123:5-20").unwrap(); + gen_graph::models::add_annotation( + &context, + &collection, + "foobar", + None, + "simple", + "m123:5-20", + ) + .unwrap(); assert!( resolve_annotation( &Region::parse("foobar:-3-5").unwrap(), @@ -343,7 +353,8 @@ mod tests { ); let block_group = get_sample_bg(conn, &collection, "derived"); assert_eq!( - BlockGroup::get_all_sequences(conn, &block_group.id, false).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(conn, &block_group.id, false) + .unwrap(), HashSet::from_iter([ "ATCGATCGATCGATCGATCGGGAACACACAGAGA".to_string(), "ATAAACGATCGATCGGGAACACACAGAGA".to_string(), @@ -403,7 +414,8 @@ mod tests { ); assert_eq!(block_groups.len(), 1); assert_eq!( - BlockGroup::get_all_sequences(conn, &block_groups[0].id, false).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(conn, &block_groups[0].id, false) + .unwrap(), HashSet::from_iter(expected_sequences), ); assert_eq!( @@ -516,7 +528,8 @@ mod tests { ); assert_eq!(block_groups.len(), 1); assert_eq!( - BlockGroup::get_all_sequences(conn, &block_groups[0].id, false).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(conn, &block_groups[0].id, false) + .unwrap(), HashSet::from_iter(expected_sequences), ); } @@ -574,7 +587,8 @@ mod tests { ); assert_eq!(block_groups.len(), 1); assert_eq!( - BlockGroup::get_all_sequences(conn, &block_groups[0].id, false).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(conn, &block_groups[0].id, false) + .unwrap(), HashSet::from_iter(expected_sequences), ); } @@ -638,7 +652,8 @@ mod tests { ); assert_eq!(block_groups.len(), 1); assert_eq!( - BlockGroup::get_all_sequences(conn, &block_groups[0].id, false).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(conn, &block_groups[0].id, false) + .unwrap(), HashSet::from_iter(expected_sequences), ); } @@ -696,7 +711,8 @@ mod tests { ); assert_eq!(block_groups.len(), 1); assert_eq!( - BlockGroup::get_all_sequences(conn, &block_groups[0].id, false).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(conn, &block_groups[0].id, false) + .unwrap(), HashSet::from_iter(expected_sequences), ); } @@ -754,7 +770,8 @@ mod tests { ); assert_eq!(block_groups.len(), 1); assert_eq!( - BlockGroup::get_all_sequences(conn, &block_groups[0].id, false).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(conn, &block_groups[0].id, false) + .unwrap(), HashSet::from_iter(expected_sequences), ); } @@ -801,7 +818,8 @@ mod tests { ); assert_eq!(block_groups.len(), 1); assert_eq!( - BlockGroup::get_all_sequences(conn, &block_groups[0].id, false).unwrap(), + gen_graph::models::get_all_sequences_with_pruning(conn, &block_groups[0].id, false) + .unwrap(), HashSet::from_iter(expected_sequences), ); @@ -833,7 +851,7 @@ mod tests { false, ) .unwrap(); - add_annotation( + gen_graph::models::add_annotation( &context, &collection, "SITE", @@ -865,7 +883,7 @@ mod tests { .unwrap(); let block_group = get_sample_bg(conn, &collection, "deleted"); - let graph = BlockGroup::get_graph(conn, &block_group.id, None).unwrap(); + let graph = gen_graph::models::load_block_group_graph(conn, &block_group.id, None).unwrap(); let node_ids = graph.nodes().map(|node| node.node_id).collect::>(); let sequences = Node::get_sequences_by_node_ids(conn, &node_ids, None); let rendered_sequence = |node: GraphNode| { @@ -909,7 +927,7 @@ mod tests { .unwrap(); let block_group = get_sample_bg(conn, &collection, "deleted2"); - let graph = BlockGroup::get_graph(conn, &block_group.id, None).unwrap(); + let graph = gen_graph::models::load_block_group_graph(conn, &block_group.id, None).unwrap(); let node_ids = graph.nodes().map(|node| node.node_id).collect::>(); let sequences = Node::get_sequences_by_node_ids(conn, &node_ids, None); let rendered_sequence = |node: GraphNode| { diff --git a/src/updates/vcf.rs b/src/updates/vcf.rs index 2cb3ea39..1cfb5efc 100644 --- a/src/updates/vcf.rs +++ b/src/updates/vcf.rs @@ -687,9 +687,10 @@ pub fn update_with_vcf( } }) .collect::>(); - BlockGroup::insert_changes(conn, &in_place_changes, Some(&mut tree_map)).unwrap(); + gen_graph::models::insert_changes(conn, &in_place_changes, Some(&mut tree_map)) + .unwrap(); } else { - BlockGroup::insert_changes(conn, chunk, Some(&mut tree_map)).unwrap(); + gen_graph::models::insert_changes(conn, chunk, Some(&mut tree_map)).unwrap(); } bar.inc(chunk.len() as u64); } @@ -779,7 +780,7 @@ mod tests { false, )?; assert_eq!( - BlockGroup::get_all_sequences( + gen_graph::models::get_all_sequences_with_pruning( conn, &get_sample_bg(conn, &collection, Sample::DEFAULT_NAME).id, false, @@ -789,14 +790,22 @@ mod tests { ); // `G1` genotype has no changes assert_eq!( - BlockGroup::get_all_sequences(conn, &get_sample_bg(conn, &collection, "G1").id, false) - .unwrap(), + gen_graph::models::get_all_sequences_with_pruning( + conn, + &get_sample_bg(conn, &collection, "G1").id, + false + ) + .unwrap(), HashSet::from_iter(vec!["ATCGATCGATCGATCGATCGGGAACACACAGAGA".to_string()]) ); // `foo` is homozygous for the first variant and does not contain the second assert_eq!( - BlockGroup::get_all_sequences(conn, &get_sample_bg(conn, &collection, "foo").id, false) - .unwrap(), + gen_graph::models::get_all_sequences_with_pruning( + conn, + &get_sample_bg(conn, &collection, "foo").id, + false + ) + .unwrap(), HashSet::from_iter(vec!["ATCATCGATCGATCGATCGGGAACACACAGAGA".to_string(),]) ); @@ -831,7 +840,7 @@ mod tests { ) .unwrap(); assert_eq!( - BlockGroup::get_all_sequences( + gen_graph::models::get_all_sequences_with_pruning( conn, &get_sample_bg(conn, &collection, Sample::DEFAULT_NAME).id, false, @@ -841,8 +850,12 @@ mod tests { ); // `bar` sample has the refrence + a deletion of the C assert_eq!( - BlockGroup::get_all_sequences(conn, &get_sample_bg(conn, &collection, "bar").id, false) - .unwrap(), + gen_graph::models::get_all_sequences_with_pruning( + conn, + &get_sample_bg(conn, &collection, "bar").id, + false + ) + .unwrap(), HashSet::from_iter(vec![ "ATCGATCGATCGATCGATCGGGAACACACAGAGA".to_string(), "ATCGATCGATGATCGATCGGGAACACACAGAGA".to_string() @@ -850,8 +863,12 @@ mod tests { ); // `baz` sample has a deletion of CG and an insertion of A assert_eq!( - BlockGroup::get_all_sequences(conn, &get_sample_bg(conn, &collection, "baz").id, false) - .unwrap(), + gen_graph::models::get_all_sequences_with_pruning( + conn, + &get_sample_bg(conn, &collection, "baz").id, + false + ) + .unwrap(), HashSet::from_iter(vec![ "ATCGATCGATATCGATCGGGAACACACAGAGA".to_string(), "ATCGATCGATCAGATCGATCGGGAACACACAGAGA".to_string(), @@ -888,7 +905,7 @@ mod tests { ) .unwrap(); assert_eq!( - BlockGroup::get_all_sequences( + gen_graph::models::get_all_sequences_with_pruning( conn, &get_sample_bg(conn, &collection, Sample::DEFAULT_NAME).id, false, @@ -897,7 +914,7 @@ mod tests { HashSet::from_iter(vec!["ATCGATCGATCGATCGATCGGGAACACACAGAGA".to_string()]) ); assert_eq!( - BlockGroup::get_all_sequences( + gen_graph::models::get_all_sequences_with_pruning( conn, &get_sample_bg(conn, &collection, "sample 1").id, false @@ -946,7 +963,7 @@ mod tests { .unwrap(); assert_eq!( - BlockGroup::get_all_sequences( + gen_graph::models::get_all_sequences_with_pruning( conn, &get_sample_bg(conn, &collection, "sample 1").id, false @@ -1021,7 +1038,7 @@ mod tests { .unwrap(); assert_eq!( - BlockGroup::get_all_sequences( + gen_graph::models::get_all_sequences_with_pruning( conn, &get_sample_bg(conn, &collection, "unknown").id, false @@ -1064,8 +1081,12 @@ mod tests { ) .unwrap(); assert_eq!( - BlockGroup::get_all_sequences(conn, &get_sample_bg(conn, &collection, "foo").id, false) - .unwrap(), + gen_graph::models::get_all_sequences_with_pruning( + conn, + &get_sample_bg(conn, &collection, "foo").id, + false + ) + .unwrap(), HashSet::from_iter( ["ATCATCGATCGATCGATCGGGAACACACAGAGA",] .iter() @@ -1104,8 +1125,12 @@ mod tests { .unwrap(); assert_eq!( - BlockGroup::get_all_sequences(conn, &get_sample_bg(conn, &collection, "foo").id, true) - .unwrap(), + gen_graph::models::get_all_sequences_with_pruning( + conn, + &get_sample_bg(conn, &collection, "foo").id, + true + ) + .unwrap(), HashSet::from_iter(vec![ "ATCGATCGATCGGATCGGGAACACACAGAGA".to_string(), "ATCGATCGATCGATCATCATCGATCGGGAACACACAGAGA".to_string() @@ -1147,7 +1172,7 @@ mod tests { let nodes = Node::query(conn, "select * from nodes;", rusqlite::params!()); assert_eq!(nodes.len(), 5); - let second_update = update_with_vcf( + let _second_update = update_with_vcf( &context, &vcf_path.to_str().unwrap().to_string(), &collection, @@ -1199,7 +1224,7 @@ mod tests { let nodes = Node::query(conn, "select * from nodes;", rusqlite::params!()); assert_eq!(nodes.len(), 8); - let second_update = update_with_vcf( + let _second_update = update_with_vcf( &context, &vcf_path.to_str().unwrap().to_string(), &collection, @@ -1426,7 +1451,7 @@ mod tests { .unwrap(); assert_eq!( - BlockGroup::get_all_sequences( + gen_graph::models::get_all_sequences_with_pruning( conn, &get_sample_bg(conn, &collection, Sample::DEFAULT_NAME).id, true, @@ -1435,18 +1460,30 @@ mod tests { HashSet::from_iter(vec!["ATCGATCGATCGATCGATCGGGAACACACAGAGA".to_string()]) ); assert_eq!( - BlockGroup::get_all_sequences(conn, &get_sample_bg(conn, &collection, "f1").id, true) - .unwrap(), + gen_graph::models::get_all_sequences_with_pruning( + conn, + &get_sample_bg(conn, &collection, "f1").id, + true + ) + .unwrap(), HashSet::from_iter(vec!["ATCTCGATCGATCGCGGGAACACACAGAGA".to_string()]) ); assert_eq!( - BlockGroup::get_all_sequences(conn, &get_sample_bg(conn, &collection, "f2").id, true) - .unwrap(), + gen_graph::models::get_all_sequences_with_pruning( + conn, + &get_sample_bg(conn, &collection, "f2").id, + true + ) + .unwrap(), HashSet::from_iter(vec!["ATCTGGATCGATCGCGGAATCAGAACACACAGGA".to_string()]) ); assert_eq!( - BlockGroup::get_all_sequences(conn, &get_sample_bg(conn, &collection, "f3").id, true) - .unwrap(), + gen_graph::models::get_all_sequences_with_pruning( + conn, + &get_sample_bg(conn, &collection, "f3").id, + true + ) + .unwrap(), HashSet::from_iter(vec!["ATCGGGATCGATCGCTCAGAACACACAGGA".to_string()]) ); } @@ -1490,7 +1527,7 @@ mod tests { ) .unwrap(); - let child_sequences = BlockGroup::get_all_sequences( + let child_sequences = gen_graph::models::get_all_sequences_with_pruning( conn, &get_sample_bg(conn, &collection, "child").id, true, @@ -1546,7 +1583,7 @@ mod tests { ) .unwrap(); - let child_sequences = BlockGroup::get_all_sequences( + let child_sequences = gen_graph::models::get_all_sequences_with_pruning( conn, &get_sample_bg(conn, &collection, "child").id, true, diff --git a/src/views/annotations.rs b/src/views/annotations.rs index 7ac65788..12acfc2d 100644 --- a/src/views/annotations.rs +++ b/src/views/annotations.rs @@ -165,8 +165,9 @@ fn load_group_annotations( // a combinatorial library). Clip them onto the block group's full graph so an // annotation still covers every surviving fragment of its original range, on every // branch, with a gap wherever an edit spliced in unrelated sequence. - let graph = BlockGroup::get_graph(conn, ¤t_block_group.id, history_ref) - .unwrap_or_else(|_| GenGraph::new()); + let graph = + gen_graph::models::load_block_group_graph(conn, ¤t_block_group.id, history_ref) + .unwrap_or_else(|_| GenGraph::new()); Ok(annotations .into_iter() .filter_map(|annotation| { @@ -662,9 +663,7 @@ mod tests { use gen_core::{HashId, Strand}; use gen_graph::{GenGraph, GraphNode}; - use gen_models::{ - annotations::add_annotation, block_group::BlockGroup, file_types::FileTypes, sample::Sample, - }; + use gen_models::{file_types::FileTypes, sample::Sample}; use super::{ AnnotationGroupTrackRequest, AnnotationSegment, annotation_index_is_tabix, @@ -760,7 +759,6 @@ mod tests { use std::{fs::File, io::BufReader, path::PathBuf}; use gen_models::{ - block_group::BlockGroup, file_types::FileTypes, operations::{OperationFile, OperationInfo}, sample::Sample, @@ -796,7 +794,7 @@ mod tests { let block_groups = Sample::get_block_groups(conn, "fixtures", "puc19-sample", None); let block_group = &block_groups[0]; - let graph = BlockGroup::get_graph(conn, &block_group.id, None).unwrap(); + let graph = gen_graph::models::load_block_group_graph(conn, &block_group.id, None).unwrap(); let node_ids: HashSet = graph.nodes().map(|n| n.node_id).collect(); let groups = @@ -837,7 +835,7 @@ mod tests { fn load_annotations_for_group_finds_every_combinatorial_branch() { use std::path::PathBuf; - use gen_models::{block_group::BlockGroup, sample::Sample}; + use gen_models::sample::Sample; use super::{AnnotationGroupTrackRequest, load_annotations_for_group}; use crate::{ @@ -873,7 +871,7 @@ mod tests { let block_groups = Sample::get_block_groups(conn, collection, Sample::DEFAULT_NAME, None); let block_group = &block_groups[0]; - let graph = BlockGroup::get_graph(conn, &block_group.id, None).unwrap(); + let graph = gen_graph::models::load_block_group_graph(conn, &block_group.id, None).unwrap(); let node_ids: HashSet = graph.nodes().map(|n| n.node_id).collect(); let entry = AnnotationGroupEntry { @@ -924,7 +922,7 @@ mod tests { false, ) .unwrap(); - add_annotation( + gen_graph::models::add_annotation( &context, &collection, "SITE", @@ -967,7 +965,9 @@ mod tests { entry.source_block_group_id, selected_block_group.id, "the entry source should differ from the currently selected block group" ); - let selected_graph = BlockGroup::get_graph(conn, &selected_block_group.id, None).unwrap(); + let selected_graph = + gen_graph::models::load_block_group_graph(conn, &selected_block_group.id, None) + .unwrap(); let node_ids = selected_graph .nodes() .map(|node| node.node_id) diff --git a/src/views/block_group.rs b/src/views/block_group.rs index 000d5568..37e0672c 100644 --- a/src/views/block_group.rs +++ b/src/views/block_group.rs @@ -261,7 +261,8 @@ pub fn view_block_group( let block_group = block_group.unwrap(); block_group_id = Some(block_group.id); - block_graph = BlockGroup::get_graph(conn, &block_group.id, history_ref)?; + block_graph = + gen_graph::models::load_block_group_graph(conn, &block_group.id, history_ref)?; explorer_state.selected_block_group_id = Some(block_group.id); focus_zone = FocusZone::Canvas; } else { @@ -1238,7 +1239,8 @@ pub fn view_block_group( // for the full duration of the blocking DB work. if is_loading && let Some(ref new_block_group_id) = explorer_state.selected_block_group_id { // Create a new graph for the selected block group - block_graph = BlockGroup::get_graph(conn, new_block_group_id, history_ref)?; + block_graph = + gen_graph::models::load_block_group_graph(conn, new_block_group_id, history_ref)?; // Update the graph controller graph_controller = create_gen_graph_controller(block_graph.clone()); let block_group = match BlockGroup::get_by_id(conn, new_block_group_id, history_ref) { diff --git a/src/views/block_group_inline.rs b/src/views/block_group_inline.rs index 598c9c76..32910f0e 100644 --- a/src/views/block_group_inline.rs +++ b/src/views/block_group_inline.rs @@ -220,7 +220,8 @@ pub fn show_inline_block_group_widget( height: u16, history_ref: Option<&str>, ) -> Result { - let graph = BlockGroup::get_graph(conn, &block_group_id, history_ref).map_err(Error::other)?; + let graph = gen_graph::models::load_block_group_graph(conn, &block_group_id, history_ref) + .map_err(Error::other)?; show_inline_widget( conn, &graph, diff --git a/src/views/gen_graph_widget.rs b/src/views/gen_graph_widget.rs index 87c6f7fc..be2c5d9c 100644 --- a/src/views/gen_graph_widget.rs +++ b/src/views/gen_graph_widget.rs @@ -252,7 +252,7 @@ pub fn create_gen_graph_widget( GraphWidget::with_renderer(renderer) } -/// Compute which edges would be removed by `BlockGroup::prune_graph`. +/// Compute which edges would be removed by `gen_graph::models::prune_graph`. /// /// Mirrors the per-source-node, per-chromosome_index deduplication logic: for each /// chromosome_index appearing on outgoing edges of a node, the edge with the highest @@ -1108,7 +1108,8 @@ mod tests { ) .unwrap(); - let gen_graph = Sample::get_graph(conn, collection, "SAMPLE1", None).unwrap(); + let gen_graph = + gen_graph::models::load_sample_graph(conn, collection, "SAMPLE1", None).unwrap(); let mut controller = create_gen_graph_controller(gen_graph); let mut terminal = create_test_terminal(120, 30); @@ -1156,7 +1157,9 @@ mod tests { ) .unwrap(); - let gen_graph = Sample::get_graph(conn, collection, Sample::DEFAULT_NAME, None).unwrap(); + let gen_graph = + gen_graph::models::load_sample_graph(conn, collection, Sample::DEFAULT_NAME, None) + .unwrap(); let mut controller = create_gen_graph_controller(gen_graph); let block = controller diff --git a/src/views/testing/connectivity_test.rs b/src/views/testing/connectivity_test.rs index cbfb0145..bcf00586 100644 --- a/src/views/testing/connectivity_test.rs +++ b/src/views/testing/connectivity_test.rs @@ -41,7 +41,8 @@ mod tests { .expect("GFA import failed"); let gen_graph = - Sample::get_graph(conn, collection_name, Sample::DEFAULT_NAME, None).unwrap(); + gen_graph::models::load_sample_graph(conn, collection_name, Sample::DEFAULT_NAME, None) + .unwrap(); // Test with small partitions to force inter-partition edges let config = GraphConfig { diff --git a/src/views/testing/keyboard_navigation_test.rs b/src/views/testing/keyboard_navigation_test.rs index 579de323..af7dc128 100644 --- a/src/views/testing/keyboard_navigation_test.rs +++ b/src/views/testing/keyboard_navigation_test.rs @@ -46,7 +46,8 @@ mod tests { .expect("GFA import failed"); let gen_graph = - Sample::get_graph(conn, collection_name, Sample::DEFAULT_NAME, None).unwrap(); + gen_graph::models::load_sample_graph(conn, collection_name, Sample::DEFAULT_NAME, None) + .unwrap(); let config = GraphConfig { partition: PartitionConfig { @@ -318,7 +319,8 @@ mod tests { .expect("GFA import failed"); let gen_graph = - Sample::get_graph(conn, collection_name, Sample::DEFAULT_NAME, None).unwrap(); + gen_graph::models::load_sample_graph(conn, collection_name, Sample::DEFAULT_NAME, None) + .unwrap(); // Configure with large partition for stability let config = GraphConfig { @@ -389,7 +391,8 @@ mod tests { .expect("GFA import failed"); let gen_graph = - Sample::get_graph(conn, collection_name, Sample::DEFAULT_NAME, None).unwrap(); + gen_graph::models::load_sample_graph(conn, collection_name, Sample::DEFAULT_NAME, None) + .unwrap(); // Configure with large partition for stability let config = GraphConfig { diff --git a/tests/history_cli_tests.rs b/tests/history_cli_tests.rs index 397f5caa..f0493b50 100644 --- a/tests/history_cli_tests.rs +++ b/tests/history_cli_tests.rs @@ -287,12 +287,9 @@ mod diff_views { let parent_block_group_id = unknown_target_block_group .parent_block_group_id .expect("lineage-derived sample should retain parent block group id"); - let parent_graph = gen_models::block_group::BlockGroup::get_graph( - &graph_conn, - &parent_block_group_id, - None, - ) - .expect("should load parent block group graph"); + let parent_graph = + gen_graph::models::load_block_group_graph(&graph_conn, &parent_block_group_id, None) + .expect("should load parent block group graph"); assert!( parent_graph.nodes().next().is_some(), "parent block group graph should contain reference context"