turso-orm-driver 0.1.7

Connection pool, transactions and typed rows for the Turso database
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
//! End-to-end tests of the driver against a real Turso database.
//!
//! This is the integration tier: every test opens a real engine — in memory
//! or in a temporary file — through the public API and exercises the pool,
//! the transaction lifecycle, streaming and typed decoding together, which
//! the unit tests inside the crate cannot do. Nothing here is mocked, so a
//! failure points at the driver's contract with the engine rather than at
//! a test double.
//!
//! Run with:
//!
//! ```text
//! cargo test -p turso-orm-driver --test driver
//! ```

use futures_util::TryStreamExt;
use turso_orm_driver::{
    ConnectOptions, ConnectionTrait, ConstraintKind, Database, ErrorKind, StreamTrait, Transaction,
    TransactionMode, TransactionTrait,
};
use turso_sql::prelude::*;

/// Opens a small in-memory pool with one `item` table.
///
/// The pool is capped at four connections so that the concurrency test
/// below has to recycle connections rather than open one per task.
async fn setup() -> Database {
    let db = Database::connect(ConnectOptions::in_memory().max_connections(4))
        .await
        .expect("open");
    db.execute_unprepared(
        "CREATE TABLE item (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, price REAL, active INTEGER NOT NULL DEFAULT 1)",
    )
    .await
    .expect("create");
    db
}

/// Builds an insert of one item with the given name and optional price.
fn insert(name: &str, price: Option<f64>) -> Statement {
    Query::insert()
        .into_table("item")
        .columns(["name", "price"])
        .values([Expr::val(name), Expr::val(price)])
        .to_statement()
}

/// Inserts, updates and reads back a row, checking that each column can be
/// decoded by name or position into the type the caller asks for.
#[tokio::test]
async fn crud_and_typed_decoding() {
    let db = setup().await;
    let res = db.execute(insert("pen", Some(1.5))).await.expect("insert");
    assert_eq!(res.rows_affected, 1);
    assert_eq!(res.last_insert_id, 1);

    // The same row is read as several Rust types: lookup is by name or
    // position and case-insensitive, and a missing column is an error for
    // `get` but `None` for `try_get`.
    let row = db
        .query_one(Query::select().from("item").to_statement())
        .await
        .expect("query")
        .expect("row");
    assert_eq!(row.columns(), ["id", "name", "price", "active"]);
    assert_eq!(row.get::<i32>("id").unwrap(), 1);
    assert_eq!(row.get::<i64>(0).unwrap(), 1);
    assert_eq!(row.get::<String>("name").unwrap(), "pen");
    assert_eq!(row.get::<Option<f64>>("price").unwrap(), Some(1.5));
    assert!(row.get::<bool>("active").unwrap());
    assert_eq!(row.get::<String>("ACTIVE").unwrap(), "1");
    assert!(row.get::<i32>("missing").is_err());
    assert_eq!(row.try_get::<i32>("missing").unwrap(), None);

    // Setting the price to NULL shows that only `Option<T>` accepts it.
    let updated = db
        .execute(
            Query::update()
                .table("item")
                .value("price", Expr::val(Option::<f64>::None))
                .and_where(Expr::col("id").eq(1))
                .to_statement(),
        )
        .await
        .expect("update");
    assert_eq!(updated.rows_affected, 1);
    let rows = db
        .query_all(Query::select().column("price").from("item").to_statement())
        .await
        .expect("query");
    assert_eq!(rows[0].get::<Option<f64>>("price").unwrap(), None);
    assert!(rows[0].get::<f64>("price").is_err());
}

