1pub use chopin_orm_macro::Model;
7pub use chopin_pg::{
8 PgResult, Row, connection::PgConnection, error::PgError, pool::PgPool, types::PgValue,
9 types::ToSql,
10};
11
12pub mod builder;
13pub use builder::{Condition, QueryBuilder};
14pub mod error;
15pub use error::{OrmError, OrmResult};
16pub mod active_model;
17pub use active_model::ActiveModel;
18pub mod migrations;
19pub use migrations::{Index, Migration, MigrationManager, MigrationStatus};
20pub mod mock;
21pub use mock::MockExecutor;
22
23pub trait Executor {
27 fn execute(&mut self, query: &str, params: &[&dyn chopin_pg::types::ToSql]) -> OrmResult<u64>;
29
30 fn query(
32 &mut self,
33 query: &str,
34 params: &[&dyn chopin_pg::types::ToSql],
35 ) -> OrmResult<Vec<Row>>;
36}
37
38impl Executor for PgPool {
39 fn execute(&mut self, query: &str, params: &[&dyn chopin_pg::types::ToSql]) -> OrmResult<u64> {
40 self.get()
41 .map_err(OrmError::from)?
42 .execute(query, params)
43 .map_err(OrmError::from)
44 }
45
46 fn query(
47 &mut self,
48 query: &str,
49 params: &[&dyn chopin_pg::types::ToSql],
50 ) -> OrmResult<Vec<Row>> {
51 self.get()
52 .map_err(OrmError::from)?
53 .query(query, params)
54 .map_err(OrmError::from)
55 }
56}
57
58impl Executor for PgConnection {
59 fn execute(&mut self, query: &str, params: &[&dyn chopin_pg::types::ToSql]) -> OrmResult<u64> {
60 chopin_pg::connection::PgConnection::execute(self, query, params).map_err(OrmError::from)
61 }
62
63 fn query(
64 &mut self,
65 query: &str,
66 params: &[&dyn chopin_pg::types::ToSql],
67 ) -> OrmResult<Vec<Row>> {
68 chopin_pg::connection::PgConnection::query(self, query, params).map_err(OrmError::from)
69 }
70}
71
72pub struct Transaction<'a> {
78 conn: &'a mut PgConnection,
79 committed: bool,
80}
81
82impl<'a> Transaction<'a> {
83 pub fn begin(conn: &'a mut PgConnection) -> OrmResult<Self> {
85 conn.execute("BEGIN", &[]).map_err(OrmError::from)?;
86 Ok(Self {
87 conn,
88 committed: false,
89 })
90 }
91
92 pub fn commit(mut self) -> OrmResult<()> {
94 self.committed = true;
95 self.conn.execute("COMMIT", &[]).map_err(OrmError::from)?;
96 Ok(())
97 }
98
99 pub fn rollback(mut self) -> OrmResult<()> {
101 self.committed = true; self.conn.execute("ROLLBACK", &[]).map_err(OrmError::from)?;
103 Ok(())
104 }
105}
106
107impl<'a> Drop for Transaction<'a> {
108 fn drop(&mut self) {
109 if !self.committed {
110 let _ = self.conn.execute("ROLLBACK", &[]);
113 #[cfg(feature = "log")]
114 log::warn!("Transaction dropped without explicit commit — rolled back");
115 }
116 }
117}
118
119impl<'a> Executor for Transaction<'a> {
120 fn execute(&mut self, query: &str, params: &[&dyn chopin_pg::types::ToSql]) -> OrmResult<u64> {
121 self.conn.execute(query, params).map_err(OrmError::from)
122 }
123
124 fn query(
125 &mut self,
126 query: &str,
127 params: &[&dyn chopin_pg::types::ToSql],
128 ) -> OrmResult<Vec<Row>> {
129 self.conn.query(query, params).map_err(OrmError::from)
130 }
131}
132
133pub trait Validate {
134 fn validate(&self) -> Result<(), Vec<String>> {
135 Ok(()) }
137}
138
139pub trait Model: FromRow + Validate + Sized + Send + Sync {
140 fn table_name() -> &'static str;
141 fn primary_key_columns() -> &'static [&'static str];
142 fn generated_columns() -> &'static [&'static str];
143 fn columns() -> &'static [&'static str];
144 fn select_clause() -> &'static str;
145
146 fn primary_key_values(&self) -> Vec<PgValue>;
147 fn set_generated_values(&mut self, values: Vec<PgValue>) -> OrmResult<()>;
148 fn get_values(&self) -> Vec<PgValue>;
149
150 fn create_table_stmt() -> String;
152
153 fn column_definitions() -> Vec<(&'static str, &'static str)>;
155
156 fn indexes() -> Vec<Index> {
158 vec![]
159 }
160
161 fn create_table(executor: &mut impl Executor) -> OrmResult<()> {
163 executor.execute(&Self::create_table_stmt(), &[])?;
164 Ok(())
165 }
166
167 fn find() -> QueryBuilder<Self> {
169 QueryBuilder::new()
170 }
171
172 fn sync_schema(executor: &mut impl Executor) -> OrmResult<()> {
174 Self::create_table(executor)?;
175
176 let db_cols_query =
178 "SELECT column_name FROM information_schema.columns WHERE table_name = $1";
179 let table_name = Self::table_name();
180 let params: Vec<&dyn chopin_pg::types::ToSql> = vec![&table_name];
181 let rows = executor.query(db_cols_query, ¶ms)?;
182
183 let mut existing_cols = Vec::new();
184 for row in rows {
185 if let Ok(chopin_pg::PgValue::Text(val)) = row.get(0) {
186 existing_cols.push(val.clone());
187 }
188 }
189
190 let definitions = Self::column_definitions();
191 for (col_name, col_def) in definitions {
192 if !existing_cols.contains(&col_name.to_string()) {
193 let alter_stmt = format!(
194 "ALTER TABLE {} ADD COLUMN {} {}",
195 Self::table_name(),
196 col_name,
197 col_def
198 );
199 executor.execute(&alter_stmt, &[])?;
200 #[cfg(feature = "log")]
201 log::info!(
202 "Auto-migrated {}: added column {}",
203 Self::table_name(),
204 col_name
205 );
206 }
207 }
208
209 for idx in Self::indexes() {
210 let unique = if idx.unique { "UNIQUE " } else { "" };
211 let create_idx = format!(
212 "CREATE {}INDEX IF NOT EXISTS {} ON {} ({})",
213 unique,
214 idx.name,
215 Self::table_name(),
216 idx.columns.join(", ")
217 );
218 executor.execute(&create_idx, &[])?;
219 }
220
221 Ok(())
222 }
223
224 fn insert(&mut self, executor: &mut impl Executor) -> OrmResult<()> {
226 if let Err(errors) = self.validate() {
227 return Err(OrmError::Validation(errors));
228 }
229 let all_cols = Self::columns();
230 let gen_cols = Self::generated_columns();
231
232 let mut cols = Vec::new();
233 let values = self.get_values();
234 let mut final_values = Vec::new();
235
236 for (i, col) in all_cols.iter().enumerate() {
237 if !gen_cols.contains(col) {
238 cols.push(*col);
239 final_values.push(values[i].clone());
240 }
241 }
242
243 let bindings: Vec<String> = (1..=cols.len()).map(|i| format!("${}", i)).collect();
244 let returning = if gen_cols.is_empty() {
245 "".to_string()
246 } else {
247 format!(" RETURNING {}", gen_cols.join(", "))
248 };
249
250 let query = format!(
251 "INSERT INTO {} ({}) VALUES ({}){}",
252 Self::table_name(),
253 cols.join(", "),
254 bindings.join(", "),
255 returning
256 );
257
258 let params: Vec<&dyn chopin_pg::types::ToSql> =
259 final_values.iter().map(|v| v as _).collect();
260
261 if gen_cols.is_empty() {
262 executor.execute(&query, ¶ms)?;
263 } else {
264 let rows = executor.query(&query, ¶ms)?;
265 if let Some(row) = rows.first() {
266 let mut returned_vals = Vec::new();
267 for i in 0..gen_cols.len() {
268 returned_vals.push(row.get(i)?);
269 }
270 self.set_generated_values(returned_vals)?;
271 }
272 }
273 Ok(())
274 }
275
276 fn upsert(&mut self, executor: &mut impl Executor) -> OrmResult<()> {
278 if let Err(errors) = self.validate() {
279 return Err(OrmError::Validation(errors));
280 }
281 let all_cols = Self::columns();
282 let pk_cols = Self::primary_key_columns();
283 let gen_cols = Self::generated_columns();
284
285 if pk_cols.is_empty() {
286 return Err(OrmError::ModelError(
287 "Cannot upsert without primary keys".to_string(),
288 ));
289 }
290
291 let mut cols = Vec::new();
292 let values = self.get_values();
293 let mut final_values = Vec::new();
294 let mut set_clauses = Vec::new();
295
296 for (i, col) in all_cols.iter().enumerate() {
297 cols.push(*col);
298 final_values.push(values[i].clone());
299 if !pk_cols.contains(col) {
300 set_clauses.push(format!("{0} = EXCLUDED.{0}", col));
301 }
302 }
303
304 let bindings: Vec<String> = (1..=cols.len()).map(|i| format!("${}", i)).collect();
305
306 let on_conflict = if set_clauses.is_empty() {
308 "DO NOTHING".to_string()
309 } else {
310 format!("DO UPDATE SET {}", set_clauses.join(", "))
311 };
312
313 let returning = if gen_cols.is_empty() {
314 "".to_string()
315 } else {
316 format!(" RETURNING {}", gen_cols.join(", "))
317 };
318
319 let query = format!(
320 "INSERT INTO {0} ({1}) VALUES ({2}) ON CONFLICT ({3}) {4}{5}",
321 Self::table_name(),
322 cols.join(", "),
323 bindings.join(", "),
324 pk_cols.join(", "),
325 on_conflict,
326 returning
327 );
328
329 let params: Vec<&dyn chopin_pg::types::ToSql> =
330 final_values.iter().map(|v| v as _).collect();
331
332 if gen_cols.is_empty() {
333 executor.execute(&query, ¶ms)?;
334 } else {
335 let rows = executor.query(&query, ¶ms)?;
336 if let Some(row) = rows.first() {
337 let mut returned_vals = Vec::new();
338 for i in 0..gen_cols.len() {
339 returned_vals.push(row.get(i)?);
340 }
341 self.set_generated_values(returned_vals)?;
342 }
343 }
344 Ok(())
345 }
346
347 fn update_columns(
349 &self,
350 executor: &mut impl Executor,
351 update_columns: &[&str],
352 ) -> OrmResult<Self> {
353 if let Err(errors) = self.validate() {
354 return Err(OrmError::Validation(errors));
355 }
356 let all_columns = Self::columns();
357 let all_values = self.get_values();
358
359 let mut set_clauses = Vec::new();
360 let mut query_values = Vec::new();
361 let mut param_idx = 1;
362
363 for col in update_columns {
364 if let Some(pos) = all_columns.iter().position(|c| c == col) {
365 set_clauses.push(format!("{} = ${}", col, param_idx));
366 query_values.push(all_values[pos].clone());
367 param_idx += 1;
368 } else {
369 return Err(OrmError::ModelError(format!("Column not found: {}", col)));
370 }
371 }
372
373 if set_clauses.is_empty() {
374 return Err(OrmError::ModelError(
375 "No valid columns provided for partial update".into(),
376 ));
377 }
378
379 let pk_cols = Self::primary_key_columns();
381 let pk_vals = self.primary_key_values();
382
383 let mut where_clauses = Vec::new();
384 for (i, pk_col) in pk_cols.iter().enumerate() {
385 where_clauses.push(format!("{} = ${}", pk_col, param_idx));
386 query_values.push(pk_vals[i].clone());
387 param_idx += 1;
388 }
389
390 let query = format!(
391 "UPDATE {} SET {} WHERE {} RETURNING {}",
392 Self::table_name(),
393 set_clauses.join(", "),
394 where_clauses.join(" AND "),
395 Self::columns().join(", ")
396 );
397
398 let params_ref: Vec<&dyn chopin_pg::types::ToSql> =
399 query_values.iter().map(|v| v as _).collect();
400 let rows = executor.query(&query, ¶ms_ref)?;
401
402 if let Some(row) = rows.first() {
403 Self::from_row(row)
404 } else {
405 Err(OrmError::ModelError(
406 "Update failed, no rows returned".into(),
407 ))
408 }
409 }
410
411 fn update(&self, executor: &mut impl Executor) -> OrmResult<()> {
413 if let Err(errors) = self.validate() {
414 return Err(OrmError::Validation(errors));
415 }
416 let cols = Self::columns();
417 let pk_cols = Self::primary_key_columns();
418
419 if pk_cols.is_empty() {
420 return Err(OrmError::ModelError(
421 "Cannot update without primary keys".to_string(),
422 ));
423 }
424
425 let mut set_clauses = Vec::new();
426 let mut param_idx = 1;
427 let values = self.get_values();
428 let mut query_values = Vec::new();
429
430 for (i, col) in cols.iter().enumerate() {
431 if !pk_cols.contains(col) {
432 set_clauses.push(format!("{} = ${}", col, param_idx));
433 query_values.push(values[i].clone());
434 param_idx += 1;
435 }
436 }
437
438 if set_clauses.is_empty() {
439 return Ok(()); }
441
442 let mut where_clauses = Vec::new();
443 let pk_values = self.primary_key_values();
444 for (i, pk_col) in pk_cols.iter().enumerate() {
445 where_clauses.push(format!("{} = ${}", pk_col, param_idx));
446 query_values.push(pk_values[i].clone());
447 param_idx += 1;
448 }
449
450 let query = format!(
451 "UPDATE {} SET {} WHERE {}",
452 Self::table_name(),
453 set_clauses.join(", "),
454 where_clauses.join(" AND ")
455 );
456
457 let params: Vec<&dyn chopin_pg::types::ToSql> =
458 query_values.iter().map(|v| v as _).collect();
459 executor.execute(&query, ¶ms)?;
460 Ok(())
461 }
462
463 fn delete(&self, executor: &mut impl Executor) -> OrmResult<()> {
465 let pk_cols = Self::primary_key_columns();
466 if pk_cols.is_empty() {
467 return Err(OrmError::ModelError(
468 "Cannot delete without primary keys".to_string(),
469 ));
470 }
471
472 let mut where_clauses = Vec::new();
473 for (idx, pk_col) in (1..).zip(pk_cols.iter()) {
474 where_clauses.push(format!("{} = ${}", pk_col, idx));
475 }
476
477 let query = format!(
478 "DELETE FROM {} WHERE {}",
479 Self::table_name(),
480 where_clauses.join(" AND ")
481 );
482
483 let pk_values = self.primary_key_values();
484 let params: Vec<&dyn chopin_pg::types::ToSql> = pk_values.iter().map(|v| v as _).collect();
485
486 executor.execute(&query, ¶ms)?;
487 Ok(())
488 }
489}
490
491pub trait FromRow: Sized {
492 fn from_row(row: &Row) -> OrmResult<Self>;
493}
494
495pub trait ExtractValue: Sized {
496 fn extract(row: &Row, col: &str) -> OrmResult<Self>;
497 fn extract_at(row: &Row, index: usize) -> OrmResult<Self>;
498 fn from_pg_value(val: PgValue) -> OrmResult<Self>;
499}
500
501impl ExtractValue for String {
503 fn extract(row: &Row, col: &str) -> OrmResult<Self> {
504 let val = row.get_by_name(col).map_err(OrmError::from)?;
505 Self::from_pg_value(val)
506 }
507 fn extract_at(row: &Row, index: usize) -> OrmResult<Self> {
508 let val = row.get(index).map_err(OrmError::from)?;
509 Self::from_pg_value(val)
510 }
511 fn from_pg_value(val: PgValue) -> OrmResult<Self> {
512 match val {
513 PgValue::Text(s) => Ok(s),
514 _ => Err(OrmError::Extraction("Expected Text".into())),
515 }
516 }
517}
518
519impl ExtractValue for i32 {
520 fn extract(row: &Row, col: &str) -> OrmResult<Self> {
521 let val = row.get_by_name(col).map_err(OrmError::from)?;
522 Self::from_pg_value(val)
523 }
524 fn extract_at(row: &Row, index: usize) -> OrmResult<Self> {
525 let val = row.get(index).map_err(OrmError::from)?;
526 Self::from_pg_value(val)
527 }
528 fn from_pg_value(val: PgValue) -> OrmResult<Self> {
529 match val {
530 PgValue::Int4(v) => Ok(v),
531 PgValue::Int2(v) => Ok(v as i32),
532 PgValue::Text(s) => s
533 .parse()
534 .map_err(|_| OrmError::Extraction("Not an i32".into())),
535 _ => Err(OrmError::Extraction("Expected Int4".into())),
536 }
537 }
538}
539
540impl ExtractValue for i64 {
541 fn extract(row: &Row, col: &str) -> OrmResult<Self> {
542 let val = row.get_by_name(col).map_err(OrmError::from)?;
543 Self::from_pg_value(val)
544 }
545 fn extract_at(row: &Row, index: usize) -> OrmResult<Self> {
546 let val = row.get(index).map_err(OrmError::from)?;
547 Self::from_pg_value(val)
548 }
549 fn from_pg_value(val: PgValue) -> OrmResult<Self> {
550 match val {
551 PgValue::Int8(v) => Ok(v),
552 PgValue::Int4(v) => Ok(v as i64),
553 PgValue::Int2(v) => Ok(v as i64),
554 PgValue::Text(s) => s
555 .parse()
556 .map_err(|_| OrmError::Extraction("Not an i64".into())),
557 _ => Err(OrmError::Extraction("Expected Int8".into())),
558 }
559 }
560}
561
562impl ExtractValue for bool {
563 fn extract(row: &Row, col: &str) -> OrmResult<Self> {
564 let val = row.get_by_name(col).map_err(OrmError::from)?;
565 Self::from_pg_value(val)
566 }
567 fn extract_at(row: &Row, index: usize) -> OrmResult<Self> {
568 let val = row.get(index).map_err(OrmError::from)?;
569 Self::from_pg_value(val)
570 }
571 fn from_pg_value(val: PgValue) -> OrmResult<Self> {
572 match val {
573 PgValue::Bool(v) => Ok(v),
574 PgValue::Text(s) => Ok(s == "t" || s == "true" || s == "1"),
575 _ => Err(OrmError::Extraction("Expected Bool".into())),
576 }
577 }
578}
579
580impl ExtractValue for f64 {
581 fn extract(row: &Row, col: &str) -> OrmResult<Self> {
582 let val = row.get_by_name(col).map_err(OrmError::from)?;
583 Self::from_pg_value(val)
584 }
585 fn extract_at(row: &Row, index: usize) -> OrmResult<Self> {
586 let val = row.get(index).map_err(OrmError::from)?;
587 Self::from_pg_value(val)
588 }
589 fn from_pg_value(val: PgValue) -> OrmResult<Self> {
590 match val {
591 PgValue::Float8(v) => Ok(v),
592 PgValue::Float4(v) => Ok(v as f64),
593 PgValue::Text(s) => s
594 .parse()
595 .map_err(|_| OrmError::Extraction("Not an f64".into())),
596 _ => Err(OrmError::Extraction("Expected Float8".into())),
597 }
598 }
599}
600
601impl<T: ExtractValue> ExtractValue for Option<T> {
603 fn extract(row: &Row, col: &str) -> OrmResult<Self> {
604 let val = row.get_by_name(col).map_err(OrmError::from)?;
605 if let PgValue::Null = val {
606 return Ok(None);
607 }
608 T::from_pg_value(val).map(Some)
609 }
610 fn extract_at(row: &Row, index: usize) -> OrmResult<Self> {
611 let val = row.get(index).map_err(OrmError::from)?;
612 if let PgValue::Null = val {
613 return Ok(None);
614 }
615 T::from_pg_value(val).map(Some)
616 }
617 fn from_pg_value(val: PgValue) -> OrmResult<Self> {
618 if let PgValue::Null = val {
619 return Ok(None);
620 }
621 T::from_pg_value(val).map(Some)
622 }
623}
624
625impl ExtractValue for f32 {
628 fn extract(row: &Row, col: &str) -> OrmResult<Self> {
629 let val = row.get_by_name(col).map_err(OrmError::from)?;
630 Self::from_pg_value(val)
631 }
632 fn extract_at(row: &Row, index: usize) -> OrmResult<Self> {
633 let val = row.get(index).map_err(OrmError::from)?;
634 Self::from_pg_value(val)
635 }
636 fn from_pg_value(val: PgValue) -> OrmResult<Self> {
637 match val {
638 PgValue::Float4(v) => Ok(v),
639 PgValue::Float8(v) => Ok(v as f32),
640 PgValue::Int4(n) => Ok(n as f32),
641 PgValue::Int8(n) => Ok(n as f32),
642 PgValue::Int2(n) => Ok(n as f32),
643 PgValue::Text(s) => s
644 .parse()
645 .map_err(|_| OrmError::Extraction(format!("Cannot parse '{}' as f32", s))),
646 _ => Err(OrmError::Extraction("Expected Float4".into())),
647 }
648 }
649}
650
651#[cfg(feature = "chrono")]
654const PG_EPOCH_OFFSET_SECS: i64 = 946_684_800;
655
656#[cfg(feature = "chrono")]
657impl ExtractValue for chrono::NaiveDateTime {
658 fn extract(row: &Row, col: &str) -> OrmResult<Self> {
659 let val = row
660 .get_by_name(col)
661 .map_err(|e| OrmError::Extraction(format!("column '{}': {}", col, e)))?;
662 Self::from_pg_value(val)
663 }
664 fn extract_at(row: &Row, index: usize) -> OrmResult<Self> {
665 let val = row
666 .get(index)
667 .map_err(|e| OrmError::Extraction(format!("index {}: {}", index, e)))?;
668 Self::from_pg_value(val)
669 }
670 fn from_pg_value(val: PgValue) -> OrmResult<Self> {
671 match val {
672 PgValue::Timestamp(micros) | PgValue::Timestamptz(micros) => {
673 let unix_micros = micros + PG_EPOCH_OFFSET_SECS * 1_000_000;
674 let secs = unix_micros.div_euclid(1_000_000);
675 let nsecs = (unix_micros.rem_euclid(1_000_000) * 1_000) as u32;
676 chrono::DateTime::from_timestamp(secs, nsecs)
677 .map(|dt| dt.naive_utc())
678 .ok_or_else(|| {
679 OrmError::Extraction(format!("Invalid timestamp microseconds: {}", micros))
680 })
681 }
682 PgValue::Text(s) | PgValue::Json(s) => {
683 chrono::NaiveDateTime::parse_from_str(&s, "%Y-%m-%d %H:%M:%S%.f")
684 .or_else(|_| chrono::NaiveDateTime::parse_from_str(&s, "%Y-%m-%dT%H:%M:%S%.f"))
685 .or_else(|_| chrono::NaiveDateTime::parse_from_str(&s, "%Y-%m-%d %H:%M:%S"))
686 .map_err(|e| {
687 OrmError::Extraction(format!(
688 "Cannot parse '{}' as NaiveDateTime: {}",
689 s, e
690 ))
691 })
692 }
693 PgValue::Null => Err(OrmError::Extraction(
694 "Cannot extract NaiveDateTime from NULL — use Option<NaiveDateTime>".to_string(),
695 )),
696 other => Err(OrmError::Extraction(format!(
697 "Cannot convert {:?} to NaiveDateTime",
698 other
699 ))),
700 }
701 }
702}
703
704#[cfg(feature = "decimal")]
707impl ExtractValue for rust_decimal::Decimal {
708 fn extract(row: &Row, col: &str) -> OrmResult<Self> {
709 let val = row
710 .get_by_name(col)
711 .map_err(|e| OrmError::Extraction(format!("column '{}': {}", col, e)))?;
712 Self::from_pg_value(val)
713 }
714 fn extract_at(row: &Row, index: usize) -> OrmResult<Self> {
715 let val = row
716 .get(index)
717 .map_err(|e| OrmError::Extraction(format!("index {}: {}", index, e)))?;
718 Self::from_pg_value(val)
719 }
720 fn from_pg_value(val: PgValue) -> OrmResult<Self> {
721 use std::str::FromStr;
722 match val {
723 PgValue::Numeric(s) | PgValue::Text(s) => {
724 rust_decimal::Decimal::from_str(&s).map_err(|e| {
725 OrmError::Extraction(format!("Cannot parse '{}' as Decimal: {}", s, e))
726 })
727 }
728 PgValue::Float8(v) => rust_decimal::Decimal::from_f64_retain(v).ok_or_else(|| {
729 OrmError::Extraction(format!("Cannot convert f64 {} to Decimal", v))
730 }),
731 PgValue::Float4(v) => {
732 rust_decimal::Decimal::from_f64_retain(v as f64).ok_or_else(|| {
733 OrmError::Extraction(format!("Cannot convert f32 {} to Decimal", v))
734 })
735 }
736 PgValue::Int4(n) => Ok(rust_decimal::Decimal::from(n)),
737 PgValue::Int8(n) => Ok(rust_decimal::Decimal::from(n)),
738 PgValue::Int2(n) => Ok(rust_decimal::Decimal::from(n)),
739 PgValue::Null => Err(OrmError::Extraction(
740 "Cannot extract Decimal from NULL — use Option<Decimal>".to_string(),
741 )),
742 other => Err(OrmError::Extraction(format!(
743 "Cannot convert {:?} to Decimal",
744 other
745 ))),
746 }
747 }
748}
749
750pub trait HasForeignKey<M: Model> {
751 fn foreign_key_info() -> (&'static str, Vec<(&'static str, &'static str)>);
753}
754
755pub trait SoftDelete: Model {
761 fn deleted_at_column() -> &'static str {
763 "deleted_at"
764 }
765
766 fn soft_delete(&self, executor: &mut impl Executor) -> OrmResult<()> {
768 let pk_cols = Self::primary_key_columns();
769 let pk_vals = self.primary_key_values();
770
771 let mut where_clauses = Vec::new();
772 for (i, pk_col) in pk_cols.iter().enumerate() {
774 where_clauses.push(format!("{} = ${}", pk_col, i + 1));
775 }
776
777 let query = format!(
778 "UPDATE {} SET {} = NOW() WHERE {}",
779 Self::table_name(),
780 Self::deleted_at_column(),
781 where_clauses.join(" AND ")
782 );
783
784 let params: Vec<&dyn chopin_pg::types::ToSql> = pk_vals.iter().map(|v| v as _).collect();
785 executor.execute(&query, ¶ms)?;
786 Ok(())
787 }
788
789 fn restore(&self, executor: &mut impl Executor) -> OrmResult<()> {
791 let pk_cols = Self::primary_key_columns();
792 let pk_vals = self.primary_key_values();
793
794 let mut where_clauses = Vec::new();
795 for (i, pk_col) in pk_cols.iter().enumerate() {
796 where_clauses.push(format!("{} = ${}", pk_col, i + 1));
797 }
798
799 let query = format!(
800 "UPDATE {} SET {} = NULL WHERE {}",
801 Self::table_name(),
802 Self::deleted_at_column(),
803 where_clauses.join(" AND ")
804 );
805
806 let params: Vec<&dyn chopin_pg::types::ToSql> = pk_vals.iter().map(|v| v as _).collect();
807 executor.execute(&query, ¶ms)?;
808 Ok(())
809 }
810
811 fn find_active() -> QueryBuilder<Self> {
813 QueryBuilder::new().filter(Condition::new(
814 format!("{} IS NULL", Self::deleted_at_column()),
815 vec![],
816 ))
817 }
818
819 fn find_with_trashed() -> QueryBuilder<Self> {
821 QueryBuilder::new()
822 }
823
824 fn find_only_trashed() -> QueryBuilder<Self> {
826 QueryBuilder::new().filter(Condition::new(
827 format!("{} IS NOT NULL", Self::deleted_at_column()),
828 vec![],
829 ))
830 }
831}
832
833pub fn batch_insert<M: Model>(models: &mut [M], executor: &mut impl Executor) -> OrmResult<()> {
839 if models.is_empty() {
840 return Ok(());
841 }
842
843 let all_cols = M::columns();
844 let gen_cols = M::generated_columns();
845
846 let insert_cols: Vec<&str> = all_cols
848 .iter()
849 .copied()
850 .filter(|c| !gen_cols.contains(c))
851 .collect();
852
853 let cols_per_row = insert_cols.len();
854
855 let mut all_values: Vec<PgValue> = Vec::with_capacity(models.len() * cols_per_row);
857 let mut value_groups: Vec<String> = Vec::with_capacity(models.len());
858 let mut idx = 1usize;
859
860 for model in models.iter() {
861 let values = model.get_values();
862 let placeholders: Vec<String> = (0..cols_per_row)
863 .map(|_| {
864 let s = format!("${}", idx);
865 idx += 1;
866 s
867 })
868 .collect();
869 value_groups.push(format!("({})", placeholders.join(", ")));
870
871 for (i, col) in all_cols.iter().enumerate() {
872 if !gen_cols.contains(col) {
873 all_values.push(values[i].clone());
874 }
875 }
876 }
877
878 let returning = if gen_cols.is_empty() {
879 String::new()
880 } else {
881 format!(" RETURNING {}", gen_cols.join(", "))
882 };
883
884 let query = format!(
885 "INSERT INTO {} ({}) VALUES {}{}",
886 M::table_name(),
887 insert_cols.join(", "),
888 value_groups.join(", "),
889 returning
890 );
891
892 let params: Vec<&dyn chopin_pg::types::ToSql> = all_values.iter().map(|v| v as _).collect();
893
894 if gen_cols.is_empty() {
895 executor.execute(&query, ¶ms)?;
896 } else {
897 let rows = executor.query(&query, ¶ms)?;
898 for (model, row) in models.iter_mut().zip(rows.iter()) {
899 let mut returned_vals = Vec::with_capacity(gen_cols.len());
900 for i in 0..gen_cols.len() {
901 returned_vals.push(row.get(i)?);
902 }
903 model.set_generated_values(returned_vals)?;
904 }
905 }
906
907 Ok(())
908}
909
910pub struct LoggedExecutor<'a, E: Executor> {
915 pub inner: &'a mut E,
916}
917
918impl<'a, E: Executor> LoggedExecutor<'a, E> {
919 pub fn new(executor: &'a mut E) -> Self {
921 Self { inner: executor }
922 }
923}
924
925impl<'a, E: Executor> Executor for LoggedExecutor<'a, E> {
926 fn execute(&mut self, query: &str, params: &[&dyn chopin_pg::types::ToSql]) -> OrmResult<u64> {
927 let start = std::time::Instant::now();
928 let res = self.inner.execute(query, params);
929 let elapsed = start.elapsed();
930 #[cfg(feature = "log")]
931 log::debug!(
932 "execute ({}ms): {} | params: {:?}",
933 elapsed.as_millis(),
934 query,
935 params.len()
936 );
937 #[cfg(not(feature = "log"))]
938 let _ = elapsed;
939 res
940 }
941
942 fn query(
943 &mut self,
944 query: &str,
945 params: &[&dyn chopin_pg::types::ToSql],
946 ) -> OrmResult<Vec<chopin_pg::Row>> {
947 let start = std::time::Instant::now();
948 let res = self.inner.query(query, params);
949 let elapsed = start.elapsed();
950 #[cfg(feature = "log")]
951 log::debug!(
952 "query ({}ms): {} | params: {:?}",
953 elapsed.as_millis(),
954 query,
955 params.len()
956 );
957 #[cfg(not(feature = "log"))]
958 let _ = elapsed;
959 res
960 }
961}
962
963#[cfg(test)]
964mod tests {
965 use super::*;
966
967 struct TestItem {
970 pub id: i32,
971 pub name: String,
972 }
973
974 impl Validate for TestItem {}
975
976 impl FromRow for TestItem {
977 fn from_row(_row: &Row) -> OrmResult<Self> {
978 Ok(Self {
979 id: 0,
980 name: String::new(),
981 })
982 }
983 }
984
985 impl Model for TestItem {
986 fn table_name() -> &'static str {
987 "items"
988 }
989 fn primary_key_columns() -> &'static [&'static str] {
990 &["id"]
991 }
992 fn generated_columns() -> &'static [&'static str] {
993 &["id"]
994 }
995 fn columns() -> &'static [&'static str] {
996 &["id", "name"]
997 }
998 fn select_clause() -> &'static str {
999 "id, name"
1000 }
1001 fn primary_key_values(&self) -> Vec<PgValue> {
1002 vec![PgValue::Int4(self.id)]
1003 }
1004 fn set_generated_values(&mut self, mut vals: Vec<PgValue>) -> OrmResult<()> {
1005 if let Some(PgValue::Int4(v)) = vals.first() {
1006 self.id = *v;
1007 }
1008 vals.clear();
1009 Ok(())
1010 }
1011 fn get_values(&self) -> Vec<PgValue> {
1012 vec![PgValue::Int4(self.id), PgValue::Text(self.name.clone())]
1013 }
1014 fn create_table_stmt() -> String {
1015 String::new()
1016 }
1017 fn column_definitions() -> Vec<(&'static str, &'static str)> {
1018 vec![]
1019 }
1020 }
1021
1022 struct SoftItem {
1025 pub id: i32,
1026 }
1027
1028 impl Validate for SoftItem {}
1029
1030 impl FromRow for SoftItem {
1031 fn from_row(_row: &Row) -> OrmResult<Self> {
1032 Ok(Self { id: 0 })
1033 }
1034 }
1035
1036 impl Model for SoftItem {
1037 fn table_name() -> &'static str {
1038 "soft_items"
1039 }
1040 fn primary_key_columns() -> &'static [&'static str] {
1041 &["id"]
1042 }
1043 fn generated_columns() -> &'static [&'static str] {
1044 &["id"]
1045 }
1046 fn columns() -> &'static [&'static str] {
1047 &["id", "name", "deleted_at"]
1048 }
1049 fn select_clause() -> &'static str {
1050 "id, name, deleted_at"
1051 }
1052 fn primary_key_values(&self) -> Vec<PgValue> {
1053 vec![PgValue::Int4(self.id)]
1054 }
1055 fn set_generated_values(&mut self, _vals: Vec<PgValue>) -> OrmResult<()> {
1056 Ok(())
1057 }
1058 fn get_values(&self) -> Vec<PgValue> {
1059 vec![]
1060 }
1061 fn create_table_stmt() -> String {
1062 String::new()
1063 }
1064 fn column_definitions() -> Vec<(&'static str, &'static str)> {
1065 vec![]
1066 }
1067 }
1068
1069 impl SoftDelete for SoftItem {}
1070
1071 #[test]
1074 fn test_soft_delete_find_active_scope() {
1075 let qb = SoftItem::find_active();
1076 let (sql, _) = qb.build_query();
1077 assert_eq!(
1078 sql,
1079 "SELECT id, name, deleted_at FROM soft_items WHERE deleted_at IS NULL"
1080 );
1081 }
1082
1083 #[test]
1084 fn test_soft_delete_find_with_trashed() {
1085 let qb = SoftItem::find_with_trashed();
1086 let (sql, _) = qb.build_query();
1087 assert_eq!(sql, "SELECT id, name, deleted_at FROM soft_items");
1088 }
1089
1090 #[test]
1091 fn test_soft_delete_find_only_trashed() {
1092 let qb = SoftItem::find_only_trashed();
1093 let (sql, _) = qb.build_query();
1094 assert_eq!(
1095 sql,
1096 "SELECT id, name, deleted_at FROM soft_items WHERE deleted_at IS NOT NULL"
1097 );
1098 }
1099
1100 #[test]
1101 fn test_batch_insert_builds_correct_sql() {
1102 let mut mock = MockExecutor::new();
1103 mock.push_result(vec![mock_row!("id" => 1), mock_row!("id" => 2)]);
1105
1106 let mut items = vec![
1107 TestItem {
1108 id: 0,
1109 name: "a".into(),
1110 },
1111 TestItem {
1112 id: 0,
1113 name: "b".into(),
1114 },
1115 ];
1116
1117 batch_insert(&mut items, &mut mock).unwrap();
1118
1119 assert_eq!(items[0].id, 1);
1121 assert_eq!(items[1].id, 2);
1122
1123 assert_eq!(mock.executed_queries.len(), 1);
1125 assert_eq!(
1126 mock.executed_queries[0].0,
1127 "INSERT INTO items (name) VALUES ($1), ($2) RETURNING id"
1128 );
1129 }
1130
1131 #[test]
1132 fn test_batch_insert_empty_slice() {
1133 let mut mock = MockExecutor::new();
1134 let mut items: Vec<TestItem> = vec![];
1135 batch_insert(&mut items, &mut mock).unwrap();
1136 assert!(mock.executed_queries.is_empty());
1137 }
1138}