Skip to main content

prax_query/
procedure.rs

1//! Stored procedure and function call support.
2//!
3//! This module provides a type-safe way to call stored procedures and functions
4//! across different database backends.
5//!
6//! # Supported Features
7//!
8//! | Feature                  | PostgreSQL | MySQL | MSSQL | SQLite | MongoDB |
9//! |--------------------------|------------|-------|-------|--------|---------|
10//! | Stored Procedures        | ✅         | ✅    | ✅    | ❌     | ❌      |
11//! | User-Defined Functions   | ✅         | ✅    | ✅    | ✅*    | ✅      |
12//! | Table-Valued Functions   | ✅         | ❌    | ✅    | ❌     | ❌      |
13//! | IN/OUT/INOUT Parameters  | ✅         | ✅    | ✅    | ❌     | ❌      |
14//!
15//! > *SQLite requires Rust UDFs via `rusqlite::functions`
16//!
17//! # Example Usage
18//!
19//! ```rust,ignore
20//! use prax_query::procedure::{ProcedureCall, ParameterMode};
21//!
22//! // Call a stored procedure
23//! let result = client
24//!     .call("get_user_orders")
25//!     .param("user_id", 42)
26//!     .exec::<OrderResult>()
27//!     .await?;
28//!
29//! // Call a procedure with OUT parameters
30//! let result = client
31//!     .call("calculate_totals")
32//!     .in_param("order_id", 123)
33//!     .out_param::<i64>("total_amount")
34//!     .out_param::<i32>("item_count")
35//!     .exec()
36//!     .await?;
37//!
38//! // Call a function
39//! let result = client
40//!     .function("calculate_tax")
41//!     .param("amount", 100.0)
42//!     .param("rate", 0.08)
43//!     .exec::<f64>()
44//!     .await?;
45//! ```
46
47use std::borrow::Cow;
48use std::collections::HashMap;
49use std::marker::PhantomData;
50
51use serde::{Deserialize, Serialize};
52
53use crate::error::{QueryError, QueryResult};
54use crate::filter::FilterValue;
55use crate::sql::DatabaseType;
56use crate::traits::{BoxFuture, QueryEngine};
57
58/// Parameter direction mode for stored procedures.
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
60pub enum ParameterMode {
61    /// Input parameter (default).
62    In,
63    /// Output parameter.
64    Out,
65    /// Input/Output parameter.
66    InOut,
67}
68
69impl Default for ParameterMode {
70    fn default() -> Self {
71        Self::In
72    }
73}
74
75/// A parameter for a stored procedure or function call.
76#[derive(Debug, Clone)]
77pub struct Parameter {
78    /// Parameter name.
79    pub name: String,
80    /// Parameter value (None for OUT parameters without initial value).
81    pub value: Option<FilterValue>,
82    /// Parameter mode (IN, OUT, INOUT).
83    pub mode: ParameterMode,
84    /// Expected type name for OUT parameters.
85    pub type_hint: Option<String>,
86}
87
88impl Parameter {
89    /// Create a new input parameter.
90    pub fn input(name: impl Into<String>, value: impl Into<FilterValue>) -> Self {
91        Self {
92            name: name.into(),
93            value: Some(value.into()),
94            mode: ParameterMode::In,
95            type_hint: None,
96        }
97    }
98
99    /// Create a new output parameter.
100    pub fn output(name: impl Into<String>) -> Self {
101        Self {
102            name: name.into(),
103            value: None,
104            mode: ParameterMode::Out,
105            type_hint: None,
106        }
107    }
108
109    /// Create a new input/output parameter.
110    pub fn inout(name: impl Into<String>, value: impl Into<FilterValue>) -> Self {
111        Self {
112            name: name.into(),
113            value: Some(value.into()),
114            mode: ParameterMode::InOut,
115            type_hint: None,
116        }
117    }
118
119    /// Set a type hint for the parameter.
120    pub fn with_type_hint(mut self, type_name: impl Into<String>) -> Self {
121        self.type_hint = Some(type_name.into());
122        self
123    }
124}
125
126/// Result from a procedure call with OUT/INOUT parameters.
127#[derive(Debug, Clone, Default)]
128pub struct ProcedureResult {
129    /// Output parameter values by name.
130    pub outputs: HashMap<String, FilterValue>,
131    /// Return value (for functions).
132    pub return_value: Option<FilterValue>,
133    /// Number of rows affected (if applicable).
134    pub rows_affected: Option<u64>,
135}
136
137impl ProcedureResult {
138    /// Get an output parameter value.
139    pub fn get(&self, name: &str) -> Option<&FilterValue> {
140        self.outputs.get(name)
141    }
142
143    /// Get an output parameter as a specific type.
144    pub fn get_as<T>(&self, name: &str) -> Option<T>
145    where
146        T: TryFrom<FilterValue>,
147    {
148        self.outputs
149            .get(name)
150            .and_then(|v| T::try_from(v.clone()).ok())
151    }
152
153    /// Get the return value.
154    pub fn return_value(&self) -> Option<&FilterValue> {
155        self.return_value.as_ref()
156    }
157
158    /// Get the return value as a specific type.
159    pub fn return_value_as<T>(&self) -> Option<T>
160    where
161        T: TryFrom<FilterValue>,
162    {
163        self.return_value.clone().and_then(|v| T::try_from(v).ok())
164    }
165}
166
167/// Builder for stored procedure calls.
168#[derive(Debug, Clone)]
169pub struct ProcedureCall {
170    /// Procedure/function name.
171    pub name: String,
172    /// Schema name (optional).
173    pub schema: Option<String>,
174    /// Parameters.
175    pub parameters: Vec<Parameter>,
176    /// Database type for SQL generation.
177    pub db_type: DatabaseType,
178    /// Whether this is a function call (vs procedure).
179    pub is_function: bool,
180}
181
182impl ProcedureCall {
183    /// Create a new procedure call.
184    pub fn new(name: impl Into<String>) -> Self {
185        Self {
186            name: name.into(),
187            schema: None,
188            parameters: Vec::new(),
189            db_type: DatabaseType::PostgreSQL,
190            is_function: false,
191        }
192    }
193
194    /// Create a new function call.
195    pub fn function(name: impl Into<String>) -> Self {
196        Self {
197            name: name.into(),
198            schema: None,
199            parameters: Vec::new(),
200            db_type: DatabaseType::PostgreSQL,
201            is_function: true,
202        }
203    }
204
205    /// Set the schema name.
206    pub fn schema(mut self, schema: impl Into<String>) -> Self {
207        self.schema = Some(schema.into());
208        self
209    }
210
211    /// Set the database type.
212    pub fn with_db_type(mut self, db_type: DatabaseType) -> Self {
213        self.db_type = db_type;
214        self
215    }
216
217    /// Add an input parameter.
218    pub fn param(mut self, name: impl Into<String>, value: impl Into<FilterValue>) -> Self {
219        self.parameters.push(Parameter::input(name, value));
220        self
221    }
222
223    /// Add an input parameter (alias for param).
224    pub fn in_param(self, name: impl Into<String>, value: impl Into<FilterValue>) -> Self {
225        self.param(name, value)
226    }
227
228    /// Add an output parameter.
229    pub fn out_param(mut self, name: impl Into<String>) -> Self {
230        self.parameters.push(Parameter::output(name));
231        self
232    }
233
234    /// Add an output parameter with type hint.
235    pub fn out_param_typed(
236        mut self,
237        name: impl Into<String>,
238        type_hint: impl Into<String>,
239    ) -> Self {
240        self.parameters
241            .push(Parameter::output(name).with_type_hint(type_hint));
242        self
243    }
244
245    /// Add an input/output parameter.
246    pub fn inout_param(mut self, name: impl Into<String>, value: impl Into<FilterValue>) -> Self {
247        self.parameters.push(Parameter::inout(name, value));
248        self
249    }
250
251    /// Add a raw parameter.
252    pub fn add_parameter(mut self, param: Parameter) -> Self {
253        self.parameters.push(param);
254        self
255    }
256
257    /// Get the fully qualified name.
258    pub fn qualified_name(&self) -> Cow<'_, str> {
259        match &self.schema {
260            Some(schema) => Cow::Owned(format!("{}.{}", schema, self.name)),
261            None => Cow::Borrowed(&self.name),
262        }
263    }
264
265    /// Check if any parameters are OUT or INOUT.
266    pub fn has_outputs(&self) -> bool {
267        self.parameters
268            .iter()
269            .any(|p| matches!(p.mode, ParameterMode::Out | ParameterMode::InOut))
270    }
271
272    /// Get input parameter values.
273    pub fn input_values(&self) -> Vec<FilterValue> {
274        self.parameters
275            .iter()
276            .filter(|p| matches!(p.mode, ParameterMode::In | ParameterMode::InOut))
277            .filter_map(|p| p.value.clone())
278            .collect()
279    }
280
281    /// Generate SQL for PostgreSQL.
282    pub fn to_postgres_sql(&self) -> (String, Vec<FilterValue>) {
283        let name = self.qualified_name();
284        let params = self.input_values();
285        let placeholders: Vec<String> = (1..=params.len()).map(|i| format!("${}", i)).collect();
286
287        let sql = if self.is_function {
288            format!("SELECT {}({})", name, placeholders.join(", "))
289        } else {
290            format!("CALL {}({})", name, placeholders.join(", "))
291        };
292
293        (sql, params)
294    }
295
296    /// Generate SQL for MySQL.
297    pub fn to_mysql_sql(&self) -> (String, Vec<FilterValue>) {
298        let name = self.qualified_name();
299        let params = self.input_values();
300        let placeholders = vec!["?"; params.len()].join(", ");
301
302        let sql = if self.is_function {
303            format!("SELECT {}({})", name, placeholders)
304        } else {
305            format!("CALL {}({})", name, placeholders)
306        };
307
308        (sql, params)
309    }
310
311    /// Generate SQL for MSSQL.
312    pub fn to_mssql_sql(&self) -> (String, Vec<FilterValue>) {
313        let name = self.qualified_name();
314        let params = self.input_values();
315        let placeholders: Vec<String> = (1..=params.len()).map(|i| format!("@P{}", i)).collect();
316
317        if self.is_function {
318            (
319                format!("SELECT {}({})", name, placeholders.join(", ")),
320                params,
321            )
322        } else if self.has_outputs() {
323            // For procedures with OUT params, use EXEC with output variable declarations
324            let mut parts = vec![String::from("DECLARE ")];
325
326            // Declare output variables
327            let out_params: Vec<_> = self
328                .parameters
329                .iter()
330                .filter(|p| matches!(p.mode, ParameterMode::Out | ParameterMode::InOut))
331                .collect();
332
333            for (i, param) in out_params.iter().enumerate() {
334                if i > 0 {
335                    parts.push(String::from(", "));
336                }
337                let type_name = param.type_hint.as_deref().unwrap_or("SQL_VARIANT");
338                parts.push(format!("@{} {}", param.name, type_name));
339            }
340            parts.push(String::from("; "));
341
342            // Build EXEC statement
343            parts.push(format!("EXEC {} ", name));
344
345            let param_parts: Vec<String> = self
346                .parameters
347                .iter()
348                .enumerate()
349                .map(|(i, p)| match p.mode {
350                    ParameterMode::In => format!("@P{}", i + 1),
351                    ParameterMode::Out => format!("@{} OUTPUT", p.name),
352                    ParameterMode::InOut => format!("@P{} = @{} OUTPUT", i + 1, p.name),
353                })
354                .collect();
355
356            parts.push(param_parts.join(", "));
357            parts.push(String::from("; "));
358
359            // Select output values
360            let select_parts: Vec<String> = out_params
361                .iter()
362                .map(|p| format!("@{} AS {}", p.name, p.name))
363                .collect();
364            parts.push(format!("SELECT {}", select_parts.join(", ")));
365
366            (parts.join(""), params)
367        } else {
368            (format!("EXEC {} {}", name, placeholders.join(", ")), params)
369        }
370    }
371
372    /// Generate SQL for SQLite (only functions supported).
373    pub fn to_sqlite_sql(&self) -> QueryResult<(String, Vec<FilterValue>)> {
374        if !self.is_function {
375            return Err(QueryError::unsupported(
376                "SQLite does not support stored procedures. Use Rust UDFs instead.",
377            ));
378        }
379
380        let name = self.qualified_name();
381        let params = self.input_values();
382        let placeholders = vec!["?"; params.len()].join(", ");
383
384        Ok((format!("SELECT {}({})", name, placeholders), params))
385    }
386
387    /// Generate SQL for the configured database type.
388    pub fn to_sql(&self) -> QueryResult<(String, Vec<FilterValue>)> {
389        match self.db_type {
390            DatabaseType::PostgreSQL => Ok(self.to_postgres_sql()),
391            DatabaseType::MySQL => Ok(self.to_mysql_sql()),
392            DatabaseType::SQLite => self.to_sqlite_sql(),
393            DatabaseType::MSSQL => Ok(self.to_mssql_sql()),
394        }
395    }
396}
397
398/// Operation for executing a procedure call.
399pub struct ProcedureCallOperation<E: QueryEngine> {
400    engine: E,
401    call: ProcedureCall,
402}
403
404impl<E: QueryEngine> ProcedureCallOperation<E> {
405    /// Create a new procedure call operation.
406    pub fn new(engine: E, call: ProcedureCall) -> Self {
407        Self { engine, call }
408    }
409
410    /// Execute the procedure and return the result.
411    pub async fn exec(self) -> QueryResult<ProcedureResult> {
412        let (sql, params) = self.call.to_sql()?;
413        let affected = self.engine.execute_raw(&sql, params).await?;
414
415        Ok(ProcedureResult {
416            outputs: HashMap::new(),
417            return_value: None,
418            rows_affected: Some(affected),
419        })
420    }
421
422    /// Execute the procedure and return typed results.
423    pub async fn exec_returning<T>(self) -> QueryResult<Vec<T>>
424    where
425        T: crate::traits::Model + Send + 'static,
426    {
427        let (sql, params) = self.call.to_sql()?;
428        self.engine.query_many(&sql, params).await
429    }
430
431    /// Execute a function and return a single value.
432    pub async fn exec_scalar<T>(self) -> QueryResult<T>
433    where
434        T: TryFrom<FilterValue, Error = String> + Send + 'static,
435    {
436        let (sql, params) = self.call.to_sql()?;
437        let result = self.engine.execute_raw(&sql, params).await?;
438
439        // For scalar functions, the result is in the first column
440        // This is a simplified implementation - real impl would parse the actual result
441        Err(QueryError::internal(format!(
442            "Scalar function result parsing not yet implemented (affected: {})",
443            result
444        )))
445    }
446}
447
448/// Operation for executing a function call that returns a value.
449#[allow(dead_code)]
450pub struct FunctionCallOperation<E: QueryEngine, T> {
451    engine: E,
452    call: ProcedureCall,
453    _marker: PhantomData<T>,
454}
455
456impl<E: QueryEngine, T> FunctionCallOperation<E, T> {
457    /// Create a new function call operation.
458    pub fn new(engine: E, call: ProcedureCall) -> Self {
459        Self {
460            engine,
461            call,
462            _marker: PhantomData,
463        }
464    }
465}
466
467/// Extension trait for query engines to support procedure calls.
468pub trait ProcedureEngine: QueryEngine {
469    /// Call a stored procedure.
470    fn call(&self, name: impl Into<String>) -> ProcedureCall {
471        ProcedureCall::new(name)
472    }
473
474    /// Call a function.
475    fn function(&self, name: impl Into<String>) -> ProcedureCall {
476        ProcedureCall::function(name)
477    }
478
479    /// Execute a procedure call.
480    fn execute_procedure(&self, call: ProcedureCall) -> BoxFuture<'_, QueryResult<ProcedureResult>>
481    where
482        Self: Clone + 'static,
483    {
484        let engine = self.clone();
485        Box::pin(async move {
486            let op = ProcedureCallOperation::new(engine, call);
487            op.exec().await
488        })
489    }
490}
491
492// Implement ProcedureEngine for all QueryEngine implementations
493impl<T: QueryEngine + Clone + 'static> ProcedureEngine for T {}
494
495/// SQLite-specific UDF registration support.
496pub mod sqlite_udf {
497    #[allow(unused_imports)]
498    use super::*;
499
500    /// A Rust function that can be registered as a SQLite UDF.
501    pub trait SqliteFunction: Send + Sync + 'static {
502        /// The name of the function.
503        fn name(&self) -> &str;
504
505        /// The number of arguments (-1 for variadic).
506        fn num_args(&self) -> i32;
507
508        /// Whether the function is deterministic.
509        fn deterministic(&self) -> bool {
510            true
511        }
512    }
513
514    /// A scalar UDF definition.
515    #[derive(Debug, Clone)]
516    pub struct ScalarUdf {
517        /// Function name.
518        pub name: String,
519        /// Number of arguments.
520        pub num_args: i32,
521        /// Whether deterministic.
522        pub deterministic: bool,
523    }
524
525    impl ScalarUdf {
526        /// Create a new scalar UDF definition.
527        pub fn new(name: impl Into<String>, num_args: i32) -> Self {
528            Self {
529                name: name.into(),
530                num_args,
531                deterministic: true,
532            }
533        }
534
535        /// Set whether the function is deterministic.
536        pub fn deterministic(mut self, deterministic: bool) -> Self {
537            self.deterministic = deterministic;
538            self
539        }
540    }
541
542    /// An aggregate UDF definition.
543    #[derive(Debug, Clone)]
544    pub struct AggregateUdf {
545        /// Function name.
546        pub name: String,
547        /// Number of arguments.
548        pub num_args: i32,
549    }
550
551    impl AggregateUdf {
552        /// Create a new aggregate UDF definition.
553        pub fn new(name: impl Into<String>, num_args: i32) -> Self {
554            Self {
555                name: name.into(),
556                num_args,
557            }
558        }
559    }
560
561    /// A window UDF definition.
562    #[derive(Debug, Clone)]
563    pub struct WindowUdf {
564        /// Function name.
565        pub name: String,
566        /// Number of arguments.
567        pub num_args: i32,
568    }
569
570    impl WindowUdf {
571        /// Create a new window UDF definition.
572        pub fn new(name: impl Into<String>, num_args: i32) -> Self {
573            Self {
574                name: name.into(),
575                num_args,
576            }
577        }
578    }
579}
580
581/// MongoDB-specific function support.
582pub mod mongodb_func {
583    use super::*;
584
585    /// A MongoDB `$function` expression for custom JavaScript functions.
586    #[derive(Debug, Clone, Serialize, Deserialize)]
587    pub struct MongoFunction {
588        /// JavaScript function body.
589        pub body: String,
590        /// Function arguments (field references or values).
591        pub args: Vec<String>,
592        /// Language (always "js" for now).
593        pub lang: String,
594    }
595
596    impl MongoFunction {
597        /// Create a new MongoDB function.
598        pub fn new(body: impl Into<String>, args: Vec<impl Into<String>>) -> Self {
599            Self {
600                body: body.into(),
601                args: args.into_iter().map(Into::into).collect(),
602                lang: "js".to_string(),
603            }
604        }
605
606        /// Convert to a BSON document for use in aggregation.
607        #[cfg(feature = "mongodb")]
608        pub fn to_bson(&self) -> bson::Document {
609            use bson::doc;
610            doc! {
611                "$function": {
612                    "body": &self.body,
613                    "args": &self.args,
614                    "lang": &self.lang,
615                }
616            }
617        }
618    }
619
620    /// A MongoDB `$accumulator` expression for custom aggregation.
621    #[derive(Debug, Clone, Serialize, Deserialize)]
622    pub struct MongoAccumulator {
623        /// Initialize the accumulator state.
624        pub init: String,
625        /// Initialize arguments.
626        pub init_args: Vec<String>,
627        /// Accumulate function.
628        pub accumulate: String,
629        /// Accumulate arguments.
630        pub accumulate_args: Vec<String>,
631        /// Merge function.
632        pub merge: String,
633        /// Finalize function (optional).
634        pub finalize: Option<String>,
635        /// Language.
636        pub lang: String,
637    }
638
639    impl MongoAccumulator {
640        /// Create a new MongoDB accumulator.
641        pub fn new(
642            init: impl Into<String>,
643            accumulate: impl Into<String>,
644            merge: impl Into<String>,
645        ) -> Self {
646            Self {
647                init: init.into(),
648                init_args: Vec::new(),
649                accumulate: accumulate.into(),
650                accumulate_args: Vec::new(),
651                merge: merge.into(),
652                finalize: None,
653                lang: "js".to_string(),
654            }
655        }
656
657        /// Set init arguments.
658        pub fn with_init_args(mut self, args: Vec<impl Into<String>>) -> Self {
659            self.init_args = args.into_iter().map(Into::into).collect();
660            self
661        }
662
663        /// Set accumulate arguments.
664        pub fn with_accumulate_args(mut self, args: Vec<impl Into<String>>) -> Self {
665            self.accumulate_args = args.into_iter().map(Into::into).collect();
666            self
667        }
668
669        /// Set finalize function.
670        pub fn with_finalize(mut self, finalize: impl Into<String>) -> Self {
671            self.finalize = Some(finalize.into());
672            self
673        }
674
675        /// Convert to a BSON document for use in aggregation.
676        #[cfg(feature = "mongodb")]
677        pub fn to_bson(&self) -> bson::Document {
678            use bson::doc;
679            let mut doc = doc! {
680                "$accumulator": {
681                    "init": &self.init,
682                    "accumulate": &self.accumulate,
683                    "accumulateArgs": &self.accumulate_args,
684                    "merge": &self.merge,
685                    "lang": &self.lang,
686                }
687            };
688
689            if !self.init_args.is_empty() {
690                doc.get_document_mut("$accumulator")
691                    .unwrap()
692                    .insert("initArgs", &self.init_args);
693            }
694
695            if let Some(ref finalize) = self.finalize {
696                doc.get_document_mut("$accumulator")
697                    .unwrap()
698                    .insert("finalize", finalize);
699            }
700
701            doc
702        }
703    }
704}
705
706#[cfg(test)]
707mod tests {
708    use super::*;
709
710    #[test]
711    fn test_procedure_call_basic() {
712        let call = ProcedureCall::new("get_user")
713            .param("id", 42i32)
714            .param("active", true);
715
716        assert_eq!(call.name, "get_user");
717        assert_eq!(call.parameters.len(), 2);
718        assert!(!call.is_function);
719    }
720
721    #[test]
722    fn test_function_call() {
723        let call = ProcedureCall::function("calculate_tax")
724            .param("amount", 100.0f64)
725            .param("rate", 0.08f64);
726
727        assert_eq!(call.name, "calculate_tax");
728        assert!(call.is_function);
729    }
730
731    #[test]
732    fn test_postgres_sql_generation() {
733        let call = ProcedureCall::new("get_orders")
734            .param("user_id", 42i32)
735            .param("status", "pending".to_string());
736
737        let (sql, params) = call.to_postgres_sql();
738        assert_eq!(sql, "CALL get_orders($1, $2)");
739        assert_eq!(params.len(), 2);
740    }
741
742    #[test]
743    fn test_postgres_function_sql() {
744        let call = ProcedureCall::function("calculate_total").param("order_id", 123i32);
745
746        let (sql, params) = call.to_postgres_sql();
747        assert_eq!(sql, "SELECT calculate_total($1)");
748        assert_eq!(params.len(), 1);
749    }
750
751    #[test]
752    fn test_mysql_sql_generation() {
753        let call = ProcedureCall::new("get_orders")
754            .with_db_type(DatabaseType::MySQL)
755            .param("user_id", 42i32);
756
757        let (sql, params) = call.to_mysql_sql();
758        assert_eq!(sql, "CALL get_orders(?)");
759        assert_eq!(params.len(), 1);
760    }
761
762    #[test]
763    fn test_mssql_sql_generation() {
764        let call = ProcedureCall::new("GetOrders")
765            .schema("dbo")
766            .with_db_type(DatabaseType::MSSQL)
767            .param("UserId", 42i32);
768
769        let (sql, params) = call.to_mssql_sql();
770        assert!(sql.contains("EXEC dbo.GetOrders"));
771        assert_eq!(params.len(), 1);
772    }
773
774    #[test]
775    fn test_mssql_with_output_params() {
776        let call = ProcedureCall::new("CalculateTotals")
777            .with_db_type(DatabaseType::MSSQL)
778            .in_param("OrderId", 123i32)
779            .out_param_typed("TotalAmount", "DECIMAL(18,2)")
780            .out_param_typed("ItemCount", "INT");
781
782        let (sql, _params) = call.to_mssql_sql();
783        assert!(sql.contains("DECLARE"));
784        assert!(sql.contains("OUTPUT"));
785        assert!(sql.contains("SELECT"));
786    }
787
788    #[test]
789    fn test_sqlite_function() {
790        let call = ProcedureCall::function("custom_hash")
791            .with_db_type(DatabaseType::SQLite)
792            .param("input", "test".to_string());
793
794        let result = call.to_sqlite_sql();
795        assert!(result.is_ok());
796
797        let (sql, params) = result.unwrap();
798        assert_eq!(sql, "SELECT custom_hash(?)");
799        assert_eq!(params.len(), 1);
800    }
801
802    #[test]
803    fn test_sqlite_procedure_error() {
804        let call = ProcedureCall::new("some_procedure")
805            .with_db_type(DatabaseType::SQLite)
806            .param("id", 42i32);
807
808        let result = call.to_sqlite_sql();
809        assert!(result.is_err());
810    }
811
812    #[test]
813    fn test_qualified_name() {
814        let call = ProcedureCall::new("get_user").schema("public");
815        assert_eq!(call.qualified_name(), "public.get_user");
816
817        let call = ProcedureCall::new("get_user");
818        assert_eq!(call.qualified_name(), "get_user");
819    }
820
821    #[test]
822    fn test_parameter_modes() {
823        let call = ProcedureCall::new("calculate")
824            .in_param("input", 100i32)
825            .out_param("result")
826            .inout_param("running_total", 50i32);
827
828        assert_eq!(call.parameters.len(), 3);
829        assert_eq!(call.parameters[0].mode, ParameterMode::In);
830        assert_eq!(call.parameters[1].mode, ParameterMode::Out);
831        assert_eq!(call.parameters[2].mode, ParameterMode::InOut);
832        assert!(call.has_outputs());
833    }
834
835    #[test]
836    fn test_procedure_result() {
837        let mut result = ProcedureResult::default();
838        result
839            .outputs
840            .insert("total".to_string(), FilterValue::Int(100));
841        result.return_value = Some(FilterValue::Bool(true));
842
843        assert!(result.get("total").is_some());
844        assert!(result.get("nonexistent").is_none());
845        assert!(result.return_value().is_some());
846    }
847
848    #[test]
849    fn test_mongo_function() {
850        use mongodb_func::MongoFunction;
851
852        let func = MongoFunction::new(
853            "function(x, y) { return x + y; }",
854            vec!["$field1", "$field2"],
855        );
856
857        assert_eq!(func.lang, "js");
858        assert_eq!(func.args.len(), 2);
859    }
860
861    #[test]
862    fn test_mongo_accumulator() {
863        use mongodb_func::MongoAccumulator;
864
865        let acc = MongoAccumulator::new(
866            "function() { return { sum: 0, count: 0 }; }",
867            "function(state, value) { state.sum += value; state.count++; return state; }",
868            "function(s1, s2) { return { sum: s1.sum + s2.sum, count: s1.count + s2.count }; }",
869        )
870        .with_finalize("function(state) { return state.sum / state.count; }")
871        .with_accumulate_args(vec!["$value"]);
872
873        assert!(acc.finalize.is_some());
874        assert_eq!(acc.accumulate_args.len(), 1);
875    }
876
877    #[test]
878    fn test_sqlite_udf_definitions() {
879        use sqlite_udf::{AggregateUdf, ScalarUdf, WindowUdf};
880
881        let scalar = ScalarUdf::new("my_hash", 1).deterministic(true);
882        assert!(scalar.deterministic);
883
884        let aggregate = AggregateUdf::new("my_sum", 1);
885        assert_eq!(aggregate.num_args, 1);
886
887        let window = WindowUdf::new("my_rank", 0);
888        assert_eq!(window.num_args, 0);
889    }
890}