/// Taking a column moves its value out and leaves `NULL` behind, whether
/// the columns are taken in select order or not.
#[tokio::test]
async fn take_moves_values_out_of_a_row() {
    let db = setup().await;
    db.execute(insert("taken", Some(2.5)))
        .await
        .expect("insert");
    let mut row = db
        .query_one(
            Query::select()
                .columns(["id", "name", "price", "active"])
                .from("item")
                .to_statement(),
        )
        .await
        .expect("query")
        .expect("row");
    // Out of order first, then in order from the hinted position.
    let price: f64 = row.take("price").expect("price");
    let id: i64 = row.take("id").expect("id");
    let name: String = row.take("name").expect("name");
    let active: bool = row.take("active").expect("active");
    assert_eq!((id, name.as_str(), price, active), (1, "taken", 2.5, true));
    assert_eq!(row.get::<Option<String>>("name").expect("null"), None);
    assert_eq!(row.get::<Option<f64>>("PRICE").expect("null"), None);
    assert!(row.take::<i64>("missing").is_err());
}

/// A unique-constraint violation is classified as such and is not mistaken
/// for lock contention.
#[tokio::test]
async fn constraint_errors_are_classified() {
    let db = setup().await;
    db.execute(insert("dup", None)).await.expect("first");
    let err = db
        .execute(insert("dup", None))
        .await
        .expect_err("duplicate");
    assert_eq!(err.constraint(), Some(ConstraintKind::Unique));
    assert!(!err.is_busy());
}

/// Every pooled connection sees the same in-memory database, and more
/// concurrent tasks than pool slots are served by recycling connections.
#[tokio::test]
async fn pool_shares_memory_database_across_tasks() {
    let db = setup().await;
    let handles: Vec<_> = (0..16)
        .map(|i| {
            let db = db.clone();
            tokio::spawn(async move { db.execute(insert(&format!("n{i}"), None)).await })
        })
        .collect();
    for h in handles {
        h.await.expect("join").expect("insert");
    }
    let row = db
        .query_one(Statement::from_string("SELECT COUNT(*) AS n FROM item"))
        .await
        .expect("count")
        .expect("row");
    assert_eq!(row.get::<i64>("n").unwrap(), 16);
}

/// Counts the items through any connection, so the same check runs on the
/// pool and inside a transaction.
async fn count(c: &impl ConnectionTrait) -> i64 {
    c.query_one(Statement::from_string("SELECT COUNT(*) AS n FROM item"))
        .await
        .expect("count")
        .expect("row")
        .get("n")
        .expect("n")
}

/// Savepoints commit and roll back independently, a dropped savepoint is
/// rolled back lazily, a dropped top-level transaction commits nothing, and
/// the closure form commits on `Ok`.
#[tokio::test]
async fn transactions_savepoints_and_drop() {
    let db = setup().await;
    let txn = db.begin().await.expect("begin");
    txn.execute(insert("a", None)).await.expect("a");
    // A rolled-back savepoint discards its own work only.
    {
        let sp: Transaction = txn.begin().await.expect("savepoint");
        assert_eq!(sp.depth(), 1);
        sp.execute(insert("b", None)).await.expect("b");
        sp.rollback().await.expect("rollback sp");
    }
    // A released savepoint keeps its work in the parent.
    {
        let sp = txn.begin().await.expect("savepoint");
        sp.execute(insert("c", None)).await.expect("c");
        sp.commit().await.expect("release sp");
    }
    // A savepoint dropped without commit is rolled back before the
    // parent's next statement, which here is the count.
    {
        let sp = txn.begin().await.expect("savepoint");
        sp.execute(insert("dropped", None)).await.expect("d");
    }
    assert_eq!(count(&txn).await, 2);
    txn.commit().await.expect("commit");
    assert_eq!(count(&db).await, 2);

    // A top-level transaction dropped without commit discards its
    // connection; nothing it wrote becomes visible.
    {
        let txn = db
            .begin_with_mode(TransactionMode::Immediate)
            .await
            .expect("begin");
        txn.execute(insert("ghost", None)).await.expect("ghost");
    }
    assert_eq!(count(&db).await, 2);

    // The closure form commits when the callback returns `Ok`.
    let n: i64 = db
        .transaction(|txn| {
            Box::pin(async move {
                txn.execute(insert("closure", None)).await?;
                Ok::<_, turso_orm_driver::Error>(count(txn).await)
            })
        })
        .await
        .expect("closure");
    assert_eq!(n, 3);
}

