1use rainier_orm::sea_query::{
30 Alias, Asterisk, ColumnRef, Cond, Expr, Func, IntoColumnRef, JoinType, OnConflict, Order,
31 Query as SqQuery, SimpleExpr, Value,
32};
33use rainier_orm::{Dialect, Entity, ShardRoute};
34
35use crate::criteria::Criteria;
36
37#[derive(Debug, Clone)]
41pub struct Prepared {
42 pub sql: String,
44 pub params: Vec<Value>,
46 pub route: ShardRoute,
48}
49
50pub fn routing_key(value: &Value) -> Option<u64> {
58 Some(match value {
59 Value::TinyInt(Some(n)) => *n as u64,
60 Value::SmallInt(Some(n)) => *n as u64,
61 Value::Int(Some(n)) => *n as u64,
62 Value::BigInt(Some(n)) => *n as u64,
63 Value::TinyUnsigned(Some(n)) => *n as u64,
64 Value::SmallUnsigned(Some(n)) => *n as u64,
65 Value::Unsigned(Some(n)) => *n as u64,
66 Value::BigUnsigned(Some(n)) => *n,
67 Value::String(Some(s)) => rainier_orm::stable_hash(s.as_bytes()),
68 Value::Char(Some(c)) => rainier_orm::stable_hash(c.encode_utf8(&mut [0u8; 4]).as_bytes()),
69 _ => return None,
70 })
71}
72
73pub fn route_for<E: Entity>(column: &str, value: &Value) -> ShardRoute {
75 if E::shard_columns().contains(&column) {
76 if let Some(key) = routing_key(value) {
77 return ShardRoute::Key(key);
78 }
79 }
80 ShardRoute::Global
81}
82
83fn route_from_pairs<E: Entity>(pairs: &[(&'static str, Value)]) -> ShardRoute {
86 if E::shard_columns().is_empty() {
87 return ShardRoute::Global;
88 }
89 for (column, value) in pairs {
90 let route = route_for::<E>(column, value);
91 if !matches!(route, ShardRoute::Global) {
92 return route;
93 }
94 }
95 ShardRoute::Global
96}
97
98fn alias(name: &str) -> Alias {
99 Alias::new(name)
100}
101
102fn column_ref<E: Entity>(spec: &str) -> ColumnRef {
104 match spec.split_once('.') {
105 Some((table, column)) => (alias(table), alias(column)).into_column_ref(),
106 None => (alias(E::table()), alias(spec)).into_column_ref(),
107 }
108}
109
110fn select_columns<E: Entity>() -> rainier_orm::sea_query::SelectStatement {
112 let mut stmt = SqQuery::select();
113 stmt.from(alias(E::table()));
114 for column in E::columns() {
115 stmt.column((alias(E::table()), alias(column.name)));
116 }
117 stmt
118}
119
120pub fn select_all<E: Entity>(dialect: Dialect) -> Prepared {
122 let stmt = select_columns::<E>();
123 let (sql, params) = dialect.build_query(&stmt);
124 Prepared { sql, params: params.0, route: ShardRoute::Global }
125}
126
127pub fn select_by_pk<E: Entity>(dialect: Dialect, key: Value) -> Prepared {
129 let route = route_for::<E>(E::primary_key(), &key);
130 let mut stmt = select_columns::<E>();
131 stmt.and_where(Expr::col(alias(E::primary_key())).eq(key)).limit(1);
132
133 let (sql, params) = dialect.build_query(&stmt);
134 Prepared { sql, params: params.0, route }
135}
136
137pub fn select_by_column<E: Entity>(
139 dialect: Dialect,
140 column: &str,
141 value: Value,
142 limit: Option<u64>,
143) -> Prepared {
144 let route = route_for::<E>(column, &value);
145 let mut stmt = select_columns::<E>();
146 stmt.and_where(Expr::col(alias(column)).eq(value));
147 if let Some(limit) = limit {
148 stmt.limit(limit);
149 }
150
151 let (sql, params) = dialect.build_query(&stmt);
152 Prepared { sql, params: params.0, route }
153}
154
155pub fn select_matching<E: Entity>(dialect: Dialect, criteria: &Criteria) -> Prepared {
157 let mut stmt = select_columns::<E>();
158 let route = apply_criteria::<E>(&mut stmt, criteria, true);
159
160 let (sql, params) = dialect.build_query(&stmt);
161 Prepared { sql, params: params.0, route }
162}
163
164pub fn count_matching<E: Entity>(dialect: Dialect, criteria: &Criteria) -> Prepared {
169 let mut stmt = SqQuery::select();
170 stmt.from(alias(E::table()));
171 let route = apply_criteria::<E>(&mut stmt, criteria, false);
172 stmt.expr_as(Func::count(Expr::col(Asterisk)), alias("cnt"));
173
174 let (sql, params) = dialect.build_query(&stmt);
175 Prepared { sql, params: params.0, route }
176}
177
178pub fn count_grouped<E: Entity>(dialect: Dialect, column: &str, criteria: &Criteria) -> Prepared {
183 let mut stmt = SqQuery::select();
184 stmt.from(alias(E::table()));
185 let route = apply_criteria::<E>(&mut stmt, criteria, false);
186
187 stmt.column(column_ref::<E>(column));
188 stmt.expr_as(Func::count(Expr::col(Asterisk)), alias("cnt"));
189 stmt.add_group_by(vec![SimpleExpr::Column(column_ref::<E>(column))]);
190
191 let (sql, params) = dialect.build_query(&stmt);
192 Prepared { sql, params: params.0, route }
193}
194
195pub fn select_pivot(dialect: Dialect, query: &crate::relation::PivotQuery) -> Prepared {
202 let mut stmt = SqQuery::select();
203 stmt.from(alias(&query.table));
204 stmt.column((alias(&query.table), alias(&query.parent_column)));
205 stmt.column((alias(&query.table), alias(&query.related_column)));
206 stmt.and_where(Expr::col(alias(&query.parent_column)).is_in(query.parent_keys.iter().cloned()));
207
208 let (sql, params) = dialect.build_query(&stmt);
209 Prepared { sql, params: params.0, route: ShardRoute::Global }
210}
211
212fn apply_criteria<E: Entity>(
214 stmt: &mut rainier_orm::sea_query::SelectStatement,
215 criteria: &Criteria,
216 with_paging: bool,
217) -> ShardRoute {
218 for (table, local, foreign) in criteria.joins() {
219 let on =
220 Expr::col((alias(E::table()), alias(local))).equals((alias(table), alias(foreign)));
221 stmt.join(JoinType::InnerJoin, alias(table), on);
222 }
223
224 let mut condition = Cond::all();
225 let mut route = ShardRoute::Global;
226
227 for constraint in criteria.constraints() {
228 if let (Some((column, value)), true) =
231 (constraint.as_equality(), matches!(route, ShardRoute::Global))
232 {
233 route = route_for::<E>(column, value);
234 }
235 condition = condition.add(constraint.to_expr(column_ref::<E>(constraint.column())));
236 }
237 stmt.cond_where(condition);
238
239 for (column, descending) in criteria.orders() {
240 let order = if *descending { Order::Desc } else { Order::Asc };
241 stmt.order_by(column_ref::<E>(column), order);
242 }
243
244 if with_paging {
245 if let Some(limit) = criteria.limit_value() {
246 stmt.limit(limit);
247 }
248 if let Some(offset) = criteria.offset_value() {
249 stmt.offset(offset);
250 }
251 }
252
253 route
254}
255
256pub fn insert<E: Entity>(dialect: Dialect, entity: &E, assigned_id: Option<u64>) -> Prepared {
261 let mut pairs = entity.insert_values();
262 if let Some(id) = assigned_id {
263 let pk = E::primary_key();
264 for (column, value) in pairs.iter_mut() {
265 if *column == pk {
266 *value = id.into();
267 }
268 }
269 }
270
271 let route = route_from_pairs::<E>(&pairs);
272 let mut stmt = SqQuery::insert();
273 stmt.into_table(alias(E::table()));
274 stmt.columns(pairs.iter().map(|(column, _)| alias(column)));
275 stmt.values_panic(pairs.into_iter().map(|(_, value)| SimpleExpr::from(Expr::val(value))));
276
277 let (sql, params) = dialect.build_query(&stmt);
278 Prepared { sql, params: params.0, route }
279}
280
281pub fn shard_key_for_insert<E: Entity>(entity: &E) -> Option<u64> {
287 let pk = E::primary_key();
288 if !E::shard_columns().contains(&pk) {
289 return None;
290 }
291 if E::columns().iter().find(|c| c.name == pk)?.auto_increment {
292 return None;
293 }
294
295 let pairs = entity.insert_values();
296 let pk_value = pairs.iter().find(|(column, _)| *column == pk).map(|(_, value)| value)?;
297 if routing_key(pk_value) != Some(0) {
298 return None; }
300
301 pairs
302 .iter()
303 .find(|(column, value)| {
304 *column != pk && E::shard_columns().contains(column) && routing_key(value).is_some()
305 })
306 .and_then(|(_, value)| routing_key(value))
307}
308
309pub fn upsert<E: Entity>(
311 dialect: Dialect,
312 entity: &E,
313 conflict_columns: &[&str],
314 update_columns: &[&str],
315) -> Prepared {
316 let pairs = entity.insert_values();
317 let route = route_from_pairs::<E>(&pairs);
318
319 let mut stmt = SqQuery::insert();
320 stmt.into_table(alias(E::table()));
321 stmt.columns(pairs.iter().map(|(column, _)| alias(column)));
322 stmt.values_panic(pairs.into_iter().map(|(_, value)| SimpleExpr::from(Expr::val(value))));
323
324 let mut on_conflict = OnConflict::columns(conflict_columns.iter().map(|c| alias(c)));
325 if !update_columns.is_empty() {
326 on_conflict.update_columns(update_columns.iter().map(|c| alias(c)));
327 } else if let Some(first) = conflict_columns.first() {
328 on_conflict.value(alias(first), Expr::col(alias(first)));
331 } else {
332 on_conflict.do_nothing();
333 }
334 stmt.on_conflict(on_conflict);
335
336 let (sql, params) = dialect.build_query(&stmt);
337 Prepared { sql, params: params.0, route }
338}
339
340pub fn update<E: Entity>(dialect: Dialect, entity: &E) -> Prepared {
342 let key = entity.pk_value();
343 let route = route_for::<E>(E::primary_key(), &key);
344
345 let mut stmt = SqQuery::update();
346 stmt.table(alias(E::table()));
347 for (column, value) in entity.update_values() {
348 stmt.value(alias(column), value);
349 }
350 stmt.and_where(Expr::col(alias(E::primary_key())).eq(key));
351
352 let (sql, params) = dialect.build_query(&stmt);
353 Prepared { sql, params: params.0, route }
354}
355
356pub fn update_matching<E: Entity>(
358 dialect: Dialect,
359 criteria: &Criteria,
360 set: Vec<(String, Value)>,
361) -> Prepared {
362 let mut stmt = SqQuery::update();
363 stmt.table(alias(E::table()));
364 for (column, value) in set {
365 stmt.value(alias(&column), value);
366 }
367
368 let mut condition = Cond::all();
369 let mut route = ShardRoute::Global;
370 for constraint in criteria.constraints() {
371 if let (Some((column, value)), true) =
372 (constraint.as_equality(), matches!(route, ShardRoute::Global))
373 {
374 route = route_for::<E>(column, value);
375 }
376 condition = condition.add(constraint.to_expr(column_ref::<E>(constraint.column())));
377 }
378 stmt.cond_where(condition);
379
380 let (sql, params) = dialect.build_query(&stmt);
381 Prepared { sql, params: params.0, route }
382}
383
384pub fn delete_by_pk<E: Entity>(dialect: Dialect, key: Value) -> Prepared {
386 let route = route_for::<E>(E::primary_key(), &key);
387
388 let mut stmt = SqQuery::delete();
389 stmt.from_table(alias(E::table()));
390 stmt.and_where(Expr::col(alias(E::primary_key())).eq(key));
391
392 let (sql, params) = dialect.build_query(&stmt);
393 Prepared { sql, params: params.0, route }
394}
395
396pub fn delete_matching<E: Entity>(dialect: Dialect, criteria: &Criteria) -> Prepared {
398 let mut stmt = SqQuery::delete();
399 stmt.from_table(alias(E::table()));
400
401 let mut condition = Cond::all();
402 let mut route = ShardRoute::Global;
403 for constraint in criteria.constraints() {
404 if let (Some((column, value)), true) =
405 (constraint.as_equality(), matches!(route, ShardRoute::Global))
406 {
407 route = route_for::<E>(column, value);
408 }
409 condition = condition.add(constraint.to_expr(column_ref::<E>(constraint.column())));
410 }
411 stmt.cond_where(condition);
412
413 let (sql, params) = dialect.build_query(&stmt);
414 Prepared { sql, params: params.0, route }
415}
416
417#[cfg(test)]
418mod tests {
419 use super::*;
420
421 #[derive(rainier_orm::Entity, Clone, Debug)]
422 #[orm(table = "posts")]
423 struct Post {
424 #[orm(pk, auto_increment)]
425 id: u64,
426 title: String,
427 published: bool,
428 }
429
430 #[derive(rainier_orm::Entity, Clone, Debug)]
431 #[orm(table = "tokens")]
432 struct Token {
433 #[orm(pk, shard_key)]
434 id: u64,
435 #[orm(shard_key)]
436 user_id: u64,
437 hash: String,
438 }
439
440 fn post() -> Post {
441 Post { id: 0, title: "Hello".into(), published: true }
442 }
443
444 #[test]
445 fn a_prepared_statement_is_send() {
446 fn assert_send<T: Send>() {}
450 assert_send::<Prepared>();
451 }
452
453 #[test]
454 fn select_all_lists_every_column() {
455 let prepared = select_all::<Post>(Dialect::Sqlite);
456 assert!(prepared.sql.contains("\"posts\""), "{}", prepared.sql);
457 assert!(prepared.sql.contains("\"title\""), "{}", prepared.sql);
458 assert!(prepared.params.is_empty());
459 assert_eq!(prepared.route, ShardRoute::Global);
460 }
461
462 #[test]
463 fn select_by_pk_binds_and_limits() {
464 let prepared = select_by_pk::<Post>(Dialect::Sqlite, 7_i64.into());
465 assert!(prepared.sql.contains("WHERE"), "{}", prepared.sql);
466 assert!(prepared.sql.contains("LIMIT"), "{}", prepared.sql);
467 assert_eq!(prepared.params.len(), 2);
470 assert_eq!(prepared.params[0], 7_i64.into());
471 }
472
473 #[test]
474 fn criteria_become_predicates_ordering_and_paging() {
475 let criteria = Criteria::new()
476 .where_eq("published", true)
477 .where_gt("id", 10_i64)
478 .order_by_desc("id")
479 .limit(5)
480 .offset(10);
481
482 let prepared = select_matching::<Post>(Dialect::Sqlite, &criteria);
483 assert!(prepared.sql.contains("ORDER BY"), "{}", prepared.sql);
484 assert!(prepared.sql.contains("LIMIT"), "{}", prepared.sql);
485 assert!(prepared.sql.contains("OFFSET"), "{}", prepared.sql);
486 assert_eq!(prepared.params.len(), 4);
488 }
489
490 #[test]
491 fn counting_drops_the_paging_but_keeps_the_filters() {
492 let criteria = Criteria::new().where_eq("published", true).limit(5).offset(10);
493 let prepared = count_matching::<Post>(Dialect::Sqlite, &criteria);
494
495 assert!(prepared.sql.contains("COUNT"), "{}", prepared.sql);
496 assert!(prepared.sql.contains("cnt"), "{}", prepared.sql);
497 assert!(!prepared.sql.contains("LIMIT"), "{}", prepared.sql);
498 assert!(!prepared.sql.contains("ORDER BY"), "{}", prepared.sql);
499 assert_eq!(prepared.params.len(), 1);
500 }
501
502 #[test]
503 fn insert_omits_an_auto_increment_key() {
504 let prepared = insert::<Post>(Dialect::Sqlite, &post(), None);
505 assert!(prepared.sql.starts_with("INSERT INTO"), "{}", prepared.sql);
506 assert!(!prepared.sql.contains("\"id\""), "auto-increment keys are the DB's job");
507 assert_eq!(prepared.params.len(), 2);
508 }
509
510 #[test]
511 fn update_writes_every_column_but_the_key() {
512 let prepared = update::<Post>(Dialect::Sqlite, &Post { id: 3, ..post() });
513 assert!(prepared.sql.starts_with("UPDATE"), "{}", prepared.sql);
514 assert!(prepared.sql.contains("WHERE"), "{}", prepared.sql);
515 assert_eq!(prepared.params.len(), 3);
517 }
518
519 #[test]
520 fn upsert_renders_a_conflict_clause() {
521 let prepared = upsert::<Post>(Dialect::Sqlite, &post(), &["title"], &["published"]);
522 assert!(prepared.sql.contains("ON CONFLICT"), "{}", prepared.sql);
523 }
524
525 #[test]
526 fn an_upsert_with_no_updates_is_insert_or_ignore() {
527 let prepared = upsert::<Post>(Dialect::Sqlite, &post(), &["title"], &[]);
528 assert!(prepared.sql.contains("ON CONFLICT"), "{}", prepared.sql);
530 assert!(!prepared.sql.contains("DO NOTHING"), "{}", prepared.sql);
531 }
532
533 #[test]
534 fn delete_targets_the_key() {
535 let prepared = delete_by_pk::<Post>(Dialect::Sqlite, 3_i64.into());
536 assert!(prepared.sql.starts_with("DELETE FROM"), "{}", prepared.sql);
537 assert_eq!(prepared.params.len(), 1);
538 }
539
540 #[test]
541 fn every_dialect_renders() {
542 for dialect in [Dialect::Sqlite, Dialect::MySql, Dialect::Postgres] {
543 let prepared = select_by_pk::<Post>(dialect, 1_i64.into());
544 assert!(prepared.sql.contains("posts"), "{dialect:?}: {}", prepared.sql);
545 }
546 assert!(select_by_pk::<Post>(Dialect::Postgres, 1_i64.into()).sql.contains("$1"));
548 }
549
550 #[test]
553 fn a_global_entity_always_routes_globally() {
554 assert_eq!(route_for::<Post>("id", &7_i64.into()), ShardRoute::Global);
555 assert_eq!(select_by_pk::<Post>(Dialect::Sqlite, 7_i64.into()).route, ShardRoute::Global);
556 }
557
558 #[test]
559 fn a_shard_key_column_routes_by_its_value() {
560 assert_eq!(route_for::<Token>("id", &7_u64.into()), ShardRoute::Key(7));
561 assert_eq!(route_for::<Token>("user_id", &9_u64.into()), ShardRoute::Key(9));
562 assert_eq!(
563 route_for::<Token>("hash", &"abc".into()),
564 ShardRoute::Global,
565 "a non-shard column never routes"
566 );
567 }
568
569 #[test]
570 fn a_criteria_equality_on_a_shard_column_pins_the_query() {
571 let criteria = Criteria::new().where_eq("user_id", 42_u64);
572 assert_eq!(select_matching::<Token>(Dialect::Sqlite, &criteria).route, ShardRoute::Key(42));
573 }
574
575 #[test]
576 fn string_routing_keys_hash_stably() {
577 let expected = rainier_orm::stable_hash(b"user@example.com");
580 assert_eq!(routing_key(&"user@example.com".into()), Some(expected));
581 }
582
583 #[test]
584 fn a_null_or_unroutable_value_has_no_key() {
585 assert_eq!(routing_key(&Value::BigInt(None)), None);
586 assert_eq!(routing_key(&Value::Bool(Some(true))), None);
587 }
588
589 #[test]
590 fn an_insert_routes_by_the_rows_shard_key() {
591 let token = Token { id: 100, user_id: 42, hash: "x".into() };
592 assert_eq!(insert::<Token>(Dialect::Sqlite, &token, None).route, ShardRoute::Key(100));
593 }
594
595 #[test]
596 fn an_unset_shard_encoded_key_asks_the_connector_to_mint_one() {
597 let token = Token { id: 0, user_id: 42, hash: "x".into() };
598 assert_eq!(shard_key_for_insert::<Token>(&token), Some(42));
599
600 let assigned = Token { id: 100, user_id: 42, hash: "x".into() };
602 assert_eq!(shard_key_for_insert::<Token>(&assigned), None);
603
604 assert_eq!(shard_key_for_insert::<Post>(&post()), None);
606 }
607
608 #[test]
609 fn a_minted_id_is_written_into_the_insert() {
610 let token = Token { id: 0, user_id: 42, hash: "x".into() };
611 let prepared = insert::<Token>(Dialect::Sqlite, &token, Some(4242));
612 assert_eq!(prepared.route, ShardRoute::Key(4242));
613 }
614}