Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

629 linesCodeBlameRaw
1//! The SQL each search runs, built from a parsed query and its reader.
2//! Pure: what is asked of D1 is tested here, including that every query
3//! of repository content carries the visibility check.
4
5use serde::Serialize;
6
7use g1t_contracts::search::COUNT_CAP;
8
9use crate::query::{Filter, ItemKind, ItemState, Query, Visibility, path_pattern};
10use crate::visibility::{Reader, clause, item_clause};
11
12/// A value bound to a `?`.
13#[derive(Clone, Debug, PartialEq, Serialize)]
14#[serde(untagged)]
15pub enum Param {
16 Text(String),
17 Int(i64),
18 Null,
19}
20
21impl Param {
22 /// Text, or null when there is none.
23 pub fn opt(value: Option<&str>) -> Param {
24 value.map_or(Param::Null, |text| Param::Text(text.to_owned()))
25 }
26}
27
28impl From<&str> for Param {
29 fn from(value: &str) -> Param {
30 Param::Text(value.to_owned())
31 }
32}
33
34impl From<String> for Param {
35 fn from(value: String) -> Param {
36 Param::Text(value)
37 }
38}
39
40/// A statement and its values, in order.
41#[derive(Clone, Debug, Default, PartialEq, Serialize)]
42pub struct Sql {
43 pub text: String,
44 pub params: Vec<Param>,
45}
46
47/// Conditions joined with AND, each with its values.
48#[derive(Default)]
49struct Where {
50 parts: Vec<String>,
51 params: Vec<Param>,
52}
53
54impl Where {
55 fn add(&mut self, condition: impl Into<String>, params: Vec<Param>) {
56 self.parts.push(condition.into());
57 self.params.extend(params);
58 }
59
60 fn text(&self) -> String {
61 if self.parts.is_empty() { "1".to_owned() } else { self.parts.join(" AND ") }
62 }
63}
64
65fn json(values: &[String]) -> Param {
66 Param::Text(serde_json::to_string(values).unwrap_or_else(|_| "[]".into()))
67}
68
69/// `column IN (…)` for kept values, `NOT IN` for left-out ones.
70fn in_list(filter: &Filter, column: &str, out: &mut Where) {
71 if !filter.include.is_empty() {
72 out.add(format!("{column} IN (SELECT value FROM json_each(?))"), vec![json(&filter.include)]);
73 }
74 if !filter.exclude.is_empty() {
75 out.add(format!("COALESCE({column}, '') NOT IN (SELECT value FROM json_each(?))"), vec![json(&filter.exclude)]);
76 }
77}
78
79/// What every query of repository content starts with: the visibility
80/// check, then `repo:`, `org:` and `is:public`/`is:private`.
81fn scope(query: &Query, reader: &Reader, alias: &str) -> Where {
82 let mut out = Where::default();
83 out.add(clause(alias), vec![Param::Text(reader.namespaces_json()), Param::Text(reader.granted_json())]);
84 in_list(&query.repos, &format!("({alias}.namespace || '/' || {alias}.name)"), &mut out);
85 in_list(&query.owners, &format!("{alias}.namespace"), &mut out);
86 match query.visibility {
87 Some(Visibility::Public) => out.add(format!("{alias}.private = 0"), vec![]),
88 Some(Visibility::Private) => out.add(format!("{alias}.private = 1"), vec![]),
89 None => {}
90 }
91 out
92}
93
94/// A page: `LIMIT ? OFFSET ?`.
95#[derive(Clone, Copy, Debug)]
96pub struct Page {
97 pub limit: u32,
98 pub offset: u32,
99}
100
101fn paged(mut sql: Sql, page: Page) -> Sql {
102 sql.text.push_str(" LIMIT ? OFFSET ?");
103 sql.params.push(Param::Int(i64::from(page.limit)));
104 sql.params.push(Param::Int(i64::from(page.offset)));
105 sql
106}
107
108/// How many rows `select` returns, stopping at one past [`COUNT_CAP`].
109pub fn count(select: &Sql) -> Sql {
110 Sql {
111 text: format!("SELECT count(*) AS n FROM ({} LIMIT {})", select.text, COUNT_CAP + 1),
112 params: select.params.clone(),
113 }
114}
115
116/// The words of a query as one lowercase string, for an exact-name boost.
117fn exact(query: &Query) -> String {
118 query.positive().map(|term| term.text.to_lowercase()).collect::<Vec<_>>().join(" ")
119}
120
121// ---- Repositories ----------------------------------------------------------
122
123const REPO_COLUMNS: &str = "r.repo_id, r.namespace, r.name, r.description, r.topics, r.language, r.private, r.pushed_at, r.created_at";
124
125/// Repositories matching the query, without order or page.
126fn repos_select(query: &Query, reader: &Reader) -> (Sql, bool) {
127 let mut filters = scope(query, reader, "r");
128 in_list(&query.languages, "r.language", &mut filters);
129 match query.prose_match() {
130 Some(expression) => {
131 let mut params = vec![Param::Text(expression)];
132 params.extend(filters.params.clone());
133 (
134 Sql {
135 text: format!(
136 "SELECT {REPO_COLUMNS}, snippet(repos_fts, 4, '', '', '…', 24) AS readme \
137 FROM repos_fts JOIN repos r ON r.rid = repos_fts.rowid \
138 WHERE repos_fts MATCH ? AND {}",
139 filters.text()
140 ),
141 params,
142 },
143 true,
144 )
145 }
146 None => (
147 Sql {
148 text: format!("SELECT {REPO_COLUMNS}, NULL AS readme FROM repos r WHERE {}", filters.text()),
149 params: filters.params,
150 },
151 false,
152 ),
153 }
154}
155
156pub fn repos_count(query: &Query, reader: &Reader) -> Sql {
157 count(&repos_select(query, reader).0)
158}
159
160pub fn repos_page(query: &Query, reader: &Reader, page: Page) -> Sql {
161 let (mut sql, text) = repos_select(query, reader);
162 if text {
163 let name = exact(query);
164 sql.text.push_str(
165 " ORDER BY (r.name = ? OR (r.namespace || '/' || r.name) = ?) DESC, \
166 bm25(repos_fts, 3.0, 10.0, 4.0, 6.0, 1.0), r.pushed_at DESC",
167 );
168 sql.params.push(Param::Text(name.clone()));
169 sql.params.push(Param::Text(name));
170 } else {
171 sql.text.push_str(" ORDER BY COALESCE(r.pushed_at, r.created_at) DESC, r.rid DESC");
172 }
173 paged(sql, page)
174}
175
176// ---- Code ------------------------------------------------------------------
177
178/// How a code search reads the index.
179#[derive(Clone, Debug, PartialEq)]
180pub enum CodePlan {
181 /// Through the trigram index; `short` words are matched directly.
182 Index { expression: String, short: Vec<String> },
183 /// Words too short for the index, in named repositories only.
184 Scan { short: Vec<String> },
185 /// No words: the files of named repositories, by path.
186 Files,
187}
188
189/// How to search code for a query, or why it cannot be.
190pub fn code_plan(query: &Query) -> Result<CodePlan, &'static str> {
191 let (expression, short) = query.code_match();
192 let scoped = !query.repos.include.is_empty();
193 match expression {
194 Some(expression) => Ok(CodePlan::Index { expression, short }),
195 None if !scoped => Err("Code search needs a word of at least three characters, or repo:owner/name."),
196 None if short.is_empty() => Ok(CodePlan::Files),
197 None => Ok(CodePlan::Scan { short }),
198 }
199}
200
201fn code_filters(query: &Query, reader: &Reader) -> Where {
202 let mut filters = scope(query, reader, "r");
203 in_list(&query.languages, "fl.language", &mut filters);
204 if !query.paths.include.is_empty() {
205 let any = vec!["fl.path LIKE ? ESCAPE '\\'"; query.paths.include.len()].join(" OR ");
206 filters.add(format!("({any})"), query.paths.include.iter().map(|p| Param::Text(path_pattern(p))).collect());
207 }
208 for path in &query.paths.exclude {
209 filters.add("fl.path NOT LIKE ? ESCAPE '\\'", vec![Param::Text(path_pattern(path))]);
210 }
211 filters
212}
213
214fn short_words(short: &[String], column: &str, out: &mut Where) {
215 for word in short {
216 out.add(format!("instr(lower({column}), ?) > 0"), vec![Param::Text(word.to_lowercase())]);
217 }
218}
219
220/// Matching files, one row each (`fid`, `score`), without order or page.
221fn code_select(plan: &CodePlan, query: &Query, reader: &Reader) -> Sql {
222 let mut filters = code_filters(query, reader);
223 match plan {
224 CodePlan::Index { expression, short } => {
225 short_words(short, "c.content", &mut filters);
226 let mut params = vec![Param::Text(expression.clone())];
227 params.extend(filters.params.clone());
228 Sql {
229 // bm25() cannot be called inside an aggregate; the rank
230 // column can, weighted as the migration sets it.
231 text: format!(
232 "SELECT c.fid AS fid, MIN(chunks_fts.rank) AS score, fl.path AS path FROM chunks_fts JOIN chunks c ON c.cid = chunks_fts.rowid JOIN files fl ON fl.fid = c.fid JOIN repos r ON r.repo_id = fl.repo_id WHERE chunks_fts MATCH ? AND {} GROUP BY c.fid",
233 filters.text()
234 ),
235 params,
236 }
237 }
238 CodePlan::Scan { short } => {
239 short_words(short, "c.content", &mut filters);
240 Sql {
241 text: format!(
242 "SELECT c.fid AS fid, 0 AS score, fl.path AS path \
243 FROM chunks c JOIN files fl ON fl.fid = c.fid JOIN repos r ON r.repo_id = fl.repo_id \
244 WHERE {} GROUP BY c.fid",
245 filters.text()
246 ),
247 params: filters.params,
248 }
249 }
250 CodePlan::Files => {
251 filters.add("fl.skipped IS NULL", vec![]);
252 Sql {
253 text: format!(
254 "SELECT fl.fid AS fid, 0 AS score, fl.path AS path \
255 FROM files fl JOIN repos r ON r.repo_id = fl.repo_id WHERE {}",
256 filters.text()
257 ),
258 params: filters.params,
259 }
260 }
261 }
262}
263
264pub fn code_count(plan: &CodePlan, query: &Query, reader: &Reader) -> Sql {
265 count(&code_select(plan, query, reader))
266}
267
268pub fn code_page(plan: &CodePlan, query: &Query, reader: &Reader, page: Page) -> Sql {
269 let mut sql = code_select(plan, query, reader);
270 sql.text.push_str(" ORDER BY score, path");
271 paged(sql, page)
272}
273
274/// The files of a page: where each is and in which repository.
275pub fn code_files(fids: &[i64]) -> Sql {
276 Sql {
277 text: "SELECT fl.fid, fl.path, fl.language, r.repo_id, r.namespace, r.name, r.default_branch, r.private \
278 FROM files fl JOIN repos r ON r.repo_id = fl.repo_id \
279 WHERE fl.fid IN (SELECT value FROM json_each(?))"
280 .to_owned(),
281 params: vec![Param::Text(serde_json::to_string(fids).unwrap_or_else(|_| "[]".into()))],
282 }
283}
284
285/// The pieces of a page's files that matched, at most `limit`.
286pub fn code_pieces(plan: &CodePlan, fids: &[i64], limit: u32) -> Sql {
287 let ids = Param::Text(serde_json::to_string(fids).unwrap_or_else(|_| "[]".into()));
288 match plan {
289 CodePlan::Index { expression, .. } => Sql {
290 text: "SELECT c.fid, c.start_line, c.content FROM chunks_fts JOIN chunks c ON c.cid = chunks_fts.rowid \
291 WHERE chunks_fts MATCH ? AND c.fid IN (SELECT value FROM json_each(?)) \
292 ORDER BY c.fid, c.start_line LIMIT ?"
293 .to_owned(),
294 params: vec![Param::Text(expression.clone()), ids, Param::Int(i64::from(limit))],
295 },
296 CodePlan::Scan { short } => {
297 let mut filters = Where::default();
298 filters.add("c.fid IN (SELECT value FROM json_each(?))", vec![ids]);
299 short_words(short, "c.content", &mut filters);
300 let mut params = filters.params;
301 params.push(Param::Int(i64::from(limit)));
302 Sql {
303 text: format!(
304 "SELECT c.fid, c.start_line, c.content FROM chunks c WHERE {} ORDER BY c.fid, c.start_line LIMIT ?",
305 filters.parts.join(" AND ")
306 ),
307 params,
308 }
309 }
310 CodePlan::Files => first_pieces(fids),
311 }
312}
313
314/// The first piece of each file, for files that matched by name.
315pub fn first_pieces(fids: &[i64]) -> Sql {
316 Sql {
317 text: "SELECT c.fid, c.start_line, c.content FROM chunks c \
318 WHERE c.fid IN (SELECT value FROM json_each(?)) AND c.start_line = 1"
319 .to_owned(),
320 params: vec![Param::Text(serde_json::to_string(fids).unwrap_or_else(|_| "[]".into()))],
321 }
322}
323
324// ---- Work items and pull requests ------------------------------------------
325
326const ITEM_COLUMNS: &str = "i.kind, i.workspace, i.number, i.title, i.state, i.status, i.author, i.requested_by, i.labels, i.updated_at, r.repo_id, r.namespace, r.name, r.private";
327
328/// What every query of work items starts with: the visibility check (a
329/// member of the workspace, or a reader of the origin), then `repo:`
330/// (any repository it is linked to), `org:` (its workspace) and
331/// `is:public`/`is:private` (whether its origin is public).
332fn item_scope(query: &Query, reader: &Reader) -> Where {
333 let mut out = Where::default();
334 out.add(
335 item_clause("i", "r"),
336 vec![Param::Text(reader.members_json()), Param::Text(reader.namespaces_json()), Param::Text(reader.granted_json())],
337 );
338 let linked = "SELECT 1 FROM repos lr WHERE (lr.namespace || '/' || lr.name) IN (SELECT value FROM json_each(?)) AND instr(i.repos, '|' || lr.repo_id || '|') > 0";
339 if !query.repos.include.is_empty() {
340 out.add(format!("EXISTS ({linked})"), vec![json(&query.repos.include)]);
341 }
342 if !query.repos.exclude.is_empty() {
343 out.add(format!("NOT EXISTS ({linked})"), vec![json(&query.repos.exclude)]);
344 }
345 in_list(&query.owners, "i.workspace", &mut out);
346 match query.visibility {
347 Some(Visibility::Public) => out.add("r.private = 0", vec![]),
348 Some(Visibility::Private) => out.add("COALESCE(r.private, 1) = 1", vec![]),
349 None => {}
350 }
351 out
352}
353
354fn items_select(query: &Query, reader: &Reader, kind: ItemKind) -> (Sql, bool) {
355 // A pull request belongs to its repository; a work item to its
356 // workspace, with its origin, if any, joined for who may read it.
357 let (mut filters, join) = match kind {
358 ItemKind::Item => (item_scope(query, reader), "LEFT JOIN repos r ON r.repo_id = i.repo_id"),
359 ItemKind::Pull => (scope(query, reader, "r"), "JOIN repos r ON r.repo_id = i.repo_id"),
360 };
361 filters.add("i.kind = ?", vec![Param::Text(if kind == ItemKind::Item { "item" } else { "pull" }.into())]);
362 match query.state {
363 Some(ItemState::Open) => filters.add("i.state = 'open'", vec![]),
364 Some(ItemState::Closed) => filters.add("i.state = 'closed'", vec![]),
365 Some(ItemState::Merged) => filters.add("i.status = 'merged'", vec![]),
366 Some(ItemState::Draft) => filters.add("i.status = 'draft'", vec![]),
367 None => {}
368 }
369 in_list(&query.authors, "i.author", &mut filters);
370 for label in &query.labels.include {
371 filters.add("instr(i.labels, ?) > 0", vec![Param::Text(format!("|{label}|"))]);
372 }
373 for label in &query.labels.exclude {
374 filters.add("instr(i.labels, ?) = 0", vec![Param::Text(format!("|{label}|"))]);
375 }
376 match query.prose_match() {
377 Some(expression) => {
378 let mut params = vec![Param::Text(expression)];
379 params.extend(filters.params.clone());
380 (
381 Sql {
382 text: format!(
383 "SELECT {ITEM_COLUMNS}, snippet(items_fts, 1, '', '', '…', 40) AS body FROM items_fts JOIN items i ON i.iid = items_fts.rowid {join} WHERE items_fts MATCH ? AND {}",
384 filters.text()
385 ),
386 params,
387 },
388 true,
389 )
390 }
391 None => (
392 Sql {
393 text: format!(
394 "SELECT {ITEM_COLUMNS}, substr(i.body, 1, 400) AS body FROM items i {join} WHERE {}",
395 filters.text()
396 ),
397 params: filters.params,
398 },
399 false,
400 ),
401 }
402}
403
404pub fn items_count(query: &Query, reader: &Reader, kind: ItemKind) -> Sql {
405 count(&items_select(query, reader, kind).0)
406}
407
408pub fn items_page(query: &Query, reader: &Reader, kind: ItemKind, page: Page) -> Sql {
409 let (mut sql, text) = items_select(query, reader, kind);
410 sql.text.push_str(if text {
411 " ORDER BY bm25(items_fts, 4.0, 1.0), i.updated_at DESC"
412 } else {
413 " ORDER BY i.updated_at DESC, i.iid DESC"
414 });
415 paged(sql, page)
416}
417
418// ---- People ----------------------------------------------------------------
419
420/// People and workspaces; only words narrow them.
421pub fn people_select(query: &Query) -> Option<Sql> {
422 let expression = query.prose_match()?;
423 Some(Sql {
424 text: "SELECT p.kind, p.slug, p.name, p.bio, p.avatar FROM people_fts JOIN people p ON p.pid = people_fts.rowid \
425 WHERE people_fts MATCH ?"
426 .to_owned(),
427 params: vec![Param::Text(expression)],
428 })
429}
430
431pub fn people_page(query: &Query, page: Page) -> Option<Sql> {
432 let mut sql = people_select(query)?;
433 sql.text.push_str(" ORDER BY (p.slug = ?) DESC, bm25(people_fts, 10.0, 5.0, 1.0), p.kind DESC");
434 sql.params.push(Param::Text(exact(query)));
435 Some(paged(sql, page))
436}
437
438// ---- Explore ---------------------------------------------------------------
439
440/// Public repositories, recently active or newly made.
441pub fn explore(newest: bool, language: Option<&str>, topic: Option<&str>, page: Page) -> Sql {
442 let mut filters = Where::default();
443 filters.add("r.private = 0", vec![]);
444 if let Some(language) = language {
445 filters.add("r.language = ?", vec![Param::Text(language.to_owned())]);
446 }
447 if let Some(topic) = topic {
448 filters.add("r.repo_id IN (SELECT repo_id FROM repo_topics WHERE topic = ?)", vec![Param::Text(topic.to_owned())]);
449 }
450 let order = if newest { "r.created_at DESC" } else { "COALESCE(r.pushed_at, r.created_at) DESC" };
451 paged(
452 Sql {
453 text: format!("SELECT {REPO_COLUMNS} FROM repos r WHERE {} ORDER BY {order}, r.rid DESC", filters.text()),
454 params: filters.params,
455 },
456 page,
457 )
458}
459
460pub const LANGUAGE_FACETS: &str = "SELECT language AS name, count(*) AS count FROM repos \
461 WHERE private = 0 AND language IS NOT NULL GROUP BY language ORDER BY count DESC, name LIMIT 24";
462
463pub const TOPIC_FACETS: &str = "SELECT t.topic AS name, count(*) AS count FROM repo_topics t \
464 JOIN repos r ON r.repo_id = t.repo_id WHERE r.private = 0 GROUP BY t.topic ORDER BY count DESC, name LIMIT 30";
465
466#[cfg(test)]
467mod tests {
468 use g1t_contracts::{Membership, User};
469
470 use super::*;
471
472 fn reader(workspaces: &[&str]) -> Reader {
473 Reader::of(&Some(User {
474 workspaces: workspaces.iter().map(|w| Membership::member(*w)).collect(),
475 ..User::default()
476 }))
477 }
478
479 const PAGE: Page = Page { limit: 20, offset: 0 };
480
481 fn placeholders(sql: &Sql) -> usize {
482 sql.text.matches('?').count()
483 }
484
485 #[test]
486 fn every_query_of_repository_content_checks_visibility() {
487 let query = Query::parse("parse repo:acme/web");
488 let reader = reader(&["acme"]);
489 let plan = code_plan(&query).unwrap();
490 for sql in [
491 repos_page(&query, &reader, PAGE),
492 repos_count(&query, &reader),
493 code_page(&plan, &query, &reader, PAGE),
494 code_count(&plan, &query, &reader),
495 items_page(&query, &reader, ItemKind::Item, PAGE),
496 items_count(&query, &reader, ItemKind::Item),
497 items_page(&query, &reader, ItemKind::Pull, PAGE),
498 items_count(&query, &reader, ItemKind::Pull),
499 ] {
500 assert!(sql.text.contains("r.private = 0 OR r.namespace IN"), "{}", sql.text);
501 assert!(sql.text.contains("OR r.repo_id IN"), "{}", sql.text);
502 assert!(sql.params.contains(&Param::Text(r#"["acme"]"#.into())));
503 assert_eq!(placeholders(&sql), sql.params.len(), "{}", sql.text);
504 }
505 }
506
507 #[test]
508 fn signed_out_readers_bind_no_workspaces() {
509 let sql = repos_page(&Query::parse("x"), &Reader::of(&None), PAGE);
510 assert!(sql.params.contains(&Param::Text("[]".into())));
511 }
512
513 #[test]
514 fn qualifiers_become_conditions() {
515 let query = Query::parse("parse language:rust path:*.rs -path:tests/ org:acme");
516 let plan = code_plan(&query).unwrap();
517 let sql = code_page(&plan, &query, &reader(&[]), PAGE);
518 assert!(sql.text.contains("fl.language IN"));
519 assert!(sql.text.contains("fl.path LIKE ? ESCAPE"));
520 assert!(sql.text.contains("fl.path NOT LIKE ?"));
521 assert!(sql.text.contains("r.namespace IN"));
522 assert!(sql.params.contains(&Param::Text("%.rs".into())));
523 assert!(sql.params.contains(&Param::Text("%tests/%".into())));
524 assert_eq!(placeholders(&sql), sql.params.len());
525 }
526
527 #[test]
528 fn work_items_are_for_members_and_readers_of_their_origin() {
529 let query = Query::parse("crash repo:acme/web -repo:acme/old org:acme is:private");
530 let sql = items_page(&query, &reader(&["acme"]), ItemKind::Item, PAGE);
531 assert!(sql.text.contains("LEFT JOIN repos r"));
532 assert!(sql.text.contains("i.workspace IN (SELECT value FROM json_each(?)) OR ((r.repo_id IS NOT NULL)"));
533 // repo: is any linked repository, org: the workspace.
534 assert!(sql.text.contains("EXISTS (SELECT 1 FROM repos lr"));
535 assert!(sql.text.contains("NOT EXISTS (SELECT 1 FROM repos lr"));
536 assert!(sql.text.contains("i.workspace IN (SELECT value FROM json_each(?))"));
537 assert!(sql.text.contains("COALESCE(r.private, 1) = 1"));
538 assert!(sql.params.contains(&Param::Text(r#"["acme/web"]"#.into())));
539 assert!(sql.params.contains(&Param::Text("item".into())));
540 assert_eq!(placeholders(&sql), sql.params.len(), "{}", sql.text);
541 }
542
543 #[test]
544 fn item_qualifiers_become_conditions() {
545 let query = Query::parse("crash is:closed author:ana label:bug -label:wontfix");
546 let sql = items_page(&query, &reader(&[]), ItemKind::Item, PAGE);
547 assert!(sql.text.contains("i.state = 'closed'"));
548 assert!(sql.text.contains("i.author IN"));
549 assert!(sql.params.contains(&Param::Text("|bug|".into())));
550 assert!(sql.text.contains("instr(i.labels, ?) = 0"));
551 assert_eq!(placeholders(&sql), sql.params.len());
552 }
553
554 #[test]
555 fn code_needs_three_characters_unless_scoped() {
556 assert!(code_plan(&Query::parse("fn")).is_err());
557 assert_eq!(code_plan(&Query::parse("fn repo:a/b")).unwrap(), CodePlan::Scan { short: vec!["fn".into()] });
558 assert_eq!(code_plan(&Query::parse("repo:a/b path:src")).unwrap(), CodePlan::Files);
559 assert!(matches!(code_plan(&Query::parse("fn main")).unwrap(), CodePlan::Index { .. }));
560 }
561
562 #[test]
563 fn every_plan_binds_what_it_asks() {
564 let reader = reader(&[]);
565 for text in ["fn repo:a/b", "repo:a/b path:src", "fn main"] {
566 let query = Query::parse(text);
567 let plan = code_plan(&query).unwrap();
568 for sql in [code_page(&plan, &query, &reader, PAGE), code_count(&plan, &query, &reader), code_pieces(&plan, &[1, 2], 10)] {
569 assert_eq!(placeholders(&sql), sql.params.len(), "{text}: {}", sql.text);
570 }
571 }
572 }
573
574 #[test]
575 fn counts_stop_past_the_cap() {
576 let sql = repos_count(&Query::parse("x"), &reader(&[]));
577 assert!(sql.text.ends_with(&format!("LIMIT {})", COUNT_CAP + 1)));
578 }
579
580 #[test]
581 fn explore_is_public_only() {
582 let sql = explore(false, Some("rust"), Some("cli"), PAGE);
583 assert!(sql.text.contains("r.private = 0"));
584 assert_eq!(placeholders(&sql), sql.params.len());
585 }
586
587 /// Writes every kind of statement to stdout as JSON, to run against
588 /// SQLite with the migrations: `cargo test -p g1t-search dump_sql --
589 /// --ignored --nocapture`.
590 #[test]
591 #[ignore]
592 fn dump_sql() {
593 let reader = reader(&["acme"]);
594 let mut out = Vec::new();
595 for text in [
596 "parse",
597 "parse repo:acme/web",
598 "\"fn main\" language:rust path:src -path:tests",
599 "fn repo:acme/web",
600 "repo:acme/web path:*.rs",
601 "crash is:open label:bug author:ana",
602 "crash repo:acme/web -repo:acme/old org:acme is:private",
603 "language:rust",
604 "ana",
605 ] {
606 let query = Query::parse(text);
607 out.push(repos_page(&query, &reader, PAGE));
608 out.push(repos_count(&query, &reader));
609 if let Ok(plan) = code_plan(&query) {
610 out.push(code_page(&plan, &query, &reader, PAGE));
611 out.push(code_count(&plan, &query, &reader));
612 out.push(code_pieces(&plan, &[1, 2], 10));
613 }
614 out.push(items_page(&query, &reader, ItemKind::Item, PAGE));
615 out.push(items_count(&query, &reader, ItemKind::Item));
616 out.push(items_page(&query, &reader, ItemKind::Pull, PAGE));
617 out.push(items_count(&query, &reader, ItemKind::Pull));
618 if let Some(sql) = people_page(&query, PAGE) {
619 out.push(sql);
620 }
621 }
622 out.push(code_files(&[1, 2]));
623 out.push(first_pieces(&[1, 2]));
624 out.push(explore(true, Some("rust"), Some("cli"), PAGE));
625 out.push(Sql { text: LANGUAGE_FACETS.into(), params: vec![] });
626 out.push(Sql { text: TOPIC_FACETS.into(), params: vec![] });
627 println!("{}", serde_json::to_string(&out).unwrap());
628 }
629}