WHERE expressions: wire into update/delete/show data + SQL gen (ADR-0026 steps 3-4)
Wires the stratified WHERE-expression fragment into the three
filter commands and compiles the resulting Expr to SQL.
Grammar (data.rs): the `update` / `delete` `where` clause is
now the expression fragment (`Subgrammar(&expr::OR_EXPR)`) in
place of the single `col = val` slot; `show data` gains an
optional `where <expr>` and an optional `limit <n>` (a
non-negative integer, validated at parse time). The
expression's right-hand operands are a schema-aware
`DynamicSubgrammar` so the hint panel still narrows to the
left column's type (ADR-0026 §8) — but the inner grammar is
permissive: a type-mismatched literal still parses (§7).
AST: `RowFilter::Where{column,value}` -> `RowFilter::Where(Expr)`;
`ShowData` gains `filter: Option<Expr>` and `limit: Option<u64>`.
A `RowFilter::eq` convenience constructor keeps simple-equality
call sites and tests readable.
SQL (db.rs): `compile_expr` lowers an `Expr` to a
parameterised WHERE — every literal a `?` placeholder,
identifiers `quote_ident`-quoted, `<>` for inequality. A
literal compared against a column binds through that column's
type where compatible and falls back to its syntactic shape on
a mismatch (§7 — permissive). `show data ... limit n` emits
`LIMIT ?` with an implicit primary-key `ORDER BY`, so it is a
stable "first n by primary key".
completion.rs: `invalid_ident_at_cursor` no longer mis-flags a
digit-led literal (`1`) as an unknown column now that the
WHERE operand slot also accepts a column reference; a
`ProseOnly` slot suppresses keyword candidates even when the
expected set also carries a column ident.
11 db integration tests cover AND / OR / NOT, BETWEEN, IN,
LIKE, filtered `show data`, and limit ordering; walker and
expr unit tests cover the parse surface. Type-mismatch /
`= NULL` diagnostic flagging (§7 highlight + hint) is the
remaining ADR-0026 piece.
This commit is contained in:
@@ -31,7 +31,10 @@ use tokio::sync::{mpsc, oneshot};
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use crate::dsl::action::ReferentialAction;
|
||||
use crate::dsl::command::{ChangeColumnMode, IndexSelector, RelationshipSelector, RowFilter};
|
||||
use crate::dsl::command::{
|
||||
ChangeColumnMode, CompareOp, Expr, IndexSelector, Operand, Predicate,
|
||||
RelationshipSelector, RowFilter,
|
||||
};
|
||||
use crate::dsl::ColumnSpec;
|
||||
use crate::dsl::shortid;
|
||||
use crate::dsl::types::Type;
|
||||
@@ -507,6 +510,8 @@ enum Request {
|
||||
},
|
||||
QueryData {
|
||||
table: String,
|
||||
filter: Option<Expr>,
|
||||
limit: Option<u64>,
|
||||
source: Option<String>,
|
||||
reply: oneshot::Sender<Result<DataResult, DbError>>,
|
||||
},
|
||||
@@ -888,11 +893,15 @@ impl Database {
|
||||
pub async fn query_data(
|
||||
&self,
|
||||
table: String,
|
||||
filter: Option<Expr>,
|
||||
limit: Option<u64>,
|
||||
source: Option<String>,
|
||||
) -> Result<DataResult, DbError> {
|
||||
let (reply, recv) = oneshot::channel();
|
||||
self.send(Request::QueryData {
|
||||
table,
|
||||
filter,
|
||||
limit,
|
||||
source,
|
||||
reply,
|
||||
})
|
||||
@@ -1278,6 +1287,8 @@ fn handle_request(conn: &Connection, persistence: Option<&Persistence>, req: Req
|
||||
}
|
||||
Request::QueryData {
|
||||
table,
|
||||
filter,
|
||||
limit,
|
||||
source,
|
||||
reply,
|
||||
} => {
|
||||
@@ -1286,6 +1297,8 @@ fn handle_request(conn: &Connection, persistence: Option<&Persistence>, req: Req
|
||||
persistence,
|
||||
source.as_deref(),
|
||||
&table,
|
||||
filter.as_ref(),
|
||||
limit,
|
||||
));
|
||||
}
|
||||
Request::RebuildFromText {
|
||||
@@ -4128,6 +4141,182 @@ fn bound_to_sqlite_value(b: &Bound) -> rusqlite::types::Value {
|
||||
}
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// WHERE-expression → parameterised SQL (ADR-0026 §6)
|
||||
// =================================================================
|
||||
|
||||
/// Compile an `Expr` to a parameterised SQL boolean expression.
|
||||
///
|
||||
/// Every literal becomes a `?` placeholder pushed onto `params`
|
||||
/// (1-based, continuing from the current `params.len()` — so
|
||||
/// the caller can pre-load SET-clause params); identifiers are
|
||||
/// `quote_ident`-quoted. The raw user text is never spliced
|
||||
/// into the SQL. Connectives, `NOT`, and parentheses come from
|
||||
/// the tree structure; the database re-derives precedence from
|
||||
/// the emitted operators.
|
||||
fn compile_expr(
|
||||
expr: &Expr,
|
||||
schema: &ReadSchema,
|
||||
params: &mut Vec<rusqlite::types::Value>,
|
||||
) -> String {
|
||||
match expr {
|
||||
Expr::Or(terms) => join_expr(terms, "OR", schema, params),
|
||||
Expr::And(terms) => join_expr(terms, "AND", schema, params),
|
||||
Expr::Not(inner) => {
|
||||
format!("(NOT {})", compile_expr(inner, schema, params))
|
||||
}
|
||||
Expr::Predicate(predicate) => compile_predicate(predicate, schema, params),
|
||||
}
|
||||
}
|
||||
|
||||
fn join_expr(
|
||||
terms: &[Expr],
|
||||
op: &str,
|
||||
schema: &ReadSchema,
|
||||
params: &mut Vec<rusqlite::types::Value>,
|
||||
) -> String {
|
||||
let parts: Vec<String> = terms
|
||||
.iter()
|
||||
.map(|t| compile_expr(t, schema, params))
|
||||
.collect();
|
||||
format!("({})", parts.join(&format!(" {op} ")))
|
||||
}
|
||||
|
||||
fn compile_predicate(
|
||||
predicate: &Predicate,
|
||||
schema: &ReadSchema,
|
||||
params: &mut Vec<rusqlite::types::Value>,
|
||||
) -> String {
|
||||
match predicate {
|
||||
Predicate::Compare { left, op, right } => {
|
||||
// A literal on one side binds against the column on
|
||||
// the other side, where there is one.
|
||||
let left_ty = operand_column_type(left, schema);
|
||||
let right_ty = operand_column_type(right, schema);
|
||||
let lhs = compile_operand(left, right_ty, params);
|
||||
let rhs = compile_operand(right, left_ty, params);
|
||||
format!("{lhs} {} {rhs}", compare_op_sql(*op))
|
||||
}
|
||||
Predicate::Like {
|
||||
target,
|
||||
pattern,
|
||||
negated,
|
||||
} => {
|
||||
let t = compile_operand(target, None, params);
|
||||
let p = compile_operand(pattern, None, params);
|
||||
let not = if *negated { "NOT " } else { "" };
|
||||
format!("{t} {not}LIKE {p}")
|
||||
}
|
||||
Predicate::Between {
|
||||
target,
|
||||
low,
|
||||
high,
|
||||
negated,
|
||||
} => {
|
||||
let ty = operand_column_type(target, schema);
|
||||
let t = compile_operand(target, None, params);
|
||||
let lo = compile_operand(low, ty, params);
|
||||
let hi = compile_operand(high, ty, params);
|
||||
let not = if *negated { "NOT " } else { "" };
|
||||
format!("{t} {not}BETWEEN {lo} AND {hi}")
|
||||
}
|
||||
Predicate::In {
|
||||
target,
|
||||
items,
|
||||
negated,
|
||||
} => {
|
||||
let ty = operand_column_type(target, schema);
|
||||
let t = compile_operand(target, None, params);
|
||||
let rendered: Vec<String> = items
|
||||
.iter()
|
||||
.map(|item| compile_operand(item, ty, params))
|
||||
.collect();
|
||||
let not = if *negated { "NOT " } else { "" };
|
||||
format!("{t} {not}IN ({})", rendered.join(", "))
|
||||
}
|
||||
Predicate::IsNull { target, negated } => {
|
||||
let t = compile_operand(target, None, params);
|
||||
let not = if *negated { "NOT " } else { "" };
|
||||
format!("{t} IS {not}NULL")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Render an operand. A column becomes a quoted identifier; a
|
||||
/// literal becomes a `?` placeholder bound against `against`'s
|
||||
/// type when one is supplied.
|
||||
fn compile_operand(
|
||||
operand: &Operand,
|
||||
against: Option<Type>,
|
||||
params: &mut Vec<rusqlite::types::Value>,
|
||||
) -> String {
|
||||
match operand {
|
||||
Operand::Column(name) => quote_ident(name),
|
||||
Operand::Literal(value) => {
|
||||
params.push(bind_where_literal(value, against));
|
||||
format!("?{}", params.len())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The user-facing type of a column operand, if the operand is
|
||||
/// a column the schema knows. Literals and unknown columns
|
||||
/// yield `None`.
|
||||
fn operand_column_type(operand: &Operand, schema: &ReadSchema) -> Option<Type> {
|
||||
match operand {
|
||||
Operand::Column(name) => schema
|
||||
.columns
|
||||
.iter()
|
||||
.find(|c| c.name.eq_ignore_ascii_case(name))
|
||||
.and_then(|c| c.user_type),
|
||||
Operand::Literal(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Bind a WHERE-clause literal. When a target column type is
|
||||
/// known and the literal converts cleanly, bind through it;
|
||||
/// otherwise bind by the literal's own syntactic shape — a
|
||||
/// type-mismatched comparison is flagged in the editor but
|
||||
/// still runs (ADR-0026 §7).
|
||||
fn bind_where_literal(value: &Value, against: Option<Type>) -> rusqlite::types::Value {
|
||||
if let Some(ty) = against
|
||||
&& let Ok(bound) = value.bind_for_column("", ty)
|
||||
{
|
||||
return bound_to_sqlite_value(&bound);
|
||||
}
|
||||
bound_to_sqlite_value(&syntactic_bound(value))
|
||||
}
|
||||
|
||||
/// Bind a literal by the shape it was written in, ignoring any
|
||||
/// column type — the permissive fallback for type-mismatched
|
||||
/// comparisons and the literal-vs-literal case.
|
||||
fn syntactic_bound(value: &Value) -> Bound {
|
||||
match value {
|
||||
Value::Null => Bound::Null,
|
||||
Value::Bool(b) => Bound::Integer(i64::from(*b)),
|
||||
Value::Text(s) => Bound::Text(s.clone()),
|
||||
Value::Number(s) => s.parse::<i64>().map_or_else(
|
||||
|_| {
|
||||
s.parse::<f64>()
|
||||
.map_or_else(|_| Bound::Text(s.clone()), Bound::Real)
|
||||
},
|
||||
Bound::Integer,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
const fn compare_op_sql(op: CompareOp) -> &'static str {
|
||||
match op {
|
||||
CompareOp::Eq => "=",
|
||||
// `<>` is standard SQL for inequality (ADR-0026 §6).
|
||||
CompareOp::NotEq => "<>",
|
||||
CompareOp::Lt => "<",
|
||||
CompareOp::LtEq => "<=",
|
||||
CompareOp::Gt => ">",
|
||||
CompareOp::GtEq => ">=",
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute an INSERT/UPDATE/DELETE and convert any rusqlite
|
||||
/// failure into a `DbError`. Wraps the raw `conn.execute` so the
|
||||
/// three callers (insert, update, delete) have a single hook for
|
||||
@@ -4366,18 +4555,19 @@ fn do_update(
|
||||
// the updated rows even if the UPDATE changed the WHERE column.
|
||||
let rowids = match filter {
|
||||
RowFilter::AllRows => select_all_rowids(conn, table)?,
|
||||
RowFilter::Where { column, value } => {
|
||||
let bound = impl_value_for(&schema, column, value)?;
|
||||
RowFilter::Where(expr) => {
|
||||
let mut where_params: Vec<rusqlite::types::Value> = Vec::new();
|
||||
let clause = compile_expr(expr, &schema, &mut where_params);
|
||||
let mut stmt = conn
|
||||
.prepare(&format!(
|
||||
"SELECT rowid FROM {ident} WHERE {col} = ?1;",
|
||||
"SELECT rowid FROM {ident} WHERE {clause};",
|
||||
ident = quote_ident(table),
|
||||
col = quote_ident(column),
|
||||
))
|
||||
.map_err(DbError::from_rusqlite)?;
|
||||
let bound_param = bound_to_sqlite_value(&bound);
|
||||
let rows = stmt
|
||||
.query_map([&bound_param], |row| row.get::<_, i64>(0))
|
||||
.query_map(rusqlite::params_from_iter(where_params.iter()), |row| {
|
||||
row.get::<_, i64>(0)
|
||||
})
|
||||
.map_err(DbError::from_rusqlite)?;
|
||||
let mut ids = Vec::new();
|
||||
for r in rows {
|
||||
@@ -4401,14 +4591,10 @@ fn do_update(
|
||||
|
||||
let where_sql = match filter {
|
||||
RowFilter::AllRows => String::new(),
|
||||
RowFilter::Where { column, value } => {
|
||||
let bound = impl_value_for(&schema, column, value)?;
|
||||
params.push(bound_to_sqlite_value(&bound));
|
||||
format!(
|
||||
" WHERE {col} = ?{n}",
|
||||
col = quote_ident(column),
|
||||
n = params.len()
|
||||
)
|
||||
RowFilter::Where(expr) => {
|
||||
// `compile_expr` continues the `?N` numbering from
|
||||
// the SET params already in `params`.
|
||||
format!(" WHERE {}", compile_expr(expr, &schema, &mut params))
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4476,14 +4662,8 @@ fn do_delete(
|
||||
let mut params: Vec<rusqlite::types::Value> = Vec::new();
|
||||
let where_sql = match filter {
|
||||
RowFilter::AllRows => String::new(),
|
||||
RowFilter::Where { column, value } => {
|
||||
let bound = impl_value_for(&schema, column, value)?;
|
||||
params.push(bound_to_sqlite_value(&bound));
|
||||
format!(
|
||||
" WHERE {col} = ?{n}",
|
||||
col = quote_ident(column),
|
||||
n = params.len()
|
||||
)
|
||||
RowFilter::Where(expr) => {
|
||||
format!(" WHERE {}", compile_expr(expr, &schema, &mut params))
|
||||
}
|
||||
};
|
||||
let sql = format!(
|
||||
@@ -4536,8 +4716,10 @@ fn do_query_data_request(
|
||||
persistence: Option<&Persistence>,
|
||||
source: Option<&str>,
|
||||
table: &str,
|
||||
filter: Option<&Expr>,
|
||||
limit: Option<u64>,
|
||||
) -> Result<DataResult, DbError> {
|
||||
let data = do_query_data(conn, table)?;
|
||||
let data = do_query_data(conn, table, filter, limit)?;
|
||||
if let (Some(p), Some(text)) = (persistence, source) {
|
||||
p.append_history(text)
|
||||
.map_err(DbError::from_persistence)?;
|
||||
@@ -4545,7 +4727,12 @@ fn do_query_data_request(
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
fn do_query_data(conn: &Connection, table: &str) -> Result<DataResult, DbError> {
|
||||
fn do_query_data(
|
||||
conn: &Connection,
|
||||
table: &str,
|
||||
filter: Option<&Expr>,
|
||||
limit: Option<u64>,
|
||||
) -> Result<DataResult, DbError> {
|
||||
let schema = read_schema(conn, table)?;
|
||||
let column_names: Vec<String> = schema.columns.iter().map(|c| c.name.clone()).collect();
|
||||
let column_types: Vec<Option<Type>> =
|
||||
@@ -4556,15 +4743,43 @@ fn do_query_data(conn: &Connection, table: &str) -> Result<DataResult, DbError>
|
||||
.map(|c| quote_ident(c))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
|
||||
// WHERE / LIMIT (ADR-0026 §5–§6). A `limit` implies a
|
||||
// stable primary-key ORDER BY so `limit n` is "first n by
|
||||
// primary key" rather than an arbitrary subset.
|
||||
let mut params: Vec<rusqlite::types::Value> = Vec::new();
|
||||
let where_sql = filter.map_or_else(String::new, |expr| {
|
||||
format!(" WHERE {}", compile_expr(expr, &schema, &mut params))
|
||||
});
|
||||
let (order_sql, limit_sql) = match limit {
|
||||
Some(n) => {
|
||||
let order = if schema.primary_key.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
let pk = schema
|
||||
.primary_key
|
||||
.iter()
|
||||
.map(|c| quote_ident(c))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
format!(" ORDER BY {pk}")
|
||||
};
|
||||
params.push(rusqlite::types::Value::Integer(
|
||||
i64::try_from(n).unwrap_or(i64::MAX),
|
||||
));
|
||||
(order, format!(" LIMIT ?{}", params.len()))
|
||||
}
|
||||
None => (String::new(), String::new()),
|
||||
};
|
||||
let sql = format!(
|
||||
"SELECT {cols} FROM {ident};",
|
||||
"SELECT {cols} FROM {ident}{where_sql}{order_sql}{limit_sql};",
|
||||
cols = cols_csv,
|
||||
ident = quote_ident(table),
|
||||
);
|
||||
debug!(sql = %sql, "query_data");
|
||||
let mut stmt = conn.prepare(&sql).map_err(DbError::from_rusqlite)?;
|
||||
let rows_iter = stmt
|
||||
.query_map([], |row| {
|
||||
.query_map(rusqlite::params_from_iter(params.iter()), |row| {
|
||||
let mut cells: Vec<rusqlite::types::Value> = Vec::with_capacity(column_names.len());
|
||||
for i in 0..column_names.len() {
|
||||
let v: rusqlite::types::Value = row.get(i)?;
|
||||
@@ -5300,7 +5515,7 @@ mod tests {
|
||||
result.client_side_notes
|
||||
);
|
||||
// Verify the column is populated 1..3.
|
||||
let data = db.query_data("T".to_string(), None).await.unwrap();
|
||||
let data = db.query_data("T".to_string(), None, None, None).await.unwrap();
|
||||
let seq_idx = data.columns.iter().position(|c| c == "seq").unwrap();
|
||||
let mut filled: Vec<i64> = data
|
||||
.rows
|
||||
@@ -5333,7 +5548,7 @@ mod tests {
|
||||
result.client_side_notes
|
||||
);
|
||||
// Verify each row has a non-null shortid value.
|
||||
let data = db.query_data("T".to_string(), None).await.unwrap();
|
||||
let data = db.query_data("T".to_string(), None, None, None).await.unwrap();
|
||||
let tag_idx = data.columns.iter().position(|c| c == "tag").unwrap();
|
||||
for row in &data.rows {
|
||||
let v = row[tag_idx].as_ref().expect("non-null shortid auto-filled");
|
||||
@@ -5366,7 +5581,7 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
let data = db.query_data("T".to_string(), None).await.unwrap();
|
||||
let data = db.query_data("T".to_string(), None, None, None).await.unwrap();
|
||||
let seq_idx = data.columns.iter().position(|c| c == "seq").unwrap();
|
||||
let mut values: Vec<i64> = data
|
||||
.rows
|
||||
@@ -5405,7 +5620,7 @@ mod tests {
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let data = db.query_data("T".to_string(), None).await.unwrap();
|
||||
let data = db.query_data("T".to_string(), None, None, None).await.unwrap();
|
||||
let seq_idx = data.columns.iter().position(|c| c == "seq").unwrap();
|
||||
let mut values: Vec<i64> = data
|
||||
.rows
|
||||
@@ -5520,7 +5735,7 @@ mod tests {
|
||||
|
||||
// Row data still accessible (id was preserved); the
|
||||
// dropped column is gone from the projection.
|
||||
let data = db.query_data("T".to_string(), None).await.unwrap();
|
||||
let data = db.query_data("T".to_string(), None, None, None).await.unwrap();
|
||||
assert_eq!(data.columns, vec!["id".to_string()]);
|
||||
assert_eq!(data.rows.len(), 1);
|
||||
}
|
||||
@@ -6044,7 +6259,7 @@ mod tests {
|
||||
assert_eq!(note.transformed, 3);
|
||||
assert_eq!(note.lossy, 0);
|
||||
// Data preserved via the per-cell transformer.
|
||||
let data = db.query_data("T".to_string(), None).await.unwrap();
|
||||
let data = db.query_data("T".to_string(), None, None, None).await.unwrap();
|
||||
assert_eq!(data.rows.len(), 3);
|
||||
}
|
||||
|
||||
@@ -6335,7 +6550,7 @@ mod tests {
|
||||
result.client_side
|
||||
);
|
||||
// Data preserved.
|
||||
let data = db.query_data("T".to_string(), None).await.unwrap();
|
||||
let data = db.query_data("T".to_string(), None, None, None).await.unwrap();
|
||||
assert_eq!(data.rows.len(), 3);
|
||||
}
|
||||
|
||||
@@ -6750,7 +6965,7 @@ mod tests {
|
||||
assert_eq!(note.auto_fill_kind, Some(AutoFillKind::Serial));
|
||||
// Confirm the filled values: existing 5, fills are 6
|
||||
// and 7 (continue sequence from MAX+1).
|
||||
let data = db.query_data("T".to_string(), None).await.unwrap();
|
||||
let data = db.query_data("T".to_string(), None, None, None).await.unwrap();
|
||||
let code_idx = data.columns.iter().position(|c| c == "code").unwrap();
|
||||
let mut values: Vec<i64> = data
|
||||
.rows
|
||||
@@ -6808,7 +7023,7 @@ mod tests {
|
||||
assert_eq!(note.auto_filled, 2);
|
||||
assert_eq!(note.auto_fill_kind, Some(AutoFillKind::ShortId));
|
||||
// All three rows now have valid shortids.
|
||||
let data = db.query_data("T".to_string(), None).await.unwrap();
|
||||
let data = db.query_data("T".to_string(), None, None, None).await.unwrap();
|
||||
let tag_idx = data.columns.iter().position(|c| c == "tag").unwrap();
|
||||
for row in &data.rows {
|
||||
let v = row[tag_idx].as_ref().expect("non-null shortid after fill");
|
||||
@@ -7334,7 +7549,7 @@ mod tests {
|
||||
// The InsertResult itself carries the just-inserted row.
|
||||
assert_eq!(result.data.rows.len(), 1);
|
||||
assert_eq!(result.data.rows[0][1], Some("Alice".to_string()));
|
||||
let data = db.query_data("Customers".to_string(), None).await.unwrap();
|
||||
let data = db.query_data("Customers".to_string(), None, None, None).await.unwrap();
|
||||
assert_eq!(data.columns, vec!["id".to_string(), "Name".to_string()]);
|
||||
assert_eq!(data.rows.len(), 1);
|
||||
assert_eq!(data.rows[0][1], Some("Alice".to_string()));
|
||||
@@ -7359,7 +7574,7 @@ mod tests {
|
||||
None)
|
||||
.await
|
||||
.unwrap();
|
||||
let data = db.query_data("Tags".to_string(), None).await.unwrap();
|
||||
let data = db.query_data("Tags".to_string(), None, None, None).await.unwrap();
|
||||
let id = data.rows[0][0].as_ref().expect("auto-generated id");
|
||||
assert!(
|
||||
id.len() >= 10 && id.len() <= 12,
|
||||
@@ -7378,7 +7593,7 @@ mod tests {
|
||||
None)
|
||||
.await
|
||||
.unwrap();
|
||||
let data = db.query_data("Customers".to_string(), None).await.unwrap();
|
||||
let data = db.query_data("Customers".to_string(), None, None, None).await.unwrap();
|
||||
assert_eq!(data.rows[0][0], Some("99".to_string()));
|
||||
assert_eq!(data.rows[0][1], Some("Bob".to_string()));
|
||||
}
|
||||
@@ -7416,10 +7631,7 @@ mod tests {
|
||||
.update(
|
||||
"Customers".to_string(),
|
||||
vec![("Name".to_string(), Value::Text("Alicia".to_string()))],
|
||||
RowFilter::Where {
|
||||
column: "id".to_string(),
|
||||
value: Value::Number("1".to_string()),
|
||||
},
|
||||
RowFilter::eq("id", Value::Number("1".to_string())),
|
||||
None)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -7427,11 +7639,252 @@ mod tests {
|
||||
// The UpdateResult contains only the updated rows.
|
||||
assert_eq!(result.data.rows.len(), 1);
|
||||
assert_eq!(result.data.rows[0][1], Some("Alicia".to_string()));
|
||||
let data = db.query_data("Customers".to_string(), None).await.unwrap();
|
||||
let data = db.query_data("Customers".to_string(), None, None, None).await.unwrap();
|
||||
assert_eq!(data.rows[0][1], Some("Alicia".to_string()));
|
||||
assert_eq!(data.rows[1][1], Some("Bob".to_string()));
|
||||
}
|
||||
|
||||
// ---- Complex WHERE expressions (ADR-0026) ----------------
|
||||
|
||||
/// `People(id serial pk, Name text, Age int, Active bool)`
|
||||
/// seeded with four rows: Alice/25/true, Bob/35/false,
|
||||
/// Carol/45/true, Dave/35/true (ids 1..4).
|
||||
async fn people_table(db: &Database) {
|
||||
db.create_table(
|
||||
"People".to_string(),
|
||||
vec![
|
||||
col("id", Type::Serial),
|
||||
col("Name", Type::Text),
|
||||
col("Age", Type::Int),
|
||||
col("Active", Type::Bool),
|
||||
],
|
||||
vec!["id".to_string()],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
for (name, age, active) in [
|
||||
("Alice", 25, true),
|
||||
("Bob", 35, false),
|
||||
("Carol", 45, true),
|
||||
("Dave", 35, true),
|
||||
] {
|
||||
db.insert(
|
||||
"People".to_string(),
|
||||
None,
|
||||
vec![
|
||||
Value::Text(name.to_string()),
|
||||
Value::Number(age.to_string()),
|
||||
Value::Bool(active),
|
||||
],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// Pull the `RowFilter` out of an `update` / `delete` parsed
|
||||
/// from DSL — the readable way to build a complex `Expr`.
|
||||
fn parse_filter(dsl: &str) -> RowFilter {
|
||||
match crate::dsl::parser::parse_command(dsl).expect("filter parse") {
|
||||
crate::dsl::command::Command::Update { filter, .. }
|
||||
| crate::dsl::command::Command::Delete { filter, .. } => filter,
|
||||
other => panic!("expected update/delete, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Pull the optional filter + limit out of a parsed
|
||||
/// `show data` command.
|
||||
fn parse_show(dsl: &str) -> (Option<Expr>, Option<u64>) {
|
||||
match crate::dsl::parser::parse_command(dsl).expect("show parse") {
|
||||
crate::dsl::command::Command::ShowData { filter, limit, .. } => {
|
||||
(filter, limit)
|
||||
}
|
||||
other => panic!("expected show data, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// The `Name` column of every remaining row, in row order.
|
||||
fn names(data: &DataResult) -> Vec<String> {
|
||||
data.rows
|
||||
.iter()
|
||||
.map(|r| r[1].clone().unwrap_or_default())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_with_and_expression_filters_rows() {
|
||||
let db = db();
|
||||
people_table(&db).await;
|
||||
let result = db
|
||||
.delete(
|
||||
"People".to_string(),
|
||||
parse_filter(
|
||||
"delete from People where Age >= 35 and Active = true",
|
||||
),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
// Carol (45/true) and Dave (35/true) match; Bob (35) is
|
||||
// inactive, Alice (25) is too young.
|
||||
assert_eq!(result.rows_affected, 2);
|
||||
let data = db.query_data("People".to_string(), None, None, None).await.unwrap();
|
||||
assert_eq!(names(&data), vec!["Alice", "Bob"]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_with_or_expression_filters_rows() {
|
||||
let db = db();
|
||||
people_table(&db).await;
|
||||
let result = db
|
||||
.delete(
|
||||
"People".to_string(),
|
||||
parse_filter("delete from People where id = 1 or id = 3"),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(result.rows_affected, 2);
|
||||
let data = db.query_data("People".to_string(), None, None, None).await.unwrap();
|
||||
assert_eq!(names(&data), vec!["Bob", "Dave"]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_with_not_expression_filters_rows() {
|
||||
let db = db();
|
||||
people_table(&db).await;
|
||||
// `not Age = 35` keeps Bob and Dave (both 35).
|
||||
let result = db
|
||||
.delete(
|
||||
"People".to_string(),
|
||||
parse_filter("delete from People where not Age = 35"),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(result.rows_affected, 2);
|
||||
let data = db.query_data("People".to_string(), None, None, None).await.unwrap();
|
||||
assert_eq!(names(&data), vec!["Bob", "Dave"]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_with_between_filters_rows() {
|
||||
let db = db();
|
||||
people_table(&db).await;
|
||||
let result = db
|
||||
.delete(
|
||||
"People".to_string(),
|
||||
parse_filter("delete from People where Age between 30 and 40"),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
// Bob (35) and Dave (35) are in range.
|
||||
assert_eq!(result.rows_affected, 2);
|
||||
let data = db.query_data("People".to_string(), None, None, None).await.unwrap();
|
||||
assert_eq!(names(&data), vec!["Alice", "Carol"]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_with_in_filters_rows() {
|
||||
let db = db();
|
||||
people_table(&db).await;
|
||||
let result = db
|
||||
.delete(
|
||||
"People".to_string(),
|
||||
parse_filter(
|
||||
"delete from People where Name in ('Alice', 'Carol')",
|
||||
),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(result.rows_affected, 2);
|
||||
let data = db.query_data("People".to_string(), None, None, None).await.unwrap();
|
||||
assert_eq!(names(&data), vec!["Bob", "Dave"]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_with_like_filters_rows() {
|
||||
let db = db();
|
||||
people_table(&db).await;
|
||||
let result = db
|
||||
.delete(
|
||||
"People".to_string(),
|
||||
parse_filter("delete from People where Name like 'A%'"),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(result.rows_affected, 1, "only Alice matches `A%`");
|
||||
let data = db.query_data("People".to_string(), None, None, None).await.unwrap();
|
||||
assert_eq!(names(&data), vec!["Bob", "Carol", "Dave"]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_with_complex_where_updates_only_matching_rows() {
|
||||
let db = db();
|
||||
people_table(&db).await;
|
||||
// Deactivate everyone over 30 who is still active.
|
||||
let result = db
|
||||
.update(
|
||||
"People".to_string(),
|
||||
vec![("Active".to_string(), Value::Bool(false))],
|
||||
parse_filter(
|
||||
"update People set Active=false \
|
||||
where Age > 30 and Active = true",
|
||||
),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
// Carol (45) and Dave (35) — Bob is already inactive.
|
||||
assert_eq!(result.rows_affected, 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn query_data_with_where_filters_rows() {
|
||||
let db = db();
|
||||
people_table(&db).await;
|
||||
let (filter, limit) = parse_show("show data People where Active = true");
|
||||
let data = db
|
||||
.query_data("People".to_string(), filter, limit, None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(names(&data), vec!["Alice", "Carol", "Dave"]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn query_data_with_limit_caps_rows_by_primary_key() {
|
||||
let db = db();
|
||||
people_table(&db).await;
|
||||
let (filter, limit) = parse_show("show data People limit 2");
|
||||
let data = db
|
||||
.query_data("People".to_string(), filter, limit, None)
|
||||
.await
|
||||
.unwrap();
|
||||
// `limit` implies an ORDER BY the primary key, so this
|
||||
// is a stable "first 2 by id".
|
||||
assert_eq!(names(&data), vec!["Alice", "Bob"]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn query_data_with_where_and_limit_combines_both() {
|
||||
let db = db();
|
||||
people_table(&db).await;
|
||||
let (filter, limit) =
|
||||
parse_show("show data People where Age >= 35 limit 1");
|
||||
let data = db
|
||||
.query_data("People".to_string(), filter, limit, None)
|
||||
.await
|
||||
.unwrap();
|
||||
// Three rows match `Age >= 35` (Bob, Carol, Dave); the
|
||||
// limit keeps the first by primary key — Bob (id 2).
|
||||
assert_eq!(names(&data), vec!["Bob"]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_with_all_rows_affects_everything() {
|
||||
let db = db();
|
||||
@@ -7473,16 +7926,13 @@ mod tests {
|
||||
let result = db
|
||||
.delete(
|
||||
"Customers".to_string(),
|
||||
RowFilter::Where {
|
||||
column: "id".to_string(),
|
||||
value: Value::Number("1".to_string()),
|
||||
},
|
||||
RowFilter::eq("id", Value::Number("1".to_string())),
|
||||
None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(result.rows_affected, 1);
|
||||
assert!(result.cascade.is_empty(), "no children to cascade to");
|
||||
let data = db.query_data("Customers".to_string(), None).await.unwrap();
|
||||
let data = db.query_data("Customers".to_string(), None, None, None).await.unwrap();
|
||||
assert_eq!(data.rows.len(), 1);
|
||||
assert_eq!(data.rows[0][1], Some("Bob".to_string()));
|
||||
}
|
||||
@@ -7585,14 +8035,11 @@ mod tests {
|
||||
// Delete Alice — cascades to Orders.
|
||||
db.delete(
|
||||
"Customers".to_string(),
|
||||
RowFilter::Where {
|
||||
column: "id".to_string(),
|
||||
value: Value::Number("1".to_string()),
|
||||
},
|
||||
RowFilter::eq("id", Value::Number("1".to_string())),
|
||||
None)
|
||||
.await
|
||||
.unwrap();
|
||||
let orders = db.query_data("Orders".to_string(), None).await.unwrap();
|
||||
let orders = db.query_data("Orders".to_string(), None, None, None).await.unwrap();
|
||||
assert!(orders.rows.is_empty(), "child rows should be cascaded");
|
||||
}
|
||||
|
||||
@@ -7620,7 +8067,7 @@ mod tests {
|
||||
None)
|
||||
.await
|
||||
.unwrap();
|
||||
let data = db.query_data("Flags".to_string(), None).await.unwrap();
|
||||
let data = db.query_data("Flags".to_string(), None, None, None).await.unwrap();
|
||||
assert_eq!(data.rows[0][1], Some("true".to_string()));
|
||||
assert_eq!(data.rows[1][1], Some("false".to_string()));
|
||||
}
|
||||
@@ -7642,7 +8089,7 @@ mod tests {
|
||||
None)
|
||||
.await
|
||||
.unwrap();
|
||||
let data = db.query_data("T".to_string(), None).await.unwrap();
|
||||
let data = db.query_data("T".to_string(), None, None, None).await.unwrap();
|
||||
assert_eq!(data.rows[0][1], None);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user