1use async_trait::async_trait;
13use futures::StreamExt;
14use sqlx::{Column, Executor, Row, TypeInfo};
15use std::collections::HashMap;
16use std::future::Future;
17use std::pin::Pin;
18use std::str::FromStr;
19use std::sync::Arc;
20use sz_orm_core::{
21 ColType, Connection, ConnectionFactory, DbError, DbType, QueryRows, QueryValues, Value,
22};
23
24use crate::error::map_sqlx_error;
25
26fn needs_raw_sql(sql: &str) -> bool {
29 let trimmed = sql.trim_start();
30 let upper = trimmed.to_uppercase();
31 upper.starts_with("BEGIN")
32 || upper.starts_with("COMMIT")
33 || upper.starts_with("ROLLBACK")
34 || upper.starts_with("SAVEPOINT")
35 || upper.starts_with("RELEASE")
36 || upper.starts_with("SET ")
37 || upper.starts_with("USE ")
38 || upper.starts_with("START TRANSACTION")
39}
40
41fn row_to_value_with_coltype_sqlite(
59 row: &sqlx::sqlite::SqliteRow,
60 ordinal: usize,
61 col_type: ColType,
62) -> Value {
63 match col_type {
64 ColType::Bool => match row.try_get::<Option<bool>, usize>(ordinal) {
65 Ok(v) => v.map(Value::Bool).unwrap_or(Value::Null),
66 Err(_) => Value::Null,
67 },
68 ColType::I8 => match row.try_get::<Option<i8>, usize>(ordinal) {
69 Ok(v) => v.map(Value::I8).unwrap_or(Value::Null),
70 Err(_) => Value::Null,
71 },
72 ColType::I16 => match row.try_get::<Option<i16>, usize>(ordinal) {
73 Ok(v) => v.map(Value::I16).unwrap_or(Value::Null),
74 Err(_) => Value::Null,
75 },
76 ColType::I32 => match row.try_get::<Option<i32>, usize>(ordinal) {
77 Ok(v) => v.map(Value::I32).unwrap_or(Value::Null),
78 Err(_) => Value::Null,
79 },
80 ColType::I64 => match row.try_get::<Option<i64>, usize>(ordinal) {
81 Ok(v) => v.map(Value::I64).unwrap_or(Value::Null),
82 Err(_) => Value::Null,
83 },
84 ColType::U8 => match row.try_get::<Option<u8>, usize>(ordinal) {
85 Ok(v) => v.map(Value::U8).unwrap_or(Value::Null),
86 Err(_) => Value::Null,
87 },
88 ColType::U16 => match row.try_get::<Option<u16>, usize>(ordinal) {
89 Ok(v) => v.map(Value::U16).unwrap_or(Value::Null),
90 Err(_) => Value::Null,
91 },
92 ColType::U32 => match row.try_get::<Option<u32>, usize>(ordinal) {
93 Ok(v) => v.map(Value::U32).unwrap_or(Value::Null),
94 Err(_) => Value::Null,
95 },
96 ColType::U64 => match row.try_get::<Option<i64>, usize>(ordinal) {
98 Ok(v) => v.map(Value::I64).unwrap_or(Value::Null),
99 Err(_) => Value::Null,
100 },
101 ColType::F32 => match row.try_get::<Option<f32>, usize>(ordinal) {
102 Ok(v) => v.map(Value::F32).unwrap_or(Value::Null),
103 Err(_) => Value::Null,
104 },
105 ColType::F64 => match row.try_get::<Option<f64>, usize>(ordinal) {
106 Ok(v) => v.map(Value::F64).unwrap_or(Value::Null),
107 Err(_) => Value::Null,
108 },
109 ColType::Decimal => match row.try_get::<Option<String>, usize>(ordinal) {
110 Ok(v) => v.map(Value::Decimal).unwrap_or(Value::Null),
111 Err(_) => Value::Null,
112 },
113 ColType::String => match row.try_get::<Option<String>, usize>(ordinal) {
114 Ok(v) => v.map(Value::String).unwrap_or(Value::Null),
115 Err(_) => Value::Null,
116 },
117 ColType::Bytes => match row.try_get::<Option<Vec<u8>>, usize>(ordinal) {
118 Ok(v) => v.map(Value::Bytes).unwrap_or(Value::Null),
119 Err(_) => Value::Null,
120 },
121 ColType::Date | ColType::DateTime | ColType::Time | ColType::Json | ColType::Uuid => {
123 match row.try_get::<Option<String>, usize>(ordinal) {
124 Ok(v) => v.map(Value::String).unwrap_or(Value::Null),
125 Err(_) => Value::Null,
126 }
127 }
128 ColType::Unknown => {
129 if let Ok(v) = row.try_get::<Option<i64>, usize>(ordinal) {
136 return v.map(Value::I64).unwrap_or(Value::Null);
137 }
138 if let Ok(v) = row.try_get::<Option<f64>, usize>(ordinal) {
139 return v.map(Value::F64).unwrap_or(Value::Null);
140 }
141 if let Ok(v) = row.try_get::<Option<bool>, usize>(ordinal) {
142 return v.map(Value::Bool).unwrap_or(Value::Null);
143 }
144 if let Ok(v) = row.try_get::<Option<String>, usize>(ordinal) {
145 return v.map(Value::String).unwrap_or(Value::Null);
146 }
147 Value::Null
148 }
149 _ => Value::Null,
151 }
152}
153
154pub struct SqlitePoolHandle {
155 pool: sqlx::SqlitePool,
156}
157
158impl SqlitePoolHandle {
159 pub async fn connect(url: &str) -> Result<Self, DbError> {
160 Self::connect_with_max(url, 10).await
162 }
163
164 pub async fn connect_with_max(url: &str, max_connections: u32) -> Result<Self, DbError> {
171 let opts = sqlx::sqlite::SqliteConnectOptions::from_str(url)
177 .map_err(map_sqlx_error)?
178 .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
179 .synchronous(sqlx::sqlite::SqliteSynchronous::Normal)
180 .busy_timeout(std::time::Duration::from_secs(5))
181 .pragma("mmap_size", "268435456");
182 let pool = sqlx::sqlite::SqlitePoolOptions::new()
183 .max_connections(max_connections)
184 .acquire_timeout(std::time::Duration::from_secs(30))
185 .idle_timeout(Some(std::time::Duration::from_secs(600)))
186 .max_lifetime(Some(std::time::Duration::from_secs(1800)))
187 .connect_with(opts)
188 .await
189 .map_err(map_sqlx_error)?;
190 Ok(Self { pool })
191 }
192
193 pub fn from_pool(pool: sqlx::SqlitePool) -> Self {
194 Self { pool }
195 }
196
197 pub fn pool(&self) -> &sqlx::SqlitePool {
198 &self.pool
199 }
200}
201
202pub struct SqlxSqliteConnectionFactory {
203 pool: Arc<SqlitePoolHandle>,
204}
205
206impl SqlxSqliteConnectionFactory {
207 pub fn new(pool: Arc<SqlitePoolHandle>) -> Self {
208 Self { pool }
209 }
210}
211
212#[async_trait]
213impl ConnectionFactory for SqlxSqliteConnectionFactory {
214 async fn create(&self) -> Result<Box<dyn Connection>, DbError> {
215 let conn = self.pool.pool.acquire().await.map_err(map_sqlx_error)?;
216 Ok(Box::new(crate::metrics_conn::MetricsConn::new(Box::new(
218 SqlxSqliteConnection {
219 conn: Some(conn),
220 connected: true,
221 in_transaction: false,
222 },
223 ))))
224 }
225}
226
227pub struct SqlxSqliteConnection {
228 conn: Option<sqlx::pool::PoolConnection<sqlx::Sqlite>>,
229 connected: bool,
230 in_transaction: bool,
231}
232
233impl Connection for SqlxSqliteConnection {
234 fn db_type(&self) -> Option<DbType> {
235 Some(DbType::Sqlite)
236 }
237
238 fn execute<'a>(
239 &'a mut self,
240 sql: &'a str,
241 ) -> Pin<Box<dyn Future<Output = Result<u64, DbError>> + Send + 'a>> {
242 Box::pin(async move {
243 let mut pool_conn = self
244 .conn
245 .take()
246 .ok_or_else(|| DbError::Internal("connection already closed".to_string()))?;
247 let result = if needs_raw_sql(sql) {
250 (&mut *pool_conn)
251 .execute(sqlx::raw_sql(sqlx::AssertSqlSafe(sql)))
252 .await
253 } else {
254 (&mut *pool_conn).execute(sqlx::AssertSqlSafe(sql)).await
255 };
256 self.conn = Some(pool_conn);
257
258 match result {
259 Ok(r) => Ok(r.rows_affected()),
260 Err(e) => {
261 let db_err = map_sqlx_error(e);
262 if matches!(db_err, DbError::ConnectionError(_) | DbError::IoError(_)) {
263 self.connected = false;
264 }
265 Err(db_err)
266 }
267 }
268 })
269 }
270
271 fn query<'a>(
272 &'a mut self,
273 sql: &'a str,
274 ) -> Pin<Box<dyn Future<Output = Result<Vec<HashMap<String, Value>>, DbError>> + Send + 'a>>
275 {
276 Box::pin(async move {
277 let mut pool_conn = self
278 .conn
279 .take()
280 .ok_or_else(|| DbError::Internal("connection already closed".to_string()))?;
281 let rows_result = (&mut *pool_conn).fetch_all(sqlx::AssertSqlSafe(sql)).await;
284 self.conn = Some(pool_conn);
285
286 let rows = rows_result.map_err(map_sqlx_error)?;
287 if rows.is_empty() {
288 return Ok(Vec::new());
289 }
290 let col_types: Vec<ColType> = rows[0]
293 .columns()
294 .iter()
295 .map(|col| ColType::parse_sqlite(col.type_info().name()))
296 .collect();
297 let col_names: Vec<String> = rows[0]
298 .columns()
299 .iter()
300 .map(|col| col.name().to_string())
301 .collect();
302 let mut result = Vec::with_capacity(rows.len());
303 for row in &rows {
304 let mut record = HashMap::with_capacity(col_types.len());
305 for (i, name) in col_names.iter().enumerate() {
306 let value = row_to_value_with_coltype_sqlite(row, i, col_types[i]);
307 record.insert(name.clone(), value);
308 }
309 result.push(record);
310 }
311 Ok(result)
312 })
313 }
314
315 fn begin_transaction<'a>(
316 &'a mut self,
317 ) -> Pin<Box<dyn Future<Output = Result<(), DbError>> + Send + 'a>> {
318 Box::pin(async move {
319 if self.in_transaction {
320 return Err(DbError::Internal("transaction already started".to_string()));
321 }
322 self.execute("BEGIN").await?;
323 self.in_transaction = true;
324 Ok(())
325 })
326 }
327
328 fn commit<'a>(&'a mut self) -> Pin<Box<dyn Future<Output = Result<(), DbError>> + Send + 'a>> {
329 Box::pin(async move {
330 if self.in_transaction {
331 self.execute("COMMIT").await?;
332 self.in_transaction = false;
333 }
334 Ok(())
335 })
336 }
337
338 fn rollback<'a>(
339 &'a mut self,
340 ) -> Pin<Box<dyn Future<Output = Result<(), DbError>> + Send + 'a>> {
341 Box::pin(async move {
342 if self.in_transaction {
343 let result = self.execute("ROLLBACK").await;
348 if result.is_ok() {
349 self.in_transaction = false;
350 }
351 result.map(|_| ())
352 } else {
353 Ok(())
354 }
355 })
356 }
357
358 fn is_connected(&self) -> bool {
359 self.connected && self.conn.is_some()
363 }
364
365 fn in_transaction(&self) -> bool {
366 self.in_transaction
367 }
368
369 fn ping<'a>(&'a mut self) -> Pin<Box<dyn Future<Output = bool> + Send + 'a>> {
370 Box::pin(async move {
371 match self.execute("SELECT 1").await {
372 Ok(_) => true,
373 Err(_) => {
374 self.connected = false;
375 false
376 }
377 }
378 })
379 }
380
381 fn close<'a>(&'a mut self) -> Pin<Box<dyn Future<Output = Result<(), DbError>> + Send + 'a>> {
382 Box::pin(async move {
383 if let Some(conn) = self.conn.take() {
384 drop(conn);
385 }
386 self.connected = false;
387 self.in_transaction = false;
388 Ok(())
389 })
390 }
391
392 fn execute_with_params<'a>(
398 &'a mut self,
399 sql: &'a str,
400 params: &'a [Value],
401 ) -> Pin<Box<dyn Future<Output = Result<u64, DbError>> + Send + 'a>> {
402 Box::pin(async move {
403 if needs_raw_sql(sql) || params.is_empty() {
404 return self.execute(sql).await;
405 }
406 let mut pool_conn = self
407 .conn
408 .take()
409 .ok_or_else(|| DbError::Internal("connection already closed".to_string()))?;
410 let mut q = sqlx::query(sqlx::AssertSqlSafe(sql));
411 for v in params {
412 q = match v {
413 Value::Null => q.bind(None::<i64>),
414 Value::Bool(b) => q.bind(*b),
415 Value::I8(n) => q.bind(*n),
416 Value::I16(n) => q.bind(*n),
417 Value::I32(n) => q.bind(*n),
418 Value::I64(n) => q.bind(*n),
419 Value::U8(n) => q.bind(*n),
420 Value::U16(n) => q.bind(*n),
421 Value::U32(n) => q.bind(*n),
422 Value::U64(n) => q.bind(*n as i64),
423 Value::F32(f) => q.bind(*f),
424 Value::F64(f) => q.bind(*f),
425 Value::String(s) => q.bind(s.as_str()),
426 Value::Decimal(s) => q.bind(s.as_str()),
427 Value::Bytes(b) => q.bind(b.as_slice()),
428 Value::Uuid(s)
431 | Value::Date(s)
432 | Value::DateTime(s)
433 | Value::Time(s)
434 | Value::Json(s) => q.bind(s.as_str()),
435 other => q.bind(other.to_string()),
436 };
437 }
438 let result = q.execute(&mut *pool_conn).await;
439 self.conn = Some(pool_conn);
440 match result {
441 Ok(r) => Ok(r.rows_affected()),
442 Err(e) => {
443 let db_err = map_sqlx_error(e);
444 if matches!(db_err, DbError::ConnectionError(_) | DbError::IoError(_)) {
445 self.connected = false;
446 }
447 Err(db_err)
448 }
449 }
450 })
451 }
452
453 fn query_with_params<'a>(
458 &'a mut self,
459 sql: &'a str,
460 params: &'a [Value],
461 ) -> Pin<Box<dyn Future<Output = Result<QueryRows, DbError>> + Send + 'a>> {
462 Box::pin(async move {
463 if params.is_empty() {
464 return self.query(sql).await;
465 }
466 let mut pool_conn = self
467 .conn
468 .take()
469 .ok_or_else(|| DbError::Internal("connection already closed".to_string()))?;
470 let mut q = sqlx::query(sqlx::AssertSqlSafe(sql));
471 for v in params {
472 q = match v {
473 Value::Null => q.bind(None::<i64>),
474 Value::Bool(b) => q.bind(*b),
475 Value::I8(n) => q.bind(*n),
476 Value::I16(n) => q.bind(*n),
477 Value::I32(n) => q.bind(*n),
478 Value::I64(n) => q.bind(*n),
479 Value::U8(n) => q.bind(*n),
480 Value::U16(n) => q.bind(*n),
481 Value::U32(n) => q.bind(*n),
482 Value::U64(n) => q.bind(*n as i64),
483 Value::F32(f) => q.bind(*f),
484 Value::F64(f) => q.bind(*f),
485 Value::String(s) => q.bind(s.as_str()),
486 Value::Decimal(s) => q.bind(s.as_str()),
487 Value::Bytes(b) => q.bind(b.as_slice()),
488 Value::Uuid(s)
491 | Value::Date(s)
492 | Value::DateTime(s)
493 | Value::Time(s)
494 | Value::Json(s) => q.bind(s.as_str()),
495 other => q.bind(other.to_string()),
496 };
497 }
498 let rows_result = q.fetch_all(&mut *pool_conn).await;
499 self.conn = Some(pool_conn);
500 let rows = rows_result.map_err(map_sqlx_error)?;
501 if rows.is_empty() {
502 return Ok(Vec::new());
503 }
504 let col_types: Vec<ColType> = rows[0]
507 .columns()
508 .iter()
509 .map(|col| ColType::parse_sqlite(col.type_info().name()))
510 .collect();
511 let col_names: Vec<String> = rows[0]
512 .columns()
513 .iter()
514 .map(|col| col.name().to_string())
515 .collect();
516 let mut result = Vec::with_capacity(rows.len());
517 for row in &rows {
518 let mut record = HashMap::with_capacity(col_types.len());
519 for (i, name) in col_names.iter().enumerate() {
520 let value = row_to_value_with_coltype_sqlite(row, i, col_types[i]);
521 record.insert(name.clone(), value);
522 }
523 result.push(record);
524 }
525 Ok(result)
526 })
527 }
528
529 fn query_values<'a>(
535 &'a mut self,
536 sql: &'a str,
537 ) -> Pin<Box<dyn Future<Output = Result<QueryValues, DbError>> + Send + 'a>> {
538 Box::pin(async move {
539 let mut pool_conn = self
540 .conn
541 .take()
542 .ok_or_else(|| DbError::Internal("connection already closed".to_string()))?;
543 let rows_result = (&mut *pool_conn).fetch_all(sqlx::AssertSqlSafe(sql)).await;
544 self.conn = Some(pool_conn);
545 let rows = rows_result.map_err(map_sqlx_error)?;
546 if rows.is_empty() {
547 return Ok((Vec::new(), Vec::new()));
548 }
549 let cols = rows[0].columns();
550 let mut col_names: Vec<String> = Vec::with_capacity(cols.len());
551 let mut col_types: Vec<ColType> = Vec::with_capacity(cols.len());
552 for col in cols {
553 col_names.push(col.name().to_string());
554 col_types.push(ColType::parse_sqlite(col.type_info().name()));
555 }
556 let mut result_rows: Vec<Vec<Value>> = Vec::with_capacity(rows.len());
557 for row in &rows {
558 let mut row_values: Vec<Value> = Vec::with_capacity(col_names.len());
559 for (idx, _) in col_names.iter().enumerate() {
560 row_values.push(row_to_value_with_coltype_sqlite(row, idx, col_types[idx]));
561 }
562 result_rows.push(row_values);
563 }
564 Ok((col_names, result_rows))
565 })
566 }
567
568 fn query_values_with_params<'a>(
572 &'a mut self,
573 sql: &'a str,
574 params: &'a [Value],
575 ) -> Pin<Box<dyn Future<Output = Result<QueryValues, DbError>> + Send + 'a>> {
576 Box::pin(async move {
577 if params.is_empty() {
578 return self.query_values(sql).await;
579 }
580 let mut pool_conn = self
581 .conn
582 .take()
583 .ok_or_else(|| DbError::Internal("connection already closed".to_string()))?;
584 let mut q = sqlx::query(sqlx::AssertSqlSafe(sql));
585 for v in params {
586 q = match v {
587 Value::Null => q.bind(None::<i64>),
588 Value::Bool(b) => q.bind(*b),
589 Value::I8(n) => q.bind(*n),
590 Value::I16(n) => q.bind(*n),
591 Value::I32(n) => q.bind(*n),
592 Value::I64(n) => q.bind(*n),
593 Value::U8(n) => q.bind(*n),
594 Value::U16(n) => q.bind(*n),
595 Value::U32(n) => q.bind(*n),
596 Value::U64(n) => q.bind(*n as i64),
597 Value::F32(f) => q.bind(*f),
598 Value::F64(f) => q.bind(*f),
599 Value::String(s) => q.bind(s.as_str()),
600 Value::Decimal(s) => q.bind(s.as_str()),
601 Value::Bytes(b) => q.bind(b.as_slice()),
602 Value::Uuid(s)
605 | Value::Date(s)
606 | Value::DateTime(s)
607 | Value::Time(s)
608 | Value::Json(s) => q.bind(s.as_str()),
609 other => q.bind(other.to_string()),
610 };
611 }
612 let rows_result = q.fetch_all(&mut *pool_conn).await;
613 self.conn = Some(pool_conn);
614 let rows = rows_result.map_err(map_sqlx_error)?;
615 if rows.is_empty() {
616 return Ok((Vec::new(), Vec::new()));
617 }
618 let cols = rows[0].columns();
619 let mut col_names: Vec<String> = Vec::with_capacity(cols.len());
620 let mut col_types: Vec<ColType> = Vec::with_capacity(cols.len());
621 for col in cols {
622 col_names.push(col.name().to_string());
623 col_types.push(ColType::parse_sqlite(col.type_info().name()));
624 }
625 let mut result_rows: Vec<Vec<Value>> = Vec::with_capacity(rows.len());
626 for row in &rows {
627 let mut row_values: Vec<Value> = Vec::with_capacity(col_names.len());
628 for (idx, _) in col_names.iter().enumerate() {
629 row_values.push(row_to_value_with_coltype_sqlite(row, idx, col_types[idx]));
630 }
631 result_rows.push(row_values);
632 }
633 Ok((col_names, result_rows))
634 })
635 }
636
637 fn query_stream<'a>(
643 &'a mut self,
644 sql: &'a str,
645 ) -> Pin<Box<dyn futures::Stream<Item = Result<HashMap<String, Value>, DbError>> + Send + 'a>>
646 {
647 Box::pin(async_stream::try_stream! {
648 let mut pool_conn = self
649 .conn
650 .take()
651 .ok_or_else(|| DbError::Internal("connection already closed".to_string()))?;
652 let mut row_stream = sqlx::query(sqlx::AssertSqlSafe(sql)).fetch(&mut *pool_conn);
653 let mut col_types: Vec<ColType> = Vec::new();
654 let mut col_names: Vec<String> = Vec::new();
655 let mut first_row = true;
656 while let Some(row_result) = row_stream.next().await {
657 let row = row_result.map_err(map_sqlx_error)?;
658 if first_row {
659 for col in row.columns() {
660 col_names.push(col.name().to_string());
661 col_types.push(ColType::parse_sqlite(col.type_info().name()));
662 }
663 first_row = false;
664 }
665 let mut record = HashMap::with_capacity(col_names.len());
666 for (i, name) in col_names.iter().enumerate() {
667 let value = row_to_value_with_coltype_sqlite(&row, i, col_types[i]);
668 record.insert(name.clone(), value);
669 }
670 yield record;
671 }
672 drop(row_stream);
675 self.conn = Some(pool_conn);
676 })
677 }
678}
679
680impl Drop for SqlxSqliteConnection {
681 fn drop(&mut self) {
682 if let Some(conn) = self.conn.take() {
683 drop(conn);
684 }
685 }
686}
687
688pub async fn sqlite_backup(
693 conn: &mut SqlxSqliteConnection,
694 dest_path: &str,
695) -> Result<(), DbError> {
696 let escaped_path = dest_path.replace('\'', "''");
697 let sql = format!("VACUUM INTO '{}'", escaped_path);
698 conn.execute(&sql).await?;
699 Ok(())
700}
701
702fn row_to_value_mysql(row: &sqlx::mysql::MySqlRow, ordinal: usize) -> Value {
708 use sqlx::TypeInfo;
709 let type_name = row.columns()[ordinal].type_info().name();
710 match type_name {
711 "BOOLEAN" => match row.try_get::<Option<bool>, usize>(ordinal) {
712 Ok(v) => v.map(Value::Bool).unwrap_or(Value::Null),
713 Err(_) => Value::Null,
714 },
715 "TINYINT" | "TINYINT UNSIGNED" => match row.try_get::<Option<i8>, usize>(ordinal) {
716 Ok(v) => v.map(Value::I8).unwrap_or(Value::Null),
717 Err(_) => match row.try_get::<Option<u8>, usize>(ordinal) {
718 Ok(v) => v.map(Value::U8).unwrap_or(Value::Null),
719 Err(_) => Value::Null,
720 },
721 },
722 "SMALLINT" | "SMALLINT UNSIGNED" => match row.try_get::<Option<i16>, usize>(ordinal) {
723 Ok(v) => v.map(Value::I16).unwrap_or(Value::Null),
724 Err(_) => match row.try_get::<Option<u16>, usize>(ordinal) {
725 Ok(v) => v.map(Value::U16).unwrap_or(Value::Null),
726 Err(_) => Value::Null,
727 },
728 },
729 "INT" | "INT UNSIGNED" | "MEDIUMINT" | "MEDIUMINT UNSIGNED" => {
730 match row.try_get::<Option<i32>, usize>(ordinal) {
731 Ok(v) => v.map(Value::I32).unwrap_or(Value::Null),
732 Err(_) => match row.try_get::<Option<u32>, usize>(ordinal) {
733 Ok(v) => v.map(Value::U32).unwrap_or(Value::Null),
734 Err(_) => Value::Null,
735 },
736 }
737 }
738 "BIGINT" | "BIGINT UNSIGNED" => match row.try_get::<Option<i64>, usize>(ordinal) {
739 Ok(v) => v.map(Value::I64).unwrap_or(Value::Null),
740 Err(_) => match row.try_get::<Option<u64>, usize>(ordinal) {
741 Ok(v) => v.map(Value::U64).unwrap_or(Value::Null),
742 Err(_) => Value::Null,
743 },
744 },
745 "FLOAT" => match row.try_get::<Option<f32>, usize>(ordinal) {
746 Ok(v) => v.map(Value::F32).unwrap_or(Value::Null),
747 Err(_) => Value::Null,
748 },
749 "DOUBLE" => match row.try_get::<Option<f64>, usize>(ordinal) {
750 Ok(v) => v.map(Value::F64).unwrap_or(Value::Null),
751 Err(_) => Value::Null,
752 },
753 "VARCHAR" | "TEXT" | "CHAR" | "TINYTEXT" | "MEDIUMTEXT" | "LONGTEXT" | "ENUM" => {
754 match row.try_get::<Option<String>, usize>(ordinal) {
755 Ok(v) => v.map(Value::String).unwrap_or(Value::Null),
756 Err(_) => Value::Null,
757 }
758 }
759 "BLOB" | "TINYBLOB" | "MEDIUMBLOB" | "LONGBLOB" | "BINARY" | "VARBINARY" => {
760 match row.try_get::<Option<Vec<u8>>, usize>(ordinal) {
761 Ok(v) => v.map(Value::Bytes).unwrap_or(Value::Null),
762 Err(_) => Value::Null,
763 }
764 }
765 "DECIMAL" | "NUMERIC" | "NEWDECIMAL" => {
767 match row.try_get::<Option<bigdecimal::BigDecimal>, usize>(ordinal) {
768 Ok(Some(v)) => Value::Decimal(v.to_string()),
769 Ok(None) => Value::Null,
770 Err(_) => match row.try_get::<Option<String>, usize>(ordinal) {
771 Ok(v) => v.map(Value::Decimal).unwrap_or(Value::Null),
772 Err(_) => Value::Null,
773 },
774 }
775 }
776 "DATE" => match row.try_get::<Option<sqlx::types::chrono::NaiveDate>, usize>(ordinal) {
778 Ok(v) => v.map(|d| Value::Date(d.to_string())).unwrap_or(Value::Null),
779 Err(_) => match row.try_get::<Option<String>, usize>(ordinal) {
780 Ok(v) => v.map(Value::String).unwrap_or(Value::Null),
781 Err(_) => Value::Null,
782 },
783 },
784 "DATETIME" | "TIMESTAMP" => {
785 match row.try_get::<Option<sqlx::types::chrono::NaiveDateTime>, usize>(ordinal) {
786 Ok(v) => v
787 .map(|d| Value::DateTime(d.to_string()))
788 .unwrap_or(Value::Null),
789 Err(_) => match row.try_get::<Option<String>, usize>(ordinal) {
790 Ok(v) => v.map(Value::String).unwrap_or(Value::Null),
791 Err(_) => Value::Null,
792 },
793 }
794 }
795 "TIME" => match row.try_get::<Option<sqlx::types::chrono::NaiveTime>, usize>(ordinal) {
796 Ok(v) => v.map(|t| Value::Time(t.to_string())).unwrap_or(Value::Null),
797 Err(_) => match row.try_get::<Option<String>, usize>(ordinal) {
798 Ok(v) => v.map(Value::String).unwrap_or(Value::Null),
799 Err(_) => Value::Null,
800 },
801 },
802 _ => {
803 if let Ok(v) = row.try_get::<Option<i64>, usize>(ordinal) {
805 return v.map(Value::I64).unwrap_or(Value::Null);
806 }
807 if let Ok(v) = row.try_get::<Option<f64>, usize>(ordinal) {
808 return v.map(Value::F64).unwrap_or(Value::Null);
809 }
810 if let Ok(v) = row.try_get::<Option<bool>, usize>(ordinal) {
811 return v.map(Value::Bool).unwrap_or(Value::Null);
812 }
813 if let Ok(v) = row.try_get::<Option<String>, usize>(ordinal) {
814 return v.map(Value::String).unwrap_or(Value::Null);
815 }
816 Value::Null
817 }
818 }
819}
820
821fn row_to_value_with_coltype_mysql(
829 row: &sqlx::mysql::MySqlRow,
830 ordinal: usize,
831 col_type: ColType,
832) -> Value {
833 match col_type {
834 ColType::Bool => match row.try_get::<Option<bool>, usize>(ordinal) {
835 Ok(v) => v.map(Value::Bool).unwrap_or(Value::Null),
836 Err(_) => Value::Null,
837 },
838 ColType::I8 => match row.try_get::<Option<i8>, usize>(ordinal) {
839 Ok(v) => v.map(Value::I8).unwrap_or(Value::Null),
840 Err(_) => match row.try_get::<Option<u8>, usize>(ordinal) {
841 Ok(v) => v.map(Value::U8).unwrap_or(Value::Null),
842 Err(_) => Value::Null,
843 },
844 },
845 ColType::I16 => match row.try_get::<Option<i16>, usize>(ordinal) {
846 Ok(v) => v.map(Value::I16).unwrap_or(Value::Null),
847 Err(_) => match row.try_get::<Option<u16>, usize>(ordinal) {
848 Ok(v) => v.map(Value::U16).unwrap_or(Value::Null),
849 Err(_) => Value::Null,
850 },
851 },
852 ColType::I32 => match row.try_get::<Option<i32>, usize>(ordinal) {
853 Ok(v) => v.map(Value::I32).unwrap_or(Value::Null),
854 Err(_) => match row.try_get::<Option<u32>, usize>(ordinal) {
855 Ok(v) => v.map(Value::U32).unwrap_or(Value::Null),
856 Err(_) => Value::Null,
857 },
858 },
859 ColType::I64 => match row.try_get::<Option<i64>, usize>(ordinal) {
860 Ok(v) => v.map(Value::I64).unwrap_or(Value::Null),
861 Err(_) => match row.try_get::<Option<u64>, usize>(ordinal) {
862 Ok(v) => v.map(Value::U64).unwrap_or(Value::Null),
863 Err(_) => Value::Null,
864 },
865 },
866 ColType::U8 => match row.try_get::<Option<u8>, usize>(ordinal) {
867 Ok(v) => v.map(Value::U8).unwrap_or(Value::Null),
868 Err(_) => Value::Null,
869 },
870 ColType::U16 => match row.try_get::<Option<u16>, usize>(ordinal) {
871 Ok(v) => v.map(Value::U16).unwrap_or(Value::Null),
872 Err(_) => Value::Null,
873 },
874 ColType::U32 => match row.try_get::<Option<u32>, usize>(ordinal) {
875 Ok(v) => v.map(Value::U32).unwrap_or(Value::Null),
876 Err(_) => Value::Null,
877 },
878 ColType::U64 => match row.try_get::<Option<u64>, usize>(ordinal) {
879 Ok(v) => v.map(Value::U64).unwrap_or(Value::Null),
880 Err(_) => Value::Null,
881 },
882 ColType::F32 => match row.try_get::<Option<f32>, usize>(ordinal) {
883 Ok(v) => v.map(Value::F32).unwrap_or(Value::Null),
884 Err(_) => Value::Null,
885 },
886 ColType::F64 => match row.try_get::<Option<f64>, usize>(ordinal) {
887 Ok(v) => v.map(Value::F64).unwrap_or(Value::Null),
888 Err(_) => Value::Null,
889 },
890 ColType::Decimal => match row.try_get::<Option<bigdecimal::BigDecimal>, usize>(ordinal) {
892 Ok(Some(v)) => Value::Decimal(v.to_string()),
893 Ok(None) => Value::Null,
894 Err(_) => match row.try_get::<Option<String>, usize>(ordinal) {
895 Ok(v) => v.map(Value::Decimal).unwrap_or(Value::Null),
896 Err(_) => Value::Null,
897 },
898 },
899 ColType::String => match row.try_get::<Option<String>, usize>(ordinal) {
900 Ok(v) => v.map(Value::String).unwrap_or(Value::Null),
901 Err(_) => Value::Null,
902 },
903 ColType::Bytes => match row.try_get::<Option<Vec<u8>>, usize>(ordinal) {
904 Ok(v) => v.map(Value::Bytes).unwrap_or(Value::Null),
905 Err(_) => Value::Null,
906 },
907 ColType::Date => {
909 match row.try_get::<Option<sqlx::types::chrono::NaiveDate>, usize>(ordinal) {
910 Ok(v) => v.map(|d| Value::Date(d.to_string())).unwrap_or(Value::Null),
911 Err(_) => match row.try_get::<Option<String>, usize>(ordinal) {
912 Ok(v) => v.map(Value::String).unwrap_or(Value::Null),
913 Err(_) => Value::Null,
914 },
915 }
916 }
917 ColType::DateTime => {
918 match row.try_get::<Option<sqlx::types::chrono::NaiveDateTime>, usize>(ordinal) {
919 Ok(v) => v
920 .map(|d| Value::DateTime(d.to_string()))
921 .unwrap_or(Value::Null),
922 Err(_) => match row.try_get::<Option<String>, usize>(ordinal) {
923 Ok(v) => v.map(Value::String).unwrap_or(Value::Null),
924 Err(_) => Value::Null,
925 },
926 }
927 }
928 ColType::Time => {
929 match row.try_get::<Option<sqlx::types::chrono::NaiveTime>, usize>(ordinal) {
930 Ok(v) => v.map(|t| Value::Time(t.to_string())).unwrap_or(Value::Null),
931 Err(_) => match row.try_get::<Option<String>, usize>(ordinal) {
932 Ok(v) => v.map(Value::String).unwrap_or(Value::Null),
933 Err(_) => Value::Null,
934 },
935 }
936 }
937 ColType::Json | ColType::Uuid => match row.try_get::<Option<String>, usize>(ordinal) {
939 Ok(v) => v.map(Value::String).unwrap_or(Value::Null),
940 Err(_) => Value::Null,
941 },
942 ColType::Unknown => {
943 if let Ok(v) = row.try_get::<Option<i64>, usize>(ordinal) {
945 return v.map(Value::I64).unwrap_or(Value::Null);
946 }
947 if let Ok(v) = row.try_get::<Option<f64>, usize>(ordinal) {
948 return v.map(Value::F64).unwrap_or(Value::Null);
949 }
950 if let Ok(v) = row.try_get::<Option<bool>, usize>(ordinal) {
951 return v.map(Value::Bool).unwrap_or(Value::Null);
952 }
953 if let Ok(v) = row.try_get::<Option<String>, usize>(ordinal) {
954 return v.map(Value::String).unwrap_or(Value::Null);
955 }
956 Value::Null
957 }
958 _ => Value::Null,
960 }
961}
962
963pub struct MySqlPoolHandle {
964 pool: sqlx::MySqlPool,
965}
966
967impl MySqlPoolHandle {
968 pub async fn connect(url: &str) -> Result<Self, DbError> {
969 Self::connect_with_max(url, 10).await
971 }
972
973 pub async fn connect_with_max(url: &str, max_connections: u32) -> Result<Self, DbError> {
976 let pool = sqlx::pool::PoolOptions::<sqlx::MySql>::new()
977 .max_connections(max_connections)
978 .acquire_timeout(std::time::Duration::from_secs(30))
979 .idle_timeout(Some(std::time::Duration::from_secs(600)))
980 .max_lifetime(Some(std::time::Duration::from_secs(1800)))
981 .connect(url)
982 .await
983 .map_err(map_sqlx_error)?;
984 Ok(Self { pool })
985 }
986
987 pub fn from_pool(pool: sqlx::MySqlPool) -> Self {
988 Self { pool }
989 }
990
991 pub fn pool(&self) -> &sqlx::MySqlPool {
992 &self.pool
993 }
994}
995
996pub struct SqlxMySqlConnectionFactory {
997 pool: Arc<MySqlPoolHandle>,
998}
999
1000impl SqlxMySqlConnectionFactory {
1001 pub fn new(pool: Arc<MySqlPoolHandle>) -> Self {
1002 Self { pool }
1003 }
1004}
1005
1006#[async_trait]
1007impl ConnectionFactory for SqlxMySqlConnectionFactory {
1008 async fn create(&self) -> Result<Box<dyn Connection>, DbError> {
1009 let conn = self.pool.pool.acquire().await.map_err(map_sqlx_error)?;
1010 Ok(Box::new(crate::metrics_conn::MetricsConn::new(Box::new(
1012 SqlxMySqlConnection {
1013 conn: Some(conn),
1014 connected: true,
1015 in_transaction: false,
1016 session_dirty: false,
1018 },
1019 ))))
1020 }
1021}
1022
1023pub struct SqlxMySqlConnection {
1024 conn: Option<sqlx::pool::PoolConnection<sqlx::MySql>>,
1025 connected: bool,
1026 in_transaction: bool,
1027 session_dirty: bool,
1030}
1031
1032pub(crate) fn marks_session_dirty(sql: &str) -> bool {
1040 let trimmed = sql.trim_start();
1042 let mut upper = String::with_capacity(32);
1043 for ch in trimmed.chars() {
1044 if upper.len() >= 32 {
1045 break;
1046 }
1047 upper.push(ch.to_ascii_uppercase());
1048 }
1049 if upper.starts_with("USE ") || upper.starts_with("USE\t") || upper == "USE" {
1050 return true;
1051 }
1052 if !upper.starts_with("SET") {
1053 return false;
1054 }
1055 let after_set = &upper[3..];
1057 if !(after_set.is_empty() || after_set.starts_with(' ') || after_set.starts_with('\t')) {
1058 return false;
1059 }
1060 let rest = after_set.trim_start();
1061 !(rest.starts_with("TRANSACTION")
1063 && rest["TRANSACTION".len()..]
1064 .chars()
1065 .next()
1066 .map(|c| c.is_whitespace() || c == ';')
1067 .unwrap_or(true))
1068}
1069
1070impl Connection for SqlxMySqlConnection {
1071 fn db_type(&self) -> Option<DbType> {
1072 Some(DbType::MySQL)
1073 }
1074
1075 fn session_reset_sql(&self) -> Option<&'static str> {
1081 Some(
1082 "SET SESSION sql_mode = DEFAULT, \
1083 SESSION character_set_client = @@GLOBAL.character_set_client, \
1084 SESSION character_set_results = @@GLOBAL.character_set_results, \
1085 SESSION character_set_connection = @@GLOBAL.character_set_connection, \
1086 SESSION time_zone = @@GLOBAL.time_zone, \
1087 SESSION autocommit = 1",
1088 )
1089 }
1090
1091 fn take_session_dirty(&mut self) -> bool {
1094 std::mem::take(&mut self.session_dirty)
1095 }
1096
1097 fn mark_session_clean(&mut self) {
1099 self.session_dirty = false;
1100 }
1101
1102 fn execute<'a>(
1103 &'a mut self,
1104 sql: &'a str,
1105 ) -> Pin<Box<dyn Future<Output = Result<u64, DbError>> + Send + 'a>> {
1106 Box::pin(async move {
1107 let mut pool_conn = self
1108 .conn
1109 .take()
1110 .ok_or_else(|| DbError::Internal("connection already closed".to_string()))?;
1111 let result = if needs_raw_sql(sql) {
1114 (&mut *pool_conn)
1115 .execute(sqlx::raw_sql(sqlx::AssertSqlSafe(sql)))
1116 .await
1117 } else {
1118 (&mut *pool_conn).execute(sqlx::AssertSqlSafe(sql)).await
1119 };
1120 self.conn = Some(pool_conn);
1121
1122 match result {
1123 Ok(r) => {
1124 if marks_session_dirty(sql) {
1126 self.session_dirty = true;
1127 }
1128 Ok(r.rows_affected())
1129 }
1130 Err(e) => {
1131 let db_err = map_sqlx_error(e);
1132 if matches!(db_err, DbError::ConnectionError(_) | DbError::IoError(_)) {
1133 self.connected = false;
1134 }
1135 Err(db_err)
1136 }
1137 }
1138 })
1139 }
1140
1141 fn query<'a>(
1142 &'a mut self,
1143 sql: &'a str,
1144 ) -> Pin<Box<dyn Future<Output = Result<Vec<HashMap<String, Value>>, DbError>> + Send + 'a>>
1145 {
1146 Box::pin(async move {
1147 let mut pool_conn = self
1148 .conn
1149 .take()
1150 .ok_or_else(|| DbError::Internal("connection already closed".to_string()))?;
1151 let rows_result = (&mut *pool_conn).fetch_all(sqlx::AssertSqlSafe(sql)).await;
1154 self.conn = Some(pool_conn);
1155
1156 let rows = rows_result.map_err(map_sqlx_error)?;
1157 if rows.is_empty() {
1158 return Ok(Vec::new());
1159 }
1160 let col_types: Vec<ColType> = rows[0]
1163 .columns()
1164 .iter()
1165 .map(|col| ColType::parse_mysql(col.type_info().name()))
1166 .collect();
1167 let col_names: Vec<String> = rows[0]
1168 .columns()
1169 .iter()
1170 .map(|col| col.name().to_string())
1171 .collect();
1172 let mut result = Vec::with_capacity(rows.len());
1173 for row in &rows {
1174 let mut record = HashMap::with_capacity(col_types.len());
1175 for (i, name) in col_names.iter().enumerate() {
1176 let value = row_to_value_with_coltype_mysql(row, i, col_types[i]);
1177 record.insert(name.clone(), value);
1178 }
1179 result.push(record);
1180 }
1181 Ok(result)
1182 })
1183 }
1184
1185 fn begin_transaction<'a>(
1186 &'a mut self,
1187 ) -> Pin<Box<dyn Future<Output = Result<(), DbError>> + Send + 'a>> {
1188 Box::pin(async move {
1189 if self.in_transaction {
1190 return Err(DbError::Internal("transaction already started".to_string()));
1191 }
1192 self.execute("BEGIN").await?;
1193 self.in_transaction = true;
1194 Ok(())
1195 })
1196 }
1197
1198 fn commit<'a>(&'a mut self) -> Pin<Box<dyn Future<Output = Result<(), DbError>> + Send + 'a>> {
1199 Box::pin(async move {
1200 if self.in_transaction {
1201 self.execute("COMMIT").await?;
1202 self.in_transaction = false;
1203 }
1204 Ok(())
1205 })
1206 }
1207
1208 fn rollback<'a>(
1209 &'a mut self,
1210 ) -> Pin<Box<dyn Future<Output = Result<(), DbError>> + Send + 'a>> {
1211 Box::pin(async move {
1212 if self.in_transaction {
1213 let result = self.execute("ROLLBACK").await;
1218 if result.is_ok() {
1219 self.in_transaction = false;
1220 }
1221 result.map(|_| ())
1222 } else {
1223 Ok(())
1224 }
1225 })
1226 }
1227
1228 fn is_connected(&self) -> bool {
1229 self.connected && self.conn.is_some()
1233 }
1234
1235 fn in_transaction(&self) -> bool {
1236 self.in_transaction
1237 }
1238
1239 fn ping<'a>(&'a mut self) -> Pin<Box<dyn Future<Output = bool> + Send + 'a>> {
1240 Box::pin(async move {
1241 match self.execute("SELECT 1").await {
1242 Ok(_) => true,
1243 Err(_) => {
1244 self.connected = false;
1245 false
1246 }
1247 }
1248 })
1249 }
1250
1251 fn close<'a>(&'a mut self) -> Pin<Box<dyn Future<Output = Result<(), DbError>> + Send + 'a>> {
1252 Box::pin(async move {
1253 if let Some(conn) = self.conn.take() {
1254 drop(conn);
1255 }
1256 self.connected = false;
1257 self.in_transaction = false;
1258 Ok(())
1259 })
1260 }
1261
1262 fn execute_with_params<'a>(
1268 &'a mut self,
1269 sql: &'a str,
1270 params: &'a [Value],
1271 ) -> Pin<Box<dyn Future<Output = Result<u64, DbError>> + Send + 'a>> {
1272 Box::pin(async move {
1273 if needs_raw_sql(sql) || params.is_empty() {
1274 return self.execute(sql).await;
1275 }
1276 let mut pool_conn = self
1277 .conn
1278 .take()
1279 .ok_or_else(|| DbError::Internal("connection already closed".to_string()))?;
1280 let mut q = sqlx::query(sqlx::AssertSqlSafe(sql));
1281 for v in params {
1282 q = match v {
1283 Value::Null => q.bind(None::<i64>),
1284 Value::Bool(b) => q.bind(*b),
1285 Value::I8(n) => q.bind(*n),
1286 Value::I16(n) => q.bind(*n),
1287 Value::I32(n) => q.bind(*n),
1288 Value::I64(n) => q.bind(*n),
1289 Value::U8(n) => q.bind(*n),
1290 Value::U16(n) => q.bind(*n),
1291 Value::U32(n) => q.bind(*n),
1292 Value::U64(n) => q.bind(*n as i64),
1293 Value::F32(f) => q.bind(*f),
1294 Value::F64(f) => q.bind(*f),
1295 Value::String(s) => q.bind(s.as_str()),
1296 Value::Decimal(s) => q.bind(s.as_str()),
1297 Value::Bytes(b) => q.bind(b.as_slice()),
1298 Value::Uuid(s)
1301 | Value::Date(s)
1302 | Value::DateTime(s)
1303 | Value::Time(s)
1304 | Value::Json(s) => q.bind(s.as_str()),
1305 other => q.bind(other.to_string()),
1306 };
1307 }
1308 let result = q.execute(&mut *pool_conn).await;
1309 self.conn = Some(pool_conn);
1310 match result {
1311 Ok(r) => {
1312 if marks_session_dirty(sql) {
1314 self.session_dirty = true;
1315 }
1316 Ok(r.rows_affected())
1317 }
1318 Err(e) => {
1319 let db_err = map_sqlx_error(e);
1320 if matches!(db_err, DbError::ConnectionError(_) | DbError::IoError(_)) {
1321 self.connected = false;
1322 }
1323 Err(db_err)
1324 }
1325 }
1326 })
1327 }
1328
1329 fn query_with_params<'a>(
1331 &'a mut self,
1332 sql: &'a str,
1333 params: &'a [Value],
1334 ) -> Pin<Box<dyn Future<Output = Result<QueryRows, DbError>> + Send + 'a>> {
1335 Box::pin(async move {
1336 if params.is_empty() {
1337 return self.query(sql).await;
1338 }
1339 let mut pool_conn = self
1340 .conn
1341 .take()
1342 .ok_or_else(|| DbError::Internal("connection already closed".to_string()))?;
1343 let mut q = sqlx::query(sqlx::AssertSqlSafe(sql));
1344 for v in params {
1345 q = match v {
1346 Value::Null => q.bind(None::<i64>),
1347 Value::Bool(b) => q.bind(*b),
1348 Value::I8(n) => q.bind(*n),
1349 Value::I16(n) => q.bind(*n),
1350 Value::I32(n) => q.bind(*n),
1351 Value::I64(n) => q.bind(*n),
1352 Value::U8(n) => q.bind(*n),
1353 Value::U16(n) => q.bind(*n),
1354 Value::U32(n) => q.bind(*n),
1355 Value::U64(n) => q.bind(*n as i64),
1356 Value::F32(f) => q.bind(*f),
1357 Value::F64(f) => q.bind(*f),
1358 Value::String(s) => q.bind(s.as_str()),
1359 Value::Decimal(s) => q.bind(s.as_str()),
1360 Value::Bytes(b) => q.bind(b.as_slice()),
1361 Value::Uuid(s)
1364 | Value::Date(s)
1365 | Value::DateTime(s)
1366 | Value::Time(s)
1367 | Value::Json(s) => q.bind(s.as_str()),
1368 other => q.bind(other.to_string()),
1369 };
1370 }
1371 let rows_result = q.fetch_all(&mut *pool_conn).await;
1372 self.conn = Some(pool_conn);
1373 let rows = rows_result.map_err(map_sqlx_error)?;
1374 if rows.is_empty() {
1375 return Ok(Vec::new());
1376 }
1377 let col_names: Vec<String> = rows[0]
1378 .columns()
1379 .iter()
1380 .map(|col| col.name().to_string())
1381 .collect();
1382 let mut result = Vec::with_capacity(rows.len());
1383 for row in &rows {
1384 let mut record = HashMap::with_capacity(col_names.len());
1385 for (i, name) in col_names.iter().enumerate() {
1386 let value = row_to_value_mysql(row, i);
1387 record.insert(name.clone(), value);
1388 }
1389 result.push(record);
1390 }
1391 Ok(result)
1392 })
1393 }
1394
1395 fn query_values<'a>(
1399 &'a mut self,
1400 sql: &'a str,
1401 ) -> Pin<Box<dyn Future<Output = Result<QueryValues, DbError>> + Send + 'a>> {
1402 Box::pin(async move {
1403 let mut pool_conn = self
1404 .conn
1405 .take()
1406 .ok_or_else(|| DbError::Internal("connection already closed".to_string()))?;
1407 let rows_result = (&mut *pool_conn).fetch_all(sqlx::AssertSqlSafe(sql)).await;
1408 self.conn = Some(pool_conn);
1409 let rows = rows_result.map_err(map_sqlx_error)?;
1410 if rows.is_empty() {
1411 return Ok((Vec::new(), Vec::new()));
1412 }
1413 let cols = rows[0].columns();
1414 let mut col_names: Vec<String> = Vec::with_capacity(cols.len());
1415 let mut col_types: Vec<ColType> = Vec::with_capacity(cols.len());
1416 for col in cols {
1417 col_names.push(col.name().to_string());
1418 col_types.push(ColType::parse_mysql(col.type_info().name()));
1419 }
1420 let mut result_rows: Vec<Vec<Value>> = Vec::with_capacity(rows.len());
1421 for row in &rows {
1422 let mut row_values: Vec<Value> = Vec::with_capacity(col_names.len());
1423 for (idx, _) in col_names.iter().enumerate() {
1424 row_values.push(row_to_value_with_coltype_mysql(row, idx, col_types[idx]));
1425 }
1426 result_rows.push(row_values);
1427 }
1428 Ok((col_names, result_rows))
1429 })
1430 }
1431
1432 fn query_values_with_params<'a>(
1434 &'a mut self,
1435 sql: &'a str,
1436 params: &'a [Value],
1437 ) -> Pin<Box<dyn Future<Output = Result<QueryValues, DbError>> + Send + 'a>> {
1438 Box::pin(async move {
1439 if params.is_empty() {
1440 return self.query_values(sql).await;
1441 }
1442 let mut pool_conn = self
1443 .conn
1444 .take()
1445 .ok_or_else(|| DbError::Internal("connection already closed".to_string()))?;
1446 let mut q = sqlx::query(sqlx::AssertSqlSafe(sql));
1447 for v in params {
1448 q = match v {
1449 Value::Null => q.bind(None::<i64>),
1450 Value::Bool(b) => q.bind(*b),
1451 Value::I8(n) => q.bind(*n),
1452 Value::I16(n) => q.bind(*n),
1453 Value::I32(n) => q.bind(*n),
1454 Value::I64(n) => q.bind(*n),
1455 Value::U8(n) => q.bind(*n),
1456 Value::U16(n) => q.bind(*n),
1457 Value::U32(n) => q.bind(*n),
1458 Value::U64(n) => q.bind(*n as i64),
1459 Value::F32(f) => q.bind(*f),
1460 Value::F64(f) => q.bind(*f),
1461 Value::String(s) => q.bind(s.as_str()),
1462 Value::Decimal(s) => q.bind(s.as_str()),
1463 Value::Bytes(b) => q.bind(b.as_slice()),
1464 Value::Uuid(s)
1467 | Value::Date(s)
1468 | Value::DateTime(s)
1469 | Value::Time(s)
1470 | Value::Json(s) => q.bind(s.as_str()),
1471 other => q.bind(other.to_string()),
1472 };
1473 }
1474 let rows_result = q.fetch_all(&mut *pool_conn).await;
1475 self.conn = Some(pool_conn);
1476 let rows = rows_result.map_err(map_sqlx_error)?;
1477 if rows.is_empty() {
1478 return Ok((Vec::new(), Vec::new()));
1479 }
1480 let cols = rows[0].columns();
1481 let mut col_names: Vec<String> = Vec::with_capacity(cols.len());
1482 let mut col_types: Vec<ColType> = Vec::with_capacity(cols.len());
1483 for col in cols {
1484 col_names.push(col.name().to_string());
1485 col_types.push(ColType::parse_mysql(col.type_info().name()));
1486 }
1487 let mut result_rows: Vec<Vec<Value>> = Vec::with_capacity(rows.len());
1488 for row in &rows {
1489 let mut row_values: Vec<Value> = Vec::with_capacity(col_names.len());
1490 for (idx, _) in col_names.iter().enumerate() {
1491 row_values.push(row_to_value_with_coltype_mysql(row, idx, col_types[idx]));
1492 }
1493 result_rows.push(row_values);
1494 }
1495 Ok((col_names, result_rows))
1496 })
1497 }
1498
1499 fn query_stream<'a>(
1505 &'a mut self,
1506 sql: &'a str,
1507 ) -> Pin<Box<dyn futures::Stream<Item = Result<HashMap<String, Value>, DbError>> + Send + 'a>>
1508 {
1509 Box::pin(async_stream::try_stream! {
1510 let mut pool_conn = self
1511 .conn
1512 .take()
1513 .ok_or_else(|| DbError::Internal("connection already closed".to_string()))?;
1514 let mut row_stream = sqlx::query(sqlx::AssertSqlSafe(sql)).fetch(&mut *pool_conn);
1515 let mut col_types: Vec<ColType> = Vec::new();
1516 let mut col_names: Vec<String> = Vec::new();
1517 let mut first_row = true;
1518 while let Some(row_result) = row_stream.next().await {
1519 let row = row_result.map_err(map_sqlx_error)?;
1520 if first_row {
1521 for col in row.columns() {
1522 col_names.push(col.name().to_string());
1523 col_types.push(ColType::parse_mysql(col.type_info().name()));
1524 }
1525 first_row = false;
1526 }
1527 let mut record = HashMap::with_capacity(col_names.len());
1528 for (i, name) in col_names.iter().enumerate() {
1529 let value = row_to_value_with_coltype_mysql(&row, i, col_types[i]);
1530 record.insert(name.clone(), value);
1531 }
1532 yield record;
1533 }
1534 drop(row_stream);
1537 self.conn = Some(pool_conn);
1538 })
1539 }
1540}
1541
1542impl Drop for SqlxMySqlConnection {
1543 fn drop(&mut self) {
1544 if let Some(conn) = self.conn.take() {
1545 drop(conn);
1546 }
1547 }
1548}
1549
1550pub async fn mysql_bulk_insert(
1557 conn: &mut SqlxMySqlConnection,
1558 table: &str,
1559 columns: &[&str],
1560 rows: &[Vec<Value>],
1561) -> Result<u64, DbError> {
1562 if rows.is_empty() {
1563 return Ok(0);
1564 }
1565 let col_list = columns.join(", ");
1566 let cols_per_row = columns.len();
1567 let row_placeholder = format!("({})", vec!["?"; cols_per_row].join(", "));
1569 let placeholders = vec![row_placeholder; rows.len()].join(", ");
1570 let sql = format!(
1571 "INSERT INTO {} ({}) VALUES {}",
1572 table, col_list, placeholders
1573 );
1574 let mut params: Vec<Value> = Vec::with_capacity(rows.len() * cols_per_row);
1576 for row in rows {
1577 for v in row {
1578 params.push(v.clone());
1579 }
1580 }
1581 conn.execute_with_params(&sql, ¶ms).await
1582}
1583
1584fn pg_decode_err(
1591 row: &sqlx::postgres::PgRow,
1592 ordinal: usize,
1593 type_name: &str,
1594 err: sqlx::Error,
1595) -> DbError {
1596 let column = row
1597 .columns()
1598 .get(ordinal)
1599 .map(|c| c.name().to_string())
1600 .unwrap_or_else(|| format!("#{ordinal}"));
1601 DbError::QueryError(format!(
1602 "PostgreSQL 列 `{column}`(类型 {type_name},序号 {ordinal})解码失败:{err};\
1603 已拒绝以 NULL 代替真实值"
1604 ))
1605}
1606
1607fn row_to_value_pg(row: &sqlx::postgres::PgRow, ordinal: usize) -> Result<Value, DbError> {
1608 use sqlx::TypeInfo;
1609 let type_name = row.columns()[ordinal].type_info().name();
1610 macro_rules! dec {
1611 ($ty:ty) => {
1612 row.try_get::<Option<$ty>, usize>(ordinal)
1613 .map_err(|e| pg_decode_err(row, ordinal, type_name, e))
1614 };
1615 }
1616 match type_name {
1617 "BOOL" => Ok(dec!(bool)?.map(Value::Bool).unwrap_or(Value::Null)),
1618 "INT2" => Ok(dec!(i16)?.map(Value::I16).unwrap_or(Value::Null)),
1619 "INT4" | "OID" => Ok(dec!(i32)?.map(Value::I32).unwrap_or(Value::Null)),
1620 "INT8" => Ok(dec!(i64)?.map(Value::I64).unwrap_or(Value::Null)),
1621 "FLOAT4" => Ok(dec!(f32)?.map(Value::F32).unwrap_or(Value::Null)),
1622 "FLOAT8" => Ok(dec!(f64)?.map(Value::F64).unwrap_or(Value::Null)),
1623 "TEXT" | "VARCHAR" | "CHAR" | "NAME" | "BPCHAR" => {
1624 Ok(dec!(String)?.map(Value::String).unwrap_or(Value::Null))
1625 }
1626 "BYTEA" => Ok(dec!(Vec<u8>)?.map(Value::Bytes).unwrap_or(Value::Null)),
1627 "NUMERIC" => Ok(dec!(bigdecimal::BigDecimal)?
1629 .map(|v| Value::Decimal(v.to_string()))
1630 .unwrap_or(Value::Null)),
1631 "UUID" => Ok(dec!(sqlx::types::Uuid)?
1632 .map(|uuid| Value::String(uuid.to_string()))
1633 .unwrap_or(Value::Null)),
1634 "TIMESTAMP" => Ok(dec!(sqlx::types::chrono::NaiveDateTime)?
1636 .map(|v| Value::DateTime(v.to_string()))
1637 .unwrap_or(Value::Null)),
1638 "TIMESTAMPTZ" => Ok(
1639 dec!(sqlx::types::chrono::DateTime<sqlx::types::chrono::Utc>)?
1640 .map(|v| Value::DateTime(v.format("%Y-%m-%dT%H:%M:%S%.6fZ").to_string()))
1641 .unwrap_or(Value::Null),
1642 ),
1643 "DATE" => Ok(dec!(sqlx::types::chrono::NaiveDate)?
1644 .map(|v| Value::Date(v.to_string()))
1645 .unwrap_or(Value::Null)),
1646 "TIME" => Ok(dec!(sqlx::types::chrono::NaiveTime)?
1647 .map(|v| Value::Time(v.to_string()))
1648 .unwrap_or(Value::Null)),
1649 "JSON" | "JSONB" => Ok(dec!(serde_json::Value)?
1650 .map(|v| Value::Json(v.to_string()))
1651 .unwrap_or(Value::Null)),
1652 _ => {
1653 if let Ok(v) = row.try_get::<Option<i64>, usize>(ordinal) {
1655 return Ok(v.map(Value::I64).unwrap_or(Value::Null));
1656 }
1657 if let Ok(v) = row.try_get::<Option<f64>, usize>(ordinal) {
1658 return Ok(v.map(Value::F64).unwrap_or(Value::Null));
1659 }
1660 if let Ok(v) = row.try_get::<Option<bool>, usize>(ordinal) {
1661 return Ok(v.map(Value::Bool).unwrap_or(Value::Null));
1662 }
1663 match row.try_get::<Option<String>, usize>(ordinal) {
1664 Ok(v) => Ok(v.map(Value::String).unwrap_or(Value::Null)),
1665 Err(e) => Err(pg_decode_err(row, ordinal, type_name, e)),
1666 }
1667 }
1668 }
1669}
1670
1671fn row_to_value_with_coltype_pg(
1679 row: &sqlx::postgres::PgRow,
1680 ordinal: usize,
1681 col_type: ColType,
1682) -> Value {
1683 match col_type {
1684 ColType::Bool => match row.try_get::<Option<bool>, usize>(ordinal) {
1685 Ok(v) => v.map(Value::Bool).unwrap_or(Value::Null),
1686 Err(_) => Value::Null,
1687 },
1688 ColType::I8 => match row.try_get::<Option<i8>, usize>(ordinal) {
1689 Ok(v) => v.map(Value::I8).unwrap_or(Value::Null),
1690 Err(_) => Value::Null,
1691 },
1692 ColType::I16 => match row.try_get::<Option<i16>, usize>(ordinal) {
1693 Ok(v) => v.map(Value::I16).unwrap_or(Value::Null),
1694 Err(_) => Value::Null,
1695 },
1696 ColType::I32 => match row.try_get::<Option<i32>, usize>(ordinal) {
1698 Ok(v) => v.map(Value::I32).unwrap_or(Value::Null),
1699 Err(_) => Value::Null,
1700 },
1701 ColType::I64 => match row.try_get::<Option<i64>, usize>(ordinal) {
1702 Ok(v) => v.map(Value::I64).unwrap_or(Value::Null),
1703 Err(_) => Value::Null,
1704 },
1705 ColType::U8 => match row.try_get::<Option<i16>, usize>(ordinal) {
1710 Ok(v) => v.map(Value::I16).unwrap_or(Value::Null),
1711 Err(_) => Value::Null,
1712 },
1713 ColType::U16 => match row.try_get::<Option<i32>, usize>(ordinal) {
1714 Ok(v) => v.map(Value::I32).unwrap_or(Value::Null),
1715 Err(_) => Value::Null,
1716 },
1717 ColType::U32 => match row.try_get::<Option<i64>, usize>(ordinal) {
1718 Ok(v) => v.map(Value::I64).unwrap_or(Value::Null),
1719 Err(_) => Value::Null,
1720 },
1721 ColType::U64 => match row.try_get::<Option<i64>, usize>(ordinal) {
1723 Ok(v) => v.map(Value::I64).unwrap_or(Value::Null),
1724 Err(_) => Value::Null,
1725 },
1726 ColType::F32 => match row.try_get::<Option<f32>, usize>(ordinal) {
1727 Ok(v) => v.map(Value::F32).unwrap_or(Value::Null),
1728 Err(_) => Value::Null,
1729 },
1730 ColType::F64 => match row.try_get::<Option<f64>, usize>(ordinal) {
1731 Ok(v) => v.map(Value::F64).unwrap_or(Value::Null),
1732 Err(_) => Value::Null,
1733 },
1734 ColType::Decimal => match row.try_get::<Option<bigdecimal::BigDecimal>, usize>(ordinal) {
1736 Ok(Some(v)) => Value::Decimal(v.to_string()),
1737 Ok(None) => Value::Null,
1738 Err(_) => match row.try_get::<Option<rust_decimal::Decimal>, usize>(ordinal) {
1739 Ok(Some(v)) => Value::Decimal(v.to_string()),
1740 Ok(None) => Value::Null,
1741 Err(_) => Value::Null,
1742 },
1743 },
1744 ColType::String => match row.try_get::<Option<String>, usize>(ordinal) {
1745 Ok(v) => v.map(Value::String).unwrap_or(Value::Null),
1746 Err(_) => Value::Null,
1747 },
1748 ColType::Bytes => match row.try_get::<Option<Vec<u8>>, usize>(ordinal) {
1749 Ok(v) => v.map(Value::Bytes).unwrap_or(Value::Null),
1750 Err(_) => Value::Null,
1751 },
1752 ColType::Date => {
1754 match row.try_get::<Option<sqlx::types::chrono::NaiveDate>, usize>(ordinal) {
1755 Ok(v) => v.map(|d| Value::Date(d.to_string())).unwrap_or(Value::Null),
1756 Err(_) => Value::Null,
1757 }
1758 }
1759 ColType::DateTime => match row
1760 .try_get::<Option<sqlx::types::chrono::NaiveDateTime>, usize>(ordinal)
1761 {
1762 Ok(v) => v
1763 .map(|d| Value::DateTime(d.to_string()))
1764 .unwrap_or(Value::Null),
1765 Err(_) => match row
1766 .try_get::<Option<sqlx::types::chrono::DateTime<sqlx::types::chrono::Utc>>, usize>(
1767 ordinal,
1768 ) {
1769 Ok(v) => v
1770 .map(|d| Value::DateTime(d.format("%Y-%m-%dT%H:%M:%S%.6fZ").to_string()))
1771 .unwrap_or(Value::Null),
1772 Err(_) => Value::Null,
1773 },
1774 },
1775 ColType::Time => {
1776 match row.try_get::<Option<sqlx::types::chrono::NaiveTime>, usize>(ordinal) {
1777 Ok(v) => v.map(|t| Value::Time(t.to_string())).unwrap_or(Value::Null),
1778 Err(_) => Value::Null,
1779 }
1780 }
1781 ColType::Json => match row.try_get::<Option<serde_json::Value>, usize>(ordinal) {
1782 Ok(v) => v.map(|j| Value::Json(j.to_string())).unwrap_or(Value::Null),
1783 Err(_) => match row.try_get::<Option<String>, usize>(ordinal) {
1784 Ok(v) => v.map(Value::String).unwrap_or(Value::Null),
1785 Err(_) => Value::Null,
1786 },
1787 },
1788 ColType::Uuid => match row.try_get::<Option<sqlx::types::Uuid>, usize>(ordinal) {
1790 Ok(v) => v
1791 .map(|uuid| Value::String(uuid.to_string()))
1792 .unwrap_or(Value::Null),
1793 Err(_) => match row.try_get::<Option<String>, usize>(ordinal) {
1794 Ok(v) => v.map(Value::String).unwrap_or(Value::Null),
1795 Err(_) => Value::Null,
1796 },
1797 },
1798 ColType::Unknown => {
1799 if let Ok(v) = row.try_get::<Option<i64>, usize>(ordinal) {
1801 return v.map(Value::I64).unwrap_or(Value::Null);
1802 }
1803 if let Ok(v) = row.try_get::<Option<f64>, usize>(ordinal) {
1804 return v.map(Value::F64).unwrap_or(Value::Null);
1805 }
1806 if let Ok(v) = row.try_get::<Option<bool>, usize>(ordinal) {
1807 return v.map(Value::Bool).unwrap_or(Value::Null);
1808 }
1809 if let Ok(v) = row.try_get::<Option<String>, usize>(ordinal) {
1810 return v.map(Value::String).unwrap_or(Value::Null);
1811 }
1812 Value::Null
1813 }
1814 _ => Value::Null,
1816 }
1817}
1818
1819fn pg_rewrite_placeholders(sql: &str) -> std::borrow::Cow<'_, str> {
1828 if !sql.as_bytes().contains(&b'?') {
1829 return std::borrow::Cow::Borrowed(sql);
1830 }
1831 let mut out = String::with_capacity(sql.len() + 8);
1832 let mut chars = sql.chars().peekable();
1833 let mut in_single = false;
1834 let mut in_double = false;
1835 let mut in_line_comment = false;
1836 let mut in_block_comment = false;
1837 let mut dollar_tag: Option<String> = None;
1838 let mut prev = '\0';
1839 let mut n = 0usize;
1840 let mut saw_native = false;
1841 while let Some(c) = chars.next() {
1842 if in_line_comment {
1843 out.push(c);
1844 if c == '\n' {
1845 in_line_comment = false;
1846 }
1847 prev = c;
1848 continue;
1849 }
1850 if in_block_comment {
1851 out.push(c);
1852 if c == '*' && chars.peek() == Some(&'/') {
1853 out.push(chars.next().unwrap_or('/'));
1854 in_block_comment = false;
1855 }
1856 prev = c;
1857 continue;
1858 }
1859 if in_single {
1860 out.push(c);
1861 if c == '\'' {
1862 if chars.peek() == Some(&'\'') {
1863 out.push(chars.next().unwrap_or('\''));
1864 } else {
1865 in_single = false;
1866 }
1867 }
1868 prev = c;
1869 continue;
1870 }
1871 if in_double {
1872 out.push(c);
1873 if c == '"' {
1874 if chars.peek() == Some(&'"') {
1875 out.push(chars.next().unwrap_or('"'));
1876 } else {
1877 in_double = false;
1878 }
1879 }
1880 prev = c;
1881 continue;
1882 }
1883 if let Some(tag) = dollar_tag.as_deref() {
1884 if c == '$' {
1886 if let Some(extra) = pg_try_close_dollar_quote(&chars, tag) {
1887 out.push(c);
1888 for _ in 0..extra {
1889 out.push(chars.next().unwrap_or('$'));
1890 }
1891 dollar_tag = None;
1892 } else {
1893 out.push(c);
1894 }
1895 } else {
1896 out.push(c);
1897 }
1898 prev = c;
1899 continue;
1900 }
1901 match c {
1902 '\'' => {
1903 in_single = true;
1904 out.push(c);
1905 }
1906 '"' => {
1907 in_double = true;
1908 out.push(c);
1909 }
1910 '-' if chars.peek() == Some(&'-') => {
1911 out.push(c);
1912 out.push(chars.next().unwrap_or('-'));
1913 in_line_comment = true;
1914 }
1915 '/' if chars.peek() == Some(&'*') => {
1916 out.push(c);
1917 out.push(chars.next().unwrap_or('*'));
1918 in_block_comment = true;
1919 }
1920 '$' => {
1921 if let Some(rest) = pg_peek_dollar_quote_rest(&chars) {
1923 out.push('$');
1924 for ch in rest.chars() {
1925 out.push(ch);
1926 chars.next();
1927 }
1928 dollar_tag = Some(rest[..rest.len() - 1].to_string());
1929 } else if chars.peek().is_some_and(|next| next.is_ascii_digit()) {
1930 saw_native = true;
1932 out.push(c);
1933 } else {
1934 out.push(c);
1935 }
1936 }
1937 '?' if prev != '@'
1938 && !matches!(chars.peek(), Some('|') | Some('&'))
1939 && !pg_question_is_json_operator(&chars) =>
1940 {
1941 n += 1;
1942 out.push('$');
1943 out.push_str(&n.to_string());
1944 }
1945 _ => out.push(c),
1946 }
1947 prev = c;
1948 }
1949 if saw_native {
1950 return std::borrow::Cow::Borrowed(sql);
1951 }
1952 std::borrow::Cow::Owned(out)
1953}
1954
1955fn pg_question_is_json_operator(chars: &std::iter::Peekable<std::str::Chars<'_>>) -> bool {
1959 let mut lookahead = chars.clone();
1960 while let Some(&c) = lookahead.peek() {
1961 match c {
1962 ' ' | '\t' => {
1963 lookahead.next();
1964 }
1965 '\'' | '"' => return true,
1966 _ => return false,
1967 }
1968 }
1969 false
1970}
1971
1972fn pg_peek_dollar_quote_rest(chars: &std::iter::Peekable<std::str::Chars<'_>>) -> Option<String> {
1980 let mut lookahead = chars.clone();
1981 if lookahead.peek() == Some(&'$') {
1982 return Some("$".to_string());
1983 }
1984 let first = *lookahead.peek()?;
1985 if !(first.is_ascii_alphabetic() || first == '_') {
1986 return None;
1987 }
1988 let mut rest = String::new();
1989 while let Some(&c) = lookahead.peek() {
1990 match c {
1991 '$' => {
1992 rest.push('$');
1993 return Some(rest);
1994 }
1995 c if c.is_ascii_alphanumeric() || c == '_' => {
1996 rest.push(c);
1997 lookahead.next();
1998 }
1999 _ => return None,
2000 }
2001 }
2002 None
2003}
2004
2005fn pg_try_close_dollar_quote(
2010 chars: &std::iter::Peekable<std::str::Chars<'_>>,
2011 tag: &str,
2012) -> Option<usize> {
2013 let mut lookahead = chars.clone();
2014 for expected in tag.chars() {
2015 if lookahead.next() != Some(expected) {
2016 return None;
2017 }
2018 }
2019 if lookahead.next() == Some('$') {
2020 Some(tag.chars().count() + 1)
2021 } else {
2022 None
2023 }
2024}
2025
2026type PgQuery<'q> = sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments>;
2028
2029fn bind_pg_value<'q>(q: PgQuery<'q>, v: &'q Value) -> PgQuery<'q> {
2034 match v {
2035 Value::Null => q.bind(None::<i64>),
2036 Value::Bool(b) => q.bind(*b),
2037 Value::I8(n) => q.bind(*n),
2038 Value::I16(n) => q.bind(*n),
2039 Value::I32(n) => q.bind(*n),
2040 Value::I64(n) => q.bind(*n),
2041 Value::U8(n) => q.bind(*n as i16),
2043 Value::U16(n) => q.bind(*n as i32),
2044 Value::U32(n) => q.bind(*n as i64),
2045 Value::U64(n) => q.bind(*n as i64),
2046 Value::F32(f) => q.bind(*f),
2047 Value::F64(f) => q.bind(*f),
2048 Value::String(s) => q.bind(s.as_str()),
2049 Value::Decimal(s) => match s.parse::<bigdecimal::BigDecimal>() {
2051 Ok(d) => q.bind(d),
2052 Err(_) => q.bind(s.as_str()),
2053 },
2054 Value::Uuid(s) => match sqlx::types::Uuid::parse_str(s) {
2055 Ok(u) => q.bind(u),
2056 Err(_) => q.bind(s.as_str()),
2057 },
2058 Value::Date(s) => match parse_pg_date(s) {
2059 Some(d) => q.bind(d),
2060 None => q.bind(s.as_str()),
2061 },
2062 Value::DateTime(s) => match parse_pg_datetime(s) {
2063 Some(PgDateTime::Zoned(d)) => q.bind(d),
2065 Some(PgDateTime::Naive(d)) => q.bind(d),
2066 None => q.bind(s.as_str()),
2067 },
2068 Value::Time(s) => match parse_pg_time(s) {
2069 Some(t) => q.bind(t),
2070 None => q.bind(s.as_str()),
2071 },
2072 Value::Json(s) => match serde_json::from_str::<serde_json::Value>(s) {
2073 Ok(j) => q.bind(j),
2074 Err(_) => q.bind(s.as_str()),
2075 },
2076 Value::Bytes(b) => q.bind(b.as_slice()),
2077 other => q.bind(other.to_string()),
2078 }
2079}
2080
2081fn parse_pg_date(s: &str) -> Option<sqlx::types::chrono::NaiveDate> {
2083 use sqlx::types::chrono::NaiveDate;
2084 let head = s.get(..10)?;
2085 NaiveDate::parse_from_str(head, "%Y-%m-%d").ok()
2086}
2087
2088enum PgDateTime {
2090 Naive(sqlx::types::chrono::NaiveDateTime),
2092 Zoned(sqlx::types::chrono::DateTime<sqlx::types::chrono::Utc>),
2094}
2095
2096fn parse_pg_datetime(s: &str) -> Option<PgDateTime> {
2100 use sqlx::types::chrono::{DateTime, NaiveDateTime, Utc};
2101 let has_offset = s.ends_with('Z')
2102 || s.ends_with('z')
2103 || s.get(10..).is_some_and(|rest| {
2104 rest.char_indices().any(|(i, c)| {
2105 (c == '+' || c == '-')
2106 && rest[..i].chars().any(|p| p == ':')
2107 && i > 0
2109 })
2110 });
2111 if has_offset {
2112 if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
2113 return Some(PgDateTime::Zoned(dt.with_timezone(&Utc)));
2114 }
2115 for fmt in [
2116 "%Y-%m-%d %H:%M:%S%.f%:z",
2117 "%Y-%m-%d %H:%M:%S%:z",
2118 "%Y-%m-%dT%H:%M:%S%.f%:z",
2119 "%Y-%m-%dT%H:%M:%S%:z",
2120 "%Y-%m-%d %H:%M:%S%.f%z",
2121 ] {
2122 if let Ok(dt) = DateTime::parse_from_str(s, fmt) {
2123 return Some(PgDateTime::Zoned(dt.with_timezone(&Utc)));
2124 }
2125 }
2126 }
2127 for fmt in ["%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"] {
2128 if let Ok(dt) = NaiveDateTime::parse_from_str(s, fmt) {
2129 return Some(PgDateTime::Naive(dt));
2130 }
2131 let with_frac = format!("{fmt}%.f");
2132 if let Ok(dt) = NaiveDateTime::parse_from_str(s, &with_frac) {
2133 return Some(PgDateTime::Naive(dt));
2134 }
2135 }
2136 if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
2137 return Some(PgDateTime::Zoned(dt.with_timezone(&Utc)));
2138 }
2139 None
2140}
2141
2142fn parse_pg_time(s: &str) -> Option<sqlx::types::chrono::NaiveTime> {
2144 use sqlx::types::chrono::NaiveTime;
2145 NaiveTime::parse_from_str(s, "%H:%M:%S")
2146 .or_else(|_| NaiveTime::parse_from_str(s, "%H:%M:%S%.f"))
2147 .ok()
2148}
2149
2150pub struct PgPoolHandle {
2151 pool: sqlx::PgPool,
2152}
2153
2154impl PgPoolHandle {
2155 pub async fn connect(url: &str) -> Result<Self, DbError> {
2156 Self::connect_with_max(url, 10).await
2158 }
2159
2160 pub async fn connect_with_max(url: &str, max_connections: u32) -> Result<Self, DbError> {
2163 let pool = sqlx::pool::PoolOptions::<sqlx::Postgres>::new()
2164 .max_connections(max_connections)
2165 .acquire_timeout(std::time::Duration::from_secs(30))
2166 .idle_timeout(Some(std::time::Duration::from_secs(600)))
2167 .max_lifetime(Some(std::time::Duration::from_secs(1800)))
2168 .connect(url)
2169 .await
2170 .map_err(map_sqlx_error)?;
2171 Ok(Self { pool })
2172 }
2173
2174 pub fn from_pool(pool: sqlx::PgPool) -> Self {
2175 Self { pool }
2176 }
2177
2178 pub fn pool(&self) -> &sqlx::PgPool {
2179 &self.pool
2180 }
2181}
2182
2183pub struct SqlxPgConnectionFactory {
2184 pool: Arc<PgPoolHandle>,
2185}
2186
2187impl SqlxPgConnectionFactory {
2188 pub fn new(pool: Arc<PgPoolHandle>) -> Self {
2189 Self { pool }
2190 }
2191}
2192
2193#[async_trait]
2194impl ConnectionFactory for SqlxPgConnectionFactory {
2195 async fn create(&self) -> Result<Box<dyn Connection>, DbError> {
2196 let conn = self.pool.pool.acquire().await.map_err(map_sqlx_error)?;
2197 Ok(Box::new(crate::metrics_conn::MetricsConn::new(Box::new(
2199 SqlxPgConnection {
2200 conn: Some(conn),
2201 connected: true,
2202 in_transaction: false,
2203 },
2204 ))))
2205 }
2206}
2207
2208pub struct SqlxPgConnection {
2209 conn: Option<sqlx::pool::PoolConnection<sqlx::Postgres>>,
2210 connected: bool,
2211 in_transaction: bool,
2212}
2213
2214impl Connection for SqlxPgConnection {
2215 fn db_type(&self) -> Option<DbType> {
2216 Some(DbType::PostgreSQL)
2217 }
2218
2219 fn session_reset_sql(&self) -> Option<&'static str> {
2223 Some("DISCARD ALL")
2224 }
2225
2226 fn execute<'a>(
2227 &'a mut self,
2228 sql: &'a str,
2229 ) -> Pin<Box<dyn Future<Output = Result<u64, DbError>> + Send + 'a>> {
2230 Box::pin(async move {
2231 let mut pool_conn = self
2232 .conn
2233 .take()
2234 .ok_or_else(|| DbError::Internal("connection already closed".to_string()))?;
2235 let result = if needs_raw_sql(sql) {
2238 (&mut *pool_conn)
2239 .execute(sqlx::raw_sql(sqlx::AssertSqlSafe(sql)))
2240 .await
2241 } else {
2242 (&mut *pool_conn).execute(sqlx::AssertSqlSafe(sql)).await
2243 };
2244 self.conn = Some(pool_conn);
2245
2246 match result {
2247 Ok(r) => Ok(r.rows_affected()),
2248 Err(e) => {
2249 let db_err = map_sqlx_error(e);
2250 if matches!(db_err, DbError::ConnectionError(_) | DbError::IoError(_)) {
2251 self.connected = false;
2252 }
2253 Err(db_err)
2254 }
2255 }
2256 })
2257 }
2258
2259 fn query<'a>(
2260 &'a mut self,
2261 sql: &'a str,
2262 ) -> Pin<Box<dyn Future<Output = Result<Vec<HashMap<String, Value>>, DbError>> + Send + 'a>>
2263 {
2264 Box::pin(async move {
2265 let mut pool_conn = self
2266 .conn
2267 .take()
2268 .ok_or_else(|| DbError::Internal("connection already closed".to_string()))?;
2269 let rows_result = (&mut *pool_conn).fetch_all(sqlx::AssertSqlSafe(sql)).await;
2272 self.conn = Some(pool_conn);
2273
2274 let rows = rows_result.map_err(map_sqlx_error)?;
2275 if rows.is_empty() {
2276 return Ok(Vec::new());
2277 }
2278 let col_types: Vec<ColType> = rows[0]
2281 .columns()
2282 .iter()
2283 .map(|col| ColType::parse_postgres(col.type_info().name()))
2284 .collect();
2285 let col_names: Vec<String> = rows[0]
2286 .columns()
2287 .iter()
2288 .map(|col| col.name().to_string())
2289 .collect();
2290 let mut result = Vec::with_capacity(rows.len());
2291 for row in &rows {
2292 let mut record = HashMap::with_capacity(col_types.len());
2293 for (i, name) in col_names.iter().enumerate() {
2294 let value = row_to_value_with_coltype_pg(row, i, col_types[i]);
2295 record.insert(name.clone(), value);
2296 }
2297 result.push(record);
2298 }
2299 Ok(result)
2300 })
2301 }
2302
2303 fn begin_transaction<'a>(
2304 &'a mut self,
2305 ) -> Pin<Box<dyn Future<Output = Result<(), DbError>> + Send + 'a>> {
2306 Box::pin(async move {
2307 if self.in_transaction {
2308 return Err(DbError::Internal("transaction already started".to_string()));
2309 }
2310 self.execute("BEGIN").await?;
2311 self.in_transaction = true;
2312 Ok(())
2313 })
2314 }
2315
2316 fn commit<'a>(&'a mut self) -> Pin<Box<dyn Future<Output = Result<(), DbError>> + Send + 'a>> {
2317 Box::pin(async move {
2318 if self.in_transaction {
2319 self.execute("COMMIT").await?;
2320 self.in_transaction = false;
2321 }
2322 Ok(())
2323 })
2324 }
2325
2326 fn rollback<'a>(
2327 &'a mut self,
2328 ) -> Pin<Box<dyn Future<Output = Result<(), DbError>> + Send + 'a>> {
2329 Box::pin(async move {
2330 if self.in_transaction {
2331 let result = self.execute("ROLLBACK").await;
2336 if result.is_ok() {
2337 self.in_transaction = false;
2338 }
2339 result.map(|_| ())
2340 } else {
2341 Ok(())
2342 }
2343 })
2344 }
2345
2346 fn is_connected(&self) -> bool {
2347 self.connected && self.conn.is_some()
2351 }
2352
2353 fn in_transaction(&self) -> bool {
2354 self.in_transaction
2355 }
2356
2357 fn ping<'a>(&'a mut self) -> Pin<Box<dyn Future<Output = bool> + Send + 'a>> {
2358 Box::pin(async move {
2359 match self.execute("SELECT 1").await {
2360 Ok(_) => true,
2361 Err(_) => {
2362 self.connected = false;
2363 false
2364 }
2365 }
2366 })
2367 }
2368
2369 fn close<'a>(&'a mut self) -> Pin<Box<dyn Future<Output = Result<(), DbError>> + Send + 'a>> {
2370 Box::pin(async move {
2371 if let Some(conn) = self.conn.take() {
2372 drop(conn);
2373 }
2374 self.connected = false;
2375 self.in_transaction = false;
2376 Ok(())
2377 })
2378 }
2379
2380 fn execute_with_params<'a>(
2398 &'a mut self,
2399 sql: &'a str,
2400 params: &'a [Value],
2401 ) -> Pin<Box<dyn Future<Output = Result<u64, DbError>> + Send + 'a>> {
2402 Box::pin(async move {
2403 if needs_raw_sql(sql) || params.is_empty() {
2404 return self.execute(sql).await;
2405 }
2406 let mut pool_conn = self
2407 .conn
2408 .take()
2409 .ok_or_else(|| DbError::Internal("connection already closed".to_string()))?;
2410 let rewritten = pg_rewrite_placeholders(sql);
2411 let mut q = sqlx::query(sqlx::AssertSqlSafe(rewritten.as_ref()));
2412 for v in params {
2413 q = bind_pg_value(q, v);
2414 }
2415 let result = q.execute(&mut *pool_conn).await;
2416 self.conn = Some(pool_conn);
2417 match result {
2418 Ok(r) => Ok(r.rows_affected()),
2419 Err(e) => {
2420 let db_err = map_sqlx_error(e);
2421 if matches!(db_err, DbError::ConnectionError(_) | DbError::IoError(_)) {
2422 self.connected = false;
2423 }
2424 Err(db_err)
2425 }
2426 }
2427 })
2428 }
2429
2430 fn query_with_params<'a>(
2434 &'a mut self,
2435 sql: &'a str,
2436 params: &'a [Value],
2437 ) -> Pin<Box<dyn Future<Output = Result<QueryRows, DbError>> + Send + 'a>> {
2438 Box::pin(async move {
2439 if params.is_empty() {
2440 return self.query(sql).await;
2441 }
2442 let mut pool_conn = self
2443 .conn
2444 .take()
2445 .ok_or_else(|| DbError::Internal("connection already closed".to_string()))?;
2446 let rewritten = pg_rewrite_placeholders(sql);
2447 let mut q = sqlx::query(sqlx::AssertSqlSafe(rewritten.as_ref()));
2448 for v in params {
2449 q = bind_pg_value(q, v);
2450 }
2451 let rows_result = q.fetch_all(&mut *pool_conn).await;
2452 self.conn = Some(pool_conn);
2453 let rows = rows_result.map_err(map_sqlx_error)?;
2454 if rows.is_empty() {
2455 return Ok(Vec::new());
2456 }
2457 let col_types: Vec<ColType> = rows[0]
2459 .columns()
2460 .iter()
2461 .map(|col| ColType::parse_postgres(col.type_info().name()))
2462 .collect();
2463 let col_names: Vec<String> = rows[0]
2464 .columns()
2465 .iter()
2466 .map(|col| col.name().to_string())
2467 .collect();
2468 let mut result = Vec::with_capacity(rows.len());
2469 for row in &rows {
2470 let mut record = HashMap::with_capacity(col_types.len());
2471 for (i, name) in col_names.iter().enumerate() {
2472 let value = row_to_value_with_coltype_pg(row, i, col_types[i]);
2473 record.insert(name.clone(), value);
2474 }
2475 result.push(record);
2476 }
2477 Ok(result)
2478 })
2479 }
2480
2481 fn query_values<'a>(
2485 &'a mut self,
2486 sql: &'a str,
2487 ) -> Pin<Box<dyn Future<Output = Result<QueryValues, DbError>> + Send + 'a>> {
2488 Box::pin(async move {
2489 let mut pool_conn = self
2490 .conn
2491 .take()
2492 .ok_or_else(|| DbError::Internal("connection already closed".to_string()))?;
2493 let rows_result = (&mut *pool_conn).fetch_all(sqlx::AssertSqlSafe(sql)).await;
2494 self.conn = Some(pool_conn);
2495 let rows = rows_result.map_err(map_sqlx_error)?;
2496 if rows.is_empty() {
2497 return Ok((Vec::new(), Vec::new()));
2498 }
2499 let cols = rows[0].columns();
2500 let mut col_names: Vec<String> = Vec::with_capacity(cols.len());
2501 let mut col_types: Vec<ColType> = Vec::with_capacity(cols.len());
2502 for col in cols {
2503 col_names.push(col.name().to_string());
2504 col_types.push(ColType::parse_postgres(col.type_info().name()));
2505 }
2506 let mut result_rows: Vec<Vec<Value>> = Vec::with_capacity(rows.len());
2507 for row in &rows {
2508 let mut row_values: Vec<Value> = Vec::with_capacity(col_names.len());
2509 for (idx, _) in col_names.iter().enumerate() {
2510 row_values.push(row_to_value_with_coltype_pg(row, idx, col_types[idx]));
2511 }
2512 result_rows.push(row_values);
2513 }
2514 Ok((col_names, result_rows))
2515 })
2516 }
2517
2518 fn query_values_with_params<'a>(
2522 &'a mut self,
2523 sql: &'a str,
2524 params: &'a [Value],
2525 ) -> Pin<Box<dyn Future<Output = Result<QueryValues, DbError>> + Send + 'a>> {
2526 Box::pin(async move {
2527 if params.is_empty() {
2528 return self.query_values(sql).await;
2529 }
2530 let mut pool_conn = self
2531 .conn
2532 .take()
2533 .ok_or_else(|| DbError::Internal("connection already closed".to_string()))?;
2534 let rewritten = pg_rewrite_placeholders(sql);
2535 let mut q = sqlx::query(sqlx::AssertSqlSafe(rewritten.as_ref()));
2536 for v in params {
2537 q = bind_pg_value(q, v);
2538 }
2539 let rows_result = q.fetch_all(&mut *pool_conn).await;
2540 self.conn = Some(pool_conn);
2541 let rows = rows_result.map_err(map_sqlx_error)?;
2542 if rows.is_empty() {
2543 return Ok((Vec::new(), Vec::new()));
2544 }
2545 let cols = rows[0].columns();
2546 let mut col_names: Vec<String> = Vec::with_capacity(cols.len());
2547 for col in cols {
2548 col_names.push(col.name().to_string());
2549 }
2550 let mut result_rows: Vec<Vec<Value>> = Vec::with_capacity(rows.len());
2551 for row in rows {
2552 let mut row_values: Vec<Value> = Vec::with_capacity(col_names.len());
2553 for (idx, _) in col_names.iter().enumerate() {
2554 let ordinal = row.columns()[idx].ordinal();
2555 row_values.push(row_to_value_pg(&row, ordinal)?);
2556 }
2557 result_rows.push(row_values);
2558 }
2559 Ok((col_names, result_rows))
2560 })
2561 }
2562
2563 fn query_stream<'a>(
2573 &'a mut self,
2574 sql: &'a str,
2575 ) -> Pin<Box<dyn futures::Stream<Item = Result<HashMap<String, Value>, DbError>> + Send + 'a>>
2576 {
2577 Box::pin(async_stream::try_stream! {
2578 let mut pool_conn = self
2579 .conn
2580 .take()
2581 .ok_or_else(|| DbError::Internal("connection already closed".to_string()))?;
2582 let mut row_stream = sqlx::query(sqlx::AssertSqlSafe(sql)).fetch(&mut *pool_conn);
2583 while let Some(row_result) = row_stream.next().await {
2586 let row = row_result.map_err(map_sqlx_error)?;
2587 let cols = row.columns();
2588 let mut record = HashMap::with_capacity(cols.len());
2589 for (i, col) in cols.iter().enumerate() {
2590 let name = col.name().to_string();
2591 let value = row_to_value_pg(&row, i)?;
2593 record.insert(name, value);
2594 }
2595 yield record;
2596 }
2597 drop(row_stream);
2600 self.conn = Some(pool_conn);
2601 })
2602 }
2603}
2604
2605impl Drop for SqlxPgConnection {
2606 fn drop(&mut self) {
2607 if let Some(conn) = self.conn.take() {
2608 drop(conn);
2609 }
2610 }
2611}
2612
2613#[async_trait]
2634pub trait PgExtensions: Send + Sync {
2635 async fn listen(&mut self, channel: &str) -> Result<(), DbError>;
2646
2647 async fn notify(&mut self, channel: &str, payload: &str) -> Result<(), DbError>;
2658
2659 async fn copy_from_stdin(&mut self, sql: &str, data: &[u8]) -> Result<u64, DbError>;
2674}
2675
2676fn validate_pg_channel_name(channel: &str) -> Result<(), DbError> {
2681 if channel.is_empty() {
2682 return Err(DbError::Internal(
2683 "PG channel name must not be empty".to_string(),
2684 ));
2685 }
2686 if !channel
2687 .chars()
2688 .all(|c| c.is_ascii_alphanumeric() || c == '_')
2689 {
2690 return Err(DbError::Internal(format!(
2691 "invalid PG channel name: {} (only alphanumeric and underscore allowed)",
2692 channel
2693 )));
2694 }
2695 Ok(())
2696}
2697
2698#[async_trait]
2699impl PgExtensions for SqlxPgConnection {
2700 async fn listen(&mut self, channel: &str) -> Result<(), DbError> {
2701 validate_pg_channel_name(channel)?;
2702 self.execute(&format!("LISTEN {}", channel)).await?;
2704 Ok(())
2705 }
2706
2707 async fn notify(&mut self, channel: &str, payload: &str) -> Result<(), DbError> {
2708 validate_pg_channel_name(channel)?;
2709 let escaped_payload = payload.replace('\'', "''");
2711 self.execute(&format!("NOTIFY {}, '{}'", channel, escaped_payload))
2712 .await?;
2713 Ok(())
2714 }
2715
2716 async fn copy_from_stdin(&mut self, sql: &str, data: &[u8]) -> Result<u64, DbError> {
2717 let mut pool_conn = self
2718 .conn
2719 .take()
2720 .ok_or_else(|| DbError::Internal("connection already closed".to_string()))?;
2721 let mut copy = (*pool_conn)
2725 .copy_in_raw(sql)
2726 .await
2727 .map_err(map_sqlx_error)?;
2728 copy.send(data).await.map_err(map_sqlx_error)?;
2730 let result = copy.finish().await.map_err(map_sqlx_error)?;
2732 self.conn = Some(pool_conn);
2733 Ok(result)
2734 }
2735}
2736
2737pub async fn pg_bulk_insert(
2745 conn: &mut SqlxPgConnection,
2746 table: &str,
2747 columns: &[&str],
2748 rows: &[Vec<Value>],
2749) -> Result<u64, DbError> {
2750 if rows.is_empty() {
2751 return Ok(0);
2752 }
2753 let col_list = columns.join(", ");
2754 let cols_per_row = columns.len();
2755 let placeholders: Vec<String> = rows
2757 .iter()
2758 .enumerate()
2759 .map(|(row_idx, _)| {
2760 let base = row_idx * cols_per_row;
2761 let ph: Vec<String> = (0..cols_per_row)
2762 .map(|i| format!("${}", base + i + 1))
2763 .collect();
2764 format!("({})", ph.join(", "))
2765 })
2766 .collect();
2767 let sql = format!(
2768 "INSERT INTO {} ({}) VALUES {}",
2769 table,
2770 col_list,
2771 placeholders.join(", ")
2772 );
2773 let mut params: Vec<Value> = Vec::with_capacity(rows.len() * cols_per_row);
2775 for row in rows {
2776 for v in row {
2777 params.push(v.clone());
2778 }
2779 }
2780 conn.execute_with_params(&sql, ¶ms).await
2781}
2782#[cfg(test)]
2783mod tests {
2784 use super::*;
2785
2786 #[test]
2787 fn test_pg_rewrite_placeholders_basic() {
2788 assert_eq!(
2789 pg_rewrite_placeholders("SELECT * FROM t WHERE a = ? AND b = ?"),
2790 "SELECT * FROM t WHERE a = $1 AND b = $2"
2791 );
2792 }
2793
2794 #[test]
2795 fn test_pg_rewrite_placeholders_skips_native() {
2796 assert_eq!(
2797 pg_rewrite_placeholders("SELECT * FROM t WHERE a = $1"),
2798 "SELECT * FROM t WHERE a = $1"
2799 );
2800 }
2801
2802 #[test]
2803 fn test_pg_rewrite_placeholders_skips_literals_and_comments() {
2804 assert_eq!(
2805 pg_rewrite_placeholders("SELECT '?' AS q -- ?\nFROM t WHERE id = ?"),
2806 "SELECT '?' AS q -- ?\nFROM t WHERE id = $1"
2807 );
2808 assert_eq!(
2809 pg_rewrite_placeholders("SELECT /* ? */ \"a?\" FROM t WHERE id = ?"),
2810 "SELECT /* ? */ \"a?\" FROM t WHERE id = $1"
2811 );
2812 }
2813
2814 #[test]
2815 fn test_pg_rewrite_placeholders_keeps_json_operators() {
2816 assert_eq!(
2817 pg_rewrite_placeholders("SELECT * FROM t WHERE doc ? 'k' AND b = ?"),
2818 "SELECT * FROM t WHERE doc ? 'k' AND b = $1"
2819 );
2820 assert_eq!(
2821 pg_rewrite_placeholders("SELECT * FROM t WHERE doc ?| array['a'] AND b = ?"),
2822 "SELECT * FROM t WHERE doc ?| array['a'] AND b = $1"
2823 );
2824 assert_eq!(
2825 pg_rewrite_placeholders("SELECT * FROM t WHERE doc ?& array['a', 'b'] AND b = ?"),
2826 "SELECT * FROM t WHERE doc ?& array['a', 'b'] AND b = $1"
2827 );
2828 assert_eq!(
2829 pg_rewrite_placeholders("SELECT * FROM t WHERE doc @? '$.a' AND b = ?"),
2830 "SELECT * FROM t WHERE doc @? '$.a' AND b = $1"
2831 );
2832 }
2833
2834 #[test]
2835 fn test_pg_rewrite_placeholders_dollar_in_literal_still_rewrites() {
2836 assert_eq!(
2838 pg_rewrite_placeholders("SELECT '$1' AS lit, ? AS val"),
2839 "SELECT '$1' AS lit, $1 AS val"
2840 );
2841 assert_eq!(
2842 pg_rewrite_placeholders("SELECT 'price: $2' AS note FROM t WHERE id = ? AND code = ?"),
2843 "SELECT 'price: $2' AS note FROM t WHERE id = $1 AND code = $2"
2844 );
2845 }
2846
2847 #[test]
2848 fn test_pg_rewrite_placeholders_skips_dollar_quoted_strings() {
2849 assert_eq!(
2851 pg_rewrite_placeholders("SELECT $$?$$ AS lit, ? AS val"),
2852 "SELECT $$?$$ AS lit, $1 AS val"
2853 );
2854 assert_eq!(
2855 pg_rewrite_placeholders("SELECT $tag$a ? b$tag$ AS lit, ? AS val"),
2856 "SELECT $tag$a ? b$tag$ AS lit, $1 AS val"
2857 );
2858 let func = "CREATE FUNCTION f() RETURNS int AS $$ BEGIN RETURN (SELECT count(*) FROM t WHERE x = ?); END; $$ LANGUAGE plpgsql";
2860 assert_eq!(pg_rewrite_placeholders(func), func);
2861 }
2862
2863 #[test]
2864 fn test_pg_rewrite_placeholders_native_skips_even_with_question() {
2865 assert_eq!(
2867 pg_rewrite_placeholders("SELECT * FROM t WHERE a = $1 AND doc ? 'k'"),
2868 "SELECT * FROM t WHERE a = $1 AND doc ? 'k'"
2869 );
2870 }
2871
2872 #[test]
2873 fn test_parse_pg_date_and_datetime() {
2874 assert!(parse_pg_date("2026-10-28").is_some());
2875 assert!(parse_pg_date("2026-10-28 12:00:00").is_some());
2876 assert!(parse_pg_date("not-a-date").is_none());
2877 assert!(parse_pg_datetime("2026-10-28 12:00:00").is_some());
2878 assert!(parse_pg_datetime("2026-10-28T12:00:00").is_some());
2879 assert!(parse_pg_datetime("2026-10-28T12:00:00Z").is_some());
2880 assert!(parse_pg_datetime("bad").is_none());
2881 }
2882
2883 #[test]
2884 fn test_needs_raw_sql_begin() {
2885 assert!(needs_raw_sql("BEGIN"));
2886 assert!(needs_raw_sql("begin"));
2887 assert!(needs_raw_sql(" BEGIN"));
2888 assert!(needs_raw_sql("\tbegin"));
2889 }
2890
2891 #[test]
2892 fn test_needs_raw_sql_commit() {
2893 assert!(needs_raw_sql("COMMIT"));
2894 assert!(needs_raw_sql("commit"));
2895 assert!(needs_raw_sql(" COMMIT WORK"));
2896 }
2897
2898 #[test]
2899 fn test_needs_raw_sql_rollback() {
2900 assert!(needs_raw_sql("ROLLBACK"));
2901 assert!(needs_raw_sql("rollback"));
2902 assert!(needs_raw_sql(" ROLLBACK TO SAVEPOINT sp1"));
2903 }
2904
2905 #[test]
2906 fn test_needs_raw_sql_savepoint() {
2907 assert!(needs_raw_sql("SAVEPOINT sp1"));
2908 assert!(needs_raw_sql("savepoint s1"));
2909 }
2910
2911 #[test]
2912 fn test_needs_raw_sql_release() {
2913 assert!(needs_raw_sql("RELEASE sp1"));
2914 assert!(needs_raw_sql("release savepoint s1"));
2915 }
2916
2917 #[test]
2918 fn test_needs_raw_sql_set() {
2919 assert!(needs_raw_sql("SET autocommit = 0"));
2920 assert!(needs_raw_sql("set names utf8"));
2921 }
2922
2923 #[test]
2924 fn test_needs_raw_sql_use() {
2925 assert!(needs_raw_sql("USE mydb"));
2926 assert!(needs_raw_sql("use mydb"));
2927 }
2928
2929 #[test]
2930 fn test_needs_raw_sql_start_transaction() {
2931 assert!(needs_raw_sql("START TRANSACTION"));
2932 assert!(needs_raw_sql("start transaction"));
2933 assert!(needs_raw_sql(" START TRANSACTION READ ONLY"));
2934 }
2935
2936 #[test]
2937 fn test_needs_raw_sql_normal_queries() {
2938 assert!(!needs_raw_sql("SELECT * FROM users"));
2939 assert!(!needs_raw_sql("INSERT INTO t VALUES (1)"));
2940 assert!(!needs_raw_sql("UPDATE t SET x = 1"));
2941 assert!(!needs_raw_sql("DELETE FROM t"));
2942 assert!(!needs_raw_sql("CREATE TABLE t (id INT)"));
2943 assert!(!needs_raw_sql("DROP TABLE t"));
2944 assert!(!needs_raw_sql("ALTER TABLE t ADD COLUMN x INT"));
2945 }
2946
2947 #[test]
2948 fn test_needs_raw_sql_edge_cases() {
2949 assert!(!needs_raw_sql(""));
2950 assert!(!needs_raw_sql(" "));
2951 assert!(!needs_raw_sql("SELECT"));
2952 assert!(needs_raw_sql("BEGINNING"));
2953 assert!(needs_raw_sql("COMMITTEE"));
2954 assert!(!needs_raw_sql("INSERT"));
2955 assert!(!needs_raw_sql("DELETE"));
2956 }
2957
2958 #[test]
2961 fn test_validate_pg_channel_name_valid() {
2962 assert!(validate_pg_channel_name("my_channel").is_ok());
2963 assert!(validate_pg_channel_name("abc123").is_ok());
2964 assert!(validate_pg_channel_name("_test").is_ok());
2965 assert!(validate_pg_channel_name("a_b_c").is_ok());
2966 }
2967
2968 #[test]
2969 fn test_validate_pg_channel_name_empty() {
2970 let err = validate_pg_channel_name("").unwrap_err();
2971 assert!(matches!(err, DbError::Internal(ref s) if s.contains("empty")));
2972 }
2973
2974 #[test]
2975 fn test_validate_pg_channel_name_invalid_chars() {
2976 let err = validate_pg_channel_name("my-channel").unwrap_err();
2977 assert!(matches!(err, DbError::Internal(ref s) if s.contains("invalid")));
2978 assert!(validate_pg_channel_name("my.channel").is_err());
2979 assert!(validate_pg_channel_name("my channel").is_err());
2980 assert!(validate_pg_channel_name("my;channel").is_err());
2981 assert!(validate_pg_channel_name("my'channel").is_err());
2982 }
2983
2984 fn closed_sqlite() -> SqlxSqliteConnection {
2987 SqlxSqliteConnection {
2988 conn: None,
2989 connected: false,
2990 in_transaction: false,
2991 }
2992 }
2993
2994 #[tokio::test]
2995 async fn test_sqlite_execute_closed() {
2996 let mut c = closed_sqlite();
2997 let result = c.execute("SELECT 1").await;
2998 assert!(result.is_err());
2999 assert!(result.unwrap_err().to_string().contains("closed"));
3000 }
3001
3002 #[tokio::test]
3003 async fn test_sqlite_query_closed() {
3004 let mut c = closed_sqlite();
3005 let result = c.query("SELECT 1").await;
3006 assert!(result.is_err());
3007 }
3008
3009 #[tokio::test]
3010 async fn test_sqlite_begin_tx_already_started() {
3011 let mut c = SqlxSqliteConnection {
3012 conn: None,
3013 connected: false,
3014 in_transaction: true,
3015 };
3016 let result = c.begin_transaction().await;
3017 assert!(result.is_err());
3018 assert!(result.unwrap_err().to_string().contains("already started"));
3019 }
3020
3021 #[tokio::test]
3022 async fn test_sqlite_begin_tx_closed() {
3023 let mut c = closed_sqlite();
3024 let result = c.begin_transaction().await;
3025 assert!(result.is_err());
3026 }
3027
3028 #[tokio::test]
3029 async fn test_sqlite_commit_no_tx() {
3030 let mut c = closed_sqlite();
3031 let result = c.commit().await;
3032 assert!(result.is_ok());
3033 }
3034
3035 #[tokio::test]
3036 async fn test_sqlite_rollback_no_tx() {
3037 let mut c = closed_sqlite();
3038 let result = c.rollback().await;
3039 assert!(result.is_ok());
3040 }
3041
3042 #[test]
3043 fn test_sqlite_is_connected_false_when_conn_none() {
3044 let c = closed_sqlite();
3045 assert!(!c.is_connected());
3046 }
3047
3048 #[test]
3049 fn test_sqlite_is_connected_false_when_connected_false() {
3050 let c = SqlxSqliteConnection {
3051 conn: None,
3052 connected: true,
3053 in_transaction: false,
3054 };
3055 assert!(!c.is_connected());
3056 }
3057
3058 #[test]
3059 fn test_sqlite_in_transaction_flag() {
3060 let c = SqlxSqliteConnection {
3061 conn: None,
3062 connected: false,
3063 in_transaction: true,
3064 };
3065 assert!(c.in_transaction());
3066 let c2 = closed_sqlite();
3067 assert!(!c2.in_transaction());
3068 }
3069
3070 #[tokio::test]
3071 async fn test_sqlite_ping_closed() {
3072 let mut c = closed_sqlite();
3073 let result = c.ping().await;
3074 assert!(!result);
3075 assert!(!c.is_connected());
3076 }
3077
3078 #[tokio::test]
3079 async fn test_sqlite_close_closed() {
3080 let mut c = closed_sqlite();
3081 let result = c.close().await;
3082 assert!(result.is_ok());
3083 assert!(!c.is_connected());
3084 assert!(!c.in_transaction());
3085 }
3086
3087 #[tokio::test]
3088 async fn test_sqlite_execute_with_params_closed() {
3089 let mut c = closed_sqlite();
3090 let result = c
3091 .execute_with_params("INSERT INTO t VALUES (?)", &[Value::I64(1)])
3092 .await;
3093 assert!(result.is_err());
3094 }
3095
3096 #[tokio::test]
3097 async fn test_sqlite_execute_with_params_empty_delegates() {
3098 let mut c = closed_sqlite();
3099 let result = c.execute_with_params("INSERT INTO t VALUES (1)", &[]).await;
3100 assert!(result.is_err());
3101 }
3102
3103 #[tokio::test]
3104 async fn test_sqlite_query_with_params_empty_delegates() {
3105 let mut c = closed_sqlite();
3106 let result = c.query_with_params("SELECT * FROM t", &[]).await;
3107 assert!(result.is_err());
3108 }
3109
3110 #[tokio::test]
3111 async fn test_sqlite_query_with_params_closed() {
3112 let mut c = closed_sqlite();
3113 let result = c
3114 .query_with_params("SELECT * FROM t WHERE id = ?", &[Value::I64(1)])
3115 .await;
3116 assert!(result.is_err());
3117 }
3118
3119 fn closed_mysql() -> SqlxMySqlConnection {
3122 SqlxMySqlConnection {
3123 conn: None,
3124 connected: false,
3125 in_transaction: false,
3126 session_dirty: false,
3127 }
3128 }
3129
3130 #[tokio::test]
3131 async fn test_mysql_execute_closed() {
3132 let mut c = closed_mysql();
3133 assert!(c.execute("SELECT 1").await.is_err());
3134 }
3135
3136 #[tokio::test]
3137 async fn test_mysql_query_closed() {
3138 let mut c = closed_mysql();
3139 assert!(c.query("SELECT 1").await.is_err());
3140 }
3141
3142 #[test]
3143 fn test_mysql_is_connected_false() {
3144 let c = closed_mysql();
3145 assert!(!c.is_connected());
3146 }
3147
3148 #[tokio::test]
3149 async fn test_mysql_close_closed() {
3150 let mut c = closed_mysql();
3151 assert!(c.close().await.is_ok());
3152 assert!(!c.is_connected());
3153 }
3154
3155 #[tokio::test]
3156 async fn test_mysql_commit_no_tx() {
3157 let mut c = closed_mysql();
3158 assert!(c.commit().await.is_ok());
3159 }
3160
3161 #[tokio::test]
3162 async fn test_mysql_rollback_no_tx() {
3163 let mut c = closed_mysql();
3164 assert!(c.rollback().await.is_ok());
3165 }
3166
3167 #[tokio::test]
3168 async fn test_mysql_begin_tx_already_started() {
3169 let mut c = SqlxMySqlConnection {
3170 conn: None,
3171 connected: false,
3172 in_transaction: true,
3173 session_dirty: false,
3174 };
3175 let result = c.begin_transaction().await;
3176 assert!(result.is_err());
3177 }
3178
3179 #[tokio::test]
3180 async fn test_mysql_ping_closed() {
3181 let mut c = closed_mysql();
3182 assert!(!c.ping().await);
3183 }
3184
3185 fn closed_pg() -> SqlxPgConnection {
3188 SqlxPgConnection {
3189 conn: None,
3190 connected: false,
3191 in_transaction: false,
3192 }
3193 }
3194
3195 #[tokio::test]
3196 async fn test_pg_execute_closed() {
3197 let mut c = closed_pg();
3198 assert!(c.execute("SELECT 1").await.is_err());
3199 }
3200
3201 #[tokio::test]
3202 async fn test_pg_query_closed() {
3203 let mut c = closed_pg();
3204 assert!(c.query("SELECT 1").await.is_err());
3205 }
3206
3207 #[test]
3208 fn test_pg_is_connected_false() {
3209 let c = closed_pg();
3210 assert!(!c.is_connected());
3211 }
3212
3213 #[tokio::test]
3214 async fn test_pg_close_closed() {
3215 let mut c = closed_pg();
3216 assert!(c.close().await.is_ok());
3217 assert!(!c.is_connected());
3218 }
3219
3220 #[tokio::test]
3221 async fn test_pg_commit_no_tx() {
3222 let mut c = closed_pg();
3223 assert!(c.commit().await.is_ok());
3224 }
3225
3226 #[tokio::test]
3227 async fn test_pg_rollback_no_tx() {
3228 let mut c = closed_pg();
3229 assert!(c.rollback().await.is_ok());
3230 }
3231
3232 #[tokio::test]
3233 async fn test_pg_begin_tx_already_started() {
3234 let mut c = SqlxPgConnection {
3235 conn: None,
3236 connected: false,
3237 in_transaction: true,
3238 };
3239 assert!(c.begin_transaction().await.is_err());
3240 }
3241
3242 #[tokio::test]
3243 async fn test_pg_ping_closed() {
3244 let mut c = closed_pg();
3245 assert!(!c.ping().await);
3246 }
3247
3248 #[tokio::test]
3251 async fn test_mysql_execute_with_params_closed() {
3252 let mut c = closed_mysql();
3253 assert!(c
3254 .execute_with_params("INSERT INTO t VALUES (?)", &[Value::I64(1)])
3255 .await
3256 .is_err());
3257 }
3258
3259 #[tokio::test]
3260 async fn test_mysql_execute_with_params_empty_delegates() {
3261 let mut c = closed_mysql();
3262 assert!(c
3263 .execute_with_params("INSERT INTO t VALUES (1)", &[])
3264 .await
3265 .is_err());
3266 }
3267
3268 #[tokio::test]
3269 async fn test_mysql_query_with_params_closed() {
3270 let mut c = closed_mysql();
3271 assert!(c
3272 .query_with_params("SELECT * FROM t WHERE id = ?", &[Value::I64(1)])
3273 .await
3274 .is_err());
3275 }
3276
3277 #[tokio::test]
3278 async fn test_mysql_query_with_params_empty_delegates() {
3279 let mut c = closed_mysql();
3280 assert!(c.query_with_params("SELECT * FROM t", &[]).await.is_err());
3281 }
3282
3283 #[tokio::test]
3284 async fn test_mysql_query_values_closed() {
3285 let mut c = closed_mysql();
3286 assert!(c.query_values("SELECT * FROM t").await.is_err());
3287 }
3288
3289 #[tokio::test]
3290 async fn test_mysql_query_values_with_params_closed() {
3291 let mut c = closed_mysql();
3292 assert!(c
3293 .query_values_with_params("SELECT * FROM t WHERE id = ?", &[Value::I64(1)])
3294 .await
3295 .is_err());
3296 }
3297
3298 #[tokio::test]
3299 async fn test_mysql_query_values_with_params_empty_delegates() {
3300 let mut c = closed_mysql();
3301 assert!(c
3302 .query_values_with_params("SELECT * FROM t", &[])
3303 .await
3304 .is_err());
3305 }
3306
3307 #[tokio::test]
3310 async fn test_pg_execute_with_params_closed() {
3311 let mut c = closed_pg();
3312 assert!(c
3313 .execute_with_params("INSERT INTO t VALUES ($1)", &[Value::I64(1)])
3314 .await
3315 .is_err());
3316 }
3317
3318 #[tokio::test]
3319 async fn test_pg_execute_with_params_empty_delegates() {
3320 let mut c = closed_pg();
3321 assert!(c
3322 .execute_with_params("INSERT INTO t VALUES (1)", &[])
3323 .await
3324 .is_err());
3325 }
3326
3327 #[tokio::test]
3328 async fn test_pg_query_with_params_closed() {
3329 let mut c = closed_pg();
3330 assert!(c
3331 .query_with_params("SELECT * FROM t WHERE id = $1", &[Value::I64(1)])
3332 .await
3333 .is_err());
3334 }
3335
3336 #[tokio::test]
3337 async fn test_pg_query_with_params_empty_delegates() {
3338 let mut c = closed_pg();
3339 assert!(c.query_with_params("SELECT * FROM t", &[]).await.is_err());
3340 }
3341
3342 #[tokio::test]
3343 async fn test_pg_query_values_closed() {
3344 let mut c = closed_pg();
3345 assert!(c.query_values("SELECT * FROM t").await.is_err());
3346 }
3347
3348 #[tokio::test]
3349 async fn test_pg_query_values_with_params_closed() {
3350 let mut c = closed_pg();
3351 assert!(c
3352 .query_values_with_params("SELECT * FROM t WHERE id = $1", &[Value::I64(1)])
3353 .await
3354 .is_err());
3355 }
3356
3357 #[tokio::test]
3358 async fn test_pg_query_values_with_params_empty_delegates() {
3359 let mut c = closed_pg();
3360 assert!(c
3361 .query_values_with_params("SELECT * FROM t", &[])
3362 .await
3363 .is_err());
3364 }
3365
3366 #[tokio::test]
3369 async fn test_sqlite_backup_closed_conn() {
3370 let mut c = closed_sqlite();
3371 let result = sqlite_backup(&mut c, "/tmp/backup.db").await;
3372 assert!(result.is_err());
3373 }
3374
3375 #[tokio::test]
3378 async fn test_sqlite_query_values_closed() {
3379 let mut c = closed_sqlite();
3380 assert!(c.query_values("SELECT * FROM t").await.is_err());
3381 }
3382
3383 #[tokio::test]
3384 async fn test_sqlite_query_values_with_params_closed() {
3385 let mut c = closed_sqlite();
3386 assert!(c
3387 .query_values_with_params("SELECT * FROM t WHERE id = ?", &[Value::I64(1)])
3388 .await
3389 .is_err());
3390 }
3391
3392 #[tokio::test]
3393 async fn test_sqlite_query_values_with_params_empty_delegates() {
3394 let mut c = closed_sqlite();
3395 assert!(c
3396 .query_values_with_params("SELECT * FROM t", &[])
3397 .await
3398 .is_err());
3399 }
3400
3401 #[tokio::test]
3404 async fn test_mysql_bulk_insert_empty_rows() {
3405 let mut c = closed_mysql();
3406 let result = mysql_bulk_insert(&mut c, "t", &["a", "b"], &[]).await;
3407 assert_eq!(result.unwrap(), 0);
3408 }
3409
3410 #[tokio::test]
3411 async fn test_mysql_bulk_insert_closed_conn() {
3412 let mut c = closed_mysql();
3413 let rows = vec![vec![Value::I64(1), Value::String("x".to_string())]];
3414 let result = mysql_bulk_insert(&mut c, "t", &["a", "b"], &rows).await;
3415 assert!(result.is_err());
3416 }
3417
3418 #[tokio::test]
3419 async fn test_pg_bulk_insert_empty_rows() {
3420 let mut c = closed_pg();
3421 let result = pg_bulk_insert(&mut c, "t", &["a", "b"], &[]).await;
3422 assert_eq!(result.unwrap(), 0);
3423 }
3424
3425 #[tokio::test]
3426 async fn test_pg_bulk_insert_closed_conn() {
3427 let mut c = closed_pg();
3428 let rows = vec![vec![Value::I64(1), Value::String("x".to_string())]];
3429 let result = pg_bulk_insert(&mut c, "t", &["a", "b"], &rows).await;
3430 assert!(result.is_err());
3431 }
3432
3433 #[test]
3436 fn test_parse_pg_datetime_naive() {
3437 match parse_pg_datetime("2026-10-28 13:14:15.123456") {
3438 Some(PgDateTime::Naive(dt)) => {
3439 assert_eq!(
3440 dt.format("%Y-%m-%d %H:%M:%S%.6f").to_string(),
3441 "2026-10-28 13:14:15.123456"
3442 );
3443 }
3444 _ => panic!("无偏移输入必须解析为 Naive"),
3445 }
3446 }
3447
3448 #[test]
3449 fn test_parse_pg_datetime_zoned_forms() {
3450 for s in [
3452 "2026-10-28T13:14:15+08:00",
3453 "2026-10-28 13:14:15+08:00",
3454 "2026-10-28 13:14:15 +0800",
3455 "2026-10-28T05:14:15Z",
3456 ] {
3457 match parse_pg_datetime(s) {
3458 Some(PgDateTime::Zoned(dt)) => {
3459 assert_eq!(
3461 dt.format("%Y-%m-%d %H:%M:%S").to_string(),
3462 "2026-10-28 05:14:15",
3463 "输入 {s}"
3464 );
3465 }
3466 other => panic!("{s} 必须解析为 Zoned,实际 {:?}", other.is_some()),
3467 }
3468 }
3469 }
3470
3471 #[test]
3472 fn test_parse_pg_time_fractional() {
3473 assert!(parse_pg_time("13:14:15").is_some());
3474 assert!(parse_pg_time("13:14:15.123456").is_some());
3475 assert!(parse_pg_time("13:14").is_none());
3476 }
3477
3478 #[test]
3481 fn test_bigdecimal_38_digits_no_truncation() {
3482 let s = "1234567890123456789012345678.9012345678"; let d: bigdecimal::BigDecimal = s.parse().unwrap();
3484 assert_eq!(d.to_string(), s, "BigDecimal 往返必须逐位一致");
3485 assert!(
3487 s.parse::<rust_decimal::Decimal>().is_err() || {
3488 let rd: rust_decimal::Decimal = s.parse().unwrap();
3489 rd.to_string() != s
3490 }
3491 );
3492 }
3493}
3494
3495#[cfg(test)]
3496mod session_dirty_tests {
3497 use super::marks_session_dirty;
3498
3499 #[test]
3500 fn set_statements_mark_dirty() {
3501 assert!(marks_session_dirty("SET SESSION sql_mode = DEFAULT"));
3502 assert!(marks_session_dirty("set names utf8mb4"));
3503 assert!(marks_session_dirty("SET autocommit = 0"));
3504 assert!(marks_session_dirty("SET @my_var = 1"));
3505 assert!(marks_session_dirty(" set\tsql_mode='x'"));
3506 }
3507
3508 #[test]
3509 fn use_marks_dirty() {
3510 assert!(marks_session_dirty("USE sz_orm_test"));
3511 assert!(marks_session_dirty("use other_db;"));
3512 }
3513
3514 #[test]
3515 fn set_transaction_not_dirty() {
3516 assert!(!marks_session_dirty(
3517 "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ"
3518 ));
3519 assert!(!marks_session_dirty("set transaction read only"));
3520 assert!(!marks_session_dirty("SET TRANSACTION;"));
3521 }
3522
3523 #[test]
3524 fn set_transaction_identifier_still_dirty() {
3525 assert!(marks_session_dirty("SET TRANSACTIONX = 1"));
3527 }
3528
3529 #[test]
3530 fn read_and_tx_statements_not_dirty() {
3531 assert!(!marks_session_dirty("SELECT 1"));
3532 assert!(!marks_session_dirty("select * from t where a=1"));
3533 assert!(!marks_session_dirty("SHOW TABLES"));
3534 assert!(!marks_session_dirty("EXPLAIN SELECT 1"));
3535 assert!(!marks_session_dirty("START TRANSACTION"));
3536 assert!(!marks_session_dirty("BEGIN"));
3537 assert!(!marks_session_dirty("COMMIT"));
3538 assert!(!marks_session_dirty("ROLLBACK"));
3539 assert!(!marks_session_dirty("INSERT INTO t VALUES (1)"));
3540 assert!(!marks_session_dirty("UPDATE t SET a = 1 WHERE id = 1"));
3541 assert!(!marks_session_dirty("DELETE FROM t WHERE id = 1"));
3542 }
3543
3544 #[test]
3545 fn word_boundary_no_false_positive() {
3546 assert!(!marks_session_dirty("SETUP_CHECK = 1"));
3548 assert!(!marks_session_dirty("SELECT * FROM USER"));
3550 }
3551}