Skip to content

Commit fdb9fc5

Browse files
committed
feat(silo): add getDescendants function, add duplicate node error handling
1 parent f3d3f36 commit fdb9fc5

10 files changed

Lines changed: 304 additions & 6 deletions

File tree

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"testCaseName": "PhyloDescendentOf should return only descendent nodes of a given node",
3+
"query": {
4+
"action": {
5+
"type": "Aggregated",
6+
"groupByFields": ["gisaid_epi_isl"],
7+
"orderByFields": [
8+
{
9+
"field": "gisaid_epi_isl",
10+
"order": "ascending"
11+
}
12+
]
13+
},
14+
"filterExpression": {
15+
"type": "PhyloDescendentOf",
16+
"column": "gisaid_epi_isl",
17+
"searchExpression": "NODE_0000072"
18+
}
19+
},
20+
"expectedQueryResult": [
21+
{
22+
"count": 1,
23+
"gisaid_epi_isl": "EPI_ISL_1003849"
24+
},
25+
{
26+
"count": 1,
27+
"gisaid_epi_isl": "EPI_ISL_1260480"
28+
}
29+
]
30+
}

src/silo/common/phylo_tree.cpp

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ std::shared_ptr<TreeNode> parse_auspice_tree(
3535
}
3636

3737
// Insert node into the map *after* children so it's fully constructed
38+
if (node_map.find(node->node_id) != node_map.end()) {
39+
throw silo::preprocessing::PreprocessingException(
40+
fmt::format("Duplicate node ID found in Newick string: '{}'", node->node_id.string)
41+
);
42+
}
3843
node_map[node->node_id] = node;
3944
return node;
4045
}
@@ -150,6 +155,11 @@ std::shared_ptr<TreeNode> parseSubtree(
150155
node->node_id = parseLabel(sv);
151156
skipWhitespace(sv);
152157

158+
if (node_map.find(node->node_id) != node_map.end()) {
159+
throw silo::preprocessing::PreprocessingException(
160+
fmt::format("Duplicate node ID found in Newick string: '{}'", node->node_id.string)
161+
);
162+
}
153163
node_map[node->node_id] = node;
154164

155165
return node;
@@ -179,7 +189,7 @@ PhyloTree PhyloTree::fromNewickString(const std::string& newick_string) {
179189
}
180190
} catch (const std::exception& e) {
181191
throw silo::preprocessing::PreprocessingException(
182-
fmt::format("Error when parsing the Newick string: '{}'", newick_string)
192+
fmt::format("Error when parsing the Newick string '{}': {}", newick_string, e.what())
183193
);
184194
}
185195

@@ -207,7 +217,7 @@ PhyloTree PhyloTree::fromNewickFile(const std::filesystem::path& newick_path) {
207217
return fromNewickString(contents.str());
208218
} catch (const std::exception& e) {
209219
throw silo::preprocessing::PreprocessingException(
210-
fmt::format("Error when parsing the Newick file: '{}'", newick_path.string())
220+
fmt::format("Error when parsing the Newick file '{}': {}", newick_path.string(), e.what())
211221
);
212222
}
213223
}
@@ -217,14 +227,60 @@ PhyloTree PhyloTree::fromFile(const std::filesystem::path& path) {
217227

218228
std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
219229

230+
if (ext != ".nwk" && ext != ".json") {
231+
throw silo::preprocessing::PreprocessingException(fmt::format(
232+
"Error when parsing tree file: '{}'. Path must end with .nwk or .json", path.string()
233+
));
234+
}
220235
if (ext == ".nwk") {
221236
return common::PhyloTree::fromNewickFile(path);
222237
} else if (ext == ".json") {
223238
return common::PhyloTree::fromAuspiceJSONFile(path);
224239
}
225-
throw silo::preprocessing::PreprocessingException(fmt::format(
226-
"Error when parsing tree file: '{}'. Path must end with .nwk or .json", path.string()
227-
));
240+
}
241+
242+
void PhyloTree::validateNodeExists(const TreeNodeId& node_id) {
243+
if (nodes.find(node_id) == nodes.end()) {
244+
throw silo::preprocessing::PreprocessingException(
245+
fmt::format("Node '{}' not found in the tree.", node_id.string)
246+
);
247+
}
248+
}
249+
250+
void PhyloTree::validateNodeExists(const std::string& node_label) {
251+
auto node_id = TreeNodeId{node_label};
252+
validateNodeExists(node_id);
253+
}
254+
255+
roaring::Roaring PhyloTree::getDescendants(const TreeNodeId& node_id) {
256+
validateNodeExists(node_id);
257+
auto child_it = nodes.find(node_id);
258+
roaring::Roaring result_bitmap;
259+
if (!child_it->second) {
260+
throw silo::preprocessing::PreprocessingException("Node is null.");
261+
}
262+
std::function<void(const std::shared_ptr<TreeNode>&)> dfs =
263+
[&](const std::shared_ptr<TreeNode>& current) {
264+
if (!current)
265+
return;
266+
if (current->isLeaf()) {
267+
if (current->row_index.has_value()) {
268+
result_bitmap.add(current->row_index.value());
269+
}
270+
}
271+
for (const auto& child : current->children) {
272+
dfs(child);
273+
}
274+
};
275+
if (child_it->second->isLeaf()) {
276+
return result_bitmap;
277+
}
278+
dfs(child_it->second);
279+
return result_bitmap;
280+
}
281+
282+
roaring::Roaring PhyloTree::getDescendants(const std::string& node_label) {
283+
return getDescendants(TreeNodeId{node_label});
228284
}
229285

