command: Operand carries a source span
Each WHERE-expression Operand now records the byte span of the terminal it was built from — the precise per-literal highlight target for an expression WARNING (finishing ADR-0027 §2's highlight/hint wiring). parse_operand captures MatchedItem::span; the RowFilter::eq convenience constructor uses Operand::NO_SPAN. PartialEq is hand-written to ignore the span — it is editor metadata, so Command equality stays whitespace- and position-independent, which the Expr test corpus relies on. No behaviour change; 1100 tests still pass, clippy clean.
This commit is contained in:
+49
-5
@@ -257,9 +257,15 @@ impl RowFilter {
|
||||
#[must_use]
|
||||
pub fn eq(column: impl Into<String>, value: Value) -> Self {
|
||||
Self::Where(Expr::Predicate(Predicate::Compare {
|
||||
left: Operand::Column(column.into()),
|
||||
left: Operand::Column {
|
||||
name: column.into(),
|
||||
span: Operand::NO_SPAN,
|
||||
},
|
||||
op: CompareOp::Eq,
|
||||
right: Operand::Literal(value),
|
||||
right: Operand::Literal {
|
||||
value,
|
||||
span: Operand::NO_SPAN,
|
||||
},
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -322,10 +328,48 @@ pub enum Predicate {
|
||||
|
||||
/// A comparison operand — a column reference or a literal
|
||||
/// (ADR-0026 §1: operands are never nested expressions).
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
///
|
||||
/// Each operand carries the byte `span` it occupied in the
|
||||
/// source. The span feeds the precise per-literal WARNING
|
||||
/// highlight (ADR-0027) and is otherwise editor metadata —
|
||||
/// `PartialEq` is hand-written to **ignore** it, so two
|
||||
/// operands are equal when their column / value match
|
||||
/// regardless of where they were typed. This keeps `Command`
|
||||
/// equality whitespace- and position-independent (the bulk of
|
||||
/// the `Expr` test corpus relies on it).
|
||||
#[derive(Debug, Clone, Eq)]
|
||||
pub enum Operand {
|
||||
Column(String),
|
||||
Literal(Value),
|
||||
Column { name: String, span: (usize, usize) },
|
||||
Literal { value: Value, span: (usize, usize) },
|
||||
}
|
||||
|
||||
impl Operand {
|
||||
/// Span used for operands built without a source position
|
||||
/// (the [`RowFilter::eq`] convenience constructor).
|
||||
pub const NO_SPAN: (usize, usize) = (0, 0);
|
||||
|
||||
/// The byte range this operand occupied in the source —
|
||||
/// [`Operand::NO_SPAN`] for programmatically-built operands.
|
||||
#[must_use]
|
||||
pub const fn span(&self) -> (usize, usize) {
|
||||
match self {
|
||||
Self::Column { span, .. } | Self::Literal { span, .. } => *span,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Operand {
|
||||
/// Compares column name / literal value only — the source
|
||||
/// `span` is deliberately excluded (see the type docs).
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Self::Column { name: a, .. }, Self::Column { name: b, .. }) => a == b,
|
||||
(Self::Literal { value: a, .. }, Self::Literal { value: b, .. }) => {
|
||||
a == b
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The six comparison operators. `<>` and `!=` both parse to
|
||||
|
||||
+30
-15
@@ -520,22 +520,28 @@ impl<'a> ExprParser<'a> {
|
||||
}
|
||||
|
||||
/// `operand := literal | column_ref`.
|
||||
///
|
||||
/// Every operand records the byte `span` of the terminal it
|
||||
/// was built from — the precise highlight target for an
|
||||
/// expression WARNING (ADR-0027).
|
||||
fn parse_operand(&mut self) -> Result<Operand, ValidationError> {
|
||||
let item = self
|
||||
.advance()
|
||||
.ok_or_else(|| drift_error("expected an operand"))?;
|
||||
let span = item.span;
|
||||
let literal = |value: Value| Operand::Literal { value, span };
|
||||
match &item.kind {
|
||||
MatchedKind::Ident { role: "expr_column", .. } => {
|
||||
Ok(Operand::Column(item.text.clone()))
|
||||
Ok(Operand::Column { name: item.text.clone(), span })
|
||||
}
|
||||
MatchedKind::Word("null") => Ok(Operand::Literal(Value::Null)),
|
||||
MatchedKind::Word("true") => Ok(Operand::Literal(Value::Bool(true))),
|
||||
MatchedKind::Word("false") => Ok(Operand::Literal(Value::Bool(false))),
|
||||
MatchedKind::Word("null") => Ok(literal(Value::Null)),
|
||||
MatchedKind::Word("true") => Ok(literal(Value::Bool(true))),
|
||||
MatchedKind::Word("false") => Ok(literal(Value::Bool(false))),
|
||||
MatchedKind::NumberLit => {
|
||||
Ok(Operand::Literal(Value::Number(item.text.clone())))
|
||||
Ok(literal(Value::Number(item.text.clone())))
|
||||
}
|
||||
MatchedKind::StringLit => {
|
||||
Ok(Operand::Literal(Value::Text(item.text.clone())))
|
||||
Ok(literal(Value::Text(item.text.clone())))
|
||||
}
|
||||
_ => Err(drift_error("expected a column or literal operand")),
|
||||
}
|
||||
@@ -598,11 +604,24 @@ mod tests {
|
||||
}
|
||||
|
||||
fn col(name: &str) -> Operand {
|
||||
Operand::Column(name.to_string())
|
||||
Operand::Column {
|
||||
name: name.to_string(),
|
||||
span: Operand::NO_SPAN,
|
||||
}
|
||||
}
|
||||
|
||||
/// A literal operand with no source span — the span is
|
||||
/// ignored by `Operand`'s `PartialEq`, so test trees built
|
||||
/// with this compare equal to walked ones.
|
||||
fn lit(value: Value) -> Operand {
|
||||
Operand::Literal {
|
||||
value,
|
||||
span: Operand::NO_SPAN,
|
||||
}
|
||||
}
|
||||
|
||||
fn num(n: &str) -> Operand {
|
||||
Operand::Literal(Value::Number(n.to_string()))
|
||||
lit(Value::Number(n.to_string()))
|
||||
}
|
||||
|
||||
fn compare(left: Operand, op: CompareOp, right: Operand) -> Expr {
|
||||
@@ -644,16 +663,12 @@ mod tests {
|
||||
compare(
|
||||
col("Name"),
|
||||
CompareOp::Eq,
|
||||
Operand::Literal(Value::Text("Ada".to_string())),
|
||||
lit(Value::Text("Ada".to_string())),
|
||||
),
|
||||
);
|
||||
assert_eq!(
|
||||
parse_expr("Active = true"),
|
||||
compare(
|
||||
col("Active"),
|
||||
CompareOp::Eq,
|
||||
Operand::Literal(Value::Bool(true)),
|
||||
),
|
||||
compare(col("Active"), CompareOp::Eq, lit(Value::Bool(true))),
|
||||
);
|
||||
assert_eq!(
|
||||
parse_expr("a = -7"),
|
||||
@@ -708,7 +723,7 @@ mod tests {
|
||||
parse_expr("Name like 'A%'"),
|
||||
Expr::Predicate(Predicate::Like {
|
||||
target: col("Name"),
|
||||
pattern: Operand::Literal(Value::Text("A%".to_string())),
|
||||
pattern: lit(Value::Text("A%".to_string())),
|
||||
negated: false,
|
||||
}),
|
||||
);
|
||||
|
||||
+11
-3
@@ -542,7 +542,13 @@ fn predicate_warnings(
|
||||
}
|
||||
|
||||
const fn is_null_literal(operand: &Operand) -> bool {
|
||||
matches!(operand, Operand::Literal(crate::dsl::value::Value::Null))
|
||||
matches!(
|
||||
operand,
|
||||
Operand::Literal {
|
||||
value: crate::dsl::value::Value::Null,
|
||||
..
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/// If one operand is a known column and the other a non-null
|
||||
@@ -556,8 +562,10 @@ fn pair_type_mismatch(
|
||||
columns: &[crate::completion::TableColumn],
|
||||
) -> Option<String> {
|
||||
let (column, literal) = match (a, b) {
|
||||
(Operand::Column(c), Operand::Literal(v))
|
||||
| (Operand::Literal(v), Operand::Column(c)) => (c, v),
|
||||
(Operand::Column { name: c, .. }, Operand::Literal { value: v, .. })
|
||||
| (Operand::Literal { value: v, .. }, Operand::Column { name: c, .. }) => {
|
||||
(c, v)
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
// `null` fits any column; `= NULL` is flagged separately.
|
||||
|
||||
Reference in New Issue
Block a user