mod admission;
pub(in crate::db) use admission::BoundSqlDdlSchemaVersionContract;
use admission::{
bind_sql_ddl_schema_version_contract, ddl_version_contract,
validate_bound_sql_ddl_version_contract,
};
mod field;
pub(in crate::db) use field::{
BoundSqlAddColumnRequest, BoundSqlAlterColumnDefaultRequest,
BoundSqlAlterColumnNullabilityRequest, BoundSqlDropColumnRequest, BoundSqlRenameColumnRequest,
};
use field::{
bind_alter_table_add_column_statement, bind_alter_table_alter_column_statement,
bind_alter_table_drop_column_statement, bind_alter_table_rename_column_statement,
};
mod index;
pub(in crate::db) use index::{BoundSqlCreateIndexRequest, BoundSqlDropIndexRequest};
use index::{bind_create_index_statement, bind_drop_index_statement};
mod report;
use report::ddl_preparation_report;
pub use report::{SqlDdlExecutionStatus, SqlDdlMutationKind, SqlDdlPreparationReport};
use crate::db::{
schema::{
AcceptedSchemaSnapshot, SchemaDdlAcceptedSnapshotDerivation,
SchemaDdlMutationAdmissionError, SchemaInfo,
derive_sql_ddl_expression_index_accepted_after,
derive_sql_ddl_field_addition_accepted_after, derive_sql_ddl_field_default_accepted_after,
derive_sql_ddl_field_drop_accepted_after, derive_sql_ddl_field_nullability_accepted_after,
derive_sql_ddl_field_path_index_accepted_after, derive_sql_ddl_field_rename_accepted_after,
derive_sql_ddl_secondary_index_drop_accepted_after,
},
sql::parser::{SqlDdlStatement, SqlStatement},
};
use thiserror::Error as ThisError;
#[cfg(test)]
use crate::db::schema::{
SchemaDdlMutationAdmission, admit_sql_ddl_expression_index_candidate,
admit_sql_ddl_field_addition_candidate, admit_sql_ddl_field_default_candidate,
admit_sql_ddl_field_drop_candidate, admit_sql_ddl_field_nullability_candidate,
admit_sql_ddl_field_path_index_candidate, admit_sql_ddl_field_rename_candidate,
admit_sql_ddl_secondary_index_drop_candidate,
};
#[derive(Clone, Debug, Eq, PartialEq)]
pub(in crate::db) struct PreparedSqlDdlCommand {
bound: BoundSqlDdlRequest,
derivation: Option<SchemaDdlAcceptedSnapshotDerivation>,
report: SqlDdlPreparationReport,
}
impl PreparedSqlDdlCommand {
#[must_use]
pub(in crate::db) const fn bound(&self) -> &BoundSqlDdlRequest {
&self.bound
}
#[must_use]
pub(in crate::db) const fn derivation(&self) -> Option<&SchemaDdlAcceptedSnapshotDerivation> {
self.derivation.as_ref()
}
#[must_use]
pub(in crate::db) const fn report(&self) -> &SqlDdlPreparationReport {
&self.report
}
#[must_use]
pub(in crate::db) const fn mutates_schema(&self) -> bool {
self.derivation.is_some()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(in crate::db) struct BoundSqlDdlRequest {
statement: BoundSqlDdlStatement,
schema_version_contract: BoundSqlDdlSchemaVersionContract,
}
impl BoundSqlDdlRequest {
#[must_use]
pub(in crate::db) const fn statement(&self) -> &BoundSqlDdlStatement {
&self.statement
}
#[must_use]
pub(in crate::db) const fn schema_version_contract(&self) -> BoundSqlDdlSchemaVersionContract {
self.schema_version_contract
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(in crate::db) enum BoundSqlDdlStatement {
AddColumn(BoundSqlAddColumnRequest),
AlterColumnDefault(BoundSqlAlterColumnDefaultRequest),
AlterColumnNullability(BoundSqlAlterColumnNullabilityRequest),
DropColumn(BoundSqlDropColumnRequest),
RenameColumn(BoundSqlRenameColumnRequest),
CreateIndex(BoundSqlCreateIndexRequest),
DropIndex(BoundSqlDropIndexRequest),
NoOp(BoundSqlDdlNoOpRequest),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(in crate::db) struct BoundSqlDdlNoOpRequest {
mutation_kind: SqlDdlMutationKind,
index_name: String,
entity_name: String,
target_store: String,
field_path: Vec<String>,
}
impl BoundSqlDdlNoOpRequest {
#[must_use]
pub(in crate::db) const fn mutation_kind(&self) -> SqlDdlMutationKind {
self.mutation_kind
}
#[must_use]
pub(in crate::db) const fn index_name(&self) -> &str {
self.index_name.as_str()
}
#[must_use]
#[cfg(test)]
pub(in crate::db) const fn entity_name(&self) -> &str {
self.entity_name.as_str()
}
#[must_use]
pub(in crate::db) const fn target_store(&self) -> &str {
self.target_store.as_str()
}
#[must_use]
pub(in crate::db) const fn field_path(&self) -> &[String] {
self.field_path.as_slice()
}
}
#[derive(Debug, Eq, PartialEq, ThisError)]
pub(in crate::db) enum SqlDdlBindError {
#[error("SQL DDL binder requires a DDL statement")]
NotDdl,
#[error("accepted schema does not expose an entity name")]
MissingEntityName,
#[error("SQL entity '{sql_entity}' does not match accepted entity '{expected_entity}'")]
EntityMismatch {
sql_entity: String,
expected_entity: String,
},
#[error("unknown field path '{field_path}' for accepted entity '{entity_name}'")]
UnknownFieldPath {
entity_name: String,
field_path: String,
},
#[error("field path '{field_path}' is not indexable")]
FieldPathNotIndexable { field_path: String },
#[error("field path '{field_path}' depends on generated-only metadata")]
FieldPathNotAcceptedCatalogBacked { field_path: String },
#[error("invalid filtered index predicate: {detail}")]
InvalidFilteredIndexPredicate { detail: String },
#[error("index name '{index_name}' already exists in the accepted schema")]
DuplicateIndexName { index_name: String },
#[error("accepted schema already has index '{existing_index}' for field path '{field_path}'")]
DuplicateFieldPathIndex {
field_path: String,
existing_index: String,
},
#[error("unknown index '{index_name}' for accepted entity '{entity_name}'")]
UnknownIndex {
entity_name: String,
index_name: String,
},
#[error(
"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"
)]
GeneratedIndexDropRejected { index_name: String },
#[error(
"index '{index_name}' is not a supported DDL-droppable secondary index; SQL DDL can currently drop only indexes created through SQL DDL"
)]
UnsupportedDropIndex { index_name: String },
#[error(
"SQL DDL ALTER TABLE ADD COLUMN DEFAULT value is not encodable for accepted entity '{entity_name}' column '{column_name}': {detail}"
)]
InvalidAlterTableAddColumnDefault {
entity_name: String,
column_name: String,
detail: String,
},
#[error(
"SQL DDL ALTER TABLE ADD COLUMN NOT NULL is not executable yet for accepted entity '{entity_name}' column '{column_name}'"
)]
UnsupportedAlterTableAddColumnNotNull {
entity_name: String,
column_name: String,
},
#[error("field '{column_name}' already exists in accepted entity '{entity_name}'")]
DuplicateColumn {
entity_name: String,
column_name: String,
},
#[error(
"SQL DDL ALTER TABLE ADD COLUMN type '{column_type}' is not supported yet for accepted entity '{entity_name}' column '{column_name}'"
)]
UnsupportedAlterTableAddColumnType {
entity_name: String,
column_name: String,
column_type: String,
},
#[error("unknown column '{column_name}' for accepted entity '{entity_name}'")]
UnknownColumn {
entity_name: String,
column_name: String,
},
#[error(
"SQL DDL ALTER TABLE ALTER COLUMN SET DEFAULT value is not encodable for accepted entity '{entity_name}' column '{column_name}': {detail}"
)]
InvalidAlterTableAlterColumnDefault {
entity_name: String,
column_name: String,
detail: String,
},
#[error(
"SQL DDL ALTER TABLE ALTER COLUMN DROP DEFAULT is not executable yet for required accepted entity '{entity_name}' column '{column_name}'"
)]
UnsupportedAlterTableDropDefaultRequired {
entity_name: String,
column_name: String,
},
#[error(
"SQL DDL ALTER TABLE ALTER COLUMN DEFAULT cannot change generated accepted field '{column_name}' on entity '{entity_name}'; change the Rust schema default instead"
)]
GeneratedFieldDefaultChangeRejected {
entity_name: String,
column_name: String,
},
#[error(
"SQL DDL ALTER TABLE ALTER COLUMN NULLABILITY cannot change generated accepted field '{column_name}' on entity '{entity_name}'; change the Rust schema nullability instead"
)]
GeneratedFieldNullabilityChangeRejected {
entity_name: String,
column_name: String,
},
#[error(
"SQL DDL ALTER TABLE DROP COLUMN cannot drop primary-key field '{column_name}' on entity '{entity_name}'"
)]
PrimaryKeyFieldDropRejected {
entity_name: String,
column_name: String,
},
#[error(
"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"
)]
GeneratedFieldDropRejected {
entity_name: String,
column_name: String,
},
#[error(
"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"
)]
IndexedFieldDropRejected {
entity_name: String,
column_name: String,
index_name: String,
},
#[error(
"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"
)]
GeneratedFieldRenameRejected {
entity_name: String,
column_name: String,
},
#[error("SQL DDL {clause} must be a positive schema version")]
NonPositiveSchemaVersion { clause: &'static str },
#[error("mutating SQL DDL requires EXPECT SCHEMA VERSION")]
MissingExpectedSchemaVersion,
#[error("mutating SQL DDL requires SET SCHEMA VERSION")]
MissingNextSchemaVersion,
#[error(
"SQL DDL expected accepted schema version {expected}, but accepted schema version is {accepted}"
)]
StaleExpectedSchemaVersion { expected: u32, accepted: u32 },
#[error("SQL DDL no-op cannot SET SCHEMA VERSION {requested}")]
EmptySchemaVersionBump { requested: u32 },
}
#[derive(Debug, Eq, PartialEq, ThisError)]
pub(in crate::db) enum SqlDdlLoweringError {
#[error("SQL DDL lowering requires a supported DDL statement")]
UnsupportedStatement,
#[error("schema mutation admission rejected DDL candidate: {0}")]
MutationAdmission(SchemaDdlMutationAdmissionError),
}
#[derive(Debug, Eq, PartialEq, ThisError)]
pub(in crate::db) enum SqlDdlPrepareError {
#[error("{0}")]
Bind(#[from] SqlDdlBindError),
#[error("{0}")]
Lowering(#[from] SqlDdlLoweringError),
}
pub(in crate::db) fn prepare_sql_ddl_statement(
statement: &SqlStatement,
accepted_before: &AcceptedSchemaSnapshot,
schema: &SchemaInfo,
index_store_path: &'static str,
) -> Result<PreparedSqlDdlCommand, SqlDdlPrepareError> {
let bound = bind_sql_ddl_statement(statement, accepted_before, schema, index_store_path)?;
validate_bound_sql_ddl_version_contract(&bound, accepted_before)?;
let derivation = if matches!(bound.statement(), BoundSqlDdlStatement::NoOp(_)) {
None
} else {
Some(derive_bound_sql_ddl_accepted_after(
accepted_before,
&bound,
)?)
};
let report = ddl_preparation_report(&bound);
Ok(PreparedSqlDdlCommand {
bound,
derivation,
report,
})
}
pub(in crate::db) fn bind_sql_ddl_statement(
statement: &SqlStatement,
accepted_before: &AcceptedSchemaSnapshot,
schema: &SchemaInfo,
index_store_path: &'static str,
) -> Result<BoundSqlDdlRequest, SqlDdlBindError> {
let SqlStatement::Ddl(ddl) = statement else {
return Err(SqlDdlBindError::NotDdl);
};
let mut bound = match ddl {
SqlDdlStatement::CreateIndex(statement) => {
bind_create_index_statement(statement, accepted_before, schema, index_store_path)
}
SqlDdlStatement::DropIndex(statement) => {
bind_drop_index_statement(statement, accepted_before, schema)
}
SqlDdlStatement::AlterTableAddColumn(statement) => {
bind_alter_table_add_column_statement(statement, accepted_before, schema)
}
SqlDdlStatement::AlterTableAlterColumn(statement) => {
bind_alter_table_alter_column_statement(statement, accepted_before, schema)
}
SqlDdlStatement::AlterTableDropColumn(statement) => {
bind_alter_table_drop_column_statement(statement, accepted_before, schema)
}
SqlDdlStatement::AlterTableRenameColumn(statement) => {
bind_alter_table_rename_column_statement(statement, accepted_before, schema)
}
}?;
bound.schema_version_contract =
bind_sql_ddl_schema_version_contract(ddl_version_contract(ddl))?;
Ok(bound)
}
#[cfg(test)]
pub(in crate::db) fn lower_bound_sql_ddl_to_schema_mutation_admission(
request: &BoundSqlDdlRequest,
) -> Result<SchemaDdlMutationAdmission, SqlDdlLoweringError> {
match request.statement() {
BoundSqlDdlStatement::AddColumn(add) => {
Ok(admit_sql_ddl_field_addition_candidate(add.field()))
}
BoundSqlDdlStatement::AlterColumnDefault(alter) => {
Ok(admit_sql_ddl_field_default_candidate(alter.field()))
}
BoundSqlDdlStatement::AlterColumnNullability(alter) => {
Ok(admit_sql_ddl_field_nullability_candidate(alter.field()))
}
BoundSqlDdlStatement::DropColumn(drop) => {
Ok(admit_sql_ddl_field_drop_candidate(drop.field()))
}
BoundSqlDdlStatement::RenameColumn(rename) => Ok(admit_sql_ddl_field_rename_candidate(
rename.field(),
rename.new_name(),
)),
BoundSqlDdlStatement::CreateIndex(create) => {
if create.candidate_index().key().is_field_path_only() {
admit_sql_ddl_field_path_index_candidate(create.candidate_index())
} else {
admit_sql_ddl_expression_index_candidate(create.candidate_index())
}
}
BoundSqlDdlStatement::DropIndex(drop) => {
admit_sql_ddl_secondary_index_drop_candidate(drop.dropped_index())
}
BoundSqlDdlStatement::NoOp(_) => return Err(SqlDdlLoweringError::UnsupportedStatement),
}
.map_err(SqlDdlLoweringError::MutationAdmission)
}
pub(in crate::db) fn derive_bound_sql_ddl_accepted_after(
accepted_before: &AcceptedSchemaSnapshot,
request: &BoundSqlDdlRequest,
) -> Result<SchemaDdlAcceptedSnapshotDerivation, SqlDdlLoweringError> {
let next_schema_version = request
.schema_version_contract()
.next_schema_version()
.ok_or(SqlDdlLoweringError::UnsupportedStatement)?;
let derivation = match request.statement() {
BoundSqlDdlStatement::AddColumn(add) => {
derive_sql_ddl_field_addition_accepted_after(accepted_before, add.field().clone())
}
BoundSqlDdlStatement::AlterColumnDefault(alter) => {
derive_sql_ddl_field_default_accepted_after(
accepted_before,
alter.field_name(),
alter.default().clone(),
)
}
BoundSqlDdlStatement::AlterColumnNullability(alter) => {
derive_sql_ddl_field_nullability_accepted_after(
accepted_before,
alter.field_name(),
alter.nullable(),
)
}
BoundSqlDdlStatement::DropColumn(drop) => {
derive_sql_ddl_field_drop_accepted_after(accepted_before, drop.field_name())
}
BoundSqlDdlStatement::RenameColumn(rename) => derive_sql_ddl_field_rename_accepted_after(
accepted_before,
rename.old_name(),
rename.new_name(),
),
BoundSqlDdlStatement::CreateIndex(create) => {
if create.candidate_index().key().is_field_path_only() {
derive_sql_ddl_field_path_index_accepted_after(
accepted_before,
create.candidate_index().clone(),
)
} else {
derive_sql_ddl_expression_index_accepted_after(
accepted_before,
create.candidate_index().clone(),
)
}
}
BoundSqlDdlStatement::DropIndex(drop) => {
derive_sql_ddl_secondary_index_drop_accepted_after(
accepted_before,
drop.dropped_index(),
)
}
BoundSqlDdlStatement::NoOp(_) => return Err(SqlDdlLoweringError::UnsupportedStatement),
}
.map_err(SqlDdlLoweringError::MutationAdmission)?;
derivation
.with_declared_schema_version(accepted_before, next_schema_version)
.map_err(SqlDdlLoweringError::MutationAdmission)
}