Skip to main content

delta_funnel/sql_server/planning/
lifecycle.rs

1//! SQL Server table lifecycle planning.
2
3use crate::DeltaFunnelError;
4
5use super::{LoadMode, MssqlDdlPlan, MssqlSchemaPlan, MssqlTargetSummary};
6
7/// Expected live state of the target table before loading starts.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum MssqlTargetTableState {
10    /// Target table should already exist before loading.
11    Exists,
12    /// Target table should not exist before loading and should be created.
13    Absent,
14    /// Target table may already exist or may be created by this lifecycle.
15    ExistsOrAbsent,
16}
17
18/// Whether guarded execution may proceed after lifecycle planning.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum MssqlLifecycleGuardrailPolicy {
21    /// Planning succeeded and later execution phases may perform their work.
22    AllowedAfterPlanning,
23    /// Planning failed, so later execution phases must not run.
24    ForbiddenAfterPlanningFailure,
25}
26
27/// An execution operation guarded by successful lifecycle planning.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum MssqlLifecycleExecutionGuardrail {
30    /// Delta source scan execution must wait for lifecycle planning.
31    DeltaSourceScanExecution,
32    /// DataFusion physical plan execution must wait for lifecycle planning.
33    DataFusionPhysicalPlanExecution,
34    /// SQL Server connection attempts must wait for lifecycle planning.
35    SqlServerConnectionAttempt,
36    /// Target table existence probes must wait for lifecycle planning.
37    TargetTableExistenceProbe,
38    /// Create-table DDL execution must wait for lifecycle planning.
39    CreateTableDdlExecution,
40    /// Bulk writer construction must wait for lifecycle planning.
41    BulkWriterConstruction,
42    /// RecordBatch handoff polling must wait for lifecycle planning.
43    RecordBatchHandoffPolling,
44}
45
46/// Planned SQL Server table lifecycle behavior for one selected output.
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct MssqlLifecyclePlan {
49    target: MssqlTargetSummary,
50    expected_target_state: MssqlTargetTableState,
51    create_table_sql_required: bool,
52    create_table_sql_present: bool,
53    executable_in_mvp: bool,
54    guardrail_policy: MssqlLifecycleGuardrailPolicy,
55    execution_guardrails: &'static [MssqlLifecycleExecutionGuardrail],
56}
57
58impl MssqlLifecyclePlan {
59    /// Returns the redacted resolved target summary.
60    #[must_use]
61    pub fn target(&self) -> &MssqlTargetSummary {
62        &self.target
63    }
64
65    /// Returns the expected live target table state before loading.
66    #[must_use]
67    pub const fn expected_target_state(&self) -> MssqlTargetTableState {
68        self.expected_target_state
69    }
70
71    /// Returns true when create-table SQL is required by the lifecycle mode.
72    #[must_use]
73    pub const fn create_table_sql_required(&self) -> bool {
74        self.create_table_sql_required
75    }
76
77    /// Returns true when create-table SQL is present in the associated DDL plan.
78    #[must_use]
79    pub const fn create_table_sql_present(&self) -> bool {
80        self.create_table_sql_present
81    }
82
83    /// Returns true when this lifecycle mode can be executed in the MVP.
84    #[must_use]
85    pub const fn executable_in_mvp(&self) -> bool {
86        self.executable_in_mvp
87    }
88
89    /// Returns whether later execution phases may proceed.
90    #[must_use]
91    pub const fn guardrail_policy(&self) -> MssqlLifecycleGuardrailPolicy {
92        self.guardrail_policy
93    }
94
95    /// Returns guarded execution operations in the order they should run.
96    #[must_use]
97    pub const fn execution_guardrails(&self) -> &[MssqlLifecycleExecutionGuardrail] {
98        self.execution_guardrails
99    }
100}
101
102const APPEND_EXISTING_EXECUTION_GUARDRAILS: &[MssqlLifecycleExecutionGuardrail] = &[
103    MssqlLifecycleExecutionGuardrail::SqlServerConnectionAttempt,
104    MssqlLifecycleExecutionGuardrail::TargetTableExistenceProbe,
105    MssqlLifecycleExecutionGuardrail::BulkWriterConstruction,
106    MssqlLifecycleExecutionGuardrail::DeltaSourceScanExecution,
107    MssqlLifecycleExecutionGuardrail::DataFusionPhysicalPlanExecution,
108    MssqlLifecycleExecutionGuardrail::RecordBatchHandoffPolling,
109];
110
111const CREATE_AND_LOAD_EXECUTION_GUARDRAILS: &[MssqlLifecycleExecutionGuardrail] = &[
112    MssqlLifecycleExecutionGuardrail::SqlServerConnectionAttempt,
113    MssqlLifecycleExecutionGuardrail::TargetTableExistenceProbe,
114    MssqlLifecycleExecutionGuardrail::CreateTableDdlExecution,
115    MssqlLifecycleExecutionGuardrail::BulkWriterConstruction,
116    MssqlLifecycleExecutionGuardrail::DeltaSourceScanExecution,
117    MssqlLifecycleExecutionGuardrail::DataFusionPhysicalPlanExecution,
118    MssqlLifecycleExecutionGuardrail::RecordBatchHandoffPolling,
119];
120
121const REPLACE_EXECUTION_GUARDRAILS: &[MssqlLifecycleExecutionGuardrail] = &[
122    MssqlLifecycleExecutionGuardrail::SqlServerConnectionAttempt,
123    MssqlLifecycleExecutionGuardrail::TargetTableExistenceProbe,
124    MssqlLifecycleExecutionGuardrail::CreateTableDdlExecution,
125    MssqlLifecycleExecutionGuardrail::BulkWriterConstruction,
126    MssqlLifecycleExecutionGuardrail::DeltaSourceScanExecution,
127    MssqlLifecycleExecutionGuardrail::DataFusionPhysicalPlanExecution,
128    MssqlLifecycleExecutionGuardrail::RecordBatchHandoffPolling,
129];
130
131/// Plans lifecycle behavior for one selected output.
132///
133/// This function is deterministic and performs no I/O or execution. Errors return no
134/// partial lifecycle plan, which keeps later execution code from accidentally
135/// acting on a failed lifecycle decision.
136pub fn plan_mssql_lifecycle(
137    schema_plan: &MssqlSchemaPlan,
138    ddl_plan: Option<&MssqlDdlPlan>,
139) -> Result<MssqlLifecyclePlan, DeltaFunnelError> {
140    let target = schema_plan.target();
141
142    if let Some(ddl_plan) = ddl_plan {
143        ensure_matching_target(target, ddl_plan.target())?;
144    }
145
146    match target.load_mode() {
147        LoadMode::AppendExisting => plan_append_existing_lifecycle(target, ddl_plan),
148        LoadMode::CreateAndLoad => plan_create_and_load_lifecycle(target, ddl_plan),
149        LoadMode::Replace => plan_replace_lifecycle(target, ddl_plan),
150    }
151}
152
153fn plan_append_existing_lifecycle(
154    target: &MssqlTargetSummary,
155    ddl_plan: Option<&MssqlDdlPlan>,
156) -> Result<MssqlLifecyclePlan, DeltaFunnelError> {
157    if ddl_plan.and_then(MssqlDdlPlan::create_table_sql).is_some() {
158        return Err(lifecycle_error(
159            target,
160            "append-existing lifecycle must not carry create-table SQL",
161        ));
162    }
163
164    Ok(MssqlLifecyclePlan {
165        target: target.clone(),
166        expected_target_state: MssqlTargetTableState::Exists,
167        create_table_sql_required: false,
168        create_table_sql_present: false,
169        executable_in_mvp: true,
170        guardrail_policy: MssqlLifecycleGuardrailPolicy::AllowedAfterPlanning,
171        execution_guardrails: APPEND_EXISTING_EXECUTION_GUARDRAILS,
172    })
173}
174
175fn plan_create_and_load_lifecycle(
176    target: &MssqlTargetSummary,
177    ddl_plan: Option<&MssqlDdlPlan>,
178) -> Result<MssqlLifecyclePlan, DeltaFunnelError> {
179    let create_table_sql_present = ddl_plan.and_then(MssqlDdlPlan::create_table_sql).is_some();
180    if !create_table_sql_present {
181        return Err(lifecycle_error(
182            target,
183            "create-and-load lifecycle requires planned create-table SQL",
184        ));
185    }
186
187    Ok(MssqlLifecyclePlan {
188        target: target.clone(),
189        expected_target_state: MssqlTargetTableState::Absent,
190        create_table_sql_required: true,
191        create_table_sql_present,
192        executable_in_mvp: true,
193        guardrail_policy: MssqlLifecycleGuardrailPolicy::AllowedAfterPlanning,
194        execution_guardrails: CREATE_AND_LOAD_EXECUTION_GUARDRAILS,
195    })
196}
197
198fn plan_replace_lifecycle(
199    target: &MssqlTargetSummary,
200    ddl_plan: Option<&MssqlDdlPlan>,
201) -> Result<MssqlLifecyclePlan, DeltaFunnelError> {
202    let create_table_sql_present = ddl_plan
203        .map(MssqlDdlPlan::create_table_sql_present)
204        .unwrap_or(false);
205    if !create_table_sql_present {
206        return Err(lifecycle_error(
207            target,
208            "replace lifecycle requires planned create-table SQL",
209        ));
210    }
211
212    Ok(MssqlLifecyclePlan {
213        target: target.clone(),
214        expected_target_state: MssqlTargetTableState::ExistsOrAbsent,
215        create_table_sql_required: true,
216        create_table_sql_present,
217        executable_in_mvp: true,
218        guardrail_policy: MssqlLifecycleGuardrailPolicy::AllowedAfterPlanning,
219        execution_guardrails: REPLACE_EXECUTION_GUARDRAILS,
220    })
221}
222
223fn ensure_matching_target(
224    schema_target: &MssqlTargetSummary,
225    ddl_target: &MssqlTargetSummary,
226) -> Result<(), DeltaFunnelError> {
227    if schema_target.output_name() != ddl_target.output_name()
228        || schema_target.table() != ddl_target.table()
229        || schema_target.connection_source() != ddl_target.connection_source()
230        || schema_target.connection() != ddl_target.connection()
231    {
232        return Err(lifecycle_error(
233            schema_target,
234            "schema plan and DDL plan targets must match",
235        ));
236    }
237
238    Ok(())
239}
240
241fn lifecycle_error(target: &MssqlTargetSummary, message: impl Into<String>) -> DeltaFunnelError {
242    DeltaFunnelError::MssqlLifecyclePlanning {
243        output_name: target.output_name().to_owned(),
244        message: message.into(),
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use arrow_schema::{DataType, Field, Schema};
251    use arrow_tiberius::PlanOptions;
252
253    use super::*;
254    use crate::{
255        MssqlConnectionConfig, MssqlTargetConfig, MssqlTargetResolutionContext, MssqlTargetTable,
256        plan_mssql_create_table_ddl, plan_mssql_output_schema,
257    };
258
259    fn secret_connection(label: &str) -> Result<MssqlConnectionConfig, DeltaFunnelError> {
260        Ok(MssqlConnectionConfig::new(
261            "server=tcp:sql.example.com;database=warehouse;user=admin;password=secret-token",
262        )?
263        .with_display_label(label))
264    }
265
266    fn schema_plan(
267        output_name: &str,
268        load_mode: LoadMode,
269        table: MssqlTargetTable,
270    ) -> Result<MssqlSchemaPlan, DeltaFunnelError> {
271        let connection = secret_connection("warehouse-primary")?;
272        let target = MssqlTargetConfig::new(table)
273            .with_load_mode(load_mode)
274            .resolve(MssqlTargetResolutionContext {
275                output_name: Some(output_name),
276                default_connection: Some(&connection),
277            })?;
278        let schema = Schema::new(vec![
279            Field::new("order_id", DataType::Int64, false),
280            Field::new("status", DataType::Utf8, true),
281        ]);
282
283        plan_mssql_output_schema(&schema, &target, PlanOptions::default())
284    }
285
286    #[test]
287    fn append_existing_reports_expected_existing_behavior() -> Result<(), DeltaFunnelError> {
288        let schema_plan = schema_plan(
289            "orders_output",
290            LoadMode::AppendExisting,
291            MssqlTargetTable::new("dbo", "orders")?,
292        )?;
293        let ddl_plan = plan_mssql_create_table_ddl(&schema_plan)?;
294
295        let lifecycle = plan_mssql_lifecycle(&schema_plan, Some(&ddl_plan))?;
296
297        assert_eq!(lifecycle.target().output_name(), "orders_output");
298        assert_eq!(
299            lifecycle.expected_target_state(),
300            MssqlTargetTableState::Exists
301        );
302        assert!(!lifecycle.create_table_sql_required());
303        assert!(!lifecycle.create_table_sql_present());
304        assert!(lifecycle.executable_in_mvp());
305        assert_eq!(
306            lifecycle.guardrail_policy(),
307            MssqlLifecycleGuardrailPolicy::AllowedAfterPlanning
308        );
309        assert_eq!(
310            lifecycle.execution_guardrails(),
311            APPEND_EXISTING_EXECUTION_GUARDRAILS
312        );
313        Ok(())
314    }
315
316    #[test]
317    fn create_and_load_reports_expected_absent_behavior() -> Result<(), DeltaFunnelError> {
318        let primary_schema_plan = schema_plan(
319            "orders_output",
320            LoadMode::CreateAndLoad,
321            MssqlTargetTable::new("dbo", "orders")?,
322        )?;
323        let ddl_plan = plan_mssql_create_table_ddl(&primary_schema_plan)?;
324
325        let lifecycle = plan_mssql_lifecycle(&primary_schema_plan, Some(&ddl_plan))?;
326
327        assert_eq!(
328            lifecycle.expected_target_state(),
329            MssqlTargetTableState::Absent
330        );
331        assert!(lifecycle.create_table_sql_required());
332        assert!(lifecycle.create_table_sql_present());
333        assert!(lifecycle.executable_in_mvp());
334        assert_eq!(
335            lifecycle.guardrail_policy(),
336            MssqlLifecycleGuardrailPolicy::AllowedAfterPlanning
337        );
338        assert_eq!(
339            lifecycle.execution_guardrails(),
340            CREATE_AND_LOAD_EXECUTION_GUARDRAILS
341        );
342        Ok(())
343    }
344
345    #[test]
346    fn lifecycle_reports_guarded_execution_by_load_mode() -> Result<(), DeltaFunnelError> {
347        let append_schema_plan = schema_plan(
348            "append_output",
349            LoadMode::AppendExisting,
350            MssqlTargetTable::new("dbo", "orders")?,
351        )?;
352        let append_ddl_plan = plan_mssql_create_table_ddl(&append_schema_plan)?;
353        let append_lifecycle = plan_mssql_lifecycle(&append_schema_plan, Some(&append_ddl_plan))?;
354
355        assert_eq!(
356            append_lifecycle.execution_guardrails(),
357            &[
358                MssqlLifecycleExecutionGuardrail::SqlServerConnectionAttempt,
359                MssqlLifecycleExecutionGuardrail::TargetTableExistenceProbe,
360                MssqlLifecycleExecutionGuardrail::BulkWriterConstruction,
361                MssqlLifecycleExecutionGuardrail::DeltaSourceScanExecution,
362                MssqlLifecycleExecutionGuardrail::DataFusionPhysicalPlanExecution,
363                MssqlLifecycleExecutionGuardrail::RecordBatchHandoffPolling,
364            ]
365        );
366
367        let create_schema_plan = schema_plan(
368            "create_output",
369            LoadMode::CreateAndLoad,
370            MssqlTargetTable::new("dbo", "orders_archive")?,
371        )?;
372        let create_ddl_plan = plan_mssql_create_table_ddl(&create_schema_plan)?;
373        let create_lifecycle = plan_mssql_lifecycle(&create_schema_plan, Some(&create_ddl_plan))?;
374
375        assert_eq!(
376            create_lifecycle.execution_guardrails(),
377            &[
378                MssqlLifecycleExecutionGuardrail::SqlServerConnectionAttempt,
379                MssqlLifecycleExecutionGuardrail::TargetTableExistenceProbe,
380                MssqlLifecycleExecutionGuardrail::CreateTableDdlExecution,
381                MssqlLifecycleExecutionGuardrail::BulkWriterConstruction,
382                MssqlLifecycleExecutionGuardrail::DeltaSourceScanExecution,
383                MssqlLifecycleExecutionGuardrail::DataFusionPhysicalPlanExecution,
384                MssqlLifecycleExecutionGuardrail::RecordBatchHandoffPolling,
385            ]
386        );
387        Ok(())
388    }
389
390    #[test]
391    fn create_and_load_requires_planned_create_table_sql() -> Result<(), DeltaFunnelError> {
392        let primary_schema_plan = schema_plan(
393            "orders_output",
394            LoadMode::CreateAndLoad,
395            MssqlTargetTable::new("dbo", "orders")?,
396        )?;
397
398        let error = plan_mssql_lifecycle(&primary_schema_plan, None)
399            .err()
400            .ok_or_else(|| DeltaFunnelError::Config {
401                message: "expected missing create-table SQL error".to_owned(),
402            })?;
403
404        assert!(matches!(
405            error,
406            DeltaFunnelError::MssqlLifecyclePlanning { .. }
407        ));
408        assert!(
409            error
410                .to_string()
411                .contains("requires planned create-table SQL")
412        );
413        Ok(())
414    }
415
416    #[test]
417    fn replace_reports_flexible_target_state_and_create_sql() -> Result<(), DeltaFunnelError> {
418        let schema_plan = schema_plan(
419            "orders_output",
420            LoadMode::Replace,
421            MssqlTargetTable::new("dbo", "orders")?,
422        )?;
423        let ddl_plan = plan_mssql_create_table_ddl(&schema_plan)?;
424
425        let lifecycle = plan_mssql_lifecycle(&schema_plan, Some(&ddl_plan))?;
426
427        assert_eq!(
428            lifecycle.expected_target_state(),
429            MssqlTargetTableState::ExistsOrAbsent
430        );
431        assert!(lifecycle.create_table_sql_required());
432        assert!(lifecycle.create_table_sql_present());
433        assert!(lifecycle.executable_in_mvp());
434        assert_eq!(
435            lifecycle.guardrail_policy(),
436            MssqlLifecycleGuardrailPolicy::AllowedAfterPlanning
437        );
438        assert_eq!(
439            lifecycle.execution_guardrails(),
440            REPLACE_EXECUTION_GUARDRAILS
441        );
442        Ok(())
443    }
444
445    #[test]
446    fn replace_requires_planned_create_table_sql() -> Result<(), DeltaFunnelError> {
447        let schema_plan = schema_plan(
448            "orders_output",
449            LoadMode::Replace,
450            MssqlTargetTable::new("dbo", "orders")?,
451        )?;
452
453        let error = plan_mssql_lifecycle(&schema_plan, None)
454            .err()
455            .ok_or_else(|| DeltaFunnelError::Config {
456                message: "expected missing replace create-table SQL error".to_owned(),
457            })?;
458
459        assert!(matches!(
460            error,
461            DeltaFunnelError::MssqlLifecyclePlanning { .. }
462        ));
463        assert!(
464            error
465                .to_string()
466                .contains("requires planned create-table SQL")
467        );
468        Ok(())
469    }
470
471    #[test]
472    fn lifecycle_failures_block_guarded_execution() -> Result<(), DeltaFunnelError> {
473        let missing_create_sql_plan = schema_plan(
474            "create_output",
475            LoadMode::CreateAndLoad,
476            MssqlTargetTable::new("dbo", "orders_archive")?,
477        )?;
478        let append_plan = schema_plan(
479            "append_output",
480            LoadMode::AppendExisting,
481            MssqlTargetTable::new("dbo", "orders_existing")?,
482        )?;
483        let create_plan = schema_plan(
484            "append_output",
485            LoadMode::CreateAndLoad,
486            MssqlTargetTable::new("dbo", "orders_existing")?,
487        )?;
488        let contradictory_ddl_plan = plan_mssql_create_table_ddl(&create_plan)?;
489
490        let cases = [
491            (
492                "missing create-table SQL",
493                plan_mssql_lifecycle(&missing_create_sql_plan, None),
494            ),
495            (
496                "append-existing create-table SQL contradiction",
497                plan_mssql_lifecycle(&append_plan, Some(&contradictory_ddl_plan)),
498            ),
499        ];
500
501        for (case_name, result) in cases {
502            let mut attempted_execution = Vec::new();
503            let error = match result {
504                Ok(_lifecycle) => {
505                    fake_guarded_execution(ALL_GUARDED_EXECUTION, &mut attempted_execution);
506                    return Err(DeltaFunnelError::Config {
507                        message: format!("expected {case_name} lifecycle error"),
508                    });
509                }
510                Err(error) => error,
511            };
512
513            assert!(matches!(
514                error,
515                DeltaFunnelError::MssqlLifecyclePlanning { .. }
516            ));
517            assert!(attempted_execution.is_empty());
518        }
519
520        Ok(())
521    }
522
523    #[test]
524    fn append_existing_rejects_create_table_sql_contradiction() -> Result<(), DeltaFunnelError> {
525        let append_plan = schema_plan(
526            "orders_output",
527            LoadMode::AppendExisting,
528            MssqlTargetTable::new("dbo", "orders")?,
529        )?;
530        let create_plan = schema_plan(
531            "orders_output",
532            LoadMode::CreateAndLoad,
533            MssqlTargetTable::new("dbo", "orders")?,
534        )?;
535        let ddl_plan = plan_mssql_create_table_ddl(&create_plan)?;
536
537        let error = plan_mssql_lifecycle(&append_plan, Some(&ddl_plan))
538            .err()
539            .ok_or_else(|| DeltaFunnelError::Config {
540                message: "expected append/create SQL contradiction".to_owned(),
541            })?;
542
543        assert!(
544            error
545                .to_string()
546                .contains("must not carry create-table SQL")
547        );
548        Ok(())
549    }
550
551    #[test]
552    fn mismatched_schema_and_ddl_targets_are_rejected() -> Result<(), DeltaFunnelError> {
553        let primary_schema_plan = schema_plan(
554            "orders_output",
555            LoadMode::CreateAndLoad,
556            MssqlTargetTable::new("dbo", "orders")?,
557        )?;
558        let other_schema_plan = schema_plan(
559            "other_output",
560            LoadMode::CreateAndLoad,
561            MssqlTargetTable::new("dbo", "other_orders")?,
562        )?;
563        let ddl_plan = plan_mssql_create_table_ddl(&other_schema_plan)?;
564
565        let error = plan_mssql_lifecycle(&primary_schema_plan, Some(&ddl_plan))
566            .err()
567            .ok_or_else(|| DeltaFunnelError::Config {
568                message: "expected target mismatch error".to_owned(),
569            })?;
570
571        assert!(error.to_string().contains("targets must match"));
572        Ok(())
573    }
574
575    #[test]
576    fn reports_and_errors_do_not_expose_connection_secrets() -> Result<(), DeltaFunnelError> {
577        let create_schema_plan = schema_plan(
578            "orders_output",
579            LoadMode::CreateAndLoad,
580            MssqlTargetTable::new("dbo", "orders")?,
581        )?;
582        let ddl_plan = plan_mssql_create_table_ddl(&create_schema_plan)?;
583        let lifecycle = plan_mssql_lifecycle(&create_schema_plan, Some(&ddl_plan))?;
584
585        let combined = format!("{lifecycle:?}");
586        assert!(!combined.contains("secret-token"));
587        assert!(!combined.contains("password"));
588        assert!(!combined.contains("server=tcp"));
589        assert!(combined.contains("warehouse-primary"));
590        Ok(())
591    }
592
593    const ALL_GUARDED_EXECUTION: &[MssqlLifecycleExecutionGuardrail] = &[
594        MssqlLifecycleExecutionGuardrail::DeltaSourceScanExecution,
595        MssqlLifecycleExecutionGuardrail::DataFusionPhysicalPlanExecution,
596        MssqlLifecycleExecutionGuardrail::SqlServerConnectionAttempt,
597        MssqlLifecycleExecutionGuardrail::TargetTableExistenceProbe,
598        MssqlLifecycleExecutionGuardrail::CreateTableDdlExecution,
599        MssqlLifecycleExecutionGuardrail::BulkWriterConstruction,
600        MssqlLifecycleExecutionGuardrail::RecordBatchHandoffPolling,
601    ];
602
603    fn fake_guarded_execution(
604        guardrails: &[MssqlLifecycleExecutionGuardrail],
605        attempted_execution: &mut Vec<MssqlLifecycleExecutionGuardrail>,
606    ) {
607        attempted_execution.extend(guardrails.iter().copied());
608    }
609}