Skip to main content

icydb_core/db/sql/ddl/
mod.rs

1//! Module: db::sql::ddl
2//! Responsibility: bind parsed SQL DDL to accepted schema catalog contracts.
3//! Does not own: mutation planning, physical index rebuilds, or SQL execution.
4//! Boundary: translates parser-owned DDL syntax into catalog-native requests.
5
6#![allow(
7    dead_code,
8    reason = "DDL binding exposes prepare-only diagnostics and test-only inspection accessors"
9)]
10
11mod field;
12pub(in crate::db) use field::{
13    BoundSqlAddColumnRequest, BoundSqlAlterColumnDefaultRequest,
14    BoundSqlAlterColumnNullabilityRequest, BoundSqlDropColumnRequest, BoundSqlRenameColumnRequest,
15};
16use field::{
17    bind_alter_table_add_column_statement, bind_alter_table_alter_column_statement,
18    bind_alter_table_drop_column_statement, bind_alter_table_rename_column_statement,
19};
20
21mod index;
22pub(in crate::db) use index::{BoundSqlCreateIndexRequest, BoundSqlDropIndexRequest};
23use index::{bind_create_index_statement, bind_drop_index_statement, ddl_key_item_report};
24
25use crate::db::{
26    schema::{
27        AcceptedSchemaSnapshot, SchemaDdlAcceptedSnapshotDerivation, SchemaDdlMutationAdmission,
28        SchemaDdlMutationAdmissionError, SchemaInfo, admit_sql_ddl_expression_index_candidate,
29        admit_sql_ddl_field_addition_candidate, admit_sql_ddl_field_default_candidate,
30        admit_sql_ddl_field_drop_candidate, admit_sql_ddl_field_nullability_candidate,
31        admit_sql_ddl_field_path_index_candidate, admit_sql_ddl_field_rename_candidate,
32        admit_sql_ddl_secondary_index_drop_candidate,
33        derive_sql_ddl_expression_index_accepted_after,
34        derive_sql_ddl_field_addition_accepted_after, derive_sql_ddl_field_default_accepted_after,
35        derive_sql_ddl_field_drop_accepted_after, derive_sql_ddl_field_nullability_accepted_after,
36        derive_sql_ddl_field_path_index_accepted_after, derive_sql_ddl_field_rename_accepted_after,
37        derive_sql_ddl_secondary_index_drop_accepted_after,
38    },
39    sql::parser::{SqlDdlStatement, SqlStatement},
40};
41use thiserror::Error as ThisError;
42
43///
44/// PreparedSqlDdlCommand
45///
46/// Fully prepared SQL DDL command. This is intentionally not executable yet:
47/// it packages the accepted-catalog binding, accepted-after derivation, and
48/// schema mutation admission proof for the future execution boundary.
49///
50#[derive(Clone, Debug, Eq, PartialEq)]
51pub(in crate::db) struct PreparedSqlDdlCommand {
52    bound: BoundSqlDdlRequest,
53    derivation: Option<SchemaDdlAcceptedSnapshotDerivation>,
54    report: SqlDdlPreparationReport,
55}
56
57impl PreparedSqlDdlCommand {
58    /// Borrow the accepted-catalog-bound DDL request.
59    #[must_use]
60    pub(in crate::db) const fn bound(&self) -> &BoundSqlDdlRequest {
61        &self.bound
62    }
63
64    /// Borrow the accepted-after derivation proof.
65    #[must_use]
66    pub(in crate::db) const fn derivation(&self) -> Option<&SchemaDdlAcceptedSnapshotDerivation> {
67        self.derivation.as_ref()
68    }
69
70    /// Borrow the developer-facing preparation report.
71    #[must_use]
72    pub(in crate::db) const fn report(&self) -> &SqlDdlPreparationReport {
73        &self.report
74    }
75
76    /// Return whether this prepared command needs schema or storage mutation.
77    #[must_use]
78    pub(in crate::db) const fn mutates_schema(&self) -> bool {
79        self.derivation.is_some()
80    }
81}
82
83///
84/// SqlDdlPreparationReport
85///
86/// Compact report for a DDL command that has passed all pre-execution
87/// frontend and schema-mutation checks.
88///
89#[derive(Clone, Debug, Eq, PartialEq)]
90pub struct SqlDdlPreparationReport {
91    mutation_kind: SqlDdlMutationKind,
92    target_index: String,
93    target_store: String,
94    field_path: Vec<String>,
95    execution_status: SqlDdlExecutionStatus,
96    rows_scanned: usize,
97    index_keys_written: usize,
98}
99
100impl SqlDdlPreparationReport {
101    /// Return the prepared DDL mutation kind.
102    #[must_use]
103    pub const fn mutation_kind(&self) -> SqlDdlMutationKind {
104        self.mutation_kind
105    }
106
107    /// Borrow the target accepted index name.
108    #[must_use]
109    pub const fn target_index(&self) -> &str {
110        self.target_index.as_str()
111    }
112
113    /// Borrow the target accepted index store path.
114    #[must_use]
115    pub const fn target_store(&self) -> &str {
116        self.target_store.as_str()
117    }
118
119    /// Borrow the target field path.
120    #[must_use]
121    pub const fn field_path(&self) -> &[String] {
122        self.field_path.as_slice()
123    }
124
125    /// Return the execution status captured by this DDL report.
126    #[must_use]
127    pub const fn execution_status(&self) -> SqlDdlExecutionStatus {
128        self.execution_status
129    }
130
131    /// Return rows scanned by DDL execution.
132    #[must_use]
133    pub const fn rows_scanned(&self) -> usize {
134        self.rows_scanned
135    }
136
137    /// Return index keys written by DDL execution.
138    #[must_use]
139    pub const fn index_keys_written(&self) -> usize {
140        self.index_keys_written
141    }
142
143    pub(in crate::db) const fn with_execution_status(
144        mut self,
145        execution_status: SqlDdlExecutionStatus,
146    ) -> Self {
147        self.execution_status = execution_status;
148        self
149    }
150
151    pub(in crate::db) const fn with_execution_metrics(
152        mut self,
153        rows_scanned: usize,
154        index_keys_written: usize,
155    ) -> Self {
156        self.rows_scanned = rows_scanned;
157        self.index_keys_written = index_keys_written;
158        self
159    }
160}
161
162///
163/// SqlDdlMutationKind
164///
165/// Developer-facing SQL DDL mutation kind.
166///
167#[derive(Clone, Copy, Debug, Eq, PartialEq)]
168pub enum SqlDdlMutationKind {
169    AddDefaultedField,
170    AddNullableField,
171    SetFieldDefault,
172    DropFieldDefault,
173    SetFieldNotNull,
174    DropFieldNotNull,
175    DropField,
176    RenameField,
177    AddFieldPathIndex,
178    AddExpressionIndex,
179    DropSecondaryIndex,
180}
181
182impl SqlDdlMutationKind {
183    /// Return the stable diagnostic label for this DDL mutation kind.
184    #[must_use]
185    pub const fn as_str(self) -> &'static str {
186        match self {
187            Self::AddDefaultedField => "add_defaulted_field",
188            Self::AddNullableField => "add_nullable_field",
189            Self::SetFieldDefault => "set_field_default",
190            Self::DropFieldDefault => "drop_field_default",
191            Self::SetFieldNotNull => "set_field_not_null",
192            Self::DropFieldNotNull => "drop_field_not_null",
193            Self::DropField => "drop_field",
194            Self::RenameField => "rename_field",
195            Self::AddFieldPathIndex => "add_field_path_index",
196            Self::AddExpressionIndex => "add_expression_index",
197            Self::DropSecondaryIndex => "drop_secondary_index",
198        }
199    }
200}
201
202///
203/// SqlDdlExecutionStatus
204///
205/// SQL DDL execution state at the current boundary.
206///
207#[derive(Clone, Copy, Debug, Eq, PartialEq)]
208pub enum SqlDdlExecutionStatus {
209    PreparedOnly,
210    Published,
211    NoOp,
212}
213
214impl SqlDdlExecutionStatus {
215    /// Return the stable diagnostic label for this execution status.
216    #[must_use]
217    pub const fn as_str(self) -> &'static str {
218        match self {
219            Self::PreparedOnly => "prepared_only",
220            Self::Published => "published",
221            Self::NoOp => "no_op",
222        }
223    }
224}
225
226///
227/// BoundSqlDdlRequest
228///
229/// Accepted-catalog SQL DDL request after parser syntax has been resolved
230/// against one runtime schema snapshot.
231///
232#[derive(Clone, Debug, Eq, PartialEq)]
233pub(in crate::db) struct BoundSqlDdlRequest {
234    statement: BoundSqlDdlStatement,
235}
236
237impl BoundSqlDdlRequest {
238    /// Borrow the bound statement payload.
239    #[must_use]
240    pub(in crate::db) const fn statement(&self) -> &BoundSqlDdlStatement {
241        &self.statement
242    }
243}
244
245///
246/// BoundSqlDdlStatement
247///
248/// Catalog-resolved DDL statement vocabulary.
249///
250#[derive(Clone, Debug, Eq, PartialEq)]
251pub(in crate::db) enum BoundSqlDdlStatement {
252    AddColumn(BoundSqlAddColumnRequest),
253    AlterColumnDefault(BoundSqlAlterColumnDefaultRequest),
254    AlterColumnNullability(BoundSqlAlterColumnNullabilityRequest),
255    DropColumn(BoundSqlDropColumnRequest),
256    RenameColumn(BoundSqlRenameColumnRequest),
257    CreateIndex(BoundSqlCreateIndexRequest),
258    DropIndex(BoundSqlDropIndexRequest),
259    NoOp(BoundSqlDdlNoOpRequest),
260}
261
262///
263/// BoundSqlDdlNoOpRequest
264///
265/// Catalog-resolved idempotent DDL request that is already satisfied or absent.
266///
267#[derive(Clone, Debug, Eq, PartialEq)]
268pub(in crate::db) struct BoundSqlDdlNoOpRequest {
269    mutation_kind: SqlDdlMutationKind,
270    index_name: String,
271    entity_name: String,
272    target_store: String,
273    field_path: Vec<String>,
274}
275
276impl BoundSqlDdlNoOpRequest {
277    /// Return the user-facing mutation family this no-op belongs to.
278    #[must_use]
279    pub(in crate::db) const fn mutation_kind(&self) -> SqlDdlMutationKind {
280        self.mutation_kind
281    }
282
283    /// Borrow the requested index name.
284    #[must_use]
285    pub(in crate::db) const fn index_name(&self) -> &str {
286        self.index_name.as_str()
287    }
288
289    /// Borrow the accepted entity name that owns this request.
290    #[must_use]
291    pub(in crate::db) const fn entity_name(&self) -> &str {
292        self.entity_name.as_str()
293    }
294
295    /// Borrow the accepted index store path, or `-` when no target exists.
296    #[must_use]
297    pub(in crate::db) const fn target_store(&self) -> &str {
298        self.target_store.as_str()
299    }
300
301    /// Borrow the target field path, empty when no target exists.
302    #[must_use]
303    pub(in crate::db) const fn field_path(&self) -> &[String] {
304        self.field_path.as_slice()
305    }
306}
307
308///
309/// SqlDdlBindError
310///
311/// Typed fail-closed reasons for SQL DDL catalog binding.
312///
313#[derive(Debug, Eq, PartialEq, ThisError)]
314pub(in crate::db) enum SqlDdlBindError {
315    #[error("SQL DDL binder requires a DDL statement")]
316    NotDdl,
317
318    #[error("accepted schema does not expose an entity name")]
319    MissingEntityName,
320
321    #[error("accepted schema does not expose an entity path")]
322    MissingEntityPath,
323
324    #[error("SQL entity '{sql_entity}' does not match accepted entity '{expected_entity}'")]
325    EntityMismatch {
326        sql_entity: String,
327        expected_entity: String,
328    },
329
330    #[error("unknown field path '{field_path}' for accepted entity '{entity_name}'")]
331    UnknownFieldPath {
332        entity_name: String,
333        field_path: String,
334    },
335
336    #[error("field path '{field_path}' is not indexable")]
337    FieldPathNotIndexable { field_path: String },
338
339    #[error("field path '{field_path}' depends on generated-only metadata")]
340    FieldPathNotAcceptedCatalogBacked { field_path: String },
341
342    #[error("invalid filtered index predicate: {detail}")]
343    InvalidFilteredIndexPredicate { detail: String },
344
345    #[error("index name '{index_name}' already exists in the accepted schema")]
346    DuplicateIndexName { index_name: String },
347
348    #[error("accepted schema already has index '{existing_index}' for field path '{field_path}'")]
349    DuplicateFieldPathIndex {
350        field_path: String,
351        existing_index: String,
352    },
353
354    #[error("unknown index '{index_name}' for accepted entity '{entity_name}'")]
355    UnknownIndex {
356        entity_name: String,
357        index_name: String,
358    },
359
360    #[error(
361        "index '{index_name}' is generated by the entity model and cannot be dropped with SQL DDL; remove the index from the entity schema macro instead"
362    )]
363    GeneratedIndexDropRejected { index_name: String },
364
365    #[error(
366        "index '{index_name}' is not a supported DDL-droppable secondary index; SQL DDL can currently drop only indexes created through SQL DDL"
367    )]
368    UnsupportedDropIndex { index_name: String },
369
370    #[error(
371        "SQL DDL ALTER TABLE ADD COLUMN is not executable yet for accepted entity '{entity_name}' column '{column_name}'"
372    )]
373    UnsupportedAlterTableAddColumn {
374        entity_name: String,
375        column_name: String,
376    },
377
378    #[error(
379        "SQL DDL ALTER TABLE ADD COLUMN DEFAULT value is not encodable for accepted entity '{entity_name}' column '{column_name}': {detail}"
380    )]
381    InvalidAlterTableAddColumnDefault {
382        entity_name: String,
383        column_name: String,
384        detail: String,
385    },
386
387    #[error(
388        "SQL DDL ALTER TABLE ADD COLUMN NOT NULL is not executable yet for accepted entity '{entity_name}' column '{column_name}'"
389    )]
390    UnsupportedAlterTableAddColumnNotNull {
391        entity_name: String,
392        column_name: String,
393    },
394
395    #[error("field '{column_name}' already exists in accepted entity '{entity_name}'")]
396    DuplicateColumn {
397        entity_name: String,
398        column_name: String,
399    },
400
401    #[error(
402        "SQL DDL ALTER TABLE ADD COLUMN type '{column_type}' is not supported yet for accepted entity '{entity_name}' column '{column_name}'"
403    )]
404    UnsupportedAlterTableAddColumnType {
405        entity_name: String,
406        column_name: String,
407        column_type: String,
408    },
409
410    #[error("unknown column '{column_name}' for accepted entity '{entity_name}'")]
411    UnknownColumn {
412        entity_name: String,
413        column_name: String,
414    },
415
416    #[error(
417        "SQL DDL ALTER TABLE ALTER COLUMN {action} is not executable yet for accepted entity '{entity_name}' column '{column_name}'"
418    )]
419    UnsupportedAlterTableAlterColumn {
420        entity_name: String,
421        column_name: String,
422        action: String,
423    },
424
425    #[error(
426        "SQL DDL ALTER TABLE ALTER COLUMN SET DEFAULT value is not encodable for accepted entity '{entity_name}' column '{column_name}': {detail}"
427    )]
428    InvalidAlterTableAlterColumnDefault {
429        entity_name: String,
430        column_name: String,
431        detail: String,
432    },
433
434    #[error(
435        "SQL DDL ALTER TABLE ALTER COLUMN DROP DEFAULT is not executable yet for required accepted entity '{entity_name}' column '{column_name}'"
436    )]
437    UnsupportedAlterTableDropDefaultRequired {
438        entity_name: String,
439        column_name: String,
440    },
441
442    #[error(
443        "SQL DDL ALTER TABLE ALTER COLUMN DEFAULT cannot change generated accepted field '{column_name}' on entity '{entity_name}'; change the Rust schema default instead"
444    )]
445    GeneratedFieldDefaultChangeRejected {
446        entity_name: String,
447        column_name: String,
448    },
449
450    #[error(
451        "SQL DDL ALTER TABLE ALTER COLUMN NULLABILITY cannot change generated accepted field '{column_name}' on entity '{entity_name}'; change the Rust schema nullability instead"
452    )]
453    GeneratedFieldNullabilityChangeRejected {
454        entity_name: String,
455        column_name: String,
456    },
457
458    #[error(
459        "SQL DDL ALTER TABLE DROP COLUMN cannot drop primary-key field '{column_name}' on entity '{entity_name}'"
460    )]
461    PrimaryKeyFieldDropRejected {
462        entity_name: String,
463        column_name: String,
464    },
465
466    #[error(
467        "SQL DDL ALTER TABLE DROP COLUMN cannot change generated accepted field '{column_name}' on entity '{entity_name}'; remove the field from the Rust schema instead"
468    )]
469    GeneratedFieldDropRejected {
470        entity_name: String,
471        column_name: String,
472    },
473
474    #[error(
475        "SQL DDL ALTER TABLE DROP COLUMN cannot drop accepted field '{column_name}' on entity '{entity_name}' while index '{index_name}' depends on it; drop dependent DDL-owned indexes first"
476    )]
477    IndexedFieldDropRejected {
478        entity_name: String,
479        column_name: String,
480        index_name: String,
481    },
482
483    #[error(
484        "SQL DDL ALTER TABLE RENAME COLUMN cannot change generated accepted field '{column_name}' on entity '{entity_name}'; rename the field in the Rust schema instead"
485    )]
486    GeneratedFieldRenameRejected {
487        entity_name: String,
488        column_name: String,
489    },
490
491    #[error(
492        "SQL DDL ALTER TABLE RENAME COLUMN is not executable for accepted entity '{entity_name}' column '{old_column_name}' to '{new_column_name}'"
493    )]
494    UnsupportedAlterTableRenameColumn {
495        entity_name: String,
496        old_column_name: String,
497        new_column_name: String,
498    },
499}
500
501///
502/// SqlDdlLoweringError
503///
504/// Typed fail-closed reasons while lowering bound DDL into schema mutation
505/// admission.
506///
507#[derive(Debug, Eq, PartialEq, ThisError)]
508pub(in crate::db) enum SqlDdlLoweringError {
509    #[error("SQL DDL lowering requires a supported DDL statement")]
510    UnsupportedStatement,
511
512    #[error("schema mutation admission rejected DDL candidate: {0:?}")]
513    MutationAdmission(SchemaDdlMutationAdmissionError),
514}
515
516///
517/// SqlDdlPrepareError
518///
519/// Typed fail-closed preparation errors for SQL DDL.
520///
521#[derive(Debug, Eq, PartialEq, ThisError)]
522pub(in crate::db) enum SqlDdlPrepareError {
523    #[error("{0}")]
524    Bind(#[from] SqlDdlBindError),
525
526    #[error("{0}")]
527    Lowering(#[from] SqlDdlLoweringError),
528}
529
530/// Prepare one parsed SQL DDL statement through every pre-execution proof.
531pub(in crate::db) fn prepare_sql_ddl_statement(
532    statement: &SqlStatement,
533    accepted_before: &AcceptedSchemaSnapshot,
534    schema: &SchemaInfo,
535    index_store_path: &'static str,
536) -> Result<PreparedSqlDdlCommand, SqlDdlPrepareError> {
537    let bound = bind_sql_ddl_statement(statement, accepted_before, schema, index_store_path)?;
538    let derivation = if matches!(bound.statement(), BoundSqlDdlStatement::NoOp(_)) {
539        None
540    } else {
541        Some(derive_bound_sql_ddl_accepted_after(
542            accepted_before,
543            &bound,
544        )?)
545    };
546    let report = ddl_preparation_report(&bound);
547
548    Ok(PreparedSqlDdlCommand {
549        bound,
550        derivation,
551        report,
552    })
553}
554
555/// Bind one parsed SQL DDL statement against accepted catalog metadata.
556pub(in crate::db) fn bind_sql_ddl_statement(
557    statement: &SqlStatement,
558    accepted_before: &AcceptedSchemaSnapshot,
559    schema: &SchemaInfo,
560    index_store_path: &'static str,
561) -> Result<BoundSqlDdlRequest, SqlDdlBindError> {
562    let SqlStatement::Ddl(ddl) = statement else {
563        return Err(SqlDdlBindError::NotDdl);
564    };
565
566    match ddl {
567        SqlDdlStatement::CreateIndex(statement) => {
568            bind_create_index_statement(statement, schema, index_store_path)
569        }
570        SqlDdlStatement::DropIndex(statement) => {
571            bind_drop_index_statement(statement, accepted_before, schema)
572        }
573        SqlDdlStatement::AlterTableAddColumn(statement) => {
574            bind_alter_table_add_column_statement(statement, accepted_before, schema)
575        }
576        SqlDdlStatement::AlterTableAlterColumn(statement) => {
577            bind_alter_table_alter_column_statement(statement, accepted_before, schema)
578        }
579        SqlDdlStatement::AlterTableDropColumn(statement) => {
580            bind_alter_table_drop_column_statement(statement, accepted_before, schema)
581        }
582        SqlDdlStatement::AlterTableRenameColumn(statement) => {
583            bind_alter_table_rename_column_statement(statement, accepted_before, schema)
584        }
585    }
586}
587
588/// Lower one bound SQL DDL request through schema mutation admission.
589pub(in crate::db) fn lower_bound_sql_ddl_to_schema_mutation_admission(
590    request: &BoundSqlDdlRequest,
591) -> Result<SchemaDdlMutationAdmission, SqlDdlLoweringError> {
592    match request.statement() {
593        BoundSqlDdlStatement::AddColumn(add) => {
594            Ok(admit_sql_ddl_field_addition_candidate(add.field()))
595        }
596        BoundSqlDdlStatement::AlterColumnDefault(alter) => {
597            Ok(admit_sql_ddl_field_default_candidate(alter.field()))
598        }
599        BoundSqlDdlStatement::AlterColumnNullability(alter) => {
600            Ok(admit_sql_ddl_field_nullability_candidate(alter.field()))
601        }
602        BoundSqlDdlStatement::DropColumn(drop) => {
603            Ok(admit_sql_ddl_field_drop_candidate(drop.field()))
604        }
605        BoundSqlDdlStatement::RenameColumn(rename) => {
606            let after = rename
607                .field()
608                .clone_with_name(rename.new_name().to_string());
609            Ok(admit_sql_ddl_field_rename_candidate(rename.field(), &after))
610        }
611        BoundSqlDdlStatement::CreateIndex(create) => {
612            if create.candidate_index().key().is_field_path_only() {
613                admit_sql_ddl_field_path_index_candidate(create.candidate_index())
614            } else {
615                admit_sql_ddl_expression_index_candidate(create.candidate_index())
616            }
617        }
618        BoundSqlDdlStatement::DropIndex(drop) => {
619            admit_sql_ddl_secondary_index_drop_candidate(drop.dropped_index())
620        }
621        BoundSqlDdlStatement::NoOp(_) => return Err(SqlDdlLoweringError::UnsupportedStatement),
622    }
623    .map_err(SqlDdlLoweringError::MutationAdmission)
624}
625
626/// Derive the accepted-after schema snapshot for one bound SQL DDL request.
627pub(in crate::db) fn derive_bound_sql_ddl_accepted_after(
628    accepted_before: &AcceptedSchemaSnapshot,
629    request: &BoundSqlDdlRequest,
630) -> Result<SchemaDdlAcceptedSnapshotDerivation, SqlDdlLoweringError> {
631    match request.statement() {
632        BoundSqlDdlStatement::AddColumn(add) => {
633            derive_sql_ddl_field_addition_accepted_after(accepted_before, add.field().clone())
634        }
635        BoundSqlDdlStatement::AlterColumnDefault(alter) => {
636            derive_sql_ddl_field_default_accepted_after(
637                accepted_before,
638                alter.field_name(),
639                alter.default().clone(),
640            )
641        }
642        BoundSqlDdlStatement::AlterColumnNullability(alter) => {
643            derive_sql_ddl_field_nullability_accepted_after(
644                accepted_before,
645                alter.field_name(),
646                alter.nullable(),
647            )
648        }
649        BoundSqlDdlStatement::DropColumn(drop) => {
650            derive_sql_ddl_field_drop_accepted_after(accepted_before, drop.field_name())
651        }
652        BoundSqlDdlStatement::RenameColumn(rename) => derive_sql_ddl_field_rename_accepted_after(
653            accepted_before,
654            rename.old_name(),
655            rename.new_name(),
656        ),
657        BoundSqlDdlStatement::CreateIndex(create) => {
658            if create.candidate_index().key().is_field_path_only() {
659                derive_sql_ddl_field_path_index_accepted_after(
660                    accepted_before,
661                    create.candidate_index().clone(),
662                )
663            } else {
664                derive_sql_ddl_expression_index_accepted_after(
665                    accepted_before,
666                    create.candidate_index().clone(),
667                )
668            }
669        }
670        BoundSqlDdlStatement::DropIndex(drop) => {
671            derive_sql_ddl_secondary_index_drop_accepted_after(
672                accepted_before,
673                drop.dropped_index(),
674            )
675        }
676        BoundSqlDdlStatement::NoOp(_) => return Err(SqlDdlLoweringError::UnsupportedStatement),
677    }
678    .map_err(SqlDdlLoweringError::MutationAdmission)
679}
680
681fn ddl_preparation_report(bound: &BoundSqlDdlRequest) -> SqlDdlPreparationReport {
682    match bound.statement() {
683        BoundSqlDdlStatement::AddColumn(add) => SqlDdlPreparationReport {
684            mutation_kind: if add.field().default().is_none() {
685                SqlDdlMutationKind::AddNullableField
686            } else {
687                SqlDdlMutationKind::AddDefaultedField
688            },
689            target_index: add.field().name().to_string(),
690            target_store: add.entity_name().to_string(),
691            field_path: vec![add.field().name().to_string()],
692            execution_status: SqlDdlExecutionStatus::PreparedOnly,
693            rows_scanned: 0,
694            index_keys_written: 0,
695        },
696        BoundSqlDdlStatement::AlterColumnDefault(alter) => SqlDdlPreparationReport {
697            mutation_kind: alter.mutation_kind(),
698            target_index: alter.field_name().to_string(),
699            target_store: alter.entity_name().to_string(),
700            field_path: vec![alter.field_name().to_string()],
701            execution_status: SqlDdlExecutionStatus::PreparedOnly,
702            rows_scanned: 0,
703            index_keys_written: 0,
704        },
705        BoundSqlDdlStatement::AlterColumnNullability(alter) => SqlDdlPreparationReport {
706            mutation_kind: alter.mutation_kind(),
707            target_index: alter.field_name().to_string(),
708            target_store: alter.entity_name().to_string(),
709            field_path: vec![alter.field_name().to_string()],
710            execution_status: SqlDdlExecutionStatus::PreparedOnly,
711            rows_scanned: 0,
712            index_keys_written: 0,
713        },
714        BoundSqlDdlStatement::DropColumn(drop) => SqlDdlPreparationReport {
715            mutation_kind: SqlDdlMutationKind::DropField,
716            target_index: drop.field_name().to_string(),
717            target_store: drop.entity_name().to_string(),
718            field_path: vec![drop.field_name().to_string()],
719            execution_status: SqlDdlExecutionStatus::PreparedOnly,
720            rows_scanned: 0,
721            index_keys_written: 0,
722        },
723        BoundSqlDdlStatement::RenameColumn(rename) => SqlDdlPreparationReport {
724            mutation_kind: SqlDdlMutationKind::RenameField,
725            target_index: rename.new_name().to_string(),
726            target_store: rename.entity_name().to_string(),
727            field_path: vec![rename.old_name().to_string(), rename.new_name().to_string()],
728            execution_status: SqlDdlExecutionStatus::PreparedOnly,
729            rows_scanned: 0,
730            index_keys_written: 0,
731        },
732        BoundSqlDdlStatement::CreateIndex(create) => {
733            let target = create.candidate_index();
734
735            SqlDdlPreparationReport {
736                mutation_kind: if target.key().is_field_path_only() {
737                    SqlDdlMutationKind::AddFieldPathIndex
738                } else {
739                    SqlDdlMutationKind::AddExpressionIndex
740                },
741                target_index: target.name().to_string(),
742                target_store: target.store().to_string(),
743                field_path: ddl_key_item_report(create.key_items()),
744                execution_status: SqlDdlExecutionStatus::PreparedOnly,
745                rows_scanned: 0,
746                index_keys_written: 0,
747            }
748        }
749        BoundSqlDdlStatement::DropIndex(drop) => SqlDdlPreparationReport {
750            mutation_kind: SqlDdlMutationKind::DropSecondaryIndex,
751            target_index: drop.index_name().to_string(),
752            target_store: drop.dropped_index().store().to_string(),
753            field_path: drop.field_path().to_vec(),
754            execution_status: SqlDdlExecutionStatus::PreparedOnly,
755            rows_scanned: 0,
756            index_keys_written: 0,
757        },
758        BoundSqlDdlStatement::NoOp(no_op) => SqlDdlPreparationReport {
759            mutation_kind: no_op.mutation_kind(),
760            target_index: no_op.index_name().to_string(),
761            target_store: no_op.target_store().to_string(),
762            field_path: no_op.field_path().to_vec(),
763            execution_status: SqlDdlExecutionStatus::PreparedOnly,
764            rows_scanned: 0,
765            index_keys_written: 0,
766        },
767    }
768}