Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions cot-macros/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ struct ModelBuilder {
name: Ident,
vis: syn::Visibility,
table_name: String,
model_type: ModelType,
pk_field: Field,
fields_struct_name: Ident,
fields_as_columns: Vec<TokenStream>,
Expand Down Expand Up @@ -109,6 +110,7 @@ impl ModelBuilder {
name: model.name.clone(),
vis: model.vis,
table_name,
model_type: model.model_type,
pk_field: model.pk_field.clone(),
fields_struct_name: format_ident!("{}Fields", model.name),
fields_as_columns: Vec::with_capacity(field_count),
Expand Down Expand Up @@ -166,6 +168,11 @@ impl ModelBuilder {
let name = &self.name;
let app_name = &self.app_name;
let table_name = &self.table_name;
let model_type = match self.model_type {
ModelType::Application => quote!(#orm_ident::ModelType::Application),
ModelType::Migration => quote!(#orm_ident::ModelType::Migration),
ModelType::Internal => quote!(#orm_ident::ModelType::Internal),
};
let fields_struct_name = &self.fields_struct_name;
let fields_as_columns = &self.fields_as_columns;
let pk_field_name = &self.pk_field.name;
Expand All @@ -185,6 +192,7 @@ impl ModelBuilder {
const COLUMNS: &'static [#orm_ident::Column] = &[
#(#fields_as_columns,)*
];
const MODEL_TYPE: #orm_ident::ModelType = #model_type;
const APP_NAME: &'static str = #app_name;
const TABLE_NAME: #orm_ident::Identifier = #orm_ident::Identifier::new(#table_name);
const PRIMARY_KEY_NAME: #orm_ident::Identifier = #orm_ident::Identifier::new(#pk_column_name);
Expand Down
49 changes: 49 additions & 0 deletions cot/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,15 @@ impl DatabaseError {
/// An alias for [`Result`] that uses [`DatabaseError`] as the error type.
pub type Result<T> = std::result::Result<T, DatabaseError>;

#[expect(missing_docs)]
#[non_exhaustive]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum ModelType {
Application,
Migration,
Internal,
}

/// A model trait for database models.
///
/// This trait is used to define a model that can be stored in a database.
Expand Down Expand Up @@ -585,6 +594,9 @@ pub trait Model: Sized + Send + 'static {
/// The columns of the model.
const COLUMNS: &'static [Column];

#[expect(missing_docs)]
const MODEL_TYPE: ModelType = ModelType::Application;

/// Creates a model instance from a database row.
///
/// # Errors
Expand Down Expand Up @@ -1184,6 +1196,12 @@ pub trait SqlxValueRef<'r>: Sized {
}
}

#[derive(Debug, Clone, Copy)]
enum DatabaseContext {
Default,
InMigration,
}

/// A database connection structure that holds the connection to the database.
///
/// It is used to execute queries and interact with the database. The connection
Expand All @@ -1192,6 +1210,7 @@ pub trait SqlxValueRef<'r>: Sized {
#[derive(Debug, Clone)]
pub struct Database {
inner: Arc<DatabaseImpl>,
context: DatabaseContext,
}

#[derive(Debug)]
Expand Down Expand Up @@ -1254,6 +1273,7 @@ impl Database {
let inner = DatabaseSqlite::new(&url).await?;
return Ok(Self {
inner: Arc::new(DatabaseImpl::Sqlite(inner)),
context: DatabaseContext::Default,
});
}

Expand All @@ -1262,6 +1282,7 @@ impl Database {
let inner = DatabasePostgres::new(&url).await?;
return Ok(Self {
inner: Arc::new(DatabaseImpl::Postgres(inner)),
context: DatabaseContext::Default,
});
}

Expand All @@ -1270,12 +1291,32 @@ impl Database {
let inner = DatabaseMySql::new(&url).await?;
return Ok(Self {
inner: Arc::new(DatabaseImpl::MySql(inner)),
context: DatabaseContext::Default,
});
}

panic!("Unsupported database URL: {url}");
}

fn for_migration(&self) -> Self {
Self {
inner: Arc::clone(&self.inner),
context: DatabaseContext::InMigration,
}
}

fn ensure_model_allowed<T: Model>(&self) -> Result<()> {
match (self.context, T::MODEL_TYPE) {
Comment thread
Guflly marked this conversation as resolved.
(DatabaseContext::InMigration, ModelType::Application) => Err(
DatabaseError::MigrationError(migrations::MigrationEngineError::Custom(format!(
"application model `{}` cannot be used in migrations; use a migration model",
std::any::type_name::<T>()
))),
),
_ => Ok(()),
}
}

/// Closes the database connection.
///
/// This method should be called when the database connection is no longer
Expand Down Expand Up @@ -1345,6 +1386,7 @@ impl Database {
}

async fn insert_or_update_impl<T: Model>(&self, data: &mut T, update: bool) -> Result<()> {
self.ensure_model_allowed::<T>()?;
let column_identifiers = T::COLUMNS
.iter()
.map(|column| Identifier::from(column.name.as_str()));
Expand Down Expand Up @@ -1451,6 +1493,7 @@ impl Database {
}

async fn update_impl<T: Model>(&self, data: &mut T) -> Result<()> {
self.ensure_model_allowed::<T>()?;
let column_identifiers = T::COLUMNS
.iter()
.map(|column| Identifier::from(column.name.as_str()));
Expand Down Expand Up @@ -1531,6 +1574,7 @@ impl Database {
}

async fn bulk_insert_impl<T: Model>(&self, data: &mut [T], update: bool) -> Result<()> {
self.ensure_model_allowed::<T>()?;
// TODO: add transactions when implemented

if data.is_empty() {
Expand Down Expand Up @@ -1741,6 +1785,7 @@ impl Database {
///
/// Can return an error if the database connection is lost.
pub async fn query<T: Model>(&self, query: &Query<T>) -> Result<Vec<T>> {
self.ensure_model_allowed::<T>()?;
let columns_to_get: Vec<_> = T::COLUMNS.iter().map(|column| column.name).collect();
let mut select = sea_query::Query::select();
select.columns(columns_to_get).from(T::TABLE_NAME);
Expand All @@ -1767,6 +1812,7 @@ impl Database {
///
/// Can return an error if the database connection is lost.
pub async fn get<T: Model>(&self, query: &Query<T>) -> Result<Option<T>> {
self.ensure_model_allowed::<T>()?;
let columns_to_get: Vec<_> = T::COLUMNS.iter().map(|column| column.name).collect();
let mut select = sea_query::Query::select();
select.columns(columns_to_get).from(T::TABLE_NAME);
Expand Down Expand Up @@ -1794,6 +1840,7 @@ impl Database {
///
/// Can return an error if the database connection is lost.
pub async fn exists<T: Model>(&self, query: &Query<T>) -> Result<bool> {
self.ensure_model_allowed::<T>()?;
let mut select = sea_query::Query::select();
select.expr(sea_query::Expr::value(1)).from(T::TABLE_NAME);
query.add_filter_to_statement(&mut select);
Expand All @@ -1816,6 +1863,7 @@ impl Database {
///
/// Can return an error if the database connection is lost.
pub async fn delete<T: Model>(&self, query: &Query<T>) -> Result<StatementResult> {
self.ensure_model_allowed::<T>()?;
let mut delete = sea_query::Query::delete();
delete.from_table(T::TABLE_NAME);
query.add_filter_to_statement(&mut delete);
Expand Down Expand Up @@ -2017,6 +2065,7 @@ impl Database {
query: &str,
values: &[&dyn ToDbValue],
) -> Result<Vec<T>> {
self.ensure_model_allowed::<T>()?;
let values = values
.iter()
.map(ToDbValue::to_db_value)
Expand Down
64 changes: 61 additions & 3 deletions cot/src/db/migrations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -532,7 +532,8 @@ impl Operation {
forwards,
backwards: _,
} => {
let context = MigrationContext::new(database);
let database = database.for_migration();
let context = MigrationContext::new(&database);
forwards(context).await?;
}
}
Expand Down Expand Up @@ -617,7 +618,8 @@ impl Operation {
backwards,
} => {
if let Some(backwards) = backwards {
let context = MigrationContext::new(database);
let database = database.for_migration();
let context = MigrationContext::new(&database);
backwards(context).await?;
} else {
return Err(crate::db::DatabaseError::MigrationError(
Expand Down Expand Up @@ -2025,7 +2027,19 @@ mod tests {
use cot::test::TestDatabase;

use super::*;
use crate::db::{ColumnType, DatabaseField, Identifier};
use crate::db::{ColumnType, DatabaseError, DatabaseField, Identifier, Model};

#[model]
struct ApplicationModel {
#[model(primary_key)]
id: i32,
}

#[model(model_type = "migration")]
struct _MigrationModel {
#[model(primary_key)]
id: i32,
}

struct TestMigration;

Expand Down Expand Up @@ -2168,6 +2182,50 @@ mod tests {
assert!(result.is_ok());
}

#[cot::test]
#[cfg_attr(
miri,
ignore = "unsupported operation: can't call foreign function `sqlite3_open_v2`"
)]
async fn operation_custom_model_access() {
let test_db = TestDatabase::new_sqlite().await.unwrap();

#[migration_op]
async fn application_model(ctx: MigrationContext<'_>) -> Result<()> {
ApplicationModel::objects().all(ctx.db).await?;
Ok(())
}

let result = Operation::custom(application_model)
.build()
.forwards(&test_db.database())
.await;
assert!(matches!(
result,
Err(DatabaseError::MigrationError(MigrationEngineError::Custom(
_
)))
));

test_db
.database()
.raw("CREATE TABLE cot__migration_model (id INTEGER PRIMARY KEY)")
.await
.unwrap();

#[migration_op]
async fn migration_model(ctx: MigrationContext<'_>) -> Result<()> {
_MigrationModel::objects().all(ctx.db).await?;
Ok(())
}

Operation::custom(migration_model)
.build()
.forwards(&test_db.database())
.await
.unwrap();
}

#[cot::test]
#[cfg_attr(
miri,
Expand Down
Loading