swadhinbiswas 🚀 release: 2.0.8 — write-path fix (ADR-080), fork-safe GIL pool (ADR-081), HTTP/3 (ADR-082), Security DI fix, docs honesty pass · 8/6/2026
731fffe
172 lines · 5.9 KB
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
//! Integration tests for the database bridge: type fidelity, blob binding,
//! robust transaction detection, streaming, isolation, and multi-engine URL
//! routing. SQLite runs in-memory (always available); Postgres/MySQL tests are
//! skipped unless a `JUSTAPI_TEST_PG_URL` / `JUSTAPI_TEST_MYSQL_URL` env var is
//! provided, so the suite stays hermetic by default.
#![cfg(feature = "db")]

use base64::Engine;
use justapi_core::db::{AnyPool, DatabaseConfig, DbKind, IsolationLevel, Param};

async fn sqlite_pool() -> AnyPool {
    AnyPool::connect(DatabaseConfig {
        url: "sqlite::memory:".into(),
        max_connections: 1,
        kind: Some(DbKind::Sqlite),
        ..Default::default()
    })
    .await
    .unwrap()
}

#[tokio::test]
async fn sqlite_types_and_blobs() {
    let pool = sqlite_pool().await;
    pool.execute(
        "CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT, ok INTEGER, data BLOB, big REAL, nul TEXT)",
    )
    .await
    .unwrap();
    pool.execute_with(
        "INSERT INTO t (name, ok, data, big, nul) VALUES (?, ?, ?, ?, ?)",
        &[
            Param::Text("hi".into()),
            Param::Int(1),
            Param::Bytes(vec![0xDE, 0xAD, 0xBE, 0xEF]),
            Param::Float(f64::NAN),
            Param::Null,
        ],
    )
    .await
    .unwrap();
    let rows = pool.query_with("SELECT * FROM t", &[]).await.unwrap();
    let row = rows.as_array().unwrap()[0].as_object().unwrap();
    assert_eq!(row["name"], serde_json::json!("hi"));
    assert_eq!(row["ok"], serde_json::json!(1));
    assert_eq!(row["nul"], serde_json::Value::Null);
    assert_eq!(
        row["data"],
        serde_json::json!({ "$bytes": base64::engine::general_purpose::STANDARD.encode([0xDE, 0xAD, 0xBE, 0xEF]) })
    );
    assert_eq!(row["big"], serde_json::Value::Null);
}

#[tokio::test]
async fn sqlite_transaction_read_and_write() {
    let pool = sqlite_pool().await;
    pool.execute("CREATE TABLE u (id INTEGER PRIMARY KEY, n INTEGER)").await.unwrap();
    let res = pool
        .transaction(&[
            ("INSERT INTO u (n) VALUES (?)".into(), vec![serde_json::json!(5)]),
            ("INSERT INTO u (n) VALUES (?)".into(), vec![serde_json::json!(7)]),
            ("SELECT SUM(n) AS total FROM u".into(), vec![]),
        ])
        .await
        .unwrap();
    assert_eq!(res, serde_json::json!([{ "total": 12 }]));
}

#[tokio::test]
async fn sqlite_transaction_isolation_serializable() {
    let pool = sqlite_pool().await;
    pool.execute("CREATE TABLE v (n INTEGER)").await.unwrap();
    let res = pool
        .transaction_with_isolation(
            &[("INSERT INTO v (n) VALUES (?)".into(), vec![Param::Int(1)])],
            Some(IsolationLevel::Serializable),
        )
        .await
        .unwrap();
    assert_eq!(res, serde_json::json!({ "rows_affected": 1 }));
}

#[tokio::test]
async fn sqlite_stream_chunks() {
    let pool = sqlite_pool().await;
    pool.execute("CREATE TABLE s (v INTEGER)").await.unwrap();
    for i in 0..7 {
        pool.execute_with("INSERT INTO s (v) VALUES (?)", &[Param::Int(i)]).await.unwrap();
    }
    let chunks = pool.query_stream("SELECT v FROM s ORDER BY v", &[], 3).await.unwrap();
    assert_eq!(chunks.len(), 3);
    assert_eq!(chunks[0].as_array().unwrap().len(), 3);
    assert_eq!(chunks[2].as_array().unwrap().len(), 1);
}

#[tokio::test]
async fn sqlite_insert_returning() {
    let pool = sqlite_pool().await;
    pool.execute("CREATE TABLE r (id INTEGER PRIMARY KEY, name TEXT)").await.unwrap();
    let row = pool
        .insert_returning("r", &["name".into()], &serde_json::json!({ "name": "x" }))
        .await
        .unwrap();
    assert_eq!(row.as_array().unwrap()[0]["name"], serde_json::json!("x"));
}

#[tokio::test]
async fn db_kind_routing() {
    assert_eq!(DbKind::from_url("postgres://h/db"), DbKind::Postgres);
    assert_eq!(DbKind::from_url("postgresql://h/db"), DbKind::Postgres);
    assert_eq!(DbKind::from_url("sqlite::memory:"), DbKind::Sqlite);
    assert_eq!(DbKind::from_url("sqlite://./x"), DbKind::Sqlite);
    assert_eq!(DbKind::from_url("mysql://h/db"), DbKind::MySql);
    assert_eq!(DbKind::from_url("mariadb://h/db"), DbKind::MySql);
}

/// Postgres round-trip (skipped unless JUSTAPI_TEST_PG_URL is set).
#[tokio::test]
async fn pg_roundtrip_if_available() {
    let url = match std::env::var("JUSTAPI_TEST_PG_URL") {
        Ok(u) => u,
        Err(_) => return,
    };
    let pool = AnyPool::connect(DatabaseConfig {
        url,
        max_connections: 4,
        kind: Some(DbKind::Postgres),
        init_sql: Some(
            "DROP TABLE IF EXISTS pg_demo; CREATE TABLE pg_demo (id SERIAL PRIMARY KEY, name TEXT, qty INT)".into(),
        ),
        ..Default::default()
    })
    .await
    .unwrap();
    pool.execute_with(
        "INSERT INTO pg_demo (name, qty) VALUES (?, ?)",
        &[Param::Text("a".into()), Param::Int(3)],
    )
    .await
    .unwrap();
    let rows = pool.query_with("SELECT * FROM pg_demo ORDER BY id", &[]).await.unwrap();
    assert_eq!(rows.as_array().unwrap()[0]["name"], serde_json::json!("a"));
}

/// MySQL round-trip (skipped unless JUSTAPI_TEST_MYSQL_URL is set).
#[tokio::test]
async fn mysql_roundtrip_if_available() {
    let url = match std::env::var("JUSTAPI_TEST_MYSQL_URL") {
        Ok(u) => u,
        Err(_) => return,
    };
    let pool = AnyPool::connect(DatabaseConfig {
        url,
        max_connections: 4,
        kind: Some(DbKind::MySql),
        init_sql: Some(
            "DROP TABLE IF EXISTS my_demo; CREATE TABLE my_demo (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(64), qty INT)".into(),
        ),
        ..Default::default()
    })
    .await
    .unwrap();
    pool.execute_with(
        "INSERT INTO my_demo (name, qty) VALUES (?, ?)",
        &[Param::Text("a".into()), Param::Int(3)],
    )
    .await
    .unwrap();
    let rows = pool.query_with("SELECT * FROM my_demo ORDER BY id", &[]).await.unwrap();
    assert_eq!(rows.as_array().unwrap()[0]["name"], serde_json::json!("a"));
}