/// Asserts that a call failed with a misuse error.
fn misuse<T: std::fmt::Debug>(result: turso_orm_driver::Result<T>) {
    assert_eq!(result.expect_err("misuse").kind(), ErrorKind::Misuse);
}

/// Only the innermost open transaction may act: a parent fails with a
/// misuse error while a nested transaction is open, and the work of a
/// failed parent is rolled back rather than committed.
#[tokio::test]
async fn nested_transactions_act_innermost_only() {
    let db = setup().await;

    // A parent cannot run a statement or begin a sibling while a nested
    // transaction is open, and recovers once it is finished.
    let txn = db.begin().await.expect("begin");
    let sp = txn.begin().await.expect("savepoint");
    misuse(txn.execute(insert("parent", None)).await);
    misuse(txn.begin().await);
    sp.execute(insert("child", None)).await.expect("child");
    sp.rollback().await.expect("rollback sp");
    txn.execute(insert("parent", None)).await.expect("parent");
    txn.commit().await.expect("commit");
    assert_eq!(count(&db).await, 1);

    // Committing a top level with a nested transaction open fails and rolls
    // everything back; the orphaned savepoint can no longer write.
    let txn = db.begin().await.expect("begin");
    txn.execute(insert("lost", None)).await.expect("lost");
    let sp = txn.begin().await.expect("savepoint");
    misuse(txn.commit().await);
    misuse(sp.execute(insert("orphan", None)).await);
    misuse(sp.rollback().await);
    assert_eq!(count(&db).await, 1);

    // Rolling back a savepoint with a deeper one open fails and drops it
    // unfinished: both are rolled back before the parent's next statement,
    // and the deeper handle can neither write nor disturb the parent.
    let txn = db.begin().await.expect("begin");
    let sp = txn.begin().await.expect("savepoint");
    let inner = sp.begin().await.expect("inner savepoint");
    inner.execute(insert("inner", None)).await.expect("inner");
    misuse(sp.rollback().await);
    assert_eq!(count(&txn).await, 1);
    misuse(inner.execute(insert("stale", None)).await);
    drop(inner);
    txn.execute(insert("kept", None)).await.expect("kept");
    txn.commit().await.expect("commit");
    assert_eq!(count(&db).await, 2);
}

/// A write-write conflict between two `BEGIN CONCURRENT` transactions is
/// reported as busy, so that a caller knows to retry the transaction.
#[tokio::test]
async fn mvcc_write_conflict_is_busy() {
    let db = Database::connect(ConnectOptions::in_memory().mvcc(true))
        .await
        .expect("open");
    db.execute_unprepared("CREATE TABLE counter (id INTEGER PRIMARY KEY, n INTEGER NOT NULL)")
        .await
        .expect("create");
    db.execute_unprepared("INSERT INTO counter VALUES (1, 0)")
        .await
        .expect("seed");
    let first = db
        .begin_with_mode(TransactionMode::Concurrent)
        .await
        .expect("begin first");
    let second = db
        .begin_with_mode(TransactionMode::Concurrent)
        .await
        .expect("begin second");
    first
        .execute_unprepared("UPDATE counter SET n = 1 WHERE id = 1")
        .await
        .expect("first update");
    let err = second
        .execute_unprepared("UPDATE counter SET n = 2 WHERE id = 1")
        .await
        .expect_err("conflict");
    assert!(err.is_busy(), "{err}");
    first.commit().await.expect("commit first");
}

/// Under MVCC, a generated `AUTOINCREMENT` id never reuses an id that was
/// inserted explicitly; `turso` 0.8.3 fixed the engine overwriting row 1.
#[tokio::test]
async fn mvcc_autoincrement_skips_explicit_ids() {
    let db = Database::connect(ConnectOptions::in_memory().mvcc(true))
        .await
        .expect("open");
    db.execute_unprepared("CREATE TABLE t (id INTEGER PRIMARY KEY AUTOINCREMENT, v TEXT NOT NULL)")
        .await
        .expect("create");
    db.execute_unprepared("INSERT INTO t VALUES (1, 'a')")
        .await
        .expect("explicit id");
    db.execute_unprepared("INSERT INTO t (v) VALUES ('b')")
        .await
        .expect("generated id");
    let rows = db
        .query_all(
            Query::select()
                .columns(["id", "v"])
                .from("t")
                .order_by("id", Order::Asc)
                .to_statement(),
        )
        .await
        .expect("select");
    let rows: Vec<(i64, String)> = rows
        .iter()
        .map(|row| (row.get("id").unwrap(), row.get("v").unwrap()))
        .collect();
    assert_eq!(rows, [(1, "a".to_owned()), (2, "b".to_owned())]);
}

