use oqx::Value;
use oqx::ast::{BinaryOp, Expr, LogicalOp};
use rusqlite::types::Value as SqlValue;
use crate::context::{Target, to_sql};
#[derive(Clone, Copy, Debug)]
pub struct TranslateCtx<'a> {
pub target: Target,
pub self_alias: &'a str,
pub doc_alias: &'a str,
pub params: &'a [Value],
}
#[derive(Clone, Debug, PartialEq)]
pub struct Frag {
pub sql: String,
pub params: Vec<SqlValue>,
}
impl Frag {
fn bare(sql: impl Into<String>) -> Self {
Self {
sql: sql.into(),
params: Vec::new(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Ty {
Text,
Int,
Num,
Bool,
Json,
Prop,
Null,
}
#[must_use]
pub fn comparable(op: BinaryOp, a: Ty, b: Ty) -> bool {
use Ty::{Bool, Int, Json, Null, Num, Prop, Text};
let numeric = |t: Ty| matches!(t, Int | Num);
let read = |t: Ty| matches!(t, Json | Prop);
let both_numeric = numeric(a) && numeric(b);
let typed = |konst: fn(Ty) -> bool| (read(a) && konst(b)) || (read(b) && konst(a));
if matches!(op, BinaryOp::Eq | BinaryOp::Ne) {
a == Text
|| b == Text
|| (a == Null && b != Prop)
|| (b == Null && a != Prop)
|| both_numeric
|| typed(|t| matches!(t, Bool | Num))
} else {
(a == Text && b == Text) || both_numeric || typed(|t| t == Num)
}
}
fn const_ty(v: &Value) -> Option<Ty> {
Some(match v {
Value::Str(_) => Ty::Text,
Value::Number(_) => Ty::Num,
Value::Bool(_) => Ty::Bool,
Value::Null | Value::Undefined => Ty::Null,
Value::Array(_) | Value::Object(_) | Value::Range(_) => return None,
})
}
#[must_use]
pub fn non_property_handles(t: Target) -> &'static [&'static str] {
match t {
Target::Docs => &[
"doc",
"blocks",
"nodes",
"out",
"in",
"out_edges",
"in_edges",
"frontmatter",
"inline",
],
Target::Blocks => &[
"block",
"doc",
"children",
"nodes",
"out_edges",
"section",
"attrs",
],
Target::Nodes => &[
"section",
"doc",
"block",
"blocks",
"subsections",
"children",
"attrs",
],
Target::Edges => &["doc"],
}
}
fn intrinsic_sql(name: &str, ctx: &TranslateCtx<'_>) -> Option<(String, Ty)> {
let (s, d) = (ctx.self_alias, ctx.doc_alias);
let sql = match (ctx.target, name) {
(Target::Docs, "$id") => format!("{s}.doc_id"),
(Target::Docs, "$path") => format!("{d}.path"),
(Target::Docs, "$content_hash") => format!("lower(hex({s}.file_hash))"),
(Target::Docs, "$updated_at") => format!(
"(SELECT c.ts FROM revisions r JOIN commits c ON c.commit_id = r.commit_id WHERE r.rev_id = {s}.current_rev)"
),
(Target::Blocks, "$id") => format!("{s}.block_id"),
(Target::Blocks, "$doc") => format!("{s}.doc_id"),
(Target::Blocks, "$path") => format!("{d}.path"),
(Target::Blocks, "$ordinal") => return Some((format!("{s}.ordinal"), Ty::Int)),
(Target::Blocks, "$depth") => return Some((format!("{s}.depth"), Ty::Int)),
(Target::Blocks, "$body") => format!("{s}.text"),
(Target::Blocks, "$content_hash") => format!("lower(hex({s}.raw_hash))"),
(Target::Nodes, "$id" | "$node_id") => format!("{s}.node_id"),
(Target::Nodes, "$doc_id") => format!("{s}.doc_id"),
(Target::Nodes, "$block_id") => format!("{s}.block_id"),
(Target::Nodes, "$path") => format!("{d}.path"),
(Target::Edges, "$id") => format!("{s}.edge_id"),
(Target::Edges, "$src") => format!("{s}.src_doc"),
(Target::Edges, "$dst") => format!("{s}.dst_node"),
(Target::Edges, "$src_block") => format!("{s}.src_block"),
(Target::Edges, "$via") => format!("{s}.via_node"),
(Target::Edges, "$from_commit") => format!("{s}.from_commit"),
(Target::Edges, "$path") => format!("{d}.path"),
(Target::Edges, "$dst_path") => {
format!("(SELECT dd.path FROM docs dd WHERE dd.doc_id = {s}.dst_node)")
}
(Target::Edges, "$dst_uri") => {
format!("(SELECT xn.uri FROM external_nodes xn WHERE xn.node_id = {s}.dst_node)")
}
_ => return None,
};
Some((sql, Ty::Text))
}
pub(crate) const RESERVED_DOC_BASENAMES: [&str; 5] =
["id", "path", "updated_at", "content_hash", "body"];
fn is_seg(s: &str) -> bool {
let mut chars = s.chars();
chars
.next()
.is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
&& chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
}
fn prop_row(doc_alias: &str, key: &str, select: &str) -> Option<String> {
if !is_seg(key) {
return None;
}
Some(format!(
"(SELECT {select} FROM properties p \
WHERE p.doc_id = {doc_alias}.doc_id AND p.key = '{key}' AND p.card = 'scalar' AND p.deleted_commit IS NULL \
AND (SELECT COUNT(*) FROM properties p2 WHERE p2.doc_id = {doc_alias}.doc_id AND p2.key = '{key}' AND p2.deleted_commit IS NULL) = 1 \
LIMIT 1)"
))
}
fn prop_scalar(doc_alias: &str, key: &str) -> Option<String> {
prop_row(
doc_alias,
key,
"COALESCE(p.val_text, p.val_num, p.val_bool)",
)
}
fn json_path(segs: &[&str]) -> Option<String> {
if segs.iter().any(|s| !is_seg(s)) {
return None;
}
Some(format!("$.{}", segs.join(".")))
}
#[derive(Clone, Debug, PartialEq)]
enum Shape {
Plain,
Const(Value),
Json { col: String, path: String },
Prop { doc_alias: String, key: String },
}
#[derive(Clone, Debug, PartialEq)]
struct Operand {
frag: Frag,
ty: Ty,
shape: Shape,
}
impl Operand {
fn plain(sql: String, ty: Ty) -> Self {
Self {
frag: Frag::bare(sql),
ty,
shape: Shape::Plain,
}
}
fn text(sql: String) -> Option<Self> {
Some(Self::plain(sql, Ty::Text))
}
fn constant(v: &Value) -> Option<Self> {
Some(Self {
frag: Frag {
sql: "?".to_owned(),
params: vec![to_sql(v)],
},
ty: const_ty(v)?,
shape: Shape::Const(v.clone()),
})
}
fn json(col: String, segs: &[&str]) -> Option<Self> {
let path = json_path(segs)?;
Some(Self {
frag: Frag::bare(format!("json_extract({col}, '{path}')")),
ty: Ty::Json,
shape: Shape::Json { col, path },
})
}
fn prop(doc_alias: &str, key: &str) -> Option<Self> {
Some(Self {
frag: Frag::bare(prop_scalar(doc_alias, key)?),
ty: Ty::Prop,
shape: Shape::Prop {
doc_alias: doc_alias.to_owned(),
key: key.to_owned(),
},
})
}
}
fn member_segments(e: &Expr) -> Option<Vec<&str>> {
match e {
Expr::Ident { name } => Some(vec![name.as_str()]),
Expr::Member { recv, name } => {
let mut base = member_segments(recv)?;
base.push(name.as_str());
Some(base)
}
_ => None,
}
}
pub fn translate_value(e: &Expr, ctx: &TranslateCtx<'_>) -> Option<Frag> {
typed_value(e, ctx).map(|(frag, _)| frag)
}
pub fn typed_value(e: &Expr, ctx: &TranslateCtx<'_>) -> Option<(Frag, Ty)> {
operand(e, ctx).map(|o| (o.frag, o.ty))
}
fn operand(e: &Expr, ctx: &TranslateCtx<'_>) -> Option<Operand> {
let (s, d, target) = (ctx.self_alias, ctx.doc_alias, ctx.target);
match e {
Expr::Lit(v) => Operand::constant(v),
Expr::Binding { index } => {
Operand::constant(ctx.params.get(*index).unwrap_or(&Value::Undefined))
}
Expr::Ident { name } => {
if name.starts_with('$') {
return intrinsic_sql(name, ctx).map(|(sql, ty)| Operand::plain(sql, ty));
}
let name = name.as_str();
if non_property_handles(target).contains(&name) {
return None;
}
match target {
Target::Docs => {
if name == "format" {
return Operand::text(format!("{s}.format"));
}
if RESERVED_DOC_BASENAMES.contains(&name) {
return None;
}
Operand::prop(d, name)
}
Target::Blocks => {
if name == "type" || name == "text" {
return Operand::text(format!("{s}.{name}"));
}
Operand::json(format!("{s}.attrs"), &[name])
}
Target::Nodes => {
if matches!(name, "kind" | "name" | "value") {
return Operand::text(format!("{s}.{name}"));
}
Operand::json(format!("{s}.attrs"), &[name])
}
Target::Edges => {
if matches!(
name,
"predicate" | "provenance" | "dst_kind" | "anchor" | "src_field"
) {
return Operand::text(format!("{s}.{name}"));
}
None
}
}
}
Expr::Member { .. } => {
let segs = member_segments(e)?;
let (head, rest) = segs.split_first()?;
if rest.is_empty() {
return None;
}
if *head == "attrs" && matches!(target, Target::Blocks | Target::Nodes) {
return Operand::json(format!("{s}.attrs"), rest);
}
if *head == "doc" {
if rest.len() != 1 {
return None;
}
let k = rest[0];
if k == "$path" {
return Operand::text(format!("{d}.path"));
}
if k == "format" {
return Operand::text(format!("{d}.format"));
}
if k.starts_with('$')
|| RESERVED_DOC_BASENAMES.contains(&k)
|| non_property_handles(Target::Docs).contains(&k)
{
return None;
}
return Operand::prop(d, k);
}
if *head == "block"
&& target == Target::Nodes
&& rest.len() == 1
&& matches!(rest[0], "type" | "text")
{
return Operand::text(format!(
"(SELECT bb.{} FROM blocks bb WHERE bb.block_id = {s}.block_id)",
rest[0]
));
}
None
}
Expr::Call {
recv: Some(recv),
name,
args,
} if args.is_empty() && (name == "lower" || name == "upper") => {
let recv = translate_value(recv, ctx)?;
Some(Operand {
frag: Frag {
sql: format!("{name}({})", recv.sql),
params: recv.params,
},
ty: Ty::Text,
shape: Shape::Plain,
})
}
_ => None,
}
}
fn is_op(op: BinaryOp) -> Option<&'static str> {
Some(match op {
BinaryOp::Eq => "IS",
BinaryOp::Ne => "IS NOT",
BinaryOp::Lt => "<",
BinaryOp::Le => "<=",
BinaryOp::Gt => ">",
BinaryOp::Ge => ">=",
BinaryOp::Add | BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Div | BinaryOp::Mod => {
return None;
}
})
}
fn typed_compare(op: BinaryOp, sql_op: &str, l: &Operand, r: &Operand) -> Option<Frag> {
let (read, konst, read_left) = match (&l.shape, &r.shape) {
(Shape::Json { .. } | Shape::Prop { .. }, Shape::Const(v)) => (&l.shape, v, true),
(Shape::Const(v), Shape::Json { .. } | Shape::Prop { .. }) => (&r.shape, v, false),
_ => return None,
};
let equality = matches!(op, BinaryOp::Eq | BinaryOp::Ne);
let wrap = if op == BinaryOp::Ne {
"IS NOT 1"
} else {
"IS 1"
};
let inner_op = match (equality, read_left, sql_op) {
(true, _, _) => "=",
(false, true, _) => sql_op,
(false, false, "<") => ">",
(false, false, "<=") => ">=",
(false, false, ">") => "<",
(false, false, ">=") => "<=",
(false, false, _) => return None,
};
let sides = |read_sql: &str| format!("{read_sql} {inner_op} ?");
let (sql, params) = match (read, konst) {
(_, Value::Bool(_)) if !equality => return None,
(Shape::Json { col, path }, Value::Bool(b)) => (
format!("(json_type({col}, '{path}') = '{b}') {wrap}"),
Vec::new(),
),
(Shape::Json { col, path }, Value::Number(_)) => (
format!(
"(json_type({col}, '{path}') IN ('integer', 'real') AND {}) {wrap}",
sides(&format!("json_extract({col}, '{path}')"))
),
vec![to_sql(konst)],
),
(Shape::Prop { doc_alias, key }, Value::Bool(_)) => (
format!(
"{} {wrap}",
prop_row(
doc_alias,
key,
&format!("p.type = 'bool' AND {}", sides("p.val_bool"))
)?
),
vec![to_sql(konst)],
),
(Shape::Prop { doc_alias, key }, Value::Number(_)) => (
format!(
"{} {wrap}",
prop_row(
doc_alias,
key,
&format!("p.type = 'number' AND {}", sides("p.val_num"))
)?
),
vec![to_sql(konst)],
),
_ => return None,
};
Some(Frag {
sql: format!("({sql})"),
params,
})
}
pub fn translate_predicate(e: &Expr, ctx: &TranslateCtx<'_>) -> Option<Frag> {
match e {
Expr::Logical {
op: LogicalOp::And,
left,
right,
} => join2(
translate_predicate(left, ctx),
translate_predicate(right, ctx),
"AND",
),
Expr::Binary { op, left, right } => {
let sql_op = is_op(*op)?;
let l = operand(left, ctx)?;
let r = operand(right, ctx)?;
if !comparable(*op, l.ty, r.ty) {
return None;
}
if let Some(typed) = typed_compare(*op, sql_op, &l, &r) {
return Some(typed);
}
let op = sql_op;
let mut params = l.frag.params;
params.extend(r.frag.params);
Some(Frag {
sql: format!("({} {op} {})", l.frag.sql, r.frag.sql),
params,
})
}
Expr::Call {
recv: Some(recv),
name,
args,
} if args.len() == 1 => {
let recv = translate_value(recv, ctx)?;
let arg = translate_value(&args[0], ctx)?;
let mut params = recv.params;
let sql = match name.as_str() {
"startsWith" => {
params.extend(arg.params.iter().cloned());
params.extend(arg.params);
format!("(substr({}, 1, length({a})) = {a})", recv.sql, a = arg.sql)
}
"endsWith" => {
params.extend(arg.params.iter().cloned());
params.extend(arg.params);
format!("(substr({}, -length({a})) = {a})", recv.sql, a = arg.sql)
}
"contains" => {
params.extend(arg.params);
format!("(instr({}, {}) > 0)", recv.sql, arg.sql)
}
_ => return None,
};
Some(Frag { sql, params })
}
_ => None,
}
}
fn join2(a: Option<Frag>, b: Option<Frag>, connective: &str) -> Option<Frag> {
let (a, b) = (a?, b?);
let mut params = a.params;
params.extend(b.params);
Some(Frag {
sql: format!("({} {connective} {})", a.sql, b.sql),
params,
})
}
#[cfg(test)]
mod tests {
use super::*;
use oqx::ast::Where;
const DOCS: TranslateCtx<'static> = TranslateCtx {
target: Target::Docs,
self_alias: "d",
doc_alias: "d",
params: &[],
};
fn text(s: &str) -> SqlValue {
SqlValue::Text(s.to_owned())
}
fn frag(sql: &str, params: &[SqlValue]) -> Option<Frag> {
Some(Frag {
sql: sql.to_owned(),
params: params.to_vec(),
})
}
fn pred(src: &str) -> Expr {
let q = oqx::parse_string(&format!("from docs where {src}")).expect("parses");
match q.r#where {
Some(Where::Scalar { expr }) => expr,
other => panic!("expected a single scalar predicate, got {other:?}"),
}
}
fn ident(name: &str) -> Box<Expr> {
Box::new(Expr::Ident {
name: name.to_owned(),
})
}
fn lit(s: &str) -> Box<Expr> {
Box::new(Expr::Lit(Value::from(s)))
}
fn eq(l: Box<Expr>, r: Box<Expr>) -> Box<Expr> {
Box::new(Expr::Binary {
op: BinaryOp::Eq,
left: l,
right: r,
})
}
#[test]
fn equality_is_null_safe_is() {
assert_eq!(
translate_predicate(&pred("$path == \"index.md\""), &DOCS),
frag("(d.path IS ?)", &[text("index.md")])
);
}
#[test]
fn inequality_is_null_safe_is_not() {
assert_eq!(
translate_predicate(&pred("$path != \"x\""), &DOCS),
frag("(d.path IS NOT ?)", &[text("x")])
);
}
#[test]
fn intrinsic_column_mapping() {
assert_eq!(
translate_predicate(&pred("$id == \"d_1\""), &DOCS),
frag("(d.doc_id IS ?)", &[text("d_1")])
);
}
#[test]
fn relational_ops_are_plain_comparisons() {
assert_eq!(
translate_predicate(&pred("$path < \"m\""), &DOCS),
frag("(d.path < ?)", &[text("m")])
);
assert_eq!(
translate_predicate(&pred("$path >= \"m\""), &DOCS),
frag("(d.path >= ?)", &[text("m")])
);
assert_eq!(translate_predicate(&pred("$path + 1"), &DOCS), None);
}
#[test]
fn starts_with_is_substr_equality() {
assert_eq!(
translate_predicate(&pred("$path.startsWith(\"lab/\")"), &DOCS),
frag(
"(substr(d.path, 1, length(?)) = ?)",
&[text("lab/"), text("lab/")]
)
);
}
#[test]
fn lower_then_starts_with_pushes_with_explicit_lower() {
assert_eq!(
translate_predicate(&pred("$path.lower().startsWith(\"lab/\")"), &DOCS),
frag(
"(substr(lower(d.path), 1, length(?)) = ?)",
&[text("lab/"), text("lab/")]
)
);
}
#[test]
fn contains_is_instr() {
assert_eq!(
translate_predicate(&pred("$path.contains(\"notes\")"), &DOCS),
frag("(instr(d.path, ?) > 0)", &[text("notes")])
);
}
#[test]
fn ends_with_is_negative_substr_equality() {
assert_eq!(
translate_predicate(&pred("$path.endsWith(\".md\")"), &DOCS),
frag(
"(substr(d.path, -length(?)) = ?)",
&[text(".md"), text(".md")]
)
);
}
#[test]
fn upper_wraps_the_receiver_in_value_position() {
assert_eq!(
translate_value(&pred("$path.upper()"), &DOCS),
frag("upper(d.path)", &[])
);
}
#[test]
fn bare_doc_property_is_the_scalar_in_scope_subquery() {
let f = translate_predicate(&pred("layer == \"canon\""), &DOCS).expect("pushable");
assert!(f.sql.contains("FROM properties p"), "{}", f.sql);
assert!(f.sql.contains("p.key = 'layer'"), "{}", f.sql);
assert!(f.sql.contains("p.card = 'scalar'"), "{}", f.sql);
assert!(
f.sql.starts_with('(') && f.sql.contains(" IS ?)"),
"{}",
f.sql
);
assert_eq!(f.params, vec![text("canon")]);
}
#[test]
fn updated_at_pushes_as_its_revisions_subquery() {
let f =
translate_predicate(&pred("$updated_at >= \"2026-01-01\""), &DOCS).expect("pushable");
assert!(
f.sql.contains("FROM revisions r JOIN commits c"),
"{}",
f.sql
);
}
#[test]
fn format_is_a_column_not_a_property() {
assert_eq!(
translate_predicate(&pred("format == \"markdown\""), &DOCS),
frag("(d.format IS ?)", &[text("markdown")])
);
}
#[test]
fn booleans_bind_as_one_and_zero() {
let blocks = TranslateCtx {
target: Target::Blocks,
self_alias: "b",
..DOCS
};
assert_eq!(
translate_predicate(&pred("type == true"), &blocks).map(|f| f.params),
Some(vec![SqlValue::Integer(1)])
);
assert_eq!(
translate_predicate(&pred("type == false"), &blocks).map(|f| f.params),
Some(vec![SqlValue::Integer(0)])
);
assert_eq!(
translate_predicate(&pred("$ordinal < 1000"), &blocks).map(|f| f.params),
Some(vec![SqlValue::Real(1000.0)])
);
assert_eq!(
translate_predicate(&pred("$path == null"), &DOCS).map(|f| f.params),
Some(vec![SqlValue::Null])
);
}
const REPRESENTATIVES: [(Ty, &str); 7] = [
(Ty::Text, "$path"),
(Ty::Int, "$ordinal"),
(Ty::Num, "1"),
(Ty::Bool, "true"),
(Ty::Null, "null"),
(Ty::Json, "checked"),
(Ty::Prop, "doc.layer"),
];
#[test]
fn representatives_carry_their_type() {
let blocks = TranslateCtx {
target: Target::Blocks,
self_alias: "b",
..DOCS
};
for (ty, src) in REPRESENTATIVES {
let (_, got) = typed_value(&pred(src), &blocks).expect(src);
assert_eq!(got, ty, "{src}");
}
assert_eq!(
typed_value(&pred("attrs.a.b"), &blocks).map(|(_, t)| t),
Some(Ty::Json)
);
assert_eq!(
typed_value(&pred("$depth"), &blocks).map(|(_, t)| t),
Some(Ty::Int)
);
assert_eq!(
typed_value(&pred("type.lower()"), &blocks).map(|(_, t)| t),
Some(Ty::Text)
);
assert_eq!(
typed_value(&pred("checked.upper()"), &blocks).map(|(_, t)| t),
Some(Ty::Text)
);
assert_eq!(
typed_value(&pred("layer"), &DOCS).map(|(_, t)| t),
Some(Ty::Prop)
);
assert_eq!(
typed_value(&pred("format"), &DOCS).map(|(_, t)| t),
Some(Ty::Text)
);
}
#[test]
fn the_comparison_matrix_decides_every_cell() {
const P: bool = true;
const D: bool = false;
const T: bool = true;
#[rustfmt::skip]
const EQUALITY: [[bool; 7]; 7] = [
[P, P, P, P, P, P, P],
[P, P, P, D, P, D, D],
[P, P, P, D, P, T, T],
[P, D, D, D, P, T, T],
[P, P, P, P, P, P, D],
[P, D, T, T, P, D, D],
[P, D, T, T, D, D, D],
];
#[rustfmt::skip]
const RELATIONAL: [[bool; 7]; 7] = [
[P, D, D, D, D, D, D],
[D, P, P, D, D, D, D],
[D, P, P, D, D, T, T],
[D, D, D, D, D, D, D],
[D, D, D, D, D, D, D],
[D, D, T, D, D, D, D],
[D, D, T, D, D, D, D],
];
let blocks = TranslateCtx {
target: Target::Blocks,
self_alias: "b",
..DOCS
};
let ops = [
(BinaryOp::Eq, "==", &EQUALITY),
(BinaryOp::Ne, "!=", &EQUALITY),
(BinaryOp::Lt, "<", &RELATIONAL),
(BinaryOp::Le, "<=", &RELATIONAL),
(BinaryOp::Gt, ">", &RELATIONAL),
(BinaryOp::Ge, ">=", &RELATIONAL),
];
for (i, (a, l)) in REPRESENTATIVES.iter().enumerate() {
for (j, (b, r)) in REPRESENTATIVES.iter().enumerate() {
for (op, spelled, matrix) in ops {
let want = matrix[i][j];
assert_eq!(matrix[j][i], want, "the matrix is symmetric ({a:?}, {b:?})");
assert_eq!(
comparable(op, *a, *b),
want,
"comparable({spelled}, {a:?}, {b:?})"
);
let src = format!("{l} {spelled} {r}");
assert_eq!(
translate_predicate(&pred(&src), &blocks).is_some(),
want,
"{src}"
);
}
}
}
}
#[test]
fn the_spec_shapes_of_decline_a() {
let blocks = TranslateCtx {
target: Target::Blocks,
self_alias: "b",
..DOCS
};
let typed = |src: &str, ctx: &TranslateCtx<'_>| {
let f = translate_predicate(&pred(src), ctx).expect(src);
assert!(
f.sql.contains("json_type(") || f.sql.contains("p.type = "),
"{src}: {}",
f.sql
);
assert!(
f.sql.ends_with(" IS 1)") || f.sql.ends_with(" IS NOT 1)"),
"{src}: {}",
f.sql
);
};
typed("checked == 1", &blocks);
typed("checked == true", &blocks);
typed("attrs.checked == true", &blocks);
typed("verified == 1", &DOCS);
typed("verified == true", &DOCS);
typed("era < 1000", &DOCS);
typed("doc.era < 1000", &blocks);
assert_eq!(
translate_predicate(&pred("$ordinal == true"), &blocks),
None
);
assert_eq!(
translate_predicate(&pred("$ordinal == 1"), &blocks),
frag("(b.ordinal IS ?)", &[SqlValue::Real(1.0)])
);
assert_eq!(translate_predicate(&pred("tags != null"), &DOCS), None);
assert_eq!(translate_predicate(&pred("tags == null"), &DOCS), None);
assert_eq!(
translate_predicate(&pred("doc.tags == null"), &blocks),
None
);
assert_eq!(
translate_predicate(&pred("checked == null"), &blocks),
frag(
"(json_extract(b.attrs, '$.checked') IS ?)",
&[SqlValue::Null]
)
);
assert_eq!(
translate_predicate(&pred("$ordinal != null"), &blocks),
frag("(b.ordinal IS NOT ?)", &[SqlValue::Null])
);
assert_eq!(
translate_predicate(&pred("checked == \"x\""), &blocks),
frag("(json_extract(b.attrs, '$.checked') IS ?)", &[text("x")])
);
assert!(translate_predicate(&pred("layer == \"canon\""), &DOCS).is_some());
assert!(translate_predicate(&pred("$ordinal == \"1\""), &blocks).is_some());
assert!(translate_predicate(&pred("$ordinal != \"1\""), &blocks).is_some());
assert_eq!(
translate_predicate(&pred("$ordinal < \"3\""), &blocks),
None
);
assert_eq!(
translate_predicate(&pred("\"3\" >= $ordinal"), &blocks),
None
);
assert_eq!(translate_predicate(&pred("$path > 5"), &DOCS), None);
assert_eq!(translate_predicate(&pred("type <= 1"), &blocks), None);
assert_eq!(
translate_predicate(&pred("$ordinal < 3"), &blocks),
frag("(b.ordinal < ?)", &[SqlValue::Real(3.0)])
);
assert_eq!(
translate_predicate(&pred("$path > \"m\""), &DOCS),
frag("(d.path > ?)", &[text("m")])
);
assert_eq!(
translate_predicate(&pred("$ordinal == checked"), &blocks),
None
);
assert_eq!(
translate_predicate(&pred("checked == level"), &blocks),
None
);
assert_eq!(
translate_predicate(&pred("doc.era == doc.year"), &blocks),
None
);
assert_eq!(translate_predicate(&pred("level < \"x\""), &blocks), None);
typed("level < 3", &blocks);
assert_eq!(translate_predicate(&pred("true == false"), &blocks), None);
assert_eq!(translate_predicate(&pred("checked < true"), &blocks), None);
assert_eq!(
translate_predicate(&pred("doc.verified >= false"), &blocks),
None
);
assert_eq!(translate_predicate(&pred("$ordinal > null"), &blocks), None);
assert!(translate_predicate(&pred("$ordinal == $depth"), &blocks).is_some());
assert!(translate_predicate(&pred("$ordinal <= $depth"), &blocks).is_some());
assert!(translate_predicate(&pred("type == null"), &blocks).is_some());
}
const PROP_SCOPE: &str = "FROM properties p WHERE p.doc_id = d.doc_id AND p.key = 'K' AND p.card = 'scalar' AND p.deleted_commit IS NULL AND (SELECT COUNT(*) FROM properties p2 WHERE p2.doc_id = d.doc_id AND p2.key = 'K' AND p2.deleted_commit IS NULL) = 1 LIMIT 1";
fn prop_sql(key: &str, select: &str, wrap: &str) -> String {
format!(
"((SELECT {select} {}) {wrap})",
PROP_SCOPE.replace('K', key)
)
}
#[test]
fn json_against_a_boolean_tests_json_type_for_the_literal() {
let blocks = TranslateCtx {
target: Target::Blocks,
self_alias: "b",
..DOCS
};
assert_eq!(
translate_predicate(&pred("checked == true"), &blocks),
frag("((json_type(b.attrs, '$.checked') = 'true') IS 1)", &[])
);
assert_eq!(
translate_predicate(&pred("checked == false"), &blocks),
frag("((json_type(b.attrs, '$.checked') = 'false') IS 1)", &[])
);
assert_eq!(
translate_predicate(&pred("checked != true"), &blocks),
frag("((json_type(b.attrs, '$.checked') = 'true') IS NOT 1)", &[])
);
assert_eq!(
translate_predicate(&pred("false == attrs.checked"), &blocks),
frag("((json_type(b.attrs, '$.checked') = 'false') IS 1)", &[])
);
let params = [Value::Bool(true)];
let e = Expr::Binary {
op: BinaryOp::Ne,
left: ident("checked"),
right: Box::new(Expr::Binding { index: 0 }),
};
assert_eq!(
translate_predicate(
&e,
&TranslateCtx {
params: ¶ms,
..blocks
}
),
frag("((json_type(b.attrs, '$.checked') = 'true') IS NOT 1)", &[])
);
}
#[test]
fn json_against_a_number_tests_the_numeric_types_then_compares() {
let nodes = TranslateCtx {
target: Target::Nodes,
self_alias: "n",
..DOCS
};
let shape = |op: &str, wrap: &str| {
format!(
"((json_type(n.attrs, '$.level') IN ('integer', 'real') AND json_extract(n.attrs, '$.level') {op} ?) {wrap})"
)
};
let two = [SqlValue::Real(2.0)];
for (src, op, wrap) in [
("level == 2", "=", "IS 1"),
("level != 2", "=", "IS NOT 1"),
("level < 2", "<", "IS 1"),
("level <= 2", "<=", "IS 1"),
("level > 2", ">", "IS 1"),
("level >= 2", ">=", "IS 1"),
] {
assert_eq!(
translate_predicate(&pred(src), &nodes),
frag(&shape(op, wrap), &two),
"{src}"
);
}
assert_eq!(
translate_predicate(&pred("2 <= attrs.level"), &nodes),
frag(&shape(">=", "IS 1"), &two)
);
assert_eq!(
translate_predicate(&pred("2 > level"), &nodes),
frag(&shape("<", "IS 1"), &two)
);
assert_eq!(
translate_predicate(&pred("2 != level"), &nodes),
frag(&shape("=", "IS NOT 1"), &two)
);
}
#[test]
fn property_against_a_boolean_tests_p_type_bool_in_the_scalar_row_subquery() {
let blocks = TranslateCtx {
target: Target::Blocks,
self_alias: "b",
..DOCS
};
assert_eq!(
translate_predicate(&pred("verified == true"), &DOCS),
frag(
&prop_sql("verified", "p.type = 'bool' AND p.val_bool = ?", "IS 1"),
&[SqlValue::Integer(1)]
)
);
assert_eq!(
translate_predicate(&pred("verified != false"), &DOCS),
frag(
&prop_sql("verified", "p.type = 'bool' AND p.val_bool = ?", "IS NOT 1"),
&[SqlValue::Integer(0)]
)
);
assert_eq!(
translate_predicate(&pred("doc.verified == false"), &blocks),
frag(
&prop_sql("verified", "p.type = 'bool' AND p.val_bool = ?", "IS 1"),
&[SqlValue::Integer(0)]
)
);
assert_eq!(
translate_predicate(&pred("true == verified"), &DOCS),
frag(
&prop_sql("verified", "p.type = 'bool' AND p.val_bool = ?", "IS 1"),
&[SqlValue::Integer(1)]
)
);
}
#[test]
fn property_against_a_number_tests_p_type_number_in_the_scalar_row_subquery() {
let blocks = TranslateCtx {
target: Target::Blocks,
self_alias: "b",
..DOCS
};
let thousand = [SqlValue::Real(1000.0)];
for (src, op, wrap) in [
("era == 1000", "=", "IS 1"),
("era != 1000", "=", "IS NOT 1"),
("era < 1000", "<", "IS 1"),
("era <= 1000", "<=", "IS 1"),
("era > 1000", ">", "IS 1"),
("era >= 1000", ">=", "IS 1"),
] {
assert_eq!(
translate_predicate(&pred(src), &DOCS),
frag(
&prop_sql(
"era",
&format!("p.type = 'number' AND p.val_num {op} ?"),
wrap
),
&thousand
),
"{src}"
);
}
assert_eq!(
translate_predicate(&pred("doc.era >= 1000"), &blocks),
frag(
&prop_sql("era", "p.type = 'number' AND p.val_num >= ?", "IS 1"),
&thousand
)
);
assert_eq!(
translate_predicate(&pred("1000 > era"), &DOCS),
frag(
&prop_sql("era", "p.type = 'number' AND p.val_num < ?", "IS 1"),
&thousand
)
);
assert_eq!(
translate_predicate(&pred("1000 <= era"), &DOCS),
frag(
&prop_sql("era", "p.type = 'number' AND p.val_num >= ?", "IS 1"),
&thousand
)
);
}
#[test]
fn typed_pushes_compose_under_and_and_keep_the_untyped_forms() {
let blocks = TranslateCtx {
target: Target::Blocks,
self_alias: "b",
..DOCS
};
let e = Expr::Logical {
op: LogicalOp::And,
left: eq(ident("type"), lit("task")),
right: Box::new(Expr::Binary {
op: BinaryOp::Eq,
left: ident("checked"),
right: Box::new(Expr::Lit(Value::Bool(false))),
}),
};
assert_eq!(
translate_predicate(&e, &blocks),
frag(
"((b.type IS ?) AND ((json_type(b.attrs, '$.checked') = 'false') IS 1))",
&[text("task")]
)
);
assert_eq!(
translate_predicate(&pred("checked == \"x\""), &blocks),
frag("(json_extract(b.attrs, '$.checked') IS ?)", &[text("x")])
);
assert_eq!(
translate_predicate(&pred("checked != null"), &blocks),
frag(
"(json_extract(b.attrs, '$.checked') IS NOT ?)",
&[SqlValue::Null]
)
);
assert_eq!(
prop_row("d", "x'y", "p.type = 'number' AND p.val_num = ?"),
None
);
}
#[test]
fn bindings_are_typed_by_their_value() {
let blocks = TranslateCtx {
target: Target::Blocks,
self_alias: "b",
..DOCS
};
let params = [
Value::from("s"),
Value::from(1.0),
Value::Bool(true),
Value::Null,
Value::Array(vec![]),
];
let ctx = TranslateCtx {
params: ¶ms,
..blocks
};
let against = |i: usize, rhs: &str| {
let e = Expr::Binary {
op: BinaryOp::Eq,
left: Box::new(Expr::Binding { index: i }),
right: Box::new(pred(rhs)),
};
translate_predicate(&e, &ctx).is_some()
};
assert!(against(0, "checked"));
assert!(against(1, "checked"));
assert!(against(2, "checked"));
assert!(!against(2, "$ordinal"));
assert!(against(1, "$ordinal"));
assert!(against(3, "checked"));
assert!(!against(3, "doc.tags"));
assert!(against(9, "checked"));
assert!(!against(9, "doc.tags"));
assert!(!against(4, "type"));
}
#[test]
fn relation_and_handle_names_are_not_property_reads() {
let blocks = TranslateCtx {
target: Target::Blocks,
self_alias: "b",
..DOCS
};
let nodes = TranslateCtx {
target: Target::Nodes,
self_alias: "n",
..DOCS
};
let edges = TranslateCtx {
target: Target::Edges,
self_alias: "e",
..DOCS
};
for (ctx, target) in [
(&DOCS, Target::Docs),
(&blocks, Target::Blocks),
(&nodes, Target::Nodes),
(&edges, Target::Edges),
] {
for name in non_property_handles(target) {
let src = format!("{name} == null");
assert_eq!(
translate_predicate(&pred(&src), ctx),
None,
"{target:?}: {src}"
);
let src = format!("{name} == \"x\"");
assert_eq!(
translate_predicate(&pred(&src), ctx),
None,
"{target:?}: {src}"
);
}
}
assert_eq!(translate_predicate(&pred("nodes == null"), &DOCS), None);
assert_eq!(
translate_predicate(&pred("frontmatter == null"), &DOCS),
None
);
assert_eq!(translate_predicate(&pred("nodes == null"), &blocks), None);
assert_eq!(translate_predicate(&pred("attrs == null"), &blocks), None);
assert_eq!(
translate_predicate(&pred("doc.nodes == null"), &blocks),
None
);
assert_eq!(
translate_predicate(&pred("doc.frontmatter == null"), &blocks),
None
);
assert_eq!(translate_predicate(&pred("doc.doc == null"), &DOCS), None);
assert!(translate_predicate(&pred("doc.layer == \"canon\""), &blocks).is_some());
assert!(translate_predicate(&pred("section == \"x\""), &DOCS).is_some());
assert!(translate_predicate(&pred("frontmatter == \"x\""), &blocks).is_some());
}
#[test]
fn handle_sets_match_the_store_context() {
use std::collections::HashMap;
use omgbase_reconcile::Config;
use omgbase_store::{BatchItem, Store};
use oqx::DataContext;
use crate::context::StoreContext;
let mut store = Store::open_in_memory().expect("store");
let repo = store.create_repo("handles").expect("repo");
let items = [
BatchItem::observed(
"a.md",
"---\ntitle: A\nlayer: canon\nverified: true\n---\n# Heading\n\nSee [b](b.md) and [[b]].\n\n- [ ] task\n - nested\n\nkey:: value\n\n## Sub\n\ntext\n",
),
BatchItem::observed("b.md", "# B\n\nBack to [a](a.md).\n"),
];
store
.observe_batch(
&repo,
&items,
"2026-09-26T00:00:00.000Z",
&Config::default(),
)
.expect("observe");
let ctx = StoreContext::new(store.conn(), &repo, HashMap::new());
let mut universe: Vec<&str> = [Target::Docs, Target::Blocks, Target::Nodes, Target::Edges]
.into_iter()
.flat_map(|t| non_property_handles(t).iter().copied())
.collect();
universe.extend([
"layer",
"title",
"verified",
"checked",
"level",
"format",
"type",
"text",
"kind",
"name",
"value",
"predicate",
"key",
"nope",
]);
universe.sort_unstable();
universe.dedup();
let is_shape = |v: &Value| matches!(v, Value::Array(_) | Value::Object(_));
for target in [Target::Docs, Target::Blocks, Target::Nodes, Target::Edges] {
let Value::Array(rows) = ctx.root(target.as_str()) else {
panic!("{target:?} root is not an array");
};
assert!(!rows.is_empty(), "{target:?} has rows");
let set = non_property_handles(target);
for name in &universe {
let mut shaped = 0;
for row in &rows {
let v = ctx.get(row, name).expect("get");
if set.contains(name) {
assert!(
v.is_absent() || is_shape(&v),
"{target:?}.{name} read as a scalar: {v:?}"
);
} else {
assert!(!is_shape(&v), "{target:?}.{name} is a handle: {v:?}");
}
shaped += usize::from(is_shape(&v));
}
if set.contains(name) {
assert!(
shaped > 0,
"{target:?}.{name} never resolved to rows or a bag"
);
}
}
}
}
#[test]
fn reserved_bare_basename_is_not_pushed() {
assert_eq!(translate_predicate(&pred("path == \"x\""), &DOCS), None);
assert_eq!(translate_predicate(&pred("body == \"x\""), &DOCS), None);
assert_eq!(translate_predicate(&pred("doc.path == \"x\""), &DOCS), None);
}
#[test]
fn docs_body_and_computed_intrinsics_are_not_columns() {
assert_eq!(translate_predicate(&pred("$body == \"x\""), &DOCS), None);
assert_eq!(translate_predicate(&pred("$title == \"x\""), &DOCS), None);
assert_eq!(translate_predicate(&pred("$tags == \"x\""), &DOCS), None);
}
#[test]
fn matches_needs_a_regexp_udf() {
assert_eq!(
translate_predicate(&pred("$path.matches(\"^lab/\")"), &DOCS),
None
);
}
#[test]
fn negation_as_a_nested_expr_is_not_and_safe() {
let e = Expr::Unary {
op: oqx::ast::UnaryOp::Not,
expr: ident("$path"),
};
assert_eq!(translate_predicate(&e, &DOCS), None);
}
#[test]
fn disjunction_as_a_nested_expr_is_declined() {
let e = Expr::Logical {
op: LogicalOp::Or,
left: eq(ident("$path"), lit("a")),
right: eq(ident("$path"), lit("b")),
};
assert_eq!(translate_predicate(&e, &DOCS), None);
}
#[test]
fn unmapped_node_intrinsic_is_not_pushed() {
let nodes = TranslateCtx {
target: Target::Nodes,
self_alias: "n",
..DOCS
};
assert_eq!(
translate_predicate(&pred("$locator == \"x\""), &nodes),
None
);
let blocks = TranslateCtx {
target: Target::Blocks,
self_alias: "b",
..DOCS
};
assert_eq!(
translate_predicate(&pred("$updated_at == \"x\""), &blocks),
None
);
}
#[test]
fn range_membership_in_and_bare_idents_are_declined() {
assert_eq!(translate_predicate(&pred("era in 800..1680"), &DOCS), None);
assert_eq!(
translate_predicate(&pred("\"a\" in list(tags)"), &DOCS),
None
);
assert_eq!(translate_predicate(&pred("verified"), &DOCS), None);
assert_eq!(translate_predicate(&pred("doc.verified"), &DOCS), None);
assert_eq!(translate_predicate(&pred("size(tags) > 1"), &DOCS), None);
assert_eq!(translate_predicate(&pred("$self.text(\"x\")"), &DOCS), None);
assert_eq!(translate_predicate(&pred("$value == \"x\""), &DOCS), None);
assert_eq!(translate_predicate(&pred("^slug == \"x\""), &DOCS), None);
assert_eq!(
translate_predicate(&pred("frontmatter.era == 1"), &DOCS),
None
);
}
#[test]
fn and_composes_two_pushable_comparisons() {
let e = Expr::Logical {
op: LogicalOp::And,
left: eq(ident("$path"), lit("a")),
right: Box::new(Expr::Binary {
op: BinaryOp::Ne,
left: ident("$id"),
right: lit("d_2"),
}),
};
assert_eq!(
translate_predicate(&e, &DOCS),
frag(
"((d.path IS ?) AND (d.doc_id IS NOT ?))",
&[text("a"), text("d_2")]
)
);
}
#[test]
fn and_declines_wholesale_if_either_side_is_not_pushable() {
let e = Expr::Logical {
op: LogicalOp::And,
left: eq(ident("$path"), lit("a")),
right: eq(ident("$body"), lit("x")),
};
assert_eq!(translate_predicate(&e, &DOCS), None);
}
#[test]
fn resolves_a_binding_to_its_param_value() {
let e = eq(ident("$path"), Box::new(Expr::Binding { index: 0 }));
let params = [Value::from("from-binding.md")];
let ctx = TranslateCtx {
params: ¶ms,
..DOCS
};
assert_eq!(
translate_predicate(&e, &ctx),
frag("(d.path IS ?)", &[text("from-binding.md")])
);
assert_eq!(
translate_predicate(&e, &DOCS),
frag("(d.path IS ?)", &[SqlValue::Null])
);
}
#[test]
fn blocks_and_nodes_flatten_bare_identifiers_into_attrs() {
let blocks = TranslateCtx {
target: Target::Blocks,
self_alias: "b",
..DOCS
};
let both = Expr::Logical {
op: LogicalOp::And,
left: eq(ident("type"), lit("task")),
right: eq(ident("marker"), lit("x")),
};
assert_eq!(
translate_predicate(&both, &blocks),
frag(
"((b.type IS ?) AND (json_extract(b.attrs, '$.marker') IS ?))",
&[text("task"), text("x")]
)
);
assert_eq!(
translate_predicate(&pred("attrs.marker == \"x\""), &blocks),
frag("(json_extract(b.attrs, '$.marker') IS ?)", &[text("x")])
);
assert_eq!(
translate_predicate(&pred("attrs.checked == true"), &blocks),
frag("((json_type(b.attrs, '$.checked') = 'true') IS 1)", &[])
);
let nodes = TranslateCtx {
target: Target::Nodes,
self_alias: "n",
..DOCS
};
assert_eq!(
translate_predicate(&pred("kind == \"md:section\""), &nodes),
frag("(n.kind IS ?)", &[text("md:section")])
);
assert_eq!(
translate_predicate(&pred("level == \"1\""), &nodes),
frag("(json_extract(n.attrs, '$.level') IS ?)", &[text("1")])
);
assert_eq!(
translate_predicate(&pred("level == 1"), &nodes),
frag(
"((json_type(n.attrs, '$.level') IN ('integer', 'real') AND json_extract(n.attrs, '$.level') = ?) IS 1)",
&[SqlValue::Real(1.0)]
)
);
assert_eq!(
translate_predicate(&pred("attrs.a.b == \"c\""), &nodes),
frag("(json_extract(n.attrs, '$.a.b') IS ?)", &[text("c")])
);
assert_eq!(
translate_predicate(&pred("attrs.marker == \"x\""), &DOCS),
None
);
}
#[test]
fn doc_and_block_reach_through() {
let blocks = TranslateCtx {
target: Target::Blocks,
self_alias: "b",
..DOCS
};
let f = translate_predicate(&pred("doc.type == \"lab-note\""), &blocks).expect("pushable");
assert!(
f.sql.contains("p.doc_id = d.doc_id AND p.key = 'type'"),
"{}",
f.sql
);
assert_eq!(
translate_predicate(&pred("doc.$path == \"a.md\""), &blocks),
frag("(d.path IS ?)", &[text("a.md")])
);
assert_eq!(
translate_predicate(&pred("doc.format == \"markdown\""), &blocks),
frag("(d.format IS ?)", &[text("markdown")])
);
assert_eq!(
translate_predicate(&pred("doc.$id == \"d_1\""), &blocks),
None
);
assert_eq!(translate_predicate(&pred("doc.a.b == 1"), &blocks), None);
let nodes = TranslateCtx {
target: Target::Nodes,
self_alias: "n",
..DOCS
};
assert_eq!(
translate_predicate(&pred("block.type == \"task\""), &nodes),
frag(
"((SELECT bb.type FROM blocks bb WHERE bb.block_id = n.block_id) IS ?)",
&[text("task")]
)
);
assert_eq!(
translate_predicate(&pred("block.type == \"task\""), &blocks),
None
);
assert_eq!(
translate_predicate(&pred("section.level == 1"), &nodes),
None
);
}
#[test]
fn edges_push_their_five_fields_and_intrinsics() {
let edges = TranslateCtx {
target: Target::Edges,
self_alias: "e",
..DOCS
};
assert_eq!(
translate_predicate(&pred("predicate == \"references\""), &edges),
frag("(e.predicate IS ?)", &[text("references")])
);
assert_eq!(
translate_predicate(&pred("$dst_path == \"index.md\""), &edges),
frag(
"((SELECT dd.path FROM docs dd WHERE dd.doc_id = e.dst_node) IS ?)",
&[text("index.md")]
)
);
assert_eq!(translate_predicate(&pred("weight == 1"), &edges), None);
}
#[test]
fn unsafe_identifier_segments_are_never_inlined() {
assert!(is_seg("layer") && is_seg("_x9"));
assert!(!is_seg("") && !is_seg("9a") && !is_seg("a-b") && !is_seg("a'b"));
assert_eq!(json_path(&["ok", "no-pe"]), None);
assert_eq!(json_path(&["ok", "a_1"]).as_deref(), Some("$.ok.a_1"));
assert_eq!(prop_scalar("d", "x'y"), None);
}
}