Skip to main content

chopin_orm/
lib.rs

1//! # chopin-orm
2//!
3//! An easy-to-use Object-Relational Mapper (ORM) for `chopin2`, backed by the high-performance
4//! `chopin-pg` synchronous PostgreSQL driver.
5
6pub 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
23/// A trait for types that can execute SQL queries and return results.
24///
25/// Implemented by `PgPool`, `PgConnection`, and `Transaction`.
26pub trait Executor {
27    /// Executes a command (e.g., INSERT, UPDATE, DELETE) and returns the number of affected rows.
28    fn execute(&mut self, query: &str, params: &[&dyn chopin_pg::types::ToSql]) -> OrmResult<u64>;
29
30    /// Executes a query and returns the resulting rows.
31    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
72/// A database transaction wrapper that automatically rolls back on drop
73/// unless explicitly committed.
74///
75/// This ensures that if a panic or early return occurs, the transaction
76/// is safely rolled back rather than leaving the connection in a dirty state.
77pub struct Transaction<'a> {
78    conn: &'a mut PgConnection,
79    committed: bool,
80}
81
82impl<'a> Transaction<'a> {
83    /// Begins a new transaction on the given connection.
84    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    /// Commits the transaction. Must be called explicitly to persist changes.
93    pub fn commit(mut self) -> OrmResult<()> {
94        self.committed = true;
95        self.conn.execute("COMMIT", &[]).map_err(OrmError::from)?;
96        Ok(())
97    }
98
99    /// Explicitly rolls back the transaction.
100    pub fn rollback(mut self) -> OrmResult<()> {
101        self.committed = true; // prevent double-rollback in Drop
102        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            // Best-effort rollback on drop — ignore errors since we may be
111            // in a panic unwind where the connection is already broken.
112            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(()) // Default passes
136    }
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    /// Generate the CREATE TABLE statement for this model
151    fn create_table_stmt() -> String;
152
153    /// Returns the literal raw SQL column definitions (name, type) for auto-migrations
154    fn column_definitions() -> Vec<(&'static str, &'static str)>;
155
156    /// Returns the list of indexes to natively enforce during migrations
157    fn indexes() -> Vec<Index> {
158        vec![]
159    }
160
161    /// Execute the CREATE TABLE statement against the database
162    fn create_table(executor: &mut impl Executor) -> OrmResult<()> {
163        executor.execute(&Self::create_table_stmt(), &[])?;
164        Ok(())
165    }
166
167    /// Instantiate a `QueryBuilder` for this model dynamically.
168    fn find() -> QueryBuilder<Self> {
169        QueryBuilder::new()
170    }
171
172    /// Automatically diffs and migrates the table schema based on structural column metadata
173    fn sync_schema(executor: &mut impl Executor) -> OrmResult<()> {
174        Self::create_table(executor)?;
175
176        // check existing columns
177        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, &params)?;
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    /// Insert the model into the database. Retrieves generated columns.
225    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, &params)?;
263        } else {
264            let rows = executor.query(&query, &params)?;
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    /// Insert the model or update it if the primary key conflicts
277    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        // EXCLUDED is a postgres keyword referring to the row proposed for insertion
307        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, &params)?;
334        } else {
335            let rows = executor.query(&query, &params)?;
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    /// Partially update the model, persisting only the specified columns to the database.
348    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        // Add primary key to WHERE clause
380        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, &params_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    /// Update the model in the database matching its primary key.
412    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(()); // Nothing to update
440        }
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, &params)?;
460        Ok(())
461    }
462
463    /// Delete the model from the database.
464    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, &params)?;
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
501// Implement ExtractValue for common types
502impl 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
601// Option wrapper
602impl<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
625// ─── f32 ExtractValue ─────────────────────────────────────────────────────────
626
627impl 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// ─── chrono::NaiveDateTime ExtractValue ───────────────────────────────────────
652
653#[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// ─── rust_decimal::Decimal ExtractValue ───────────────────────────────────────
705
706#[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    /// Returns the table name of the child and a list of (child_column, parent_column) mappings.
752    fn foreign_key_info() -> (&'static str, Vec<(&'static str, &'static str)>);
753}
754
755/// Marker trait for models with a `deleted_at` timestamp column.
756///
757/// When implemented, `QueryBuilder` can automatically scope queries to exclude
758/// soft-deleted rows (via `with_soft_delete_scope()`), and the model gains
759/// `soft_delete()` and `restore()` helpers.
760pub trait SoftDelete: Model {
761    /// The column name used for soft-delete timestamps (default: `"deleted_at"`).
762    fn deleted_at_column() -> &'static str {
763        "deleted_at"
764    }
765
766    /// Soft-delete this model by setting `deleted_at = NOW()`.
767    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        // Parameter $1 is reserved for the SET clause (not needed for NOW()).
773        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, &params)?;
786        Ok(())
787    }
788
789    /// Restore a soft-deleted model by setting `deleted_at = NULL`.
790    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, &params)?;
808        Ok(())
809    }
810
811    /// Returns a QueryBuilder pre-scoped to exclude soft-deleted rows.
812    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    /// Returns a QueryBuilder that includes soft-deleted rows.
820    fn find_with_trashed() -> QueryBuilder<Self> {
821        QueryBuilder::new()
822    }
823
824    /// Returns a QueryBuilder scoped to only soft-deleted rows.
825    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
833/// Batch insert a slice of models in a single `INSERT` round-trip.
834///
835/// Generates `INSERT INTO t (cols) VALUES ($1,$2),($3,$4),…` with RETURNING
836/// for generated columns. Each model is mutated in-place to receive its
837/// generated values (e.g. auto-increment IDs).
838pub 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    // Determine insertable columns (non-generated).
847    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    // Collect all values in row-major order.
856    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, &params)?;
896    } else {
897        let rows = executor.query(&query, &params)?;
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
910/// A transparent middleware executor that intercepts queries and parameters.
911///
912/// Under the `log` feature flag, this emits `tracing` debug logs containing executed SQL,
913/// elapsed execution time, and parameter payload metrics.
914pub struct LoggedExecutor<'a, E: Executor> {
915    pub inner: &'a mut E,
916}
917
918impl<'a, E: Executor> LoggedExecutor<'a, E> {
919    /// Wraps an existing `Executor` (like `PgConnection` or `PgPool`) in logging telemetry.
920    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    // ─── Minimal test model ──────────────────────────────────────────────────
968
969    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    // ─── SoftDelete model ────────────────────────────────────────────────────
1023
1024    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    // ─── Tests ───────────────────────────────────────────────────────────────
1072
1073    #[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        // Queue two rows of generated IDs.
1104        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        // Verify generated IDs were set back.
1120        assert_eq!(items[0].id, 1);
1121        assert_eq!(items[1].id, 2);
1122
1123        // Verify the SQL shape.
1124        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}