1#![allow(
7 dead_code,
8 reason = "DDL binding exposes prepare-only diagnostics and test-only inspection accessors"
9)]
10
11use crate::db::{
12 data::encode_runtime_value_for_accepted_field_contract,
13 predicate::parse_sql_predicate,
14 query::predicate::validate_predicate,
15 schema::{
16 AcceptedFieldDecodeContract, AcceptedSchemaSnapshot, FieldId, PersistedFieldKind,
17 PersistedFieldOrigin, PersistedFieldSnapshot, PersistedIndexExpressionOp,
18 PersistedIndexExpressionSnapshot, PersistedIndexKeyItemSnapshot, PersistedIndexKeySnapshot,
19 PersistedIndexSnapshot, SchemaDdlAcceptedSnapshotDerivation,
20 SchemaDdlIndexDropCandidateError, SchemaDdlMutationAdmission,
21 SchemaDdlMutationAdmissionError, SchemaExpressionIndexInfo,
22 SchemaExpressionIndexKeyItemInfo, SchemaFieldDefault, SchemaFieldSlot,
23 SchemaFieldWritePolicy, SchemaInfo, admit_sql_ddl_expression_index_candidate,
24 admit_sql_ddl_field_addition_candidate, admit_sql_ddl_field_default_candidate,
25 admit_sql_ddl_field_drop_candidate, admit_sql_ddl_field_nullability_candidate,
26 admit_sql_ddl_field_path_index_candidate, admit_sql_ddl_field_rename_candidate,
27 admit_sql_ddl_secondary_index_drop_candidate,
28 canonicalize_strict_sql_literal_for_persisted_kind,
29 derive_sql_ddl_expression_index_accepted_after,
30 derive_sql_ddl_field_addition_accepted_after, derive_sql_ddl_field_default_accepted_after,
31 derive_sql_ddl_field_drop_accepted_after, derive_sql_ddl_field_nullability_accepted_after,
32 derive_sql_ddl_field_path_index_accepted_after, derive_sql_ddl_field_rename_accepted_after,
33 derive_sql_ddl_secondary_index_drop_accepted_after,
34 resolve_sql_ddl_field_drop_dependent_index, resolve_sql_ddl_secondary_index_drop_candidate,
35 },
36 sql::{
37 identifier::identifiers_tail_match,
38 parser::{
39 SqlAlterColumnAction, SqlAlterTableAddColumnStatement,
40 SqlAlterTableAlterColumnStatement, SqlAlterTableDropColumnStatement,
41 SqlAlterTableRenameColumnStatement, SqlCreateIndexExpressionKey, SqlCreateIndexKeyItem,
42 SqlCreateIndexStatement, SqlCreateIndexUniqueness, SqlDdlStatement,
43 SqlDropIndexStatement, SqlStatement,
44 },
45 },
46};
47use crate::model::field::{DEFAULT_BIG_INT_MAX_BYTES, FieldStorageDecode, LeafCodec, ScalarCodec};
48use thiserror::Error as ThisError;
49
50#[derive(Clone, Debug, Eq, PartialEq)]
58pub(in crate::db) struct PreparedSqlDdlCommand {
59 bound: BoundSqlDdlRequest,
60 derivation: Option<SchemaDdlAcceptedSnapshotDerivation>,
61 report: SqlDdlPreparationReport,
62}
63
64impl PreparedSqlDdlCommand {
65 #[must_use]
67 pub(in crate::db) const fn bound(&self) -> &BoundSqlDdlRequest {
68 &self.bound
69 }
70
71 #[must_use]
73 pub(in crate::db) const fn derivation(&self) -> Option<&SchemaDdlAcceptedSnapshotDerivation> {
74 self.derivation.as_ref()
75 }
76
77 #[must_use]
79 pub(in crate::db) const fn report(&self) -> &SqlDdlPreparationReport {
80 &self.report
81 }
82
83 #[must_use]
85 pub(in crate::db) const fn mutates_schema(&self) -> bool {
86 self.derivation.is_some()
87 }
88}
89
90#[derive(Clone, Debug, Eq, PartialEq)]
97pub struct SqlDdlPreparationReport {
98 mutation_kind: SqlDdlMutationKind,
99 target_index: String,
100 target_store: String,
101 field_path: Vec<String>,
102 execution_status: SqlDdlExecutionStatus,
103 rows_scanned: usize,
104 index_keys_written: usize,
105}
106
107impl SqlDdlPreparationReport {
108 #[must_use]
110 pub const fn mutation_kind(&self) -> SqlDdlMutationKind {
111 self.mutation_kind
112 }
113
114 #[must_use]
116 pub const fn target_index(&self) -> &str {
117 self.target_index.as_str()
118 }
119
120 #[must_use]
122 pub const fn target_store(&self) -> &str {
123 self.target_store.as_str()
124 }
125
126 #[must_use]
128 pub const fn field_path(&self) -> &[String] {
129 self.field_path.as_slice()
130 }
131
132 #[must_use]
134 pub const fn execution_status(&self) -> SqlDdlExecutionStatus {
135 self.execution_status
136 }
137
138 #[must_use]
140 pub const fn rows_scanned(&self) -> usize {
141 self.rows_scanned
142 }
143
144 #[must_use]
146 pub const fn index_keys_written(&self) -> usize {
147 self.index_keys_written
148 }
149
150 pub(in crate::db) const fn with_execution_status(
151 mut self,
152 execution_status: SqlDdlExecutionStatus,
153 ) -> Self {
154 self.execution_status = execution_status;
155 self
156 }
157
158 pub(in crate::db) const fn with_execution_metrics(
159 mut self,
160 rows_scanned: usize,
161 index_keys_written: usize,
162 ) -> Self {
163 self.rows_scanned = rows_scanned;
164 self.index_keys_written = index_keys_written;
165 self
166 }
167}
168
169#[derive(Clone, Copy, Debug, Eq, PartialEq)]
175pub enum SqlDdlMutationKind {
176 AddDefaultedField,
177 AddNullableField,
178 SetFieldDefault,
179 DropFieldDefault,
180 SetFieldNotNull,
181 DropFieldNotNull,
182 DropField,
183 RenameField,
184 AddFieldPathIndex,
185 AddExpressionIndex,
186 DropSecondaryIndex,
187}
188
189impl SqlDdlMutationKind {
190 #[must_use]
192 pub const fn as_str(self) -> &'static str {
193 match self {
194 Self::AddDefaultedField => "add_defaulted_field",
195 Self::AddNullableField => "add_nullable_field",
196 Self::SetFieldDefault => "set_field_default",
197 Self::DropFieldDefault => "drop_field_default",
198 Self::SetFieldNotNull => "set_field_not_null",
199 Self::DropFieldNotNull => "drop_field_not_null",
200 Self::DropField => "drop_field",
201 Self::RenameField => "rename_field",
202 Self::AddFieldPathIndex => "add_field_path_index",
203 Self::AddExpressionIndex => "add_expression_index",
204 Self::DropSecondaryIndex => "drop_secondary_index",
205 }
206 }
207}
208
209#[derive(Clone, Copy, Debug, Eq, PartialEq)]
215pub enum SqlDdlExecutionStatus {
216 PreparedOnly,
217 Published,
218 NoOp,
219}
220
221impl SqlDdlExecutionStatus {
222 #[must_use]
224 pub const fn as_str(self) -> &'static str {
225 match self {
226 Self::PreparedOnly => "prepared_only",
227 Self::Published => "published",
228 Self::NoOp => "no_op",
229 }
230 }
231}
232
233#[derive(Clone, Debug, Eq, PartialEq)]
240pub(in crate::db) struct BoundSqlDdlRequest {
241 statement: BoundSqlDdlStatement,
242}
243
244impl BoundSqlDdlRequest {
245 #[must_use]
247 pub(in crate::db) const fn statement(&self) -> &BoundSqlDdlStatement {
248 &self.statement
249 }
250}
251
252#[derive(Clone, Debug, Eq, PartialEq)]
258pub(in crate::db) enum BoundSqlDdlStatement {
259 AddColumn(BoundSqlAddColumnRequest),
260 AlterColumnDefault(BoundSqlAlterColumnDefaultRequest),
261 AlterColumnNullability(BoundSqlAlterColumnNullabilityRequest),
262 DropColumn(BoundSqlDropColumnRequest),
263 RenameColumn(BoundSqlRenameColumnRequest),
264 CreateIndex(BoundSqlCreateIndexRequest),
265 DropIndex(BoundSqlDropIndexRequest),
266 NoOp(BoundSqlDdlNoOpRequest),
267}
268
269#[derive(Clone, Debug, Eq, PartialEq)]
275pub(in crate::db) struct BoundSqlAddColumnRequest {
276 entity_name: String,
277 field: PersistedFieldSnapshot,
278}
279
280impl BoundSqlAddColumnRequest {
281 #[must_use]
283 pub(in crate::db) const fn entity_name(&self) -> &str {
284 self.entity_name.as_str()
285 }
286
287 #[must_use]
289 pub(in crate::db) const fn field(&self) -> &PersistedFieldSnapshot {
290 &self.field
291 }
292}
293
294#[derive(Clone, Debug, Eq, PartialEq)]
300pub(in crate::db) struct BoundSqlAlterColumnDefaultRequest {
301 entity_name: String,
302 field: PersistedFieldSnapshot,
303 default: SchemaFieldDefault,
304 mutation_kind: SqlDdlMutationKind,
305}
306
307impl BoundSqlAlterColumnDefaultRequest {
308 #[must_use]
310 pub(in crate::db) const fn entity_name(&self) -> &str {
311 self.entity_name.as_str()
312 }
313
314 #[must_use]
316 pub(in crate::db) const fn field_name(&self) -> &str {
317 self.field.name()
318 }
319
320 #[must_use]
322 pub(in crate::db) const fn field(&self) -> &PersistedFieldSnapshot {
323 &self.field
324 }
325
326 #[must_use]
328 pub(in crate::db) const fn default(&self) -> &SchemaFieldDefault {
329 &self.default
330 }
331
332 #[must_use]
334 pub(in crate::db) const fn mutation_kind(&self) -> SqlDdlMutationKind {
335 self.mutation_kind
336 }
337}
338
339#[derive(Clone, Debug, Eq, PartialEq)]
345pub(in crate::db) struct BoundSqlAlterColumnNullabilityRequest {
346 entity_name: String,
347 field: PersistedFieldSnapshot,
348 nullable: bool,
349 mutation_kind: SqlDdlMutationKind,
350}
351
352impl BoundSqlAlterColumnNullabilityRequest {
353 #[must_use]
355 pub(in crate::db) const fn entity_name(&self) -> &str {
356 self.entity_name.as_str()
357 }
358
359 #[must_use]
361 pub(in crate::db) const fn field_name(&self) -> &str {
362 self.field.name()
363 }
364
365 #[must_use]
367 pub(in crate::db) const fn field(&self) -> &PersistedFieldSnapshot {
368 &self.field
369 }
370
371 #[must_use]
373 pub(in crate::db) const fn nullable(&self) -> bool {
374 self.nullable
375 }
376
377 #[must_use]
379 pub(in crate::db) const fn mutation_kind(&self) -> SqlDdlMutationKind {
380 self.mutation_kind
381 }
382}
383
384#[derive(Clone, Debug, Eq, PartialEq)]
390pub(in crate::db) struct BoundSqlDropColumnRequest {
391 entity_name: String,
392 field: PersistedFieldSnapshot,
393}
394
395impl BoundSqlDropColumnRequest {
396 #[must_use]
398 pub(in crate::db) const fn entity_name(&self) -> &str {
399 self.entity_name.as_str()
400 }
401
402 #[must_use]
404 pub(in crate::db) const fn field_name(&self) -> &str {
405 self.field.name()
406 }
407
408 #[must_use]
410 pub(in crate::db) const fn field(&self) -> &PersistedFieldSnapshot {
411 &self.field
412 }
413}
414
415#[derive(Clone, Debug, Eq, PartialEq)]
421pub(in crate::db) struct BoundSqlRenameColumnRequest {
422 entity_name: String,
423 field: PersistedFieldSnapshot,
424 new_name: String,
425}
426
427impl BoundSqlRenameColumnRequest {
428 #[must_use]
430 pub(in crate::db) const fn entity_name(&self) -> &str {
431 self.entity_name.as_str()
432 }
433
434 #[must_use]
436 pub(in crate::db) const fn old_name(&self) -> &str {
437 self.field.name()
438 }
439
440 #[must_use]
442 pub(in crate::db) const fn new_name(&self) -> &str {
443 self.new_name.as_str()
444 }
445
446 #[must_use]
448 pub(in crate::db) const fn field(&self) -> &PersistedFieldSnapshot {
449 &self.field
450 }
451}
452
453#[derive(Clone, Debug, Eq, PartialEq)]
459pub(in crate::db) struct BoundSqlDdlNoOpRequest {
460 mutation_kind: SqlDdlMutationKind,
461 index_name: String,
462 entity_name: String,
463 target_store: String,
464 field_path: Vec<String>,
465}
466
467impl BoundSqlDdlNoOpRequest {
468 #[must_use]
470 pub(in crate::db) const fn mutation_kind(&self) -> SqlDdlMutationKind {
471 self.mutation_kind
472 }
473
474 #[must_use]
476 pub(in crate::db) const fn index_name(&self) -> &str {
477 self.index_name.as_str()
478 }
479
480 #[must_use]
482 pub(in crate::db) const fn entity_name(&self) -> &str {
483 self.entity_name.as_str()
484 }
485
486 #[must_use]
488 pub(in crate::db) const fn target_store(&self) -> &str {
489 self.target_store.as_str()
490 }
491
492 #[must_use]
494 pub(in crate::db) const fn field_path(&self) -> &[String] {
495 self.field_path.as_slice()
496 }
497}
498
499#[derive(Clone, Debug, Eq, PartialEq)]
505pub(in crate::db) struct BoundSqlCreateIndexRequest {
506 index_name: String,
507 entity_name: String,
508 key_items: Vec<BoundSqlDdlCreateIndexKey>,
509 field_paths: Vec<BoundSqlDdlFieldPath>,
510 candidate_index: PersistedIndexSnapshot,
511}
512
513impl BoundSqlCreateIndexRequest {
514 #[must_use]
516 pub(in crate::db) const fn index_name(&self) -> &str {
517 self.index_name.as_str()
518 }
519
520 #[must_use]
522 pub(in crate::db) const fn entity_name(&self) -> &str {
523 self.entity_name.as_str()
524 }
525
526 #[must_use]
528 pub(in crate::db) const fn field_paths(&self) -> &[BoundSqlDdlFieldPath] {
529 self.field_paths.as_slice()
530 }
531
532 #[must_use]
534 pub(in crate::db) const fn key_items(&self) -> &[BoundSqlDdlCreateIndexKey] {
535 self.key_items.as_slice()
536 }
537
538 #[must_use]
540 pub(in crate::db) const fn candidate_index(&self) -> &PersistedIndexSnapshot {
541 &self.candidate_index
542 }
543}
544
545#[derive(Clone, Debug, Eq, PartialEq)]
551pub(in crate::db) struct BoundSqlDropIndexRequest {
552 index_name: String,
553 entity_name: String,
554 dropped_index: PersistedIndexSnapshot,
555 field_path: Vec<String>,
556}
557
558impl BoundSqlDropIndexRequest {
559 #[must_use]
561 pub(in crate::db) const fn index_name(&self) -> &str {
562 self.index_name.as_str()
563 }
564
565 #[must_use]
567 pub(in crate::db) const fn entity_name(&self) -> &str {
568 self.entity_name.as_str()
569 }
570
571 #[must_use]
573 pub(in crate::db) const fn dropped_index(&self) -> &PersistedIndexSnapshot {
574 &self.dropped_index
575 }
576
577 #[must_use]
579 pub(in crate::db) const fn field_path(&self) -> &[String] {
580 self.field_path.as_slice()
581 }
582}
583
584#[derive(Clone, Debug, Eq, PartialEq)]
590pub(in crate::db) struct BoundSqlDdlFieldPath {
591 root: String,
592 segments: Vec<String>,
593 accepted_path: Vec<String>,
594}
595
596impl BoundSqlDdlFieldPath {
597 #[must_use]
599 pub(in crate::db) const fn root(&self) -> &str {
600 self.root.as_str()
601 }
602
603 #[must_use]
605 pub(in crate::db) const fn segments(&self) -> &[String] {
606 self.segments.as_slice()
607 }
608
609 #[must_use]
611 pub(in crate::db) const fn accepted_path(&self) -> &[String] {
612 self.accepted_path.as_slice()
613 }
614}
615
616#[derive(Debug, Eq, PartialEq, ThisError)]
622pub(in crate::db) enum SqlDdlBindError {
623 #[error("SQL DDL binder requires a DDL statement")]
624 NotDdl,
625
626 #[error("accepted schema does not expose an entity name")]
627 MissingEntityName,
628
629 #[error("accepted schema does not expose an entity path")]
630 MissingEntityPath,
631
632 #[error("SQL entity '{sql_entity}' does not match accepted entity '{expected_entity}'")]
633 EntityMismatch {
634 sql_entity: String,
635 expected_entity: String,
636 },
637
638 #[error("unknown field path '{field_path}' for accepted entity '{entity_name}'")]
639 UnknownFieldPath {
640 entity_name: String,
641 field_path: String,
642 },
643
644 #[error("field path '{field_path}' is not indexable")]
645 FieldPathNotIndexable { field_path: String },
646
647 #[error("field path '{field_path}' depends on generated-only metadata")]
648 FieldPathNotAcceptedCatalogBacked { field_path: String },
649
650 #[error("invalid filtered index predicate: {detail}")]
651 InvalidFilteredIndexPredicate { detail: String },
652
653 #[error("index name '{index_name}' already exists in the accepted schema")]
654 DuplicateIndexName { index_name: String },
655
656 #[error("accepted schema already has index '{existing_index}' for field path '{field_path}'")]
657 DuplicateFieldPathIndex {
658 field_path: String,
659 existing_index: String,
660 },
661
662 #[error("unknown index '{index_name}' for accepted entity '{entity_name}'")]
663 UnknownIndex {
664 entity_name: String,
665 index_name: String,
666 },
667
668 #[error(
669 "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"
670 )]
671 GeneratedIndexDropRejected { index_name: String },
672
673 #[error(
674 "index '{index_name}' is not a supported DDL-droppable secondary index; SQL DDL can currently drop only indexes created through SQL DDL"
675 )]
676 UnsupportedDropIndex { index_name: String },
677
678 #[error(
679 "SQL DDL ALTER TABLE ADD COLUMN is not executable yet for accepted entity '{entity_name}' column '{column_name}'"
680 )]
681 UnsupportedAlterTableAddColumn {
682 entity_name: String,
683 column_name: String,
684 },
685
686 #[error(
687 "SQL DDL ALTER TABLE ADD COLUMN DEFAULT value is not encodable for accepted entity '{entity_name}' column '{column_name}': {detail}"
688 )]
689 InvalidAlterTableAddColumnDefault {
690 entity_name: String,
691 column_name: String,
692 detail: String,
693 },
694
695 #[error(
696 "SQL DDL ALTER TABLE ADD COLUMN NOT NULL is not executable yet for accepted entity '{entity_name}' column '{column_name}'"
697 )]
698 UnsupportedAlterTableAddColumnNotNull {
699 entity_name: String,
700 column_name: String,
701 },
702
703 #[error("field '{column_name}' already exists in accepted entity '{entity_name}'")]
704 DuplicateColumn {
705 entity_name: String,
706 column_name: String,
707 },
708
709 #[error(
710 "SQL DDL ALTER TABLE ADD COLUMN type '{column_type}' is not supported yet for accepted entity '{entity_name}' column '{column_name}'"
711 )]
712 UnsupportedAlterTableAddColumnType {
713 entity_name: String,
714 column_name: String,
715 column_type: String,
716 },
717
718 #[error("unknown column '{column_name}' for accepted entity '{entity_name}'")]
719 UnknownColumn {
720 entity_name: String,
721 column_name: String,
722 },
723
724 #[error(
725 "SQL DDL ALTER TABLE ALTER COLUMN {action} is not executable yet for accepted entity '{entity_name}' column '{column_name}'"
726 )]
727 UnsupportedAlterTableAlterColumn {
728 entity_name: String,
729 column_name: String,
730 action: String,
731 },
732
733 #[error(
734 "SQL DDL ALTER TABLE ALTER COLUMN SET DEFAULT value is not encodable for accepted entity '{entity_name}' column '{column_name}': {detail}"
735 )]
736 InvalidAlterTableAlterColumnDefault {
737 entity_name: String,
738 column_name: String,
739 detail: String,
740 },
741
742 #[error(
743 "SQL DDL ALTER TABLE ALTER COLUMN DROP DEFAULT is not executable yet for required accepted entity '{entity_name}' column '{column_name}'"
744 )]
745 UnsupportedAlterTableDropDefaultRequired {
746 entity_name: String,
747 column_name: String,
748 },
749
750 #[error(
751 "SQL DDL ALTER TABLE ALTER COLUMN DEFAULT cannot change generated accepted field '{column_name}' on entity '{entity_name}'; change the Rust schema default instead"
752 )]
753 GeneratedFieldDefaultChangeRejected {
754 entity_name: String,
755 column_name: String,
756 },
757
758 #[error(
759 "SQL DDL ALTER TABLE ALTER COLUMN NULLABILITY cannot change generated accepted field '{column_name}' on entity '{entity_name}'; change the Rust schema nullability instead"
760 )]
761 GeneratedFieldNullabilityChangeRejected {
762 entity_name: String,
763 column_name: String,
764 },
765
766 #[error(
767 "SQL DDL ALTER TABLE DROP COLUMN cannot drop primary-key field '{column_name}' on entity '{entity_name}'"
768 )]
769 PrimaryKeyFieldDropRejected {
770 entity_name: String,
771 column_name: String,
772 },
773
774 #[error(
775 "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"
776 )]
777 GeneratedFieldDropRejected {
778 entity_name: String,
779 column_name: String,
780 },
781
782 #[error(
783 "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"
784 )]
785 IndexedFieldDropRejected {
786 entity_name: String,
787 column_name: String,
788 index_name: String,
789 },
790
791 #[error(
792 "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"
793 )]
794 GeneratedFieldRenameRejected {
795 entity_name: String,
796 column_name: String,
797 },
798
799 #[error(
800 "SQL DDL ALTER TABLE RENAME COLUMN is not executable for accepted entity '{entity_name}' column '{old_column_name}' to '{new_column_name}'"
801 )]
802 UnsupportedAlterTableRenameColumn {
803 entity_name: String,
804 old_column_name: String,
805 new_column_name: String,
806 },
807}
808
809#[derive(Debug, Eq, PartialEq, ThisError)]
816pub(in crate::db) enum SqlDdlLoweringError {
817 #[error("SQL DDL lowering requires a supported DDL statement")]
818 UnsupportedStatement,
819
820 #[error("schema mutation admission rejected DDL candidate: {0:?}")]
821 MutationAdmission(SchemaDdlMutationAdmissionError),
822}
823
824#[derive(Debug, Eq, PartialEq, ThisError)]
830pub(in crate::db) enum SqlDdlPrepareError {
831 #[error("{0}")]
832 Bind(#[from] SqlDdlBindError),
833
834 #[error("{0}")]
835 Lowering(#[from] SqlDdlLoweringError),
836}
837
838pub(in crate::db) fn prepare_sql_ddl_statement(
840 statement: &SqlStatement,
841 accepted_before: &AcceptedSchemaSnapshot,
842 schema: &SchemaInfo,
843 index_store_path: &'static str,
844) -> Result<PreparedSqlDdlCommand, SqlDdlPrepareError> {
845 let bound = bind_sql_ddl_statement(statement, accepted_before, schema, index_store_path)?;
846 let derivation = if matches!(bound.statement(), BoundSqlDdlStatement::NoOp(_)) {
847 None
848 } else {
849 Some(derive_bound_sql_ddl_accepted_after(
850 accepted_before,
851 &bound,
852 )?)
853 };
854 let report = ddl_preparation_report(&bound);
855
856 Ok(PreparedSqlDdlCommand {
857 bound,
858 derivation,
859 report,
860 })
861}
862
863pub(in crate::db) fn bind_sql_ddl_statement(
865 statement: &SqlStatement,
866 accepted_before: &AcceptedSchemaSnapshot,
867 schema: &SchemaInfo,
868 index_store_path: &'static str,
869) -> Result<BoundSqlDdlRequest, SqlDdlBindError> {
870 let SqlStatement::Ddl(ddl) = statement else {
871 return Err(SqlDdlBindError::NotDdl);
872 };
873
874 match ddl {
875 SqlDdlStatement::CreateIndex(statement) => {
876 bind_create_index_statement(statement, schema, index_store_path)
877 }
878 SqlDdlStatement::DropIndex(statement) => {
879 bind_drop_index_statement(statement, accepted_before, schema)
880 }
881 SqlDdlStatement::AlterTableAddColumn(statement) => {
882 bind_alter_table_add_column_statement(statement, accepted_before, schema)
883 }
884 SqlDdlStatement::AlterTableAlterColumn(statement) => {
885 bind_alter_table_alter_column_statement(statement, accepted_before, schema)
886 }
887 SqlDdlStatement::AlterTableDropColumn(statement) => {
888 bind_alter_table_drop_column_statement(statement, accepted_before, schema)
889 }
890 SqlDdlStatement::AlterTableRenameColumn(statement) => {
891 bind_alter_table_rename_column_statement(statement, accepted_before, schema)
892 }
893 }
894}
895
896fn bind_create_index_statement(
897 statement: &SqlCreateIndexStatement,
898 schema: &SchemaInfo,
899 index_store_path: &'static str,
900) -> Result<BoundSqlDdlRequest, SqlDdlBindError> {
901 let entity_name = schema
902 .entity_name()
903 .ok_or(SqlDdlBindError::MissingEntityName)?;
904
905 if !identifiers_tail_match(statement.entity.as_str(), entity_name) {
906 return Err(SqlDdlBindError::EntityMismatch {
907 sql_entity: statement.entity.clone(),
908 expected_entity: entity_name.to_string(),
909 });
910 }
911
912 let key_items = statement
913 .key_items
914 .iter()
915 .map(|key_item| bind_create_index_key_item(key_item, entity_name, schema))
916 .collect::<Result<Vec<_>, _>>()?;
917 let field_paths = create_index_field_path_report_items(key_items.as_slice());
918 if let Some(existing_index) = find_field_path_index_by_name(schema, statement.name.as_str()) {
919 if key_items_are_field_path_only(key_items.as_slice())
920 && statement.if_not_exists
921 && existing_field_path_index_matches_request(
922 existing_index,
923 field_paths.as_slice(),
924 statement.predicate_sql.as_deref(),
925 statement.uniqueness,
926 )
927 {
928 return Ok(BoundSqlDdlRequest {
929 statement: BoundSqlDdlStatement::NoOp(BoundSqlDdlNoOpRequest {
930 mutation_kind: SqlDdlMutationKind::AddFieldPathIndex,
931 index_name: statement.name.clone(),
932 entity_name: entity_name.to_string(),
933 target_store: existing_index.store().to_string(),
934 field_path: ddl_field_path_report(field_paths.as_slice()),
935 }),
936 });
937 }
938
939 return Err(SqlDdlBindError::DuplicateIndexName {
940 index_name: statement.name.clone(),
941 });
942 }
943 let predicate_sql =
944 validated_create_index_predicate_sql(statement.predicate_sql.as_deref(), schema)?;
945 if let Some(existing_index) = find_expression_index_by_name(schema, statement.name.as_str()) {
946 if statement.if_not_exists
947 && existing_expression_index_matches_request(
948 existing_index,
949 key_items.as_slice(),
950 predicate_sql.as_deref(),
951 statement.uniqueness,
952 )
953 {
954 return Ok(BoundSqlDdlRequest {
955 statement: BoundSqlDdlStatement::NoOp(BoundSqlDdlNoOpRequest {
956 mutation_kind: SqlDdlMutationKind::AddExpressionIndex,
957 index_name: statement.name.clone(),
958 entity_name: entity_name.to_string(),
959 target_store: existing_index.store().to_string(),
960 field_path: ddl_key_item_report(key_items.as_slice()),
961 }),
962 });
963 }
964
965 return Err(SqlDdlBindError::DuplicateIndexName {
966 index_name: statement.name.clone(),
967 });
968 }
969 if key_items_are_field_path_only(key_items.as_slice()) {
970 reject_duplicate_field_path_index(
971 field_paths.as_slice(),
972 predicate_sql.as_deref(),
973 schema,
974 )?;
975 } else {
976 reject_duplicate_expression_index(key_items.as_slice(), predicate_sql.as_deref(), schema)?;
977 }
978 let candidate_index = candidate_index_snapshot(
979 statement.name.as_str(),
980 key_items.as_slice(),
981 predicate_sql.as_deref(),
982 statement.uniqueness,
983 schema,
984 index_store_path,
985 )?;
986
987 Ok(BoundSqlDdlRequest {
988 statement: BoundSqlDdlStatement::CreateIndex(BoundSqlCreateIndexRequest {
989 index_name: statement.name.clone(),
990 entity_name: entity_name.to_string(),
991 key_items,
992 field_paths,
993 candidate_index,
994 }),
995 })
996}
997
998fn bind_drop_index_statement(
999 statement: &SqlDropIndexStatement,
1000 accepted_before: &AcceptedSchemaSnapshot,
1001 schema: &SchemaInfo,
1002) -> Result<BoundSqlDdlRequest, SqlDdlBindError> {
1003 let entity_name = schema
1004 .entity_name()
1005 .ok_or(SqlDdlBindError::MissingEntityName)?;
1006
1007 if let Some(sql_entity) = statement.entity.as_deref()
1008 && !identifiers_tail_match(sql_entity, entity_name)
1009 {
1010 return Err(SqlDdlBindError::EntityMismatch {
1011 sql_entity: sql_entity.to_string(),
1012 expected_entity: entity_name.to_string(),
1013 });
1014 }
1015 let drop_candidate = resolve_sql_ddl_secondary_index_drop_candidate(
1016 accepted_before,
1017 &statement.name,
1018 )
1019 .map_err(|error| match error {
1020 SchemaDdlIndexDropCandidateError::Generated => {
1021 SqlDdlBindError::GeneratedIndexDropRejected {
1022 index_name: statement.name.clone(),
1023 }
1024 }
1025 SchemaDdlIndexDropCandidateError::Unknown => SqlDdlBindError::UnknownIndex {
1026 entity_name: entity_name.to_string(),
1027 index_name: statement.name.clone(),
1028 },
1029 SchemaDdlIndexDropCandidateError::Unsupported => SqlDdlBindError::UnsupportedDropIndex {
1030 index_name: statement.name.clone(),
1031 },
1032 });
1033 let (dropped_index, field_path) = match drop_candidate {
1034 Ok((dropped_index, field_path)) => (dropped_index, field_path),
1035 Err(SqlDdlBindError::UnknownIndex { .. }) if statement.if_exists => {
1036 return Ok(BoundSqlDdlRequest {
1037 statement: BoundSqlDdlStatement::NoOp(BoundSqlDdlNoOpRequest {
1038 mutation_kind: SqlDdlMutationKind::DropSecondaryIndex,
1039 index_name: statement.name.clone(),
1040 entity_name: entity_name.to_string(),
1041 target_store: "-".to_string(),
1042 field_path: Vec::new(),
1043 }),
1044 });
1045 }
1046 Err(error) => return Err(error),
1047 };
1048 Ok(BoundSqlDdlRequest {
1049 statement: BoundSqlDdlStatement::DropIndex(BoundSqlDropIndexRequest {
1050 index_name: statement.name.clone(),
1051 entity_name: entity_name.to_string(),
1052 dropped_index,
1053 field_path,
1054 }),
1055 })
1056}
1057
1058fn bind_alter_table_add_column_statement(
1059 statement: &SqlAlterTableAddColumnStatement,
1060 accepted_before: &AcceptedSchemaSnapshot,
1061 schema: &SchemaInfo,
1062) -> Result<BoundSqlDdlRequest, SqlDdlBindError> {
1063 let entity_name = schema
1064 .entity_name()
1065 .ok_or(SqlDdlBindError::MissingEntityName)?;
1066
1067 if !identifiers_tail_match(statement.entity.as_str(), entity_name) {
1068 return Err(SqlDdlBindError::EntityMismatch {
1069 sql_entity: statement.entity.clone(),
1070 expected_entity: entity_name.to_string(),
1071 });
1072 }
1073
1074 if schema
1075 .field_nullable(statement.column_name.as_str())
1076 .is_some()
1077 {
1078 return Err(SqlDdlBindError::DuplicateColumn {
1079 entity_name: entity_name.to_string(),
1080 column_name: statement.column_name.clone(),
1081 });
1082 }
1083
1084 let (kind, storage_decode, leaf_codec) = persisted_field_contract_for_sql_column_type(
1085 statement.column_type.as_str(),
1086 )
1087 .ok_or_else(|| SqlDdlBindError::UnsupportedAlterTableAddColumnType {
1088 entity_name: entity_name.to_string(),
1089 column_name: statement.column_name.clone(),
1090 column_type: statement.column_type.clone(),
1091 })?;
1092 let default = schema_field_default_for_sql_default(
1093 entity_name,
1094 statement.column_name.as_str(),
1095 statement.default.as_ref(),
1096 &kind,
1097 statement.nullable,
1098 storage_decode,
1099 leaf_codec,
1100 )?;
1101 if !statement.nullable && default.is_none() {
1102 return Err(SqlDdlBindError::UnsupportedAlterTableAddColumnNotNull {
1103 entity_name: entity_name.to_string(),
1104 column_name: statement.column_name.clone(),
1105 });
1106 }
1107 let field = PersistedFieldSnapshot::new_with_write_policy_and_origin(
1108 next_sql_ddl_field_id(accepted_before),
1109 statement.column_name.clone(),
1110 next_sql_ddl_field_slot(accepted_before),
1111 kind,
1112 Vec::new(),
1113 statement.nullable,
1114 default,
1115 SchemaFieldWritePolicy::from_model_policies(None, None),
1116 PersistedFieldOrigin::SqlDdl,
1117 storage_decode,
1118 leaf_codec,
1119 );
1120
1121 Ok(BoundSqlDdlRequest {
1122 statement: BoundSqlDdlStatement::AddColumn(BoundSqlAddColumnRequest {
1123 entity_name: entity_name.to_string(),
1124 field,
1125 }),
1126 })
1127}
1128
1129fn alter_table_alter_column_bind_error(
1130 statement: &SqlAlterTableAlterColumnStatement,
1131 schema: &SchemaInfo,
1132) -> SqlDdlBindError {
1133 let Some(entity_name) = schema.entity_name() else {
1134 return SqlDdlBindError::MissingEntityName;
1135 };
1136
1137 if !identifiers_tail_match(statement.entity.as_str(), entity_name) {
1138 return SqlDdlBindError::EntityMismatch {
1139 sql_entity: statement.entity.clone(),
1140 expected_entity: entity_name.to_string(),
1141 };
1142 }
1143
1144 if schema
1145 .field_nullable(statement.column_name.as_str())
1146 .is_none()
1147 {
1148 return SqlDdlBindError::UnknownColumn {
1149 entity_name: entity_name.to_string(),
1150 column_name: statement.column_name.clone(),
1151 };
1152 }
1153
1154 SqlDdlBindError::UnsupportedAlterTableAlterColumn {
1155 entity_name: entity_name.to_string(),
1156 column_name: statement.column_name.clone(),
1157 action: statement.action.label().to_string(),
1158 }
1159}
1160
1161fn bind_alter_table_alter_column_statement(
1162 statement: &SqlAlterTableAlterColumnStatement,
1163 accepted_before: &AcceptedSchemaSnapshot,
1164 schema: &SchemaInfo,
1165) -> Result<BoundSqlDdlRequest, SqlDdlBindError> {
1166 let Some(entity_name) = schema.entity_name() else {
1167 return Err(SqlDdlBindError::MissingEntityName);
1168 };
1169
1170 if !identifiers_tail_match(statement.entity.as_str(), entity_name) {
1171 return Err(SqlDdlBindError::EntityMismatch {
1172 sql_entity: statement.entity.clone(),
1173 expected_entity: entity_name.to_string(),
1174 });
1175 }
1176
1177 let field = accepted_before
1178 .persisted_snapshot()
1179 .fields()
1180 .iter()
1181 .find(|field| field.name() == statement.column_name)
1182 .ok_or_else(|| SqlDdlBindError::UnknownColumn {
1183 entity_name: entity_name.to_string(),
1184 column_name: statement.column_name.clone(),
1185 })?;
1186
1187 match &statement.action {
1188 SqlAlterColumnAction::SetDefault(default) => {
1189 reject_generated_field_default_change(entity_name, field)?;
1190 let default =
1191 schema_field_default_for_alter_column_default(entity_name, field, default)?;
1192 Ok(bind_alter_table_alter_column_default(
1193 entity_name,
1194 field,
1195 default,
1196 SqlDdlMutationKind::SetFieldDefault,
1197 ))
1198 }
1199 SqlAlterColumnAction::DropDefault => {
1200 if !field.default().is_none() {
1201 reject_generated_field_default_change(entity_name, field)?;
1202 }
1203 if !field.nullable() && !field.default().is_none() {
1204 return Err(SqlDdlBindError::UnsupportedAlterTableDropDefaultRequired {
1205 entity_name: entity_name.to_string(),
1206 column_name: statement.column_name.clone(),
1207 });
1208 }
1209 Ok(bind_alter_table_alter_column_default(
1210 entity_name,
1211 field,
1212 SchemaFieldDefault::None,
1213 SqlDdlMutationKind::DropFieldDefault,
1214 ))
1215 }
1216 SqlAlterColumnAction::SetNotNull => Ok(bind_alter_table_alter_column_nullability(
1217 entity_name,
1218 field,
1219 false,
1220 SqlDdlMutationKind::SetFieldNotNull,
1221 )?),
1222 SqlAlterColumnAction::DropNotNull => Ok(bind_alter_table_alter_column_nullability(
1223 entity_name,
1224 field,
1225 true,
1226 SqlDdlMutationKind::DropFieldNotNull,
1227 )?),
1228 }
1229}
1230
1231fn bind_alter_table_drop_column_statement(
1232 statement: &SqlAlterTableDropColumnStatement,
1233 accepted_before: &AcceptedSchemaSnapshot,
1234 schema: &SchemaInfo,
1235) -> Result<BoundSqlDdlRequest, SqlDdlBindError> {
1236 let entity_name = schema
1237 .entity_name()
1238 .ok_or(SqlDdlBindError::MissingEntityName)?;
1239
1240 if !identifiers_tail_match(statement.entity.as_str(), entity_name) {
1241 return Err(SqlDdlBindError::EntityMismatch {
1242 sql_entity: statement.entity.clone(),
1243 expected_entity: entity_name.to_string(),
1244 });
1245 }
1246
1247 let accepted = accepted_before.persisted_snapshot();
1248 let Some(field) = accepted
1249 .fields()
1250 .iter()
1251 .find(|field| field.name() == statement.column_name)
1252 else {
1253 if statement.if_exists {
1254 return Ok(BoundSqlDdlRequest {
1255 statement: BoundSqlDdlStatement::NoOp(BoundSqlDdlNoOpRequest {
1256 mutation_kind: SqlDdlMutationKind::DropField,
1257 index_name: statement.column_name.clone(),
1258 entity_name: entity_name.to_string(),
1259 target_store: "-".to_string(),
1260 field_path: vec![statement.column_name.clone()],
1261 }),
1262 });
1263 }
1264
1265 return Err(SqlDdlBindError::UnknownColumn {
1266 entity_name: entity_name.to_string(),
1267 column_name: statement.column_name.clone(),
1268 });
1269 };
1270
1271 if accepted.primary_key_field_ids().contains(&field.id()) {
1272 return Err(SqlDdlBindError::PrimaryKeyFieldDropRejected {
1273 entity_name: entity_name.to_string(),
1274 column_name: statement.column_name.clone(),
1275 });
1276 }
1277
1278 if field.generated() {
1279 return Err(SqlDdlBindError::GeneratedFieldDropRejected {
1280 entity_name: entity_name.to_string(),
1281 column_name: statement.column_name.clone(),
1282 });
1283 }
1284
1285 if let Some(index_name) =
1286 resolve_sql_ddl_field_drop_dependent_index(accepted_before, field.id())
1287 {
1288 return Err(SqlDdlBindError::IndexedFieldDropRejected {
1289 entity_name: entity_name.to_string(),
1290 column_name: statement.column_name.clone(),
1291 index_name,
1292 });
1293 }
1294
1295 Ok(BoundSqlDdlRequest {
1296 statement: BoundSqlDdlStatement::DropColumn(BoundSqlDropColumnRequest {
1297 entity_name: entity_name.to_string(),
1298 field: field.clone(),
1299 }),
1300 })
1301}
1302
1303fn bind_alter_table_rename_column_statement(
1304 statement: &SqlAlterTableRenameColumnStatement,
1305 accepted_before: &AcceptedSchemaSnapshot,
1306 schema: &SchemaInfo,
1307) -> Result<BoundSqlDdlRequest, SqlDdlBindError> {
1308 let entity_name = schema
1309 .entity_name()
1310 .ok_or(SqlDdlBindError::MissingEntityName)?;
1311
1312 if !identifiers_tail_match(statement.entity.as_str(), entity_name) {
1313 return Err(SqlDdlBindError::EntityMismatch {
1314 sql_entity: statement.entity.clone(),
1315 expected_entity: entity_name.to_string(),
1316 });
1317 }
1318
1319 let accepted = accepted_before.persisted_snapshot();
1320 let Some(field) = accepted
1321 .fields()
1322 .iter()
1323 .find(|field| field.name() == statement.old_column_name)
1324 else {
1325 return Err(SqlDdlBindError::UnknownColumn {
1326 entity_name: entity_name.to_string(),
1327 column_name: statement.old_column_name.clone(),
1328 });
1329 };
1330
1331 if statement.old_column_name == statement.new_column_name {
1332 return Ok(BoundSqlDdlRequest {
1333 statement: BoundSqlDdlStatement::NoOp(BoundSqlDdlNoOpRequest {
1334 mutation_kind: SqlDdlMutationKind::RenameField,
1335 index_name: statement.old_column_name.clone(),
1336 entity_name: entity_name.to_string(),
1337 target_store: "-".to_string(),
1338 field_path: vec![statement.old_column_name.clone()],
1339 }),
1340 });
1341 }
1342
1343 if accepted
1344 .fields()
1345 .iter()
1346 .any(|field| field.name() == statement.new_column_name)
1347 {
1348 return Err(SqlDdlBindError::DuplicateColumn {
1349 entity_name: entity_name.to_string(),
1350 column_name: statement.new_column_name.clone(),
1351 });
1352 }
1353
1354 if field.generated() {
1355 return Err(SqlDdlBindError::GeneratedFieldRenameRejected {
1356 entity_name: entity_name.to_string(),
1357 column_name: statement.old_column_name.clone(),
1358 });
1359 }
1360
1361 Ok(BoundSqlDdlRequest {
1362 statement: BoundSqlDdlStatement::RenameColumn(BoundSqlRenameColumnRequest {
1363 entity_name: entity_name.to_string(),
1364 field: field.clone(),
1365 new_name: statement.new_column_name.clone(),
1366 }),
1367 })
1368}
1369
1370fn bind_alter_table_alter_column_default(
1371 entity_name: &str,
1372 field: &PersistedFieldSnapshot,
1373 default: SchemaFieldDefault,
1374 mutation_kind: SqlDdlMutationKind,
1375) -> BoundSqlDdlRequest {
1376 if field.default() == &default {
1377 return BoundSqlDdlRequest {
1378 statement: BoundSqlDdlStatement::NoOp(BoundSqlDdlNoOpRequest {
1379 mutation_kind,
1380 index_name: field.name().to_string(),
1381 entity_name: entity_name.to_string(),
1382 target_store: entity_name.to_string(),
1383 field_path: vec![field.name().to_string()],
1384 }),
1385 };
1386 }
1387
1388 BoundSqlDdlRequest {
1389 statement: BoundSqlDdlStatement::AlterColumnDefault(BoundSqlAlterColumnDefaultRequest {
1390 entity_name: entity_name.to_string(),
1391 field: field.clone(),
1392 default,
1393 mutation_kind,
1394 }),
1395 }
1396}
1397
1398fn reject_generated_field_default_change(
1399 entity_name: &str,
1400 field: &PersistedFieldSnapshot,
1401) -> Result<(), SqlDdlBindError> {
1402 if field.generated() {
1403 return Err(SqlDdlBindError::GeneratedFieldDefaultChangeRejected {
1404 entity_name: entity_name.to_string(),
1405 column_name: field.name().to_string(),
1406 });
1407 }
1408
1409 Ok(())
1410}
1411
1412fn bind_alter_table_alter_column_nullability(
1413 entity_name: &str,
1414 field: &PersistedFieldSnapshot,
1415 nullable: bool,
1416 mutation_kind: SqlDdlMutationKind,
1417) -> Result<BoundSqlDdlRequest, SqlDdlBindError> {
1418 if field.nullable() == nullable {
1419 return Ok(BoundSqlDdlRequest {
1420 statement: BoundSqlDdlStatement::NoOp(BoundSqlDdlNoOpRequest {
1421 mutation_kind,
1422 index_name: field.name().to_string(),
1423 entity_name: entity_name.to_string(),
1424 target_store: entity_name.to_string(),
1425 field_path: vec![field.name().to_string()],
1426 }),
1427 });
1428 }
1429
1430 reject_generated_field_nullability_change(entity_name, field)?;
1431
1432 Ok(BoundSqlDdlRequest {
1433 statement: BoundSqlDdlStatement::AlterColumnNullability(
1434 BoundSqlAlterColumnNullabilityRequest {
1435 entity_name: entity_name.to_string(),
1436 field: field.clone(),
1437 nullable,
1438 mutation_kind,
1439 },
1440 ),
1441 })
1442}
1443
1444fn reject_generated_field_nullability_change(
1445 entity_name: &str,
1446 field: &PersistedFieldSnapshot,
1447) -> Result<(), SqlDdlBindError> {
1448 if field.generated() {
1449 return Err(SqlDdlBindError::GeneratedFieldNullabilityChangeRejected {
1450 entity_name: entity_name.to_string(),
1451 column_name: field.name().to_string(),
1452 });
1453 }
1454
1455 Ok(())
1456}
1457
1458fn schema_field_default_for_sql_default(
1459 entity_name: &str,
1460 column_name: &str,
1461 default: Option<&crate::value::Value>,
1462 kind: &PersistedFieldKind,
1463 nullable: bool,
1464 storage_decode: FieldStorageDecode,
1465 leaf_codec: LeafCodec,
1466) -> Result<SchemaFieldDefault, SqlDdlBindError> {
1467 let Some(default) = default else {
1468 return Ok(SchemaFieldDefault::None);
1469 };
1470 if matches!(default, crate::value::Value::Null) {
1471 return Err(SqlDdlBindError::InvalidAlterTableAddColumnDefault {
1472 entity_name: entity_name.to_string(),
1473 column_name: column_name.to_string(),
1474 detail: "NULL cannot be used as an accepted database default".to_string(),
1475 });
1476 }
1477
1478 let normalized = canonicalize_strict_sql_literal_for_persisted_kind(kind, default)
1479 .unwrap_or_else(|| default.clone());
1480 let contract =
1481 AcceptedFieldDecodeContract::new(column_name, kind, nullable, storage_decode, leaf_codec);
1482 let payload = encode_runtime_value_for_accepted_field_contract(contract, &normalized).map_err(
1483 |error| SqlDdlBindError::InvalidAlterTableAddColumnDefault {
1484 entity_name: entity_name.to_string(),
1485 column_name: column_name.to_string(),
1486 detail: error.to_string(),
1487 },
1488 )?;
1489
1490 Ok(SchemaFieldDefault::SlotPayload(payload))
1491}
1492
1493fn schema_field_default_for_alter_column_default(
1494 entity_name: &str,
1495 field: &PersistedFieldSnapshot,
1496 default: &crate::value::Value,
1497) -> Result<SchemaFieldDefault, SqlDdlBindError> {
1498 if matches!(default, crate::value::Value::Null) {
1499 return Err(SqlDdlBindError::InvalidAlterTableAlterColumnDefault {
1500 entity_name: entity_name.to_string(),
1501 column_name: field.name().to_string(),
1502 detail: "NULL cannot be used as an accepted database default".to_string(),
1503 });
1504 }
1505
1506 let normalized = canonicalize_strict_sql_literal_for_persisted_kind(field.kind(), default)
1507 .unwrap_or_else(|| default.clone());
1508 let contract = AcceptedFieldDecodeContract::new(
1509 field.name(),
1510 field.kind(),
1511 field.nullable(),
1512 field.storage_decode(),
1513 field.leaf_codec(),
1514 );
1515 let payload = encode_runtime_value_for_accepted_field_contract(contract, &normalized).map_err(
1516 |error| SqlDdlBindError::InvalidAlterTableAlterColumnDefault {
1517 entity_name: entity_name.to_string(),
1518 column_name: field.name().to_string(),
1519 detail: error.to_string(),
1520 },
1521 )?;
1522
1523 Ok(SchemaFieldDefault::SlotPayload(payload))
1524}
1525
1526fn next_sql_ddl_field_id(accepted_before: &AcceptedSchemaSnapshot) -> FieldId {
1527 let snapshot = accepted_before.persisted_snapshot();
1528 let next = snapshot
1529 .fields()
1530 .iter()
1531 .map(|field| field.id().get())
1532 .chain(
1533 snapshot
1534 .row_layout()
1535 .retired_field_slots()
1536 .iter()
1537 .map(|(field_id, _)| field_id.get()),
1538 )
1539 .max()
1540 .unwrap_or(0)
1541 .checked_add(1)
1542 .expect("accepted field IDs should not be exhausted");
1543
1544 FieldId::new(next)
1545}
1546
1547fn next_sql_ddl_field_slot(accepted_before: &AcceptedSchemaSnapshot) -> SchemaFieldSlot {
1548 accepted_before
1549 .persisted_snapshot()
1550 .row_layout()
1551 .next_unallocated_slot()
1552}
1553
1554fn persisted_field_contract_for_sql_column_type(
1555 column_type: &str,
1556) -> Option<(PersistedFieldKind, FieldStorageDecode, LeafCodec)> {
1557 let normalized = column_type.trim().to_ascii_lowercase();
1558 match normalized.as_str() {
1559 "bool" | "boolean" => Some((
1560 PersistedFieldKind::Bool,
1561 FieldStorageDecode::ByKind,
1562 LeafCodec::Scalar(ScalarCodec::Bool),
1563 )),
1564 "int8" => Some((
1565 PersistedFieldKind::Int8,
1566 FieldStorageDecode::ByKind,
1567 LeafCodec::Scalar(ScalarCodec::Int64),
1568 )),
1569 "int16" => Some((
1570 PersistedFieldKind::Int16,
1571 FieldStorageDecode::ByKind,
1572 LeafCodec::Scalar(ScalarCodec::Int64),
1573 )),
1574 "int32" => Some((
1575 PersistedFieldKind::Int32,
1576 FieldStorageDecode::ByKind,
1577 LeafCodec::Scalar(ScalarCodec::Int64),
1578 )),
1579 "int64" => Some((
1580 PersistedFieldKind::Int64,
1581 FieldStorageDecode::ByKind,
1582 LeafCodec::Scalar(ScalarCodec::Int64),
1583 )),
1584 "int128" => Some((
1585 PersistedFieldKind::Int128,
1586 FieldStorageDecode::ByKind,
1587 LeafCodec::StructuralFallback,
1588 )),
1589 "nat8" => Some((
1590 PersistedFieldKind::Nat8,
1591 FieldStorageDecode::ByKind,
1592 LeafCodec::Scalar(ScalarCodec::Nat64),
1593 )),
1594 "nat16" => Some((
1595 PersistedFieldKind::Nat16,
1596 FieldStorageDecode::ByKind,
1597 LeafCodec::Scalar(ScalarCodec::Nat64),
1598 )),
1599 "nat32" => Some((
1600 PersistedFieldKind::Nat32,
1601 FieldStorageDecode::ByKind,
1602 LeafCodec::Scalar(ScalarCodec::Nat64),
1603 )),
1604 "nat64" => Some((
1605 PersistedFieldKind::Nat64,
1606 FieldStorageDecode::ByKind,
1607 LeafCodec::Scalar(ScalarCodec::Nat64),
1608 )),
1609 "nat128" => Some((
1610 PersistedFieldKind::Nat128,
1611 FieldStorageDecode::ByKind,
1612 LeafCodec::StructuralFallback,
1613 )),
1614 "text" | "string" => Some((
1615 PersistedFieldKind::Text { max_len: None },
1616 FieldStorageDecode::ByKind,
1617 LeafCodec::Scalar(ScalarCodec::Text),
1618 )),
1619 _ => persisted_big_int_contract_for_sql_column_type(&normalized),
1620 }
1621}
1622
1623fn persisted_big_int_contract_for_sql_column_type(
1624 normalized: &str,
1625) -> Option<(PersistedFieldKind, FieldStorageDecode, LeafCodec)> {
1626 if let Some(max_bytes) = sql_big_int_type_max_bytes(normalized, "int_big") {
1627 return Some((
1628 PersistedFieldKind::IntBig { max_bytes },
1629 FieldStorageDecode::ByKind,
1630 LeafCodec::StructuralFallback,
1631 ));
1632 }
1633
1634 sql_big_int_type_max_bytes(normalized, "nat_big").map(|max_bytes| {
1635 (
1636 PersistedFieldKind::NatBig { max_bytes },
1637 FieldStorageDecode::ByKind,
1638 LeafCodec::StructuralFallback,
1639 )
1640 })
1641}
1642
1643fn sql_big_int_type_max_bytes(normalized: &str, type_name: &str) -> Option<u32> {
1644 if normalized == type_name {
1645 return Some(DEFAULT_BIG_INT_MAX_BYTES);
1646 }
1647
1648 let inner = normalized
1649 .strip_prefix(type_name)?
1650 .strip_prefix("(max_bytes=")?
1651 .strip_suffix(')')?;
1652 let max_bytes = inner.parse::<u32>().ok()?;
1653 if max_bytes == 0 {
1654 return None;
1655 }
1656
1657 Some(max_bytes)
1658}
1659
1660#[derive(Clone, Debug, Eq, PartialEq)]
1661pub(in crate::db) enum BoundSqlDdlCreateIndexKey {
1662 FieldPath(BoundSqlDdlFieldPath),
1663 Expression(BoundSqlDdlExpressionKey),
1664}
1665
1666#[derive(Clone, Debug, Eq, PartialEq)]
1672pub(in crate::db) struct BoundSqlDdlExpressionKey {
1673 op: PersistedIndexExpressionOp,
1674 source: BoundSqlDdlFieldPath,
1675 canonical_sql: String,
1676}
1677
1678impl BoundSqlDdlExpressionKey {
1679 #[must_use]
1681 pub(in crate::db) const fn op(&self) -> PersistedIndexExpressionOp {
1682 self.op
1683 }
1684
1685 #[must_use]
1687 pub(in crate::db) const fn source(&self) -> &BoundSqlDdlFieldPath {
1688 &self.source
1689 }
1690
1691 #[must_use]
1693 pub(in crate::db) const fn canonical_sql(&self) -> &str {
1694 self.canonical_sql.as_str()
1695 }
1696}
1697
1698fn bind_create_index_key_item(
1699 key_item: &SqlCreateIndexKeyItem,
1700 entity_name: &str,
1701 schema: &SchemaInfo,
1702) -> Result<BoundSqlDdlCreateIndexKey, SqlDdlBindError> {
1703 match key_item {
1704 SqlCreateIndexKeyItem::FieldPath(field_path) => {
1705 bind_create_index_field_path(field_path.as_str(), entity_name, schema)
1706 .map(BoundSqlDdlCreateIndexKey::FieldPath)
1707 }
1708 SqlCreateIndexKeyItem::Expression(expression) => {
1709 bind_create_index_expression_key(expression, entity_name, schema)
1710 }
1711 }
1712}
1713
1714fn bind_create_index_expression_key(
1715 expression: &SqlCreateIndexExpressionKey,
1716 entity_name: &str,
1717 schema: &SchemaInfo,
1718) -> Result<BoundSqlDdlCreateIndexKey, SqlDdlBindError> {
1719 let source = bind_create_index_field_path(expression.field_path.as_str(), entity_name, schema)?;
1720
1721 Ok(BoundSqlDdlCreateIndexKey::Expression(
1722 BoundSqlDdlExpressionKey {
1723 op: expression_op_from_sql_function(expression.function),
1724 source,
1725 canonical_sql: expression.canonical_sql(),
1726 },
1727 ))
1728}
1729
1730const fn expression_op_from_sql_function(
1731 function: crate::db::sql::parser::SqlCreateIndexExpressionFunction,
1732) -> PersistedIndexExpressionOp {
1733 match function {
1734 crate::db::sql::parser::SqlCreateIndexExpressionFunction::Lower => {
1735 PersistedIndexExpressionOp::Lower
1736 }
1737 crate::db::sql::parser::SqlCreateIndexExpressionFunction::Upper => {
1738 PersistedIndexExpressionOp::Upper
1739 }
1740 crate::db::sql::parser::SqlCreateIndexExpressionFunction::Trim => {
1741 PersistedIndexExpressionOp::Trim
1742 }
1743 }
1744}
1745
1746fn key_items_are_field_path_only(key_items: &[BoundSqlDdlCreateIndexKey]) -> bool {
1747 key_items
1748 .iter()
1749 .all(|key_item| matches!(key_item, BoundSqlDdlCreateIndexKey::FieldPath(_)))
1750}
1751
1752fn create_index_field_path_report_items(
1753 key_items: &[BoundSqlDdlCreateIndexKey],
1754) -> Vec<BoundSqlDdlFieldPath> {
1755 key_items
1756 .iter()
1757 .map(|key_item| match key_item {
1758 BoundSqlDdlCreateIndexKey::FieldPath(field_path) => field_path.clone(),
1759 BoundSqlDdlCreateIndexKey::Expression(expression) => expression.source().clone(),
1760 })
1761 .collect()
1762}
1763
1764fn bind_create_index_field_path(
1765 field_path: &str,
1766 entity_name: &str,
1767 schema: &SchemaInfo,
1768) -> Result<BoundSqlDdlFieldPath, SqlDdlBindError> {
1769 let mut path = field_path
1770 .split('.')
1771 .map(str::trim)
1772 .filter(|segment| !segment.is_empty());
1773 let Some(root) = path.next() else {
1774 return Err(SqlDdlBindError::UnknownFieldPath {
1775 entity_name: entity_name.to_string(),
1776 field_path: field_path.to_string(),
1777 });
1778 };
1779 let segments = path.map(str::to_string).collect::<Vec<_>>();
1780
1781 let capabilities = if segments.is_empty() {
1782 schema.sql_capabilities(root)
1783 } else {
1784 schema.nested_sql_capabilities(root, segments.as_slice())
1785 }
1786 .ok_or_else(|| SqlDdlBindError::UnknownFieldPath {
1787 entity_name: entity_name.to_string(),
1788 field_path: field_path.to_string(),
1789 })?;
1790
1791 if !capabilities.orderable() {
1792 return Err(SqlDdlBindError::FieldPathNotIndexable {
1793 field_path: field_path.to_string(),
1794 });
1795 }
1796
1797 let mut accepted_path = Vec::with_capacity(segments.len() + 1);
1798 accepted_path.push(root.to_string());
1799 accepted_path.extend(segments.iter().cloned());
1800
1801 Ok(BoundSqlDdlFieldPath {
1802 root: root.to_string(),
1803 segments,
1804 accepted_path,
1805 })
1806}
1807
1808fn find_field_path_index_by_name<'a>(
1809 schema: &'a SchemaInfo,
1810 index_name: &str,
1811) -> Option<&'a crate::db::schema::SchemaIndexInfo> {
1812 schema
1813 .field_path_indexes()
1814 .iter()
1815 .find(|index| index.name() == index_name)
1816}
1817
1818fn existing_field_path_index_matches_request(
1819 index: &crate::db::schema::SchemaIndexInfo,
1820 field_paths: &[BoundSqlDdlFieldPath],
1821 predicate_sql: Option<&str>,
1822 uniqueness: SqlCreateIndexUniqueness,
1823) -> bool {
1824 let fields = index.fields();
1825
1826 index.unique() == matches!(uniqueness, SqlCreateIndexUniqueness::Unique)
1827 && index.predicate_sql() == predicate_sql
1828 && fields.len() == field_paths.len()
1829 && fields
1830 .iter()
1831 .zip(field_paths)
1832 .all(|(field, requested)| field.path() == requested.accepted_path())
1833}
1834
1835fn find_expression_index_by_name<'a>(
1836 schema: &'a SchemaInfo,
1837 index_name: &str,
1838) -> Option<&'a SchemaExpressionIndexInfo> {
1839 schema
1840 .expression_indexes()
1841 .iter()
1842 .find(|index| index.name() == index_name)
1843}
1844
1845fn existing_expression_index_matches_request(
1846 index: &SchemaExpressionIndexInfo,
1847 key_items: &[BoundSqlDdlCreateIndexKey],
1848 predicate_sql: Option<&str>,
1849 uniqueness: SqlCreateIndexUniqueness,
1850) -> bool {
1851 let existing_key_items = index.key_items();
1852
1853 index.unique() == matches!(uniqueness, SqlCreateIndexUniqueness::Unique)
1854 && index.predicate_sql() == predicate_sql
1855 && existing_key_items.len() == key_items.len()
1856 && existing_key_items
1857 .iter()
1858 .zip(key_items)
1859 .all(existing_expression_key_item_matches_request)
1860}
1861
1862fn existing_expression_key_item_matches_request(
1863 existing: (
1864 &SchemaExpressionIndexKeyItemInfo,
1865 &BoundSqlDdlCreateIndexKey,
1866 ),
1867) -> bool {
1868 let (existing, requested) = existing;
1869 match (existing, requested) {
1870 (
1871 SchemaExpressionIndexKeyItemInfo::FieldPath(existing),
1872 BoundSqlDdlCreateIndexKey::FieldPath(requested),
1873 ) => existing.path() == requested.accepted_path(),
1874 (
1875 SchemaExpressionIndexKeyItemInfo::Expression(existing),
1876 BoundSqlDdlCreateIndexKey::Expression(requested),
1877 ) => existing_expression_component_matches_request(
1878 existing.op(),
1879 existing.source().path(),
1880 existing.canonical_text(),
1881 requested,
1882 ),
1883 _ => false,
1884 }
1885}
1886
1887fn existing_expression_component_matches_request(
1888 existing_op: PersistedIndexExpressionOp,
1889 existing_path: &[String],
1890 existing_canonical_text: &str,
1891 requested: &BoundSqlDdlExpressionKey,
1892) -> bool {
1893 let requested_path = requested.source().accepted_path();
1894 let requested_canonical_text = format!("expr:v1:{}", requested.canonical_sql());
1895
1896 existing_op == requested.op()
1897 && existing_path == requested_path
1898 && existing_canonical_text == requested_canonical_text
1899}
1900
1901fn reject_duplicate_expression_index(
1902 key_items: &[BoundSqlDdlCreateIndexKey],
1903 predicate_sql: Option<&str>,
1904 schema: &SchemaInfo,
1905) -> Result<(), SqlDdlBindError> {
1906 let Some(existing_index) = schema.expression_indexes().iter().find(|index| {
1907 existing_expression_index_matches_request(
1908 index,
1909 key_items,
1910 predicate_sql,
1911 if index.unique() {
1912 SqlCreateIndexUniqueness::Unique
1913 } else {
1914 SqlCreateIndexUniqueness::NonUnique
1915 },
1916 )
1917 }) else {
1918 return Ok(());
1919 };
1920
1921 Err(SqlDdlBindError::DuplicateFieldPathIndex {
1922 field_path: ddl_key_item_report(key_items).join(","),
1923 existing_index: existing_index.name().to_string(),
1924 })
1925}
1926
1927fn reject_duplicate_field_path_index(
1928 field_paths: &[BoundSqlDdlFieldPath],
1929 predicate_sql: Option<&str>,
1930 schema: &SchemaInfo,
1931) -> Result<(), SqlDdlBindError> {
1932 let Some(existing_index) = schema.field_path_indexes().iter().find(|index| {
1933 let fields = index.fields();
1934 index.predicate_sql() == predicate_sql
1935 && fields.len() == field_paths.len()
1936 && fields
1937 .iter()
1938 .zip(field_paths)
1939 .all(|(field, requested)| field.path() == requested.accepted_path())
1940 }) else {
1941 return Ok(());
1942 };
1943
1944 Err(SqlDdlBindError::DuplicateFieldPathIndex {
1945 field_path: ddl_field_path_report(field_paths).join(","),
1946 existing_index: existing_index.name().to_string(),
1947 })
1948}
1949
1950fn candidate_index_snapshot(
1951 index_name: &str,
1952 key_items: &[BoundSqlDdlCreateIndexKey],
1953 predicate_sql: Option<&str>,
1954 uniqueness: SqlCreateIndexUniqueness,
1955 schema: &SchemaInfo,
1956 index_store_path: &'static str,
1957) -> Result<PersistedIndexSnapshot, SqlDdlBindError> {
1958 let key = if key_items_are_field_path_only(key_items) {
1959 PersistedIndexKeySnapshot::FieldPath(
1960 key_items
1961 .iter()
1962 .map(|key_item| {
1963 let BoundSqlDdlCreateIndexKey::FieldPath(field_path) = key_item else {
1964 unreachable!("field-path-only index checked before field-path lowering");
1965 };
1966
1967 accepted_index_field_path_snapshot(schema, field_path)
1968 })
1969 .collect::<Result<Vec<_>, _>>()?,
1970 )
1971 } else {
1972 PersistedIndexKeySnapshot::Items(
1973 key_items
1974 .iter()
1975 .map(|key_item| match key_item {
1976 BoundSqlDdlCreateIndexKey::FieldPath(field_path) => {
1977 accepted_index_field_path_snapshot(schema, field_path)
1978 .map(PersistedIndexKeyItemSnapshot::FieldPath)
1979 }
1980 BoundSqlDdlCreateIndexKey::Expression(expression) => {
1981 accepted_index_expression_snapshot(schema, expression)
1982 }
1983 })
1984 .collect::<Result<Vec<_>, _>>()?,
1985 )
1986 };
1987
1988 Ok(PersistedIndexSnapshot::new_sql_ddl(
1989 schema.next_secondary_index_ordinal(),
1990 index_name.to_string(),
1991 index_store_path.to_string(),
1992 matches!(uniqueness, SqlCreateIndexUniqueness::Unique),
1993 key,
1994 predicate_sql.map(str::to_string),
1995 ))
1996}
1997
1998fn accepted_index_field_path_snapshot(
1999 schema: &SchemaInfo,
2000 field_path: &BoundSqlDdlFieldPath,
2001) -> Result<crate::db::schema::PersistedIndexFieldPathSnapshot, SqlDdlBindError> {
2002 schema
2003 .accepted_index_field_path_snapshot(field_path.root(), field_path.segments())
2004 .ok_or_else(|| SqlDdlBindError::FieldPathNotAcceptedCatalogBacked {
2005 field_path: field_path.accepted_path().join("."),
2006 })
2007}
2008
2009fn accepted_index_expression_snapshot(
2010 schema: &SchemaInfo,
2011 expression: &BoundSqlDdlExpressionKey,
2012) -> Result<PersistedIndexKeyItemSnapshot, SqlDdlBindError> {
2013 let source = accepted_index_field_path_snapshot(schema, expression.source())?;
2014 let Some(output_kind) = expression_output_kind(expression.op(), source.kind()) else {
2015 return Err(SqlDdlBindError::FieldPathNotIndexable {
2016 field_path: expression.source().accepted_path().join("."),
2017 });
2018 };
2019
2020 Ok(PersistedIndexKeyItemSnapshot::Expression(Box::new(
2021 PersistedIndexExpressionSnapshot::new(
2022 expression.op(),
2023 source.clone(),
2024 source.kind().clone(),
2025 output_kind,
2026 format!("expr:v1:{}", expression.canonical_sql()),
2027 ),
2028 )))
2029}
2030
2031fn expression_output_kind(
2032 op: PersistedIndexExpressionOp,
2033 source_kind: &PersistedFieldKind,
2034) -> Option<PersistedFieldKind> {
2035 match op {
2036 PersistedIndexExpressionOp::Lower
2037 | PersistedIndexExpressionOp::Upper
2038 | PersistedIndexExpressionOp::Trim
2039 | PersistedIndexExpressionOp::LowerTrim => {
2040 if matches!(source_kind, PersistedFieldKind::Text { .. }) {
2041 Some(source_kind.clone())
2042 } else {
2043 None
2044 }
2045 }
2046 PersistedIndexExpressionOp::Date => {
2047 if matches!(
2048 source_kind,
2049 PersistedFieldKind::Date | PersistedFieldKind::Timestamp
2050 ) {
2051 Some(PersistedFieldKind::Date)
2052 } else {
2053 None
2054 }
2055 }
2056 PersistedIndexExpressionOp::Year
2057 | PersistedIndexExpressionOp::Month
2058 | PersistedIndexExpressionOp::Day => {
2059 if matches!(
2060 source_kind,
2061 PersistedFieldKind::Date | PersistedFieldKind::Timestamp
2062 ) {
2063 Some(PersistedFieldKind::Int64)
2064 } else {
2065 None
2066 }
2067 }
2068 }
2069}
2070
2071fn validated_create_index_predicate_sql(
2072 predicate_sql: Option<&str>,
2073 schema: &SchemaInfo,
2074) -> Result<Option<String>, SqlDdlBindError> {
2075 let Some(predicate_sql) = predicate_sql else {
2076 return Ok(None);
2077 };
2078 let predicate = parse_sql_predicate(predicate_sql).map_err(|error| {
2079 SqlDdlBindError::InvalidFilteredIndexPredicate {
2080 detail: error.to_string(),
2081 }
2082 })?;
2083 validate_predicate(schema, &predicate).map_err(|error| {
2084 SqlDdlBindError::InvalidFilteredIndexPredicate {
2085 detail: error.to_string(),
2086 }
2087 })?;
2088
2089 Ok(Some(predicate_sql.to_string()))
2090}
2091
2092fn ddl_field_path_report(field_paths: &[BoundSqlDdlFieldPath]) -> Vec<String> {
2093 match field_paths {
2094 [field_path] => field_path.accepted_path().to_vec(),
2095 _ => vec![
2096 field_paths
2097 .iter()
2098 .map(|field_path| field_path.accepted_path().join("."))
2099 .collect::<Vec<_>>()
2100 .join(","),
2101 ],
2102 }
2103}
2104
2105fn ddl_key_item_report(key_items: &[BoundSqlDdlCreateIndexKey]) -> Vec<String> {
2106 match key_items {
2107 [key_item] => vec![ddl_key_item_text(key_item)],
2108 _ => vec![
2109 key_items
2110 .iter()
2111 .map(ddl_key_item_text)
2112 .collect::<Vec<_>>()
2113 .join(","),
2114 ],
2115 }
2116}
2117
2118fn ddl_key_item_text(key_item: &BoundSqlDdlCreateIndexKey) -> String {
2119 match key_item {
2120 BoundSqlDdlCreateIndexKey::FieldPath(field_path) => field_path.accepted_path().join("."),
2121 BoundSqlDdlCreateIndexKey::Expression(expression) => expression.canonical_sql().to_string(),
2122 }
2123}
2124
2125pub(in crate::db) fn lower_bound_sql_ddl_to_schema_mutation_admission(
2127 request: &BoundSqlDdlRequest,
2128) -> Result<SchemaDdlMutationAdmission, SqlDdlLoweringError> {
2129 match request.statement() {
2130 BoundSqlDdlStatement::AddColumn(add) => {
2131 Ok(admit_sql_ddl_field_addition_candidate(add.field()))
2132 }
2133 BoundSqlDdlStatement::AlterColumnDefault(alter) => {
2134 Ok(admit_sql_ddl_field_default_candidate(alter.field()))
2135 }
2136 BoundSqlDdlStatement::AlterColumnNullability(alter) => {
2137 Ok(admit_sql_ddl_field_nullability_candidate(alter.field()))
2138 }
2139 BoundSqlDdlStatement::DropColumn(drop) => {
2140 Ok(admit_sql_ddl_field_drop_candidate(drop.field()))
2141 }
2142 BoundSqlDdlStatement::RenameColumn(rename) => {
2143 let after = rename
2144 .field()
2145 .clone_with_name(rename.new_name().to_string());
2146 Ok(admit_sql_ddl_field_rename_candidate(rename.field(), &after))
2147 }
2148 BoundSqlDdlStatement::CreateIndex(create) => {
2149 if create.candidate_index().key().is_field_path_only() {
2150 admit_sql_ddl_field_path_index_candidate(create.candidate_index())
2151 } else {
2152 admit_sql_ddl_expression_index_candidate(create.candidate_index())
2153 }
2154 }
2155 BoundSqlDdlStatement::DropIndex(drop) => {
2156 admit_sql_ddl_secondary_index_drop_candidate(drop.dropped_index())
2157 }
2158 BoundSqlDdlStatement::NoOp(_) => return Err(SqlDdlLoweringError::UnsupportedStatement),
2159 }
2160 .map_err(SqlDdlLoweringError::MutationAdmission)
2161}
2162
2163pub(in crate::db) fn derive_bound_sql_ddl_accepted_after(
2165 accepted_before: &AcceptedSchemaSnapshot,
2166 request: &BoundSqlDdlRequest,
2167) -> Result<SchemaDdlAcceptedSnapshotDerivation, SqlDdlLoweringError> {
2168 match request.statement() {
2169 BoundSqlDdlStatement::AddColumn(add) => {
2170 derive_sql_ddl_field_addition_accepted_after(accepted_before, add.field().clone())
2171 }
2172 BoundSqlDdlStatement::AlterColumnDefault(alter) => {
2173 derive_sql_ddl_field_default_accepted_after(
2174 accepted_before,
2175 alter.field_name(),
2176 alter.default().clone(),
2177 )
2178 }
2179 BoundSqlDdlStatement::AlterColumnNullability(alter) => {
2180 derive_sql_ddl_field_nullability_accepted_after(
2181 accepted_before,
2182 alter.field_name(),
2183 alter.nullable(),
2184 )
2185 }
2186 BoundSqlDdlStatement::DropColumn(drop) => {
2187 derive_sql_ddl_field_drop_accepted_after(accepted_before, drop.field_name())
2188 }
2189 BoundSqlDdlStatement::RenameColumn(rename) => derive_sql_ddl_field_rename_accepted_after(
2190 accepted_before,
2191 rename.old_name(),
2192 rename.new_name(),
2193 ),
2194 BoundSqlDdlStatement::CreateIndex(create) => {
2195 if create.candidate_index().key().is_field_path_only() {
2196 derive_sql_ddl_field_path_index_accepted_after(
2197 accepted_before,
2198 create.candidate_index().clone(),
2199 )
2200 } else {
2201 derive_sql_ddl_expression_index_accepted_after(
2202 accepted_before,
2203 create.candidate_index().clone(),
2204 )
2205 }
2206 }
2207 BoundSqlDdlStatement::DropIndex(drop) => {
2208 derive_sql_ddl_secondary_index_drop_accepted_after(
2209 accepted_before,
2210 drop.dropped_index(),
2211 )
2212 }
2213 BoundSqlDdlStatement::NoOp(_) => return Err(SqlDdlLoweringError::UnsupportedStatement),
2214 }
2215 .map_err(SqlDdlLoweringError::MutationAdmission)
2216}
2217
2218fn ddl_preparation_report(bound: &BoundSqlDdlRequest) -> SqlDdlPreparationReport {
2219 match bound.statement() {
2220 BoundSqlDdlStatement::AddColumn(add) => SqlDdlPreparationReport {
2221 mutation_kind: if add.field().default().is_none() {
2222 SqlDdlMutationKind::AddNullableField
2223 } else {
2224 SqlDdlMutationKind::AddDefaultedField
2225 },
2226 target_index: add.field().name().to_string(),
2227 target_store: add.entity_name().to_string(),
2228 field_path: vec![add.field().name().to_string()],
2229 execution_status: SqlDdlExecutionStatus::PreparedOnly,
2230 rows_scanned: 0,
2231 index_keys_written: 0,
2232 },
2233 BoundSqlDdlStatement::AlterColumnDefault(alter) => SqlDdlPreparationReport {
2234 mutation_kind: alter.mutation_kind(),
2235 target_index: alter.field_name().to_string(),
2236 target_store: alter.entity_name().to_string(),
2237 field_path: vec![alter.field_name().to_string()],
2238 execution_status: SqlDdlExecutionStatus::PreparedOnly,
2239 rows_scanned: 0,
2240 index_keys_written: 0,
2241 },
2242 BoundSqlDdlStatement::AlterColumnNullability(alter) => SqlDdlPreparationReport {
2243 mutation_kind: alter.mutation_kind(),
2244 target_index: alter.field_name().to_string(),
2245 target_store: alter.entity_name().to_string(),
2246 field_path: vec![alter.field_name().to_string()],
2247 execution_status: SqlDdlExecutionStatus::PreparedOnly,
2248 rows_scanned: 0,
2249 index_keys_written: 0,
2250 },
2251 BoundSqlDdlStatement::DropColumn(drop) => SqlDdlPreparationReport {
2252 mutation_kind: SqlDdlMutationKind::DropField,
2253 target_index: drop.field_name().to_string(),
2254 target_store: drop.entity_name().to_string(),
2255 field_path: vec![drop.field_name().to_string()],
2256 execution_status: SqlDdlExecutionStatus::PreparedOnly,
2257 rows_scanned: 0,
2258 index_keys_written: 0,
2259 },
2260 BoundSqlDdlStatement::RenameColumn(rename) => SqlDdlPreparationReport {
2261 mutation_kind: SqlDdlMutationKind::RenameField,
2262 target_index: rename.new_name().to_string(),
2263 target_store: rename.entity_name().to_string(),
2264 field_path: vec![rename.old_name().to_string(), rename.new_name().to_string()],
2265 execution_status: SqlDdlExecutionStatus::PreparedOnly,
2266 rows_scanned: 0,
2267 index_keys_written: 0,
2268 },
2269 BoundSqlDdlStatement::CreateIndex(create) => {
2270 let target = create.candidate_index();
2271
2272 SqlDdlPreparationReport {
2273 mutation_kind: if target.key().is_field_path_only() {
2274 SqlDdlMutationKind::AddFieldPathIndex
2275 } else {
2276 SqlDdlMutationKind::AddExpressionIndex
2277 },
2278 target_index: target.name().to_string(),
2279 target_store: target.store().to_string(),
2280 field_path: ddl_key_item_report(create.key_items()),
2281 execution_status: SqlDdlExecutionStatus::PreparedOnly,
2282 rows_scanned: 0,
2283 index_keys_written: 0,
2284 }
2285 }
2286 BoundSqlDdlStatement::DropIndex(drop) => SqlDdlPreparationReport {
2287 mutation_kind: SqlDdlMutationKind::DropSecondaryIndex,
2288 target_index: drop.index_name().to_string(),
2289 target_store: drop.dropped_index().store().to_string(),
2290 field_path: drop.field_path().to_vec(),
2291 execution_status: SqlDdlExecutionStatus::PreparedOnly,
2292 rows_scanned: 0,
2293 index_keys_written: 0,
2294 },
2295 BoundSqlDdlStatement::NoOp(no_op) => SqlDdlPreparationReport {
2296 mutation_kind: no_op.mutation_kind(),
2297 target_index: no_op.index_name().to_string(),
2298 target_store: no_op.target_store().to_string(),
2299 field_path: no_op.field_path().to_vec(),
2300 execution_status: SqlDdlExecutionStatus::PreparedOnly,
2301 rows_scanned: 0,
2302 index_keys_written: 0,
2303 },
2304 }
2305}