amql_engine/
query_options.rs1#[cfg(all(feature = "resolver", feature = "fs"))]
11use crate::query::QueryResult;
12use crate::store::Annotation;
13use serde::{Deserialize, Serialize};
14
15#[non_exhaustive]
17#[derive(Debug, Clone, Default, Serialize, Deserialize)]
18#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
19pub struct QueryOptions {
20 pub limit: Option<usize>,
22 pub offset: Option<usize>,
24 pub sort_by: Option<String>,
28}
29
30impl QueryOptions {
31 pub fn new(limit: Option<usize>, offset: Option<usize>, sort_by: Option<String>) -> Self {
33 Self {
34 limit,
35 offset,
36 sort_by,
37 }
38 }
39}
40
41#[cfg(all(feature = "resolver", feature = "fs"))]
43pub fn apply_to_query_results(
44 mut results: Vec<QueryResult>,
45 opts: &QueryOptions,
46) -> Vec<QueryResult> {
47 if let Some(ref field) = opts.sort_by {
48 sort_query_results(&mut results, field);
49 }
50 let offset = opts.offset.unwrap_or(0);
51 results
52 .into_iter()
53 .skip(offset)
54 .take(opts.limit.unwrap_or(usize::MAX))
55 .collect()
56}
57
58pub fn apply_to_annotations(mut results: Vec<Annotation>, opts: &QueryOptions) -> Vec<Annotation> {
60 if let Some(ref field) = opts.sort_by {
61 sort_annotations(&mut results, field);
62 }
63 let offset = opts.offset.unwrap_or(0);
64 results
65 .into_iter()
66 .skip(offset)
67 .take(opts.limit.unwrap_or(usize::MAX))
68 .collect()
69}
70
71#[cfg(all(feature = "resolver", feature = "fs"))]
72fn sort_query_results(results: &mut [QueryResult], field: &str) {
73 let (field, desc) = parse_field(field);
74 results.sort_by(|a, b| {
75 let ord = match field {
76 "line" => a.code_element.source.line.cmp(&b.code_element.source.line),
77 "name" => {
78 let an: &str = &a.code_element.name;
79 let bn: &str = &b.code_element.name;
80 an.cmp(bn)
81 }
82 "file" => {
83 let af: &str = &a.code_element.file;
84 let bf: &str = &b.code_element.file;
85 af.cmp(bf)
86 }
87 f if f.starts_with("attr:") => {
88 let attr = &f["attr:".len()..];
89 let attr_key = crate::types::AttrName::from(attr);
90 let av = a
91 .code_element
92 .attrs
93 .get(&attr_key)
94 .map(attr_sort_key)
95 .unwrap_or_default();
96 let bv = b
97 .code_element
98 .attrs
99 .get(&attr_key)
100 .map(attr_sort_key)
101 .unwrap_or_default();
102 av.cmp(&bv)
103 }
104 _ => std::cmp::Ordering::Equal,
105 };
106 if desc {
107 ord.reverse()
108 } else {
109 ord
110 }
111 });
112}
113
114fn sort_annotations(results: &mut [Annotation], field: &str) {
115 let (field, desc) = parse_field(field);
116 results.sort_by(|a, b| {
117 let ord = match field {
118 "tag" => {
119 let at: &str = &a.tag;
120 let bt: &str = &b.tag;
121 at.cmp(bt)
122 }
123 "file" => {
124 let af: &str = &a.file;
125 let bf: &str = &b.file;
126 af.cmp(bf)
127 }
128 "binding" => {
129 let ab: &str = &a.binding;
130 let bb: &str = &b.binding;
131 ab.cmp(bb)
132 }
133 f if f.starts_with("attr:") => {
134 let attr = &f["attr:".len()..];
135 let attr_key = crate::types::AttrName::from(attr);
136 let av = a
137 .attrs
138 .get(&attr_key)
139 .map(attr_sort_key)
140 .unwrap_or_default();
141 let bv = b
142 .attrs
143 .get(&attr_key)
144 .map(attr_sort_key)
145 .unwrap_or_default();
146 av.cmp(&bv)
147 }
148 _ => std::cmp::Ordering::Equal,
149 };
150 if desc {
151 ord.reverse()
152 } else {
153 ord
154 }
155 });
156}
157
158fn parse_field(s: &str) -> (&str, bool) {
160 if let Some(f) = s.strip_prefix('-') {
161 (f, true)
162 } else {
163 (s, false)
164 }
165}
166
167fn attr_sort_key(v: &serde_json::Value) -> String {
169 match v {
170 serde_json::Value::String(s) => s.clone(),
171 serde_json::Value::Number(n) => {
172 if let Some(i) = n.as_u64() {
174 format!("{i:020}")
175 } else if let Some(i) = n.as_i64() {
176 format!("{:020}", i.wrapping_sub(i64::MIN))
178 } else {
179 n.to_string()
180 }
181 }
182 serde_json::Value::Bool(b) => b.to_string(),
183 serde_json::Value::Null => String::new(),
184 other => other.to_string(),
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191 use crate::store::Annotation;
192 use crate::types::{Binding, RelativePath, TagName};
193 use rustc_hash::FxHashMap;
194
195 fn make_ann(tag: &str, file: &str) -> Annotation {
196 Annotation {
197 tag: TagName::from(tag),
198 attrs: FxHashMap::default(),
199 binding: Binding::from(""),
200 file: RelativePath::from(file),
201 children: vec![],
202 }
203 }
204
205 #[test]
206 fn sorts_annotations_by_tag() {
207 let results = vec![
209 make_ann("route", "b.ts"),
210 make_ann("controller", "a.ts"),
211 make_ann("middleware", "c.ts"),
212 ];
213 let opts = QueryOptions {
214 sort_by: Some("tag".into()),
215 ..Default::default()
216 };
217
218 let results = apply_to_annotations(results, &opts);
220
221 let tags: Vec<&str> = results.iter().map(|a| a.tag.as_ref()).collect();
223 assert_eq!(
224 tags,
225 ["controller", "middleware", "route"],
226 "should be sorted by tag asc"
227 );
228 }
229
230 #[test]
231 fn applies_limit_and_offset() {
232 let results: Vec<Annotation> = (0..5).map(|i| make_ann(&format!("t{i}"), "f.ts")).collect();
234 let opts = QueryOptions {
235 offset: Some(1),
236 limit: Some(2),
237 ..Default::default()
238 };
239
240 let results = apply_to_annotations(results, &opts);
242
243 assert_eq!(results.len(), 2, "exactly 2 results after offset+limit");
245 let tag: &str = &results[0].tag;
246 assert_eq!(
247 tag, "t1",
248 "first result is the second element (after offset 1)"
249 );
250 }
251
252 #[test]
253 fn sorts_descending_with_minus_prefix() {
254 let results = vec![
256 make_ann("a", "f.ts"),
257 make_ann("c", "f.ts"),
258 make_ann("b", "f.ts"),
259 ];
260 let opts = QueryOptions {
261 sort_by: Some("-tag".into()),
262 ..Default::default()
263 };
264
265 let results = apply_to_annotations(results, &opts);
267
268 let tags: Vec<&str> = results.iter().map(|a| a.tag.as_ref()).collect();
270 assert_eq!(tags, ["c", "b", "a"], "should be sorted by tag desc");
271 }
272}