1use crate::prelude::*;
2
3use toasty::stmt::{self, Value};
4use toasty_core::{
5 driver::{Operation, operation::RawSqlRet},
6 schema::db,
7};
8
9#[driver_test(requires(sql))]
10pub async fn statement_and_query_on_db(t: &mut Test) -> Result<()> {
11 #[derive(Debug, toasty::Model)]
12 struct User {
13 #[key]
14 #[auto]
15 id: u64,
16 #[index]
17 name: String,
18 }
19
20 let mut db = t.setup_db(models!(User)).await;
21 let table = table_name(&db, "users");
22
23 toasty::create!(User { name: "Alice" })
24 .exec(&mut db)
25 .await?;
26
27 let count = toasty::sql::statement(format!(
28 "UPDATE {table} SET name = {} WHERE name = {}",
29 placeholder(&db, 1),
30 placeholder(&db, 2),
31 ))
32 .bind("Bob")
33 .bind("Alice")
34 .exec(&mut db)
35 .await?;
36 assert_eq!(count, 1);
37
38 let rows = toasty::sql::query(format!("SELECT name FROM {table}"))
39 .exec(&mut db)
40 .await?;
41 assert_eq!(rows.len(), 1);
42 assert_eq!(field(&rows[0], 0), &toasty::stmt::Value::from("Bob"));
43
44 Ok(())
45}
46
47#[driver_test(requires(sql))]
48pub async fn query_on_connection(t: &mut Test) -> Result<()> {
49 #[derive(Debug, toasty::Model)]
50 struct User {
51 #[key]
52 #[auto]
53 id: u64,
54 #[index]
55 name: String,
56 }
57
58 let mut db = t.setup_db(models!(User)).await;
59 let table = table_name(&db, "users");
60
61 toasty::create!(User { name: "Alice" })
62 .exec(&mut db)
63 .await?;
64
65 let mut conn = db.connection().await?;
66 let rows = toasty::sql::query(format!(
67 "SELECT name FROM {table} WHERE name = {}",
68 placeholder(&db, 1),
69 ))
70 .bind("Alice")
71 .exec(&mut conn)
72 .await?;
73
74 assert_eq!(rows.len(), 1);
75 assert_eq!(field(&rows[0], 0), &toasty::stmt::Value::from("Alice"));
76
77 Ok(())
78}
79
80#[driver_test(requires(sql))]
81pub async fn statement_inside_transaction_rolls_back(t: &mut Test) -> Result<()> {
82 #[derive(Debug, toasty::Model)]
83 struct User {
84 #[key]
85 #[auto]
86 id: u64,
87 #[index]
88 name: String,
89 }
90
91 let mut db = t.setup_db(models!(User)).await;
92 let table = table_name(&db, "users");
93
94 toasty::create!(User { name: "Alice" })
95 .exec(&mut db)
96 .await?;
97
98 let mut tx = db.transaction().await?;
99 toasty::sql::statement(format!("UPDATE {table} SET name = 'Bob'"))
100 .exec(&mut tx)
101 .await?;
102 tx.rollback().await?;
103
104 let user = User::filter_by_name("Alice").get(&mut db).await?;
105 assert_eq!(user.name, "Alice");
106
107 Ok(())
108}
109
110#[driver_test(requires(sql))]
111pub async fn query_is_sent_as_raw_sql_driver_operation(t: &mut Test) -> Result<()> {
112 #[derive(Debug, toasty::Model)]
113 struct Item {
114 #[key]
115 #[auto]
116 id: u64,
117 }
118
119 let mut db = t.setup_db(models!(Item)).await;
120 let sql = format!("SELECT {}", placeholder(&db, 1));
121
122 t.log().clear();
123 let rows = toasty::sql::query(sql.clone())
124 .bind_typed(Value::I64(123), db::Type::Integer(8))
125 .column_types([stmt::Type::I64])
126 .exec(&mut db)
127 .await?;
128
129 assert_eq!(rows.len(), 1);
130 assert_eq!(field(&rows[0], 0), &Value::I64(123));
131
132 let (op, _) = t.log().pop();
133 let Operation::RawSql(op) = op else {
134 panic!("expected RawSql operation, got {op:?}");
135 };
136
137 assert_eq!(op.sql, sql);
138 assert_eq!(op.params.len(), 1);
139 assert_eq!(op.params[0].value, Value::I64(123));
140 assert_eq!(op.params[0].ty, db::Type::Integer(8));
141 assert!(matches!(op.ret, RawSqlRet::Types(types) if types == vec![stmt::Type::I64]));
142 assert!(t.log().is_empty());
143
144 Ok(())
145}
146
147#[driver_test(requires(sql))]
148pub async fn query_infers_storage_values(t: &mut Test) -> Result<()> {
149 #[derive(Debug, toasty::Model)]
150 struct Item {
151 #[key]
152 #[auto]
153 id: u64,
154 bool_v: bool,
155 int_v: i64,
156 float_v: f64,
157 string_v: String,
158 bytes_v: Vec<u8>,
159 uuid_v: uuid::Uuid,
160 }
161
162 let mut db = t.setup_db(models!(Item)).await;
163 let table = table_name(&db, "items");
164 let uuid = uuid::Uuid::from_u128(0x0123456789abcdef0123456789abcdef);
165
166 let item = toasty::create!(Item {
167 bool_v: true,
168 int_v: -42,
169 float_v: 10.25,
170 string_v: "hello".to_string(),
171 bytes_v: vec![0, 1, 2, 255],
172 uuid_v: uuid,
173 })
174 .exec(&mut db)
175 .await?;
176
177 let rows = toasty::sql::query(format!(
178 "SELECT bool_v, int_v, float_v, string_v, bytes_v, uuid_v FROM {table} WHERE id = {}",
179 placeholder(&db, 1),
180 ))
181 .bind(item.id)
182 .exec(&mut db)
183 .await?;
184
185 assert_eq!(rows.len(), 1);
186
187 let expected_bool = if is_postgresql(&db) {
188 Value::Bool(true)
189 } else {
190 Value::I64(1)
191 };
192 let expected_uuid = match db.capability().sql_placeholder.unwrap() {
193 toasty::SqlPlaceholder::DollarNumber => Value::Uuid(uuid),
194 toasty::SqlPlaceholder::QuestionMark => Value::String(uuid.to_string()),
195 toasty::SqlPlaceholder::NumberedQuestionMark => Value::Bytes(uuid.as_bytes().to_vec()),
196 };
197 let expected = vec![
198 expected_bool,
199 Value::I64(-42),
200 Value::F64(10.25),
201 Value::String("hello".to_string()),
202 Value::Bytes(vec![0, 1, 2, 255]),
203 expected_uuid,
204 ];
205
206 assert_eq!(fields(&rows[0]), expected.as_slice());
207
208 Ok(())
209}
210
211#[driver_test(requires(sql))]
212pub async fn query_column_types_decode_scalars(t: &mut Test) -> Result<()> {
213 #[derive(Debug, toasty::Model)]
214 struct Item {
215 #[key]
216 #[auto]
217 id: u64,
218 bool_v: bool,
219 i8_v: i8,
220 i16_v: i16,
221 i32_v: i32,
222 i64_v: i64,
223 u8_v: u8,
224 u16_v: u16,
225 u32_v: u32,
226 u64_v: u64,
227 f32_v: f32,
228 f64_v: f64,
229 string_v: String,
230 bytes_v: Vec<u8>,
231 #[column(type = text)]
232 uuid_v: uuid::Uuid,
233 }
234
235 let mut db = t.setup_db(models!(Item)).await;
236 let table = table_name(&db, "items");
237 let uuid = uuid::Uuid::from_u128(0xfedcba9876543210fedcba9876543210);
238
239 let item = toasty::create!(Item {
240 bool_v: true,
241 i8_v: -8,
242 i16_v: -16,
243 i32_v: -32,
244 i64_v: -64,
245 u8_v: 8,
246 u16_v: 16,
247 u32_v: 32,
248 u64_v: 64,
249 f32_v: 1.25,
250 f64_v: 2.5,
251 string_v: "hello".to_string(),
252 bytes_v: vec![3, 4, 5, 255],
253 uuid_v: uuid,
254 })
255 .exec(&mut db)
256 .await?;
257
258 let rows = toasty::sql::query(format!(
259 "SELECT bool_v, i8_v, i16_v, i32_v, i64_v, u8_v, u16_v, u32_v, u64_v, \
260 f32_v, f64_v, string_v, bytes_v, uuid_v FROM {table} WHERE id = {}",
261 placeholder(&db, 1),
262 ))
263 .bind(item.id)
264 .column_types([
265 stmt::Type::Bool,
266 stmt::Type::I8,
267 stmt::Type::I16,
268 stmt::Type::I32,
269 stmt::Type::I64,
270 stmt::Type::U8,
271 stmt::Type::U16,
272 stmt::Type::U32,
273 stmt::Type::U64,
274 stmt::Type::F32,
275 stmt::Type::F64,
276 stmt::Type::String,
277 stmt::Type::Bytes,
278 stmt::Type::Uuid,
279 ])
280 .exec(&mut db)
281 .await?;
282
283 let expected = vec![
284 Value::Bool(true),
285 Value::I8(-8),
286 Value::I16(-16),
287 Value::I32(-32),
288 Value::I64(-64),
289 Value::U8(8),
290 Value::U16(16),
291 Value::U32(32),
292 Value::U64(64),
293 Value::F32(1.25),
294 Value::F64(2.5),
295 Value::String("hello".to_string()),
296 Value::Bytes(vec![3, 4, 5, 255]),
297 Value::Uuid(uuid),
298 ];
299
300 assert_eq!(rows.len(), 1);
301 assert_eq!(fields(&rows[0]), expected.as_slice());
302
303 Ok(())
304}
305
306#[driver_test(requires(sql))]
307pub async fn statement_binds_typed_null(t: &mut Test) -> Result<()> {
308 #[derive(Debug, toasty::Model)]
309 struct Item {
310 #[key]
311 #[auto]
312 id: u64,
313 name: Option<String>,
314 }
315
316 let mut db = t.setup_db(models!(Item)).await;
317 let table = table_name(&db, "items");
318
319 let count = toasty::sql::statement(format!(
320 "INSERT INTO {table} (name) VALUES ({})",
321 placeholder(&db, 1),
322 ))
323 .bind_typed(Value::Null, toasty::schema::db::Type::Text)
324 .exec(&mut db)
325 .await?;
326 assert_eq!(count, 1);
327
328 let rows = toasty::sql::query(format!("SELECT name FROM {table}"))
329 .exec(&mut db)
330 .await?;
331
332 assert_eq!(rows.len(), 1);
333 assert_eq!(field(&rows[0], 0), &Value::Null);
334
335 Ok(())
336}
337
338#[driver_test(requires(and(sql, vec_scalar)))]
339pub async fn query_column_types_decode_list(t: &mut Test) -> Result<()> {
340 #[derive(Debug, toasty::Model)]
341 struct Item {
342 #[key]
343 #[auto]
344 id: u64,
345 tags: Vec<String>,
346 }
347
348 let mut db = t.setup_db(models!(Item)).await;
349 let table = table_name(&db, "items");
350 let tags = vec!["rust".to_string(), "toasty".to_string()];
351
352 let item = toasty::create!(Item { tags: tags.clone() })
353 .exec(&mut db)
354 .await?;
355
356 let rows = toasty::sql::query(format!(
357 "SELECT tags FROM {table} WHERE id = {}",
358 placeholder(&db, 1),
359 ))
360 .bind(item.id)
361 .column_types([stmt::Type::list(stmt::Type::String)])
362 .exec(&mut db)
363 .await?;
364
365 assert_eq!(rows.len(), 1);
366 assert_eq!(
367 field(&rows[0], 0),
368 &Value::List(tags.into_iter().map(Value::String).collect())
369 );
370
371 Ok(())
372}
373
374#[driver_test(requires(and(sql, native_datetime)))]
375pub async fn query_column_types_decode_temporal(t: &mut Test) -> Result<(), BoxError> {
376 #[derive(Debug, toasty::Model)]
377 struct Item {
378 #[key]
379 #[auto]
380 id: u64,
381 timestamp_v: jiff::Timestamp,
382 date_v: jiff::civil::Date,
383 time_v: jiff::civil::Time,
384 datetime_v: jiff::civil::DateTime,
385 }
386
387 let mut db = t.setup_db(models!(Item)).await;
388 let table = table_name(&db, "items");
389 let timestamp = jiff::Timestamp::from_second(1_700_000_000)?
390 .checked_add(jiff::Span::new().nanoseconds(123_456_000))?;
391 let date = jiff::civil::date(2025, 6, 15);
392 let time = jiff::civil::time(9, 30, 45, 123_456_000);
393 let datetime = jiff::civil::datetime(2025, 6, 15, 9, 30, 45, 123_456_000);
394
395 let item = toasty::create!(Item {
396 timestamp_v: timestamp,
397 date_v: date,
398 time_v: time,
399 datetime_v: datetime,
400 })
401 .exec(&mut db)
402 .await?;
403
404 let rows = toasty::sql::query(format!(
405 "SELECT timestamp_v, date_v, time_v, datetime_v FROM {table} WHERE id = {}",
406 placeholder(&db, 1),
407 ))
408 .bind(item.id)
409 .column_types([
410 stmt::Type::Timestamp,
411 stmt::Type::Date,
412 stmt::Type::Time,
413 stmt::Type::DateTime,
414 ])
415 .exec(&mut db)
416 .await?;
417
418 let expected = vec![
419 Value::Timestamp(timestamp),
420 Value::Date(date),
421 Value::Time(time),
422 Value::DateTime(datetime),
423 ];
424
425 assert_eq!(rows.len(), 1);
426 assert_eq!(fields(&rows[0]), expected.as_slice());
427
428 Ok(())
429}
430
431#[driver_test(requires(and(sql, native_decimal)))]
432pub async fn query_column_types_decode_decimal(t: &mut Test) -> Result<(), BoxError> {
433 use rust_decimal::Decimal;
434 use std::str::FromStr;
435
436 #[derive(Debug, toasty::Model)]
437 struct Item {
438 #[key]
439 #[auto]
440 id: u64,
441 #[column(type = numeric(28, 10))]
442 val: Decimal,
443 }
444
445 let mut db = t.setup_db(models!(Item)).await;
446 let table = table_name(&db, "items");
447 let val = Decimal::from_str("123456789012345678.1234567890")?;
448
449 let item = toasty::create!(Item { val }).exec(&mut db).await?;
450
451 let rows = toasty::sql::query(format!(
452 "SELECT val FROM {table} WHERE id = {}",
453 placeholder(&db, 1),
454 ))
455 .bind(item.id)
456 .column_types([stmt::Type::Decimal])
457 .exec(&mut db)
458 .await?;
459
460 assert_eq!(rows.len(), 1);
461 assert_eq!(field(&rows[0], 0), &Value::Decimal(val));
462
463 Ok(())
464}
465
466#[driver_test(requires(and(sql, native_decimal, bigdecimal_implemented)))]
467pub async fn query_column_types_decode_bigdecimal(t: &mut Test) -> Result<(), BoxError> {
468 use bigdecimal::BigDecimal;
469 use std::str::FromStr;
470
471 #[derive(Debug, toasty::Model)]
472 struct Item {
473 #[key]
474 #[auto]
475 id: u64,
476 #[column(type = numeric(38, 20))]
477 val: BigDecimal,
478 }
479
480 let mut db = t.setup_db(models!(Item)).await;
481 let table = table_name(&db, "items");
482 let val = BigDecimal::from_str("123456789012345678.12345678901234567890")?;
483
484 let item = toasty::create!(Item { val: val.clone() })
485 .exec(&mut db)
486 .await?;
487
488 let rows = toasty::sql::query(format!(
489 "SELECT val FROM {table} WHERE id = {}",
490 placeholder(&db, 1),
491 ))
492 .bind(item.id)
493 .column_types([stmt::Type::BigDecimal])
494 .exec(&mut db)
495 .await?;
496
497 assert_eq!(rows.len(), 1);
498 assert_eq!(field(&rows[0], 0), &Value::BigDecimal(val));
499
500 Ok(())
501}
502
503#[driver_test(requires(not(sql)))]
504pub async fn raw_sql_rejects_non_sql_drivers(t: &mut Test) -> Result<()> {
505 #[derive(Debug, toasty::Model)]
506 struct Item {
507 #[key]
508 id: uuid::Uuid,
509 name: String,
510 }
511
512 let mut db = t.setup_db(models!(Item)).await;
513 let err = toasty::sql::statement("SELECT 1")
514 .exec(&mut db)
515 .await
516 .unwrap_err();
517
518 assert!(
519 err.is_unsupported_feature(),
520 "expected UnsupportedFeature, got: {err}"
521 );
522
523 Ok(())
524}
525
526fn table_name(db: &toasty::Db, suffix: &str) -> String {
527 db.schema()
528 .db
529 .tables
530 .iter()
531 .find(|table| table.name.ends_with(suffix))
532 .map(|table| table.name.clone())
533 .unwrap_or_else(|| panic!("table {suffix} not found"))
534}
535
536fn placeholder(db: &toasty::Db, index: usize) -> String {
537 render_placeholder(
538 db.capability()
539 .sql_placeholder
540 .expect("SQL driver has placeholders"),
541 index,
542 )
543}
544
545fn render_placeholder(placeholder: toasty::SqlPlaceholder, index: usize) -> String {
546 match placeholder {
547 toasty::SqlPlaceholder::QuestionMark => "?".to_string(),
548 toasty::SqlPlaceholder::NumberedQuestionMark => format!("?{index}"),
549 toasty::SqlPlaceholder::DollarNumber => format!("${index}"),
550 }
551}
552
553fn is_postgresql(db: &toasty::Db) -> bool {
554 matches!(
555 db.capability().sql_placeholder,
556 Some(toasty::SqlPlaceholder::DollarNumber)
557 )
558}
559
560fn fields(value: &Value) -> &[Value] {
561 let Value::Record(record) = value else {
562 panic!("expected record, got {value:?}");
563 };
564
565 record
566}
567
568fn field(value: &toasty::stmt::Value, index: usize) -> &toasty::stmt::Value {
569 &fields(value)[index]
570}