/// Under MVCC, a join through a non-unique index sees the rows inserted
/// after a checkpoint as well as the checkpointed ones; `turso` 0.8.3 fixed
/// the engine skipping the index seek that found them.
#[tokio::test]
async fn mvcc_index_join_sees_rows_inserted_after_checkpoint() {
    let db = Database::connect(ConnectOptions::in_memory().mvcc(true))
        .await
        .expect("open");
    for sql in [
        "CREATE TABLE t (id INTEGER PRIMARY KEY, a INTEGER)",
        "CREATE INDEX ia ON t (a)",
        "CREATE TABLE o (k INTEGER)",
        "INSERT INTO t VALUES (15, 2), (24, 3), (52, 3)",
        "INSERT INTO o VALUES (2), (3)",
    ] {
        db.execute_unprepared(sql).await.expect(sql);
    }
    // The checkpoint moves the three rows into the B-tree, so the next
    // insert is the only row left in the MVCC store; the pragma answers
    // with a status row, hence the query.
    db.query_all(Statement::from_string("PRAGMA wal_checkpoint(TRUNCATE)"))
        .await
        .expect("checkpoint");
    db.execute_unprepared("INSERT INTO t VALUES (48, 3)")
        .await
        .expect("insert after checkpoint");
    let rows = db
        .query_all(
            Query::select()
                .expr(Expr::col(("t", "id")))
                .from("o")
                .join(
                    JoinType::Inner,
                    "t",
                    Expr::col(("t", "a")).eq(Expr::col(("o", "k"))),
                )
                .order_by(("t", "id"), Order::Asc)
                .to_statement(),
        )
        .await
        .expect("join");
    let ids: Vec<i64> = rows.iter().map(|row| row.get(0).unwrap()).collect();
    assert_eq!(ids, [15, 24, 48, 52]);
}

/// Rows stream in order from the pool, where the stream carries its own
/// pooled connection, and from a transaction, where it borrows the pinned
/// one.
#[tokio::test]
async fn streaming() {
    let db = setup().await;
    for i in 0..5 {
        db.execute(insert(&format!("s{i}"), None))
            .await
            .expect("insert");
    }
    let names: Vec<String> = db
        .stream(
            Query::select()
                .column("name")
                .from("item")
                .order_by("id", Order::Asc)
                .to_statement(),
        )
        .await
        .expect("stream")
        .map_ok(|row| row.get::<String>("name").unwrap())
        .try_collect()
        .await
        .expect("collect");
    assert_eq!(names, ["s0", "s1", "s2", "s3", "s4"]);

    let txn = db.begin().await.expect("begin");
    let rows: Vec<_> = txn
        .stream(Statement::from_string("SELECT id FROM item"))
        .await
        .expect("stream")
        .try_collect()
        .await
        .expect("collect");
    assert_eq!(rows.len(), 5);
    txn.commit().await.expect("commit");
}

/// A file-backed database is created on disk, answers a ping and runs a
/// multi-statement batch.
#[tokio::test]
async fn file_database_and_ping() {
    let dir = tempfile::tempdir().expect("tempdir");
    let path = dir.path().join("t.db");
    let db = Database::connect(ConnectOptions::new(&path))
        .await
        .expect("open");
    db.ping().await.expect("ping");
    db.execute_unprepared("CREATE TABLE t (id INTEGER PRIMARY KEY); INSERT INTO t DEFAULT VALUES;")
        .await
        .expect("batch");
    assert!(path.exists());
}