mirror of
https://github.com/ModernRelay/omnigraph.git
synced 2026-06-18 02:24:27 +02:00
Merge pull request #278 from ModernRelay/codex/compiler-error-rename
Rename compiler error and fix cluster config warnings
This commit is contained in:
commit
a9a5453520
17 changed files with 534 additions and 333 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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| {
|
||||
let name = uri.rsplit_once('/').map_or(uri.as_str(), |(_, name)| name);
|
||||
format!("{}/{name}", self.display(CLUSTER_RECOVERIES_DIR))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) async fn list_recovery_sidecars(
|
||||
&self,
|
||||
diagnostics: &mut Vec<Diagnostic>,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3375,6 +3375,96 @@ policies:
|
|||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_only_commands_ignore_missing_recovery_sidecar_dir() {
|
||||
let dir = fixture();
|
||||
write_applyable_state(dir.path());
|
||||
assert!(!dir.path().join(CLUSTER_RECOVERIES_DIR).exists());
|
||||
|
||||
let status = status_config_dir(dir.path()).await;
|
||||
assert!(status.ok, "{:?}", status.diagnostics);
|
||||
assert!(
|
||||
!status.diagnostics.iter().any(|diagnostic| matches!(
|
||||
diagnostic.code.as_str(),
|
||||
"recovery_sidecar_read_error" | "cluster_recovery_pending"
|
||||
)),
|
||||
"{:?}",
|
||||
status.diagnostics
|
||||
);
|
||||
|
||||
let plan = plan_config_dir(dir.path()).await;
|
||||
assert!(plan.ok, "{:?}", plan.diagnostics);
|
||||
assert!(
|
||||
!plan.diagnostics.iter().any(|diagnostic| matches!(
|
||||
diagnostic.code.as_str(),
|
||||
"recovery_sidecar_read_error" | "cluster_recovery_pending"
|
||||
)),
|
||||
"{:?}",
|
||||
plan.diagnostics
|
||||
);
|
||||
}
|
||||
|
||||
#[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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)));
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)));
|
||||
|
|
|
|||
|
|
@ -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,77 @@ 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, &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() {
|
||||
if matches!(
|
||||
path.file_name().and_then(|name| name.to_str()),
|
||||
Some(".git" | "target")
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
))
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(", "),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)));
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}")]
|
||||
|
|
|
|||
|
|
@ -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`)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue