1mod admission;
7mod field;
8pub(in crate::db) use field::{
9 BoundSqlAddColumnRequest, BoundSqlAlterColumnDefaultRequest,
10 BoundSqlAlterColumnNullabilityRequest, BoundSqlDropColumnRequest, BoundSqlRenameColumnRequest,
11};
12use field::{
13 bind_alter_table_add_column_statement, bind_alter_table_alter_column_statement,
14 bind_alter_table_drop_column_statement, bind_alter_table_rename_column_statement,
15};
16
17mod index;
18pub(in crate::db) use index::{BoundSqlCreateIndexRequest, BoundSqlDropIndexRequest};
19use index::{bind_create_index_statement, bind_drop_index_statement, ddl_key_item_report};
20
21use crate::db::{
22 schema::{
23 AcceptedSchemaSnapshot, SchemaDdlAcceptedSnapshotDerivation,
24 SchemaDdlMutationAdmissionError, SchemaInfo, SchemaVersion,
25 derive_sql_ddl_expression_index_accepted_after,
26 derive_sql_ddl_field_addition_accepted_after, derive_sql_ddl_field_default_accepted_after,
27 derive_sql_ddl_field_drop_accepted_after, derive_sql_ddl_field_nullability_accepted_after,
28 derive_sql_ddl_field_path_index_accepted_after, derive_sql_ddl_field_rename_accepted_after,
29 derive_sql_ddl_secondary_index_drop_accepted_after,
30 },
31 sql::parser::{SqlDdlSchemaVersionContract, SqlDdlStatement, SqlStatement},
32};
33use thiserror::Error as ThisError;
34
35#[cfg(test)]
36use crate::db::schema::{
37 SchemaDdlMutationAdmission, admit_sql_ddl_expression_index_candidate,
38 admit_sql_ddl_field_addition_candidate, admit_sql_ddl_field_default_candidate,
39 admit_sql_ddl_field_drop_candidate, admit_sql_ddl_field_nullability_candidate,
40 admit_sql_ddl_field_path_index_candidate, admit_sql_ddl_field_rename_candidate,
41 admit_sql_ddl_secondary_index_drop_candidate,
42};
43
44#[derive(Clone, Debug, Eq, PartialEq)]
52pub(in crate::db) struct PreparedSqlDdlCommand {
53 bound: BoundSqlDdlRequest,
54 derivation: Option<SchemaDdlAcceptedSnapshotDerivation>,
55 report: SqlDdlPreparationReport,
56}
57
58impl PreparedSqlDdlCommand {
59 #[must_use]
61 pub(in crate::db) const fn bound(&self) -> &BoundSqlDdlRequest {
62 &self.bound
63 }
64
65 #[must_use]
67 pub(in crate::db) const fn derivation(&self) -> Option<&SchemaDdlAcceptedSnapshotDerivation> {
68 self.derivation.as_ref()
69 }
70
71 #[must_use]
73 pub(in crate::db) const fn report(&self) -> &SqlDdlPreparationReport {
74 &self.report
75 }
76
77 #[must_use]
79 pub(in crate::db) const fn mutates_schema(&self) -> bool {
80 self.derivation.is_some()
81 }
82}
83
84#[derive(Clone, Debug, Eq, PartialEq)]
91pub struct SqlDdlPreparationReport {
92 mutation_kind: SqlDdlMutationKind,
93 target_index: String,
94 target_store: String,
95 field_path: Vec<String>,
96 execution_status: SqlDdlExecutionStatus,
97 rows_scanned: usize,
98 index_keys_written: usize,
99}
100
101impl SqlDdlPreparationReport {
102 #[must_use]
104 pub const fn mutation_kind(&self) -> SqlDdlMutationKind {
105 self.mutation_kind
106 }
107
108 #[must_use]
110 pub const fn target_index(&self) -> &str {
111 self.target_index.as_str()
112 }
113
114 #[must_use]
116 pub const fn target_store(&self) -> &str {
117 self.target_store.as_str()
118 }
119
120 #[must_use]
122 pub const fn field_path(&self) -> &[String] {
123 self.field_path.as_slice()
124 }
125
126 #[must_use]
128 pub const fn execution_status(&self) -> SqlDdlExecutionStatus {
129 self.execution_status
130 }
131
132 #[must_use]
134 pub const fn rows_scanned(&self) -> usize {
135 self.rows_scanned
136 }
137
138 #[must_use]
140 pub const fn index_keys_written(&self) -> usize {
141 self.index_keys_written
142 }
143
144 pub(in crate::db) const fn with_execution_status(
145 mut self,
146 execution_status: SqlDdlExecutionStatus,
147 ) -> Self {
148 self.execution_status = execution_status;
149 self
150 }
151
152 pub(in crate::db) const fn with_execution_metrics(
153 mut self,
154 rows_scanned: usize,
155 index_keys_written: usize,
156 ) -> Self {
157 self.rows_scanned = rows_scanned;
158 self.index_keys_written = index_keys_written;
159 self
160 }
161}
162
163#[derive(Clone, Copy, Debug, Eq, PartialEq)]
169pub enum SqlDdlMutationKind {
170 AddDefaultedField,
171 AddNullableField,
172 SetFieldDefault,
173 DropFieldDefault,
174 SetFieldNotNull,
175 DropFieldNotNull,
176 DropField,
177 RenameField,
178 AddFieldPathIndex,
179 AddExpressionIndex,
180 DropSecondaryIndex,
181}
182
183impl SqlDdlMutationKind {
184 #[must_use]
186 pub const fn as_str(self) -> &'static str {
187 match self {
188 Self::AddDefaultedField => "add_defaulted_field",
189 Self::AddNullableField => "add_nullable_field",
190 Self::SetFieldDefault => "set_field_default",
191 Self::DropFieldDefault => "drop_field_default",
192 Self::SetFieldNotNull => "set_field_not_null",
193 Self::DropFieldNotNull => "drop_field_not_null",
194 Self::DropField => "drop_field",
195 Self::RenameField => "rename_field",
196 Self::AddFieldPathIndex => "add_field_path_index",
197 Self::AddExpressionIndex => "add_expression_index",
198 Self::DropSecondaryIndex => "drop_secondary_index",
199 }
200 }
201}
202
203#[derive(Clone, Copy, Debug, Eq, PartialEq)]
209pub enum SqlDdlExecutionStatus {
210 PreparedOnly,
211 Published,
212 NoOp,
213}
214
215impl SqlDdlExecutionStatus {
216 #[must_use]
218 pub const fn as_str(self) -> &'static str {
219 match self {
220 Self::PreparedOnly => "prepared_only",
221 Self::Published => "published",
222 Self::NoOp => "no_op",
223 }
224 }
225}
226
227#[derive(Clone, Debug, Eq, PartialEq)]
234pub(in crate::db) struct BoundSqlDdlRequest {
235 statement: BoundSqlDdlStatement,
236 schema_version_contract: BoundSqlDdlSchemaVersionContract,
237}
238
239impl BoundSqlDdlRequest {
240 #[must_use]
242 pub(in crate::db) const fn statement(&self) -> &BoundSqlDdlStatement {
243 &self.statement
244 }
245
246 #[must_use]
248 pub(in crate::db) const fn schema_version_contract(&self) -> BoundSqlDdlSchemaVersionContract {
249 self.schema_version_contract
250 }
251}
252
253#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
260pub(in crate::db) struct BoundSqlDdlSchemaVersionContract {
261 expected_schema_version: Option<SchemaVersion>,
262 next_schema_version: Option<SchemaVersion>,
263}
264
265impl BoundSqlDdlSchemaVersionContract {
266 #[must_use]
268 pub(in crate::db) const fn expected_schema_version(self) -> Option<SchemaVersion> {
269 self.expected_schema_version
270 }
271
272 #[must_use]
274 pub(in crate::db) const fn next_schema_version(self) -> Option<SchemaVersion> {
275 self.next_schema_version
276 }
277}
278
279#[derive(Clone, Debug, Eq, PartialEq)]
285pub(in crate::db) enum BoundSqlDdlStatement {
286 AddColumn(BoundSqlAddColumnRequest),
287 AlterColumnDefault(BoundSqlAlterColumnDefaultRequest),
288 AlterColumnNullability(BoundSqlAlterColumnNullabilityRequest),
289 DropColumn(BoundSqlDropColumnRequest),
290 RenameColumn(BoundSqlRenameColumnRequest),
291 CreateIndex(BoundSqlCreateIndexRequest),
292 DropIndex(BoundSqlDropIndexRequest),
293 NoOp(BoundSqlDdlNoOpRequest),
294}
295
296#[derive(Clone, Debug, Eq, PartialEq)]
302pub(in crate::db) struct BoundSqlDdlNoOpRequest {
303 mutation_kind: SqlDdlMutationKind,
304 index_name: String,
305 entity_name: String,
306 target_store: String,
307 field_path: Vec<String>,
308}
309
310impl BoundSqlDdlNoOpRequest {
311 #[must_use]
313 pub(in crate::db) const fn mutation_kind(&self) -> SqlDdlMutationKind {
314 self.mutation_kind
315 }
316
317 #[must_use]
319 pub(in crate::db) const fn index_name(&self) -> &str {
320 self.index_name.as_str()
321 }
322
323 #[must_use]
325 #[cfg(test)]
326 pub(in crate::db) const fn entity_name(&self) -> &str {
327 self.entity_name.as_str()
328 }
329
330 #[must_use]
332 pub(in crate::db) const fn target_store(&self) -> &str {
333 self.target_store.as_str()
334 }
335
336 #[must_use]
338 pub(in crate::db) const fn field_path(&self) -> &[String] {
339 self.field_path.as_slice()
340 }
341}
342
343#[derive(Debug, Eq, PartialEq, ThisError)]
349pub(in crate::db) enum SqlDdlBindError {
350 #[error("SQL DDL binder requires a DDL statement")]
351 NotDdl,
352
353 #[error("accepted schema does not expose an entity name")]
354 MissingEntityName,
355
356 #[error("SQL entity '{sql_entity}' does not match accepted entity '{expected_entity}'")]
357 EntityMismatch {
358 sql_entity: String,
359 expected_entity: String,
360 },
361
362 #[error("unknown field path '{field_path}' for accepted entity '{entity_name}'")]
363 UnknownFieldPath {
364 entity_name: String,
365 field_path: String,
366 },
367
368 #[error("field path '{field_path}' is not indexable")]
369 FieldPathNotIndexable { field_path: String },
370
371 #[error("field path '{field_path}' depends on generated-only metadata")]
372 FieldPathNotAcceptedCatalogBacked { field_path: String },
373
374 #[error("invalid filtered index predicate: {detail}")]
375 InvalidFilteredIndexPredicate { detail: String },
376
377 #[error("index name '{index_name}' already exists in the accepted schema")]
378 DuplicateIndexName { index_name: String },
379
380 #[error("accepted schema already has index '{existing_index}' for field path '{field_path}'")]
381 DuplicateFieldPathIndex {
382 field_path: String,
383 existing_index: String,
384 },
385
386 #[error("unknown index '{index_name}' for accepted entity '{entity_name}'")]
387 UnknownIndex {
388 entity_name: String,
389 index_name: String,
390 },
391
392 #[error(
393 "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"
394 )]
395 GeneratedIndexDropRejected { index_name: String },
396
397 #[error(
398 "index '{index_name}' is not a supported DDL-droppable secondary index; SQL DDL can currently drop only indexes created through SQL DDL"
399 )]
400 UnsupportedDropIndex { index_name: String },
401
402 #[error(
403 "SQL DDL ALTER TABLE ADD COLUMN DEFAULT value is not encodable for accepted entity '{entity_name}' column '{column_name}': {detail}"
404 )]
405 InvalidAlterTableAddColumnDefault {
406 entity_name: String,
407 column_name: String,
408 detail: String,
409 },
410
411 #[error(
412 "SQL DDL ALTER TABLE ADD COLUMN NOT NULL is not executable yet for accepted entity '{entity_name}' column '{column_name}'"
413 )]
414 UnsupportedAlterTableAddColumnNotNull {
415 entity_name: String,
416 column_name: String,
417 },
418
419 #[error("field '{column_name}' already exists in accepted entity '{entity_name}'")]
420 DuplicateColumn {
421 entity_name: String,
422 column_name: String,
423 },
424
425 #[error(
426 "SQL DDL ALTER TABLE ADD COLUMN type '{column_type}' is not supported yet for accepted entity '{entity_name}' column '{column_name}'"
427 )]
428 UnsupportedAlterTableAddColumnType {
429 entity_name: String,
430 column_name: String,
431 column_type: String,
432 },
433
434 #[error("unknown column '{column_name}' for accepted entity '{entity_name}'")]
435 UnknownColumn {
436 entity_name: String,
437 column_name: String,
438 },
439
440 #[error(
441 "SQL DDL ALTER TABLE ALTER COLUMN SET DEFAULT value is not encodable for accepted entity '{entity_name}' column '{column_name}': {detail}"
442 )]
443 InvalidAlterTableAlterColumnDefault {
444 entity_name: String,
445 column_name: String,
446 detail: String,
447 },
448
449 #[error(
450 "SQL DDL ALTER TABLE ALTER COLUMN DROP DEFAULT is not executable yet for required accepted entity '{entity_name}' column '{column_name}'"
451 )]
452 UnsupportedAlterTableDropDefaultRequired {
453 entity_name: String,
454 column_name: String,
455 },
456
457 #[error(
458 "SQL DDL ALTER TABLE ALTER COLUMN DEFAULT cannot change generated accepted field '{column_name}' on entity '{entity_name}'; change the Rust schema default instead"
459 )]
460 GeneratedFieldDefaultChangeRejected {
461 entity_name: String,
462 column_name: String,
463 },
464
465 #[error(
466 "SQL DDL ALTER TABLE ALTER COLUMN NULLABILITY cannot change generated accepted field '{column_name}' on entity '{entity_name}'; change the Rust schema nullability instead"
467 )]
468 GeneratedFieldNullabilityChangeRejected {
469 entity_name: String,
470 column_name: String,
471 },
472
473 #[error(
474 "SQL DDL ALTER TABLE DROP COLUMN cannot drop primary-key field '{column_name}' on entity '{entity_name}'"
475 )]
476 PrimaryKeyFieldDropRejected {
477 entity_name: String,
478 column_name: String,
479 },
480
481 #[error(
482 "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"
483 )]
484 GeneratedFieldDropRejected {
485 entity_name: String,
486 column_name: String,
487 },
488
489 #[error(
490 "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"
491 )]
492 IndexedFieldDropRejected {
493 entity_name: String,
494 column_name: String,
495 index_name: String,
496 },
497
498 #[error(
499 "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"
500 )]
501 GeneratedFieldRenameRejected {
502 entity_name: String,
503 column_name: String,
504 },
505
506 #[error("SQL DDL {clause} must be a positive schema version")]
507 NonPositiveSchemaVersion { clause: &'static str },
508
509 #[error("mutating SQL DDL requires EXPECT SCHEMA VERSION")]
510 MissingExpectedSchemaVersion,
511
512 #[error("mutating SQL DDL requires SET SCHEMA VERSION")]
513 MissingNextSchemaVersion,
514
515 #[error(
516 "SQL DDL expected accepted schema version {expected}, but accepted schema version is {accepted}"
517 )]
518 StaleExpectedSchemaVersion { expected: u32, accepted: u32 },
519
520 #[error("SQL DDL no-op cannot SET SCHEMA VERSION {requested}")]
521 EmptySchemaVersionBump { requested: u32 },
522}
523
524#[derive(Debug, Eq, PartialEq, ThisError)]
531pub(in crate::db) enum SqlDdlLoweringError {
532 #[error("SQL DDL lowering requires a supported DDL statement")]
533 UnsupportedStatement,
534
535 #[error("schema mutation admission rejected DDL candidate: {0}")]
536 MutationAdmission(SchemaDdlMutationAdmissionError),
537}
538
539#[derive(Debug, Eq, PartialEq, ThisError)]
545pub(in crate::db) enum SqlDdlPrepareError {
546 #[error("{0}")]
547 Bind(#[from] SqlDdlBindError),
548
549 #[error("{0}")]
550 Lowering(#[from] SqlDdlLoweringError),
551}
552
553pub(in crate::db) fn prepare_sql_ddl_statement(
555 statement: &SqlStatement,
556 accepted_before: &AcceptedSchemaSnapshot,
557 schema: &SchemaInfo,
558 index_store_path: &'static str,
559) -> Result<PreparedSqlDdlCommand, SqlDdlPrepareError> {
560 let bound = bind_sql_ddl_statement(statement, accepted_before, schema, index_store_path)?;
561 validate_bound_sql_ddl_version_contract(&bound, accepted_before)?;
562 let derivation = if matches!(bound.statement(), BoundSqlDdlStatement::NoOp(_)) {
563 None
564 } else {
565 Some(derive_bound_sql_ddl_accepted_after(
566 accepted_before,
567 &bound,
568 )?)
569 };
570 let report = ddl_preparation_report(&bound);
571
572 Ok(PreparedSqlDdlCommand {
573 bound,
574 derivation,
575 report,
576 })
577}
578
579pub(in crate::db) fn bind_sql_ddl_statement(
581 statement: &SqlStatement,
582 accepted_before: &AcceptedSchemaSnapshot,
583 schema: &SchemaInfo,
584 index_store_path: &'static str,
585) -> Result<BoundSqlDdlRequest, SqlDdlBindError> {
586 let SqlStatement::Ddl(ddl) = statement else {
587 return Err(SqlDdlBindError::NotDdl);
588 };
589
590 let mut bound = match ddl {
591 SqlDdlStatement::CreateIndex(statement) => {
592 bind_create_index_statement(statement, schema, index_store_path)
593 }
594 SqlDdlStatement::DropIndex(statement) => {
595 bind_drop_index_statement(statement, accepted_before, schema)
596 }
597 SqlDdlStatement::AlterTableAddColumn(statement) => {
598 bind_alter_table_add_column_statement(statement, accepted_before, schema)
599 }
600 SqlDdlStatement::AlterTableAlterColumn(statement) => {
601 bind_alter_table_alter_column_statement(statement, accepted_before, schema)
602 }
603 SqlDdlStatement::AlterTableDropColumn(statement) => {
604 bind_alter_table_drop_column_statement(statement, accepted_before, schema)
605 }
606 SqlDdlStatement::AlterTableRenameColumn(statement) => {
607 bind_alter_table_rename_column_statement(statement, accepted_before, schema)
608 }
609 }?;
610 bound.schema_version_contract =
611 bind_sql_ddl_schema_version_contract(ddl_version_contract(ddl))?;
612
613 Ok(bound)
614}
615
616#[cfg(test)]
618pub(in crate::db) fn lower_bound_sql_ddl_to_schema_mutation_admission(
619 request: &BoundSqlDdlRequest,
620) -> Result<SchemaDdlMutationAdmission, SqlDdlLoweringError> {
621 match request.statement() {
622 BoundSqlDdlStatement::AddColumn(add) => {
623 Ok(admit_sql_ddl_field_addition_candidate(add.field()))
624 }
625 BoundSqlDdlStatement::AlterColumnDefault(alter) => {
626 Ok(admit_sql_ddl_field_default_candidate(alter.field()))
627 }
628 BoundSqlDdlStatement::AlterColumnNullability(alter) => {
629 Ok(admit_sql_ddl_field_nullability_candidate(alter.field()))
630 }
631 BoundSqlDdlStatement::DropColumn(drop) => {
632 Ok(admit_sql_ddl_field_drop_candidate(drop.field()))
633 }
634 BoundSqlDdlStatement::RenameColumn(rename) => {
635 let after = rename
636 .field()
637 .clone_with_name(rename.new_name().to_string());
638 Ok(admit_sql_ddl_field_rename_candidate(rename.field(), &after))
639 }
640 BoundSqlDdlStatement::CreateIndex(create) => {
641 if create.candidate_index().key().is_field_path_only() {
642 admit_sql_ddl_field_path_index_candidate(create.candidate_index())
643 } else {
644 admit_sql_ddl_expression_index_candidate(create.candidate_index())
645 }
646 }
647 BoundSqlDdlStatement::DropIndex(drop) => {
648 admit_sql_ddl_secondary_index_drop_candidate(drop.dropped_index())
649 }
650 BoundSqlDdlStatement::NoOp(_) => return Err(SqlDdlLoweringError::UnsupportedStatement),
651 }
652 .map_err(SqlDdlLoweringError::MutationAdmission)
653}
654
655pub(in crate::db) fn derive_bound_sql_ddl_accepted_after(
657 accepted_before: &AcceptedSchemaSnapshot,
658 request: &BoundSqlDdlRequest,
659) -> Result<SchemaDdlAcceptedSnapshotDerivation, SqlDdlLoweringError> {
660 let next_schema_version = request
661 .schema_version_contract()
662 .next_schema_version()
663 .ok_or(SqlDdlLoweringError::UnsupportedStatement)?;
664 let derivation = match request.statement() {
665 BoundSqlDdlStatement::AddColumn(add) => {
666 derive_sql_ddl_field_addition_accepted_after(accepted_before, add.field().clone())
667 }
668 BoundSqlDdlStatement::AlterColumnDefault(alter) => {
669 derive_sql_ddl_field_default_accepted_after(
670 accepted_before,
671 alter.field_name(),
672 alter.default().clone(),
673 )
674 }
675 BoundSqlDdlStatement::AlterColumnNullability(alter) => {
676 derive_sql_ddl_field_nullability_accepted_after(
677 accepted_before,
678 alter.field_name(),
679 alter.nullable(),
680 )
681 }
682 BoundSqlDdlStatement::DropColumn(drop) => {
683 derive_sql_ddl_field_drop_accepted_after(accepted_before, drop.field_name())
684 }
685 BoundSqlDdlStatement::RenameColumn(rename) => derive_sql_ddl_field_rename_accepted_after(
686 accepted_before,
687 rename.old_name(),
688 rename.new_name(),
689 ),
690 BoundSqlDdlStatement::CreateIndex(create) => {
691 if create.candidate_index().key().is_field_path_only() {
692 derive_sql_ddl_field_path_index_accepted_after(
693 accepted_before,
694 create.candidate_index().clone(),
695 )
696 } else {
697 derive_sql_ddl_expression_index_accepted_after(
698 accepted_before,
699 create.candidate_index().clone(),
700 )
701 }
702 }
703 BoundSqlDdlStatement::DropIndex(drop) => {
704 derive_sql_ddl_secondary_index_drop_accepted_after(
705 accepted_before,
706 drop.dropped_index(),
707 )
708 }
709 BoundSqlDdlStatement::NoOp(_) => return Err(SqlDdlLoweringError::UnsupportedStatement),
710 }
711 .map_err(SqlDdlLoweringError::MutationAdmission)?;
712
713 derivation
714 .with_declared_schema_version(accepted_before, next_schema_version)
715 .map_err(SqlDdlLoweringError::MutationAdmission)
716}
717
718const fn ddl_version_contract(ddl: &SqlDdlStatement) -> SqlDdlSchemaVersionContract {
719 match ddl {
720 SqlDdlStatement::CreateIndex(statement) => statement.schema_version_contract,
721 SqlDdlStatement::DropIndex(statement) => statement.schema_version_contract,
722 SqlDdlStatement::AlterTableAddColumn(statement) => statement.schema_version_contract,
723 SqlDdlStatement::AlterTableAlterColumn(statement) => statement.schema_version_contract,
724 SqlDdlStatement::AlterTableDropColumn(statement) => statement.schema_version_contract,
725 SqlDdlStatement::AlterTableRenameColumn(statement) => statement.schema_version_contract,
726 }
727}
728
729fn bind_sql_ddl_schema_version_contract(
730 contract: SqlDdlSchemaVersionContract,
731) -> Result<BoundSqlDdlSchemaVersionContract, SqlDdlBindError> {
732 Ok(BoundSqlDdlSchemaVersionContract {
733 expected_schema_version: bind_sql_ddl_schema_version(
734 "EXPECT SCHEMA VERSION",
735 contract.expected_schema_version,
736 )?,
737 next_schema_version: bind_sql_ddl_schema_version(
738 "SET SCHEMA VERSION",
739 contract.next_schema_version,
740 )?,
741 })
742}
743
744fn bind_sql_ddl_schema_version(
745 clause: &'static str,
746 value: Option<u32>,
747) -> Result<Option<SchemaVersion>, SqlDdlBindError> {
748 value
749 .map(|raw| {
750 if raw == 0 {
751 Err(SqlDdlBindError::NonPositiveSchemaVersion { clause })
752 } else {
753 Ok(SchemaVersion::new(raw))
754 }
755 })
756 .transpose()
757}
758
759fn validate_bound_sql_ddl_version_contract(
760 bound: &BoundSqlDdlRequest,
761 accepted_before: &AcceptedSchemaSnapshot,
762) -> Result<(), SqlDdlBindError> {
763 let contract = bound.schema_version_contract();
764 let accepted_version = accepted_before.persisted_snapshot().version();
765 if let Some(expected) = contract.expected_schema_version()
766 && expected != accepted_version
767 {
768 return Err(SqlDdlBindError::StaleExpectedSchemaVersion {
769 expected: expected.get(),
770 accepted: accepted_version.get(),
771 });
772 }
773 if matches!(bound.statement(), BoundSqlDdlStatement::NoOp(_)) {
774 if contract.expected_schema_version().is_none() {
775 return Err(SqlDdlBindError::MissingExpectedSchemaVersion);
776 }
777 if let Some(requested) = contract.next_schema_version() {
778 return Err(SqlDdlBindError::EmptySchemaVersionBump {
779 requested: requested.get(),
780 });
781 }
782
783 return Ok(());
784 }
785 if contract.expected_schema_version().is_none() {
786 return Err(SqlDdlBindError::MissingExpectedSchemaVersion);
787 }
788 if contract.next_schema_version().is_none() {
789 return Err(SqlDdlBindError::MissingNextSchemaVersion);
790 }
791
792 Ok(())
793}
794
795fn ddl_preparation_report(bound: &BoundSqlDdlRequest) -> SqlDdlPreparationReport {
796 match bound.statement() {
797 BoundSqlDdlStatement::AddColumn(add) => SqlDdlPreparationReport {
798 mutation_kind: if add.field().default().is_none() {
799 SqlDdlMutationKind::AddNullableField
800 } else {
801 SqlDdlMutationKind::AddDefaultedField
802 },
803 target_index: add.field().name().to_string(),
804 target_store: add.entity_name().to_string(),
805 field_path: vec![add.field().name().to_string()],
806 execution_status: SqlDdlExecutionStatus::PreparedOnly,
807 rows_scanned: 0,
808 index_keys_written: 0,
809 },
810 BoundSqlDdlStatement::AlterColumnDefault(alter) => SqlDdlPreparationReport {
811 mutation_kind: alter.mutation_kind(),
812 target_index: alter.field_name().to_string(),
813 target_store: alter.entity_name().to_string(),
814 field_path: vec![alter.field_name().to_string()],
815 execution_status: SqlDdlExecutionStatus::PreparedOnly,
816 rows_scanned: 0,
817 index_keys_written: 0,
818 },
819 BoundSqlDdlStatement::AlterColumnNullability(alter) => SqlDdlPreparationReport {
820 mutation_kind: alter.mutation_kind(),
821 target_index: alter.field_name().to_string(),
822 target_store: alter.entity_name().to_string(),
823 field_path: vec![alter.field_name().to_string()],
824 execution_status: SqlDdlExecutionStatus::PreparedOnly,
825 rows_scanned: 0,
826 index_keys_written: 0,
827 },
828 BoundSqlDdlStatement::DropColumn(drop) => SqlDdlPreparationReport {
829 mutation_kind: SqlDdlMutationKind::DropField,
830 target_index: drop.field_name().to_string(),
831 target_store: drop.entity_name().to_string(),
832 field_path: vec![drop.field_name().to_string()],
833 execution_status: SqlDdlExecutionStatus::PreparedOnly,
834 rows_scanned: 0,
835 index_keys_written: 0,
836 },
837 BoundSqlDdlStatement::RenameColumn(rename) => SqlDdlPreparationReport {
838 mutation_kind: SqlDdlMutationKind::RenameField,
839 target_index: rename.new_name().to_string(),
840 target_store: rename.entity_name().to_string(),
841 field_path: vec![rename.old_name().to_string(), rename.new_name().to_string()],
842 execution_status: SqlDdlExecutionStatus::PreparedOnly,
843 rows_scanned: 0,
844 index_keys_written: 0,
845 },
846 BoundSqlDdlStatement::CreateIndex(create) => {
847 let target = create.candidate_index();
848
849 SqlDdlPreparationReport {
850 mutation_kind: if target.key().is_field_path_only() {
851 SqlDdlMutationKind::AddFieldPathIndex
852 } else {
853 SqlDdlMutationKind::AddExpressionIndex
854 },
855 target_index: target.name().to_string(),
856 target_store: target.store().to_string(),
857 field_path: ddl_key_item_report(create.key_items()),
858 execution_status: SqlDdlExecutionStatus::PreparedOnly,
859 rows_scanned: 0,
860 index_keys_written: 0,
861 }
862 }
863 BoundSqlDdlStatement::DropIndex(drop) => SqlDdlPreparationReport {
864 mutation_kind: SqlDdlMutationKind::DropSecondaryIndex,
865 target_index: drop.index_name().to_string(),
866 target_store: drop.dropped_index().store().to_string(),
867 field_path: drop.field_path().to_vec(),
868 execution_status: SqlDdlExecutionStatus::PreparedOnly,
869 rows_scanned: 0,
870 index_keys_written: 0,
871 },
872 BoundSqlDdlStatement::NoOp(no_op) => SqlDdlPreparationReport {
873 mutation_kind: no_op.mutation_kind(),
874 target_index: no_op.index_name().to_string(),
875 target_store: no_op.target_store().to_string(),
876 field_path: no_op.field_path().to_vec(),
877 execution_status: SqlDdlExecutionStatus::PreparedOnly,
878 rows_scanned: 0,
879 index_keys_written: 0,
880 },
881 }
882}