rquickjs_extra_sqlite/
statement.rs1use 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}