@@ -11,6 +11,15 @@ use chrono::{DateTime, Utc};
11
11
use serde:: { Deserialize , Serialize } ;
12
12
use uuid:: Uuid ;
13
13
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
+
14
23
/// Unique identifier for a job.
15
24
///
16
25
/// Each job gets a unique UUID when created to enable tracking and management
@@ -52,6 +61,96 @@ pub enum JobStatus {
52
61
Archived ,
53
62
}
54
63
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
+
55
154
/// Configuration for job result storage.
56
155
///
57
156
/// This enum determines where and how job results are stored when jobs complete successfully.
@@ -2142,4 +2241,82 @@ mod tests {
2142
2241
assert ! ( job. timed_out_at. is_some( ) ) ;
2143
2242
assert ! ( job. error_message. is_some( ) ) ;
2144
2243
}
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
+ }
2145
2322
}
0 commit comments