cratefield_tables/sql.rs
1//! The statements that read and write a declared table's rows
2//! (issue #153).
3//!
4//! A `[tables]` declaration already becomes DDL, a module, a privacy
5//! declaration and an access level. This is the layer underneath the CRUD
6//! routes: given a [`TableDef`] and a row, the five statements that put a
7//! row in a database and get it back out, built with sea-query and
8//! rendered through the `Database` port like every other module's SQL
9//! (ADR 0004).
10//!
11//! # Where the injection boundary is
12//!
13//! A declared table's column names come from a manifest, and a row's keys
14//! come from an HTTP request. Only the first may become an identifier.
15//! Every builder here walks `table.fields` and never the row's keys: a
16//! key the table does not declare cannot reach the SQL at all, whatever
17//! it contains, because nothing iterates over it. Values are bound
18//! positionally by the adapter and are never rendered into the text.
19//!
20//! That is a property worth stating rather than assuming, so
21//! `a_row_key_cannot_become_an_identifier` asserts it directly.
22//!
23//! # Why decoding can fail
24//!
25//! A column that does not come back as its declared kind is an error, not
26//! a `null`. The declaration says a database has a `TEXT` column named
27//! `body`; a row answering with an integer means the database no longer
28//! matches the manifest, which is a finding — `fz tables drift` is the
29//! command that reports it — and turning it into a silent `null` would
30//! publish wrong data through the same API that promises the declared
31//! JSON Schema.
32
33use cratefield_core::{Row, Statement};
34use sea_query::{Alias, Cond, Expr, Order, Query, SimpleExpr, Value as SeaValue};
35use serde_json::{Map, Value};
36
37use crate::schema::{FieldDef, FieldKind, TableDef};
38use crate::validate::{RowErrors, validate_row};
39use crate::value::ErrorCode;
40
41/// A column that did not come back as the declaration says it is.
42///
43/// Carries the table and column so the operator knows where to look, and
44/// a detail that describes the shape rather than quoting the value —
45/// these rows are a venture's data and this message goes to a log.
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct DecodeError {
48 /// The declared table the row belongs to.
49 pub table: String,
50 /// The declared column that could not be read.
51 pub column: String,
52 /// What was wrong, in terms of kinds and never of values.
53 pub detail: String,
54}
55
56impl std::fmt::Display for DecodeError {
57 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 write!(
59 f,
60 "table `{}`, column `{}`: {}",
61 self.table, self.column, self.detail
62 )
63 }
64}
65
66impl std::error::Error for DecodeError {}
67
68/// A JSON value for one declared field, as a bound parameter, or `None`
69/// when the value is not the kind the column is declared as.
70///
71/// A `null` — or an absent field — becomes a **typed** null matching the
72/// column, not an untyped one. Postgres binds parameters by type and
73/// cannot infer one for a bare null, so an untyped null is an error there
74/// and a silent success on SQLite: exactly the asymmetry that passes
75/// every local test and fails in production.
76///
77/// A value of the wrong kind is `None` rather than that same typed null,
78/// which is the whole reason this answers an `Option`. A `42` for a
79/// `uuid` column silently becoming `NULL` makes `WHERE id = NULL` — never
80/// true — so a delete would remove nothing and report success.
81#[must_use]
82pub fn to_sql(field: &FieldDef, value: Option<&Value>) -> Option<SeaValue> {
83 let Some(value) = value.filter(|value| !value.is_null()) else {
84 return Some(null_for(&field.kind));
85 };
86 match &field.kind {
87 FieldKind::Boolean => value.as_bool().map(|flag| SeaValue::Bool(Some(flag))),
88 FieldKind::Integer { .. } => {
89 crate::value::integral(value).map(|n| SeaValue::BigInt(Some(n)))
90 }
91 FieldKind::Real { .. } => value.as_f64().map(|n| SeaValue::Double(Some(n))),
92 // Stored as TEXT, so what goes in is the serialization and what
93 // comes back is parsed. `to_string` rather than the raw request
94 // bytes: the value has been through `serde_json`, so the stored
95 // text is canonical JSON rather than whatever spacing was sent.
96 // Any JSON value is a legal one, which is why this cannot fail.
97 FieldKind::Json => Some(SeaValue::String(Some(Box::new(value.to_string())))),
98 FieldKind::Text { .. }
99 | FieldKind::Timestamp
100 | FieldKind::Uuid
101 | FieldKind::Enum { .. } => value
102 .as_str()
103 .map(|text| SeaValue::String(Some(Box::new(text.to_owned())))),
104 }
105}
106
107/// [`to_sql`], with the mismatch turned into the row error it is.
108///
109/// Reachable only past [`validate_row`], which refuses a wrong-typed
110/// value first — so this is the second line rather than the first, and it
111/// exists because "the caller validated" is the assumption that stops
112/// being true the day somebody adds a second caller.
113fn bound(field: &FieldDef, value: Option<&Value>) -> Result<SeaValue, RowErrors> {
114 to_sql(field, value).ok_or_else(|| {
115 RowErrors::one(
116 &field.name,
117 ErrorCode::WrongType,
118 format!("must be {}", field.kind.as_str()),
119 )
120 })
121}
122
123/// The typed null for a column of this kind.
124fn null_for(kind: &FieldKind) -> SeaValue {
125 match kind {
126 FieldKind::Boolean => SeaValue::Bool(None),
127 FieldKind::Integer { .. } => SeaValue::BigInt(None),
128 FieldKind::Real { .. } => SeaValue::Double(None),
129 FieldKind::Text { .. }
130 | FieldKind::Timestamp
131 | FieldKind::Uuid
132 | FieldKind::Json
133 | FieldKind::Enum { .. } => SeaValue::String(None),
134 }
135}
136
137/// One column of a returned row, as the JSON the declaration promises.
138///
139/// # Errors
140///
141/// When the value is not the declared kind — which means the database no
142/// longer matches the declaration.
143pub fn from_sql(table: &str, field: &FieldDef, value: &SeaValue) -> Result<Value, DecodeError> {
144 let wrong = |detail: &str| DecodeError {
145 table: table.to_owned(),
146 column: field.name.clone(),
147 detail: detail.to_owned(),
148 };
149 // SQLite has no typed nulls: its adapter maps every `NULL` to
150 // `SeaValue::String(None)` whatever the column is declared as,
151 // because the value carries no type to map from. So a null in an
152 // `integer`, `real` or `boolean` column arrives here looking like a
153 // text null, and reading that as a wrong kind answers 500 for a row
154 // that is exactly what the declaration says it is.
155 //
156 // Only the *null* case widens. A text value in an integer column is
157 // still an error, which is the part worth keeping: that one means the
158 // database has drifted from the declaration.
159 if matches!(value, SeaValue::String(None)) {
160 return Ok(Value::Null);
161 }
162 match &field.kind {
163 FieldKind::Boolean => match value {
164 SeaValue::Bool(None) | SeaValue::Int(None) | SeaValue::BigInt(None) => Ok(Value::Null),
165 SeaValue::Bool(Some(flag)) => Ok(Value::Bool(*flag)),
166 // SQLite has no boolean type and the DDL renders the column
167 // as INTEGER there, so a `true` comes back as 1. Postgres
168 // answers with a real boolean. Both are the declared kind.
169 SeaValue::Int(Some(n)) => Ok(Value::Bool(*n != 0)),
170 SeaValue::BigInt(Some(n)) => Ok(Value::Bool(*n != 0)),
171 _ => Err(wrong(
172 "declared boolean, and the database did not answer with one",
173 )),
174 },
175 FieldKind::Integer { .. } => match integer(value) {
176 Integral::Whole(n) => Ok(Value::from(n)),
177 Integral::Null => Ok(Value::Null),
178 Integral::Other => Err(wrong(
179 "declared integer, and the database did not answer with one",
180 )),
181 },
182 FieldKind::Real { .. } => match value {
183 SeaValue::Double(None) | SeaValue::Float(None) => Ok(Value::Null),
184 SeaValue::Double(Some(n)) => Ok(number(*n).ok_or_else(|| {
185 wrong("declared real, and the database answered with a value JSON cannot hold")
186 })?),
187 SeaValue::Float(Some(n)) => Ok(number(f64::from(*n)).ok_or_else(|| {
188 wrong("declared real, and the database answered with a value JSON cannot hold")
189 })?),
190 // SQLite's column affinity stores a whole number in a REAL
191 // column as an integer and answers with one.
192 _ => match integer(value) {
193 #[expect(
194 clippy::cast_precision_loss,
195 reason = "a REAL column's value was written as an f64; coming back through \
196 SQLite's integer affinity, f64 is the kind it is declared as"
197 )]
198 Integral::Whole(n) => {
199 Ok(number(n as f64).ok_or_else(|| wrong("not a JSON number"))?)
200 }
201 Integral::Null => Ok(Value::Null),
202 Integral::Other => Err(wrong(
203 "declared real, and the database did not answer with a number",
204 )),
205 },
206 },
207 FieldKind::Json => match value {
208 SeaValue::String(None) => Ok(Value::Null),
209 SeaValue::String(Some(text)) => serde_json::from_str(text).map_err(|_ignored| {
210 // The message says the shape, never the text: a `json`
211 // column holds whatever the venture put in it.
212 wrong("declared json, and the stored text is not JSON")
213 }),
214 _ => Err(wrong(
215 "declared json, and the database did not answer with text",
216 )),
217 },
218 FieldKind::Text { .. }
219 | FieldKind::Timestamp
220 | FieldKind::Uuid
221 | FieldKind::Enum { .. } => match value {
222 SeaValue::String(None) => Ok(Value::Null),
223 SeaValue::String(Some(text)) => Ok(Value::String((**text).clone())),
224 _ => Err(wrong(
225 "declared text, and the database did not answer with text",
226 )),
227 },
228 }
229}
230
231/// What a value is, when the declaration says the column holds a whole
232/// number. Three cases rather than a nested `Option`, because "a null
233/// integer" and "not an integer at all" become different answers — one is
234/// a JSON `null` and the other is a [`DecodeError`].
235enum Integral {
236 /// A whole number, in any of the widths an engine may answer with.
237 Whole(i64),
238 /// SQL `NULL` in an integer column.
239 Null,
240 /// Not an integer at all.
241 Other,
242}
243
244fn integer(value: &SeaValue) -> Integral {
245 let inner = match value {
246 SeaValue::TinyInt(inner) => inner.map(i64::from),
247 SeaValue::SmallInt(inner) => inner.map(i64::from),
248 SeaValue::Int(inner) => inner.map(i64::from),
249 SeaValue::BigInt(inner) => *inner,
250 _ => return Integral::Other,
251 };
252 inner.map_or(Integral::Null, Integral::Whole)
253}
254
255/// A finite `f64` as a JSON number. NaN and the infinities have no JSON
256/// form, and `serde_json` renders them as `null` rather than refusing.
257fn number(n: f64) -> Option<Value> {
258 serde_json::Number::from_f64(n).map(Value::Number)
259}
260
261/// A returned row as the JSON the declaration promises.
262///
263/// Every declared field is a key, in declaration order, whether or not
264/// the database answered with one — a missing column is an error for the
265/// reason a mistyped one is.
266///
267/// # Errors
268///
269/// When a column is absent or is not the declared kind.
270pub fn row_json(table: &TableDef, row: &Row) -> Result<Value, DecodeError> {
271 let mut out = Map::new();
272 for field in &table.fields {
273 let value = row
274 .get::<SeaValue>(&field.name)
275 .ok_or_else(|| DecodeError {
276 table: table.name.clone(),
277 column: field.name.clone(),
278 detail: "declared, and the row does not have it".to_owned(),
279 })?;
280 out.insert(field.name.clone(), from_sql(&table.name, field, &value)?);
281 }
282 Ok(Value::Object(out))
283}
284
285/// `INSERT` for one row.
286///
287/// The row is validated here rather than trusted: a statement built from
288/// a row that would not pass [`validate_row`] is a statement that reaches
289/// the database to be refused by a constraint, in a message written for a
290/// DBA rather than for the caller.
291///
292/// # Errors
293///
294/// The row's own validation errors, as the problem+json body describes.
295pub fn insert(table: &TableDef, row: &Value) -> Result<Statement, RowErrors> {
296 validate_row(table, row)?;
297 let object = row.as_object().cloned().unwrap_or_default();
298 // Declared fields the row leaves out are left out of the statement
299 // too, so the column's DDL default applies. Binding an explicit null
300 // instead would overwrite a default with nothing.
301 let present: Vec<&FieldDef> = table
302 .fields
303 .iter()
304 .filter(|field| object.contains_key(&field.name))
305 .collect();
306
307 let mut values = Vec::with_capacity(present.len());
308 for field in &present {
309 values.push(bound(field, object.get(&field.name))?.into());
310 }
311
312 let mut insert = Query::insert();
313 insert
314 .into_table(Alias::new(&table.name))
315 .columns(present.iter().map(|field| Alias::new(&field.name)))
316 .values_panic(values);
317 Ok(Statement::render(&insert))
318}
319
320/// Rows belonging to one subject: the condition an `owner` table's
321/// access level puts on every read.
322///
323/// A parameter rather than something the caller applies afterwards. The
324/// condition joins the same `WHERE` as the key and the cursor, because a
325/// filter applied to rows already fetched turns a `LIMIT 20` into a page
326/// of however many survived — and the shortfall is a count of the rows
327/// the caller was not allowed to see.
328#[derive(Debug, Clone, Copy)]
329pub struct Owned<'a> {
330 /// The table's subject column.
331 pub column: &'a str,
332 /// The caller's own id.
333 pub subject: &'a Value,
334}
335
336/// `SELECT` of one row by primary key, optionally scoped to one subject.
337///
338/// With `owned`, a row belonging to somebody else does not match — so the
339/// caller is answered "not found" rather than "forbidden", which is an
340/// answer about a row they were never in a position to learn exists.
341///
342/// # Errors
343///
344/// When `key` does not carry every primary-key column the table declares
345/// — half a composite key selects an arbitrary row — or when the subject
346/// column is not a field of the table.
347pub fn select_one(
348 table: &TableDef,
349 key: &Value,
350 owned: Option<Owned<'_>>,
351) -> Result<Statement, DecodeError> {
352 let mut select = Query::select();
353 select
354 .columns(table.fields.iter().map(|field| Alias::new(&field.name)))
355 .from(Alias::new(&table.name))
356 .cond_where(key_predicate(table, key)?.add_option(owned_predicate(table, owned)?))
357 .limit(1);
358 Ok(Statement::render(&select))
359}
360
361/// `subject_column = ?`, when the read is scoped to one subject.
362fn owned_predicate(
363 table: &TableDef,
364 owned: Option<Owned<'_>>,
365) -> Result<Option<sea_query::SimpleExpr>, DecodeError> {
366 let Some(owned) = owned else {
367 return Ok(None);
368 };
369 let field = table
370 .fields
371 .iter()
372 .find(|field| field.name == owned.column)
373 .ok_or_else(|| DecodeError {
374 table: table.name.clone(),
375 column: owned.column.to_owned(),
376 detail: "the subject column this read is scoped to is not a field of the table"
377 .to_owned(),
378 })?;
379 let value = to_sql(field, Some(owned.subject)).ok_or_else(|| DecodeError {
380 table: table.name.clone(),
381 column: owned.column.to_owned(),
382 detail: format!(
383 "the subject this read is scoped to is not {}",
384 field.kind.as_str()
385 ),
386 })?;
387 Ok(Some(Expr::col(Alias::new(owned.column)).eq(value)))
388}
389
390/// One equality condition on a declared column.
391///
392/// Equality, and nothing else. #153's own rule bounds the declaration
393/// surface — *anything referencing another row or another request is a
394/// function, not a field* — and the same instinct bounds what a caller
395/// may ask of one: ranges, prefixes and `LIKE` are queries a module
396/// writes, not vocabulary a manifest grows into.
397#[derive(Debug, Clone)]
398pub struct Filter {
399 /// The declared column.
400 pub column: String,
401 /// What it must equal, as JSON.
402 pub value: Value,
403}
404
405/// What a page asks for.
406///
407/// A struct rather than five positional arguments: the last two are both
408/// optional and both about *which rows*, and a call site that swapped
409/// them would compile.
410#[derive(Debug, Clone, Copy)]
411pub struct Page<'a> {
412 /// The most rows to return.
413 pub limit: u64,
414 /// The key of the last row of the previous page.
415 pub after: Option<&'a Value>,
416 /// The subject scope an `owner` table's access level decided.
417 pub owned: Option<Owned<'a>>,
418 /// What the caller asked to narrow by.
419 pub filters: &'a [Filter],
420 /// What the caller asked to order by, before the primary key.
421 pub sort: Option<Sort<'a>>,
422}
423
424/// An ordering on one declared column.
425///
426/// One column, because a second is a tiebreaker and the primary key is
427/// already that — and because every column a page is ordered by has to
428/// appear in the cursor, so the vocabulary that stays small is the one
429/// whose cursor stays readable.
430#[derive(Debug, Clone, Copy, PartialEq, Eq)]
431pub struct Sort<'a> {
432 /// The declared column.
433 pub column: &'a str,
434 /// Whether the largest value comes first.
435 pub descending: bool,
436}
437
438/// `SELECT` of a page, ordered by primary key, optionally after a cursor.
439///
440/// Ordered by the primary key because a page has to be stable: a `LIMIT`
441/// with no `ORDER BY` is whatever the engine felt like, and two requests
442/// for "the first ten" may then share rows or skip them.
443///
444/// # Errors
445///
446/// When `after` is given and does not carry every primary-key column.
447pub fn select_page(table: &TableDef, page: Page<'_>) -> Result<Statement, DecodeError> {
448 let mut select = Query::select();
449 select
450 .columns(table.fields.iter().map(|field| Alias::new(&field.name)))
451 .from(Alias::new(&table.name))
452 .limit(page.limit);
453 // The sort column first, then the primary key. The key is the
454 // tiebreaker, and it has to be there: two rows with the same sort
455 // value in no fixed order make a page that repeats one and skips the
456 // other, and nothing in the answer says so.
457 //
458 // Both take the same direction, so the cursor comparison is one
459 // operator rather than one per column.
460 let direction = if page.sort.is_some_and(|sort| sort.descending) {
461 Order::Desc
462 } else {
463 Order::Asc
464 };
465 if let Some(sort) = page.sort {
466 sortable(table, sort)?;
467 select.order_by(Alias::new(sort.column), direction.clone());
468 }
469 for column in &table.primary_key {
470 select.order_by(Alias::new(column), direction.clone());
471 }
472 // Every condition `AND`ed: the cursor is where the page starts, the
473 // scope is which rows the caller may see, and the filters are which
474 // of those they asked for. Applying any of them afterwards would page
475 // through rows that are then discarded and hand back short pages that
476 // count them.
477 //
478 // The scope goes in first and is never replaced by a filter — a
479 // caller filtering on the subject column narrows their own rows and
480 // cannot reach anybody else's, because both conditions stand.
481 let mut all = Cond::all().add_option(owned_predicate(table, page.owned)?);
482 if let Some(after) = page.after {
483 all = all.add(after_predicate(table, after, page.sort)?);
484 }
485 for filter in page.filters {
486 all = all.add(filter_predicate(table, filter)?);
487 }
488 select.cond_where(all);
489 Ok(Statement::render(&select))
490}
491
492/// `column = ?`, for one caller-supplied filter — or `column IS NULL`,
493/// which is the same question asked about a value SQL will not compare.
494///
495/// The column must be a declared field and the value must be its kind.
496/// Neither is ignored: a filter that is silently dropped answers a
497/// question the caller did not ask, with more rows than they asked for.
498///
499/// **A `null` is `IS NULL` and not `= NULL`.** Nothing equals null in
500/// SQL, itself included, so `WHERE body = ?` bound to a null is false for
501/// every row — a caller asking for the rows whose `body` is unset got a
502/// `200` and an empty page whatever the table held, which is a wrong
503/// answer that looks like a right one. The three-valued logic is SQL's,
504/// not this API's: the filter vocabulary is equality on a declared
505/// column, and "the rows where this column is unset" is a thing equality
506/// means everywhere outside SQL.
507///
508/// Only a batch read can ask it. A query string is text, so `?body=` is
509/// the empty string and there is no spelling of null in it — which is why
510/// this went unnoticed: the path that could express it is the one with no
511/// way to say it.
512fn filter_predicate(table: &TableDef, filter: &Filter) -> Result<SimpleExpr, DecodeError> {
513 let field = table
514 .fields
515 .iter()
516 .find(|field| field.name == filter.column)
517 .ok_or_else(|| DecodeError {
518 table: table.name.clone(),
519 column: filter.column.clone(),
520 detail: "is not a field of this table".to_owned(),
521 })?;
522 if filter.value.is_null() {
523 return Ok(Expr::col(Alias::new(&filter.column)).is_null());
524 }
525 // A JSON column is stored as its serialization, so `= ?` compares
526 // text. `serde_json` runs with `preserve_order` here — required, so
527 // the JSON Schema view lists properties in declaration order — which
528 // makes that text depend on the order the keys were written in.
529 // `{"tier":"gold","since":2026}` and `{"since":2026,"tier":"gold"}`
530 // are the same value and two different rows to compare against, so
531 // the same question gets two answers and neither is wrong-looking.
532 //
533 // The query-string parser already refuses these, and says why: "a
534 // JSON column has no single text form to compare for equality". It
535 // is refused here instead so the batch path, which carries JSON
536 // values rather than text, cannot ask what that one cannot.
537 if matches!(field.kind, FieldKind::Json) {
538 return Err(DecodeError {
539 table: table.name.clone(),
540 column: filter.column.clone(),
541 detail: "is json, which has no single text form to compare for equality; reading \
542 inside a JSON column is a query a module or a sidecar writes"
543 .to_owned(),
544 });
545 }
546 let value = to_sql(field, Some(&filter.value)).ok_or_else(|| DecodeError {
547 table: table.name.clone(),
548 column: filter.column.clone(),
549 detail: format!("is {} and the value given is not", field.kind.as_str()),
550 })?;
551 Ok(Expr::col(Alias::new(&filter.column)).eq(value))
552}
553
554/// `UPDATE` of one row by primary key, replacing every declared
555/// non-key field, optionally scoped to one subject.
556///
557/// With `owned`, a row belonging to somebody else matches nothing, so the
558/// caller is told no row was changed rather than being refused — the same
559/// answer a read gives, for the same reason.
560///
561/// A replacement rather than a merge: the caller sends the row it wants
562/// to exist. A merge would make "unset this field" unexpressible, because
563/// an absent key and a `null` one would both mean "leave it".
564///
565/// # Errors
566///
567/// The row's validation errors, or a key that is not complete.
568pub fn update(
569 table: &TableDef,
570 key: &Value,
571 row: &Value,
572 owned: Option<Owned<'_>>,
573) -> Result<Statement, UpdateError> {
574 validate_row(table, row).map_err(UpdateError::Row)?;
575 let object = row.as_object().cloned().unwrap_or_default();
576 let predicate = key_predicate(table, key)
577 .map_err(UpdateError::Key)?
578 .add_option(owned_predicate(table, owned).map_err(UpdateError::Key)?);
579
580 let mut update = Query::update();
581 update.table(Alias::new(&table.name));
582 for field in &table.fields {
583 if table.primary_key.contains(&field.name) {
584 // Changing a row's identity is a delete and an insert, not an
585 // update: rows referencing it by foreign key would follow the
586 // value or be orphaned depending on the constraint, which is
587 // not a decision an edit should make silently.
588 continue;
589 }
590 update.value(
591 Alias::new(&field.name),
592 bound(field, object.get(&field.name)).map_err(UpdateError::Row)?,
593 );
594 }
595 update.cond_where(predicate);
596 Ok(Statement::render(&update))
597}
598
599/// What an `UPDATE` can be refused for.
600#[derive(Debug, Clone, PartialEq, Eq)]
601#[non_exhaustive]
602pub enum UpdateError {
603 /// The new row is not a legal row for this table.
604 Row(RowErrors),
605 /// The key does not name the row to change.
606 Key(DecodeError),
607}
608
609impl std::fmt::Display for UpdateError {
610 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
611 match self {
612 Self::Row(errors) => write!(f, "{errors}"),
613 Self::Key(error) => write!(f, "{error}"),
614 }
615 }
616}
617
618impl std::error::Error for UpdateError {}
619
620/// `DELETE` of one row by primary key, optionally scoped to one subject.
621///
622/// The scope is the difference between a caller deleting their own row
623/// and a caller deleting any row whose key they can guess.
624///
625/// # Errors
626///
627/// When `key` does not carry every primary-key column — a partial key
628/// would delete every row that matches the half it names.
629pub fn delete(
630 table: &TableDef,
631 key: &Value,
632 owned: Option<Owned<'_>>,
633) -> Result<Statement, DecodeError> {
634 let mut delete = Query::delete();
635 delete
636 .from_table(Alias::new(&table.name))
637 .cond_where(key_predicate(table, key)?.add_option(owned_predicate(table, owned)?));
638 Ok(Statement::render(&delete))
639}
640
641/// `pk_a = ? AND pk_b = ?`, refusing a key that is not complete.
642fn key_predicate(table: &TableDef, key: &Value) -> Result<Cond, DecodeError> {
643 let mut all = Cond::all();
644 for column in &table.primary_key {
645 all = all.add(Expr::col(Alias::new(column)).eq(key_part(table, key, column)?));
646 }
647 Ok(all)
648}
649
650/// Lexicographic "after this key", as an OR of ANDs.
651///
652/// `(a > x) OR (a = x AND b > y)` rather than a row-value comparison:
653/// `(a, b) > (x, y)` is standard SQL that SQLite does not have, and a
654/// cursor that worked on Postgres and not on D1 would be a difference
655/// between two deployments of the same declaration.
656fn after_predicate(
657 table: &TableDef,
658 after: &Value,
659 sort: Option<Sort<'_>>,
660) -> Result<Cond, DecodeError> {
661 // The ordering the page is in, which is what the cursor walks: the
662 // sort column, then every key column. A cursor that compared only the
663 // key would resume in the wrong place the moment the page was not in
664 // key order.
665 let mut columns: Vec<&str> = Vec::with_capacity(table.primary_key.len() + 1);
666 if let Some(sort) = sort {
667 columns.push(sort.column);
668 }
669 columns.extend(table.primary_key.iter().map(String::as_str));
670 let descending = sort.is_some_and(|sort| sort.descending);
671
672 let mut any = Cond::any();
673 for (at, column) in columns.iter().enumerate() {
674 let mut branch = Cond::all();
675 for earlier in &columns[..at] {
676 branch =
677 branch.add(Expr::col(Alias::new(*earlier)).eq(cursor_part(table, after, earlier)?));
678 }
679 let value = cursor_part(table, after, column)?;
680 let beyond = if descending {
681 Expr::col(Alias::new(*column)).lt(value)
682 } else {
683 Expr::col(Alias::new(*column)).gt(value)
684 };
685 any = any.add(branch.add(beyond));
686 }
687 Ok(any)
688}
689
690/// Whether a column can be ordered by at all.
691///
692/// Not a nullable one. SQLite sorts `NULL` first and Postgres sorts it
693/// last, so a page ordered by a nullable column is a different page on
694/// each engine — the same declaration serving two answers, which is the
695/// failure the boolean and real decoding already guard against.
696fn sortable(table: &TableDef, sort: Sort<'_>) -> Result<(), DecodeError> {
697 let field = table
698 .fields
699 .iter()
700 .find(|field| field.name == sort.column)
701 .ok_or_else(|| DecodeError {
702 table: table.name.clone(),
703 column: sort.column.to_owned(),
704 detail: "is not a field of this table".to_owned(),
705 })?;
706 let keyed = table.primary_key.iter().any(|key| key == sort.column);
707 if field.required || keyed {
708 return Ok(());
709 }
710 Err(DecodeError {
711 table: table.name.clone(),
712 column: sort.column.to_owned(),
713 detail: "is optional, and the engines disagree about where nulls sort — a page ordered by \
714 it would differ between two deployments of one declaration"
715 .to_owned(),
716 })
717}
718
719/// One value out of a cursor, for a column the page is ordered by.
720fn cursor_part(table: &TableDef, cursor: &Value, column: &str) -> Result<SeaValue, DecodeError> {
721 key_part(table, cursor, column)
722}
723
724/// The bound value for one key column, refusing anything that would not
725/// actually identify a row.
726///
727/// A value of the wrong kind is refused rather than bound as a null: the
728/// null would make `WHERE id = NULL`, which is never true, so the
729/// statement would match nothing and report success.
730fn key_part(table: &TableDef, key: &Value, column: &str) -> Result<SeaValue, DecodeError> {
731 let missing = |detail: &str| DecodeError {
732 table: table.name.clone(),
733 column: column.to_owned(),
734 detail: detail.to_owned(),
735 };
736 let field = table
737 .fields
738 .iter()
739 .find(|field| field.name == column)
740 .ok_or_else(|| missing("a primary-key column that is not a declared field"))?;
741 let value = key
742 .get(column)
743 .filter(|value| !value.is_null())
744 .ok_or_else(|| missing("part of the primary key, and the key does not give it"))?;
745 to_sql(field, Some(value)).ok_or_else(|| {
746 missing(&format!(
747 "part of the primary key, and the value given is not {}",
748 field.kind.as_str()
749 ))
750 })
751}