use std::collections::HashMap;
use omgbase_mutate::effects::{SELECTION_CEILING, Selection, SelectionExpect, js_number};
use omgbase_mutate::{Effect, ErrorCode, MutationError, ParentRow, ShapeError, ShapeFixContext};
use omgbase_store::effects_plan::PlanContext;
use omgbase_store::mutate::is_id_ref;
use omgbase_store::{DocStore, Store};
use oqx::Consumer;
use oqx::ast::{Expr, Query, SelectItem, TemplatePart};
use serde_json::{Map, Value as Json, json};
use crate::context::{SemanticVec, Target};
use crate::error::{Result, SurfaceError};
use crate::fixes;
use crate::names;
use crate::paths::storage_path;
use crate::planner::root_target;
use crate::query::{
HIT_ID, HIT_PATH, QueryOptions, RESERVED_ALIAS_PREFIXES, check_reserved_aliases,
check_root_spellings, query_ast,
};
pub const HINT: &str = "selection";
pub const SLOT_PREFIX: &str = "__omg_sel_";
const WRAP_HEAD: &str = "select __omg_x: ";
const WRAP_TAIL: &str = " from docs";
const PROBE: &str = "$sel";
const OPS: &str = "create/insert/set/unset/move/remove/split/merge";
pub const LEADER: &str = "-> ";
fn refused_with(message: impl Into<String>, extra: Json) -> SurfaceError {
let message = message.into();
let mut data = json!({ "reason": message, "hint": HINT });
if let (Some(d), Some(x)) = (data.as_object_mut(), extra.as_object()) {
for (k, v) in x {
d.insert(k.clone(), v.clone());
}
}
SurfaceError::with_data("filter_invalid", message, data)
}
fn refused(message: impl Into<String>) -> SurfaceError {
refused_with(message, json!({}))
}
fn shape_error(message: String) -> SurfaceError {
SurfaceError::filter_invalid(message, "arguments")
}
fn shape_error_of(e: ShapeError) -> SurfaceError {
let mut err = shape_error(e.message);
if let Some(fix) = e.fix
&& let Some(d) = err.data.as_mut().and_then(Json::as_object_mut)
{
d.insert("fix".to_owned(), json!(fix));
}
err
}
fn parent_lookup(store: &Store) -> impl Fn(&str) -> Option<ParentRow> + '_ {
move |id: &str| {
store
.conn()
.query_row(
"SELECT b.parent_block, d.path FROM blocks b JOIN docs d ON d.doc_id = b.doc_id WHERE b.block_id = ? AND b.deleted_commit IS NULL",
[id],
|row| {
Ok(ParentRow {
parent: row.get(0)?,
path: row.get(1)?,
})
},
)
.ok()
}
}
fn no_parent(_: &str) -> Option<ParentRow> {
None
}
fn shape_fix<'a>(
request: &'a fixes::Request,
parent_of: &'a dyn Fn(&str) -> Option<ParentRow>,
) -> ShapeFixContext<'a> {
ShapeFixContext {
select: request.select.as_ref(),
effects: &request.effects,
expect: request.expect.as_ref(),
parent_of,
}
}
pub fn validate_plain(store: &Store, effects: &Json) -> Result<Vec<Effect>> {
let list = effects
.as_array()
.ok_or_else(|| shape_error("`effects` must be an array of effects".to_owned()))?;
if list.is_empty() {
return Err(shape_error(
"`effects` must name at least one effect".to_owned(),
));
}
let request = fixes::Request {
select: None,
effects: effects.clone(),
expect: None,
};
let lookup = parent_lookup(store);
let fix = shape_fix(&request, &lookup);
list.iter()
.enumerate()
.map(|(i, e)| Effect::validate_with(e, i, Some(&fix)).map_err(shape_error_of))
.collect()
}
fn leader_of(v: &Json) -> Option<&str> {
v.as_str()?.strip_prefix(LEADER)
}
fn contains_leader(v: &Json) -> bool {
match v {
Json::String(_) => leader_of(v).is_some(),
Json::Array(a) => a.iter().any(contains_leader),
Json::Object(o) => o.values().any(contains_leader),
_ => false,
}
}
fn path_looking(s: &str) -> bool {
s.contains('/') || s.ends_with(".md")
}
fn is_node_id(s: &str) -> bool {
s.len() == 14
&& s.starts_with("n_")
&& s[2..]
.bytes()
.all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
}
fn is_target_id(s: &str) -> bool {
is_id_ref(s, "b") || is_id_ref(s, "d") || is_node_id(s)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Member {
Target,
Into,
Targets(usize),
}
fn symbol_kind(member: Member, op: &str, field: Option<&str>) -> &'static str {
match member {
Member::Targets(_) => "block",
Member::Into => "document",
Member::Target => match op {
"split" => "block",
"set" => match field {
Some(f) if f == "markdown" || f == "attrs.checked" => "block",
_ => "document",
},
_ => "document",
},
}
}
fn bare_target(s: &str, here: &str, member: Member, op: &str, field: Option<&str>) -> Result<Json> {
if s.contains('#') {
let message = format!(
"{here}: {} is a locator, not a target — a bare target is a document path, a `b_`/`d_`/`n_` id or `$new.n`",
Json::String(s.to_owned())
);
return Err(SurfaceError::with_data(
"filter_invalid",
message.clone(),
json!({ "reason": message, "hint": "arguments", "fix": crate::fixes::LOCATOR }),
));
}
if omgbase_mutate::parse_new_symbol(s).is_some() {
return Ok(json!({ "kind": symbol_kind(member, op, field), "id": s }));
}
if is_id_ref(s, "b") {
return Ok(json!({ "kind": "block", "id": s }));
}
if is_id_ref(s, "d") {
return Ok(json!({ "kind": "document", "id": s }));
}
if is_node_id(s) {
return Ok(json!({ "kind": "node", "id": s }));
}
if path_looking(s) {
return Ok(json!({ "kind": "document", "path": s }));
}
Err(shape_error(format!(
"{here}: a target is {{ kind: …, … }} or a bare document path, a `b_`/`d_`/`n_` id or `$new.n` (got {})",
Json::String(s.to_owned())
)))
}
fn desugar_expression(src: &str, here: &str) -> Result<Json> {
let wrapped = format!("{WRAP_HEAD}{src}{WRAP_TAIL}");
let head = WRAP_HEAD.chars().count();
let shift = LEADER.chars().count() as isize - head as isize;
let q = oqx::parse_string(&wrapped)
.map_err(|e| refused(format!("{here}: {}", shift_offsets(&e.message, shift))))?;
let one = q.select.len() == 1
&& matches!(&q.source, Expr::Ident { name, .. } if name == "docs")
&& q.from.is_empty()
&& q.r#where.is_none()
&& q.order_by.is_none()
&& q.follow.is_none()
&& q.frontier.is_none()
&& q.limit.is_none()
&& q.offset.is_none()
&& !q.distinct
&& !q.values
&& q.consumer == Consumer::Collect;
if !one {
return Err(refused(format!(
"{here}: `oqx` must be one expression over the hit, not a query"
)));
}
let folded = match q.select.first() {
Some(SelectItem::Field { expr, .. }) => match expr {
Expr::Lit {
value: oqx::Value::Str(text),
..
} => Some(text.clone()),
Expr::Template { parts, .. } if parts.len() == 1 => match &parts[0] {
TemplatePart::Text(text) => Some(text.clone()),
TemplatePart::Expr(_) => None,
},
_ => None,
},
_ => None,
};
Ok(match folded {
Some(text) => Json::String(text),
None => json!({ "oqx": src }),
})
}
pub(crate) fn literal_of(src: &str) -> String {
match desugar_expression(src, "") {
Ok(Json::String(text)) => text,
_ => src.to_owned(),
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
struct Desugared {
symbols: Vec<Member>,
}
fn refuse_leader(v: &Json, here: &str, request: Option<&fixes::Request>) -> Result<()> {
match v {
Json::String(_) if leader_of(v).is_some() => {
let message =
format!("{here}: `-> ` is not allowed here — this member takes no expression");
Err(match request.and_then(|r| fixes::literal_slot(r, here)) {
Some(fix) => refused_with(message, json!({ "fix": fix })),
None => refused(message),
})
}
Json::Array(a) => a
.iter()
.enumerate()
.try_for_each(|(k, x)| refuse_leader(x, &format!("{here}[{k}]"), request)),
Json::Object(o) => o
.iter()
.try_for_each(|(k, x)| refuse_leader(x, &format!("{here}.{k}"), request)),
_ => Ok(()),
}
}
fn desugar_effect(
raw: &Json,
index: usize,
request: Option<&fixes::Request>,
) -> Result<(Json, Desugared)> {
let mut d = Desugared::default();
let Some(o) = raw.as_object() else {
return Ok((raw.clone(), d));
};
let Some(op) = o.get("op").and_then(Json::as_str).map(str::to_owned) else {
return Ok((raw.clone(), d));
};
let here = format!("effects[{index}]");
let field = o.get("field").and_then(Json::as_str).map(str::to_owned);
let mut out = o.clone();
let mut slot_keys: Vec<&str> = vec!["op"];
fn leader_at(
out: &mut Map<String, Json>,
slot_keys: &mut Vec<&str>,
key: &'static str,
here: &str,
) -> Result<()> {
slot_keys.push(key);
if let Some(src) = out.get(key).and_then(leader_of).map(str::to_owned) {
let v = desugar_expression(&src, &format!("{here}.{key}"))?;
out.insert(key.to_owned(), v);
}
Ok(())
}
fn one_target(
v: &Json,
here: &str,
member: Member,
op: &str,
field: Option<&str>,
d: &mut Desugared,
request: Option<&fixes::Request>,
) -> Result<Json> {
match v {
Json::String(s) => {
let literal = match leader_of(v) {
Some(src) => match desugar_expression(src, here)? {
Json::String(folded) => folded,
directive => return Ok(directive),
},
None => s.clone(),
};
let t = bare_target(&literal, here, member, op, field)?;
if omgbase_mutate::parse_new_symbol(&literal).is_some() {
d.symbols.push(member);
}
Ok(t)
}
Json::Object(t) => {
let mut out = t.clone();
for m in ["id", "path"] {
if let Some(src) = t.get(m).and_then(leader_of) {
let v = desugar_expression(src, &format!("{here}.{m}"))?;
out.insert(m.to_owned(), v);
}
}
for (k, x) in t {
if k != "id" && k != "path" {
refuse_leader(x, &format!("{here}.{k}"), request)?;
}
}
Ok(Json::Object(out))
}
other => Ok(other.clone()),
}
}
let target = |out: &mut Map<String, Json>,
slot_keys: &mut Vec<&str>,
key: &'static str,
member: Member,
d: &mut Desugared|
-> Result<()> {
slot_keys.push(key);
if let Some(v) = out.get(key).cloned() {
let t = one_target(
&v,
&format!("{here}.{key}"),
member,
&op,
field.as_deref(),
d,
request,
)?;
out.insert(key.to_owned(), t);
}
Ok(())
};
let targets =
|out: &mut Map<String, Json>, slot_keys: &mut Vec<&str>, d: &mut Desugared| -> Result<()> {
slot_keys.push("targets");
if let Some(Json::Array(list)) = out.get("targets").cloned() {
let mut items = Vec::with_capacity(list.len());
for (k, v) in list.iter().enumerate() {
items.push(one_target(
v,
&format!("{here}.targets[{k}]"),
Member::Targets(k),
&op,
field.as_deref(),
d,
request,
)?);
}
out.insert("targets".to_owned(), Json::Array(items));
}
Ok(())
};
let at = |out: &mut Map<String, Json>, slot_keys: &mut Vec<&str>| -> Result<()> {
slot_keys.push("at");
let Some(a) = out.get("at").cloned() else {
return Ok(());
};
let Some(a) = a.as_object() else {
return refuse_leader(&a, &format!("{here}.at"), request);
};
let mut next = a.clone();
for m in ["before", "after"] {
if let Some(src) = a.get(m).and_then(leader_of) {
let v = desugar_expression(src, &format!("{here}.at.{m}"))?;
next.insert(m.to_owned(), v);
}
}
for (k, x) in a {
if k != "before" && k != "after" {
refuse_leader(x, &format!("{here}.at.{k}"), request)?;
}
}
out.insert("at".to_owned(), Json::Object(next));
Ok(())
};
match op.as_str() {
"create" => {
leader_at(&mut out, &mut slot_keys, "path", &here)?;
leader_at(&mut out, &mut slot_keys, "markdown", &here)?;
slot_keys.push("frontmatter");
if let Some(Json::Object(fm)) = o.get("frontmatter") {
let mut next = fm.clone();
for (k, v) in fm {
if let Some(src) = leader_of(v) {
let x = desugar_expression(src, &format!("{here}.frontmatter.{k}"))?;
next.insert(k.clone(), x);
}
}
out.insert("frontmatter".to_owned(), Json::Object(next));
}
}
"insert" => {
target(&mut out, &mut slot_keys, "into", Member::Into, &mut d)?;
at(&mut out, &mut slot_keys)?;
leader_at(&mut out, &mut slot_keys, "markdown", &here)?;
}
"set" => {
target(&mut out, &mut slot_keys, "target", Member::Target, &mut d)?;
leader_at(&mut out, &mut slot_keys, "value", &here)?;
}
"unset" => target(&mut out, &mut slot_keys, "target", Member::Target, &mut d)?,
"move" => {
if o.contains_key("targets") || o.contains_key("into") {
targets(&mut out, &mut slot_keys, &mut d)?;
target(&mut out, &mut slot_keys, "into", Member::Into, &mut d)?;
at(&mut out, &mut slot_keys)?;
} else {
target(&mut out, &mut slot_keys, "target", Member::Target, &mut d)?;
leader_at(&mut out, &mut slot_keys, "to", &here)?;
}
}
"remove" => {
if o.contains_key("targets") {
targets(&mut out, &mut slot_keys, &mut d)?;
} else {
target(&mut out, &mut slot_keys, "target", Member::Target, &mut d)?;
}
}
"split" => target(&mut out, &mut slot_keys, "target", Member::Target, &mut d)?,
"merge" => {
targets(&mut out, &mut slot_keys, &mut d)?;
leader_at(&mut out, &mut slot_keys, "separator", &here)?;
}
"retarget" => {}
_ => return Ok((Json::Object(out), d)),
}
for (key, v) in o {
if !slot_keys.contains(&key.as_str()) {
refuse_leader(v, &format!("{here}.{key}"), request)?;
}
}
Ok((Json::Object(out), d))
}
fn needs_sugar(effects: &Json) -> bool {
let Some(list) = effects.as_array() else {
return false;
};
list.iter().any(|e| {
let Some(o) = e.as_object() else {
return false;
};
o.get("target").is_some_and(Json::is_string)
|| o.get("into").is_some_and(Json::is_string)
|| o.get("targets")
.and_then(Json::as_array)
.is_some_and(|a| a.iter().any(Json::is_string))
|| contains_leader(e)
|| contains_oqx(e)
})
}
#[derive(Clone, Debug, PartialEq)]
pub struct SelectRequest {
pub select: Json,
pub templates: Json,
pub expect: Option<Json>,
}
impl SelectRequest {
pub fn from_args(args: &Json) -> Result<Option<Self>> {
let select = args.get("select").filter(|v| !v.is_null());
let expect = args.get("expect").filter(|v| !v.is_null());
let effects = args.get("effects").cloned().unwrap_or(Json::Null);
if select.is_none() && expect.is_none() {
if needs_sugar(&effects) {
return Ok(Some(Self {
select: Json::Null,
templates: effects,
expect: None,
}));
}
return Ok(None);
}
let Some(select) = select else {
return Err(refused(
"`expect` bounds a selection — pass `select` with it",
));
};
Ok(Some(Self {
select: select.clone(),
templates: effects,
expect: expect.cloned(),
}))
}
#[must_use]
pub fn has_selector(&self) -> bool {
!self.select.is_null()
}
#[must_use]
pub fn select_str(&self) -> &str {
self.select.as_str().unwrap_or_default()
}
#[must_use]
pub fn templates(&self) -> usize {
self.templates.as_array().map_or(0, Vec::len)
}
}
fn validate_expect(v: Option<&Json>) -> Result<Option<SelectionExpect>> {
let Some(v) = v else {
return Ok(None);
};
let obj = v
.as_object()
.ok_or_else(|| refused("`expect` must be an object { max }"))?;
let extra: Vec<String> = obj
.keys()
.filter(|k| *k != "max")
.map(|k| format!("`{k}`"))
.collect();
if !extra.is_empty() {
return Err(refused(format!(
"`expect`: unknown key{} {}",
if extra.len() > 1 { "s" } else { "" },
extra.join(", ")
)));
}
let max = obj
.get("max")
.and_then(Json::as_u64)
.and_then(|n| usize::try_from(n).ok())
.ok_or_else(|| refused("`expect.max` must be a non-negative integer"))?;
Ok(Some(SelectionExpect { max }))
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum SlotKind {
Identity { key: String, member: String },
TargetsIdentity { k: usize },
WholeTarget { key: String, member: Member },
WholeTargets { k: usize },
Str { key: String },
At { member: String },
Value,
Frontmatter { key: String },
}
#[derive(Clone, Debug)]
struct Slot {
name: String,
kind: SlotKind,
here: String,
expression: String,
item: SelectItem,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum DefaultTarget {
Target,
Into,
Targets,
}
#[derive(Clone, Debug)]
pub struct Template {
index: usize,
base: Map<String, Json>,
slots: Vec<Slot>,
default_target: Option<DefaultTarget>,
symbols: Vec<Member>,
op: String,
}
impl Template {
#[must_use]
pub fn slot_names(&self) -> Vec<String> {
self.slots
.iter()
.map(|s| slot_name(self.index, &s.name))
.collect()
}
}
fn slot_name(t: usize, slot: &str) -> String {
format!("{SLOT_PREFIX}{t}_{slot}")
}
fn oqx_of(v: &Json) -> Option<&str> {
let obj = v.as_object()?;
if obj.len() != 1 {
return None;
}
obj.get("oqx")?.as_str()
}
fn contains_oqx(v: &Json) -> bool {
if oqx_of(v).is_some() {
return true;
}
match v {
Json::Array(a) => a.iter().any(contains_oqx),
Json::Object(o) => o.values().any(contains_oqx),
_ => false,
}
}
fn row_ref(v: &Json) -> Option<(&str, &str)> {
let o = v.as_object()?;
if o.len() != 2 {
return None;
}
Some((o.get(HIT_ID)?.as_str()?, o.get(HIT_PATH)?.as_str()?))
}
fn describe(v: Option<&Json>) -> String {
match v {
None | Some(Json::Null) => "absent".to_owned(),
Some(Json::String(s)) => format!("a string ({})", Json::String(s.clone())),
Some(Json::Number(n)) => format!(
"a number ({})",
n.as_f64().map_or_else(|| n.to_string(), js_number)
),
Some(Json::Bool(b)) => format!("a boolean ({b})"),
Some(Json::Array(a)) => format!("a list of {}", a.len()),
Some(Json::Object(_)) => "an object".to_owned(),
}
}
fn shift_offsets(message: &str, delta: isize) -> String {
const MARK: &str = "(at offset ";
let Some(i) = message.find(MARK) else {
return message.to_owned();
};
let rest = &message[i + MARK.len()..];
let digits: String = rest.chars().take_while(char::is_ascii_digit).collect();
if digits.is_empty() {
return message.to_owned();
}
let n: isize = digits.parse().unwrap_or(0);
format!(
"{}{MARK}{}{}",
&message[..i],
(n + delta).max(0),
&rest[digits.len()..]
)
}
fn parse_expression(source: &str, here: &str) -> Result<SelectItem> {
let wrapped = format!("{WRAP_HEAD}{source}{WRAP_TAIL}");
let head = WRAP_HEAD.chars().count();
let mut q = oqx::parse_string(&wrapped).map_err(|e| {
refused(format!(
"{here}: {}",
shift_offsets(&e.message, -(head as isize))
))
})?;
let one = q.select.len() == 1
&& matches!(&q.source, Expr::Ident { name, .. } if name == "docs")
&& q.from.is_empty()
&& q.r#where.is_none()
&& q.order_by.is_none()
&& q.follow.is_none()
&& q.frontier.is_none()
&& q.limit.is_none()
&& q.offset.is_none()
&& !q.distinct
&& !q.values
&& q.consumer == Consumer::Collect;
if !one {
return Err(refused(format!(
"{here}: `oqx` must be one expression over the hit, not a query"
)));
}
Ok(q.select.pop().expect("one item"))
}
fn renamed(item: &SelectItem, name: &str) -> SelectItem {
match item {
SelectItem::Field {
expr, lift, span, ..
} => SelectItem::Field {
name: name.to_owned(),
expr: expr.clone(),
lift: *lift,
span: *span,
},
SelectItem::Collect { op, span, .. } => SelectItem::Collect {
name: name.to_owned(),
op: op.clone(),
span: *span,
},
}
}
fn hit_kind(root: Target) -> &'static str {
match root {
Target::Docs => "document",
Target::Blocks => "block",
Target::Nodes => "node",
Target::Links => "link",
Target::Revisions => "revision",
}
}
fn hit_ref(root: Target, id: &str) -> Json {
json!({ "kind": hit_kind(root), "id": id })
}
fn needs_selector(where_: &str) -> SurfaceError {
refused(format!(
"{where_}: an expression target needs a selector — without `select` there is no row to read it from"
))
}
struct Admit<'a> {
here: String,
raw: &'a Map<String, Json>,
root: Target,
plain: bool,
base: Map<String, Json>,
probe: Map<String, Json>,
slots: Vec<Slot>,
slot_keys: Vec<&'static str>,
}
impl Admit<'_> {
fn push(&mut self, name: String, kind: SlotKind, at: &str, expression: &str) -> Result<()> {
let here = format!("{}.{at}", self.here);
let item = parse_expression(expression, &here)?;
self.slots.push(Slot {
name,
kind,
here,
expression: expression.to_owned(),
item,
});
Ok(())
}
fn string_slot(&mut self, key: &'static str) -> Result<()> {
self.slot_keys.push(key);
let Some(src) = self.raw.get(key).and_then(oqx_of) else {
return Ok(());
};
let src = src.to_owned();
self.base.remove(key);
self.probe.insert(key.to_owned(), json!(PROBE));
self.push(
key.to_owned(),
SlotKind::Str {
key: key.to_owned(),
},
key,
&src,
)
}
fn value_slot(&mut self) -> Result<()> {
self.slot_keys.push("value");
let Some(src) = self.raw.get("value").and_then(oqx_of) else {
return Ok(());
};
let src = src.to_owned();
self.base.remove("value");
self.probe.insert("value".to_owned(), Json::Null);
self.push("value".to_owned(), SlotKind::Value, "value", &src)
}
fn target_slot(
&mut self,
key: &'static str,
defaultable: bool,
probe_kind: &str,
primary: bool,
) -> Result<bool> {
self.slot_keys.push(key);
let Some(t) = self.raw.get(key) else {
if !defaultable || self.plain {
return Ok(false);
}
self.probe.insert(key.to_owned(), hit_ref(self.root, PROBE));
return Ok(true);
};
if let Some(src) = oqx_of(t).map(str::to_owned) {
if self.plain && primary {
return Err(needs_selector(&format!("{}.{key}", self.here)));
}
self.base.remove(key);
self.probe
.insert(key.to_owned(), json!({ "kind": probe_kind, "id": PROBE }));
let member = if key == "into" {
Member::Into
} else {
Member::Target
};
self.push(
key.to_owned(),
SlotKind::WholeTarget {
key: key.to_owned(),
member,
},
key,
&src,
)?;
return Ok(false);
}
if let Some(t) = t.as_object() {
let id_x = t.get("id").and_then(oqx_of).map(str::to_owned);
let path_x = t.get("path").and_then(oqx_of).map(str::to_owned);
if id_x.is_some() && path_x.is_some() {
return Err(refused(format!(
"{}.{key}: give `id` or `path` as an expression, not both",
self.here
)));
}
if let Some((member, src)) = id_x
.map(|s| ("id", s))
.or_else(|| path_x.map(|s| ("path", s)))
{
if self.plain && primary {
return Err(needs_selector(&format!("{}.{key}", self.here)));
}
let mut literal = t.clone();
literal.remove(member);
self.base.insert(key.to_owned(), Json::Object(literal));
if let Some(p) = self.probe.get_mut(key).and_then(Json::as_object_mut) {
p.insert(member.to_owned(), json!(PROBE));
}
self.push(
key.to_owned(),
SlotKind::Identity {
key: key.to_owned(),
member: member.to_owned(),
},
&format!("{key}.{member}"),
&src,
)?;
}
for (k, v) in t {
if k != "id" && k != "path" && contains_oqx(v) {
return Err(refused(format!(
"{}.{key}.{k}: `{{ oqx }}` is not allowed here",
self.here
)));
}
}
}
Ok(false)
}
fn targets_slot(&mut self, defaultable: bool) -> Result<bool> {
self.slot_keys.push("targets");
let Some(list) = self.raw.get("targets") else {
if !defaultable || self.plain {
return Ok(false);
}
self.probe
.insert("targets".to_owned(), json!([hit_ref(self.root, PROBE)]));
return Ok(true);
};
let Some(list) = list.as_array() else {
return Ok(false);
};
for (k, t) in list.iter().enumerate() {
if let Some(src) = oqx_of(t).map(str::to_owned) {
if self.plain {
return Err(needs_selector(&format!("{}.targets[{k}]", self.here)));
}
if let Some(arr) = self.base.get_mut("targets").and_then(Json::as_array_mut) {
arr[k] = json!({});
}
if let Some(arr) = self.probe.get_mut("targets").and_then(Json::as_array_mut) {
arr[k] = json!({ "kind": "block", "id": PROBE });
}
self.push(
format!("targets_{k}"),
SlotKind::WholeTargets { k },
&format!("targets[{k}]"),
&src,
)?;
continue;
}
let Some(t) = t.as_object() else {
continue;
};
if t.get("path").and_then(oqx_of).is_some() {
return Err(refused(format!(
"{}.targets[{k}]: a block target has `id`, not `path`",
self.here
)));
}
if let Some(src) = t.get("id").and_then(oqx_of).map(str::to_owned) {
if self.plain {
return Err(needs_selector(&format!("{}.targets[{k}].id", self.here)));
}
let mut literal = t.clone();
literal.remove("id");
if let Some(arr) = self.base.get_mut("targets").and_then(Json::as_array_mut) {
arr[k] = Json::Object(literal);
}
if let Some(p) = self
.probe
.get_mut("targets")
.and_then(Json::as_array_mut)
.and_then(|a| a.get_mut(k))
.and_then(Json::as_object_mut)
{
p.insert("id".to_owned(), json!(PROBE));
}
self.push(
format!("targets_{k}"),
SlotKind::TargetsIdentity { k },
&format!("targets[{k}].id"),
&src,
)?;
}
for (key, v) in t {
if key != "id" && contains_oqx(v) {
return Err(refused(format!(
"{}.targets[{k}].{key}: `{{ oqx }}` is not allowed here",
self.here
)));
}
}
}
Ok(false)
}
fn at_slot(&mut self) -> Result<()> {
self.slot_keys.push("at");
let Some(at) = self.raw.get("at").and_then(Json::as_object) else {
return Ok(());
};
for member in ["before", "after"] {
let Some(src) = at.get(member).and_then(oqx_of).map(str::to_owned) else {
continue;
};
self.base.insert("at".to_owned(), json!({}));
self.probe.insert("at".to_owned(), json!({ member: PROBE }));
self.push(
"at".to_owned(),
SlotKind::At {
member: member.to_owned(),
},
&format!("at.{member}"),
&src,
)?;
}
Ok(())
}
fn frontmatter_slots(&mut self) -> Result<()> {
self.slot_keys.push("frontmatter");
let Some(fm) = self.raw.get("frontmatter").and_then(Json::as_object) else {
return Ok(());
};
for (j, (k, v)) in fm.iter().enumerate() {
let Some(src) = oqx_of(v).map(str::to_owned) else {
continue;
};
if let Some(b) = self
.base
.get_mut("frontmatter")
.and_then(Json::as_object_mut)
{
b.remove(k);
}
if let Some(p) = self
.probe
.get_mut("frontmatter")
.and_then(Json::as_object_mut)
{
p.insert(k.clone(), Json::Null);
}
self.push(
format!("fm_{j}"),
SlotKind::Frontmatter { key: k.clone() },
&format!("frontmatter.{k}"),
&src,
)?;
}
Ok(())
}
}
fn admit_template(
index: usize,
raw: &Json,
root: Target,
plain: bool,
sugar: &Desugared,
fix: Option<&ShapeFixContext<'_>>,
) -> Result<Template> {
let here = format!("effects[{index}]");
let Some(op) = raw
.as_object()
.and_then(|o| o.get("op"))
.and_then(Json::as_str)
else {
return Err(refused(format!(
"{here}: a template is an effect object whose `op` is one of {OPS}"
)));
};
let raw = raw.as_object().expect("checked");
if op == "retarget" {
return Err(refused(format!(
"{here}: `retarget` is not row-shaped — it cannot be a selector template"
)));
}
if !matches!(
op,
"create" | "insert" | "set" | "unset" | "move" | "remove" | "split" | "merge"
) {
return Err(refused(format!(
"{here}: `op` must be one of {OPS} (got \"{op}\")"
)));
}
let mut a = Admit {
here: here.clone(),
raw,
root,
plain,
base: raw.clone(),
probe: raw.clone(),
slots: Vec::new(),
slot_keys: Vec::new(),
};
let mut default_target = None;
match op {
"create" => {
a.string_slot("path")?;
a.string_slot("markdown")?;
a.frontmatter_slots()?;
}
"insert" => {
if a.target_slot("into", true, "document", true)? {
default_target = Some(DefaultTarget::Into);
}
a.at_slot()?;
a.string_slot("markdown")?;
}
"set" => {
if a.target_slot("target", true, "document", true)? {
default_target = Some(DefaultTarget::Target);
}
a.value_slot()?;
}
"unset" => {
if a.target_slot("target", true, "document", true)? {
default_target = Some(DefaultTarget::Target);
}
}
"move" => {
if raw.contains_key("into") {
if a.targets_slot(true)? {
default_target = Some(DefaultTarget::Targets);
}
a.target_slot("into", false, "document", false)?;
a.at_slot()?;
} else {
if a.target_slot("target", true, "document", true)? {
default_target = Some(DefaultTarget::Target);
}
a.string_slot("to")?;
}
}
"remove" => {
let doc_form = raw.contains_key("target")
|| (!raw.contains_key("targets") && root == Target::Docs);
if doc_form {
if a.target_slot("target", true, "document", true)? {
default_target = Some(DefaultTarget::Target);
}
} else if a.targets_slot(true)? {
default_target = Some(DefaultTarget::Targets);
}
}
"split" => {
if a.target_slot("target", true, "block", true)? {
default_target = Some(DefaultTarget::Target);
}
}
"merge" => {
if !raw.contains_key("targets") {
return Err(refused(format!(
"{here}: `merge` needs `targets` — one hit cannot be merged with itself"
)));
}
a.targets_slot(false)?;
a.string_slot("separator")?;
}
_ => unreachable!("the ops above"),
}
for (key, v) in raw {
if key == "op" || a.slot_keys.contains(&key.as_str()) {
continue;
}
if contains_oqx(v) {
return Err(refused(format!(
"{here}.{key}: `{{ oqx }}` is not allowed here"
)));
}
}
let Admit {
base, probe, slots, ..
} = a;
Effect::validate_with(&Json::Object(probe), index, fix).map_err(shape_error_of)?;
Ok(Template {
index,
base,
slots,
default_target,
symbols: sugar.symbols.clone(),
op: op.to_owned(),
})
}
#[derive(Clone, Debug)]
pub struct Compiled {
pub raw: Query,
pub root: Target,
pub templates: Vec<Template>,
pub rewritten: Query,
pub expect: Option<SelectionExpect>,
pub bound: usize,
pub request: fixes::Request,
}
impl Compiled {
#[must_use]
pub fn implicit(&self) -> bool {
self.expect.is_none()
}
}
fn admit_selector(select: &str, request: Option<&fixes::Request>) -> Result<(Query, Target)> {
let raw = oqx::parse_string(select).map_err(|e| refused(format!("select: {}", e.message)))?;
let root_fix = |links: bool| -> Json {
let span = raw.source.span();
match request {
Some(r) => json!({ "fix": fixes::root_fix(r, select, (span.start, span.end), links) }),
None => json!({}),
}
};
check_root_spellings(&raw).map_err(|e| refused(format!("select: {}", e.message)))?;
check_reserved_aliases(&raw).map_err(|e| refused(format!("select: {}", e.message)))?;
if !matches!(
raw.consumer,
Consumer::Collect | Consumer::First | Consumer::Single
) {
return Err(refused(format!(
"select: `{}` yields no rows — a selector's consumer is collect, first or single",
raw.consumer.as_str()
)));
}
if raw.values {
return Err(refused("select: a selector yields rows, not `values`"));
}
let root = match root_target(&raw.source) {
None => {
return Err(refused_with(
"select: a selector scans a root collection — `from docs`, `from blocks` or `from nodes`",
root_fix(false),
));
}
Some(Target::Links) => {
return Err(refused_with(
"select: `links` rows have no effect target — select docs, blocks or nodes",
root_fix(true),
));
}
Some(Target::Revisions) => {
return Err(refused_with(
"select: `revisions` rows have no effect target — select docs, blocks or nodes",
root_fix(false),
));
}
Some(t) => t,
};
if raw.follow.is_some() {
return Err(refused(
"select: `follow` is not allowed in a selector — a hit must be a row of the root",
));
}
if !raw.from.is_empty() {
return Err(refused(
"select: a body `from` re-projects the rows — a hit must be a row of the root",
));
}
if raw.offset.is_some() {
return Err(refused(
"select: `offset` is not allowed — a selection is a set, not a page",
));
}
if raw.distinct {
return Err(refused("select: `distinct` is not allowed in a selector"));
}
Ok((raw, root))
}
pub fn compile(req: &SelectRequest) -> Result<Compiled> {
let expect = validate_expect(req.expect.as_ref())?;
let select = req
.select
.as_str()
.filter(|s| !s.trim().is_empty())
.ok_or_else(|| refused("`select` must be an OQX query (a non-empty string)"))?;
let templates = req
.templates
.as_array()
.ok_or_else(|| refused("`effects` must be an array of effect templates"))?;
if templates.is_empty() {
return Err(refused("`effects` must name at least one template"));
}
let request = fixes::Request {
select: Some(req.select.clone()),
effects: req.templates.clone(),
expect: req.expect.clone(),
};
let (raw, root) = admit_selector(select, Some(&request))?;
let fix = shape_fix(&request, &no_parent);
let templates = templates
.iter()
.enumerate()
.map(|(t, v)| {
let (v, d) = desugar_effect(v, t, Some(&request))?;
admit_template(t, &v, root, false, &d, Some(&fix))
})
.collect::<Result<Vec<_>>>()?;
let mut rewritten = raw.clone();
for tpl in &templates {
for slot in &tpl.slots {
rewritten
.select
.push(renamed(&slot.item, &slot_name(tpl.index, &slot.name)));
}
}
let bound = expect.map_or(SELECTION_CEILING, |e| e.max);
Ok(Compiled {
raw,
root,
templates,
rewritten,
expect,
bound,
request,
})
}
pub type Row = (String, String);
fn slot_data(k: usize, t: Option<usize>, row: &Row, expression: &str) -> Json {
let mut d = json!({
"effect_index": k,
"row": { "id": row.0, "path": row.1 },
"expression": expression,
});
if let Some(t) = t {
d["template_index"] = json!(t);
}
d
}
struct Origin<'a> {
k: usize,
template: Option<usize>,
row: &'a Row,
root: Target,
request: Option<&'a fixes::Request>,
}
fn identity_value<'v>(v: Option<&'v Json>, s: &Slot, o: &Origin<'_>) -> Result<&'v Json> {
let data = || slot_data(o.k, o.template, o.row, &s.expression);
let absent = v.is_none_or(Json::is_null);
if absent || v.and_then(Json::as_array).is_some_and(Vec::is_empty) {
return Err(MutationError::with_data(
ErrorCode::TargetMissing,
format!(
"{}: the expression yielded no identity for row {}",
s.here, o.row.0
),
data(),
)
.into());
}
let v = v.expect("not absent");
if let Some(a) = v.as_array() {
let mut d = data();
d["count"] = json!(a.len());
let first = a
.first()
.and_then(|h| row_ref(h).map(|(id, _)| id).or_else(|| h.as_str()));
if let (Some(req), Some(first)) = (o.request, first)
&& let Some(fix) =
fixes::ambiguous_slot(req, &s.here, first, (o.root.as_str(), &o.row.0))
{
d["fix"] = json!(fix);
}
return Err(MutationError::with_data(
ErrorCode::AmbiguousTarget,
format!(
"{}: the expression yielded {} identities for row {}; a target resolves to exactly one",
s.here,
a.len(),
o.row.0
),
d,
)
.into());
}
if v.as_str().is_none() && row_ref(v).is_none() {
return Err(refused_with(
format!(
"{}: the expression yielded {}, not an identity (a string or a row)",
s.here,
describe(Some(v))
),
data(),
));
}
Ok(v)
}
fn whole_target(
v: &Json,
s: &Slot,
o: &Origin<'_>,
member: Member,
op: &str,
field: Option<&str>,
) -> Result<(Json, bool)> {
let is_row = row_ref(v).is_some();
let r = match row_ref(v) {
Some((id, _)) => id,
None => v
.as_str()
.expect("identity_value admitted a string or a row"),
};
if !is_row
&& !path_looking(r)
&& !is_target_id(r)
&& omgbase_mutate::parse_new_symbol(r).is_none()
{
return Err(refused_with(
format!(
"{}: the expression yielded {}, not an identity (a document path, an id or a row)",
s.here,
describe(Some(v))
),
slot_data(o.k, o.template, o.row, &s.expression),
));
}
let t = bare_target(r, &s.here, member, op, field)?;
Ok((t, omgbase_mutate::parse_new_symbol(r).is_some()))
}
fn bind_one(root: Target, hit: &Json, t: &Template, o: &Origin<'_>) -> Result<(Json, Vec<Member>)> {
let mut e = Json::Object(t.base.clone());
let mut symbols = t.symbols.clone();
match t.default_target {
Some(DefaultTarget::Target) => e["target"] = hit_ref(root, &o.row.0),
Some(DefaultTarget::Into) => e["into"] = hit_ref(root, &o.row.0),
Some(DefaultTarget::Targets) => e["targets"] = json!([hit_ref(root, &o.row.0)]),
None => {}
}
let field = t
.base
.get("field")
.and_then(Json::as_str)
.map(str::to_owned);
for s in &t.slots {
let v = hit.get(slot_name(t.index, &s.name));
let absent = v.is_none_or(Json::is_null);
let data = || slot_data(o.k, o.template, o.row, &s.expression);
match &s.kind {
SlotKind::Identity { .. } | SlotKind::TargetsIdentity { .. } => {
let v = identity_value(v, s, o)?;
let (member, value) = match (v.as_str(), row_ref(v)) {
(Some(s), _) => (None, json!(s)),
(None, Some((id, _))) => (Some("id".to_owned()), json!(id)),
(None, None) => unreachable!("identity_value admitted it"),
};
let (target, written) = match &s.kind {
SlotKind::Identity { key, member } => (e.get_mut(key), member.clone()),
SlotKind::TargetsIdentity { k } => (
e.get_mut("targets")
.and_then(Json::as_array_mut)
.and_then(|a| a.get_mut(*k)),
"id".to_owned(),
),
_ => unreachable!("identity kinds"),
};
if let Some(obj) = target.and_then(Json::as_object_mut) {
obj.insert(member.unwrap_or(written), value);
}
}
SlotKind::WholeTarget { key, member } => {
let v = identity_value(v, s, o)?;
let (target, symbol) = whole_target(v, s, o, *member, &t.op, field.as_deref())?;
if symbol {
symbols.push(*member);
}
e[key] = target;
}
SlotKind::WholeTargets { k } => {
let v = identity_value(v, s, o)?;
let (target, symbol) = whole_target(v, s, o, Member::Targets(*k), &t.op, None)?;
if symbol {
symbols.push(Member::Targets(*k));
}
if let Some(slot) = e
.get_mut("targets")
.and_then(Json::as_array_mut)
.and_then(|a| a.get_mut(*k))
{
*slot = target;
}
}
SlotKind::Str { .. } | SlotKind::At { .. } => {
if absent {
return Err(refused_with(
format!(
"{}: the expression yielded absent for row {}",
s.here, o.row.0
),
data(),
));
}
let Some(text) = v.and_then(Json::as_str) else {
return Err(refused_with(
format!(
"{}: the expression yielded {}, expected a string",
s.here,
describe(v)
),
data(),
));
};
match &s.kind {
SlotKind::At { member } => e["at"] = json!({ member: text }),
SlotKind::Str { key } => e[key] = json!(text),
_ => unreachable!("string kinds"),
}
}
SlotKind::Value | SlotKind::Frontmatter { .. } => {
if absent {
return Err(refused_with(
format!(
"{}: the expression yielded absent for row {}{}",
s.here,
o.row.0,
if s.kind == SlotKind::Value {
" — use `unset` to remove a key"
} else {
""
}
),
data(),
));
}
let v = v.expect("not absent").clone();
match &s.kind {
SlotKind::Frontmatter { key } => {
if let Some(fm) = e.get_mut("frontmatter").and_then(Json::as_object_mut) {
fm.insert(key.clone(), v);
}
}
_ => e["value"] = v,
}
}
}
}
Effect::validate(&e, o.k).map_err(shape_error)?;
Ok((e, symbols))
}
pub type Settle<'a> = dyn FnMut(&mut Json, &[Member], &[Effect]) + 'a;
pub fn no_settle(_: &mut Json, _: &[Member], _: &[Effect]) {}
pub fn settle_symbols(
store: &mut Store,
ctx: &PlanContext,
doc_store: &dyn DocStore,
e: &mut Json,
members: &[Member],
prefix: &[Effect],
) {
if prefix.is_empty() {
return;
}
let Ok(plan) = store.plan_selected(ctx, prefix, None, doc_store) else {
return;
};
for m in members {
let slot = match m {
Member::Target => e.get_mut("target"),
Member::Into => e.get_mut("into"),
Member::Targets(k) => e
.get_mut("targets")
.and_then(Json::as_array_mut)
.and_then(|a| a.get_mut(*k)),
};
let Some(obj) = slot.and_then(Json::as_object_mut) else {
continue;
};
let Some(n) = obj
.get("id")
.and_then(Json::as_str)
.and_then(omgbase_mutate::parse_new_symbol)
else {
continue;
};
if let Some(x) = plan.new_ids.get(n) {
obj.insert("kind".to_owned(), json!(x.kind.as_str()));
}
}
}
fn finish(
e: &mut Json,
k: usize,
symbols: &[Member],
prefix: &[Effect],
settle: &mut Settle<'_>,
first: Effect,
) -> Result<Effect> {
if symbols.is_empty() || prefix.is_empty() {
return Ok(first);
}
settle(e, symbols, prefix);
Effect::validate(e, k).map_err(|m| {
let mut err = shape_error(m);
if let Some(d) = err.data.as_mut().and_then(Json::as_object_mut) {
d.insert("effect_index".to_owned(), json!(k));
}
err
})
}
fn row_of(hit: &Json) -> Row {
(
hit.get(HIT_ID)
.and_then(Json::as_str)
.unwrap_or_default()
.to_owned(),
hit.get(HIT_PATH)
.and_then(Json::as_str)
.unwrap_or_default()
.to_owned(),
)
}
#[derive(Clone, Debug, Default)]
pub struct Bound {
pub effects: Vec<Effect>,
pub rows: Vec<Row>,
}
pub fn bind(c: &Compiled, hits: &[Json], settle: &mut Settle<'_>) -> Result<Bound> {
let mut b = Bound::default();
for hit in hits {
let row = row_of(hit);
for t in &c.templates {
let k = b.effects.len();
let o = Origin {
k,
template: Some(t.index),
row: &row,
root: c.root,
request: Some(&c.request),
};
let (mut e, symbols) = bind_one(c.root, hit, t, &o)?;
let first = Effect::validate(&e, k).map_err(shape_error)?;
let effect = finish(&mut e, k, &symbols, &b.effects, settle, first)?;
b.effects.push(effect);
}
b.rows.push(row);
}
Ok(b)
}
fn too_large(truncated: bool, hits: usize, c: &Compiled, count: usize) -> SurfaceError {
let mut data = json!({
"cardinality": if truncated { json!(format!(">{}", c.bound)) } else { json!(hits) },
"max": c.bound,
});
if c.implicit() {
data["implicit"] = json!(true);
}
data["fix"] = json!(fixes::bounded(&c.request, count));
let message = format!(
"the selection has {} hits, over the bound of {}{} — narrow the selector or raise `expect.max`",
if truncated {
format!("more than {}", c.bound)
} else {
hits.to_string()
},
c.bound,
if c.implicit() {
" (the implicit ceiling; pass expect.max to raise it)"
} else {
""
}
);
MutationError::with_data(ErrorCode::SelectionTooLarge, message, data).into()
}
#[derive(Clone, Debug)]
pub struct Expansion {
pub effects: Vec<Effect>,
pub selection: Option<Selection>,
pub rows: Vec<Row>,
pub templates: Option<Vec<Json>>,
}
pub fn expand(
store: &mut Store,
ctx: &PlanContext,
doc_store: &dyn DocStore,
req: &SelectRequest,
semantic: HashMap<String, SemanticVec>,
) -> Result<Expansion> {
if !req.has_selector() {
return expand_plain(store, ctx, doc_store, req);
}
let c = compile(req)?;
let vocabulary = names::VocabularyCache::default();
{
let own = Query {
select: c
.rewritten
.select
.iter()
.filter(|it| {
!RESERVED_ALIAS_PREFIXES
.iter()
.any(|p| it.name().starts_with(p))
})
.cloned()
.collect(),
..c.rewritten.clone()
};
names::check_names(
store,
&ctx.repo_id,
&own,
names::UnknownNames::Refuse,
&vocabulary,
)
.map_err(|e| refused(format!("select: {}", e.message)))?;
}
let opts = QueryOptions {
limit: Some(c.bound.saturating_add(1)),
cursor: None,
provider: None,
in_memory: false,
bare: false,
unknown_names: None,
vocabulary: Some(&vocabulary),
};
let res = query_ast(store, &ctx.repo_id, &c.rewritten, opts, semantic)?;
if res.truncated || res.hits.len() > c.bound {
let count = if res.truncated {
let unbounded = QueryOptions {
limit: Some(usize::MAX >> 2),
cursor: None,
provider: None,
in_memory: false,
bare: false,
unknown_names: None,
vocabulary: Some(&vocabulary),
};
query_ast(store, &ctx.repo_id, &c.rewritten, unbounded, HashMap::new())?
.hits
.len()
} else {
res.hits.len()
};
return Err(too_large(res.truncated, res.hits.len(), &c, count));
}
let mut settle = |e: &mut Json, m: &[Member], p: &[Effect]| {
settle_symbols(store, ctx, doc_store, e, m, p);
};
let b = bind(&c, &res.hits, &mut settle)?;
let selection = Selection {
query: req.select_str().to_owned(),
expect: c.expect,
templates: req.templates.as_array().cloned().unwrap_or_default(),
cardinality: b.rows.len(),
identities: b.rows.iter().map(|(id, _)| id.clone()).collect(),
};
Ok(Expansion {
effects: b.effects,
selection: Some(selection),
rows: b.rows,
templates: None,
})
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum Missing {
Doc(String),
Block(String),
Parent(String),
Node(String),
}
impl Missing {
fn error(&self) -> SurfaceError {
let (code, message, data) = match self {
Missing::Doc(id) => (
ErrorCode::DocMissing,
format!("doc {id} not found"),
json!({ "doc": id }),
),
Missing::Block(id) => (
ErrorCode::BlockMissing,
format!("block {id} not found"),
json!({ "block": id }),
),
Missing::Parent(id) => (
ErrorCode::ParentMissing,
format!("parent {id} not found"),
json!({}),
),
Missing::Node(id) => (
ErrorCode::BlockMissing,
format!("node {id} not found"),
json!({}),
),
};
MutationError::with_data(code, message, data).into()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct Scope {
root: Target,
id: String,
missing: Missing,
}
fn scope_of(
store: &Store,
repo_id: &str,
e: &Map<String, Json>,
i: usize,
sugar: &Desugared,
) -> Result<Scope> {
let here = format!("effects[{i}]");
let op = e.get("op").and_then(Json::as_str).unwrap_or_default();
let first = |kinds: &'static [&'static str]| -> Result<(String, Option<Json>, &'static [&'static str], Member)> {
let list = e.get("targets").and_then(Json::as_array);
if list.is_none_or(Vec::is_empty) {
Effect::validate(&Json::Object(e.clone()), i).map_err(shape_error)?;
}
Ok((
"targets[0]".to_owned(),
list.and_then(|l| l.first()).cloned(),
kinds,
Member::Targets(0),
))
};
const PLACEMENT: &[&str] = &["document", "section", "block"];
const DOCUMENT: &[&str] = &["document"];
const BLOCK: &[&str] = &["block"];
const SET: &[&str] = &["document", "block", "node"];
let target = |kinds: &'static [&'static str]| {
(
"target".to_owned(),
e.get("target").cloned(),
kinds,
Member::Target,
)
};
let mut placement = false;
let (key, v, kinds, member) = match op {
"create" => {
return Err(refused(format!(
"{here}: a `create` has no row — an expression on a create needs a selector (`select`)"
)));
}
"insert" => {
placement = true;
(
"into".to_owned(),
e.get("into").cloned(),
PLACEMENT,
Member::Into,
)
}
"set" => target(SET),
"unset" => target(DOCUMENT),
"move" => {
if e.contains_key("targets") || e.contains_key("into") {
first(BLOCK)?
} else {
target(DOCUMENT)
}
}
"remove" => {
if e.contains_key("targets") {
first(BLOCK)?
} else {
target(DOCUMENT)
}
}
"split" => target(BLOCK),
"merge" => first(BLOCK)?,
_ => {
Effect::validate(&Json::Object(e.clone()), i).map_err(shape_error)?;
return Err(shape_error(format!(
"{here}: `op` {} is not row-shaped",
Json::String(op.to_owned())
)));
}
};
let where_ = format!("{here}.{key}");
if v.as_ref().is_some_and(|v| {
oqx_of(v).is_some()
|| v.as_object().is_some_and(|o| {
o.get("id").is_some_and(|x| oqx_of(x).is_some())
|| o.get("path").is_some_and(|x| oqx_of(x).is_some())
})
}) {
return Err(needs_selector(&where_));
}
let kinds_text = kinds
.iter()
.map(|k| format!("\"{k}\""))
.collect::<Vec<_>>()
.join(" | ");
let Some(t) = v
.as_ref()
.and_then(Json::as_object)
.filter(|o| o.get("kind").is_some_and(Json::is_string))
else {
return Err(shape_error(format!(
"{where_}: a target is {{ kind: {kinds_text}, … }}"
)));
};
let kind = t["kind"].as_str().expect("checked");
let id_of = |key: &str| t.get(key).and_then(Json::as_str).map(str::to_owned);
if sugar.symbols.contains(&member)
&& let Some(sym) = id_of("id").filter(|id| omgbase_mutate::parse_new_symbol(id).is_some())
{
return Err(refused(format!(
"{where_}: {sym} has no row yet — it is created by this plan; use a literal target or a selector"
)));
}
if !kinds.contains(&kind) {
return Err(shape_error(format!(
"{where_}: `kind` must be one of {} (got \"{kind}\")",
kinds.join(", ")
)));
}
Ok(match kind {
"document" => {
let id = id_of("id");
let path = id_of("path");
if id.is_none() && path.is_none() {
return Err(shape_error(format!(
"{where_}: a document target needs `id` or `path`"
)));
}
let by_id = match &id {
Some(id) => {
let found = store.find_doc_by_ref(repo_id, id)?;
Some(found.ok_or_else(|| Missing::Doc(id.clone()).error())?)
}
None => None,
};
let by_path = match &path {
Some(path) => {
let found = store.find_doc_by_ref(repo_id, storage_path(path))?;
Some(found.ok_or_else(|| Missing::Doc(path.clone()).error())?)
}
None => None,
};
if let (Some(a), Some(b)) = (&by_id, &by_path)
&& a.doc_id != b.doc_id
{
return Err(shape_error(format!(
"id {} and path {} name different documents",
Json::String(id.clone().unwrap_or_default()),
Json::String(path.clone().unwrap_or_default())
)));
}
let info = by_id.or(by_path).expect("one of the two");
Scope {
root: Target::Docs,
id: info.doc_id.clone(),
missing: Missing::Doc(info.doc_id),
}
}
"block" => {
let id = id_of("id").unwrap_or_default();
Scope {
root: Target::Blocks,
id: id.clone(),
missing: if placement {
Missing::Parent(id)
} else {
Missing::Block(id)
},
}
}
"section" => {
let id = id_of("heading_id").unwrap_or_default();
Scope {
root: Target::Blocks,
id: id.clone(),
missing: Missing::Parent(id),
}
}
_ => {
let id = id_of("id").unwrap_or_default();
Scope {
root: Target::Nodes,
id: id.clone(),
missing: Missing::Node(id),
}
}
})
}
enum Plain {
Literal(Json, Vec<Member>),
Bound(Json, Vec<Member>),
}
pub fn expand_plain(
store: &mut Store,
ctx: &PlanContext,
doc_store: &dyn DocStore,
req: &SelectRequest,
) -> Result<Expansion> {
let raw = req
.templates
.as_array()
.ok_or_else(|| shape_error("`effects` must be an array of effects".to_owned()))?;
if raw.is_empty() {
return Err(shape_error(
"`effects` must name at least one effect".to_owned(),
));
}
let request = fixes::Request {
select: None,
effects: Json::Array(raw.clone()),
expect: None,
};
let mut desugared = Vec::with_capacity(raw.len());
for (i, v) in raw.iter().enumerate() {
desugared.push(desugar_effect(v, i, Some(&request))?);
}
let any_expression = desugared.iter().any(|(v, _)| contains_oqx(v));
let mut staged: Vec<Option<Plain>> = (0..raw.len()).map(|_| None).collect();
if any_expression {
struct Group {
scope: Scope,
members: Vec<Template>,
}
let mut groups: Vec<Group> = Vec::new();
for (i, (v, d)) in desugared.iter().enumerate() {
let Some(o) = v.as_object().filter(|_| contains_oqx(v)) else {
staged[i] = Some(Plain::Literal(v.clone(), d.symbols.clone()));
continue;
};
let scope = scope_of(store, &ctx.repo_id, o, i, d)?;
let lookup = parent_lookup(store);
let fix = shape_fix(&request, &lookup);
let t = admit_template(i, v, scope.root, true, d, Some(&fix))?;
if t.slots.is_empty() {
staged[i] = Some(Plain::Literal(v.clone(), d.symbols.clone()));
continue;
}
match groups.iter_mut().find(|g| g.scope == scope) {
Some(g) => g.members.push(t),
None => groups.push(Group {
scope,
members: vec![t],
}),
}
}
for g in &groups {
let src = format!(
"from {} where id == {}",
g.scope.root.as_str(),
Json::String(g.scope.id.clone())
);
let mut q = oqx::parse_string(&src)?;
for t in &g.members {
for s in &t.slots {
q.select
.push(renamed(&s.item, &slot_name(t.index, &s.name)));
}
}
let opts = QueryOptions {
limit: Some(2),
cursor: None,
provider: None,
in_memory: false,
bare: false,
unknown_names: None,
vocabulary: None,
};
let res = query_ast(store, &ctx.repo_id, &q, opts, HashMap::new())?;
let Some(hit) = res.hits.first() else {
return Err(g.scope.missing.error());
};
let row = row_of(hit);
for t in &g.members {
let o = Origin {
k: t.index,
template: None,
row: &row,
root: g.scope.root,
request: Some(&request),
};
let (e, symbols) = bind_one(g.scope.root, hit, t, &o)?;
staged[t.index] = Some(Plain::Bound(e, symbols));
}
}
} else {
for (i, (v, d)) in desugared.into_iter().enumerate() {
staged[i] = Some(Plain::Literal(v, d.symbols));
}
}
let mut effects: Vec<Effect> = Vec::with_capacity(raw.len());
for (i, p) in staged.into_iter().enumerate() {
let (mut e, symbols) = match p.expect("every effect staged") {
Plain::Literal(e, s) | Plain::Bound(e, s) => (e, s),
};
let first = {
let lookup = parent_lookup(store);
let fix = shape_fix(&request, &lookup);
Effect::validate_with(&e, i, Some(&fix)).map_err(shape_error_of)?
};
let mut settle = |e: &mut Json, m: &[Member], p: &[Effect]| {
settle_symbols(store, ctx, doc_store, e, m, p);
};
let effect = finish(&mut e, i, &symbols, &effects, &mut settle, first)?;
effects.push(effect);
}
Ok(Expansion {
effects,
selection: None,
rows: Vec::new(),
templates: any_expression.then(|| raw.clone()),
})
}
#[must_use]
pub fn annotate(mut e: SurfaceError, rows: &[Row], templates: usize) -> SurfaceError {
if templates == 0 {
return e;
}
if let Some(data) = e.data.as_mut().and_then(Json::as_object_mut)
&& let Some(k) = data
.get("effect_index")
.and_then(Json::as_u64)
.and_then(|k| usize::try_from(k).ok())
&& !data.contains_key("template_index")
&& let Some((id, path)) = rows.get(k / templates)
{
data.insert("template_index".to_owned(), json!(k % templates));
data.insert("row".to_owned(), json!({ "id": id, "path": path }));
}
e
}
pub fn plan_selected(
store: &mut Store,
doc_store: &dyn DocStore,
ctx: &PlanContext,
req: &SelectRequest,
semantic: HashMap<String, SemanticVec>,
) -> Result<omgbase_mutate::Plan> {
let x = expand(store, ctx, doc_store, req, semantic)?;
let templates = if req.has_selector() {
req.templates()
} else {
0
};
let concrete = x.effects.len();
let plan = store
.plan_selected(ctx, &x.effects, x.selection, doc_store)
.map_err(|e| annotate(SurfaceError::from(e), &x.rows, templates));
let mut plan = match plan {
Ok(plan) => plan,
Err(e) => {
let req_fix = fixes::Request {
select: req.has_selector().then(|| req.select.clone()),
effects: req.templates.clone(),
expect: req.expect.clone(),
};
let origins: Option<Vec<fixes::Origin>> = (templates > 0).then(|| {
(0..concrete)
.map(|k| fixes::Origin {
row_id: x
.rows
.get(k / templates)
.map(|(id, _)| id.clone())
.unwrap_or_default(),
template_index: k % templates,
})
.collect()
});
let allocated = |i: usize| -> Option<usize> {
if i == 0 {
return Some(0);
}
store
.plan_selected(ctx, &x.effects[..i.min(concrete)], None, doc_store)
.ok()
.map(|p| p.new_ids.len())
};
return Err(fixes::planning_fix(
&req_fix,
e,
origins.as_deref(),
allocated,
));
}
};
plan.templates = x.templates;
Ok(plan)
}
#[must_use]
pub fn unbounded_apply(req: &SelectRequest, cardinality: usize) -> SurfaceError {
refused_with(
UNBOUNDED,
json!({
"fix": fixes::bounded(
&fixes::Request {
select: Some(req.select.clone()),
effects: req.templates.clone(),
expect: None,
},
cardinality,
)
}),
)
}
const UNBOUNDED: &str = "`expect.max` is required on `apply` with `select` — bound the selection, or preview it with `plan`";
pub fn require_max(req: &SelectRequest) -> Result<()> {
if req.has_selector() && req.expect.is_none() {
return Err(refused(UNBOUNDED));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn req(select: &str, templates: Json, max: Option<usize>) -> SelectRequest {
SelectRequest {
select: json!(select),
templates,
expect: max.map(|max| json!({ "max": max })),
}
}
const ROLLUP: &str = "from docs where type == \"workstream\" and status == \"completed\"";
fn rollup_templates() -> Json {
json!([
{ "op": "insert", "into": { "kind": "document", "path": { "oqx": "project" } }, "at": "end",
"markdown": { "oqx": "\"- completed: [\" + title + \"](\" + path + \")\"" } },
{ "op": "set", "field": "frontmatter.rolled_up", "value": true },
])
}
fn hit(id: &str, path: &str, slots: Json) -> Json {
let mut h = json!({ "id": id, "path": path });
if let Some(o) = slots.as_object() {
for (k, v) in o {
h[k] = v.clone();
}
}
h
}
fn data(e: &SurfaceError) -> &Json {
e.data.as_ref().expect("data")
}
#[test]
fn the_rewrite_appends_slot_projections_after_the_users_items() {
let c = compile(&req(ROLLUP, rollup_templates(), None)).unwrap();
assert_eq!(
oqx::print_query(&c.rewritten).unwrap(),
"select __omg_sel_0_into: project, __omg_sel_0_markdown: \"- completed: [\" + title + \"](\" + path + \")\" from docs where type == \"workstream\" and status == \"completed\""
);
assert_eq!(
c.templates[0].slot_names(),
["__omg_sel_0_into", "__omg_sel_0_markdown"]
);
assert!(c.templates[1].slot_names().is_empty());
assert_eq!(c.templates[1].default_target, Some(DefaultTarget::Target));
assert_eq!((c.bound, c.implicit()), (SELECTION_CEILING, true));
let c = compile(&req(
"select hub: project from docs",
json!([{ "op": "insert", "into": { "kind": "document", "path": { "oqx": "hub" } }, "markdown": "x" }]),
Some(5),
))
.unwrap();
assert_eq!(
oqx::print_query(&c.rewritten).unwrap(),
"select hub: project, __omg_sel_0_into: hub from docs"
);
assert_eq!(
oqx::print_query(&oqx::resolve_aliases(&c.rewritten).unwrap()).unwrap(),
"select hub: project, __omg_sel_0_into: project from docs"
);
assert_eq!((c.bound, c.implicit()), (5, false));
let c = compile(&req(
"from docs",
json!([{ "op": "set", "target": { "kind": "document", "id": { "oqx": "^docs single { id values where data.slug == ^hub_slug }" } }, "field": "frontmatter.x", "value": 1 }]),
None,
))
.unwrap();
assert!(
oqx::print_query(&c.rewritten)
.unwrap()
.starts_with("select __omg_sel_0_target: ^docs single {"),
);
}
#[test]
fn the_static_pass_refuses_what_cannot_be_a_selection() {
let t = json!([{ "op": "set", "field": "frontmatter.x", "value": 1 }]);
let refuse = |select: &str, templates: Json, max: Option<usize>| -> SurfaceError {
let e = compile(&req(select, templates, max)).unwrap_err();
assert_eq!(e.code, "filter_invalid", "{select}");
e
};
let selector: [(&str, &str); 11] = [
(
"docs count { }",
"select: `count` yields no rows — a selector's consumer is collect, first or single",
),
(
"select title values from docs",
"select: a selector yields rows, not `values`",
),
(
"from links",
"select: `links` rows have no effect target — select docs, blocks or nodes",
),
(
"from revisions where origin == \"api\"",
"select: `revisions` rows have no effect target — select docs, blocks or nodes",
),
(
"entries(docs) collect { }",
"select: a selector scans a root collection — `from docs`, `from blocks` or `from nodes`",
),
(
"from docs follow refs(\"a\")",
"select: `follow` is not allowed in a selector — a hit must be a row of the root",
),
(
"from docs offset 2",
"select: `offset` is not allowed — a selection is a set, not a page",
),
(
"select distinct type from docs",
"select: `distinct` is not allowed in a selector",
),
(
"select __omg_x: 1 from docs",
"select: projection `__omg_x` uses the reserved prefix `__omg_` — choose another alias",
),
(
"select __oqx_id: 1 from docs",
"select: projection `__oqx_id` uses the reserved prefix `__oqx_` — choose another alias",
),
(
"from docs where $repo",
"select: `$repo` was removed in surface 2.0 — reach the repository's collections through the root row: `^docs` from a top-level row (one caret per enclosing block, or the absolute `0^docs`), and the repository id as `^id` / `0^id`",
),
];
for (select, message) in selector {
let e = refuse(select, t.clone(), None);
assert_eq!(data(&e)["hint"], HINT, "{select}");
assert_eq!(e.message, message, "{select}");
}
let e = refuse("from docs where (", t.clone(), None);
assert!(e.message.starts_with("select: "), "{}", e.message);
assert_eq!(data(&e)["hint"], HINT);
for ok in [
"from docs where x exists { } order by title limit 3",
"docs first { where status == \"a\" }",
"docs single { }",
] {
compile(&req(ok, t.clone(), None)).unwrap_or_else(|e| panic!("{ok}: {e}"));
}
let e = compile(&SelectRequest {
select: json!(" "),
templates: t.clone(),
expect: None,
})
.unwrap_err();
assert_eq!(
e.message,
"`select` must be an OQX query (a non-empty string)"
);
let e = compile(&SelectRequest {
select: json!("from docs"),
templates: json!({}),
expect: None,
})
.unwrap_err();
assert_eq!(e.message, "`effects` must be an array of effect templates");
let e = refuse("from docs", json!([]), None);
assert_eq!(e.message, "`effects` must name at least one template");
for (expect, message) in [
(json!(3), "`expect` must be an object { max }"),
(json!({ "max": 1, "x": 2 }), "`expect`: unknown key `x`"),
(
json!({ "max": 1, "x": 2, "y": 3 }),
"`expect`: unknown keys `x`, `y`",
),
(json!({}), "`expect.max` must be a non-negative integer"),
(
json!({ "max": -1 }),
"`expect.max` must be a non-negative integer",
),
(
json!({ "max": 1.5 }),
"`expect.max` must be a non-negative integer",
),
] {
let e = compile(&SelectRequest {
select: json!("from docs"),
templates: t.clone(),
expect: Some(expect),
})
.unwrap_err();
assert_eq!(e.message, message);
}
let templates: [(Json, &str); 9] = [
(
json!([{ "op": "retarget", "repairs": [] }]),
"effects[0]: `retarget` is not row-shaped — it cannot be a selector template",
),
(
json!([{ "op": "merge", "separator": " " }]),
"effects[0]: `merge` needs `targets` — one hit cannot be merged with itself",
),
(
json!([{ "op": "nope" }]),
"effects[0]: `op` must be one of create/insert/set/unset/move/remove/split/merge (got \"nope\")",
),
(
json!(["x"]),
"effects[0]: a template is an effect object whose `op` is one of create/insert/set/unset/move/remove/split/merge",
),
(
json!([{ "op": "set", "field": { "oqx": "\"frontmatter.x\"" }, "value": 1 }]),
"effects[0].field: `{ oqx }` is not allowed here",
),
(
json!([{ "op": "set", "target": { "kind": "document", "id": { "oqx": "a" }, "path": { "oqx": "b" } }, "field": "frontmatter.x", "value": 1 }]),
"effects[0].target: give `id` or `path` as an expression, not both",
),
(
json!([{ "op": "set", "target": { "kind": { "oqx": "k" }, "id": "d_1" }, "field": "frontmatter.x", "value": 1 }]),
"effects[0].target.kind: `{ oqx }` is not allowed here",
),
(
json!([{ "op": "remove", "targets": [{ "kind": "block", "path": { "oqx": "p" } }] }]),
"effects[0].targets[0]: a block target has `id`, not `path`",
),
(
json!([{ "op": "set", "field": "frontmatter.x", "value": { "oqx": "a, b" } }]),
"effects[0].value: `oqx` must be one expression over the hit, not a query",
),
];
for (templates, message) in templates {
let e = refuse("from docs", templates, None);
assert_eq!(e.message, message);
assert_eq!(data(&e)["hint"], HINT);
}
compile(&req(
"from docs",
json!([{ "op": "set", "field": "frontmatter.x", "value": { "k": { "oqx": "x" } } }]),
None,
))
.unwrap();
compile(&req("from docs", json!([{ "op": "create", "path": "n.md", "markdown": "x", "frontmatter": { "k": { "deep": { "oqx": "x" } } } }]), None)).unwrap();
let e = refuse(
"from nodes",
json!([{ "op": "insert", "markdown": "x" }]),
None,
);
assert_eq!(
e.message,
"effects[0].into: `kind` must be one of document, section, block (got \"node\")"
);
assert_eq!(data(&e)["hint"], "arguments");
let e = refuse("from docs", json!([{ "op": "split", "at": [1] }]), None);
assert_eq!(data(&e)["hint"], "arguments");
let e = refuse(
"from blocks",
json!([{ "op": "unset", "field": "frontmatter.x" }]),
None,
);
assert!(e.message.contains("(got \"block\")"), "{}", e.message);
let c = compile(&req(
"from blocks",
json!([{ "op": "remove", "targets": [{ "oqx": "x" }] }]),
None,
))
.unwrap();
assert_eq!(c.templates[0].slot_names(), ["__omg_sel_0_targets_0"]);
assert!(matches!(
c.templates[0].slots[0].kind,
SlotKind::WholeTargets { k: 0 }
));
let e = refuse(
"from docs",
json!([{ "op": "set", "field": "frontmatter.x", "value": { "oqx": "1 +" } }]),
None,
);
assert_eq!(
e.message,
"effects[0].value: unexpected 'from' — expected a value (at offset 4)"
);
let e = refuse(
"from docs",
json!([{ "op": "set", "field": "frontmatter.x", "value": { "oqx": "a from blocks" } }]),
None,
);
assert_eq!(
e.message,
"effects[0].value: duplicate `from` clause (at offset 14)"
);
}
#[test]
fn the_request_reads_expect_only_with_a_select() {
assert_eq!(
SelectRequest::from_args(&json!({ "effects": [] })).unwrap(),
None
);
let r = SelectRequest::from_args(
&json!({ "select": "from docs", "effects": [{ "op": "x" }], "expect": { "max": 3 } }),
)
.unwrap()
.unwrap();
assert_eq!(r.expect, Some(json!({ "max": 3 })));
assert_eq!(r.templates(), 1);
let e = SelectRequest::from_args(&json!({ "effects": [], "expect": { "max": 1 } }))
.unwrap_err();
assert_eq!(
e.message,
"`expect` bounds a selection — pass `select` with it"
);
assert!(require_max(&r.clone()).is_ok());
let e = require_max(&SelectRequest { expect: None, ..r }).unwrap_err();
assert_eq!(
e.message,
"`expect.max` is required on `apply` with `select` — bound the selection, or preview it with `plan`"
);
assert_eq!(data(&e)["hint"], HINT);
}
#[test]
fn binding_types_each_slot_and_names_the_row() {
let c = compile(&req(ROLLUP, rollup_templates(), Some(10))).unwrap();
let hits = vec" }),
),
hit(
"d_5",
"/workstreams/b.md",
json!({ "__omg_sel_0_into": { "id": "d_9", "path": "/projects/y.md" }, "__omg_sel_0_markdown": "- completed: [B](/workstreams/b.md)" }),
),
];
let Bound { effects, rows, .. } = bind(&c, &hits, &mut no_settle).unwrap();
assert_eq!(
rows,
[
("d_2".to_owned(), "/workstreams/a.md".to_owned()),
("d_5".to_owned(), "/workstreams/b.md".to_owned())
]
);
let json: Vec<Json> = effects.iter().map(Effect::to_json).collect();
assert_eq!(
json,
[
json!({ "op": "insert", "into": { "kind": "document", "path": "/projects/x.md" }, "at": "end", "markdown": "- completed: [A](/workstreams/a.md)" }),
json!({ "op": "set", "target": { "kind": "document", "id": "d_2" }, "field": "frontmatter.rolled_up", "value": true }),
json!({ "op": "insert", "into": { "kind": "document", "id": "d_9" }, "at": "end", "markdown": "- completed: [B](/workstreams/b.md)" }),
json!({ "op": "set", "target": { "kind": "document", "id": "d_5" }, "field": "frontmatter.rolled_up", "value": true }),
]
);
for absent in [
json!({}),
json!({ "__omg_sel_0_into": null }),
json!({ "__omg_sel_0_into": [] }),
] {
let mut h = hit("d_2", "/w.md", absent);
h["__omg_sel_0_markdown"] = json!("x");
let e = bind(&c, &[h], &mut no_settle).unwrap_err();
assert_eq!(e.code, "target_missing");
assert_eq!(
e.message,
"effects[0].into.path: the expression yielded no identity for row d_2"
);
assert_eq!(
*data(&e),
json!({ "effect_index": 0, "template_index": 0, "row": { "id": "d_2", "path": "/w.md" }, "expression": "project" })
);
}
let h = hit(
"d_2",
"/w.md",
json!({ "__omg_sel_0_into": ["/a.md"], "__omg_sel_0_markdown": "x" }),
);
let e = bind(&c, &[h], &mut no_settle).unwrap_err();
assert_eq!(e.code, "ambiguous_target");
assert_eq!(
e.message,
"effects[0].into.path: the expression yielded 1 identities for row d_2; a target resolves to exactly one"
);
assert_eq!(data(&e)["count"], 1);
assert_eq!(data(&e)["expression"], "project");
let h = hit(
"d_2",
"/w.md",
json!({ "__omg_sel_0_into": 3, "__omg_sel_0_markdown": "x" }),
);
let e = bind(&c, &[h], &mut no_settle).unwrap_err();
assert_eq!(e.code, "filter_invalid");
assert_eq!(
e.message,
"effects[0].into.path: the expression yielded a number (3), not an identity (a string or a row)"
);
assert_eq!(
*data(&e),
json!({ "reason": e.message, "hint": HINT, "effect_index": 0, "template_index": 0, "row": { "id": "d_2", "path": "/w.md" }, "expression": "project" })
);
let h = hit(
"d_2",
"/w.md",
json!({ "__omg_sel_0_into": "/p.md", "__omg_sel_0_markdown": 7.5 }),
);
let e = bind(&c, &[h], &mut no_settle).unwrap_err();
assert_eq!(
e.message,
"effects[0].markdown: the expression yielded a number (7.5), expected a string"
);
let h = hit("d_2", "/w.md", json!({ "__omg_sel_0_into": "/p.md" }));
let e = bind(&c, &[h], &mut no_settle).unwrap_err();
assert_eq!(
e.message,
"effects[0].markdown: the expression yielded absent for row d_2"
);
let c = compile(&req("from docs", json!([{ "op": "set", "field": "frontmatter.x", "value": 1 }, { "op": "insert", "into": { "kind": "document", "path": { "oqx": "p" } }, "markdown": "x" }]), None)).unwrap();
let e = bind(
&c,
&[hit(
"d_1",
"/a.md",
json!({ "__omg_sel_1_into": { "id": "d_2", "path": "/p.md", "extra": 1 } }),
)],
&mut no_settle,
)
.unwrap_err();
assert_eq!(
e.message,
"effects[1].into.path: the expression yielded an object, not an identity (a string or a row)"
);
assert_eq!(data(&e)["effect_index"], 1);
assert_eq!(data(&e)["template_index"], 1);
}
#[test]
fn binding_the_hit_by_kind_and_the_other_slots() {
let c = compile(&req(
"from blocks",
json!([
{ "op": "move", "into": { "kind": "block", "id": { "oqx": "parent" } }, "at": { "after": { "oqx": "anchor" } } },
{ "op": "set", "field": "attrs.checked", "value": { "oqx": "done" } },
{ "op": "remove", "targets": [{ "kind": "block", "id": { "oqx": "sibling" } }, { "kind": "block", "id": { "oqx": "other" } }] },
{ "op": "remove" },
{ "op": "merge", "targets": [{ "kind": "block", "id": { "oqx": "a" } }, { "kind": "block", "id": "b_5" }], "separator": { "oqx": "sep" } },
{ "op": "split", "at": [2] },
]),
Some(1),
))
.unwrap();
assert_eq!(c.templates[0].default_target, Some(DefaultTarget::Targets));
assert_eq!(c.templates[1].default_target, Some(DefaultTarget::Target));
assert_eq!(c.templates[2].default_target, None);
assert_eq!(c.templates[3].default_target, Some(DefaultTarget::Targets));
assert_eq!(c.templates[5].default_target, Some(DefaultTarget::Target));
assert_eq!(
c.templates
.iter()
.flat_map(Template::slot_names)
.collect::<Vec<_>>(),
[
"__omg_sel_0_into",
"__omg_sel_0_at",
"__omg_sel_1_value",
"__omg_sel_2_targets_0",
"__omg_sel_2_targets_1",
"__omg_sel_4_targets_0",
"__omg_sel_4_separator",
]
);
let h = hit(
"b_1",
"/a.md",
json!({
"__omg_sel_0_into": { "id": "b_9", "path": "/a.md" }, "__omg_sel_0_at": "b_8",
"__omg_sel_1_value": false,
"__omg_sel_2_targets_0": "b_2", "__omg_sel_2_targets_1": { "id": "b_3", "path": "/a.md" },
"__omg_sel_4_targets_0": "b_4", "__omg_sel_4_separator": " / ",
}),
);
let Bound { effects, .. } = bind(&c, &[h], &mut no_settle).unwrap();
let json: Vec<Json> = effects.iter().map(Effect::to_json).collect();
assert_eq!(
json[0],
json!({ "op": "move", "targets": [{ "kind": "block", "id": "b_1" }], "into": { "kind": "block", "id": "b_9" }, "at": { "after": "b_8" } })
);
assert_eq!(
json[1],
json!({ "op": "set", "target": { "kind": "block", "id": "b_1" }, "field": "attrs.checked", "value": false })
);
assert_eq!(
json[2],
json!({ "op": "remove", "targets": [{ "kind": "block", "id": "b_2" }, { "kind": "block", "id": "b_3" }] })
);
assert_eq!(
json[3],
json!({ "op": "remove", "targets": [{ "kind": "block", "id": "b_1" }] })
);
assert_eq!(
json[4],
json!({ "op": "merge", "targets": [{ "kind": "block", "id": "b_4" }, { "kind": "block", "id": "b_5" }], "separator": " / " })
);
assert_eq!(
json[5],
json!({ "op": "split", "target": { "kind": "block", "id": "b_1" }, "at": [2] })
);
let c = compile(&req("from docs", json!([{ "op": "remove" }, { "op": "move", "to": { "oqx": "\"archive/\" + title + \".md\"" } }]), None)).unwrap();
assert_eq!(c.templates[0].default_target, Some(DefaultTarget::Target));
assert_eq!(c.templates[1].slot_names(), ["__omg_sel_1_to"]);
let Bound { effects, .. } = bind(
&c,
&[hit(
"d_1",
"/a.md",
json!({ "__omg_sel_1_to": "archive/A.md" }),
)],
&mut no_settle,
)
.unwrap();
assert_eq!(
effects[0].to_json(),
json!({ "op": "remove", "target": { "kind": "document", "id": "d_1" } })
);
assert_eq!(
effects[1].to_json(),
json!({ "op": "move", "target": { "kind": "document", "id": "d_1" }, "to": "archive/A.md", "retarget_inbound": true })
);
let c = compile(&req(
"from docs",
json!([{ "op": "set", "field": "frontmatter.hub", "value": { "oqx": "hub" } }]),
None,
))
.unwrap();
let e = bind(&c, &[hit("d_1", "/a.md", json!({}))], &mut no_settle).unwrap_err();
assert_eq!(
e.message,
"effects[0].value: the expression yielded absent for row d_1 — use `unset` to remove a key"
);
let Bound { effects, .. } = bind(
&c,
&[hit(
"d_1",
"/a.md",
json!({ "__omg_sel_0_value": { "id": "d_2", "path": "/p.md" } }),
)],
&mut no_settle,
)
.unwrap();
assert_eq!(
effects[0].to_json()["value"],
json!({ "id": "d_2", "path": "/p.md" })
);
let c = compile(&req(
"from docs",
json!([{ "op": "create", "path": { "oqx": "\"/notes/\" + slug + \".md\"" }, "markdown": { "oqx": "\"# \" + title" }, "frontmatter": { "source": { "oqx": "path" }, "layer": "draft" } }]),
None,
))
.unwrap();
assert_eq!(
c.templates[0].slot_names(),
[
"__omg_sel_0_path",
"__omg_sel_0_markdown",
"__omg_sel_0_fm_0"
]
);
let Bound { effects, .. } = bind(&c, &[hit("d_1", "/a.md", json!({ "__omg_sel_0_path": "/notes/a.md", "__omg_sel_0_markdown": "# A", "__omg_sel_0_fm_0": "/a.md" }))], &mut no_settle).unwrap();
assert_eq!(
effects[0].to_json(),
json!({ "op": "create", "path": "/notes/a.md", "markdown": "# A", "frontmatter": { "layer": "draft", "source": "/a.md" } })
);
let e = bind(
&c,
&[hit(
"d_1",
"/a.md",
json!({ "__omg_sel_0_path": "/notes/a.md", "__omg_sel_0_markdown": "# A" }),
)],
&mut no_settle,
)
.unwrap_err();
assert_eq!(
e.message,
"effects[0].frontmatter.source: the expression yielded absent for row d_1"
);
let Bound { effects, rows, .. } = bind(&c, &[], &mut no_settle).unwrap();
assert!(effects.is_empty() && rows.is_empty());
}
#[test]
fn lowering_errors_gain_the_template_and_the_row() {
let rows = vec![
("d_1".to_owned(), "/a.md".to_owned()),
("d_2".to_owned(), "/b.md".to_owned()),
];
let e = SurfaceError::with_data(
"conflicting_effects",
"x",
json!({ "effects": [1, 3], "effect_index": 3 }),
);
let a = annotate(e, &rows, 2);
let d = a.data.unwrap();
assert_eq!(d["template_index"], 1);
assert_eq!(d["row"], json!({ "id": "d_2", "path": "/b.md" }));
let e = SurfaceError::with_data("doc_missing", "x", json!({ "doc": "d" }));
assert_eq!(annotate(e.clone(), &rows, 2), e);
let e = SurfaceError::new("filter_invalid", "x");
assert_eq!(annotate(e.clone(), &rows, 0), e);
}
#[test]
fn offsets_shift_back_by_the_wrapper() {
assert_eq!(
shift_offsets("unexpected ')' (at offset 20)", -16),
"unexpected ')' (at offset 4)"
);
assert_eq!(shift_offsets("no offset here", -16), "no offset here");
assert_eq!(shift_offsets("x (at offset 3)", -16), "x (at offset 0)");
assert_eq!(shift_offsets("x (at offset 3)", 3), "x (at offset 6)");
assert_eq!(describe(Some(&json!(2.0))), "a number (2)");
assert_eq!(describe(Some(&json!([1, 2]))), "a list of 2");
assert_eq!(describe(Some(&json!("q"))), "a string (\"q\")");
assert_eq!(describe(None), "absent");
}
fn desugar(v: Json) -> Result<Json> {
desugar_effect(&v, 0, None).map(|(v, _)| v)
}
#[test]
fn the_leader_is_exact_and_everything_else_is_literal() {
for literal in ["->x", " -> x", "-->", "->", "a -> b", "${HOME}", "x"] {
let e = json!({ "op": "insert", "into": { "kind": "document", "path": "hub.md" }, "markdown": literal });
assert_eq!(desugar(e.clone()).unwrap(), e, "{literal}");
assert!(!needs_sugar(&json!([e])), "{literal}");
}
assert_eq!(
desugar(json!({ "op": "insert", "into": { "kind": "document", "path": "hub.md" }, "markdown": "-> \"-> note\"" })).unwrap(),
json!({ "op": "insert", "into": { "kind": "document", "path": "hub.md" }, "markdown": "-> note" })
);
assert_eq!(
desugar(json!({ "op": "insert", "into": { "kind": "document", "path": "hub.md" }, "markdown": "-> title" })).unwrap(),
json!({ "op": "insert", "into": { "kind": "document", "path": "hub.md" }, "markdown": { "oqx": "title" } })
);
assert_eq!(
desugar(json!({ "op": "set", "target": "d_0", "field": "frontmatter.priority", "value": "-> priority + 1" })).unwrap(),
json!({ "op": "set", "target": { "kind": "document", "id": "d_0" }, "field": "frontmatter.priority", "value": { "oqx": "priority + 1" } })
);
let nested = json!({ "op": "set", "target": "d_0", "field": "frontmatter.x", "value": { "a": "-> b" } });
assert_eq!(desugar(nested.clone()).unwrap()["value"], nested["value"]);
assert!(needs_sugar(
&json!([{ "op": "insert", "into": "hub.md", "markdown": "x" }])
));
assert!(needs_sugar(
&json!([{ "op": "insert", "into": { "kind": "document", "path": "hub.md" }, "markdown": "-> title" }])
));
assert!(needs_sugar(
&json!([{ "op": "remove", "targets": ["b_1"] }])
));
let plain = SelectRequest::from_args(&json!({ "effects": [{ "op": "insert", "into": { "kind": "document", "path": "hub.md" }, "markdown": "->x" }] })).unwrap();
assert!(plain.is_none());
let sugar = SelectRequest::from_args(
&json!({ "effects": [{ "op": "insert", "into": "hub.md", "markdown": "-> x" }] }),
)
.unwrap()
.unwrap();
assert!(!sugar.has_selector());
assert!(require_max(&sugar).is_ok());
}
#[test]
fn constant_folding_of_literal_leaders() {
assert_eq!(desugar_expression("\"x\"", "h").unwrap(), json!("x"));
assert_eq!(desugar_expression("`x`", "h").unwrap(), json!("x"));
assert_eq!(desugar_expression("`a\\`b`", "h").unwrap(), json!("a`b"));
assert_eq!(
desugar_expression("`${title} (${era})`", "h").unwrap(),
json!({ "oqx": "`${title} (${era})`" })
);
assert_eq!(desugar_expression("1", "h").unwrap(), json!({ "oqx": "1" }));
assert_eq!(
desugar_expression("\"a\" + \"b\"", "h").unwrap(),
json!({ "oqx": "\"a\" + \"b\"" })
);
assert_eq!(
desugar(json!({ "op": "insert", "into": "-> \"hub.md\"", "markdown": "x" })).unwrap(),
json!({ "op": "insert", "into": { "kind": "document", "path": "hub.md" }, "markdown": "x" })
);
let e =
desugar(json!({ "op": "insert", "into": "hub.md", "markdown": "-> 1 +" })).unwrap_err();
assert_eq!(e.code, "filter_invalid");
assert_eq!(data(&e)["hint"], HINT);
assert_eq!(
e.message,
"effects[0].markdown: unexpected 'from' — expected a value (at offset 7)"
);
let e =
desugar(json!({ "op": "insert", "into": "hub.md", "markdown": "-> a from blocks" }))
.unwrap_err();
assert!(e.message.ends_with("(at offset 17)"), "{}", e.message);
}
#[test]
fn bare_targets_normalize_before_the_shape_check() {
let d = |v: Json| desugar(v).unwrap();
assert_eq!(
d(json!({ "op": "insert", "into": "hub.md", "markdown": "x" }))["into"],
json!({ "kind": "document", "path": "hub.md" })
);
assert_eq!(
d(json!({ "op": "insert", "into": "/notes/a", "markdown": "x" }))["into"],
json!({ "kind": "document", "path": "/notes/a" })
);
assert_eq!(
d(json!({ "op": "insert", "into": "d_2", "markdown": "x" }))["into"],
json!({ "kind": "document", "id": "d_2" })
);
assert_eq!(
d(json!({ "op": "set", "target": "b_5", "field": "attrs.checked", "value": true }))["target"],
json!({ "kind": "block", "id": "b_5" })
);
assert_eq!(
d(json!({ "op": "set", "target": "n_0123456789ab", "field": "value", "value": 1 }))["target"],
json!({ "kind": "node", "id": "n_0123456789ab" })
);
assert_eq!(
d(json!({ "op": "remove", "targets": ["b_8", { "kind": "block", "id": "b_9" }] }))["targets"],
json!([{ "kind": "block", "id": "b_8" }, { "kind": "block", "id": "b_9" }])
);
let (v, sugar) = desugar_effect(
&json!({ "op": "insert", "into": "$new.0", "markdown": "x" }),
0,
None,
)
.unwrap();
assert_eq!(v["into"], json!({ "kind": "document", "id": "$new.0" }));
assert_eq!(sugar.symbols, [Member::Into]);
let (v, sugar) = desugar_effect(
&json!({ "op": "set", "target": "$new.2", "field": "markdown", "value": "x" }),
0,
None,
)
.unwrap();
assert_eq!(v["target"], json!({ "kind": "block", "id": "$new.2" }));
assert_eq!(sugar.symbols, [Member::Target]);
let (v, sugar) =
desugar_effect(&json!({ "op": "remove", "targets": ["$new.1"] }), 0, None).unwrap();
assert_eq!(v["targets"], json!([{ "kind": "block", "id": "$new.1" }]));
assert_eq!(sugar.symbols, [Member::Targets(0)]);
assert_eq!(
d(json!({ "op": "insert", "into": "-> project", "markdown": "x" }))["into"],
json!({ "oqx": "project" })
);
assert_eq!(
d(
json!({ "op": "insert", "into": { "kind": "document", "path": "-> project" }, "markdown": "x" })
)["into"],
json!({ "kind": "document", "path": { "oqx": "project" } })
);
let e = desugar(json!({ "op": "insert", "into": "hub.md#Status", "markdown": "x" }))
.unwrap_err();
assert_eq!(e.code, "filter_invalid");
assert_eq!(data(&e)["hint"], "arguments");
assert_eq!(data(&e)["fix"], crate::fixes::LOCATOR);
assert_eq!(
e.message,
"effects[0].into: \"hub.md#Status\" is a locator, not a target — a bare target is a document path, a `b_`/`d_`/`n_` id or `$new.n`"
);
let e = desugar(json!({ "op": "insert", "into": "hub", "markdown": "x" })).unwrap_err();
assert_eq!(data(&e)["hint"], "arguments");
assert_eq!(
e.message,
"effects[0].into: a target is { kind: …, … } or a bare document path, a `b_`/`d_`/`n_` id or `$new.n` (got \"hub\")"
);
let e = compile(&req(
"from docs",
json!([{ "op": "split", "target": "d_0", "at": [1] }]),
None,
))
.unwrap_err();
assert_eq!(
e.message,
"effects[0].target: `kind` must be one of block (got \"document\")"
);
assert_eq!(data(&e)["hint"], "arguments");
}
#[test]
fn a_leader_in_a_non_expression_slot_is_refused() {
let refuse = |v: Json| -> SurfaceError {
let e = desugar(v).unwrap_err();
assert_eq!(e.code, "filter_invalid");
assert_eq!(data(&e)["hint"], HINT);
e
};
for (v, here) in [
(
json!({ "op": "set", "target": "d_0", "field": "-> f", "value": 1 }),
"effects[0].field",
),
(
json!({ "op": "set", "target": "d_0", "field": "frontmatter.x", "value": 1, "expect": { "revision": "-> r" } }),
"effects[0].expect.revision",
),
(
json!({ "op": "insert", "into": { "kind": "section", "heading_id": "-> h" }, "markdown": "x" }),
"effects[0].into.heading_id",
),
(
json!({ "op": "insert", "into": "hub.md", "at": "-> end", "markdown": "x" }),
"effects[0].at",
),
(
json!({ "op": "insert", "into": "hub.md", "at": { "before": "b_1", "x": "-> y" }, "markdown": "x" }),
"effects[0].at.x",
),
(
json!({ "op": "split", "target": "b_1", "at": ["-> 2"] }),
"effects[0].at[0]",
),
(
json!({ "op": "set", "target": "d_0", "field": "frontmatter.x", "value": 1, "bogus": "-> z" }),
"effects[0].bogus",
),
(
json!({ "op": "retarget", "repairs": [{ "from": "-> a", "to": "b" }] }),
"effects[0].repairs[0].from",
),
] {
let e = refuse(v);
assert_eq!(
e.message,
format!("{here}: `-> ` is not allowed here — this member takes no expression")
);
}
for v in [
json!({ "op": "frobnicate", "x": "-> y" }),
json!({ "op": "-> set", "target": "d_0", "field": "frontmatter.x", "value": 1 }),
] {
assert_eq!(desugar(v.clone()).unwrap(), v);
let e = Effect::validate(&v, 0).map_err(shape_error).unwrap_err();
assert!(e.message.contains("`op` must be one of"), "{}", e.message);
assert_eq!(data(&e)["hint"], "arguments");
}
}
#[test]
fn binding_a_whole_target() {
let c = compile(&req(
"from docs",
json!([{ "op": "insert", "into": "-> project", "markdown": "x" }, { "op": "set", "target": "-> hub", "field": "frontmatter.x", "value": 1 }]),
None,
))
.unwrap();
let Bound { effects, .. } = bind(
&c,
&[hit("d_1", "/a.md", json!({ "__omg_sel_0_into": "hub.md", "__omg_sel_1_target": { "id": "d_7", "path": "/hub.md" } }))],
&mut no_settle,
)
.unwrap();
assert_eq!(
effects.iter().map(Effect::to_json).collect::<Vec<_>>(),
[
json!({ "op": "insert", "into": { "kind": "document", "path": "hub.md" }, "at": "end", "markdown": "x" }),
json!({ "op": "set", "target": { "kind": "document", "id": "d_7" }, "field": "frontmatter.x", "value": 1 }),
]
);
let Bound { effects, .. } = bind(
&c,
&[hit(
"d_1",
"/a.md",
json!({ "__omg_sel_0_into": "b_3", "__omg_sel_1_target": "d_2" }),
)],
&mut no_settle,
)
.unwrap();
assert_eq!(
effects[0].to_json()["into"],
json!({ "kind": "block", "id": "b_3" })
);
let e = bind(
&c,
&[hit(
"d_1",
"/a.md",
json!({ "__omg_sel_0_into": "hub", "__omg_sel_1_target": "d_2" }),
)],
&mut no_settle,
)
.unwrap_err();
assert_eq!(
e.message,
"effects[0].into: the expression yielded a string (\"hub\"), not an identity (a document path, an id or a row)"
);
assert_eq!(data(&e)["hint"], HINT);
assert_eq!(data(&e)["template_index"], 0);
let mut seen: Vec<(Vec<Member>, usize)> = Vec::new();
let mut settle =
|_: &mut Json, m: &[Member], p: &[Effect]| seen.push((m.to_vec(), p.len()));
let Bound { effects, .. } = bind(
&c,
&[hit(
"d_1",
"/a.md",
json!({ "__omg_sel_0_into": "$new.0", "__omg_sel_1_target": "d_2" }),
)],
&mut settle,
)
.unwrap();
assert_eq!(
effects[0].to_json()["into"],
json!({ "kind": "document", "id": "$new.0" })
);
assert!(seen.is_empty());
}
use omgbase_store::{Config, MemDocStore, SequentialMinter};
const TS: &str = "2026-10-13T10:00:00.000Z";
fn seeded() -> (Store, PlanContext, MemDocStore) {
let mut store =
Store::open_in_memory_with_minter(Box::new(SequentialMinter::new())).unwrap();
let repo = store.create_repo("fixture").unwrap();
for (path, source) in [
(
"hub.md",
"---\ntype: project\n---\n\n# Hub\n\n## Status\n\nNothing yet.\n",
),
(
"ws/alpha.md",
"---\ntype: workstream\nstatus: completed\nproject: hub.md\n---\n\n# Alpha\n",
),
] {
store
.observe_one(&repo, path, source, TS, &Config::default())
.unwrap();
}
let ctx = PlanContext {
repo_id: repo,
ts: "2026-10-13T10:01:00.000Z".to_owned(),
config: Config::default(),
};
(store, ctx, MemDocStore::new())
}
fn plan_plain(
store: &mut Store,
ctx: &PlanContext,
ds: &MemDocStore,
effects: Json,
) -> Result<omgbase_mutate::Plan> {
let req = SelectRequest::from_args(&json!({ "effects": effects }))
.unwrap()
.expect("sugar");
plan_selected(store, ds, ctx, &req, HashMap::new())
}
#[test]
fn the_selector_less_path_evaluates_in_the_targets_row() {
let (mut store, ctx, ds) = seeded();
let plan = plan_plain(
&mut store,
&ctx,
&ds,
json!([
{ "op": "insert", "into": "hub.md", "markdown": "-> `## ${title} (${type})`" },
{ "op": "set", "target": "/ws/alpha.md", "field": "frontmatter.hub", "value": "-> project" },
{ "op": "set", "target": "d_1", "field": "frontmatter.n", "value": "-> 1 + 1" },
]),
)
.unwrap();
assert!(plan.selection.is_none());
assert_eq!(plan.planner_v, "p3");
let effects: Vec<Json> = plan.effects.iter().map(Effect::to_json).collect();
assert_eq!(effects[0]["markdown"], "## Hub (project)");
assert_eq!(effects[1]["value"], "hub.md");
assert_eq!(
effects[1]["target"],
json!({ "kind": "document", "id": "d_1", "path": "ws/alpha.md" })
);
assert_eq!(effects[2]["value"], 2);
let templates = plan.templates.clone().expect("templates");
assert_eq!(templates[0]["markdown"], "-> `## ${title} (${type})`");
assert_eq!(templates[1]["target"], "/ws/alpha.md");
let wire = plan.to_json(false);
assert!(wire.get("templates").is_some());
assert!(plan.core_json().get("templates").is_none());
let literal = store
.plan_effects(
&ctx,
&omgbase_mutate::Effect::list_from_json(&json!([
{ "op": "insert", "into": { "kind": "document", "path": "hub.md" }, "markdown": "## Hub (project)" },
{ "op": "set", "target": { "kind": "document", "path": "ws/alpha.md" }, "field": "frontmatter.hub", "value": "hub.md" },
{ "op": "set", "target": { "kind": "document", "id": "d_1" }, "field": "frontmatter.n", "value": 2 },
]))
.unwrap(),
&ds,
)
.unwrap();
assert_eq!(literal.plan_id, plan.plan_id);
assert!(literal.templates.is_none());
let folded = plan_plain(
&mut store,
&ctx,
&ds,
json!([
{ "op": "insert", "into": "hub.md", "markdown": "-> \"## Hub (project)\"" },
{ "op": "set", "target": "/ws/alpha.md", "field": "frontmatter.hub", "value": "-> `hub.md`" },
{ "op": "set", "target": "d_1", "field": "frontmatter.n", "value": 2 },
]),
)
.unwrap();
assert_eq!(folded.plan_id, plan.plan_id);
assert!(folded.templates.is_none());
let record = omgbase_mutate::effects::intent_record(
&plan.plan_id,
&plan.effects,
None,
plan.templates.as_deref(),
);
assert!(
record.contains("\"templates\":[{\"into\":\"hub.md\""),
"{record}"
);
}
#[test]
fn the_selector_less_path_refuses_what_has_no_row() {
let (mut store, ctx, ds) = seeded();
let refuse = |store: &mut Store, effects: Json| -> SurfaceError {
plan_plain(store, &ctx, &ds, effects).unwrap_err()
};
let e = refuse(
&mut store,
json!([{ "op": "create", "path": "n.md", "markdown": "-> title" }]),
);
assert_eq!(e.code, "filter_invalid");
assert_eq!(data(&e)["hint"], HINT);
assert_eq!(
e.message,
"effects[0]: a `create` has no row — an expression on a create needs a selector (`select`)"
);
let plan = plan_plain(
&mut store,
&ctx,
&ds,
json!([{ "op": "create", "path": "n.md", "markdown": "-> \"# N\"" }]),
)
.unwrap();
assert_eq!(plan.effects[0].to_json()["markdown"], "# N");
assert!(plan.templates.is_none());
let e = refuse(
&mut store,
json!([{ "op": "insert", "into": "-> project", "markdown": "-> title" }]),
);
assert_eq!(
e.message,
"effects[0].into: an expression target needs a selector — without `select` there is no row to read it from"
);
let e = refuse(
&mut store,
json!([{ "op": "set", "target": { "kind": "document", "id": "-> x" }, "field": "frontmatter.a", "value": "-> title" }]),
);
assert!(
e.message
.starts_with("effects[0].target: an expression target needs a selector"),
"{}",
e.message
);
let e = refuse(
&mut store,
json!([
{ "op": "create", "path": "n.md", "markdown": "# N" },
{ "op": "insert", "into": "$new.0", "markdown": "-> title" },
]),
);
assert_eq!(
e.message,
"effects[1].into: $new.0 has no row yet — it is created by this plan; use a literal target or a selector"
);
let e = refuse(
&mut store,
json!([{ "op": "set", "target": "gone.md", "field": "frontmatter.a", "value": "-> title" }]),
);
assert_eq!(e.code, "doc_missing");
assert_eq!(data(&e)["doc"], "gone.md");
let e = refuse(
&mut store,
json!([{ "op": "insert", "into": "b_99", "markdown": "-> text" }]),
);
assert_eq!(e.code, "parent_missing");
let e = refuse(
&mut store,
json!([{ "op": "set", "target": "b_99", "field": "markdown", "value": "-> text" }]),
);
assert_eq!(e.code, "block_missing");
let e = refuse(
&mut store,
json!([{ "op": "insert", "into": "hub.md", "markdown": "-> nope" }]),
);
assert_eq!(
e.message,
"effects[0].markdown: the expression yielded absent for row d_0"
);
assert_eq!(
*data(&e),
json!({ "reason": e.message, "hint": HINT, "effect_index": 0, "row": { "id": "d_0", "path": "/hub.md" }, "expression": "nope" })
);
}
#[test]
fn bare_new_symbols_settle_against_the_prefix_plan() {
let (mut store, ctx, ds) = seeded();
let plan = plan_plain(
&mut store,
&ctx,
&ds,
json!([
{ "op": "create", "path": "n.md", "markdown": "# N\n\nBody." },
{ "op": "insert", "into": "$new.0", "markdown": "Tail." },
{ "op": "set", "target": "$new.2", "field": "markdown", "value": "Body, edited." },
{ "op": "remove", "targets": ["$new.1"] },
]),
)
.unwrap();
let effects: Vec<Json> = plan.effects.iter().map(Effect::to_json).collect();
assert_eq!(
effects[1]["into"],
json!({ "kind": "document", "id": "$new.0", "path": "n.md" })
);
assert_eq!(
effects[2]["target"],
json!({ "kind": "block", "id": "$new.2" })
);
assert_eq!(
effects[3]["targets"],
json!([{ "kind": "block", "id": "$new.1" }])
);
assert!(plan.templates.is_none());
let e = plan_plain(
&mut store,
&ctx,
&ds,
json!([
{ "op": "create", "path": "n.md", "markdown": "# N" },
{ "op": "unset", "target": "$new.1", "field": "frontmatter.k" },
]),
)
.unwrap_err();
assert_eq!(e.code, "filter_invalid");
assert_eq!(
e.message,
"effects[1].target: `kind` must be one of document (got \"block\")"
);
assert_eq!(data(&e)["effect_index"], 1);
assert_eq!(data(&e)["hint"], "arguments");
let e = plan_plain(
&mut store,
&ctx,
&ds,
json!([{ "op": "insert", "into": "$new.3", "markdown": "x" }]),
)
.unwrap_err();
assert_eq!(e.code, "target_missing");
assert_eq!(data(&e)["symbol"], "$new.3");
}
}