use std::rc::Rc;
use oqx::semantics::equals;
use oqx::{DataContext, RowIndex, Value};
use rusqlite::types::Value as SqlValue;
use crate::context::{StoreContext, Target};
use crate::paths::storage_path;
use crate::planner::{columns, from_by_doc, from_clause, guards, guards_by_doc, order_clause};
use crate::translate::{RESERVED_DOC_BASENAMES, non_property_handles};
#[derive(Clone, Debug)]
enum Probe {
Column(&'static str),
Property {
key: String,
source: Option<&'static str>,
},
}
fn column_probe(t: Target, key: &str) -> Option<&'static str> {
Some(match (t, key) {
(Target::Docs, "$id") => "d.doc_id = ?",
(Target::Docs, "$path") => "d.path = ?",
(Target::Blocks, "$id") => "b.block_id = ?",
(Target::Blocks, "$doc") => "b.doc_id = ?",
(Target::Blocks, "type") => "b.type = ?",
(Target::Blocks, "$path") => "d.path = ?",
(Target::Nodes, "$id" | "$node_id") => "n.node_id = ?",
(Target::Nodes, "$doc_id") => "n.doc_id = ?",
(Target::Nodes, "kind") => "n.kind = ?",
(Target::Nodes, "name") => "n.name = ?",
(Target::Nodes, "$path") => "d.path = ?",
(Target::Edges, "$id") => "e.edge_id = ?",
(Target::Edges, "$src") => "e.src_doc = ?",
(Target::Edges, "$dst") => "e.dst_node = ?",
(Target::Edges, "$path") => "d.path = ?",
(Target::Edges, "$dst_path") => {
"e.dst_node IN (SELECT doc_id FROM docs WHERE repo_id = ?r AND path = ?)"
}
_ => return None,
})
}
fn probe_for(t: Target, path: &[String]) -> Option<Probe> {
let [key] = path else { return None };
if let Some(sql) = column_probe(t, key) {
return Some(Probe::Column(sql));
}
if t != Target::Docs {
return None;
}
if key == "$title" {
return Some(Probe::Property {
key: key.clone(),
source: Some("computed"),
});
}
if key.starts_with('$') || key == "format" {
return None;
}
if non_property_handles(Target::Docs).contains(&key.as_str())
|| RESERVED_DOC_BASENAMES.contains(&key.as_str())
{
return None;
}
Some(Probe::Property {
key: key.clone(),
source: None,
})
}
#[must_use]
pub fn indexable_paths(t: Target) -> &'static [&'static str] {
match t {
Target::Docs => &["$id", "$path", "$title", "<property key>"],
Target::Blocks => &["$id", "$doc", "type", "$path"],
Target::Nodes => &["$id", "$node_id", "$doc_id", "kind", "name", "$path"],
Target::Edges => &["$id", "$src", "$dst", "$path", "$dst_path"],
}
}
pub(crate) fn index_for<'c, 'a>(
ctx: &'c StoreContext<'a>,
target: Target,
path: &[String],
) -> Option<Rc<dyn RowIndex + 'c>> {
let probe = probe_for(target, path)?;
Some(Rc::new(StoreIndex {
ctx,
target,
path: path[0].clone(),
probe,
}))
}
struct StoreIndex<'c, 'a> {
ctx: &'c StoreContext<'a>,
target: Target,
path: String,
probe: Probe,
}
impl StoreIndex<'_, '_> {
fn rows(&self, value: &Value) -> oqx::Result<Vec<Value>> {
if value.is_absent() {
return self.fallback(value);
}
match &self.probe {
Probe::Column(sql) if self.path == "$path" || self.path == "$dst_path" => match value {
Value::Str(s) if s.starts_with('/') => self.column(sql, storage_path(s)),
_ => Ok(Vec::new()),
},
Probe::Column(sql) => match value {
Value::Str(s) => self.column(sql, s),
_ => Ok(Vec::new()),
},
Probe::Property { key, source } => match value {
Value::Str(s) => self.property(
key,
*source,
"p.type = 'string' AND p.val_text = ?",
SqlValue::Text(s.clone()),
),
Value::Number(n) if n.is_nan() => Ok(Vec::new()),
Value::Number(n) => self.property(
key,
*source,
"p.type = 'number' AND p.val_num = ?",
SqlValue::Real(*n),
),
Value::Bool(b) => self.property(
key,
*source,
"p.type = 'bool' AND p.val_bool = ?",
SqlValue::Integer(i64::from(*b)),
),
_ => self.fallback(value),
},
}
}
fn column(&self, probe: &str, value: &str) -> oqx::Result<Vec<Value>> {
let t = self.target;
let repo = self.ctx.repo_id();
let by_doc = t != Target::Docs && self.path == "$path";
let (from, guard, mut params) = if by_doc {
(
from_by_doc(t),
guards_by_doc(t),
vec![
SqlValue::Text(repo.to_owned()),
SqlValue::Text(repo.to_owned()),
],
)
} else {
(
from_clause(t),
guards(t),
vec![SqlValue::Text(repo.to_owned())],
)
};
if probe.contains("?r") {
params.push(SqlValue::Text(repo.to_owned()));
}
params.push(SqlValue::Text(value.to_owned()));
let sql = format!(
"SELECT {} FROM {} WHERE {} AND {} ORDER BY {}",
columns(t),
from,
guard,
probe.replace("?r", "?"),
order_clause(t)
);
self.ctx.probe_rows(t, &sql, ¶ms)
}
fn property(
&self,
key: &str,
source: Option<&str>,
typed: &str,
bound: SqlValue,
) -> oqx::Result<Vec<Value>> {
let repo = self.ctx.repo_id();
let src = source.map_or("", |_| " AND p.source = ?");
let src2 = source.map_or("", |_| " AND p2.source = ?");
let sql = format!(
"SELECT d.* FROM properties p CROSS JOIN docs d ON d.doc_id = p.doc_id
WHERE p.repo_id = ? AND p.key = ? AND {typed} AND p.card = 'scalar' AND p.deleted_commit IS NULL{src}
AND {} AND (SELECT COUNT(*) FROM properties p2 WHERE p2.doc_id = d.doc_id AND p2.key = ? AND p2.deleted_commit IS NULL{src2}) = 1
ORDER BY {}",
guards(Target::Docs),
order_clause(Target::Docs)
);
let mut params = vec![
SqlValue::Text(repo.to_owned()),
SqlValue::Text(key.to_owned()),
bound,
];
if let Some(s) = source {
params.push(SqlValue::Text(s.to_owned()));
}
params.push(SqlValue::Text(repo.to_owned()));
params.push(SqlValue::Text(key.to_owned()));
if let Some(s) = source {
params.push(SqlValue::Text(s.to_owned()));
}
self.ctx.probe_rows(Target::Docs, &sql, ¶ms)
}
fn fallback(&self, value: &Value) -> oqx::Result<Vec<Value>> {
let rows = self.ctx.scan_rows(self.target)?;
let mut out = Vec::new();
for r in rows.iter() {
if equals(&self.ctx.get(r, &self.path)?, value) {
out.push(r.clone());
}
}
Ok(out)
}
}
impl RowIndex for StoreIndex<'_, '_> {
fn lookup(&self, value: &Value) -> Vec<usize> {
let Ok(all) = self.ctx.scan_rows(self.target) else {
return Vec::new();
};
let Ok(hits) = self.rows(value) else {
return Vec::new();
};
let ids: Vec<Value> = hits.iter().map(|r| self.ctx.identity(r)).collect();
all.iter()
.enumerate()
.filter(|(_, r)| ids.contains(&self.ctx.identity(r)))
.map(|(i, _)| i)
.collect()
}
fn lookup_rows(&self, value: &Value) -> Option<oqx::Result<Vec<Value>>> {
Some(self.rows(value))
}
}