230286
} // namespace silo::common

src/silo/common/phylo_tree.h

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,30 @@
66
#include <nlohmann/json.hpp>
77

88
#include "silo/common/tree_node_id.h"
9+
#include "silo/query_engine/batched_bitmap_reader.h"
910

1011
namespace silo::common {
1112
using silo::common::TreeNodeId;
1213

1314
class TreeNode {
1415
public:
1516
TreeNodeId node_id;
17+
std::optional<size_t> row_index; // index of corresponding sequence in the database (will be
18+
// empty for internal nodes)
1619
std::vector<std::shared_ptr<TreeNode>> children;
1720
std::optional<std::shared_ptr<TreeNode>> parent;
1821
int depth;
22+
23+
bool isLeaf() { return children.empty(); }
24+
bool rowIndexExists() const { return row_index.has_value(); }
1925
};
2026

2127
class PhyloTree {
2228
public:
2329
std::unordered_map<TreeNodeId, std::shared_ptr<TreeNode>> nodes;
2430

31+
// Functions for reading and parsing phylogenetic trees
32+
2533
static PhyloTree fromAuspiceJSONFile(const std::filesystem::path& json_path);
2634

2735
static PhyloTree fromAuspiceJSONString(const std::string& json_string);
@@ -31,6 +39,17 @@ class PhyloTree {
3139
static PhyloTree fromNewickString(const std::string& newick_string);
3240

3341
static PhyloTree fromFile(const std::filesystem::path& path);
42+
43+
// Functions for querying the phylogenetic tree
44+
45+
void validateNodeExists(const TreeNodeId& node_id);
46+
47+
void validateNodeExists(const std::string& node_label);
48+
49+
// returns a bitmap of all descendants node{node_id} that are also in the database
50+
roaring::Roaring getDescendants(const TreeNodeId& node_id);
51+
52+
roaring::Roaring getDescendants(const std::string& node_label);
3453
};
3554

3655
} // namespace silo::common

src/silo/common/phylo_tree.test.cpp

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,29 @@ TEST(PhyloTree, throwsOnInvalidJSON) {
5050
);
5151
}
5252

