@@ -11,6 +11,15 @@ use chrono::{DateTime, Utc};
1111use serde:: { Deserialize , Serialize } ;
1212use uuid:: Uuid ;
1313
14+ #[ cfg( any( feature = "postgres" , feature = "mysql" ) ) ]
15+ use sqlx:: { Decode , Encode , Type } ;
16+
17+ #[ cfg( feature = "postgres" ) ]
18+ use sqlx:: Postgres ;
19+
20+ #[ cfg( feature = "mysql" ) ]
21+ use sqlx:: MySql ;
22+
1423/// Unique identifier for a job.
1524///
1625/// Each job gets a unique UUID when created to enable tracking and management
@@ -52,6 +61,96 @@ pub enum JobStatus {
5261 Archived ,
5362}
5463
64+ // SQLx implementations for JobStatus to handle database encoding/decoding
65+
66+ #[ cfg( feature = "postgres" ) ]
67+ impl Type < Postgres > for JobStatus {
68+ fn type_info ( ) -> sqlx:: postgres:: PgTypeInfo {
69+ <String as Type < Postgres > >:: type_info ( )
70+ }
71+ }
72+
73+ #[ cfg( feature = "postgres" ) ]
74+ impl Encode < ' _ , Postgres > for JobStatus {
75+ fn encode_by_ref ( & self , buf : & mut sqlx:: postgres:: PgArgumentBuffer ) -> Result < sqlx:: encode:: IsNull , Box < dyn std:: error:: Error + Send + Sync + ' static > > {
76+ let status_str = match self {
77+ JobStatus :: Pending => "Pending" ,
78+ JobStatus :: Running => "Running" ,
79+ JobStatus :: Completed => "Completed" ,
80+ JobStatus :: Failed => "Failed" ,
81+ JobStatus :: Dead => "Dead" ,
82+ JobStatus :: TimedOut => "TimedOut" ,
83+ JobStatus :: Retrying => "Retrying" ,
84+ JobStatus :: Archived => "Archived" ,
85+ } ;
86+ <& str as Encode < ' _ , Postgres > >:: encode_by_ref ( & status_str, buf)
87+ }
88+ }
89+
90+ #[ cfg( feature = "postgres" ) ]
91+ impl Decode < ' _ , Postgres > for JobStatus {
92+ fn decode ( value : sqlx:: postgres:: PgValueRef < ' _ > ) -> Result < Self , sqlx:: error:: BoxDynError > {
93+ let status_str = <String as Decode < Postgres > >:: decode ( value) ?;
94+ // Handle both quoted (old format) and unquoted (new format) status values
95+ let cleaned_str = status_str. trim_matches ( '"' ) ;
96+ match cleaned_str {
97+ "Pending" => Ok ( JobStatus :: Pending ) ,
98+ "Running" => Ok ( JobStatus :: Running ) ,
99+ "Completed" => Ok ( JobStatus :: Completed ) ,
100+ "Failed" => Ok ( JobStatus :: Failed ) ,
101+ "Dead" => Ok ( JobStatus :: Dead ) ,
102+ "TimedOut" => Ok ( JobStatus :: TimedOut ) ,
103+ "Retrying" => Ok ( JobStatus :: Retrying ) ,
104+ "Archived" => Ok ( JobStatus :: Archived ) ,
105+ _ => Err ( format ! ( "Unknown job status: {}" , status_str) . into ( ) ) ,
106+ }
107+ }
108+ }
109+
110+ #[ cfg( feature = "mysql" ) ]
111+ impl Type < MySql > for JobStatus {
112+ fn type_info ( ) -> sqlx:: mysql:: MySqlTypeInfo {
113+ <String as Type < MySql > >:: type_info ( )
114+ }
115+ }
116+
117+ #[ cfg( feature = "mysql" ) ]
118+ impl Encode < ' _ , MySql > for JobStatus {
119+ fn encode_by_ref ( & self , buf : & mut Vec < u8 > ) -> Result < sqlx:: encode:: IsNull , Box < dyn std:: error:: Error + Send + Sync + ' static > > {
120+ let status_str = match self {
121+ JobStatus :: Pending => "Pending" ,
122+ JobStatus :: Running => "Running" ,
123+ JobStatus :: Completed => "Completed" ,
124+ JobStatus :: Failed => "Failed" ,
125+ JobStatus :: Dead => "Dead" ,
126+ JobStatus :: TimedOut => "TimedOut" ,
127+ JobStatus :: Retrying => "Retrying" ,
128+ JobStatus :: Archived => "Archived" ,
129+ } ;
130+ <& str as Encode < ' _ , MySql > >:: encode_by_ref ( & status_str, buf)
131+ }
132+ }
133+
134+ #[ cfg( feature = "mysql" ) ]
135+ impl Decode < ' _ , MySql > for JobStatus {
136+ fn decode ( value : sqlx:: mysql:: MySqlValueRef < ' _ > ) -> Result < Self , sqlx:: error:: BoxDynError > {
137+ let status_str = <String as Decode < MySql > >:: decode ( value) ?;
138+ // Handle both quoted (old format) and unquoted (new format) status values
139+ let cleaned_str = status_str. trim_matches ( '"' ) ;
140+ match cleaned_str {
141+ "Pending" => Ok ( JobStatus :: Pending ) ,
142+ "Running" => Ok ( JobStatus :: Running ) ,
143+ "Completed" => Ok ( JobStatus :: Completed ) ,
144+ "Failed" => Ok ( JobStatus :: Failed ) ,
145+ "Dead" => Ok ( JobStatus :: Dead ) ,
146+ "TimedOut" => Ok ( JobStatus :: TimedOut ) ,
147+ "Retrying" => Ok ( JobStatus :: Retrying ) ,
148+ "Archived" => Ok ( JobStatus :: Archived ) ,
149+ _ => Err ( format ! ( "Unknown job status: {}" , status_str) . into ( ) ) ,
150+ }
151+ }
152+ }
153+
55154/// Configuration for job result storage.
56155///
57156/// This enum determines where and how job results are stored when jobs complete successfully.
@@ -2142,4 +2241,82 @@ mod tests {
21422241 assert ! ( job. timed_out_at. is_some( ) ) ;
21432242 assert ! ( job. error_message. is_some( ) ) ;
21442243 }
2244+
2245+ #[ test]
2246+ fn test_job_status_backward_compatibility_string_matching ( ) {
2247+ // Test that our string matching logic handles both quoted and unquoted formats
2248+ // This simulates what happens in the Decode implementation
2249+ let test_cases = [
2250+ // (input_string, expected_status)
2251+ ( "Pending" , JobStatus :: Pending ) ,
2252+ ( "\" Pending\" " , JobStatus :: Pending ) ,
2253+ ( "Running" , JobStatus :: Running ) ,
2254+ ( "\" Running\" " , JobStatus :: Running ) ,
2255+ ( "Completed" , JobStatus :: Completed ) ,
2256+ ( "\" Completed\" " , JobStatus :: Completed ) ,
2257+ ( "Failed" , JobStatus :: Failed ) ,
2258+ ( "\" Failed\" " , JobStatus :: Failed ) ,
2259+ ( "Dead" , JobStatus :: Dead ) ,
2260+ ( "\" Dead\" " , JobStatus :: Dead ) ,
2261+ ( "TimedOut" , JobStatus :: TimedOut ) ,
2262+ ( "\" TimedOut\" " , JobStatus :: TimedOut ) ,
2263+ ( "Retrying" , JobStatus :: Retrying ) ,
2264+ ( "\" Retrying\" " , JobStatus :: Retrying ) ,
2265+ ( "Archived" , JobStatus :: Archived ) ,
2266+ ( "\" Archived\" " , JobStatus :: Archived ) ,
2267+ ] ;
2268+
2269+ for ( input, expected) in & test_cases {
2270+ // This is the same logic used in our Decode implementations
2271+ let cleaned_str = input. trim_matches ( '"' ) ;
2272+ let parsed_status = match cleaned_str {
2273+ "Pending" => JobStatus :: Pending ,
2274+ "Running" => JobStatus :: Running ,
2275+ "Completed" => JobStatus :: Completed ,
2276+ "Failed" => JobStatus :: Failed ,
2277+ "Dead" => JobStatus :: Dead ,
2278+ "TimedOut" => JobStatus :: TimedOut ,
2279+ "Retrying" => JobStatus :: Retrying ,
2280+ "Archived" => JobStatus :: Archived ,
2281+ _ => panic ! ( "Unknown job status: {}" , input) ,
2282+ } ;
2283+
2284+ assert_eq ! ( * expected, parsed_status, "Failed to parse '{}' correctly" , input) ;
2285+ }
2286+ }
2287+
2288+ #[ test]
2289+ fn test_job_status_encoding_logic ( ) {
2290+ // Verify that our encoding logic produces the expected unquoted strings
2291+ let statuses = [
2292+ ( JobStatus :: Pending , "Pending" ) ,
2293+ ( JobStatus :: Running , "Running" ) ,
2294+ ( JobStatus :: Completed , "Completed" ) ,
2295+ ( JobStatus :: Failed , "Failed" ) ,
2296+ ( JobStatus :: Dead , "Dead" ) ,
2297+ ( JobStatus :: TimedOut , "TimedOut" ) ,
2298+ ( JobStatus :: Retrying , "Retrying" ) ,
2299+ ( JobStatus :: Archived , "Archived" ) ,
2300+ ] ;
2301+
2302+ for ( status, expected_str) in & statuses {
2303+ // This matches the logic in our Encode implementations
2304+ let encoded_str = match status {
2305+ JobStatus :: Pending => "Pending" ,
2306+ JobStatus :: Running => "Running" ,
2307+ JobStatus :: Completed => "Completed" ,
2308+ JobStatus :: Failed => "Failed" ,
2309+ JobStatus :: Dead => "Dead" ,
2310+ JobStatus :: TimedOut => "TimedOut" ,
2311+ JobStatus :: Retrying => "Retrying" ,
2312+ JobStatus :: Archived => "Archived" ,
2313+ } ;
2314+
2315+ assert_eq ! ( * expected_str, encoded_str, "Encoding mismatch for {:?}" , status) ;
2316+
2317+ // Verify the encoded string does not have quotes
2318+ assert ! ( !encoded_str. starts_with( '"' ) , "Encoded string should not start with quotes: {}" , encoded_str) ;
2319+ assert ! ( !encoded_str. ends_with( '"' ) , "Encoded string should not end with quotes: {}" , encoded_str) ;
2320+ }
2321+ }
21452322}
0 commit comments