rename compiler NanoError and fix cluster config warnings

This commit is contained in:
aaltshuler 2026-06-17 23:44:24 +03:00
parent 5243c048aa
commit 4590c91f9d
17 changed files with 499 additions and 333 deletions

View file

@ -1050,7 +1050,7 @@ async fn main() -> Result<()> {
// The actor attributes graph-moving operations (sidecars,
// audit entries, engine schema-apply commits). Cluster FACTS
// stay unlayered; the operator's identity resolves --as flag
// first, then the per-operator omnigraph.yaml `cli.actor`.
// first, then per-operator config `operator.actor`.
let actor = resolve_cluster_actor(cli.as_actor.as_deref())?;
let output = apply_config_dir_with_options(config, ApplyOptions { actor }).await;
finish_cluster_apply(&output, json)?;
@ -1062,7 +1062,7 @@ async fn main() -> Result<()> {
} => {
let Some(approver) = resolve_cluster_actor(cli.as_actor.as_deref())? else {
bail!(
"`cluster approve` requires an approver: pass the global --as <ACTOR> flag or set `cli.actor` in your omnigraph.yaml — an approval without an approver is meaningless"
"`cluster approve` requires an approver: pass the global --as <ACTOR> flag or set `operator.actor` in ~/.omnigraph/config.yaml — an approval without an approver is meaningless"
);
};
let output = approve_config_dir(config, &resource, &approver).await;

View file

@ -796,6 +796,10 @@ fn cluster_approve_uses_operator_actor_fallback() {
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("--as"), "{stderr}");
assert!(stderr.contains("operator.actor"), "{stderr}");
assert!(stderr.contains("config.yaml"), "{stderr}");
assert!(!stderr.contains("cli.actor"), "{stderr}");
assert!(!stderr.contains("omnigraph.yaml"), "{stderr}");
}
#[test]

View file

@ -160,7 +160,7 @@ pub async fn plan_config_dir(config_dir: impl AsRef<Path>) -> PlanOutput {
// Plan is read-only: pending sidecars are reported, never acted on
// (RFC-004 open question 3 keeps read-only commands warn-only).
warn_pending_recovery_sidecars(&desired.config_dir, &mut diagnostics);
warn_pending_recovery_sidecars(&backend, &mut diagnostics).await;
let mut prior_resources = BTreeMap::new();
let mut prior_state: Option<ClusterState> = None;
@ -1260,7 +1260,7 @@ pub async fn status_config_dir(config_dir: impl AsRef<Path>) -> StatusOutput {
backend
.observe_lock(&mut observations, &mut diagnostics)
.await;
warn_pending_recovery_sidecars(&parsed.config_dir, &mut diagnostics);
warn_pending_recovery_sidecars(&backend, &mut diagnostics).await;
let mut resource_digests = BTreeMap::new();
let mut resource_statuses = BTreeMap::new();

View file

@ -321,6 +321,32 @@ impl ClusterStore {
// ---- recovery sidecars ----
pub(crate) async fn list_recovery_sidecar_locations(
&self,
diagnostics: &mut Vec<Diagnostic>,
) -> Vec<String> {
let dir_uri = self.uri(CLUSTER_RECOVERIES_DIR);
let mut uris = match self.adapter.list_dir(&dir_uri).await {
Ok(uris) => uris,
Err(err) => {
diagnostics.push(Diagnostic::warning(
"recovery_sidecar_read_error",
CLUSTER_RECOVERIES_DIR,
format!("could not list '{CLUSTER_RECOVERIES_DIR}': {err}"),
));
return Vec::new();
}
};
uris.retain(|uri| uri.ends_with(".json"));
uris.sort();
uris.into_iter()
.map(|uri| match uri.rsplit('/').next() {
Some(name) => format!("{}/{name}", self.display(CLUSTER_RECOVERIES_DIR)),
None => uri,
})
.collect()
}
pub(crate) async fn list_recovery_sidecars(
&self,
diagnostics: &mut Vec<Diagnostic>,

View file

@ -427,21 +427,14 @@ pub(crate) async fn mark_approvals_consumed(backend: &ClusterStore, approval_ids
}
/// Read-only commands report pending sidecars without acting on them.
pub(crate) fn warn_pending_recovery_sidecars(config_dir: &Path, diagnostics: &mut Vec<Diagnostic>) {
let recoveries_dir = config_dir.join(CLUSTER_RECOVERIES_DIR);
let Ok(entries) = fs::read_dir(&recoveries_dir) else {
return;
};
let mut names: Vec<String> = entries
.flatten()
.filter(|entry| entry.path().extension().is_some_and(|ext| ext == "json"))
.map(|entry| entry.file_name().to_string_lossy().into_owned())
.collect();
names.sort();
for name in names {
pub(crate) async fn warn_pending_recovery_sidecars(
backend: &ClusterStore,
diagnostics: &mut Vec<Diagnostic>,
) {
for location in backend.list_recovery_sidecar_locations(diagnostics).await {
diagnostics.push(Diagnostic::warning(
"cluster_recovery_pending",
format!("{CLUSTER_RECOVERIES_DIR}/{name}"),
location,
"a recovery sidecar from an interrupted apply is pending; the next state-mutating command will classify it",
));
}

View file

@ -3375,6 +3375,67 @@ policies:
);
}
#[tokio::test]
async fn read_only_commands_warn_on_pending_recovery_sidecar_in_storage_root() {
let dir = fixture();
let storage = tempfile::tempdir().unwrap();
let storage_path = storage.path().to_string_lossy().to_string();
let mut config = fs::read_to_string(dir.path().join(CLUSTER_CONFIG_FILE)).unwrap();
config = config.replace(
"version: 1\n",
&format!("version: 1\nstorage: {storage_path}\n"),
);
fs::write(dir.path().join(CLUSTER_CONFIG_FILE), config).unwrap();
let desired = validate_config_dir(dir.path());
assert!(desired.ok, "{:?}", desired.diagnostics);
let schema_digest = desired
.resource_digests
.get("schema.knowledge")
.unwrap()
.clone();
let graph_composite = graph_digest(
"knowledge",
Some(&schema_digest),
Some(&BTreeMap::new()),
None,
None,
);
write_state_resources(
storage.path(),
&[
("graph.knowledge", graph_composite.as_str()),
("schema.knowledge", schema_digest.as_str()),
],
);
write_create_sidecar(storage.path(), "knowledge", "irrelevant", "01STORAGE");
let status = status_config_dir(dir.path()).await;
assert!(status.ok, "{:?}", status.diagnostics);
assert!(
status
.diagnostics
.iter()
.any(|diagnostic| diagnostic.code == "cluster_recovery_pending"
&& diagnostic.path.contains("01STORAGE.json")),
"{:?}",
status.diagnostics
);
let plan = plan_config_dir(dir.path()).await;
assert!(plan.ok, "{:?}", plan.diagnostics);
assert!(
plan.diagnostics
.iter()
.any(|diagnostic| diagnostic.code == "cluster_recovery_pending"
&& diagnostic.path.contains("01STORAGE.json")),
"{:?}",
plan.diagnostics
);
assert!(!dir.path().join(CLUSTER_RECOVERIES_DIR).exists());
}
#[tokio::test]
async fn plan_annotates_apply_dispositions() {
let dir = fixture();

View file

@ -6,7 +6,7 @@ use std::sync::Arc;
use arrow_schema::{DataType, Field, Schema, SchemaRef};
use crate::error::{NanoError, Result};
use crate::error::{CompilerError, Result};
use crate::schema::ast::{Cardinality, Constraint, ConstraintBound, SchemaDecl, SchemaFile};
use crate::types::{PropType, ScalarType};
@ -151,7 +151,7 @@ pub fn build_catalog(schema: &SchemaFile) -> Result<Catalog> {
for decl in &schema.declarations {
if let SchemaDecl::Node(node) = decl {
if node_types.contains_key(&node.name) {
return Err(NanoError::Catalog(format!(
return Err(CompilerError::Catalog(format!(
"duplicate node type: {}",
node.name
)));
@ -250,19 +250,19 @@ pub fn build_catalog(schema: &SchemaFile) -> Result<Catalog> {
for decl in &schema.declarations {
if let SchemaDecl::Edge(edge) = decl {
if edge_types.contains_key(&edge.name) {
return Err(NanoError::Catalog(format!(
return Err(CompilerError::Catalog(format!(
"duplicate edge type: {}",
edge.name
)));
}
if !node_types.contains_key(&edge.from_type) {
return Err(NanoError::Catalog(format!(
return Err(CompilerError::Catalog(format!(
"edge {} references unknown source type: {}",
edge.name, edge.from_type
)));
}
if !node_types.contains_key(&edge.to_type) {
return Err(NanoError::Catalog(format!(
return Err(CompilerError::Catalog(format!(
"edge {} references unknown target type: {}",
edge.name, edge.to_type
)));
@ -302,7 +302,7 @@ pub fn build_catalog(schema: &SchemaFile) -> Result<Catalog> {
if let Some(existing) = edge_name_index.get(&normalized_name)
&& existing != &edge.name
{
return Err(NanoError::Catalog(format!(
return Err(CompilerError::Catalog(format!(
"edge name collision after case folding: '{}' conflicts with '{}'",
edge.name, existing
)));

View file

@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::catalog::{Catalog, build_catalog};
use crate::error::{NanoError, Result};
use crate::error::{CompilerError, Result};
use crate::schema::ast::{Annotation, Cardinality, Constraint, PropDecl, SchemaDecl, SchemaFile};
use crate::types::PropType;
@ -119,7 +119,7 @@ pub fn build_schema_ir(schema: &SchemaFile) -> Result<SchemaIR> {
pub fn build_catalog_from_ir(ir: &SchemaIR) -> Result<Catalog> {
if ir.ir_version != SCHEMA_IR_VERSION {
return Err(NanoError::Catalog(format!(
return Err(CompilerError::Catalog(format!(
"unsupported schema ir_version {} (expected {})",
ir.ir_version, SCHEMA_IR_VERSION
)));
@ -167,12 +167,12 @@ pub fn build_catalog_from_ir(ir: &SchemaIR) -> Result<Catalog> {
pub fn schema_ir_json(ir: &SchemaIR) -> Result<String> {
serde_json::to_string(ir)
.map_err(|err| NanoError::Catalog(format!("serialize schema ir error: {}", err)))
.map_err(|err| CompilerError::Catalog(format!("serialize schema ir error: {}", err)))
}
pub fn schema_ir_pretty_json(ir: &SchemaIR) -> Result<String> {
serde_json::to_string_pretty(ir)
.map_err(|err| NanoError::Catalog(format!("serialize schema ir error: {}", err)))
.map_err(|err| CompilerError::Catalog(format!("serialize schema ir error: {}", err)))
}
pub fn schema_ir_hash(ir: &SchemaIR) -> Result<String> {
@ -228,7 +228,7 @@ fn canonical_properties(
.map(|property| {
let prop_id = stable_prop_id(&owner_key, &property.name);
if let Some(previous) = seen_prop_ids.insert(prop_id, property.name.clone()) {
return Err(NanoError::Catalog(format!(
return Err(CompilerError::Catalog(format!(
"property id collision on {}: '{}' and '{}' both hash to {}",
owner_name, previous, property.name, prop_id
)));
@ -308,7 +308,7 @@ fn check_type_id_collision(
name: &str,
) -> Result<()> {
if let Some(previous) = seen_type_ids.insert(type_id, name.to_string()) {
return Err(NanoError::Catalog(format!(
return Err(CompilerError::Catalog(format!(
"type id collision: '{}' and '{}' both hash to {}",
previous, name, type_id
)));

View file

@ -55,7 +55,7 @@ pub fn decode_string_literal(raw: &str) -> Result<String> {
let escaped = chars
.next()
.ok_or_else(|| NanoError::Parse("unterminated escape sequence".to_string()))?;
.ok_or_else(|| CompilerError::Parse("unterminated escape sequence".to_string()))?;
match escaped {
'"' => decoded.push('"'),
'\\' => decoded.push('\\'),
@ -63,7 +63,7 @@ pub fn decode_string_literal(raw: &str) -> Result<String> {
'r' => decoded.push('\r'),
't' => decoded.push('\t'),
other => {
return Err(NanoError::Parse(format!(
return Err(CompilerError::Parse(format!(
"unsupported escape sequence: \\{}",
other
)));
@ -75,7 +75,7 @@ pub fn decode_string_literal(raw: &str) -> Result<String> {
}
#[derive(Debug, Error)]
pub enum NanoError {
pub enum CompilerError {
#[error("parse error: {0}")]
Parse(String),
@ -118,11 +118,16 @@ pub enum NanoError {
Manifest(String),
}
pub type Result<T> = std::result::Result<T, NanoError>;
#[deprecated(note = "use CompilerError")]
pub type NanoError = CompilerError;
pub type Result<T> = std::result::Result<T, CompilerError>;
#[cfg(test)]
mod tests {
use super::{SourceSpan, decode_string_literal, render_span};
use std::path::Path;
use super::{CompilerError, SourceSpan, decode_string_literal, render_span};
#[test]
fn source_span_preserves_zero_width() {
@ -143,4 +148,71 @@ mod tests {
let decoded = decode_string_literal("\"a\\n\\r\\t\\\\\\\"b\"").unwrap();
assert_eq!(decoded, "a\n\r\t\\\"b");
}
#[test]
fn compiler_error_parse_display_is_stable() {
let err = CompilerError::Parse("bad token".to_string());
assert_eq!(err.to_string(), "parse error: bad token");
}
#[allow(deprecated)]
#[test]
fn legacy_nano_error_alias_constructs_variants() {
let err = super::NanoError::Parse("bad token".to_string());
assert_eq!(err.to_string(), "parse error: bad token");
}
#[test]
fn legacy_name_is_confined_to_alias_and_compatibility_test() {
let legacy_name = ["Nano", "Error"].concat();
let workspace_root = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(Path::parent)
.expect("compiler crate should live under crates/");
let allowed_file = workspace_root.join("crates/omnigraph-compiler/src/error.rs");
let mut offenders = Vec::new();
visit_rs_files(&workspace_root.join("crates"), &mut |path| {
let text = std::fs::read_to_string(path).expect("source file should be readable");
let count = text.matches(&legacy_name).count();
if path == allowed_file {
if count != 2 {
offenders.push(format!(
"{} contains {count} legacy-name occurrences; expected exactly 2",
display_path(workspace_root, path)
));
}
} else if count > 0 {
offenders.push(format!(
"{} contains {count} legacy-name occurrence(s)",
display_path(workspace_root, path)
));
}
});
assert!(
offenders.is_empty(),
"legacy compiler error name should stay compatibility-only:\n{}",
offenders.join("\n")
);
}
fn visit_rs_files(dir: &Path, visit: &mut impl FnMut(&Path)) {
for entry in std::fs::read_dir(dir).expect("source directory should be readable") {
let entry = entry.expect("source entry should be readable");
let path = entry.path();
if path.is_dir() {
visit_rs_files(&path, visit);
} else if path.extension().and_then(|ext| ext.to_str()) == Some("rs") {
visit(&path);
}
}
}
fn display_path(root: &Path, path: &Path) -> String {
path.strip_prefix(root)
.unwrap_or(path)
.to_string_lossy()
.into_owned()
}
}

View file

@ -14,7 +14,7 @@ pub fn lower_query(
type_ctx: &TypeContext,
) -> Result<QueryIR> {
if !query.mutations.is_empty() {
return Err(crate::error::NanoError::Plan(
return Err(crate::error::CompilerError::Plan(
"cannot lower mutation query with read-query lowerer".to_string(),
));
}
@ -62,7 +62,7 @@ pub fn lower_query(
pub fn lower_mutation_query(query: &QueryDecl) -> Result<MutationIR> {
if query.mutations.is_empty() {
return Err(crate::error::NanoError::Plan(
return Err(crate::error::CompilerError::Plan(
"query does not contain a mutation body".to_string(),
));
}
@ -261,7 +261,7 @@ fn lower_clauses(
let edge = catalog
.lookup_edge_by_name(&traversal.edge_name)
.ok_or_else(|| {
crate::error::NanoError::Plan(format!(
crate::error::CompilerError::Plan(format!(
"lowering traversal referenced missing edge '{}' after typecheck",
traversal.edge_name
))

View file

@ -3,7 +3,7 @@ use pest::error::InputLocation;
use pest_derive::Parser;
use crate::error::{
NanoError, ParseDiagnostic, Result, SourceSpan, decode_string_literal, render_span,
CompilerError, ParseDiagnostic, Result, SourceSpan, decode_string_literal, render_span,
};
use super::ast::*;
@ -13,7 +13,7 @@ use super::ast::*;
struct QueryParser;
pub fn parse_query(input: &str) -> Result<QueryFile> {
parse_query_diagnostic(input).map_err(|e| NanoError::Parse(e.to_string()))
parse_query_diagnostic(input).map_err(|e| CompilerError::Parse(e.to_string()))
}
pub fn parse_query_diagnostic(input: &str) -> std::result::Result<QueryFile, ParseDiagnostic> {
@ -24,7 +24,7 @@ pub fn parse_query_diagnostic(input: &str) -> std::result::Result<QueryFile, Par
if let Rule::query_file = pair.as_rule() {
for inner in pair.into_inner() {
if let Rule::query_decl = inner.as_rule() {
queries.push(parse_query_decl(inner).map_err(nano_error_to_diagnostic)?);
queries.push(parse_query_decl(inner).map_err(compiler_error_to_diagnostic)?);
}
}
}
@ -40,7 +40,7 @@ fn pest_error_to_diagnostic(err: pest::error::Error<Rule>) -> ParseDiagnostic {
ParseDiagnostic::new(err.to_string(), span)
}
fn nano_error_to_diagnostic(err: NanoError) -> ParseDiagnostic {
fn compiler_error_to_diagnostic(err: CompilerError) -> ParseDiagnostic {
ParseDiagnostic::new(err.to_string(), None)
}
@ -71,7 +71,7 @@ fn parse_query_decl(pair: pest::iterators::Pair<Rule>) -> Result<QueryDecl> {
match annotation_name {
"description" => {
if description.replace(value).is_some() {
return Err(NanoError::Parse(format!(
return Err(CompilerError::Parse(format!(
"query `{}` cannot include duplicate @description annotations",
name
)));
@ -79,14 +79,14 @@ fn parse_query_decl(pair: pest::iterators::Pair<Rule>) -> Result<QueryDecl> {
}
"instruction" => {
if instruction.replace(value).is_some() {
return Err(NanoError::Parse(format!(
return Err(CompilerError::Parse(format!(
"query `{}` cannot include duplicate @instruction annotations",
name
)));
}
}
other => {
return Err(NanoError::Parse(format!(
return Err(CompilerError::Parse(format!(
"unsupported query annotation: @{}",
other
)));
@ -94,10 +94,9 @@ fn parse_query_decl(pair: pest::iterators::Pair<Rule>) -> Result<QueryDecl> {
}
}
Rule::query_body => {
let body = item
.into_inner()
.next()
.ok_or_else(|| NanoError::Parse("query body cannot be empty".to_string()))?;
let body = item.into_inner().next().ok_or_else(|| {
CompilerError::Parse("query body cannot be empty".to_string())
})?;
match body.as_rule() {
Rule::read_query_body => {
for section in body.into_inner() {
@ -127,7 +126,7 @@ fn parse_query_decl(pair: pest::iterators::Pair<Rule>) -> Result<QueryDecl> {
let int_pair = section.into_inner().next().unwrap();
limit =
Some(int_pair.as_str().parse::<u64>().map_err(|e| {
NanoError::Parse(format!("invalid limit: {}", e))
CompilerError::Parse(format!("invalid limit: {}", e))
})?);
}
_ => {}
@ -138,7 +137,7 @@ fn parse_query_decl(pair: pest::iterators::Pair<Rule>) -> Result<QueryDecl> {
for mutation_pair in body.into_inner() {
if let Rule::mutation_stmt = mutation_pair.as_rule() {
let stmt = mutation_pair.into_inner().next().ok_or_else(|| {
NanoError::Parse(
CompilerError::Parse(
"mutation statement cannot be empty".to_string(),
)
})?;
@ -170,14 +169,14 @@ fn parse_query_annotation(pair: pest::iterators::Pair<Rule>) -> Result<(&'static
let inner = pair
.into_inner()
.next()
.ok_or_else(|| NanoError::Parse("query annotation cannot be empty".to_string()))?;
.ok_or_else(|| CompilerError::Parse("query annotation cannot be empty".to_string()))?;
match inner.as_rule() {
Rule::description_annotation => {
let value = inner
.into_inner()
.next()
.ok_or_else(|| {
NanoError::Parse("@description requires a string literal".to_string())
CompilerError::Parse("@description requires a string literal".to_string())
})
.map(|value| parse_string_lit(value.as_str()))??;
Ok(("description", value))
@ -187,12 +186,12 @@ fn parse_query_annotation(pair: pest::iterators::Pair<Rule>) -> Result<(&'static
.into_inner()
.next()
.ok_or_else(|| {
NanoError::Parse("@instruction requires a string literal".to_string())
CompilerError::Parse("@instruction requires a string literal".to_string())
})
.map(|value| parse_string_lit(value.as_str()))??;
Ok(("instruction", value))
}
other => Err(NanoError::Parse(format!(
other => Err(CompilerError::Parse(format!(
"unexpected query annotation rule: {:?}",
other
))),
@ -208,30 +207,29 @@ fn parse_param(pair: pest::iterators::Pair<Rule>) -> Result<Param> {
let mut type_inner = type_ref.into_inner();
let core = type_inner
.next()
.ok_or_else(|| NanoError::Parse("parameter type is missing".to_string()))?;
let base = match core.as_rule() {
Rule::base_type => core.as_str().to_string(),
Rule::list_type => {
let inner = core
.into_inner()
.next()
.ok_or_else(|| NanoError::Parse("list type missing item type".to_string()))?;
format!("[{}]", inner.as_str().trim())
}
Rule::vector_type => {
let vector = core
.into_inner()
.next()
.ok_or_else(|| NanoError::Parse("Vector type missing dimension".to_string()))?;
format!("Vector({})", vector.as_str().trim())
}
other => {
return Err(NanoError::Parse(format!(
"unexpected param type rule: {:?}",
other
)));
}
};
.ok_or_else(|| CompilerError::Parse("parameter type is missing".to_string()))?;
let base =
match core.as_rule() {
Rule::base_type => core.as_str().to_string(),
Rule::list_type => {
let inner = core.into_inner().next().ok_or_else(|| {
CompilerError::Parse("list type missing item type".to_string())
})?;
format!("[{}]", inner.as_str().trim())
}
Rule::vector_type => {
let vector = core.into_inner().next().ok_or_else(|| {
CompilerError::Parse("Vector type missing dimension".to_string())
})?;
format!("Vector({})", vector.as_str().trim())
}
other => {
return Err(CompilerError::Parse(format!(
"unexpected param type rule: {:?}",
other
)));
}
};
Ok(Param {
name,
@ -256,7 +254,7 @@ fn parse_clause(pair: pest::iterators::Pair<Rule>) -> Result<Clause> {
}
Ok(Clause::Negation(clauses))
}
_ => Err(NanoError::Parse(format!(
_ => Err(CompilerError::Parse(format!(
"unexpected clause rule: {:?}",
inner.as_rule()
))),
@ -267,13 +265,13 @@ fn parse_text_search_clause(pair: pest::iterators::Pair<Rule>) -> Result<Clause>
let inner = pair
.into_inner()
.next()
.ok_or_else(|| NanoError::Parse("text search clause cannot be empty".to_string()))?;
.ok_or_else(|| CompilerError::Parse("text search clause cannot be empty".to_string()))?;
let expr = match inner.as_rule() {
Rule::search_call => parse_search_call(inner)?,
Rule::fuzzy_call => parse_fuzzy_call(inner)?,
Rule::match_text_call => parse_match_text_call(inner)?,
other => {
return Err(NanoError::Parse(format!(
return Err(CompilerError::Parse(format!(
"unexpected text search clause rule: {:?}",
other
)));
@ -325,7 +323,7 @@ fn parse_mutation_stmt(pair: pest::iterators::Pair<Rule>) -> Result<Mutation> {
Rule::insert_stmt => parse_insert_mutation(pair).map(Mutation::Insert),
Rule::update_stmt => parse_update_mutation(pair).map(Mutation::Update),
Rule::delete_stmt => parse_delete_mutation(pair).map(Mutation::Delete),
other => Err(NanoError::Parse(format!(
other => Err(CompilerError::Parse(format!(
"unexpected mutation statement rule: {:?}",
other
))),
@ -363,7 +361,7 @@ fn parse_update_mutation(pair: pest::iterators::Pair<Rule>) -> Result<UpdateMuta
}
let predicate = predicate.ok_or_else(|| {
NanoError::Parse("update mutation requires a where predicate".to_string())
CompilerError::Parse("update mutation requires a where predicate".to_string())
})?;
Ok(UpdateMutation {
@ -378,7 +376,9 @@ fn parse_delete_mutation(pair: pest::iterators::Pair<Rule>) -> Result<DeleteMuta
let type_name = inner.next().unwrap().as_str().to_string();
let predicate = inner
.next()
.ok_or_else(|| NanoError::Parse("delete mutation requires a where predicate".to_string()))
.ok_or_else(|| {
CompilerError::Parse("delete mutation requires a where predicate".to_string())
})
.and_then(parse_mutation_predicate)?;
Ok(DeleteMutation {
type_name,
@ -416,7 +416,7 @@ fn parse_match_value(pair: pest::iterators::Pair<Rule>) -> Result<MatchValue> {
}
Rule::now_call => Ok(MatchValue::Now),
Rule::literal => Ok(MatchValue::Literal(parse_literal(value_inner)?)),
_ => Err(NanoError::Parse(format!(
_ => Err(CompilerError::Parse(format!(
"unexpected match value: {:?}",
value_inner.as_rule()
))),
@ -436,9 +436,9 @@ fn parse_traversal(pair: pest::iterators::Pair<Rule>) -> Result<Traversal> {
let (min, max) = parse_traversal_bounds(next)?;
min_hops = min;
max_hops = max;
inner
.next()
.ok_or_else(|| NanoError::Parse("traversal missing destination variable".to_string()))?
inner.next().ok_or_else(|| {
CompilerError::Parse("traversal missing destination variable".to_string())
})?
} else {
next
};
@ -459,16 +459,16 @@ fn parse_traversal_bounds(pair: pest::iterators::Pair<Rule>) -> Result<(u32, Opt
let mut inner = pair.into_inner();
let min = inner
.next()
.ok_or_else(|| NanoError::Parse("traversal bound missing min hop".to_string()))?
.ok_or_else(|| CompilerError::Parse("traversal bound missing min hop".to_string()))?
.as_str()
.parse::<u32>()
.map_err(|e| NanoError::Parse(format!("invalid traversal min bound: {}", e)))?;
.map_err(|e| CompilerError::Parse(format!("invalid traversal min bound: {}", e)))?;
let max = inner
.next()
.map(|p| {
p.as_str()
.parse::<u32>()
.map_err(|e| NanoError::Parse(format!("invalid traversal max bound: {}", e)))
.map_err(|e| CompilerError::Parse(format!("invalid traversal max bound: {}", e)))
})
.transpose()?;
Ok((min, max))
@ -507,7 +507,12 @@ fn parse_expr(pair: pest::iterators::Pair<Rule>) -> Result<Expr> {
"avg" => AggFunc::Avg,
"min" => AggFunc::Min,
"max" => AggFunc::Max,
other => return Err(NanoError::Parse(format!("unknown aggregate: {}", other))),
other => {
return Err(CompilerError::Parse(format!(
"unknown aggregate: {}",
other
)));
}
};
let arg = parse_expr(parts.next().unwrap())?;
Ok(Expr::Aggregate {
@ -522,7 +527,7 @@ fn parse_expr(pair: pest::iterators::Pair<Rule>) -> Result<Expr> {
Rule::bm25_call => parse_bm25_call(inner),
Rule::rrf_call => parse_rrf_call(inner),
Rule::ident => Ok(Expr::AliasRef(inner.as_str().to_string())),
_ => Err(NanoError::Parse(format!(
_ => Err(CompilerError::Parse(format!(
"unexpected expr rule: {:?}",
inner.as_rule()
))),
@ -533,12 +538,12 @@ fn parse_search_call(pair: pest::iterators::Pair<Rule>) -> Result<Expr> {
let mut args = pair.into_inner();
let field = args
.next()
.ok_or_else(|| NanoError::Parse("search() missing field argument".to_string()))?;
.ok_or_else(|| CompilerError::Parse("search() missing field argument".to_string()))?;
let query = args
.next()
.ok_or_else(|| NanoError::Parse("search() missing query argument".to_string()))?;
.ok_or_else(|| CompilerError::Parse("search() missing query argument".to_string()))?;
if args.next().is_some() {
return Err(NanoError::Parse(
return Err(CompilerError::Parse(
"search() accepts exactly 2 arguments".to_string(),
));
}
@ -552,13 +557,13 @@ fn parse_fuzzy_call(pair: pest::iterators::Pair<Rule>) -> Result<Expr> {
let mut args = pair.into_inner();
let field = args
.next()
.ok_or_else(|| NanoError::Parse("fuzzy() missing field argument".to_string()))?;
.ok_or_else(|| CompilerError::Parse("fuzzy() missing field argument".to_string()))?;
let query = args
.next()
.ok_or_else(|| NanoError::Parse("fuzzy() missing query argument".to_string()))?;
.ok_or_else(|| CompilerError::Parse("fuzzy() missing query argument".to_string()))?;
let max_edits = args.next().map(parse_expr).transpose()?.map(Box::new);
if args.next().is_some() {
return Err(NanoError::Parse(
return Err(CompilerError::Parse(
"fuzzy() accepts at most 3 arguments".to_string(),
));
}
@ -573,12 +578,12 @@ fn parse_match_text_call(pair: pest::iterators::Pair<Rule>) -> Result<Expr> {
let mut args = pair.into_inner();
let field = args
.next()
.ok_or_else(|| NanoError::Parse("match_text() missing field argument".to_string()))?;
.ok_or_else(|| CompilerError::Parse("match_text() missing field argument".to_string()))?;
let query = args
.next()
.ok_or_else(|| NanoError::Parse("match_text() missing query argument".to_string()))?;
.ok_or_else(|| CompilerError::Parse("match_text() missing query argument".to_string()))?;
if args.next().is_some() {
return Err(NanoError::Parse(
return Err(CompilerError::Parse(
"match_text() accepts exactly 2 arguments".to_string(),
));
}
@ -592,12 +597,12 @@ fn parse_bm25_call(pair: pest::iterators::Pair<Rule>) -> Result<Expr> {
let mut args = pair.into_inner();
let field = args
.next()
.ok_or_else(|| NanoError::Parse("bm25() missing field argument".to_string()))?;
.ok_or_else(|| CompilerError::Parse("bm25() missing field argument".to_string()))?;
let query = args
.next()
.ok_or_else(|| NanoError::Parse("bm25() missing query argument".to_string()))?;
.ok_or_else(|| CompilerError::Parse("bm25() missing query argument".to_string()))?;
if args.next().is_some() {
return Err(NanoError::Parse(
return Err(CompilerError::Parse(
"bm25() accepts exactly 2 arguments".to_string(),
));
}
@ -611,14 +616,14 @@ fn parse_rank_expr(pair: pest::iterators::Pair<Rule>) -> Result<Expr> {
let inner = if pair.as_rule() == Rule::rank_expr {
pair.into_inner()
.next()
.ok_or_else(|| NanoError::Parse("rank expression cannot be empty".to_string()))?
.ok_or_else(|| CompilerError::Parse("rank expression cannot be empty".to_string()))?
} else {
pair
};
match inner.as_rule() {
Rule::nearest_ordering => parse_nearest_ordering(inner),
Rule::bm25_call => parse_bm25_call(inner),
other => Err(NanoError::Parse(format!(
other => Err(CompilerError::Parse(format!(
"rrf() rank expression must be nearest(...) or bm25(...), got {:?}",
other
))),
@ -629,13 +634,13 @@ fn parse_rrf_call(pair: pest::iterators::Pair<Rule>) -> Result<Expr> {
let mut args = pair.into_inner();
let primary = args
.next()
.ok_or_else(|| NanoError::Parse("rrf() missing primary rank expression".to_string()))?;
let secondary = args
.next()
.ok_or_else(|| NanoError::Parse("rrf() missing secondary rank expression".to_string()))?;
.ok_or_else(|| CompilerError::Parse("rrf() missing primary rank expression".to_string()))?;
let secondary = args.next().ok_or_else(|| {
CompilerError::Parse("rrf() missing secondary rank expression".to_string())
})?;
let k = args.next().map(parse_expr).transpose()?.map(Box::new);
if args.next().is_some() {
return Err(NanoError::Parse(
return Err(CompilerError::Parse(
"rrf() accepts at most 3 arguments".to_string(),
));
}
@ -654,7 +659,7 @@ fn parse_comp_op(pair: pest::iterators::Pair<Rule>) -> Result<CompOp> {
"<" => Ok(CompOp::Lt),
">=" => Ok(CompOp::Ge),
"<=" => Ok(CompOp::Le),
other => Err(NanoError::Parse(format!("unknown operator: {}", other))),
other => Err(CompilerError::Parse(format!("unknown operator: {}", other))),
}
}
@ -673,14 +678,14 @@ fn parse_literal(pair: pest::iterators::Pair<Rule>) -> Result<Literal> {
let n: i64 = inner
.as_str()
.parse()
.map_err(|e| NanoError::Parse(format!("invalid integer: {}", e)))?;
.map_err(|e| CompilerError::Parse(format!("invalid integer: {}", e)))?;
Ok(Literal::Integer(n))
}
Rule::float_lit => {
let f: f64 = inner
.as_str()
.parse()
.map_err(|e| NanoError::Parse(format!("invalid float: {}", e)))?;
.map_err(|e| CompilerError::Parse(format!("invalid float: {}", e)))?;
Ok(Literal::Float(f))
}
Rule::bool_lit => {
@ -688,7 +693,7 @@ fn parse_literal(pair: pest::iterators::Pair<Rule>) -> Result<Literal> {
"true" => true,
"false" => false,
other => {
return Err(NanoError::Parse(format!(
return Err(CompilerError::Parse(format!(
"invalid boolean literal: {}",
other
)));
@ -701,7 +706,9 @@ fn parse_literal(pair: pest::iterators::Pair<Rule>) -> Result<Literal> {
.into_inner()
.next()
.map(|s| parse_string_lit(s.as_str()))
.ok_or_else(|| NanoError::Parse("date literal requires a string".to_string()))?;
.ok_or_else(|| {
CompilerError::Parse("date literal requires a string".to_string())
})?;
Ok(Literal::Date(date_str?))
}
Rule::datetime_lit => {
@ -710,7 +717,7 @@ fn parse_literal(pair: pest::iterators::Pair<Rule>) -> Result<Literal> {
.next()
.map(|s| parse_string_lit(s.as_str()))
.ok_or_else(|| {
NanoError::Parse("datetime literal requires a string".to_string())
CompilerError::Parse("datetime literal requires a string".to_string())
})?;
Ok(Literal::DateTime(dt_str?))
}
@ -723,7 +730,7 @@ fn parse_literal(pair: pest::iterators::Pair<Rule>) -> Result<Literal> {
}
Ok(Literal::List(items))
}
_ => Err(NanoError::Parse(format!(
_ => Err(CompilerError::Parse(format!(
"unexpected literal: {:?}",
inner.as_rule()
))),
@ -746,14 +753,14 @@ fn parse_ordering(pair: pest::iterators::Pair<Rule>) -> Result<Ordering> {
let mut inner = pair.into_inner();
let first = inner
.next()
.ok_or_else(|| NanoError::Parse("ordering cannot be empty".to_string()))?;
.ok_or_else(|| CompilerError::Parse("ordering cannot be empty".to_string()))?;
let (expr, descending) = match first.as_rule() {
Rule::nearest_ordering => (parse_nearest_ordering(first)?, false),
Rule::expr => {
let expr = parse_expr(first)?;
let direction = inner.next().map(|p| p.as_str().to_string());
if matches!(expr, Expr::Nearest { .. }) && direction.is_some() {
return Err(NanoError::Parse(
return Err(CompilerError::Parse(
"nearest() ordering does not accept asc/desc modifiers".to_string(),
));
}
@ -761,7 +768,7 @@ fn parse_ordering(pair: pest::iterators::Pair<Rule>) -> Result<Ordering> {
(expr, descending)
}
other => {
return Err(NanoError::Parse(format!(
return Err(CompilerError::Parse(format!(
"unexpected ordering rule: {:?}",
other
)));
@ -775,22 +782,22 @@ fn parse_nearest_ordering(pair: pest::iterators::Pair<Rule>) -> Result<Expr> {
let mut inner = pair.into_inner();
let prop = inner
.next()
.ok_or_else(|| NanoError::Parse("nearest() missing property".to_string()))?;
.ok_or_else(|| CompilerError::Parse("nearest() missing property".to_string()))?;
let mut prop_parts = prop.into_inner();
let var = prop_parts
.next()
.ok_or_else(|| NanoError::Parse("nearest() missing variable".to_string()))?
.ok_or_else(|| CompilerError::Parse("nearest() missing variable".to_string()))?
.as_str();
let variable = var.strip_prefix('$').unwrap_or(var).to_string();
let property = prop_parts
.next()
.ok_or_else(|| NanoError::Parse("nearest() missing property name".to_string()))?
.ok_or_else(|| CompilerError::Parse("nearest() missing property name".to_string()))?
.as_str()
.to_string();
let query = inner
.next()
.ok_or_else(|| NanoError::Parse("nearest() missing query expression".to_string()))?;
.ok_or_else(|| CompilerError::Parse("nearest() missing query expression".to_string()))?;
Ok(Expr::Nearest {
variable,
property,

View file

@ -4,7 +4,7 @@ use std::sync::Arc;
use arrow_schema::{DataType, Field, Schema, SchemaRef};
use crate::catalog::Catalog;
use crate::error::{NanoError, Result};
use crate::error::{CompilerError, Result};
use crate::types::{Direction, PropType, ScalarType};
use super::ast::*;
@ -82,7 +82,7 @@ pub fn typecheck_query_decl(catalog: &Catalog, query: &QueryDecl) -> Result<Chec
pub fn typecheck_query(catalog: &Catalog, query: &QueryDecl) -> Result<TypeContext> {
if !query.mutations.is_empty() {
return Err(NanoError::Type(
return Err(CompilerError::Type(
"mutation query cannot be typechecked with read-query API".to_string(),
));
}
@ -115,14 +115,14 @@ fn parse_declared_param_types(params: &[Param]) -> Result<HashMap<String, PropTy
let mut out = HashMap::with_capacity(params.len());
for p in params {
if p.name == NOW_PARAM_NAME {
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"parameter name `${}` is reserved for runtime timestamp injection",
NOW_PARAM_NAME
)));
}
let prop_type =
PropType::from_param_type_name(&p.type_name, p.nullable).ok_or_else(|| {
NanoError::Type(format!(
CompilerError::Type(format!(
"unknown parameter type `{}` for `${}`",
p.type_name, p.name
))
@ -168,12 +168,12 @@ fn typecheck_read_query(catalog: &Catalog, query: &QueryDecl) -> Result<TypeCont
.iter()
.any(|ord| expr_contains_rrf_with_aliases(&ord.expr, &alias_exprs));
if has_rrf && query.limit.is_none() {
return Err(NanoError::Type(
return Err(CompilerError::Type(
"T21: rrf ordering requires a limit clause".to_string(),
));
}
if has_standalone_nearest && query.limit.is_none() {
return Err(NanoError::Type(
return Err(CompilerError::Type(
"T17: nearest ordering requires a limit clause".to_string(),
));
}
@ -183,7 +183,7 @@ fn typecheck_read_query(catalog: &Catalog, query: &QueryDecl) -> Result<TypeCont
.iter()
.any(|ord| matches!(ord.expr, Expr::AliasRef(_)))
{
return Err(NanoError::Type(
return Err(CompilerError::Type(
"T18: alias-based ordering is not supported together with nearest in phase 1"
.to_string(),
));
@ -201,7 +201,7 @@ fn typecheck_read_query(catalog: &Catalog, query: &QueryDecl) -> Result<TypeCont
match &proj.expr {
Expr::PropAccess { .. } | Expr::Variable(_) => {}
_ => {
return Err(NanoError::Type(
return Err(CompilerError::Type(
"T9: non-aggregate expressions in an aggregate query must be \
property accesses or variables"
.to_string(),
@ -221,7 +221,7 @@ fn typecheck_mutation(catalog: &Catalog, mutation: &Mutation, params: &[Param])
match mutation {
Mutation::Insert(insert) => {
if insert.assignments.is_empty() {
return Err(NanoError::Type(
return Err(CompilerError::Type(
"T10: insert mutation requires at least one assignment".to_string(),
));
}
@ -235,7 +235,7 @@ fn typecheck_mutation(catalog: &Catalog, mutation: &Mutation, params: &[Param])
.properties
.get(&assignment.property)
.ok_or_else(|| {
NanoError::Type(format!(
CompilerError::Type(format!(
"T11: type `{}` has no property `{}`",
insert.type_name, assignment.property
))
@ -265,13 +265,13 @@ fn typecheck_mutation(catalog: &Catalog, mutation: &Mutation, params: &[Param])
if assigned_props.contains(embed.source.as_str()) {
continue;
}
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"T12: insert for `{}` must provide non-nullable property `{}` or @embed source `{}`",
insert.type_name, prop_name, embed.source
)));
}
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"T12: insert for `{}` must provide non-nullable property `{}`",
insert.type_name, prop_name
)));
@ -308,7 +308,7 @@ fn typecheck_mutation(catalog: &Catalog, mutation: &Mutation, params: &[Param])
.properties
.get(&assignment.property)
.ok_or_else(|| {
NanoError::Type(format!(
CompilerError::Type(format!(
"T11: type `{}` has no property `{}`",
insert.type_name, assignment.property
))
@ -324,13 +324,13 @@ fn typecheck_mutation(catalog: &Catalog, mutation: &Mutation, params: &[Param])
}
if !has_from {
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"T12: insert for `{}` must provide required endpoint `from`",
insert.type_name
)));
}
if !has_to {
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"T12: insert for `{}` must provide required endpoint `to`",
insert.type_name
)));
@ -341,7 +341,7 @@ fn typecheck_mutation(catalog: &Catalog, mutation: &Mutation, params: &[Param])
continue;
}
if !insert.assignments.iter().any(|a| &a.property == prop_name) {
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"T12: insert for `{}` must provide non-nullable property `{}`",
insert.type_name, prop_name
)));
@ -350,7 +350,7 @@ fn typecheck_mutation(catalog: &Catalog, mutation: &Mutation, params: &[Param])
return Ok(insert.type_name.clone());
}
Err(NanoError::Type(format!(
Err(CompilerError::Type(format!(
"T10: unknown node/edge type `{}`",
insert.type_name
)))
@ -359,19 +359,19 @@ fn typecheck_mutation(catalog: &Catalog, mutation: &Mutation, params: &[Param])
let node_type = if let Some(node_type) = catalog.node_types.get(&update.type_name) {
node_type
} else if catalog.edge_types.contains_key(&update.type_name) {
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"T16: update mutation for edge type `{}` is not supported",
update.type_name
)));
} else {
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"T10: unknown node/edge type `{}`",
update.type_name
)));
};
if update.assignments.is_empty() {
return Err(NanoError::Type(
return Err(CompilerError::Type(
"T10: update mutation requires at least one assignment".to_string(),
));
}
@ -383,7 +383,7 @@ fn typecheck_mutation(catalog: &Catalog, mutation: &Mutation, params: &[Param])
.properties
.get(&assignment.property)
.ok_or_else(|| {
NanoError::Type(format!(
CompilerError::Type(format!(
"T11: type `{}` has no property `{}`",
update.type_name, assignment.property
))
@ -422,7 +422,7 @@ fn typecheck_mutation(catalog: &Catalog, mutation: &Mutation, params: &[Param])
)?;
Ok(delete.type_name.clone())
} else {
Err(NanoError::Type(format!(
Err(CompilerError::Type(format!(
"T10: unknown node/edge type `{}`",
delete.type_name
)))
@ -435,7 +435,7 @@ fn ensure_no_duplicate_assignment_names(assignments: &[MutationAssignment]) -> R
let mut seen = std::collections::HashSet::new();
for assignment in assignments {
if !seen.insert(&assignment.property) {
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"T13: duplicate assignment for property `{}`",
assignment.property
)));
@ -454,13 +454,13 @@ fn typecheck_mutation_predicate(
.properties
.get(&predicate.property)
.ok_or_else(|| {
NanoError::Type(format!(
CompilerError::Type(format!(
"T11: type `{}` has no property `{}`",
type_name, predicate.property
))
})?;
if matches!(prop_type.scalar, ScalarType::Blob) {
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"T11: blob property `{}` cannot be used in WHERE predicates",
predicate.property
)));
@ -493,7 +493,7 @@ fn typecheck_edge_mutation_predicate(
.properties
.get(&predicate.property)
.ok_or_else(|| {
NanoError::Type(format!(
CompilerError::Type(format!(
"T11: type `{}` has no property `{}`",
type_name, predicate.property
))
@ -517,7 +517,7 @@ fn check_match_value_type(
MatchValue::Literal(lit) => check_literal_type(lit, expected, property),
MatchValue::Variable(v) => {
let Some(actual) = params.get(v) else {
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"T14: mutation variable `${}` must be a declared query parameter",
v
)));
@ -528,7 +528,7 @@ fn check_match_value_type(
&& matches!(actual.scalar, ScalarType::String)
&& !actual.list);
if !compatible {
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"T7: cannot assign/compare {} with {} for property `{}`",
actual.display_name(),
expected.display_name(),
@ -543,7 +543,7 @@ fn check_match_value_type(
fn check_now_match_value_type(expected: &PropType, property: &str) -> Result<()> {
if expected.list || expected.scalar != ScalarType::DateTime {
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"T7: cannot assign/compare DateTime with {} for property `{}`",
expected.display_name(),
property
@ -597,7 +597,7 @@ fn typecheck_clauses(
}
}
if !has_outer {
return Err(NanoError::Type(
return Err(CompilerError::Type(
"T9: negation block must reference at least one outer-bound variable"
.to_string(),
));
@ -616,7 +616,7 @@ fn typecheck_binding(
) -> Result<()> {
// T1: binding type must exist in catalog
if !catalog.node_types.contains_key(&binding.type_name) {
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"T1: unknown node type `{}`",
binding.type_name
)));
@ -627,14 +627,14 @@ fn typecheck_binding(
// T2 + T3: property match fields must exist and have correct types
for pm in &binding.prop_matches {
let prop = node_type.properties.get(&pm.prop_name).ok_or_else(|| {
NanoError::Type(format!(
CompilerError::Type(format!(
"T2: type `{}` has no property `{}`",
binding.type_name, pm.prop_name
))
})?;
if matches!(prop.scalar, ScalarType::Blob) {
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"T3: blob property `{}.{}` cannot be used in match patterns",
binding.type_name, pm.prop_name
)));
@ -658,7 +658,7 @@ fn typecheck_binding(
if let Some(existing) = ctx.bindings.get(&binding.variable)
&& existing.type_name != binding.type_name
{
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"variable `${}` already bound to type `{}`, cannot rebind to `{}`",
binding.variable, existing.type_name, binding.type_name
)));
@ -680,7 +680,7 @@ fn check_binding_literal_type(lit: &Literal, expected: &PropType, property: &str
if expected.list {
let lit_type = literal_type(lit)?;
if lit_type.list {
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"T3: list equality is not supported for property `{}`; use a scalar value to match list membership",
property
)));
@ -688,7 +688,7 @@ fn check_binding_literal_type(lit: &Literal, expected: &PropType, property: &str
let expected_member = PropType::scalar(expected.scalar, expected.nullable);
if !types_compatible(&lit_type, &expected_member) {
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"T3: property `{}` has type {} but membership match got {}",
property,
expected.display_name(),
@ -708,7 +708,7 @@ fn check_binding_variable_type(
) -> Result<()> {
if expected.list {
if actual.list {
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"T7: list equality is not supported for property `{}`; use a scalar parameter for membership matching",
property
)));
@ -716,7 +716,7 @@ fn check_binding_variable_type(
let expected_member = PropType::scalar(expected.scalar, expected.nullable);
if !types_compatible(actual, &expected_member) {
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"T7: cannot compare {} membership against {} for property `{}`",
actual.display_name(),
expected.display_name(),
@ -727,7 +727,7 @@ fn check_binding_variable_type(
}
if !types_compatible(actual, expected) {
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"T7: cannot assign/compare {} with {} for property `{}`",
actual.display_name(),
expected.display_name(),
@ -746,23 +746,23 @@ fn typecheck_traversal(
let edge = catalog
.lookup_edge_by_name(&traversal.edge_name)
.ok_or_else(|| {
NanoError::Type(format!("T4: unknown edge type `{}`", traversal.edge_name))
CompilerError::Type(format!("T4: unknown edge type `{}`", traversal.edge_name))
})?;
if traversal.min_hops == 0 {
return Err(NanoError::Type(
return Err(CompilerError::Type(
"T15: traversal min hop bound must be >= 1".to_string(),
));
}
if let Some(max_hops) = traversal.max_hops {
if max_hops < traversal.min_hops {
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"T15: invalid traversal bounds {{{},{}}}; max must be >= min",
traversal.min_hops, max_hops
)));
}
} else {
return Err(NanoError::Type(
return Err(CompilerError::Type(
"T15: unbounded traversal is disabled; use bounded traversal {min,max}".to_string(),
));
}
@ -784,7 +784,7 @@ fn typecheck_traversal(
// dst should be edge.from_type
bind_traversal_endpoint(ctx, &traversal.dst, &edge.from_type, edge)?;
} else {
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"T5: variable `${}` has type `{}`, which is not an endpoint of edge `{}: {} -> {}`",
traversal.src, src_bv.type_name, edge.name, edge.from_type, edge.to_type
)));
@ -798,7 +798,7 @@ fn typecheck_traversal(
direction = Direction::In;
bind_traversal_endpoint(ctx, &traversal.src, &edge.to_type, edge)?;
} else {
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"T5: variable `${}` has type `{}`, which is not an endpoint of edge `{}: {} -> {}`",
traversal.dst, dst_bv.type_name, edge.name, edge.from_type, edge.to_type
)));
@ -833,7 +833,7 @@ fn bind_traversal_endpoint(
}
if let Some(existing) = ctx.bindings.get(var) {
if existing.type_name != expected_type {
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"T5: variable `${}` has type `{}` but edge `{}` expects `{}`",
var, existing.type_name, edge.name, expected_type
)));
@ -863,27 +863,27 @@ fn typecheck_filter(
if let (ResolvedType::Scalar(l), ResolvedType::Scalar(r)) = (&left_type, &right_type) {
if filter.op == CompOp::Contains {
if !l.list {
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"T7: contains requires a list property on the left, got {}",
l.display_name()
)));
}
if r.list {
return Err(NanoError::Type(
return Err(CompilerError::Type(
"T7: contains requires a scalar right operand".to_string(),
));
}
if matches!(l.scalar, ScalarType::Vector(_))
|| matches!(r.scalar, ScalarType::Vector(_))
{
return Err(NanoError::Type(
return Err(CompilerError::Type(
"T7: vector membership filters are not supported".to_string(),
));
}
let expected_member = PropType::scalar(l.scalar, l.nullable);
if !types_compatible(&expected_member, r) {
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"T7: cannot test membership of {} in {}",
r.display_name(),
l.display_name()
@ -894,29 +894,29 @@ fn typecheck_filter(
// T7: check type compatibility
if l.list || r.list {
return Err(NanoError::Type(
return Err(CompilerError::Type(
"T7: list comparisons in filters are not supported; use `contains` for list membership".to_string(),
));
}
if matches!(l.scalar, ScalarType::Vector(_)) || matches!(r.scalar, ScalarType::Vector(_)) {
return Err(NanoError::Type(
return Err(CompilerError::Type(
"T7: vector comparisons in filters are not supported".to_string(),
));
}
if matches!(l.scalar, ScalarType::Blob) || matches!(r.scalar, ScalarType::Blob) {
return Err(NanoError::Type(
return Err(CompilerError::Type(
"T7: blob comparisons in filters are not supported".to_string(),
));
}
if !types_compatible(l, r) {
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"T7: cannot compare {} with {}",
l.display_name(),
r.display_name()
)));
}
} else {
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"T7: filter comparisons require scalar operands, got {} and {}",
left_type.display_name(),
right_type.display_name()
@ -940,15 +940,15 @@ fn resolve_expr_type(
Expr::PropAccess { variable, property } => {
// T6: variable must be bound and property must exist
let bv = ctx.bindings.get(variable).ok_or_else(|| {
NanoError::Type(format!("T6: variable `${}` is not bound", variable))
CompilerError::Type(format!("T6: variable `${}` is not bound", variable))
})?;
let node_type = catalog.node_types.get(&bv.type_name).ok_or_else(|| {
NanoError::Type(format!("T6: type `{}` not found in catalog", bv.type_name))
CompilerError::Type(format!("T6: type `{}` not found in catalog", bv.type_name))
})?;
let prop = node_type.properties.get(property).ok_or_else(|| {
NanoError::Type(format!(
CompilerError::Type(format!(
"T6: type `{}` has no property `{}`",
bv.type_name, property
))
@ -962,19 +962,19 @@ fn resolve_expr_type(
query,
} => {
let node_binding = ctx.bindings.get(variable).ok_or_else(|| {
NanoError::Type(format!("T15: variable `${}` is not bound", variable))
CompilerError::Type(format!("T15: variable `${}` is not bound", variable))
})?;
let node_type = catalog
.node_types
.get(&node_binding.type_name)
.ok_or_else(|| {
NanoError::Type(format!(
CompilerError::Type(format!(
"T15: type `{}` not found in catalog",
node_binding.type_name
))
})?;
let prop_type = node_type.properties.get(property).ok_or_else(|| {
NanoError::Type(format!(
CompilerError::Type(format!(
"T15: type `{}` has no property `{}`",
node_binding.type_name, property
))
@ -982,7 +982,7 @@ fn resolve_expr_type(
let vector_dim = match prop_type.scalar {
ScalarType::Vector(dim) => dim,
_ => {
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"T15: nearest requires a Vector property, got {}.{}: {}",
node_binding.type_name,
property,
@ -991,7 +991,7 @@ fn resolve_expr_type(
}
};
if prop_type.list {
return Err(NanoError::Type(
return Err(CompilerError::Type(
"T15: nearest does not support list-wrapped vectors".to_string(),
));
}
@ -1000,7 +1000,7 @@ fn resolve_expr_type(
&& let Some(dim) = numeric_vector_literal_dim(lit)
{
if dim != vector_dim {
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"T15: nearest vector dimension mismatch: property is Vector({}), query literal has {} elements",
vector_dim, dim
)));
@ -1019,7 +1019,7 @@ fn resolve_expr_type(
_ => unreachable!(),
};
if qdim != vector_dim {
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"T15: nearest vector dimension mismatch: property is Vector({}), query is Vector({})",
vector_dim, qdim
)));
@ -1029,14 +1029,14 @@ fn resolve_expr_type(
// query-time string embedding is supported by the runtime executor
}
ResolvedType::Scalar(s) => {
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"T15: nearest query must be Vector({}) or String, got {}",
vector_dim,
s.display_name()
)));
}
_ => {
return Err(NanoError::Type(
return Err(CompilerError::Type(
"T15: nearest query must be a scalar expression".to_string(),
));
}
@ -1052,13 +1052,13 @@ fn resolve_expr_type(
match field_type {
ResolvedType::Scalar(s) if s.scalar == ScalarType::String && !s.list => {}
ResolvedType::Scalar(s) => {
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"T19: search field must be String, got {}",
s.display_name()
)));
}
_ => {
return Err(NanoError::Type(
return Err(CompilerError::Type(
"T19: search field must be a scalar String expression".to_string(),
));
}
@ -1068,13 +1068,13 @@ fn resolve_expr_type(
match query_type {
ResolvedType::Scalar(s) if s.scalar == ScalarType::String && !s.list => {}
ResolvedType::Scalar(s) => {
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"T19: search query must be String, got {}",
s.display_name()
)));
}
_ => {
return Err(NanoError::Type(
return Err(CompilerError::Type(
"T19: search query must be a scalar String expression".to_string(),
));
}
@ -1094,13 +1094,13 @@ fn resolve_expr_type(
match field_type {
ResolvedType::Scalar(s) if s.scalar == ScalarType::String && !s.list => {}
ResolvedType::Scalar(s) => {
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"T19: fuzzy field must be String, got {}",
s.display_name()
)));
}
_ => {
return Err(NanoError::Type(
return Err(CompilerError::Type(
"T19: fuzzy field must be a scalar String expression".to_string(),
));
}
@ -1110,13 +1110,13 @@ fn resolve_expr_type(
match query_type {
ResolvedType::Scalar(s) if s.scalar == ScalarType::String && !s.list => {}
ResolvedType::Scalar(s) => {
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"T19: fuzzy query must be String, got {}",
s.display_name()
)));
}
_ => {
return Err(NanoError::Type(
return Err(CompilerError::Type(
"T19: fuzzy query must be a scalar String expression".to_string(),
));
}
@ -1135,13 +1135,13 @@ fn resolve_expr_type(
| ScalarType::U64
) => {}
ResolvedType::Scalar(s) => {
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"T19: fuzzy max_edits must be an integer scalar, got {}",
s.display_name()
)));
}
_ => {
return Err(NanoError::Type(
return Err(CompilerError::Type(
"T19: fuzzy max_edits must be an integer scalar expression".to_string(),
));
}
@ -1158,13 +1158,13 @@ fn resolve_expr_type(
match field_type {
ResolvedType::Scalar(s) if s.scalar == ScalarType::String && !s.list => {}
ResolvedType::Scalar(s) => {
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"T20: match_text field must be String, got {}",
s.display_name()
)));
}
_ => {
return Err(NanoError::Type(
return Err(CompilerError::Type(
"T20: match_text field must be a scalar String expression".to_string(),
));
}
@ -1174,13 +1174,13 @@ fn resolve_expr_type(
match query_type {
ResolvedType::Scalar(s) if s.scalar == ScalarType::String && !s.list => {}
ResolvedType::Scalar(s) => {
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"T20: match_text query must be String, got {}",
s.display_name()
)));
}
_ => {
return Err(NanoError::Type(
return Err(CompilerError::Type(
"T20: match_text query must be a scalar String expression".to_string(),
));
}
@ -1196,13 +1196,13 @@ fn resolve_expr_type(
match field_type {
ResolvedType::Scalar(s) if s.scalar == ScalarType::String && !s.list => {}
ResolvedType::Scalar(s) => {
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"T20: bm25 field must be String, got {}",
s.display_name()
)));
}
_ => {
return Err(NanoError::Type(
return Err(CompilerError::Type(
"T20: bm25 field must be a scalar String expression".to_string(),
));
}
@ -1212,13 +1212,13 @@ fn resolve_expr_type(
match query_type {
ResolvedType::Scalar(s) if s.scalar == ScalarType::String && !s.list => {}
ResolvedType::Scalar(s) => {
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"T20: bm25 query must be String, got {}",
s.display_name()
)));
}
_ => {
return Err(NanoError::Type(
return Err(CompilerError::Type(
"T20: bm25 query must be a scalar String expression".to_string(),
));
}
@ -1235,12 +1235,12 @@ fn resolve_expr_type(
k,
} => {
if !matches!(primary.as_ref(), Expr::Nearest { .. } | Expr::Bm25 { .. }) {
return Err(NanoError::Type(
return Err(CompilerError::Type(
"T21: rrf primary expression must be nearest(...) or bm25(...)".to_string(),
));
}
if !matches!(secondary.as_ref(), Expr::Nearest { .. } | Expr::Bm25 { .. }) {
return Err(NanoError::Type(
return Err(CompilerError::Type(
"T21: rrf secondary expression must be nearest(...) or bm25(...)".to_string(),
));
}
@ -1252,13 +1252,13 @@ fn resolve_expr_type(
match ty {
ResolvedType::Scalar(s) if s.scalar == ScalarType::F64 && !s.list => {}
ResolvedType::Scalar(s) => {
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"T21: rrf rank expressions must evaluate to F64, got {}",
s.display_name()
)));
}
_ => {
return Err(NanoError::Type(
return Err(CompilerError::Type(
"T21: rrf rank expressions must be scalar numeric expressions"
.to_string(),
));
@ -1279,13 +1279,13 @@ fn resolve_expr_type(
| ScalarType::U64
) => {}
ResolvedType::Scalar(s) => {
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"T21: rrf k must be an integer scalar, got {}",
s.display_name()
)));
}
_ => {
return Err(NanoError::Type(
return Err(CompilerError::Type(
"T21: rrf k must be an integer scalar expression".to_string(),
));
}
@ -1293,7 +1293,7 @@ fn resolve_expr_type(
if let Expr::Literal(Literal::Integer(v)) = k_expr.as_ref()
&& *v <= 0
{
return Err(NanoError::Type(
return Err(CompilerError::Type(
"T21: rrf k must be greater than 0".to_string(),
));
}
@ -1311,7 +1311,7 @@ fn resolve_expr_type(
} else if let Some(bv) = ctx.bindings.get(name) {
Ok(ResolvedType::Node(bv.type_name.clone()))
} else {
Err(NanoError::Type(format!(
Err(CompilerError::Type(format!(
"variable `${}` is not bound",
name
)))
@ -1327,7 +1327,7 @@ fn resolve_expr_type(
if let ResolvedType::Scalar(s) = &arg_type
&& (s.list || !s.scalar.is_numeric())
{
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"T8: {} requires numeric type, got {}",
func,
s.display_name()
@ -1338,7 +1338,7 @@ fn resolve_expr_type(
if let ResolvedType::Scalar(s) = &arg_type
&& (s.list || (!s.scalar.is_numeric() && s.scalar != ScalarType::String))
{
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"T8: {} requires numeric or string type, got {}",
func,
s.display_name()
@ -1420,7 +1420,7 @@ fn resolved_type_to_field_shape(
ResolvedType::Scalar(prop_type) => Ok((prop_type.to_arrow(), prop_type.nullable)),
ResolvedType::Node(type_name) => {
let node_type = catalog.node_types.get(type_name).ok_or_else(|| {
NanoError::Type(format!("type `{}` not found in catalog", type_name))
CompilerError::Type(format!("type `{}` not found in catalog", type_name))
})?;
let fields: Vec<Field> = node_type
.arrow_schema
@ -1450,14 +1450,14 @@ fn literal_type(lit: &Literal) -> Result<PropType> {
}
let first = literal_type(&items[0])?;
if first.list {
return Err(NanoError::Type(
return Err(CompilerError::Type(
"nested list literals are not supported".to_string(),
));
}
for item in items.iter().skip(1) {
let item_type = literal_type(item)?;
if item_type.list || !types_compatible(&first, &item_type) {
return Err(NanoError::Type(
return Err(CompilerError::Type(
"list literal elements must share a compatible scalar type".to_string(),
));
}
@ -1473,7 +1473,7 @@ fn check_literal_type(lit: &Literal, expected: &PropType, prop_name: &str) -> Re
return if expected.nullable {
Ok(())
} else {
Err(NanoError::Type(format!(
Err(CompilerError::Type(format!(
"T3: property `{}` is non-nullable but got null",
prop_name
)))
@ -1487,7 +1487,7 @@ fn check_literal_type(lit: &Literal, expected: &PropType, prop_name: &str) -> Re
if actual_dim == expected_dim {
return Ok(());
}
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"T3: property `{}` has type Vector({}) but got vector literal with {} elements",
prop_name, expected_dim, actual_dim
)));
@ -1495,7 +1495,7 @@ fn check_literal_type(lit: &Literal, expected: &PropType, prop_name: &str) -> Re
let lit_type = literal_type(lit)?;
if !types_compatible(&lit_type, expected) {
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"T3: property `{}` has type {} but got {}",
prop_name,
expected.display_name(),
@ -1507,7 +1507,7 @@ fn check_literal_type(lit: &Literal, expected: &PropType, prop_name: &str) -> Re
match lit {
Literal::String(v) => {
if !allowed.contains(v) {
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"T3: property `{}` expects one of [{}], got '{}'",
prop_name,
allowed.join(", "),
@ -1520,7 +1520,7 @@ fn check_literal_type(lit: &Literal, expected: &PropType, prop_name: &str) -> Re
match item {
Literal::String(v) if allowed.contains(v) => {}
Literal::String(v) => {
return Err(NanoError::Type(format!(
return Err(CompilerError::Type(format!(
"T3: property `{}` expects one of [{}], got '{}'",
prop_name,
allowed.join(", "),

View file

@ -3,7 +3,7 @@ use std::fmt;
use serde_json::Value;
use crate::error::NanoError;
use crate::error::CompilerError;
use crate::ir::ParamMap;
use crate::json_output::{JS_MAX_SAFE_INTEGER_U64, is_js_safe_integer_i64};
use crate::query::ast::{Literal, Param, QueryDecl};
@ -17,7 +17,7 @@ pub enum JsonParamMode {
#[derive(Debug)]
pub enum RunInputError {
Core(NanoError),
Core(CompilerError),
Message(String),
}
@ -45,8 +45,8 @@ impl Error for RunInputError {
}
}
impl From<NanoError> for RunInputError {
fn from(value: NanoError) -> Self {
impl From<CompilerError> for RunInputError {
fn from(value: CompilerError) -> Self {
Self::Core(value)
}
}
@ -120,7 +120,7 @@ impl ToParam for i64 {
impl ToParam for isize {
fn to_param(self) -> crate::error::Result<Literal> {
let value = i64::try_from(self).map_err(|_| {
NanoError::Execution(format!(
CompilerError::Execution(format!(
"param value {} exceeds current engine range for numeric literals (max {})",
self,
i64::MAX
@ -151,7 +151,7 @@ impl ToParam for u32 {
impl ToParam for u64 {
fn to_param(self) -> crate::error::Result<Literal> {
let value = i64::try_from(self).map_err(|_| {
NanoError::Execution(format!(
CompilerError::Execution(format!(
"param value {} exceeds current engine range for numeric literals (max {})",
self,
i64::MAX
@ -164,7 +164,7 @@ impl ToParam for u64 {
impl ToParam for usize {
fn to_param(self) -> crate::error::Result<Literal> {
let value = i64::try_from(self).map_err(|_| {
NanoError::Execution(format!(
CompilerError::Execution(format!(
"param value {} exceeds current engine range for numeric literals (max {})",
self,
i64::MAX
@ -177,7 +177,7 @@ impl ToParam for usize {
impl ToParam for f32 {
fn to_param(self) -> crate::error::Result<Literal> {
if !self.is_finite() {
return Err(NanoError::Execution(format!(
return Err(CompilerError::Execution(format!(
"invalid float parameter {}",
self
)));
@ -189,7 +189,7 @@ impl ToParam for f32 {
impl ToParam for f64 {
fn to_param(self) -> crate::error::Result<Literal> {
if !self.is_finite() {
return Err(NanoError::Execution(format!(
return Err(CompilerError::Execution(format!(
"invalid float parameter {}",
self
)));

View file

@ -5,7 +5,7 @@ use arrow_ipc::writer::StreamWriter;
use arrow_schema::{DataType, Field, Schema, SchemaRef};
use serde::de::DeserializeOwned;
use crate::error::{NanoError, Result};
use crate::error::{CompilerError, Result};
use crate::json_output::{record_batches_to_json_rows, record_batches_to_rust_json_rows};
#[derive(Debug, Clone, Copy, Default)]
@ -47,7 +47,7 @@ impl QueryResult {
}
arrow_select::concat::concat_batches(&self.schema, &self.batches)
.map_err(|err| NanoError::Execution(err.to_string()))
.map_err(|err| CompilerError::Execution(err.to_string()))
}
pub fn to_sdk_json(&self) -> serde_json::Value {
@ -60,7 +60,7 @@ impl QueryResult {
pub fn deserialize<T: DeserializeOwned>(&self) -> Result<T> {
serde_json::from_value(self.to_rust_json()).map_err(|err| {
NanoError::Execution(format!("failed to deserialize query result: {}", err))
CompilerError::Execution(format!("failed to deserialize query result: {}", err))
})
}

View file

@ -5,7 +5,7 @@ use pest::error::InputLocation;
use pest_derive::Parser;
use crate::error::{
NanoError, ParseDiagnostic, Result, SourceSpan, decode_string_literal, render_span,
CompilerError, ParseDiagnostic, Result, SourceSpan, decode_string_literal, render_span,
};
use crate::types::{PropType, ScalarType};
@ -16,7 +16,7 @@ use super::ast::*;
struct SchemaParser;
pub fn parse_schema(input: &str) -> Result<SchemaFile> {
parse_schema_diagnostic(input).map_err(|e| NanoError::Parse(e.to_string()))
parse_schema_diagnostic(input).map_err(|e| CompilerError::Parse(e.to_string()))
}
pub fn parse_schema_diagnostic(input: &str) -> std::result::Result<SchemaFile, ParseDiagnostic> {
@ -27,7 +27,8 @@ pub fn parse_schema_diagnostic(input: &str) -> std::result::Result<SchemaFile, P
if pair.as_rule() == Rule::schema_file {
for inner in pair.into_inner() {
if let Rule::schema_decl = inner.as_rule() {
declarations.push(parse_schema_decl(inner).map_err(nano_error_to_diagnostic)?);
declarations
.push(parse_schema_decl(inner).map_err(compiler_error_to_diagnostic)?);
}
}
}
@ -46,13 +47,13 @@ pub fn parse_schema_diagnostic(input: &str) -> std::result::Result<SchemaFile, P
let iface_refs: Vec<&InterfaceDecl> = interfaces.iter().collect();
for decl in &mut declarations {
if let SchemaDecl::Node(node) = decl {
resolve_interfaces(node, &iface_refs).map_err(nano_error_to_diagnostic)?;
resolve_interfaces(node, &iface_refs).map_err(compiler_error_to_diagnostic)?;
}
}
let schema = SchemaFile { declarations };
validate_schema_annotations(&schema).map_err(nano_error_to_diagnostic)?;
validate_constraints(&schema).map_err(nano_error_to_diagnostic)?;
validate_schema_annotations(&schema).map_err(compiler_error_to_diagnostic)?;
validate_constraints(&schema).map_err(compiler_error_to_diagnostic)?;
Ok(schema)
}
@ -64,7 +65,7 @@ fn pest_error_to_diagnostic(err: pest::error::Error<Rule>) -> ParseDiagnostic {
ParseDiagnostic::new(err.to_string(), span)
}
fn nano_error_to_diagnostic(err: NanoError) -> ParseDiagnostic {
fn compiler_error_to_diagnostic(err: CompilerError) -> ParseDiagnostic {
ParseDiagnostic::new(err.to_string(), None)
}
@ -74,7 +75,7 @@ fn parse_schema_decl(pair: pest::iterators::Pair<Rule>) -> Result<SchemaDecl> {
Rule::interface_decl => Ok(SchemaDecl::Interface(parse_interface_decl(inner)?)),
Rule::node_decl => Ok(SchemaDecl::Node(parse_node_decl(inner)?)),
Rule::edge_decl => Ok(SchemaDecl::Edge(parse_edge_decl(inner)?)),
_ => Err(NanoError::Parse(format!(
_ => Err(CompilerError::Parse(format!(
"unexpected rule: {:?}",
inner.as_rule()
))),
@ -180,21 +181,20 @@ fn parse_cardinality(pair: pest::iterators::Pair<Rule>) -> Result<Cardinality> {
let min_str = inner.next().unwrap().as_str();
let min = min_str
.parse::<u32>()
.map_err(|_| NanoError::Parse(format!("invalid cardinality min: {}", min_str)))?;
let max = if let Some(max_pair) = inner.next() {
let max_str = max_pair.as_str();
Some(
max_str
.parse::<u32>()
.map_err(|_| NanoError::Parse(format!("invalid cardinality max: {}", max_str)))?,
)
} else {
None
};
.map_err(|_| CompilerError::Parse(format!("invalid cardinality min: {}", min_str)))?;
let max =
if let Some(max_pair) = inner.next() {
let max_str = max_pair.as_str();
Some(max_str.parse::<u32>().map_err(|_| {
CompilerError::Parse(format!("invalid cardinality max: {}", max_str))
})?)
} else {
None
};
if let Some(max_val) = max {
if min > max_val {
return Err(NanoError::Parse(format!(
return Err(CompilerError::Parse(format!(
"cardinality min ({}) exceeds max ({})",
min, max_val
)));
@ -219,7 +219,7 @@ fn parse_body_constraint(pair: pest::iterators::Pair<Rule>) -> Result<Constraint
.map(|a| extract_ident_from_constraint_arg(a))
.collect::<Result<Vec<_>>>()?;
if names.is_empty() {
return Err(NanoError::Parse(
return Err(CompilerError::Parse(
"@key constraint requires at least one property name".to_string(),
));
}
@ -228,7 +228,7 @@ fn parse_body_constraint(pair: pest::iterators::Pair<Rule>) -> Result<Constraint
"unique" => {
let names = extract_ident_list_from_args(args)?;
if names.is_empty() {
return Err(NanoError::Parse(
return Err(CompilerError::Parse(
"@unique constraint requires at least one property name".to_string(),
));
}
@ -237,7 +237,7 @@ fn parse_body_constraint(pair: pest::iterators::Pair<Rule>) -> Result<Constraint
"index" => {
let names = extract_ident_list_from_args(args)?;
if names.is_empty() {
return Err(NanoError::Parse(
return Err(CompilerError::Parse(
"@index constraint requires at least one property name".to_string(),
));
}
@ -246,7 +246,7 @@ fn parse_body_constraint(pair: pest::iterators::Pair<Rule>) -> Result<Constraint
"range" => {
// @range(prop, min..max)
if args.len() < 2 {
return Err(NanoError::Parse(
return Err(CompilerError::Parse(
"@range requires property name and bounds: @range(prop, min..max)".to_string(),
));
}
@ -258,7 +258,7 @@ fn parse_body_constraint(pair: pest::iterators::Pair<Rule>) -> Result<Constraint
"check" => {
// @check(prop, "regex")
if args.len() < 2 {
return Err(NanoError::Parse(
return Err(CompilerError::Parse(
"@check requires property name and pattern: @check(prop, \"regex\")"
.to_string(),
));
@ -267,7 +267,10 @@ fn parse_body_constraint(pair: pest::iterators::Pair<Rule>) -> Result<Constraint
let pattern = extract_string_from_constraint_arg(&args[1])?;
Ok(Constraint::Check { property, pattern })
}
other => Err(NanoError::Parse(format!("unknown constraint: @{}", other))),
other => Err(CompilerError::Parse(format!(
"unknown constraint: @{}",
other
))),
}
}
@ -281,7 +284,7 @@ fn extract_ident_from_constraint_arg(pair: pest::iterators::Pair<Rule>) -> Resul
return Ok(inner.as_str().to_string());
}
}
Err(NanoError::Parse(
Err(CompilerError::Parse(
"expected property name in constraint".to_string(),
))
}
@ -309,7 +312,7 @@ fn extract_string_from_constraint_arg(pair: &pest::iterators::Pair<Rule>) -> Res
}
find_string(pair)?
.ok_or_else(|| NanoError::Parse("expected string argument in constraint".to_string()))
.ok_or_else(|| CompilerError::Parse("expected string argument in constraint".to_string()))
}
fn extract_range_bounds(
@ -327,7 +330,9 @@ fn extract_range_bounds(
}
}
found.ok_or_else(|| {
NanoError::Parse("expected range bounds (min..max) in @range constraint".to_string())
CompilerError::Parse(
"expected range bounds (min..max) in @range constraint".to_string(),
)
})?
};
@ -378,7 +383,7 @@ fn parse_constraint_bound(pair: &pest::iterators::Pair<Rule>) -> Result<Constrai
}
}
Err(NanoError::Parse(format!(
Err(CompilerError::Parse(format!(
"invalid constraint bound: {}",
text
)))
@ -411,7 +416,7 @@ fn resolve_interfaces(node: &mut NodeDecl, interfaces: &[&InterfaceDecl]) -> Res
for iface_name in &node.implements {
let iface = interface_map.get(iface_name.as_str()).ok_or_else(|| {
NanoError::Parse(format!(
CompilerError::Parse(format!(
"node {} implements unknown interface '{}'",
node.name, iface_name
))
@ -421,7 +426,7 @@ fn resolve_interfaces(node: &mut NodeDecl, interfaces: &[&InterfaceDecl]) -> Res
if let Some(existing) = node.properties.iter().find(|p| p.name == iface_prop.name) {
// Property exists — verify type compatibility
if existing.prop_type != iface_prop.prop_type {
return Err(NanoError::Parse(format!(
return Err(CompilerError::Parse(format!(
"node {} property '{}' has type {} but interface {} declares it as {}",
node.name,
iface_prop.name,
@ -472,36 +477,35 @@ fn parse_type_ref(pair: pest::iterators::Pair<Rule>) -> Result<PropType> {
let mut inner = pair
.into_inner()
.next()
.ok_or_else(|| NanoError::Parse("type reference is missing core type".to_string()))?;
.ok_or_else(|| CompilerError::Parse("type reference is missing core type".to_string()))?;
if inner.as_rule() == Rule::core_type {
inner = inner
.into_inner()
.next()
.ok_or_else(|| NanoError::Parse("type reference is missing core type".to_string()))?;
inner = inner.into_inner().next().ok_or_else(|| {
CompilerError::Parse("type reference is missing core type".to_string())
})?;
}
match inner.as_rule() {
Rule::base_type => {
let scalar = ScalarType::from_str_name(inner.as_str())
.ok_or_else(|| NanoError::Parse(format!("unknown type: {}", inner.as_str())))?;
.ok_or_else(|| CompilerError::Parse(format!("unknown type: {}", inner.as_str())))?;
Ok(PropType::scalar(scalar, nullable))
}
Rule::vector_type => {
let dim_text = inner
.into_inner()
.next()
.ok_or_else(|| NanoError::Parse("Vector type missing dimension".to_string()))?
.ok_or_else(|| CompilerError::Parse("Vector type missing dimension".to_string()))?
.as_str();
let dim = dim_text
.parse::<u32>()
.map_err(|e| NanoError::Parse(format!("invalid Vector dimension: {}", e)))?;
.map_err(|e| CompilerError::Parse(format!("invalid Vector dimension: {}", e)))?;
if dim == 0 {
return Err(NanoError::Parse(
return Err(CompilerError::Parse(
"Vector dimension must be greater than zero".to_string(),
));
}
if dim > i32::MAX as u32 {
return Err(NanoError::Parse(format!(
return Err(CompilerError::Parse(format!(
"Vector dimension {} exceeds maximum supported {}",
dim,
i32::MAX
@ -510,15 +514,14 @@ fn parse_type_ref(pair: pest::iterators::Pair<Rule>) -> Result<PropType> {
Ok(PropType::scalar(ScalarType::Vector(dim), nullable))
}
Rule::list_type => {
let element = inner
.into_inner()
.next()
.ok_or_else(|| NanoError::Parse("list type missing element type".to_string()))?;
let element = inner.into_inner().next().ok_or_else(|| {
CompilerError::Parse("list type missing element type".to_string())
})?;
let scalar = ScalarType::from_str_name(element.as_str()).ok_or_else(|| {
NanoError::Parse(format!("unknown list element type: {}", element.as_str()))
CompilerError::Parse(format!("unknown list element type: {}", element.as_str()))
})?;
if matches!(scalar, ScalarType::Blob) {
return Err(NanoError::Parse(
return Err(CompilerError::Parse(
"list of Blob is not supported".to_string(),
));
}
@ -532,7 +535,7 @@ fn parse_type_ref(pair: pest::iterators::Pair<Rule>) -> Result<PropType> {
}
}
if values.is_empty() {
return Err(NanoError::Parse(
return Err(CompilerError::Parse(
"enum type must include at least one value".to_string(),
));
}
@ -540,13 +543,13 @@ fn parse_type_ref(pair: pest::iterators::Pair<Rule>) -> Result<PropType> {
dedup.sort();
dedup.dedup();
if dedup.len() != values.len() {
return Err(NanoError::Parse(
return Err(CompilerError::Parse(
"enum type cannot include duplicate values".to_string(),
));
}
Ok(PropType::enum_type(values, nullable))
}
other => Err(NanoError::Parse(format!(
other => Err(CompilerError::Parse(format!(
"unexpected type rule: {:?}",
other
))),
@ -595,19 +598,19 @@ fn validate_string_annotation(
continue;
}
if seen {
return Err(NanoError::Parse(format!(
return Err(CompilerError::Parse(format!(
"{} declares @{} multiple times",
target, annotation
)));
}
let value = ann.value.as_deref().ok_or_else(|| {
NanoError::Parse(format!(
CompilerError::Parse(format!(
"@{} on {} requires a non-empty value",
annotation, target
))
})?;
if value.trim().is_empty() {
return Err(NanoError::Parse(format!(
return Err(CompilerError::Parse(format!(
"@{} on {} requires a non-empty value",
annotation, target
)));
@ -631,7 +634,7 @@ fn validate_schema_annotations(schema: &SchemaFile) -> Result<()> {
|| ann.name == "index"
|| ann.name == "embed"
{
return Err(NanoError::Parse(format!(
return Err(CompilerError::Parse(format!(
"@{} is only supported on node properties or as body constraint (node {})",
ann.name, node.name
)));
@ -660,7 +663,7 @@ fn validate_schema_annotations(schema: &SchemaFile) -> Result<()> {
|| ann.name == "index"
|| ann.name == "embed"
{
return Err(NanoError::Parse(format!(
return Err(CompilerError::Parse(format!(
"@{} is not supported on edges (edge {})",
ann.name, edge.name
)));
@ -714,13 +717,13 @@ fn validate_property_annotations(
|| ann.name == "index"
|| ann.name == "embed")
{
return Err(NanoError::Parse(format!(
return Err(CompilerError::Parse(format!(
"@{} is not supported on list property {}.{}",
ann.name, type_name, prop.name
)));
}
if is_vector && (ann.name == "key" || ann.name == "unique") {
return Err(NanoError::Parse(format!(
return Err(CompilerError::Parse(format!(
"@{} is not supported on vector property {}.{}",
ann.name, type_name, prop.name
)));
@ -731,13 +734,13 @@ fn validate_property_annotations(
|| ann.name == "index"
|| ann.name == "embed")
{
return Err(NanoError::Parse(format!(
return Err(CompilerError::Parse(format!(
"@{} is not supported on blob property {}.{}",
ann.name, type_name, prop.name
)));
}
if ann.name == "instruction" {
return Err(NanoError::Parse(format!(
return Err(CompilerError::Parse(format!(
"@instruction is only supported on node and edge types (property {}.{})",
type_name, prop.name
)));
@ -745,7 +748,7 @@ fn validate_property_annotations(
// Edge-specific restrictions
if is_edge && (ann.name == "key" || ann.name == "embed") {
return Err(NanoError::Parse(format!(
return Err(CompilerError::Parse(format!(
"@{} is not supported on edge properties (edge {}.{})",
ann.name, type_name, prop.name
)));
@ -755,13 +758,13 @@ fn validate_property_annotations(
match ann.name.as_str() {
"key" => {
if ann.value.is_some() {
return Err(NanoError::Parse(format!(
return Err(CompilerError::Parse(format!(
"@key on {}.{} does not accept a value",
type_name, prop.name
)));
}
if key_seen {
return Err(NanoError::Parse(format!(
return Err(CompilerError::Parse(format!(
"property {}.{} declares @key multiple times",
type_name, prop.name
)));
@ -770,13 +773,13 @@ fn validate_property_annotations(
}
"unique" => {
if ann.value.is_some() {
return Err(NanoError::Parse(format!(
return Err(CompilerError::Parse(format!(
"@unique on {}.{} does not accept a value",
type_name, prop.name
)));
}
if unique_seen {
return Err(NanoError::Parse(format!(
return Err(CompilerError::Parse(format!(
"property {}.{} declares @unique multiple times",
type_name, prop.name
)));
@ -785,13 +788,13 @@ fn validate_property_annotations(
}
"index" => {
if ann.value.is_some() {
return Err(NanoError::Parse(format!(
return Err(CompilerError::Parse(format!(
"@index on {}.{} does not accept a value",
type_name, prop.name
)));
}
if index_seen {
return Err(NanoError::Parse(format!(
return Err(CompilerError::Parse(format!(
"property {}.{} declares @index multiple times",
type_name, prop.name
)));
@ -800,7 +803,7 @@ fn validate_property_annotations(
}
"embed" => {
if embed_seen {
return Err(NanoError::Parse(format!(
return Err(CompilerError::Parse(format!(
"property {}.{} declares @embed multiple times",
type_name, prop.name
)));
@ -808,20 +811,20 @@ fn validate_property_annotations(
embed_seen = true;
if !is_vector {
return Err(NanoError::Parse(format!(
return Err(CompilerError::Parse(format!(
"@embed is only supported on vector properties ({}.{})",
type_name, prop.name
)));
}
let source_prop = ann.value.as_deref().ok_or_else(|| {
NanoError::Parse(format!(
CompilerError::Parse(format!(
"@embed on {}.{} requires a source property name",
type_name, prop.name
))
})?;
if source_prop.trim().is_empty() {
return Err(NanoError::Parse(format!(
return Err(CompilerError::Parse(format!(
"@embed on {}.{} requires a non-empty source property name",
type_name, prop.name
)));
@ -831,14 +834,14 @@ fn validate_property_annotations(
.iter()
.find(|p| p.name == source_prop)
.ok_or_else(|| {
NanoError::Parse(format!(
CompilerError::Parse(format!(
"@embed on {}.{} references unknown source property {}",
type_name, prop.name, source_prop
))
})?;
if source_decl.prop_type.list || source_decl.prop_type.scalar != ScalarType::String
{
return Err(NanoError::Parse(format!(
return Err(CompilerError::Parse(format!(
"@embed source property {}.{} must be String",
type_name, source_prop
)));
@ -848,7 +851,7 @@ fn validate_property_annotations(
// a typo can't be silently ignored (it would never validate).
for key in ann.kwargs.keys() {
if key != "model" {
return Err(NanoError::Parse(format!(
return Err(CompilerError::Parse(format!(
"@embed on {}.{} has unknown argument '{}=' (only 'model' is supported)",
type_name, prop.name, key
)));
@ -893,45 +896,45 @@ fn validate_type_constraints(
match constraint {
Constraint::Key(cols) => {
if is_edge {
return Err(NanoError::Parse(format!(
return Err(CompilerError::Parse(format!(
"@key constraint is not supported on edges (edge {})",
type_name
)));
}
key_count += 1;
if key_count > 1 {
return Err(NanoError::Parse(format!(
return Err(CompilerError::Parse(format!(
"node type {} has multiple @key constraints; only one is supported",
type_name
)));
}
for col in cols {
let prop = prop_names.get(col.as_str()).ok_or_else(|| {
NanoError::Parse(format!(
CompilerError::Parse(format!(
"@key on {} references unknown property '{}'",
type_name, col
))
})?;
if prop.prop_type.nullable {
return Err(NanoError::Parse(format!(
return Err(CompilerError::Parse(format!(
"@key property {}.{} cannot be nullable",
type_name, col
)));
}
if prop.prop_type.list {
return Err(NanoError::Parse(format!(
return Err(CompilerError::Parse(format!(
"@key is not supported on list property {}.{}",
type_name, col
)));
}
if matches!(prop.prop_type.scalar, ScalarType::Vector(_)) {
return Err(NanoError::Parse(format!(
return Err(CompilerError::Parse(format!(
"@key is not supported on vector property {}.{}",
type_name, col
)));
}
if matches!(prop.prop_type.scalar, ScalarType::Blob) {
return Err(NanoError::Parse(format!(
return Err(CompilerError::Parse(format!(
"@key is not supported on blob property {}.{}",
type_name, col
)));
@ -945,7 +948,7 @@ fn validate_type_constraints(
continue;
}
if !prop_names.contains_key(col.as_str()) {
return Err(NanoError::Parse(format!(
return Err(CompilerError::Parse(format!(
"@unique on {} references unknown property '{}'",
type_name, col
)));
@ -958,13 +961,13 @@ fn validate_type_constraints(
continue;
}
let prop = prop_names.get(col.as_str()).ok_or_else(|| {
NanoError::Parse(format!(
CompilerError::Parse(format!(
"@index on {} references unknown property '{}'",
type_name, col
))
})?;
if matches!(prop.prop_type.scalar, ScalarType::Blob) {
return Err(NanoError::Parse(format!(
return Err(CompilerError::Parse(format!(
"@index is not supported on blob property {}.{}",
type_name, col
)));
@ -973,19 +976,19 @@ fn validate_type_constraints(
}
Constraint::Range { property, .. } => {
if is_edge {
return Err(NanoError::Parse(format!(
return Err(CompilerError::Parse(format!(
"@range constraint is not supported on edges (edge {})",
type_name
)));
}
let prop = prop_names.get(property.as_str()).ok_or_else(|| {
NanoError::Parse(format!(
CompilerError::Parse(format!(
"@range on {} references unknown property '{}'",
type_name, property
))
})?;
if !prop.prop_type.scalar.is_numeric() {
return Err(NanoError::Parse(format!(
return Err(CompilerError::Parse(format!(
"@range on {}.{} requires a numeric type, got {}",
type_name,
property,
@ -995,19 +998,19 @@ fn validate_type_constraints(
}
Constraint::Check { property, .. } => {
if is_edge {
return Err(NanoError::Parse(format!(
return Err(CompilerError::Parse(format!(
"@check constraint is not supported on edges (edge {})",
type_name
)));
}
let prop = prop_names.get(property.as_str()).ok_or_else(|| {
NanoError::Parse(format!(
CompilerError::Parse(format!(
"@check on {} references unknown property '{}'",
type_name, property
))
})?;
if prop.prop_type.scalar != ScalarType::String {
return Err(NanoError::Parse(format!(
return Err(CompilerError::Parse(format!(
"@check on {}.{} requires String type, got {}",
type_name,
property,

View file

@ -74,7 +74,7 @@ pub enum MergeConflictKind {
#[derive(Debug, Error)]
pub enum OmniError {
#[error("{0}")]
Compiler(#[from] omnigraph_compiler::error::NanoError),
Compiler(#[from] omnigraph_compiler::error::CompilerError),
#[error("storage: {0}")]
Lance(String),
#[error("query: {0}")]

View file

@ -12,7 +12,7 @@
- **D₂ parse-time rejection**: a single mutation query that mixes inserts/updates with deletes errors out *before any I/O* with kind `BadRequest`. Message: `mutation '<name>' on the same query mixes inserts/updates and deletes; split into separate mutations: (1) inserts and updates, then (2) deletes`. See [query-language.md](../queries/index.md) for the rule.
- `MergeConflicts(Vec<MergeConflict>)`
Compiler-side `NanoError` covers parse / catalog / type / storage / plan / execution / arrow / lance / IO / manifest / unique-constraint, each with structured spans (`SourceSpan { start, end }`) for ariadne-style diagnostics.
Compiler-side `CompilerError` covers parse / catalog / type / storage / plan / execution / arrow / lance / IO / manifest / unique-constraint, each with structured spans (`SourceSpan { start, end }`) for ariadne-style diagnostics. The legacy `NanoError` name remains as a deprecated compatibility alias.
## Result serialization (`omnigraph_compiler::result::QueryResult`)