You've already forked seamantic
Initial commit
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
# OS Files
|
||||||
|
*~
|
||||||
|
._*
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Rust
|
||||||
|
/target
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
hard_tabs = true
|
||||||
|
wrap_comments = true
|
||||||
Vendored
+9
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"vadimcn.vscode-lldb",
|
||||||
|
"barbosshack.crates-io",
|
||||||
|
"usernamehw.errorlens",
|
||||||
|
"tamasfe.even-better-toml",
|
||||||
|
"rust-lang.rust-analyzer",
|
||||||
|
]
|
||||||
|
}
|
||||||
Vendored
+33
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
// VSCode
|
||||||
|
"editor.detectIndentation": false,
|
||||||
|
"editor.insertSpaces": false,
|
||||||
|
"editor.tabSize": 4,
|
||||||
|
"files.exclude": {
|
||||||
|
"**/target": true,
|
||||||
|
"**/Cargo.lock": true,
|
||||||
|
},
|
||||||
|
"files.insertFinalNewline": true,
|
||||||
|
"files.trimFinalNewlines": true,
|
||||||
|
"files.trimTrailingWhitespace": true,
|
||||||
|
"files.watcherExclude": {
|
||||||
|
"**/.git/**": true,
|
||||||
|
"**/target/**": true,
|
||||||
|
},
|
||||||
|
// Extensions
|
||||||
|
"crates.listPreReleases": true,
|
||||||
|
"evenBetterToml.formatter.alignComments": true,
|
||||||
|
"evenBetterToml.formatter.alignEntries": false,
|
||||||
|
"evenBetterToml.formatter.allowedBlankLines": 1,
|
||||||
|
"evenBetterToml.formatter.arrayAutoExpand": true,
|
||||||
|
"evenBetterToml.formatter.arrayTrailingComma": true,
|
||||||
|
"evenBetterToml.formatter.columnWidth": 80,
|
||||||
|
"evenBetterToml.formatter.reorderKeys": true,
|
||||||
|
"evenBetterToml.formatter.trailingNewline": true,
|
||||||
|
"rust-analyzer.imports.granularity.enforce": true,
|
||||||
|
"rust-analyzer.imports.granularity.group": "module",
|
||||||
|
"rust-analyzer.imports.group.enable": true,
|
||||||
|
"rust-analyzer.imports.merge.glob": false,
|
||||||
|
"rust-analyzer.imports.preferNoStd": true,
|
||||||
|
"rust-analyzer.showUnlinkedFileNotification": false,
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
How to Contribute
|
||||||
|
=================
|
||||||
|
|
||||||
|
We'd love to accept your patches and contributions to this project.
|
||||||
|
We just need you to follow the Contributor License Agreement outlined
|
||||||
|
in the latest v0.0.x of https://github.com/Skrunix/license
|
||||||
Generated
+1983
File diff suppressed because it is too large
Load Diff
+67
@@ -0,0 +1,67 @@
|
|||||||
|
[package]
|
||||||
|
name = "seamantic"
|
||||||
|
version = "0.0.1"
|
||||||
|
|
||||||
|
categories = []
|
||||||
|
description = "A library to enhance SeaORM"
|
||||||
|
repository = "https://github.com/QuantumShade/seamantic"
|
||||||
|
|
||||||
|
authors = []
|
||||||
|
edition = "2024"
|
||||||
|
license-file = "LICENSE.md"
|
||||||
|
resolver = "2"
|
||||||
|
rust-version = "1.85.0"
|
||||||
|
|
||||||
|
[lints.rust]
|
||||||
|
arithmetic_overflow = "forbid"
|
||||||
|
missing_docs = "forbid"
|
||||||
|
unsafe_code = "forbid"
|
||||||
|
|
||||||
|
[lints.clippy]
|
||||||
|
arithmetic_side_effects = "forbid"
|
||||||
|
as_conversions = "forbid"
|
||||||
|
checked_conversions = "forbid"
|
||||||
|
default_union_representation = "forbid"
|
||||||
|
expect_used = "forbid"
|
||||||
|
indexing_slicing = "forbid"
|
||||||
|
integer_division = "forbid"
|
||||||
|
integer_division_remainder_used = "forbid"
|
||||||
|
transmute_undefined_repr = "forbid"
|
||||||
|
unchecked_duration_subtraction = "forbid"
|
||||||
|
unwrap_used = "forbid"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
codegen-units = 1
|
||||||
|
lto = "fat"
|
||||||
|
opt-level = 3
|
||||||
|
overflow-checks = true
|
||||||
|
strip = "debuginfo"
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "migrations"
|
||||||
|
path = "examples/migrations/main.rs"
|
||||||
|
required-features = ["sqlite"]
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
serde = ["dep:serde"]
|
||||||
|
sqlite = ["sea-orm-migration/sqlx-sqlite"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
sea-orm = { version = "^1", default-features = false }
|
||||||
|
sea-orm-migration = { version = "^1", default-features = false }
|
||||||
|
|
||||||
|
serde = { version = "^1", default-features = false, features = [
|
||||||
|
"derive",
|
||||||
|
"std",
|
||||||
|
], optional = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
sea-orm-migration = { version = "^1", default-features = false, features = [
|
||||||
|
"runtime-tokio-rustls",
|
||||||
|
] }
|
||||||
|
serde_test = { version = "^1", default-features = false }
|
||||||
|
tokio = { version = "^1", default-features = false, features = [
|
||||||
|
"rt",
|
||||||
|
"macros",
|
||||||
|
] }
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
Skrunix Software License
|
||||||
|
========================
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software is
|
||||||
|
outlined in the latest v0.0.x of https://github.com/Skrunix/license
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS"
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Seamantic
|
||||||
|
|
||||||
|
A library to enhance SeaORM
|
||||||
|
|
||||||
|
## Various builds
|
||||||
|
|
||||||
|
- build: `cargo hack --feature-powerset build`
|
||||||
|
- clippy: `cargo hack --feature-powerset clippy -- -D warnings`
|
||||||
|
- test: `cargo hack --feature-powerset test`
|
||||||
|
- example: `cargo run --example=migrations --features=sqlite`
|
||||||
|
- fmt: `cargo fmt --check`
|
||||||
|
- docs: `RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features`
|
||||||
|
- publish: `cargo publish --dry-run`
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
use seamantic::schema::{sqlite_case_insensitive_string, sqlite_rowid_alias};
|
||||||
|
|
||||||
|
use sea_orm_migration::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Iden)]
|
||||||
|
pub(super) enum Objects {
|
||||||
|
Table,
|
||||||
|
Id,
|
||||||
|
Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(DeriveMigrationName)]
|
||||||
|
pub(super) struct Migration;
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl MigrationTrait for Migration {
|
||||||
|
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager
|
||||||
|
.create_table(
|
||||||
|
Table::create()
|
||||||
|
.table(Objects::Table)
|
||||||
|
.col(sqlite_rowid_alias(Objects::Id))
|
||||||
|
.col(sqlite_case_insensitive_string(Objects::Name))
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager
|
||||||
|
.drop_table(Table::drop().table(Objects::Table).to_owned())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
//! This example shows how to use `seamantic::migrations!` and how to
|
||||||
|
//! make a migration with the sqlite schema helpers
|
||||||
|
|
||||||
|
use sea_orm::{ConnectOptions, Database};
|
||||||
|
use sea_orm_migration::MigratorTrait;
|
||||||
|
|
||||||
|
seamantic::migrations! {
|
||||||
|
"seaql_migrations_test";
|
||||||
|
m_01,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main(flavor = "current_thread")]
|
||||||
|
async fn main() {
|
||||||
|
let options = ConnectOptions::new("sqlite:/tmp/db?mode=memory");
|
||||||
|
let database = Database::connect(options).await.expect("Database::connect");
|
||||||
|
Migrator::up(&database, None).await.expect("Migrator::up");
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
[toolchain]
|
||||||
|
channel = "stable"
|
||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
//! A library for enhacing SeaORM
|
||||||
|
|
||||||
|
pub use sea_orm;
|
||||||
|
pub use sea_orm_migration;
|
||||||
|
|
||||||
|
pub mod model;
|
||||||
|
pub mod schema;
|
||||||
|
|
||||||
|
/// A macro for defining a Migrator with a custom migration table while
|
||||||
|
/// avoiding typing repetition.
|
||||||
|
///
|
||||||
|
/// This macro will `mod` every migration given.
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// seamantic::migrations! {
|
||||||
|
/// "seaql_migrations_auth"; // table name
|
||||||
|
/// m_000001, //
|
||||||
|
/// m_000002, // migrations
|
||||||
|
/// m_000003, //
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! migrations {
|
||||||
|
($name:literal; $($migration:ident,)*) => {
|
||||||
|
use $crate::sea_orm::sea_query::IntoIden as _;
|
||||||
|
|
||||||
|
$(mod $migration;)*
|
||||||
|
|
||||||
|
/// Auto-generated migration manager
|
||||||
|
#[automatically_derived]
|
||||||
|
pub struct Migrator;
|
||||||
|
|
||||||
|
#[automatically_derived]
|
||||||
|
#[$crate::sea_orm_migration::async_trait::async_trait]
|
||||||
|
impl $crate::sea_orm_migration::MigratorTrait for Migrator {
|
||||||
|
fn migration_table_name() -> $crate::sea_orm::DynIden {
|
||||||
|
$crate::sea_orm_migration::prelude::Alias::new($name).into_iden()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn migrations() -> Vec<Box<dyn $crate::sea_orm_migration::MigrationTrait>> {
|
||||||
|
vec![$(Box::new($migration::Migration),)*]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
+168
@@ -0,0 +1,168 @@
|
|||||||
|
//! Typed IDs for use as primary keys
|
||||||
|
|
||||||
|
use core::cmp::Ordering;
|
||||||
|
use core::fmt;
|
||||||
|
use core::hash::{Hash, Hasher};
|
||||||
|
use core::marker::PhantomData;
|
||||||
|
|
||||||
|
use sea_orm::sea_query::{ArrayType, Nullable, ValueType, ValueTypeErr};
|
||||||
|
use sea_orm::{ColIdx, ColumnType, DbErr, QueryResult, TryFromU64, TryGetError, TryGetable, Value};
|
||||||
|
|
||||||
|
/// The internal representation used by the database
|
||||||
|
pub type SeaOrmRepr = i64;
|
||||||
|
|
||||||
|
/// An opaque type representing a row ID
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
#[cfg_attr(feature = "serde", serde(transparent))]
|
||||||
|
#[repr(transparent)]
|
||||||
|
pub struct Id<T> {
|
||||||
|
id: SeaOrmRepr,
|
||||||
|
#[cfg_attr(feature = "serde", serde(skip_serializing, default))]
|
||||||
|
_phantom: PhantomData<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manual implementation since `T: Clone` is not required
|
||||||
|
impl<T> Clone for Id<T> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
*self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manual implementation since `T: Copy` is not required
|
||||||
|
impl<T> Copy for Id<T> {}
|
||||||
|
|
||||||
|
// Manual implementation since `T: PartialEq` is not required
|
||||||
|
impl<T> PartialEq for Id<T> {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.id == other.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manual implementation since `T: Eq` is not required
|
||||||
|
impl<T> Eq for Id<T> {}
|
||||||
|
|
||||||
|
// Manual implementation since `T: PartialOrd` is not required
|
||||||
|
impl<T> PartialOrd for Id<T> {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manual implementation since `T: Ord` is not required
|
||||||
|
impl<T> Ord for Id<T> {
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
|
self.id.cmp(&other.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manual implementation since `T: Hash` is not required
|
||||||
|
impl<T> Hash for Id<T> {
|
||||||
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
|
self.id.hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Id<T> {
|
||||||
|
/// Allows the conversion from a raw value to [Id], though the use is discouraged.
|
||||||
|
pub fn from_raw(raw: SeaOrmRepr) -> Self {
|
||||||
|
Self {
|
||||||
|
id: raw,
|
||||||
|
_phantom: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Allows extracting the raw value, though the use is discouraged.
|
||||||
|
pub fn into_raw(self) -> SeaOrmRepr {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> fmt::Debug for Id<T> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("Id")
|
||||||
|
.field("T", &core::any::type_name::<T>())
|
||||||
|
.field("id", &self.id)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> ValueType for Id<T> {
|
||||||
|
fn try_from(v: Value) -> Result<Self, ValueTypeErr> {
|
||||||
|
<SeaOrmRepr as ValueType>::try_from(v).map(|id| Self {
|
||||||
|
id,
|
||||||
|
_phantom: PhantomData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn type_name() -> String {
|
||||||
|
format!("Id<{}>", &core::any::type_name::<T>())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn array_type() -> ArrayType {
|
||||||
|
SeaOrmRepr::array_type()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn column_type() -> ColumnType {
|
||||||
|
SeaOrmRepr::column_type()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<Id<T>> for Value {
|
||||||
|
fn from(value: Id<T>) -> Self {
|
||||||
|
value.id.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> TryGetable for Id<T> {
|
||||||
|
fn try_get_by<I: ColIdx>(res: &QueryResult, index: I) -> Result<Self, TryGetError> {
|
||||||
|
SeaOrmRepr::try_get_by(res, index).map(|id| Self {
|
||||||
|
id,
|
||||||
|
_phantom: PhantomData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> TryFromU64 for Id<T> {
|
||||||
|
fn try_from_u64(n: u64) -> Result<Self, DbErr> {
|
||||||
|
SeaOrmRepr::try_from_u64(n).map(|id| Self {
|
||||||
|
id,
|
||||||
|
_phantom: PhantomData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Nullable for Id<T> {
|
||||||
|
fn null() -> Value {
|
||||||
|
SeaOrmRepr::null()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use sea_orm::{
|
||||||
|
ActiveModelBehavior, DeriveEntityModel, DerivePrimaryKey, DeriveRelation, EnumIter,
|
||||||
|
PrimaryKeyTrait,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::Id;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, DeriveEntityModel)]
|
||||||
|
#[sea_orm(table_name = "ids")]
|
||||||
|
pub struct Model {
|
||||||
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
|
id: Id<Model>,
|
||||||
|
nullable: Option<Id<Model>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
|
|
||||||
|
#[derive(Debug, EnumIter, DeriveRelation)]
|
||||||
|
pub enum Relation {}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
fn test_serde() {
|
||||||
|
let id: Id<()> = Id::from_raw(1234);
|
||||||
|
serde_test::assert_tokens(&id, &[serde_test::Token::I64(1234)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
//! SeaORM column types for enforcing data consistency
|
||||||
|
|
||||||
|
pub mod id;
|
||||||
|
pub mod path;
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
//! [Path] and [PathBuf] utilities
|
||||||
|
|
||||||
|
use std::ffi::OsString;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
use std::os::unix::ffi::OsStringExt as _;
|
||||||
|
#[cfg(windows)]
|
||||||
|
compile_error!("PathBytes is not supported on Windows");
|
||||||
|
|
||||||
|
use sea_orm::sea_query::{ArrayType, Nullable, ValueType, ValueTypeErr};
|
||||||
|
use sea_orm::{ColIdx, ColumnType, QueryResult, TryGetError, TryGetable, Value};
|
||||||
|
|
||||||
|
type SeaOrmRepr = Vec<u8>;
|
||||||
|
|
||||||
|
/// Wrapper around [PathBuf] to store paths as bytes in SeaORM
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
#[cfg_attr(feature = "serde", serde(transparent))]
|
||||||
|
#[repr(transparent)]
|
||||||
|
pub struct PathBytes(pub PathBuf);
|
||||||
|
|
||||||
|
impl From<PathBuf> for PathBytes {
|
||||||
|
fn from(value: PathBuf) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PathBytes> for PathBuf {
|
||||||
|
fn from(value: PathBytes) -> Self {
|
||||||
|
value.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<Path> for PathBytes {
|
||||||
|
fn as_ref(&self) -> &Path {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ValueType for PathBytes {
|
||||||
|
fn try_from(v: Value) -> Result<Self, ValueTypeErr> {
|
||||||
|
<SeaOrmRepr as ValueType>::try_from(v)
|
||||||
|
.map(OsString::from_vec)
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.map(Self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn type_name() -> String {
|
||||||
|
core::any::type_name::<PathBuf>().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn array_type() -> ArrayType {
|
||||||
|
SeaOrmRepr::array_type()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn column_type() -> ColumnType {
|
||||||
|
SeaOrmRepr::column_type()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PathBytes> for Value {
|
||||||
|
fn from(value: PathBytes) -> Self {
|
||||||
|
value.0.into_os_string().into_vec().into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryGetable for PathBytes {
|
||||||
|
fn try_get_by<I: ColIdx>(res: &QueryResult, index: I) -> Result<Self, TryGetError> {
|
||||||
|
SeaOrmRepr::try_get_by(res, index)
|
||||||
|
.map(OsString::from_vec)
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.map(Self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Nullable for PathBytes {
|
||||||
|
fn null() -> Value {
|
||||||
|
SeaOrmRepr::null()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use sea_orm::{
|
||||||
|
ActiveModelBehavior, DeriveEntityModel, DerivePrimaryKey, DeriveRelation, EnumIter,
|
||||||
|
PrimaryKeyTrait,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::PathBytes;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, DeriveEntityModel)]
|
||||||
|
#[sea_orm(table_name = "media_library")]
|
||||||
|
pub struct Model {
|
||||||
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
|
id: u8,
|
||||||
|
path: PathBytes,
|
||||||
|
nullable: Option<PathBytes>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
|
|
||||||
|
#[derive(Debug, EnumIter, DeriveRelation)]
|
||||||
|
pub enum Relation {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
//! Helpers for defining SeaORM schemas
|
||||||
|
|
||||||
|
#[cfg(feature = "sqlite")]
|
||||||
|
mod sqlite;
|
||||||
|
#[cfg(feature = "sqlite")]
|
||||||
|
pub use sqlite::*;
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
use sea_orm_migration::schema::{integer_null, string};
|
||||||
|
use sea_orm_migration::sea_query::{ColumnDef, IntoIden};
|
||||||
|
|
||||||
|
/// Sets the column to be an alias for SQLite's rowid.
|
||||||
|
///
|
||||||
|
/// Required conditions:
|
||||||
|
/// - This column must *not* be auto_increment
|
||||||
|
/// - There cannot be other primary_key columns in the table
|
||||||
|
///
|
||||||
|
/// When using `DeriveEntityModel`, the type must be `i64` (or equivalent)
|
||||||
|
/// and should be tagged with:
|
||||||
|
///
|
||||||
|
/// `#[sea_orm(primary_key, auto_increment = false)]`
|
||||||
|
pub fn sqlite_rowid_alias<T: IntoIden>(name: T) -> ColumnDef {
|
||||||
|
integer_null(name).primary_key().take()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the column to be a case insensitive string
|
||||||
|
pub fn sqlite_case_insensitive_string<T: IntoIden>(name: T) -> ColumnDef {
|
||||||
|
string(name).extra("COLLATE NOCASE").take()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use sea_orm_migration::async_trait::async_trait;
|
||||||
|
use sea_orm_migration::sea_orm::QueryFilter;
|
||||||
|
use sea_orm_migration::sea_orm::{
|
||||||
|
ActiveModelBehavior, ActiveModelTrait, ActiveValue::NotSet, ActiveValue::Set, ColumnTrait,
|
||||||
|
ConnectOptions, Database, DatabaseConnection, DbErr, DeriveEntityModel,
|
||||||
|
DeriveMigrationName, DerivePrimaryKey, EntityTrait, EnumIter, PrimaryKeyTrait, RelationDef,
|
||||||
|
RelationTrait,
|
||||||
|
};
|
||||||
|
use sea_orm_migration::sea_query::{self, Iden, Table};
|
||||||
|
use sea_orm_migration::{MigrationTrait, MigratorTrait, SchemaManager};
|
||||||
|
|
||||||
|
use crate::model::id::{Id, SeaOrmRepr};
|
||||||
|
|
||||||
|
use super::{sqlite_case_insensitive_string, sqlite_rowid_alias};
|
||||||
|
|
||||||
|
async fn new_memory_db() -> DatabaseConnection {
|
||||||
|
let options = ConnectOptions::new("sqlite:/tmp/db?mode=memory");
|
||||||
|
Database::connect(options).await.expect("Database::connect")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_sqlite_rowid_alias() {
|
||||||
|
struct Migrator;
|
||||||
|
impl MigratorTrait for Migrator {
|
||||||
|
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
|
||||||
|
vec![Box::new(Migration)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Iden)]
|
||||||
|
pub(super) enum TestTable {
|
||||||
|
Table,
|
||||||
|
Id,
|
||||||
|
}
|
||||||
|
#[derive(DeriveMigrationName)]
|
||||||
|
struct Migration;
|
||||||
|
#[async_trait]
|
||||||
|
impl MigrationTrait for Migration {
|
||||||
|
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager
|
||||||
|
.create_table(
|
||||||
|
Table::create()
|
||||||
|
.table(TestTable::Table)
|
||||||
|
.col(sqlite_rowid_alias(TestTable::Id))
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, DeriveEntityModel)]
|
||||||
|
#[sea_orm(table_name = "test_table")]
|
||||||
|
pub struct Model {
|
||||||
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
|
id: Id<Model>,
|
||||||
|
}
|
||||||
|
#[derive(Debug, EnumIter)]
|
||||||
|
pub enum Relation {}
|
||||||
|
impl RelationTrait for Relation {
|
||||||
|
fn def(&self) -> RelationDef {
|
||||||
|
panic!("No RelationDef")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
|
|
||||||
|
let db = new_memory_db().await;
|
||||||
|
Migrator::up(&db, None).await.expect("up");
|
||||||
|
|
||||||
|
// Starts at 1 and increments
|
||||||
|
for i in 1..=3 {
|
||||||
|
let model = ActiveModel { id: NotSet };
|
||||||
|
let model = model.insert(&db).await.expect("insert");
|
||||||
|
assert_eq!(model.id.into_raw(), i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the top number and re-add
|
||||||
|
for i in 3..=3 {
|
||||||
|
let model = ActiveModel {
|
||||||
|
id: Set(Id::from_raw(i)),
|
||||||
|
};
|
||||||
|
model.delete(&db).await.expect("delete");
|
||||||
|
let model = ActiveModel { id: NotSet };
|
||||||
|
let model = model.insert(&db).await.expect("insert");
|
||||||
|
assert_eq!(model.id.into_raw(), i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jump to 100 and increment
|
||||||
|
for i in 100..=103 {
|
||||||
|
let model = ActiveModel {
|
||||||
|
id: Set(Id::from_raw(i)),
|
||||||
|
};
|
||||||
|
let model = model.insert(&db).await.expect("insert");
|
||||||
|
assert_eq!(model.id.into_raw(), i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue to increment
|
||||||
|
for i in 104..=104 {
|
||||||
|
let model = ActiveModel { id: NotSet };
|
||||||
|
let model = model.insert(&db).await.expect("insert");
|
||||||
|
assert_eq!(model.id.into_raw(), i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jump to SeaOrmRepr::MAX and increment
|
||||||
|
for i in SeaOrmRepr::MAX..=SeaOrmRepr::MAX {
|
||||||
|
let model = ActiveModel {
|
||||||
|
id: Set(Id::from_raw(i)),
|
||||||
|
};
|
||||||
|
let model = model.insert(&db).await.expect("insert");
|
||||||
|
assert_eq!(model.id.into_raw(), i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next ones are random around the center
|
||||||
|
for _ in 0..3 {
|
||||||
|
let model = ActiveModel { id: NotSet };
|
||||||
|
let model = model.insert(&db).await.expect("insert");
|
||||||
|
assert!(model.id.into_raw() > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zero ID is valid
|
||||||
|
for i in 0..=0 {
|
||||||
|
let model = ActiveModel {
|
||||||
|
id: Set(Id::from_raw(i)),
|
||||||
|
};
|
||||||
|
let model = model.insert(&db).await.expect("insert");
|
||||||
|
assert_eq!(model.id.into_raw(), i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Negative ID is valid
|
||||||
|
for i in SeaOrmRepr::MIN..=(SeaOrmRepr::MIN + 3) {
|
||||||
|
let model = ActiveModel {
|
||||||
|
id: Set(Id::from_raw(i)),
|
||||||
|
};
|
||||||
|
let model = model.insert(&db).await.expect("insert");
|
||||||
|
assert_eq!(model.id.into_raw(), i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_sqlite_case_insensitive_string() {
|
||||||
|
struct Migrator;
|
||||||
|
impl MigratorTrait for Migrator {
|
||||||
|
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
|
||||||
|
vec![Box::new(Migration)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Iden)]
|
||||||
|
pub(super) enum TestTable {
|
||||||
|
Table,
|
||||||
|
Id,
|
||||||
|
CiStr,
|
||||||
|
}
|
||||||
|
#[derive(DeriveMigrationName)]
|
||||||
|
struct Migration;
|
||||||
|
#[async_trait]
|
||||||
|
impl MigrationTrait for Migration {
|
||||||
|
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager
|
||||||
|
.create_table(
|
||||||
|
Table::create()
|
||||||
|
.table(TestTable::Table)
|
||||||
|
.col(sqlite_rowid_alias(TestTable::Id))
|
||||||
|
.col(sqlite_case_insensitive_string(TestTable::CiStr))
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, DeriveEntityModel)]
|
||||||
|
#[sea_orm(table_name = "test_table")]
|
||||||
|
pub struct Model {
|
||||||
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
|
id: i64,
|
||||||
|
ci_str: String,
|
||||||
|
}
|
||||||
|
#[derive(Debug, EnumIter)]
|
||||||
|
pub enum Relation {}
|
||||||
|
impl RelationTrait for Relation {
|
||||||
|
fn def(&self) -> RelationDef {
|
||||||
|
panic!("No RelationDef")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
|
|
||||||
|
let db = new_memory_db().await;
|
||||||
|
Migrator::up(&db, None).await.expect("up");
|
||||||
|
|
||||||
|
let i = 50;
|
||||||
|
|
||||||
|
// Insert a lowercase string
|
||||||
|
{
|
||||||
|
let model = ActiveModel {
|
||||||
|
id: Set(i),
|
||||||
|
ci_str: Set("abcd".to_owned()),
|
||||||
|
};
|
||||||
|
model.insert(&db).await.expect("insert");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query by uppercase string
|
||||||
|
{
|
||||||
|
let model = Entity::find()
|
||||||
|
.filter(Column::CiStr.contains("ABCD"))
|
||||||
|
.one(&db)
|
||||||
|
.await
|
||||||
|
.expect("find by case insensitive string")
|
||||||
|
.expect("find by case insensitive string");
|
||||||
|
assert_eq!(model.id, i);
|
||||||
|
// The string should be read back as-is
|
||||||
|
assert_eq!(model.ci_str, "abcd");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user