@@ -29,7 +29,8 @@ use crate::pipelines::{query_job_by_pub_id, query_pipeline_by_pub_id};
2929use crate :: rest:: AppState ;
3030use crate :: rest_utils:: {
3131 BearerAuth , ErrorResp , PipelineJobCheckpointPath , PipelineJobPath , authenticate, bad_request,
32- log_and_map, not_found, paginate_results, validate_pagination_params,
32+ conflict, internal_server_error, log_and_map, not_found, paginate_results,
33+ validate_pagination_params,
3334} ;
3435use crate :: types:: public:: LogLevel ;
3536use crate :: { AuthData , queries:: api_queries, to_micros, types:: public} ;
@@ -194,6 +195,76 @@ pub(crate) async fn create_job(
194195 Ok ( job_id)
195196}
196197
198+ fn replaceable_job ( jobs : Vec < DbPipelineJob > ) -> Result < String , ErrorResp > {
199+ let count = jobs. len ( ) ;
200+ let Some ( job) = jobs. into_iter ( ) . next ( ) else {
201+ return Err ( not_found ( "Job for pipeline" ) ) ;
202+ } ;
203+
204+ if count != 1 {
205+ return Err ( internal_server_error ( format ! (
206+ "expected one job for pipeline, found {count}"
207+ ) ) ) ;
208+ }
209+
210+ let state = job. state . unwrap_or_else ( || "Created" . to_string ( ) ) ;
211+ if state != "Stopped" && state != "Failed" {
212+ return Err ( conflict ( format ! (
213+ "cannot restart job {} without state while it is in state {state}; stop the job first" ,
214+ job. id
215+ ) ) ) ;
216+ }
217+
218+ Ok ( job. id )
219+ }
220+
221+ /// Replaces a pipeline's single terminal job with a fresh job.
222+ pub ( crate ) async fn replace_job_without_state (
223+ db : & DatabaseSource ,
224+ pipeline_pub_id : & str ,
225+ auth : & AuthData ,
226+ ) -> Result < String , ErrorResp > {
227+ let new_job_id = generate_id ( IdTypes :: JobConfig ) ;
228+ let new_status_id = generate_id ( IdTypes :: JobStatus ) ;
229+ let database = db. client ( ) . await ?;
230+ let jobs =
231+ api_queries:: fetch_get_pipeline_jobs ( & database, & auth. organization_id , & pipeline_pub_id)
232+ . await ?;
233+ let old_job_id = replaceable_job ( jobs) ?;
234+
235+ // TODO: this really should all be within a transaction, however we can't use transactions
236+ // today due to our sqlite/postgres compatibility layer
237+ let inserted = api_queries:: execute_clone_job_for_restart (
238+ & database,
239+ & new_job_id,
240+ & auth. user_id ,
241+ & old_job_id,
242+ & auth. organization_id ,
243+ )
244+ . await ?;
245+ if inserted != 1 {
246+ return Err ( internal_server_error ( "failed to clone job configuration" ) ) ;
247+ }
248+
249+ api_queries:: execute_create_job_status (
250+ & database,
251+ & new_status_id,
252+ & new_job_id,
253+ & auth. organization_id ,
254+ )
255+ . await ?;
256+
257+ // Delete children explicitly because the SQLite checkpoints table does not have the same
258+ // foreign-key cascade as PostgreSQL.
259+ api_queries:: execute_delete_job_checkpoints ( & database, & old_job_id, & auth. organization_id )
260+ . await ?;
261+ api_queries:: execute_delete_job_log_messages ( & database, & old_job_id) . await ?;
262+ api_queries:: execute_delete_job_status ( & database, & old_job_id, & auth. organization_id ) . await ?;
263+ api_queries:: execute_delete_job_config ( & database, & old_job_id, & auth. organization_id ) . await ?;
264+
265+ Ok ( new_job_id)
266+ }
267+
197268pub ( crate ) fn get_action ( state : & str , running_desired : & bool ) -> ( String , Option < StopType > , bool ) {
198269 enum Progress {
199270 InProgress ,
0 commit comments