use selene_core::{DbString, LabelSet, PropertyValueType};
mod context;
pub use context::{ConditionClause, ExpectedType, PatternElementKind, Side, TypeMismatchContext};
use crate::{
GqlStatus, GqlType, PathMode, PathSelector, ProcedureMutability, SourceSpan,
analyze::binding::BindingDeclKind,
};
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
#[non_exhaustive]
pub enum AnalysisError {
#[error("undefined reference: {name}")]
#[diagnostic(code(SLENE_GQL_42N03))]
UndefinedReference {
name: DbString,
#[label("not bound in scope")]
span: SourceSpan,
#[help]
hint: Option<String>,
},
#[error("binding {name} is already declared in this scope")]
#[diagnostic(code(SLENE_GQL_42N10))]
Shadow {
name: DbString,
#[label("conflicts with an earlier binding")]
span: SourceSpan,
#[label("first declared here")]
prior_span: SourceSpan,
},
#[error(
"pattern variable {name} is already bound as a {prior} and cannot be reused as a {current}"
)]
#[diagnostic(code(SLENE_GQL_42N10))]
PatternKindMismatch {
name: DbString,
prior: PatternElementKind,
current: PatternElementKind,
#[label("incompatible reuse")]
span: SourceSpan,
#[label("first declared here")]
prior_span: SourceSpan,
},
#[error(
"binding {name} is already bound as {prior_kind:?} and cannot be reused as a {new_kind}"
)]
#[diagnostic(code(SLENE_GQL_42N10))]
AliasReusedAsPatternBinding {
name: DbString,
prior_kind: BindingDeclKind,
new_kind: PatternElementKind,
#[label("alias cannot be reused as a pattern binding")]
span: SourceSpan,
},
#[error("not implemented: {message}")]
#[diagnostic(code(SLENE_GQL_42N01))]
NotImplemented {
message: String,
#[label("not implemented yet")]
span: SourceSpan,
#[help]
hint: Option<String>,
},
#[error(
"unbounded variable-length edge pattern requires a restrictive path mode, selective path selector, or DIFFERENT EDGES match mode"
)]
#[diagnostic(code(SLENE_GQL_42001))]
UnboundedRequiresGate {
mode: PathMode,
selector: Option<PathSelector>,
#[label("unbounded quantifier requires an ISO 16.4 gate")]
span: SourceSpan,
},
#[error("invalid VALUE subquery shape: {message}")]
#[diagnostic(code(SLENE_GQL_42001))]
ValueSubqueryShapeViolation {
message: String,
#[label("violates ISO 20.6 scalar value query expression shape")]
span: SourceSpan,
},
#[error("invalid aggregate expression: {message}")]
#[diagnostic(code(SLENE_GQL_42001))]
AggregateNestingViolation {
message: String,
#[label("aggregate cannot contain another aggregate")]
span: SourceSpan,
},
#[error("grouped projection item must be a grouping key or aggregate expression")]
#[diagnostic(code(SLENE_GQL_42001))]
GroupedProjectionItemNotGrouped {
#[label("not a grouping key or aggregate expression")]
span: SourceSpan,
},
#[error("RETURN * requires a non-unit incoming binding table")]
#[diagnostic(code(SLENE_GQL_42001))]
ReturnStarRequiresInput {
#[label("no incoming bindings to expand")]
span: SourceSpan,
},
#[error("ORDER BY sort key cannot contain a nested query specification")]
#[diagnostic(code(SLENE_GQL_42001))]
SortKeyContainsNestedQuery {
#[label("nested query specification is not allowed in a sort key")]
span: SourceSpan,
},
#[error("ORDER BY sort key cannot contain an aggregate function in this RETURN context")]
#[diagnostic(code(SLENE_GQL_42001))]
SortKeyContainsAggregate {
#[label("aggregate function is not allowed in this sort key")]
span: SourceSpan,
},
#[error("invalid reference: {message}")]
#[diagnostic(code(SLENE_GQL_42002))]
InvalidReference {
message: String,
#[label("invalid reference here")]
span: SourceSpan,
},
#[error("expression nesting depth {depth} exceeds analyzer limit")]
#[diagnostic(code(SLENE_GQL_5GQL1))]
RecursionLimitExceeded {
depth: u32,
},
#[error("{context}: expected {expected}, found {found:?}")]
#[diagnostic(code(SLENE_GQL_22G03))]
TypeMismatch {
context: TypeMismatchContext,
expected: ExpectedType,
found: GqlType,
#[label("incompatible type")]
span: SourceSpan,
},
#[error("conflicting declared types for parameter ${name}")]
#[diagnostic(code(SLENE_GQL_22G03))]
ConflictingParameterTypes {
name: DbString,
declarations: Vec<(GqlType, SourceSpan)>,
},
#[error("unknown procedure: {}", display_qualified_name(name))]
#[diagnostic(code(SLENE_GQL_42N04))]
UnknownProcedure {
name: Box<[DbString]>,
#[label("procedure is not registered")]
span: SourceSpan,
},
#[error(
"wrong argument count for {}: expected {}, found {actual}",
display_qualified_name(procedure),
display_argument_range(*minimum, *expected)
)]
#[diagnostic(code(SLENE_GQL_22G03))]
WrongArgumentCount {
procedure: Box<[DbString]>,
expected: usize,
minimum: usize,
actual: usize,
#[label("wrong number of arguments")]
span: SourceSpan,
},
#[error(
"unknown YIELD column {column} for procedure {}",
display_qualified_name(procedure)
)]
#[diagnostic(code(SLENE_GQL_42N03))]
UnknownYieldColumn {
procedure: Box<[DbString]>,
column: DbString,
#[label("column is not produced by this procedure")]
span: SourceSpan,
},
#[error(
"mutating procedure {} cannot be invoked in a read pipeline",
display_qualified_name(procedure)
)]
#[diagnostic(code(SLENE_GQL_25G02))]
MutatingProcedureInReadPipeline {
procedure: Box<[DbString]>,
mutability: ProcedureMutability,
#[label("read pipelines cannot invoke mutating procedures")]
span: SourceSpan,
},
#[error("{labels:?} does not match any node type in graph type {graph_type}")]
#[diagnostic(code(SLENE_A_010))]
SchemaUnknownNodeType {
labels: LabelSet,
graph_type: DbString,
#[label("unknown node type")]
span: SourceSpan,
},
#[error("edge label {label} does not match any edge type in graph type {graph_type}")]
#[diagnostic(code(SLENE_A_011))]
SchemaUnknownEdgeType {
label: DbString,
graph_type: DbString,
#[label("unknown edge type")]
span: SourceSpan,
},
#[error(
"edge label {label}: declared as {expected_source} -> {expected_target} but used as {observed_source:?} -> {observed_target:?}"
)]
#[diagnostic(code(SLENE_A_012))]
SchemaEdgeEndpointMismatch {
label: DbString,
expected_source: String,
expected_target: String,
observed_source: Box<LabelSet>,
observed_target: Box<LabelSet>,
#[label("endpoint types do not match edge declaration")]
span: SourceSpan,
},
#[error("property {property} is not declared by {declared_in} in graph type {graph_type}")]
#[diagnostic(code(SLENE_A_013))]
SchemaUndeclaredProperty {
property: DbString,
declared_in: DbString,
graph_type: DbString,
#[label("property is not declared")]
span: SourceSpan,
},
#[error("property {property} of {declared_in} declared {expected} but value is {found:?}")]
#[diagnostic(code(SLENE_A_014))]
SchemaPropertyTypeMismatch {
property: DbString,
declared_in: DbString,
expected: PropertyValueType,
found: GqlType,
#[label("value type is incompatible with property declaration")]
span: SourceSpan,
},
#[error("required property {property} of {declared_in} missing at INSERT site")]
#[diagnostic(code(SLENE_A_015))]
SchemaRequiredPropertyMissing {
property: DbString,
declared_in: DbString,
#[label("required property is not supplied")]
span: SourceSpan,
},
#[error("required property {property} of {declared_in} cannot be REMOVE'd")]
#[diagnostic(code(SLENE_A_016))]
SchemaRequiredPropertyRemoved {
property: DbString,
declared_in: DbString,
#[label("required property cannot be removed")]
span: SourceSpan,
},
#[error("INSERT requires a single label or label conjunction; {form} is not allowed")]
#[diagnostic(code(SLENE_A_017))]
SchemaInvalidInsertLabelExpr {
form: InvalidLabelForm,
#[label("invalid INSERT label expression")]
span: SourceSpan,
},
#[error("required edge label {label} of {declared_in} cannot be REMOVE'd")]
#[diagnostic(code(SLENE_A_018))]
SchemaRequiredEdgeLabelRemoved {
label: DbString,
declared_in: DbString,
#[label("edge label cannot be removed")]
span: SourceSpan,
},
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum InvalidLabelForm {
Disjunction,
Negation,
Wildcard,
Missing,
}
impl std::fmt::Display for InvalidLabelForm {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(match self {
Self::Disjunction => "label disjunction",
Self::Negation => "label negation",
Self::Wildcard => "label wildcard",
Self::Missing => "missing label",
})
}
}
fn display_qualified_name(name: &[DbString]) -> QualifiedNameDisplay<'_> {
QualifiedNameDisplay(name)
}
struct QualifiedNameDisplay<'a>(&'a [DbString]);
impl std::fmt::Display for QualifiedNameDisplay<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fmt_qualified_name(f, self.0)
}
}
fn fmt_qualified_name(f: &mut std::fmt::Formatter<'_>, name: &[DbString]) -> std::fmt::Result {
let mut first = true;
for segment in name {
if !first {
f.write_str(".")?;
}
let text = segment.as_str();
if text.contains('.') || text.contains('"') {
write!(f, "\"{}\"", text.replace('"', "\"\""))?;
} else {
f.write_str(text)?;
}
first = false;
}
Ok(())
}
fn display_argument_range(minimum: usize, maximum: usize) -> String {
if minimum == maximum {
maximum.to_string()
} else {
format!("{minimum}..={maximum}")
}
}
impl AnalysisError {
#[must_use]
pub const fn gqlstatus(&self) -> GqlStatus {
match self {
Self::UndefinedReference { .. } => GqlStatus::UNDEFINED_REFERENCE,
Self::Shadow { .. }
| Self::PatternKindMismatch { .. }
| Self::AliasReusedAsPatternBinding { .. } => GqlStatus::DUPLICATE_OBJECT,
Self::NotImplemented { .. } => GqlStatus::FEATURE_NOT_SUPPORTED,
Self::UnboundedRequiresGate { .. } => GqlStatus::SYNTAX_ERROR,
Self::ValueSubqueryShapeViolation { .. } => GqlStatus::SYNTAX_ERROR,
Self::AggregateNestingViolation { .. } => GqlStatus::SYNTAX_ERROR,
Self::GroupedProjectionItemNotGrouped { .. } => GqlStatus::SYNTAX_ERROR,
Self::ReturnStarRequiresInput { .. } => GqlStatus::SYNTAX_ERROR,
Self::SortKeyContainsNestedQuery { .. } => GqlStatus::SYNTAX_ERROR,
Self::SortKeyContainsAggregate { .. } => GqlStatus::SYNTAX_ERROR,
Self::InvalidReference { .. } => GqlStatus::INVALID_REFERENCE,
Self::RecursionLimitExceeded { .. } => GqlStatus::PROGRAM_LIMIT_EXCEEDED,
Self::TypeMismatch { .. } | Self::ConflictingParameterTypes { .. } => {
GqlStatus::DATATYPE_MISMATCH
}
Self::UnknownProcedure { .. } => GqlStatus::UNKNOWN_PROCEDURE,
Self::WrongArgumentCount { .. } => GqlStatus::DATATYPE_MISMATCH,
Self::UnknownYieldColumn { .. } => GqlStatus::UNDEFINED_REFERENCE,
Self::MutatingProcedureInReadPipeline { .. } => {
GqlStatus::INVALID_TRANSACTION_STATE_MIXING
}
Self::SchemaUnknownNodeType { .. }
| Self::SchemaUnknownEdgeType { .. }
| Self::SchemaEdgeEndpointMismatch { .. }
| Self::SchemaUndeclaredProperty { .. }
| Self::SchemaPropertyTypeMismatch { .. }
| Self::SchemaRequiredPropertyMissing { .. }
| Self::SchemaRequiredPropertyRemoved { .. }
| Self::SchemaInvalidInsertLabelExpr { .. }
| Self::SchemaRequiredEdgeLabelRemoved { .. } => GqlStatus::GRAPH_TYPE_VIOLATION,
}
}
pub(crate) fn undefined_reference(name: DbString, span: SourceSpan) -> Self {
Self::UndefinedReference {
name,
span,
hint: Some("declare the variable before this reference".into()),
}
}
}