53+
TEST(PhyloTree, throwsOnInvalidAuspiceJSONDuplicateNodeId) {
54+
EXPECT_THROW(
55+
PhyloTree::fromAuspiceJSONString(R"({
56+
"version": "schema version",
57+
"meta": {},
58+
"tree": {
59+
"name": "ROOT",
60+
"children": [
61+
{
62+
"name": "CHILD",
63+
"children": [
64+
{
65+
"name": "CHILD"
66+
}
67+
]
68+
}
69+
]
70+
}
71+
})"),
72+
silo::preprocessing::PreprocessingException
73+
);
74+
}
75+
5376
TEST(PhyloTree, correctlyParsesFromNewick) {
5477
auto phylo_tree_file = PhyloTree::fromNewickString("((CHILD2)CHILD)ROOT;");
5578
ASSERT_EQ(phylo_tree_file.nodes.size(), 3);
@@ -95,3 +118,9 @@ TEST(PhyloTree, throwsOnInvalidNewickNoSemicolon) {
95118
silo::preprocessing::PreprocessingException
96119
);
97120
}
121+
122+
TEST(PhyloTree, throwsOnInvalidNewickWithDuplicateNodeId) {
123+
EXPECT_THROW(
124+
PhyloTree::fromNewickString("((CHILD)CHILD)ROOT"), silo::preprocessing::PreprocessingException
125+
);
126+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
#include "silo/query_engine/filter/expressions/phylo_tree_filter.h"
2+
3+
#include <optional>
4+
#include <utility>
5+
6+
#include <fmt/format.h>
7+
#include <nlohmann/json.hpp>
8+
9+
#include "silo/common/panic.h"
10+
#include "silo/common/string.h"
11+
#include "silo/database.h"
12+
#include "silo/query_engine/bad_request.h"
13+
#include "silo/query_engine/filter/expressions/expression.h"
14+
#include "silo/query_engine/filter/operators/bitmap_producer.h"
15+
#include "silo/query_engine/filter/operators/operator.h"
16+
#include "silo/storage/table_partition.h"
17+
18+
namespace silo::query_engine::filter::expressions {
19+
20+
PhyloChildFilter::PhyloChildFilter(std::string column_name, std::string internal_node)
21+
: column_name(std::move(column_name)),
22+
internal_node(std::move(internal_node)) {}
23+
24+
std::string PhyloChildFilter::toString() const {
25+
return fmt::format("column {} phylo_child_of {}", column_name, internal_node);
26+
};
27+
28+
namespace {
29+
std::unique_ptr<silo::query_engine::filter::operators::Operator> createMatchingBitmap(
30+
const storage::column::StringColumnPartition& string_column,
31+
const std::string& internal_node,
32+
size_t row_count
33+
) {
34+
return std::make_unique<operators::BitmapProducer>(
35+
[&, row_count]() {
36+
roaring::Roaring result_bitmap = string_column.getDescendants(internal_node);
37+
return CopyOnWriteBitmap(std::move(result_bitmap));
38+
},
39+
row_count
40+
);
41+
}
42+
43+
} // namespace
44+
45+
std::unique_ptr<silo::query_engine::filter::operators::Operator> PhyloChildFilter::compile(
46+
const Database& /*database*/,
47+
const storage::TablePartition& database_partition,
48+
Expression::AmbiguityMode /*mode*/
49+
) const {
50+
CHECK_SILO_QUERY(
51+
database_partition.columns.string_columns.contains(column_name),
52+
fmt::format("The database does not contain the string column '{}'", column_name)
53+
);
54+
55+
SILO_ASSERT(database_partition.columns.string_columns.contains(column_name));
56+
const auto& string_column = database_partition.columns.string_columns.at(column_name);
57+
return createMatchingBitmap(string_column, internal_node, database_partition.sequence_count);
58+
}
59+
60+
// NOLINTNEXTLINE(readability-identifier-naming)
61+
void from_json(const nlohmann::json& json, std::unique_ptr<PhyloChildFilter>& filter) {
62+
CHECK_SILO_QUERY(
63+
json.contains("column"), "The field 'column' is required in an PhyloChildFilter expression"
64+
)
65+
CHECK_SILO_QUERY(
66+
json["column"].is_string(),
67+
"The field 'column' in an PhyloChildFilter expression needs to be a string"
68+
)
69+
CHECK_SILO_QUERY(
70+
json.contains("internal_node"),
71+
"The field 'internal_node' is required in an PhyloChildFilter expression"
72+
)
73+
CHECK_SILO_QUERY(
74+
json["internal_node"].is_string(),
75+
"The field 'internal_node' in an PhyloChildFilter expression needs to be a string"
76+
)
77+
filter = std::make_unique<PhyloChildFilter>(
78+
json["column"].get<std::string>(), json["internal_node"].get<std::string>()
79+
);
80+
}
81+
82+
} // namespace silo::query_engine::filter::expressions
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#pragma once
2+
3+
#include <memory>
4+
#include <string>
5+
6+
#include <nlohmann/json_fwd.hpp>
7+
8+
#include "silo/database.h"
9+
#include "silo/query_engine/filter/expressions/expression.h"
10+
#include "silo/query_engine/filter/operators/operator.h"
11+
#include "silo/storage/table_partition.h"
12+
13+
namespace silo::query_engine::filter::expressions {
14+
15+
class PhyloChildFilter : public Expression {
16+
std::string column_name;
17+
std::string internal_node;
18+
19+
public:
20+
explicit PhyloChildFilter(std::string column_name, std::string internal_node);
21+
22+
std::string toString() const override;
23+
24+
[[nodiscard]] std::unique_ptr<silo::query_engine::filter::operators::Operator> compile(
25+
const Database& database,
26+
const storage::TablePartition& database_partition,
27+
AmbiguityMode mode
28+
) const override;
29+
30+
private:
31+
std::optional<const roaring::Roaring*> getBitmapForValue(
32+
const silo::storage::column::StringColumnPartition& phylo_tree_index_column
33+
) const;
34+
};
35+
36+
// NOLINTNEXTLINE(readability-identifier-naming)
37+
void from_json(const nlohmann::json& json, std::unique_ptr<PhyloChildFilter>& filter);
38+
39+
} // namespace silo::query_engine::filter::expressions

src/silo/storage/column/string_column.cpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@
44

55
#include "silo/common/bidirectional_map.h"
66
#include "silo/common/string.h"
7+
#include "silo/common/tree_node_id.h"
8+
#include "silo/initialize/initialize_exception.h"
79

810
using silo::common::String;
911
using silo::common::STRING_SIZE;
12+
using silo::common::TreeNodeId;
1013

1114
namespace silo::storage::column {
1215

@@ -39,6 +42,18 @@ StringColumnPartition::StringColumnPartition(StringColumnMetadata* metadata)
3942
void StringColumnPartition::insert(const std::string& value) {
4043
const String<STRING_SIZE> tmp(value, metadata->dictionary);
4144
values.push_back(tmp);
45+
if (metadata->phylo_tree.has_value()) {
46+
auto child_it = (metadata->phylo_tree->nodes).find(TreeNodeId{value});
47+
if (child_it == metadata->phylo_tree->nodes.end()) {
48+
return;
49+
}
50+
if (child_it->second->rowIndexExists()) {
51+
throw silo::initialize::InitializeException(
52+
fmt::format("Node '{}' already exists in the phylogenetic tree.", value)
53+
);
54+
}
55+
child_it->second->row_index = values.size() - 1;
56+
}
4257
}
4358

4459
void StringColumnPartition::insertNull() {

0 commit comments

Comments
 (0)