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.

633 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 that the reader may see: one they may
331/// not is answered as one that does not exist, so neither `repo:` nor
332/// `-repo:` says a private repository is there), `org:` (its workspace)
333/// and `is:public`/`is:private` (whether its origin is public).
334fn item_scope(query: &Query, reader: &Reader) -> Where {
335 let mut out = Where::default();
336 out.add(
337 item_clause("i", "r"),
338 vec![Param::Text(reader.members_json()), Param::Text(reader.namespaces_json()), Param::Text(reader.granted_json())],
339 );
340 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";
341 let linked = format!("{linked} AND {}", clause("lr"));
342 let named = |names: &[String]| vec![json(names), Param::Text(reader.namespaces_json()), Param::Text(reader.granted_json())];
343 if !query.repos.include.is_empty() {
344 out.add(format!("EXISTS ({linked})"), named(&query.repos.include));
345 }
346 if !query.repos.exclude.is_empty() {
347 out.add(format!("NOT EXISTS ({linked})"), named(&query.repos.exclude));
348 }
349 in_list(&query.owners, "i.workspace", &mut out);
350 match query.visibility {
351 Some(Visibility::Public) => out.add("r.private = 0", vec![]),
352 Some(Visibility::Private) => out.add("COALESCE(r.private, 1) = 1", vec![]),
353 None => {}
354 }
355 out
356}
357
358fn items_select(query: &Query, reader: &Reader, kind: ItemKind) -> (Sql, bool) {
359 // A pull request belongs to its repository; a work item to its
360 // workspace, with its origin, if any, joined for who may read it.
361 let (mut filters, join) = match kind {
362 ItemKind::Item => (item_scope(query, reader), "LEFT JOIN repos r ON r.repo_id = i.repo_id"),
363 ItemKind::Pull => (scope(query, reader, "r"), "JOIN repos r ON r.repo_id = i.repo_id"),
364 };
365 filters.add("i.kind = ?", vec![Param::Text(if kind == ItemKind::Item { "item" } else { "pull" }.into())]);
366 match query.state {
367 Some(ItemState::Open) => filters.add("i.state = 'open'", vec![]),
368 Some(ItemState::Closed) => filters.add("i.state = 'closed'", vec![]),
369 Some(ItemState::Merged) => filters.add("i.status = 'merged'", vec![]),
370 Some(ItemState::Draft) => filters.add("i.status = 'draft'", vec![]),
371 None => {}
372 }
373 in_list(&query.authors, "i.author", &mut filters);
374 for label in &query.labels.include {
375 filters.add("instr(i.labels, ?) > 0", vec![Param::Text(format!("|{label}|"))]);
376 }
377 for label in &query.labels.exclude {
378 filters.add("instr(i.labels, ?) = 0", vec![Param::Text(format!("|{label}|"))]);
379 }
380 match query.prose_match() {
381 Some(expression) => {
382 let mut params = vec![Param::Text(expression)];
383 params.extend(filters.params.clone());
384 (
385 Sql {
386 text: format!(
387 "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 {}",
388 filters.text()
389 ),
390 params,
391 },
392 true,
393 )
394 }
395 None => (
396 Sql {
397 text: format!(
398 "SELECT {ITEM_COLUMNS}, substr(i.body, 1, 400) AS body FROM items i {join} WHERE {}",
399 filters.text()
400 ),
401 params: filters.params,
402 },
403 false,
404 ),
405 }
406}
407
408pub fn items_count(query: &Query, reader: &Reader, kind: ItemKind) -> Sql {
409 count(&items_select(query, reader, kind).0)
410}
411
412pub fn items_page(query: &Query, reader: &Reader, kind: ItemKind, page: Page) -> Sql {
413 let (mut sql, text) = items_select(query, reader, kind);
414 sql.text.push_str(if text {
415 " ORDER BY bm25(items_fts, 4.0, 1.0), i.updated_at DESC"
416 } else {
417 " ORDER BY i.updated_at DESC, i.iid DESC"
418 });
419 paged(sql, page)
420}
421
422// ---- People ----------------------------------------------------------------
423
424/// People and workspaces; only words narrow them.
425pub fn people_select(query: &Query) -> Option<Sql> {
426 let expression = query.prose_match()?;
427 Some(Sql {
428 text: "SELECT p.kind, p.slug, p.name, p.bio, p.avatar FROM people_fts JOIN people p ON p.pid = people_fts.rowid \
429 WHERE people_fts MATCH ?"
430 .to_owned(),
431 params: vec![Param::Text(expression)],
432 })
433}
434
435pub fn people_page(query: &Query, page: Page) -> Option<Sql> {
436 let mut sql = people_select(query)?;
437 sql.text.push_str(" ORDER BY (p.slug = ?) DESC, bm25(people_fts, 10.0, 5.0, 1.0), p.kind DESC");
438 sql.params.push(Param::Text(exact(query)));
439 Some(paged(sql, page))
440}
441
442// ---- Explore ---------------------------------------------------------------
443
444/// Public repositories, recently active or newly made.
445pub fn explore(newest: bool, language: Option<&str>, topic: Option<&str>, page: Page) -> Sql {
446 let mut filters = Where::default();
447 filters.add("r.private = 0", vec![]);
448 if let Some(language) = language {
449 filters.add("r.language = ?", vec![Param::Text(language.to_owned())]);
450 }
451 if let Some(topic) = topic {
452 filters.add("r.repo_id IN (SELECT repo_id FROM repo_topics WHERE topic = ?)", vec![Param::Text(topic.to_owned())]);
453 }
454 let order = if newest { "r.created_at DESC" } else { "COALESCE(r.pushed_at, r.created_at) DESC" };
455 paged(
456 Sql {
457 text: format!("SELECT {REPO_COLUMNS} FROM repos r WHERE {} ORDER BY {order}, r.rid DESC", filters.text()),
458 params: filters.params,
459 },
460 page,
461 )
462}
463
464pub const LANGUAGE_FACETS: &str = "SELECT language AS name, count(*) AS count FROM repos \
465 WHERE private = 0 AND language IS NOT NULL GROUP BY language ORDER BY count DESC, name LIMIT 24";
466
467pub const TOPIC_FACETS: &str = "SELECT t.topic AS name, count(*) AS count FROM repo_topics t \
468 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";
469
470#[cfg(test)]
471mod tests {
472 use g1t_contracts::{Membership, User};
473
474 use super::*;
475
476 fn reader(workspaces: &[&str]) -> Reader {
477 Reader::of(&Some(User {
478 workspaces: workspaces.iter().map(|w| Membership::member(*w)).collect(),
479 ..User::default()
480 }))
481 }
482
483 const PAGE: Page = Page { limit: 20, offset: 0 };
484
485 fn placeholders(sql: &Sql) -> usize {
486 sql.text.matches('?').count()
487 }
488
489 #[test]
490 fn every_query_of_repository_content_checks_visibility() {
491 let query = Query::parse("parse repo:acme/web");
492 let reader = reader(&["acme"]);
493 let plan = code_plan(&query).unwrap();
494 for sql in [
495 repos_page(&query, &reader, PAGE),
496 repos_count(&query, &reader),
497 code_page(&plan, &query, &reader, PAGE),
498 code_count(&plan, &query, &reader),
499 items_page(&query, &reader, ItemKind::Item, PAGE),
500 items_count(&query, &reader, ItemKind::Item),
501 items_page(&query, &reader, ItemKind::Pull, PAGE),
502 items_count(&query, &reader, ItemKind::Pull),
503 ] {
504 assert!(sql.text.contains("r.private = 0 OR r.namespace IN"), "{}", sql.text);
505 assert!(sql.text.contains("OR r.repo_id IN"), "{}", sql.text);
506 assert!(sql.params.contains(&Param::Text(r#"["acme"]"#.into())));
507 assert_eq!(placeholders(&sql), sql.params.len(), "{}", sql.text);
508 }
509 }
510
511 #[test]
512 fn signed_out_readers_bind_no_workspaces() {
513 let sql = repos_page(&Query::parse("x"), &Reader::of(&None), PAGE);
514 assert!(sql.params.contains(&Param::Text("[]".into())));
515 }
516
517 #[test]
518 fn qualifiers_become_conditions() {
519 let query = Query::parse("parse language:rust path:*.rs -path:tests/ org:acme");
520 let plan = code_plan(&query).unwrap();
521 let sql = code_page(&plan, &query, &reader(&[]), PAGE);
522 assert!(sql.text.contains("fl.language IN"));
523 assert!(sql.text.contains("fl.path LIKE ? ESCAPE"));
524 assert!(sql.text.contains("fl.path NOT LIKE ?"));
525 assert!(sql.text.contains("r.namespace IN"));
526 assert!(sql.params.contains(&Param::Text("%.rs".into())));
527 assert!(sql.params.contains(&Param::Text("%tests/%".into())));
528 assert_eq!(placeholders(&sql), sql.params.len());
529 }
530
531 #[test]
532 fn work_items_are_for_members_and_readers_of_their_origin() {
533 let query = Query::parse("crash repo:acme/web -repo:acme/old org:acme is:private");
534 let sql = items_page(&query, &reader(&["acme"]), ItemKind::Item, PAGE);
535 assert!(sql.text.contains("LEFT JOIN repos r"));
536 assert!(sql.text.contains("i.workspace IN (SELECT value FROM json_each(?)) OR ((r.repo_id IS NOT NULL)"));
537 // repo: is any linked repository, org: the workspace.
538 assert!(sql.text.contains("EXISTS (SELECT 1 FROM repos lr"));
539 assert!(sql.text.contains("NOT EXISTS (SELECT 1 FROM repos lr"));
540 assert!(sql.text.contains("i.workspace IN (SELECT value FROM json_each(?))"));
541 assert!(sql.text.contains("COALESCE(r.private, 1) = 1"));
542 assert!(sql.params.contains(&Param::Text(r#"["acme/web"]"#.into())));
543 assert!(sql.params.contains(&Param::Text("item".into())));
544 assert_eq!(placeholders(&sql), sql.params.len(), "{}", sql.text);
545 }
546
547 #[test]
548 fn item_qualifiers_become_conditions() {
549 let query = Query::parse("crash is:closed author:ana label:bug -label:wontfix");
550 let sql = items_page(&query, &reader(&[]), ItemKind::Item, PAGE);
551 assert!(sql.text.contains("i.state = 'closed'"));
552 assert!(sql.text.contains("i.author IN"));
553 assert!(sql.params.contains(&Param::Text("|bug|".into())));
554 assert!(sql.text.contains("instr(i.labels, ?) = 0"));
555 assert_eq!(placeholders(&sql), sql.params.len());
556 }
557
558 #[test]
559 fn code_needs_three_characters_unless_scoped() {
560 assert!(code_plan(&Query::parse("fn")).is_err());
561 assert_eq!(code_plan(&Query::parse("fn repo:a/b")).unwrap(), CodePlan::Scan { short: vec!["fn".into()] });
562 assert_eq!(code_plan(&Query::parse("repo:a/b path:src")).unwrap(), CodePlan::Files);
563 assert!(matches!(code_plan(&Query::parse("fn main")).unwrap(), CodePlan::Index { .. }));
564 }
565
566 #[test]
567 fn every_plan_binds_what_it_asks() {
568 let reader = reader(&[]);
569 for text in ["fn repo:a/b", "repo:a/b path:src", "fn main"] {
570 let query = Query::parse(text);
571 let plan = code_plan(&query).unwrap();
572 for sql in [code_page(&plan, &query, &reader, PAGE), code_count(&plan, &query, &reader), code_pieces(&plan, &[1, 2], 10)] {
573 assert_eq!(placeholders(&sql), sql.params.len(), "{text}: {}", sql.text);
574 }
575 }
576 }
577
578 #[test]
579 fn counts_stop_past_the_cap() {
580 let sql = repos_count(&Query::parse("x"), &reader(&[]));
581 assert!(sql.text.ends_with(&format!("LIMIT {})", COUNT_CAP + 1)));
582 }
583
584 #[test]
585 fn explore_is_public_only() {
586 let sql = explore(false, Some("rust"), Some("cli"), PAGE);
587 assert!(sql.text.contains("r.private = 0"));
588 assert_eq!(placeholders(&sql), sql.params.len());
589 }
590
591 /// Writes every kind of statement to stdout as JSON, to run against
592 /// SQLite with the migrations: `cargo test -p g1t-search dump_sql --
593 /// --ignored --nocapture`.
594 #[test]
595 #[ignore]
596 fn dump_sql() {
597 let reader = reader(&["acme"]);
598 let mut out = Vec::new();
599 for text in [
600 "parse",
601 "parse repo:acme/web",
602 "\"fn main\" language:rust path:src -path:tests",
603 "fn repo:acme/web",
604 "repo:acme/web path:*.rs",
605 "crash is:open label:bug author:ana",
606 "crash repo:acme/web -repo:acme/old org:acme is:private",
607 "language:rust",
608 "ana",
609 ] {
610 let query = Query::parse(text);
611 out.push(repos_page(&query, &reader, PAGE));
612 out.push(repos_count(&query, &reader));
613 if let Ok(plan) = code_plan(&query) {
614 out.push(code_page(&plan, &query, &reader, PAGE));
615 out.push(code_count(&plan, &query, &reader));
616 out.push(code_pieces(&plan, &[1, 2], 10));
617 }
618 out.push(items_page(&query, &reader, ItemKind::Item, PAGE));
619 out.push(items_count(&query, &reader, ItemKind::Item));
620 out.push(items_page(&query, &reader, ItemKind::Pull, PAGE));
621 out.push(items_count(&query, &reader, ItemKind::Pull));
622 if let Some(sql) = people_page(&query, PAGE) {
623 out.push(sql);
624 }
625 }
626 out.push(code_files(&[1, 2]));
627 out.push(first_pieces(&[1, 2]));
628 out.push(explore(true, Some("rust"), Some("cli"), PAGE));
629 out.push(Sql { text: LANGUAGE_FACETS.into(), params: vec![] });
630 out.push(Sql { text: TOPIC_FACETS.into(), params: vec![] });
631 println!("{}", serde_json::to_string(&out).unwrap());
632 }
633}