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:
claude@clouddev1
2026-05-18 23:12:33 +00:00
parent 59e6a541bf
commit f75f71bbe4
22 changed files with 1013 additions and 193 deletions
+503 -56
View File
@@ -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);
}