1use std::io::Read as _;
13
14use crate::commands::verify;
15use crate::doc::Doc;
16use crate::error::{usage, Result};
17use crate::store::Store;
18use crate::Ctx;
19
20use super::stats;
21
22pub fn run(ctx: &Ctx, sql: &str, plain: bool, write: bool, stdin_value: bool) -> Result<()> {
23 let prj = ctx.open()?;
24 let sql_from_stdin = sql == "-";
25 let sql = if sql_from_stdin {
26 let mut buf = String::new();
27 std::io::stdin().read_to_string(&mut buf)?;
28 buf
29 } else {
30 sql.to_string()
31 };
32 if sql.trim().is_empty() {
33 return Err(usage("query: empty SQL"));
34 }
35 let params: Vec<String> = if stdin_value {
38 if sql_from_stdin {
39 return Err(usage(
40 "query: --stdin cannot combine with reading the SQL from stdin (-)",
41 ));
42 }
43 let mut buf = String::new();
44 std::io::stdin().read_to_string(&mut buf)?;
45 vec![buf.trim_end().to_string()]
46 } else {
47 Vec::new()
48 };
49
50 let (mut store, errors) = ctx.load(&prj)?;
52 for e in &errors {
53 eprintln!("warning: {e}");
54 }
55 store.refresh_projections(&prj.pcfg)?;
56
57 if write {
58 let baseline = store.baseline()?;
59 let before_blocks = block_texts(&mut store)?;
60 let before = verify_problems(&prj, &mut store, &errors)?;
66
67 let summary = store.run_user_write(&sql, ¶ms).map_err(usage)?;
68 apply_block_edits(&prj, &mut store, &before_blocks)?;
69 reconcile_model(&prj, &mut store, &baseline, !ctx.no_sync)?;
70
71 let after = verify_problems(&prj, &mut store, &errors)?;
72 let new: Vec<String> = after.difference(&before).cloned().collect();
73 if !new.is_empty() {
74 eprintln!("refusing to write — the edit would introduce verify problems:");
75 for p in &new {
76 eprintln!(" {p}");
77 }
78 return Err(usage(format!(
79 "{} new problem(s); no files changed",
80 new.len()
81 )));
82 }
83 ctx.flush(&prj, store)?;
84 println!("query: {summary} (verified, written)");
85 return Ok(());
86 }
87
88 let (labels, rows) = store.run_user_query(&sql, ¶ms).map_err(usage)?;
89 stats::print_markdown(&stats::table_body(&labels, &rows), plain);
90 Ok(())
91}
92
93fn docs_of(store: &mut Store) -> Result<Vec<Doc>> {
95 Ok(store.all_docs()?.into_iter().map(|(_, d)| d).collect())
96}
97
98fn materialize_inserts(prj: &crate::project::Project, store: &mut Store) -> Result<()> {
106 use crate::store::g_str;
107 let (_, rows) = store.select(
108 "SELECT id, type, status, title, body FROM docs WHERE dkey IS NULL",
109 vec![],
110 )?;
111 if rows.is_empty() {
112 return Ok(());
113 }
114 let pcfg = &prj.pcfg;
115
116 struct Raw {
119 id: Option<String>,
120 type_name: String,
121 status: String,
122 title: String,
123 body: String,
124 }
125 let mut raws = Vec::with_capacity(rows.len());
126 for r in &rows {
127 let type_name = g_str(&r[1]).unwrap_or_default();
128 let t = pcfg.types.get(&type_name).ok_or_else(|| {
129 usage(format!(
130 "query: INSERT into docs needs a known type (got {type_name:?})"
131 ))
132 })?;
133 let status = {
134 let s = g_str(&r[2]).unwrap_or_default();
135 if s.is_empty() {
136 t.default_status.clone()
137 } else {
138 s
139 }
140 };
141 if !t.statuses.contains(&status) {
142 return Err(usage(format!(
143 "query: unknown status {status:?} for type {type_name:?}"
144 )));
145 }
146 let title = g_str(&r[3]).unwrap_or_default();
147 if title.trim().is_empty() {
148 return Err(usage(
149 "query: INSERT into docs needs a non-empty title".to_string(),
150 ));
151 }
152 raws.push(Raw {
153 id: g_str(&r[0]).filter(|s| !s.is_empty()),
154 type_name,
155 status,
156 title,
157 body: g_str(&r[4]).unwrap_or_default(),
158 });
159 }
160
161 store.exec("DELETE FROM docs WHERE dkey IS NULL", vec![])?;
164 for raw in raws {
165 let t = &pcfg.types[&raw.type_name];
166 let id = match raw.id {
167 Some(id) => id,
168 None => store.next_id_for(&t.prefix, pcfg.pad)?,
169 };
170 let mut fm = crate::frontmatter::Frontmatter::new();
171 fm.set_str("id", &id);
172 fm.set_str("status", &raw.status);
173 crate::commands::touch(&mut fm);
174 let body = if raw.body.trim().is_empty() {
175 crate::commands::new::scaffold_body(&raw.title, t)
176 } else {
177 raw.body
178 };
179 let doc = Doc {
180 path: prj.doc_path(&id, &raw.status),
181 frontmatter: fm,
182 body,
183 title: raw.title,
184 };
185 store.put_doc(pcfg, None, &doc)?;
186 }
187 Ok(())
188}
189
190fn reconcile_model(
198 prj: &crate::project::Project,
199 store: &mut Store,
200 baseline: &crate::store::Baseline,
201 run_sync: bool,
202) -> Result<()> {
203 store.cascade_removals(&prj.pcfg, baseline)?;
205 materialize_inserts(prj, store)?;
207 if run_sync {
208 crate::commands::sync::pass(prj, store)?;
209 }
210 Ok(())
211}
212
213fn verify_problems(
219 prj: &crate::project::Project,
220 store: &mut Store,
221 errors: &[String],
222) -> Result<std::collections::HashSet<String>> {
223 Ok(
224 verify::collect_problems(prj, &docs_of(store)?, errors.to_vec())
225 .into_iter()
226 .collect(),
227 )
228}
229
230fn block_texts(store: &mut Store) -> Result<std::collections::HashMap<(String, i64), String>> {
234 let (_, rows) = store.select("SELECT doc_id, seq, text FROM blocks", vec![])?;
235 let mut map = std::collections::HashMap::new();
236 for r in &rows {
237 if let (Some(id), Some(seq)) = (crate::store::g_str(&r[0]), crate::store::g_i64(&r[1])) {
238 map.insert((id, seq), crate::store::g_str(&r[2]).unwrap_or_default());
239 }
240 }
241 Ok(map)
242}
243
244fn apply_block_edits(
250 prj: &crate::project::Project,
251 store: &mut Store,
252 before: &std::collections::HashMap<(String, i64), String>,
253) -> Result<()> {
254 let (_, rows) = store.select("SELECT doc_id, seq, text FROM blocks", vec![])?;
255 let mut by_doc: std::collections::BTreeMap<String, Vec<(usize, String)>> =
256 std::collections::BTreeMap::new();
257 for r in &rows {
258 let (Some(id), Some(seq)) = (crate::store::g_str(&r[0]), crate::store::g_i64(&r[1])) else {
259 continue;
260 };
261 let text = crate::store::g_str(&r[2]).unwrap_or_default();
262 if before.get(&(id.clone(), seq)).is_some_and(|t| *t != text) {
263 by_doc.entry(id).or_default().push((seq as usize, text));
264 }
265 }
266 if by_doc.is_empty() {
267 return Ok(());
268 }
269 for (doc_id, edits) in by_doc {
270 let Some(dkey) = store.dkey_opt(&doc_id)? else {
271 continue;
272 };
273 let mut doc = store.doc(dkey)?;
274 let spans = crate::body::section_spans(&doc.body);
275 doc.body = crate::body::apply_section_edits(&doc.body, &spans, &edits);
276 doc.title = crate::body::title(&doc.body);
277 super::touch(&mut doc.frontmatter);
278 store.put_doc(&prj.pcfg, Some(dkey), &doc)?;
279 }
280 Ok(())
281}