From 4590c91f9d7f53e86fdb461eab99803b5e833548 Mon Sep 17 00:00:00 2001 From: aaltshuler Date: Wed, 17 Jun 2026 23:44:24 +0300 Subject: [PATCH] rename compiler NanoError and fix cluster config warnings --- crates/omnigraph-cli/src/main.rs | 4 +- crates/omnigraph-cli/tests/cli_cluster.rs | 4 + crates/omnigraph-cluster/src/lib.rs | 4 +- crates/omnigraph-cluster/src/store.rs | 26 +++ crates/omnigraph-cluster/src/sweep.rs | 19 +- crates/omnigraph-cluster/src/tests.rs | 61 ++++++ crates/omnigraph-compiler/src/catalog/mod.rs | 12 +- .../src/catalog/schema_ir.rs | 12 +- crates/omnigraph-compiler/src/error.rs | 82 ++++++- crates/omnigraph-compiler/src/ir/lower.rs | 6 +- crates/omnigraph-compiler/src/query/parser.rs | 185 ++++++++-------- .../omnigraph-compiler/src/query/typecheck.rs | 206 +++++++++--------- crates/omnigraph-compiler/src/query_input.rs | 18 +- crates/omnigraph-compiler/src/result.rs | 6 +- .../omnigraph-compiler/src/schema/parser.rs | 183 ++++++++-------- crates/omnigraph/src/error.rs | 2 +- docs/user/operations/errors.md | 2 +- 17 files changed, 499 insertions(+), 333 deletions(-) diff --git a/crates/omnigraph-cli/src/main.rs b/crates/omnigraph-cli/src/main.rs index bb3b062..fa6f4db 100644 --- a/crates/omnigraph-cli/src/main.rs +++ b/crates/omnigraph-cli/src/main.rs @@ -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 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 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; diff --git a/crates/omnigraph-cli/tests/cli_cluster.rs b/crates/omnigraph-cli/tests/cli_cluster.rs index e35a54d..d2b6d13 100644 --- a/crates/omnigraph-cli/tests/cli_cluster.rs +++ b/crates/omnigraph-cli/tests/cli_cluster.rs @@ -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] diff --git a/crates/omnigraph-cluster/src/lib.rs b/crates/omnigraph-cluster/src/lib.rs index 1c4e4fc..0dad78c 100644 --- a/crates/omnigraph-cluster/src/lib.rs +++ b/crates/omnigraph-cluster/src/lib.rs @@ -160,7 +160,7 @@ pub async fn plan_config_dir(config_dir: impl AsRef) -> 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 = None; @@ -1260,7 +1260,7 @@ pub async fn status_config_dir(config_dir: impl AsRef) -> 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(); diff --git a/crates/omnigraph-cluster/src/store.rs b/crates/omnigraph-cluster/src/store.rs index 5129397..9a2e748 100644 --- a/crates/omnigraph-cluster/src/store.rs +++ b/crates/omnigraph-cluster/src/store.rs @@ -321,6 +321,32 @@ impl ClusterStore { // ---- recovery sidecars ---- + pub(crate) async fn list_recovery_sidecar_locations( + &self, + diagnostics: &mut Vec, + ) -> Vec { + 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, diff --git a/crates/omnigraph-cluster/src/sweep.rs b/crates/omnigraph-cluster/src/sweep.rs index 6539cae..27e6e9c 100644 --- a/crates/omnigraph-cluster/src/sweep.rs +++ b/crates/omnigraph-cluster/src/sweep.rs @@ -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) { - let recoveries_dir = config_dir.join(CLUSTER_RECOVERIES_DIR); - let Ok(entries) = fs::read_dir(&recoveries_dir) else { - return; - }; - let mut names: Vec = 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, +) { + 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", )); } diff --git a/crates/omnigraph-cluster/src/tests.rs b/crates/omnigraph-cluster/src/tests.rs index ac448cf..536e904 100644 --- a/crates/omnigraph-cluster/src/tests.rs +++ b/crates/omnigraph-cluster/src/tests.rs @@ -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(); diff --git a/crates/omnigraph-compiler/src/catalog/mod.rs b/crates/omnigraph-compiler/src/catalog/mod.rs index 93f8d89..2287c3b 100644 --- a/crates/omnigraph-compiler/src/catalog/mod.rs +++ b/crates/omnigraph-compiler/src/catalog/mod.rs @@ -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 { 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 { 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 { 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 ))); diff --git a/crates/omnigraph-compiler/src/catalog/schema_ir.rs b/crates/omnigraph-compiler/src/catalog/schema_ir.rs index d90539e..4a56ffa 100644 --- a/crates/omnigraph-compiler/src/catalog/schema_ir.rs +++ b/crates/omnigraph-compiler/src/catalog/schema_ir.rs @@ -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 { pub fn build_catalog_from_ir(ir: &SchemaIR) -> Result { 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 { pub fn schema_ir_json(ir: &SchemaIR) -> Result { 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 { 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 { @@ -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 ))); diff --git a/crates/omnigraph-compiler/src/error.rs b/crates/omnigraph-compiler/src/error.rs index ea48759..cbf5c4d 100644 --- a/crates/omnigraph-compiler/src/error.rs +++ b/crates/omnigraph-compiler/src/error.rs @@ -55,7 +55,7 @@ pub fn decode_string_literal(raw: &str) -> Result { 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 { '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 { } #[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 = std::result::Result; +#[deprecated(note = "use CompilerError")] +pub type NanoError = CompilerError; + +pub type Result = std::result::Result; #[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() + } } diff --git a/crates/omnigraph-compiler/src/ir/lower.rs b/crates/omnigraph-compiler/src/ir/lower.rs index 6999d69..9427e27 100644 --- a/crates/omnigraph-compiler/src/ir/lower.rs +++ b/crates/omnigraph-compiler/src/ir/lower.rs @@ -14,7 +14,7 @@ pub fn lower_query( type_ctx: &TypeContext, ) -> Result { 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 { 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 )) diff --git a/crates/omnigraph-compiler/src/query/parser.rs b/crates/omnigraph-compiler/src/query/parser.rs index 4ba8476..3284876 100644 --- a/crates/omnigraph-compiler/src/query/parser.rs +++ b/crates/omnigraph-compiler/src/query/parser.rs @@ -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 { - 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 { @@ -24,7 +24,7 @@ pub fn parse_query_diagnostic(input: &str) -> std::result::Result) -> 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) -> Result { 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) -> Result { } "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) -> Result { } } 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) -> Result { let int_pair = section.into_inner().next().unwrap(); limit = Some(int_pair.as_str().parse::().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) -> Result { 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) -> 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) -> 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) -> Result { 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) -> Result { } 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) -> Result 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) -> Result { 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) -> Result) -> Result) -> Result { } 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) -> Result { 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) -> 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::() - .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::() - .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) -> Result { "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) -> Result { 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) -> Result { 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) -> Result { 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) -> Result { 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) -> Result { 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) -> Result { 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) -> Result { 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) -> Result { "<" => 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) -> Result { 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) -> Result { "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) -> Result { .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) -> Result { .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) -> Result { } 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) -> Result { 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) -> Result { (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) -> Result { 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, diff --git a/crates/omnigraph-compiler/src/query/typecheck.rs b/crates/omnigraph-compiler/src/query/typecheck.rs index b2c235a..2ac1604 100644 --- a/crates/omnigraph-compiler/src/query/typecheck.rs +++ b/crates/omnigraph-compiler/src/query/typecheck.rs @@ -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 Result { 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 Result Result Result {} _ => { - 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 = node_type .arrow_schema @@ -1450,14 +1450,14 @@ fn literal_type(lit: &Literal) -> Result { } 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(", "), diff --git a/crates/omnigraph-compiler/src/query_input.rs b/crates/omnigraph-compiler/src/query_input.rs index b85decf..b641f3e 100644 --- a/crates/omnigraph-compiler/src/query_input.rs +++ b/crates/omnigraph-compiler/src/query_input.rs @@ -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 for RunInputError { - fn from(value: NanoError) -> Self { +impl From 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 { 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 { 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 { 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 { 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 { if !self.is_finite() { - return Err(NanoError::Execution(format!( + return Err(CompilerError::Execution(format!( "invalid float parameter {}", self ))); diff --git a/crates/omnigraph-compiler/src/result.rs b/crates/omnigraph-compiler/src/result.rs index 7de77ac..d92dd1e 100644 --- a/crates/omnigraph-compiler/src/result.rs +++ b/crates/omnigraph-compiler/src/result.rs @@ -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(&self) -> Result { 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)) }) } diff --git a/crates/omnigraph-compiler/src/schema/parser.rs b/crates/omnigraph-compiler/src/schema/parser.rs index c5f4355..6e34e53 100644 --- a/crates/omnigraph-compiler/src/schema/parser.rs +++ b/crates/omnigraph-compiler/src/schema/parser.rs @@ -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 { - 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 { @@ -27,7 +27,8 @@ pub fn parse_schema_diagnostic(input: &str) -> std::result::Result std::result::Result = 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) -> 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) -> Result { 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) -> Result { let min_str = inner.next().unwrap().as_str(); let min = min_str .parse::() - .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::() - .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::().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) -> Result>>()?; 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) -> Result { 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) -> Result { 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) -> Result { // @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) -> Result { // @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) -> Result 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) -> 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) -> 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) -> Result 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) -> Result { 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::() - .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) -> Result { 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) -> Result { } } 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) -> Result { 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, diff --git a/crates/omnigraph/src/error.rs b/crates/omnigraph/src/error.rs index 11f4da0..a24f153 100644 --- a/crates/omnigraph/src/error.rs +++ b/crates/omnigraph/src/error.rs @@ -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}")] diff --git a/docs/user/operations/errors.md b/docs/user/operations/errors.md index 48f1fc9..85b4fde 100644 --- a/docs/user/operations/errors.md +++ b/docs/user/operations/errors.md @@ -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 '' 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)` -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`)