rquickjs_extra_sqlite/
statement.rs

1use rquickjs::function::Rest;
2use rquickjs::{class::Trace, Ctx, JsLifetime, Object, Result};
3use rquickjs_extra_utils::result::ResultExt;
4use sqlx::query::Query;
5use sqlx::sqlite::SqliteArguments;
6use sqlx::Sqlite;
7use sqlx::{sqlite::SqliteStatement, Column as _, Row as _, SqlitePool, Statement as _};
8
9use super::{Argument, Value};
10
11#[derive(Trace, JsLifetime)]
12#[rquickjs::class]
13pub struct Statement {
14    #[qjs(skip_trace)]
15    stmt: SqliteStatement<'static>,
16    #[qjs(skip_trace)]
17    pool: SqlitePool,
18}
19
20impl Statement {
21    pub fn new(stmt: SqliteStatement<'static>, pool: SqlitePool) -> Self {
22        Self { stmt, pool }
23    }
24
25    fn query<'js, 'q>(
26        &'q self,
27        ctx: &Ctx<'js>,
28        binds: &'q [Argument<'js>],
29    ) -> Result<Query<'q, Sqlite, SqliteArguments<'q>>>
30    where
31        'js: 'q,
32    {
33        let mut query = self.stmt.query();
34        for value in binds {
35            value.try_bind(ctx, &mut query)?;
36        }
37        Ok(query)
38    }
39
40    fn row_to_object<'js>(ctx: &Ctx<'js>, row: &sqlx::sqlite::SqliteRow) -> Result<Object<'js>> {
41        let obj = Object::new(ctx.clone())?;
42        for column in row.columns() {
43            let value = Value::try_read(ctx, column, row)?;
44            obj.set(column.name(), value)?;
45        }
46        Ok(obj)
47    }
48}
49
50#[rquickjs::methods(rename_all = "camelCase")]
51impl Statement {
52    async fn all<'js>(
53        &self,
54        ctx: Ctx<'js>,
55        anon_params: Rest<Argument<'js>>,
56    ) -> Result<Vec<Object<'js>>> {
57        let query = self.query(&ctx, &anon_params.0)?;
58
59        let rows = query.fetch_all(&self.pool).await.or_throw(&ctx)?;
60
61        let mut res = Vec::with_capacity(rows.len());
62        for row in rows {
63            let obj = Self::row_to_object(&ctx, &row)?;
64            res.push(obj);
65        }
66        Ok(res)
67    }
68
69    async fn get<'js>(
70        &self,
71        ctx: Ctx<'js>,
72        anon_params: Rest<Argument<'js>>,
73    ) -> Result<Option<Object<'js>>> {
74        let query = self.query(&ctx, &anon_params.0)?;
75
76        let Some(row) = query.fetch_optional(&self.pool).await.or_throw(&ctx)? else {
77            return Ok(None);
78        };
79
80        let obj = Self::row_to_object(&ctx, &row)?;
81        Ok(Some(obj))
82    }
83
84    async fn run<'js>(
85        &self,
86        ctx: Ctx<'js>,
87        anon_params: Rest<Argument<'js>>,
88    ) -> Result<Object<'js>> {
89        let query = self.query(&ctx, &anon_params.0)?;
90
91        let res = query.execute(&self.pool).await.or_throw(&ctx)?;
92
93        let obj = Object::new(ctx.clone())?;
94        obj.set("changes", res.rows_affected())?;
95        obj.set("lastInsertRowid", res.last_insert_rowid())?;
96        Ok(obj)
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use rquickjs::CatchResultExt;
103    use rquickjs_extra_test::{call_test, test_async_with, ModuleEvaluator};
104
105    use crate::SqliteModule;
106
107    #[tokio::test]
108    async fn test_statement_all() {
109        test_async_with(|ctx| {
110            Box::pin(async move {
111                ModuleEvaluator::eval_rust::<SqliteModule>(ctx.clone(), "sqlite")
112                    .await
113                    .unwrap();
114
115                let module = ModuleEvaluator::eval_js(
116                    ctx.clone(),
117                    "test",
118                    r#"
119                        import { open } from "sqlite";
120
121                        export async function test() {
122                            const db = await open({ inMemory: true });
123                            await db.exec("CREATE TABLE IF NOT EXISTS test (id INTEGER PRIMARY KEY, name TEXT)");
124                            await db.exec("INSERT INTO test (name) VALUES ('test')");
125                            await db.exec("INSERT INTO test (name) VALUES ('test2')");
126                            const stmt = await db.prepare("SELECT * FROM test");
127                            const rows = await stmt.all();
128                            return rows[1].id;
129                        }
130                    "#,
131                )
132                .await
133                .catch(&ctx)
134                .unwrap();
135
136                let result = call_test::<i64, _>(&ctx, &module, ()).await;
137                assert_eq!(result, 2);
138            })
139        })
140        .await;
141    }
142
143    #[tokio::test]
144    async fn test_statement_get() {
145        test_async_with(|ctx| {
146            Box::pin(async move {
147                ModuleEvaluator::eval_rust::<SqliteModule>(ctx.clone(), "sqlite")
148                    .await
149                    .unwrap();
150
151                let module = ModuleEvaluator::eval_js(
152                    ctx.clone(),
153                    "test",
154                    r#"
155                        import { open } from "sqlite";
156
157                        export async function test() {
158                            const db = await open({ inMemory: true });
159                            await db.exec("CREATE TABLE IF NOT EXISTS test (id INTEGER PRIMARY KEY, name TEXT)");
160                            await db.exec("INSERT INTO test (name) VALUES ('test')");
161                            const stmt = await db.prepare("SELECT * FROM test WHERE name = ?");
162                            const row = await stmt.get('test');
163                            return row.id;
164                        }
165                    "#,
166                )
167                .await
168                .catch(&ctx)
169                .unwrap();
170
171                let result = call_test::<i64, _>(&ctx, &module, ()).await;
172                assert_eq!(result, 1);
173            })
174        })
175        .await;
176    }
177
178    #[tokio::test]
179    async fn test_statement_run() {
180        test_async_with(|ctx| {
181            Box::pin(async move {
182                ModuleEvaluator::eval_rust::<SqliteModule>(ctx.clone(), "sqlite")
183                    .await
184                    .unwrap();
185
186                let module = ModuleEvaluator::eval_js(
187                    ctx.clone(),
188                    "test",
189                    r#"
190                        import { open } from "sqlite";
191
192                        export async function test() {
193                            const db = await open({ inMemory: true });
194                            await db.exec("CREATE TABLE IF NOT EXISTS test (id INTEGER PRIMARY KEY, name TEXT)");
195                            const stmt = await db.prepare("INSERT INTO test (name) VALUES (?), (?)");
196                            const res = await stmt.run('test', 'test2');
197                            return res.changes;
198                        }
199                    "#,
200                )
201                .await
202                .catch(&ctx)
203                .unwrap();
204
205                let result = call_test::<i64, _>(&ctx, &module, ()).await;
206                assert_eq!(result, 2);
207            })
208        })
209        .await;
210    }
211}