Skip to main content

toasty_driver_integration_suite/tests/
raw_sql.rs

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}