use std::io::Read as _;
use crate::commands::verify;
use crate::doc::Doc;
use crate::error::{usage, Result};
use crate::store::Store;
use crate::Ctx;
use super::stats;
pub fn run(ctx: &Ctx, sql: &str, plain: bool, write: bool, stdin_value: bool) -> Result<()> {
let prj = ctx.open()?;
let sql_from_stdin = sql == "-";
let sql = if sql_from_stdin {
let mut buf = String::new();
std::io::stdin().read_to_string(&mut buf)?;
buf
} else {
sql.to_string()
};
if sql.trim().is_empty() {
return Err(usage("query: empty SQL"));
}
let params: Vec<String> = if stdin_value {
if sql_from_stdin {
return Err(usage(
"query: --stdin cannot combine with reading the SQL from stdin (-)",
));
}
let mut buf = String::new();
std::io::stdin().read_to_string(&mut buf)?;
vec![buf.trim_end().to_string()]
} else {
Vec::new()
};
let (mut store, errors) = ctx.load(&prj)?;
for e in &errors {
eprintln!("warning: {e}");
}
store.refresh_projections(&prj.pcfg)?;
if write {
let baseline = store.baseline()?;
let before_blocks = block_texts(&mut store)?;
let before = verify_problems(&prj, &mut store, &errors)?;
let summary = store.run_user_write(&sql, ¶ms).map_err(usage)?;
apply_block_edits(&prj, &mut store, &before_blocks)?;
reconcile_model(&prj, &mut store, &baseline, !ctx.no_sync)?;
let after = verify_problems(&prj, &mut store, &errors)?;
let new: Vec<String> = after.difference(&before).cloned().collect();
if !new.is_empty() {
eprintln!("refusing to write — the edit would introduce verify problems:");
for p in &new {
eprintln!(" {p}");
}
return Err(usage(format!(
"{} new problem(s); no files changed",
new.len()
)));
}
ctx.flush(&prj, store)?;
println!("query: {summary} (verified, written)");
return Ok(());
}
let (labels, rows) = store.run_user_query(&sql, ¶ms).map_err(usage)?;
stats::print_markdown(&stats::table_body(&labels, &rows), plain);
Ok(())
}
fn docs_of(store: &mut Store) -> Result<Vec<Doc>> {
Ok(store.all_docs()?.into_iter().map(|(_, d)| d).collect())
}
fn materialize_inserts(prj: &crate::project::Project, store: &mut Store) -> Result<()> {
use crate::store::g_str;
let (_, rows) = store.select(
"SELECT id, type, status, title, body FROM docs WHERE dkey IS NULL",
vec![],
)?;
if rows.is_empty() {
return Ok(());
}
let pcfg = &prj.pcfg;
struct Raw {
id: Option<String>,
type_name: String,
status: String,
title: String,
body: String,
}
let mut raws = Vec::with_capacity(rows.len());
for r in &rows {
let type_name = g_str(&r[1]).unwrap_or_default();
let t = pcfg.types.get(&type_name).ok_or_else(|| {
usage(format!(
"query: INSERT into docs needs a known type (got {type_name:?})"
))
})?;
let status = {
let s = g_str(&r[2]).unwrap_or_default();
if s.is_empty() {
t.default_status.clone()
} else {
s
}
};
if !t.statuses.contains(&status) {
return Err(usage(format!(
"query: unknown status {status:?} for type {type_name:?}"
)));
}
let title = g_str(&r[3]).unwrap_or_default();
if title.trim().is_empty() {
return Err(usage(
"query: INSERT into docs needs a non-empty title".to_string(),
));
}
raws.push(Raw {
id: g_str(&r[0]).filter(|s| !s.is_empty()),
type_name,
status,
title,
body: g_str(&r[4]).unwrap_or_default(),
});
}
store.exec("DELETE FROM docs WHERE dkey IS NULL", vec![])?;
for raw in raws {
let t = &pcfg.types[&raw.type_name];
let id = match raw.id {
Some(id) => id,
None => store.next_id_for(&t.prefix, pcfg.pad)?,
};
let mut fm = crate::frontmatter::Frontmatter::new();
fm.set_str("id", &id);
fm.set_str("status", &raw.status);
crate::commands::touch(&mut fm);
let body = if raw.body.trim().is_empty() {
crate::commands::new::scaffold_body(&raw.title, t)
} else {
raw.body
};
let doc = Doc {
path: prj.doc_path(&id, &raw.status),
frontmatter: fm,
body,
title: raw.title,
};
store.put_doc(pcfg, None, &doc)?;
}
Ok(())
}
fn reconcile_model(
prj: &crate::project::Project,
store: &mut Store,
baseline: &crate::store::Baseline,
run_sync: bool,
) -> Result<()> {
store.cascade_removals(&prj.pcfg, baseline)?;
materialize_inserts(prj, store)?;
if run_sync {
crate::commands::sync::pass(prj, store)?;
}
Ok(())
}
fn verify_problems(
prj: &crate::project::Project,
store: &mut Store,
errors: &[String],
) -> Result<std::collections::HashSet<String>> {
Ok(
verify::collect_problems(prj, &docs_of(store)?, errors.to_vec())
.into_iter()
.collect(),
)
}
fn block_texts(store: &mut Store) -> Result<std::collections::HashMap<(String, i64), String>> {
let (_, rows) = store.select("SELECT doc_id, seq, text FROM blocks", vec![])?;
let mut map = std::collections::HashMap::new();
for r in &rows {
if let (Some(id), Some(seq)) = (crate::store::g_str(&r[0]), crate::store::g_i64(&r[1])) {
map.insert((id, seq), crate::store::g_str(&r[2]).unwrap_or_default());
}
}
Ok(map)
}
fn apply_block_edits(
prj: &crate::project::Project,
store: &mut Store,
before: &std::collections::HashMap<(String, i64), String>,
) -> Result<()> {
let (_, rows) = store.select("SELECT doc_id, seq, text FROM blocks", vec![])?;
let mut by_doc: std::collections::BTreeMap<String, Vec<(usize, String)>> =
std::collections::BTreeMap::new();
for r in &rows {
let (Some(id), Some(seq)) = (crate::store::g_str(&r[0]), crate::store::g_i64(&r[1])) else {
continue;
};
let text = crate::store::g_str(&r[2]).unwrap_or_default();
if before.get(&(id.clone(), seq)).is_some_and(|t| *t != text) {
by_doc.entry(id).or_default().push((seq as usize, text));
}
}
if by_doc.is_empty() {
return Ok(());
}
for (doc_id, edits) in by_doc {
let Some(dkey) = store.dkey_opt(&doc_id)? else {
continue;
};
let mut doc = store.doc(dkey)?;
let spans = crate::body::section_spans(&doc.body);
doc.body = crate::body::apply_section_edits(&doc.body, &spans, &edits);
doc.title = crate::body::title(&doc.body);
super::touch(&mut doc.frontmatter);
store.put_doc(&prj.pcfg, Some(dkey), &doc)?;
}
Ok(())
}