Skip to main content

opys_engine/store/
projection.rs

1//! The derived tables (`fields`, `sections`) and plan-guarded user SQL.
2//!
3//! `fields`/`sections` mirror the `[[stats]]` corpus-table shapes so users
4//! learn one vocabulary; they are rebuilt from scratch here (never written by
5//! commands), immediately before any user SQL runs. User SQL executes against
6//! the LIVE store, so it is statement-guarded on the plan: read SQL must be all
7//! `Query` (SELECT) and `--write` SQL all DML (INSERT/UPDATE/DELETE) — a mixed
8//! compound (`DELETE …; SELECT 1`, `UPDATE …; DROP TABLE docs`) is rejected
9//! before anything executes, not after.
10
11use futures::executor::block_on;
12use gluesql::core::ast::Statement;
13use gluesql::prelude::Payload;
14use serde_json::Value as Json;
15
16use crate::commands::stats::{
17    cell, json_scalar, section_json, structured_section_json, yaml_to_json,
18};
19use crate::error::Result;
20use crate::project_config::{ProjectConfig, SectionKind};
21
22use super::{IntoParam, Store};
23
24impl Store {
25    /// Rebuild the derived `fields`/`sections`/`blocks` tables from the current
26    /// authoritative state. `fields`/`sections` mirror the `[[stats]]` corpus
27    /// shapes; `blocks` decomposes each body into one row per `##` section
28    /// (+ preamble) for querying into bodies.
29    pub fn refresh_projections(&mut self, pcfg: &ProjectConfig) -> Result<()> {
30        self.exec("DELETE FROM fields", vec![])?;
31        self.exec("DELETE FROM sections", vec![])?;
32        self.exec("DELETE FROM blocks", vec![])?;
33
34        let docs = self.all_docs()?;
35        let mut field_rows = Vec::new();
36        let mut section_rows = Vec::new();
37        let mut block_rows = Vec::new();
38        for (_, d) in &docs {
39            let Some(id) = d.id().map(str::to_string) else {
40                continue;
41            };
42            // Body decomposition: one row per `##` section (+ any preamble), for
43            // querying into document bodies. Independent of the doc's type.
44            let secs: Vec<(String, String)> = crate::body::sections(&d.body)
45                .into_iter()
46                .filter_map(|(h, t)| {
47                    let t = t.trim().to_string();
48                    (!(h.is_empty() && t.is_empty())).then_some((h, t))
49                })
50                .collect();
51            for (seq, (heading, text)) in secs.into_iter().enumerate() {
52                block_rows.push(vec![
53                    id.clone().into_param(),
54                    (seq as i64).into_param(),
55                    heading.into_param(),
56                    text.into_param(),
57                ]);
58            }
59            let Some(tname) = pcfg.type_name_for_id(&id) else {
60                continue;
61            };
62            let t = &pcfg.types[tname];
63            for fname in t.fields.keys() {
64                if let Some(v) = d.frontmatter.get(fname) {
65                    match yaml_to_json(v) {
66                        Json::Array(items) => {
67                            for it in &items {
68                                field_rows.push(vec![
69                                    id.clone().into_param(),
70                                    fname.clone().into_param(),
71                                    json_scalar(it).into_param(),
72                                ]);
73                            }
74                        }
75                        other => field_rows.push(vec![
76                            id.clone().into_param(),
77                            fname.clone().into_param(),
78                            json_scalar(&other).into_param(),
79                        ]),
80                    }
81                }
82            }
83            for sec in &t.sections {
84                let s = if sec.kind == SectionKind::Structured {
85                    structured_section_json(d, sec.structure.as_deref(), &sec.heading)
86                } else {
87                    section_json(d, sec.kind, &sec.heading)
88                };
89                if let Some(s) = s {
90                    section_rows.push(vec![
91                        id.clone().into_param(),
92                        sec.heading.clone().into_param(),
93                        s["kind"].as_str().unwrap_or("").to_string().into_param(),
94                        s["items"].as_i64().unwrap_or(0).into_param(),
95                        s["unchecked"].as_i64().unwrap_or(0).into_param(),
96                    ]);
97                }
98            }
99        }
100        self.insert_batch("fields", 3, field_rows)?;
101        self.insert_batch("sections", 5, section_rows)?;
102        self.insert_batch("blocks", 4, block_rows)?;
103        Ok(())
104    }
105
106    /// Execute user-supplied SQL read-only against the live store. Every
107    /// statement must be a SELECT — checked on the *plan*, before anything
108    /// executes. Returns (labels, stringified rows); `Err` carries a
109    /// human-readable problem.
110    pub fn run_user_query(
111        &mut self,
112        sql: &str,
113        params: &[String],
114    ) -> std::result::Result<(Vec<String>, Vec<Vec<String>>), String> {
115        let bound: Vec<_> = params.iter().map(|p| p.clone().into_param()).collect();
116        let stmts = block_on(self.glue.plan_with_params(sql, bound))
117            .map_err(|e| format!("query failed ({e})"))?;
118        if stmts.is_empty() {
119            return Err("query produced no result set".to_string());
120        }
121        for s in &stmts {
122            if !matches!(s, Statement::Query(_)) {
123                return Err(format!("query must be a SELECT (got {})", stmt_kind(s)));
124            }
125        }
126        let mut last = None;
127        for s in &stmts {
128            last = Some(
129                block_on(self.glue.execute_stmt(s)).map_err(|e| format!("query failed ({e})"))?,
130            );
131        }
132        match last {
133            Some(Payload::Select { labels, rows }) => {
134                let rows = rows.iter().map(|r| r.iter().map(cell).collect()).collect();
135                Ok((labels, rows))
136            }
137            other => Err(format!("query produced no result set ({other:?})")),
138        }
139    }
140
141    /// Execute user-supplied write SQL (INSERT/UPDATE/DELETE) against the live
142    /// store, returning a one-line summary of the row counts. The caller is
143    /// responsible for gating on `verify` and flushing — this only mutates the
144    /// in-memory store. `Err` carries a human-readable problem.
145    ///
146    /// Every statement must be a DML mutation — checked on the *plan*, before
147    /// anything executes, so a compound like `INSERT …; DROP TABLE docs` is
148    /// rejected outright. This keeps schema-level statements (CREATE/DROP/ALTER
149    /// TABLE, indexes) off the authoritative tables the store and flush depend
150    /// on; the verify gate alone can't catch them (an empty or reshaped table
151    /// has no verify problems).
152    pub fn run_user_write(
153        &mut self,
154        sql: &str,
155        params: &[String],
156    ) -> std::result::Result<String, String> {
157        let bound: Vec<_> = params.iter().map(|p| p.clone().into_param()).collect();
158        let stmts = block_on(self.glue.plan_with_params(sql, bound))
159            .map_err(|e| format!("statement failed ({e})"))?;
160        if stmts.is_empty() {
161            return Err("no statements to run".to_string());
162        }
163        for s in &stmts {
164            if !matches!(
165                s,
166                Statement::Insert { .. } | Statement::Update { .. } | Statement::Delete { .. }
167            ) {
168                return Err(format!(
169                    "query --write allows only INSERT/UPDATE/DELETE (got {})",
170                    stmt_kind(s)
171                ));
172            }
173        }
174        let mut payloads = Vec::with_capacity(stmts.len());
175        for s in &stmts {
176            payloads.push(
177                block_on(self.glue.execute_stmt(s))
178                    .map_err(|e| format!("statement failed ({e})"))?,
179            );
180        }
181        let parts: Vec<String> = payloads
182            .iter()
183            .map(|p| match p {
184                Payload::Insert(n) => format!("{n} inserted"),
185                Payload::Update(n) => format!("{n} updated"),
186                Payload::Delete(n) => format!("{n} deleted"),
187                Payload::Select { rows, .. } => format!("{} selected", rows.len()),
188                _ => "ok".to_string(),
189            })
190            .collect();
191        Ok(parts.join(", "))
192    }
193}
194
195/// A human-readable name for a rejected statement kind.
196fn stmt_kind(s: &Statement) -> &'static str {
197    match s {
198        Statement::Insert { .. } => "INSERT",
199        Statement::Update { .. } => "UPDATE",
200        Statement::Delete { .. } => "DELETE",
201        Statement::CreateTable { .. } => "CREATE TABLE",
202        Statement::DropTable { .. } => "DROP TABLE",
203        Statement::AlterTable { .. } => "ALTER TABLE",
204        Statement::CreateIndex { .. } => "CREATE INDEX",
205        Statement::DropIndex { .. } => "DROP INDEX",
206        Statement::Query(_) => "SELECT",
207        _ => "an unsupported statement",
208    }
209}