10 Commits

30 changed files with 2982 additions and 894 deletions
+4
View File
@@ -0,0 +1,4 @@
allow-expect-in-tests = true
check-private-items = true
allowed-duplicate-crates = []
+22 -6
View File
@@ -1,7 +1,23 @@
# OS Files
*~
._*
.DS_Store
*
!**/
# Rust
/target
!.zed/settings.json
!.gitignore
!/CONTRIBUTING.md
!/LICENSE.md
!/README.md
!/.clippy.toml
!/.rustfmt.toml
!/.tombi.toml
!/Cargo.toml
!/Cargo.lock
!/justfile
!/rust-toolchain.toml
!/crates/**/*.rs
!/crates/**/Cargo.toml
!/crates/**/README.md
!/lints/*.toml
-1
View File
@@ -1,2 +1 @@
hard_tabs = true
wrap_comments = true
+10
View File
@@ -0,0 +1,10 @@
[format.rules]
indent-style = "tab"
indent-width = 4
line-width = 120
# Disable TOML 1.1 features from tombi as long as we support rust <1.94
[[schemas]]
toml-version = "v1.0.0"
path = "tombi://www.schemastore.org/cargo.json"
include = ["Cargo.toml"]
-9
View File
@@ -1,9 +0,0 @@
{
"recommendations": [
"vadimcn.vscode-lldb",
"barbosshack.crates-io",
"usernamehw.errorlens",
"tamasfe.even-better-toml",
"rust-lang.rust-analyzer",
]
}
-33
View File
@@ -1,33 +0,0 @@
{
// 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,
}
+26
View File
@@ -0,0 +1,26 @@
{
"lsp": {
"just-lsp": {
"initialization_options": {
"formatting": {
"indentation": "\t",
},
},
},
"rust-analyzer": {
"initialization_options": {
"imports": {
"granularity": { "enforce": true, "group": "module" },
"group": { "enable": true },
"merge": { "glob": false },
"preferNoStd": true,
},
"server": {
"extraEnv": {
"RUSTUP_TOOLCHAIN": "stable",
},
},
},
},
},
}
+2 -1
View File
@@ -3,4 +3,5 @@ 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
in the latest v0.0.x of https://git.skrundz.dev/skrunix/license
(mirrored to https://github.com/skrunix/license)
Generated
+1395 -459
View File
File diff suppressed because it is too large Load Diff
+535 -35
View File
@@ -1,43 +1,543 @@
[workspace]
members = ["seamantic"]
resolver = "2"
resolver = "3"
members = ["crates/*"]
[workspace.package]
authors = []
edition = "2024"
rust-version = "1.94"
license-file = "LICENSE.md"
rust-version = "1.85.0"
[workspace.lints.rust]
arithmetic_overflow = "forbid"
missing_docs = "forbid"
unsafe_code = "forbid"
unused_doc_comments = "forbid"
[workspace.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"
publish = false
[workspace.dependencies]
sea-orm = { version = "2.0.0-rc.10", default-features = false }
sea-orm-migration = { version = "2.0.0-rc.10", default-features = false }
seamantic = { path = "crates/seamantic", version = "=1.0.1", default-features = false }
serde = { version = "^1", default-features = false }
serde_test = { version = "^1", default-features = false }
tokio = { version = "^1", default-features = false }
sea-orm = { version = "^2.0.0", default-features = false }
sea-orm-migration = { version = "^2.0.0", default-features = false }
serde = { version = "^1.0.220", default-features = false }
serde_test = { version = "^1.0.0", default-features = false }
tokio = { version = "^1.38.2", default-features = false }
[workspace.lints.clippy]
cargo = { level = "warn", priority = -1 }
complexity = { level = "warn", priority = -1 }
correctness = { level = "deny", priority = -1 }
perf = { level = "warn", priority = -1 }
style = { level = "warn", priority = -1 }
suspicious = { level = "warn", priority = -1 }
absolute-paths = "allow"
alloc-instead-of-core = "warn"
allow-attributes = "warn"
allow-attributes-without-reason = "deny"
arbitrary-source-item-ordering = "allow"
arithmetic-side-effects = "deny"
as-conversions = "deny"
as-pointer-underscore = "deny"
as-ptr-cast-mut = "deny"
as-underscore = "deny"
assertions-on-result-states = "warn"
assigning-clones = "warn"
big-endian-bytes = "allow"
bool-to-int-with-if = "warn"
borrow-as-ptr = "warn"
branches-sharing-code = "warn"
case-sensitive-file-extension-comparisons = "warn"
cast-lossless = "deny"
cast-possible-truncation = "deny"
cast-possible-wrap = "deny"
cast-precision-loss = "deny"
cast-ptr-alignment = "deny"
cast-sign-loss = "deny"
cfg-not-test = "deny"
checked-conversions = "deny"
clear-with-drain = "warn"
clone-on-ref-ptr = "warn"
cloned-instead-of-copied = "warn"
coerce-container-to-any = "warn"
cognitive-complexity = "allow"
collapsible-else-if = "warn"
collection-is-never-read = "warn"
comparison-chain = "warn"
copy-iterator = "warn"
create-dir = "warn"
dbg-macro = "warn"
debug-assert-with-mut-call = "deny"
decimal-bitwise-operands = "warn"
decimal-literal-representation = "warn"
default-numeric-fallback = "warn"
default-trait-access = "warn"
default-union-representation = "allow"
deref-by-slicing = "warn"
derive-partial-eq-without-eq = "warn"
disallowed-script-idents = "deny"
doc-broken-link = "warn"
doc-comment-double-space-linebreaks = "warn"
doc-include-without-cfg = "warn"
doc-link-code = "warn"
doc-link-with-quotes = "warn"
doc-markdown = "warn"
doc-paragraphs-missing-punctuation = "warn"
duration-suboptimal-units = "warn"
elidable-lifetime-names = "warn"
else-if-without-else = "warn"
empty-drop = "warn"
empty-enum-variants-with-brackets = "warn"
empty-enums = "warn"
empty-structs-with-brackets = "warn"
enum-glob-use = "warn"
equatable-if-let = "warn"
error-impl-error = "warn"
exhaustive-enums = "warn"
exhaustive-structs = "warn"
exit = "warn"
expect-used = "warn"
expl-impl-clone-on-copy = "warn"
explicit-deref-methods = "warn"
explicit-into-iter-loop = "warn"
explicit-iter-loop = "warn"
fallible-impl-from = "warn"
field-scoped-visibility-modifiers = "warn"
filetype-is-file = "warn"
filter-map-next = "warn"
flat-map-option = "warn"
float-arithmetic = "warn"
float-cmp = "warn"
float-cmp-const = "warn"
fn-params-excessive-bools = "allow"
fn-to-numeric-cast-any = "warn"
format-collect = "warn"
format-push-string = "warn"
future-not-send = "warn"
get-unwrap = "warn"
host-endian-bytes = "allow"
if-not-else = "warn"
if-then-some-else-none = "warn"
ignore-without-reason = "warn"
ignored-unit-patterns = "warn"
impl-trait-in-params = "warn"
implicit-clone = "warn"
implicit-hasher = "warn"
implicit-return = "allow"
imprecise-flops = "warn"
inconsistent-struct-constructor = "warn"
index-refutable-slice = "deny"
indexing-slicing = "deny"
inefficient-to-string = "warn"
infinite-loop = "warn"
inline-always = "warn"
inline-asm-x86-att-syntax = "warn"
inline-asm-x86-intel-syntax = "warn"
inline-modules = "allow"
inline-trait-bounds = "allow"
integer-division = "deny"
integer-division-remainder-used = "deny"
into-iter-without-iter = "warn"
invalid-upcast-comparisons = "warn"
ip-constant = "warn"
items-after-statements = "warn"
iter-filter-is-ok = "warn"
iter-filter-is-some = "warn"
iter-not-returning-iterator = "warn"
iter-on-empty-collections = "warn"
iter-on-single-items = "warn"
iter-over-hash-type = "warn"
iter-with-drain = "warn"
iter-without-into-iter = "warn"
large-digit-groups = "warn"
large-futures = "warn"
large-include-file = "warn"
large-stack-arrays = "warn"
large-stack-frames = "warn"
large-types-passed-by-value = "warn"
let-underscore-must-use = "warn"
let-underscore-untyped = "warn"
linkedlist = "warn"
literal-string-with-formatting-args = "warn"
little-endian-bytes = "allow"
lossy-float-literal = "warn"
macro-use-imports = "warn"
manual-assert = "warn"
manual-assert-eq = "warn"
manual-ilog2 = "warn"
manual-instant-elapsed = "warn"
manual-is-power-of-two = "warn"
manual-is-variant-and = "warn"
manual-let-else = "warn"
manual-midpoint = "warn"
manual-string-new = "warn"
many-single-char-names = "warn"
map-err-ignore = "warn"
map-unwrap-or = "warn"
map-with-unused-argument-over-ranges = "warn"
match-bool = "warn"
match-same-arms = "warn"
match-wild-err-arm = "warn"
match-wildcard-for-single-variants = "warn"
maybe-infinite-iter = "warn"
mem-forget = "warn"
min-ident-chars = "allow"
mismatching-type-param-order = "warn"
missing-assert-message = "warn"
missing-asserts-for-indexing = "warn"
missing-const-for-fn = "warn"
missing-docs-in-private-items = "warn"
missing-errors-doc = "warn"
missing-fields-in-debug = "warn"
missing-inline-in-public-items = "warn"
missing-panics-doc = "warn"
missing-trait-methods = "warn"
mixed-read-write-in-expression = "warn"
mod-module-files = "allow"
module-name-repetitions = "warn"
modulo-arithmetic = "warn"
multiple-crate-versions = "allow"
multiple-inherent-impl = "warn"
multiple-unsafe-ops-per-block = "warn"
must-use-candidate = "warn"
mut-mut = "warn"
mutex-atomic = "warn"
mutex-integer = "warn"
naive-bytecount = "warn"
needless-bitwise-bool = "warn"
needless-collect = "warn"
needless-continue = "warn"
needless-for-each = "warn"
needless-pass-by-ref-mut = "warn"
needless-pass-by-value = "warn"
needless-raw-string-hashes = "warn"
needless-raw-strings = "warn"
needless-type-cast = "warn"
no-effect-underscore-binding = "warn"
no-mangle-with-rust-abi = "warn"
non-ascii-literal = "allow"
non-send-fields-in-send-ty = "warn"
non-std-lazy-statics = "warn"
non-zero-suggestions = "warn"
nonminimal-bool = "warn"
nonstandard-macro-braces = "warn"
option-as-ref-cloned = "warn"
option-if-let-else = "warn"
option-option = "warn"
or-fun-call = "warn"
overly-complex-bool-expr = "allow"
panic = "warn"
panic-in-result-fn = "warn"
partial-pub-fields = "warn"
path-buf-push-overwrite = "warn"
pathbuf-init-then-push = "warn"
pattern-type-mismatch = "allow"
pointer-format = "warn"
precedence-bits = "warn"
print-stderr = "warn"
print-stdout = "warn"
ptr-as-ptr = "warn"
ptr-cast-constness = "warn"
ptr-offset-by-literal = "warn"
pub-underscore-fields = "warn"
pub-use = "allow"
pub-with-shorthand = "allow"
pub-without-shorthand = "warn"
question-mark-used = "allow"
range-minus-one = "warn"
range-plus-one = "warn"
rc-buffer = "warn"
rc-mutex = "warn"
read-zero-byte-vec = "warn"
redundant-clone = "warn"
redundant-closure-for-method-calls = "warn"
redundant-else = "warn"
redundant-pub-crate = "allow"
redundant-test-prefix = "warn"
redundant-type-annotations = "warn"
ref-as-ptr = "warn"
ref-binding-to-reference = "warn"
ref-option = "warn"
ref-option-ref = "warn"
ref-patterns = "warn"
renamed-function-params = "warn"
rest-pat-in-fully-bound-structs = "warn"
return-and-then = "warn"
return-self-not-must-use = "warn"
same-functions-in-if-condition = "warn"
same-length-and-capacity = "warn"
same-name-method = "warn"
search-is-some = "warn"
self-named-module-files = "warn"
self-only-used-in-recursion = "warn"
semicolon-if-nothing-returned = "warn"
semicolon-inside-block = "warn"
semicolon-outside-block = "allow"
separated-literal-suffix = "allow"
set-contains-or-insert = "warn"
shadow-reuse = "allow"
shadow-same = "allow"
shadow-unrelated = "allow"
should-panic-without-expect = "warn"
significant-drop-in-scrutinee = "warn"
significant-drop-tightening = "warn"
similar-names = "allow"
single-call-fn = "allow"
single-char-lifetime-names = "allow"
single-char-pattern = "allow"
single-match-else = "warn"
single-option-map = "warn"
stable-sort-primitive = "warn"
std-instead-of-alloc = "warn"
std-instead-of-core = "warn"
str-split-at-newline = "warn"
str-to-string = "warn"
string-add = "warn"
string-add-assign = "warn"
string-lit-as-bytes = "warn"
string-lit-chars-any = "warn"
string-slice = "warn"
struct-excessive-bools = "allow"
struct-field-names = "warn"
suboptimal-flops = "warn"
suspicious-operation-groupings = "warn"
suspicious-xor-used-as-pow = "warn"
tests-outside-test-module = "warn"
todo = "warn"
too-long-first-doc-paragraph = "allow"
too-many-lines = "allow"
trailing-empty-array = "warn"
trait-duplication-in-bounds = "warn"
transmute-ptr-to-ptr = "warn"
transmute-undefined-repr = "warn"
trivial-regex = "warn"
trivially-copy-pass-by-ref = "warn"
try-err = "warn"
tuple-array-conversions = "warn"
type-repetition-in-bounds = "warn"
unchecked-time-subtraction = "deny"
undocumented-unsafe-blocks = "deny"
unicode-not-nfc = "warn"
unimplemented = "warn"
uninhabited-references = "warn"
uninlined-format-args = "warn"
unnecessary-box-returns = "warn"
unnecessary-debug-formatting = "warn"
unnecessary-join = "warn"
unnecessary-literal-bound = "warn"
unnecessary-safety-comment = "warn"
unnecessary-safety-doc = "warn"
unnecessary-self-imports = "warn"
unnecessary-semicolon = "warn"
unnecessary-struct-initialization = "warn"
unnecessary-trailing-comma = "warn"
unnecessary-wraps = "warn"
unneeded-field-pattern = "warn"
unnested-or-patterns = "warn"
unreachable = "warn"
unreadable-literal = "warn"
unsafe-derive-deserialize = "warn"
unseparated-literal-suffix = "warn"
unused-async = "warn"
unused-async-trait-impl = "warn"
unused-peekable = "warn"
unused-result-ok = "warn"
unused-rounding = "warn"
unused-self = "warn"
unused-trait-names = "warn"
unwrap-in-result = "warn"
unwrap-used = "warn"
use-debug = "warn"
use-self = "warn"
used-underscore-binding = "warn"
used-underscore-items = "warn"
useless-let-if-seq = "warn"
verbose-bit-mask = "warn"
verbose-file-reads = "warn"
volatile-composites = "warn"
while-float = "warn"
wildcard-enum-match-arm = "warn"
wildcard-imports = "warn"
with-capacity-zero = "warn"
zero-sized-map-values = "warn"
[workspace.lints.rust]
deprecated-safe = "forbid"
future-incompatible = "deny"
keyword-idents = "deny"
let-underscore = "warn"
nonstandard-style = "warn"
refining-impl-trait = "warn"
rust-2018-compatibility = "deny"
rust-2018-idioms = "deny"
rust-2021-compatibility = "deny"
rust-2024-compatibility = "deny"
unknown-or-malformed-diagnostic-attributes = "deny"
unused = { level = "warn", priority = -1 }
ambiguous-glob-reexports = "warn"
ambiguous-negative-literals = "deny"
ambiguous-wide-pointer-comparisons = "warn"
arithmetic-overflow = "deny"
asm-sub-register = "warn"
async-fn-in-trait = "warn"
bad-asm-style = "warn"
binary-asm-labels = "deny"
bindings-with-variant-name = "deny"
break-with-label-and-loop = "warn"
c-void-returns = "warn"
clashing-extern-declarations = "warn"
closure-returning-async-block = "warn"
confusable-idents = "warn"
const-item-interior-mutations = "warn"
const-item-mutation = "warn"
dangerous-implicit-autorefs = "deny"
dangling-pointers-from-locals = "warn"
dangling-pointers-from-temporaries = "warn"
dead-code-pub-in-binary = "warn"
deprecated = "warn"
deprecated-in-future = "allow"
deprecated-where-clause-location = "warn"
deref-into-dyn-supertrait = "warn"
deref-nullptr = "deny"
double-negations = "warn"
drop-bounds = "warn"
dropping-copy-types = "warn"
dropping-references = "warn"
duplicate-features = "deny"
duplicate-macro-attributes = "warn"
dyn-drop = "warn"
enum-intrinsics-non-enums = "deny"
explicit-builtin-cfgs-in-flags = "deny"
exported-private-dependencies = "warn"
ffi-unwind-calls = "warn"
for-loops-over-fallibles = "warn"
forgetting-copy-types = "warn"
forgetting-references = "warn"
function-casts-as-integer = "warn"
function-item-references = "warn"
hidden-glob-reexports = "warn"
impl-trait-redundant-captures = "warn"
improper-ctypes = "warn"
improper-ctypes-definitions = "warn"
improper-gpu-kernel-arg = "warn"
incomplete-features = "warn"
incomplete-include = "deny"
ineffective-unstable-trait-impl = "deny"
inline-no-sanitize = "warn"
integer-to-ptr-transmutes = "warn"
internal-features = "warn"
invalid-atomic-ordering = "deny"
invalid-doc-attributes = "warn"
invalid-from-utf8 = "warn"
invalid-from-utf8-unchecked = "deny"
invalid-nan-comparisons = "warn"
invalid-null-arguments = "deny"
invalid-reference-casting = "deny"
invalid-runtime-symbol-definitions = "deny"
invalid-value = "warn"
irrefutable-let-patterns = "warn"
large-assignments = "warn"
linker-info = "allow"
linker-messages = "warn"
long-running-const-eval = "deny"
macro-use-extern-crate = "warn"
meta-variable-misuse = "warn"
mismatched-lifetime-syntaxes = "warn"
missing-abi = "warn"
missing-copy-implementations = "warn"
missing-debug-implementations = "warn"
missing-docs = "warn"
missing-gpu-kernel-export-name = "warn"
mixed-script-confusables = "warn"
mutable-transmutes = "deny"
named-arguments-used-positionally = "warn"
named-asm-labels = "deny"
no-mangle-const-items = "deny"
no-mangle-generic-items = "warn"
non-ascii-idents = "deny"
non-contiguous-range-endpoints = "warn"
non-local-definitions = "warn"
non-shorthand-field-patterns = "warn"
noop-method-call = "warn"
opaque-hidden-inferred-bound = "warn"
overflowing-literals = "deny"
overlapping-range-endpoints = "warn"
private-bounds = "warn"
private-interfaces = "warn"
ptr-to-integer-transmute-in-consts = "warn"
redundant-imports = "warn"
redundant-lifetimes = "warn"
renamed-and-removed-lints = "warn"
rtsan-nonblocking-async = "warn"
single-use-lifetimes = "warn"
special-module-name = "warn"
stable-features = "warn"
suspicious-double-ref-op = "warn"
suspicious-runtime-symbol-definitions = "warn"
text-direction-codepoint-in-comment = "deny"
text-direction-codepoint-in-literal = "deny"
trivial-bounds = "warn"
trivial-casts = "warn"
trivial-numeric-casts = "warn"
type-alias-bounds = "warn"
uncommon-codepoints = "warn"
unconditional-panic = "deny"
unconditional-recursion = "warn"
undropped-manually-drops = "deny"
unexpected-cfgs = "warn"
unfulfilled-lint-expectations = "warn"
ungated-async-fn-track-caller = "warn"
unit-bindings = "warn"
unknown-crate-types = "deny"
unknown-lints = "warn"
unnameable-test-items = "warn"
unnameable-types = "warn"
unnecessary-transmutes = "warn"
unpredictable-function-pointer-comparisons = "warn"
unreachable-cfg-select-predicates = "warn"
unreachable-pub = "warn"
unsafe-code = "forbid"
unstable-features = "forbid"
unused-associated-type-bounds = "warn"
unused-comparisons = "warn"
unused-crate-dependencies = "warn"
unused-import-braces = "warn"
unused-lifetimes = "warn"
unused-qualifications = "warn"
unused-results = "warn"
useless-deprecated = "deny"
useless-ptr-null-checks = "warn"
uses-power-alignment = "warn"
variant-size-differences = "warn"
warnings = "warn"
while-true = "warn"
# unstable lints
# default-overrides-default-fields = "deny"
# deprecated-llvm-intrinsic = "warn"
# implicit-provenance-casts = "deny"
# multiple-supertrait-upcastable = "warn"
# must-not-suspend = "warn"
# non-exhaustive-omitted-patterns = "warn"
# resolving-to-items-shadowing-supertrait-items = "warn"
# shadowing-supertrait-items = "warn"
# tail-call-track-caller = "warn"
# test-unstable-lint = "deny"
# unqualified-local-imports = "deny"
[workspace.lints.rustdoc]
bare-urls = "warn"
broken-intra-doc-links = "warn"
invalid-codeblock-attributes = "warn"
invalid-html-tags = "warn"
invalid-rust-codeblocks = "warn"
missing-crate-level-docs = "warn"
private-doc-tests = "warn"
private-intra-doc-links = "warn"
redundant-explicit-links = "warn"
unescaped-backticks = "warn"
# unstable lints
# missing-doc-code-examples = "warn"
[profile.release]
opt-level = 3
strip = "debuginfo"
overflow-checks = true
lto = "fat"
codegen-units = 1
+11 -12
View File
@@ -1,17 +1,16 @@
# Seamantic
[![Crates Version](https://img.shields.io/crates/v/seamantic.svg)](https://crates.io/crates/seamantic)
## Development
A library to enhance SeaORM
Run the full validation suite before committing:
## Various builds
```sh
just all
just install
```
- build: `cargo hack --feature-powerset build`
- clippy: `cargo hack --feature-powerset clippy -- -D warnings`
- test: `cargo hack --feature-powerset test`
- test old: `cargo +1.85 hack --feature-powerset test`
- example: `cargo run --example=migrations --features=sqlite`
- fmt: `cargo fmt --check`
- docs: `RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features`
- semver: `cargo semver-checks --all-features`
- publish: `cargo publish --dry-run -p seamantic`
##### Publish
- `cargo semver-checks --all-features`
- `cargo publish --dry-run -p <crate>`
- `cargo publish --dry-run --workspace`
@@ -1,40 +1,41 @@
[package]
name = "seamantic"
version = "0.0.7"
categories = []
version = "1.0.1"
description = "A library to enhance SeaORM"
repository = "https://github.com/QuantumShade/seamantic"
repository = "https://git.skrundz.dev/quantumshade/seamantic"
keywords = ["SeaORM"]
categories = ["database"]
include = ["/src"]
publish = true
authors.workspace = true
edition.workspace = true
license-file.workspace = true
rust-version.workspace = true
license-file.workspace = true
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[lints]
workspace = true
[[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 = { workspace = true }
sea-orm-migration = { workspace = true }
serde = { workspace = true, features = ["derive", "std"], optional = true }
[dev-dependencies]
sea-orm = { workspace = true, features = ["entity-registry", "schema-sync"] }
sea-orm-migration = { workspace = true, features = ["runtime-tokio-rustls"] }
serde_test = { workspace = true }
tokio = { workspace = true, features = ["rt", "macros"] }
tokio = { workspace = true, features = ["macros", "rt"] }
[features]
default = ["sqlite"]
serde = ["dep:serde"]
sqlite = ["sea-orm-migration/sqlx-sqlite"]
[lints]
workspace = true
@@ -14,6 +14,10 @@ pub(super) struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
#[expect(
elided_lifetimes_in_paths,
reason = "async_trait causes lifetimes to be strange"
)]
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
@@ -28,6 +32,10 @@ impl MigrationTrait for Migration {
Ok(())
}
#[expect(
elided_lifetimes_in_paths,
reason = "async_trait causes lifetimes to be strange"
)]
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Objects::Table).to_owned())
@@ -15,3 +15,6 @@ async fn main() {
let database = Database::connect(options).await.expect("Database::connect");
Migrator::up(&database, None).await.expect("Migrator::up");
}
// dev-dependencies cannot be optional and `serde_test` is not used
use {serde_test as _, tokio as _};
@@ -1,4 +1,4 @@
//! A library for enhacing SeaORM
//! A library for enhacing `SeaORM`.
#![cfg_attr(docsrs, feature(doc_cfg))]
@@ -31,6 +31,7 @@ macro_rules! migrations {
/// Auto-generated migration manager
#[automatically_derived]
#[non_exhaustive]
pub struct Migrator;
#[automatically_derived]
@@ -46,3 +47,8 @@ macro_rules! migrations {
}
};
}
// dev-dependencies cannot be optional and `tokio` and `serde_test` are only used
// when the `sqlite` feature is active
#[cfg(test)]
use {serde_test as _, tokio as _};
@@ -1,37 +1,45 @@
//! [Duration] utilities
//! [Duration] utilities.
use core::time::Duration;
use sea_orm::sea_query::{ArrayType, Nullable, ValueType, ValueTypeErr};
use sea_orm::{ColIdx, ColumnType, QueryResult, TryGetError, TryGetable, Value};
use sea_orm::{ColIdx, ColumnType, DbErr, QueryResult, TryFromU64, TryGetError, TryGetable, Value};
// "u64 unsupported by sqlx-sqlite", so use i64 as the bit representation
/// The internal representation that `SeaORM` uses for durations.
///
/// "u64 unsupported by sqlx-sqlite", so use i64 as the bit representation.
type SeaOrmRepr = i64;
/// The internal represenation that [Duration] uses.
type DurationRepr = u64;
/// Wrapper around [Duration] to store a number of seconds
/// Wrapper around [Duration] to store a number of seconds.
///
/// ### Warning:
/// Sub-second precision will be lost
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
/// # Warning:
/// Sub-second precision will be lost.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
#[repr(transparent)]
#[expect(clippy::exhaustive_structs, reason = "unlikely to change")]
pub struct Seconds(pub Duration);
impl From<Duration> for Seconds {
#[inline]
fn from(value: Duration) -> Self {
Self(Duration::from_secs(value.as_secs()))
}
}
impl From<Seconds> for Duration {
#[inline]
fn from(value: Seconds) -> Self {
value.0
}
}
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
impl ValueType for Seconds {
#[inline]
fn try_from(v: Value) -> Result<Self, ValueTypeErr> {
<SeaOrmRepr as ValueType>::try_from(v)
.map(|i| DurationRepr::from_ne_bytes(i.to_ne_bytes()))
@@ -39,26 +47,32 @@ impl ValueType for Seconds {
.map(Self)
}
#[inline]
fn type_name() -> String {
core::any::type_name::<Duration>().to_string()
core::any::type_name::<Self>().to_owned()
}
#[inline]
fn array_type() -> ArrayType {
SeaOrmRepr::array_type()
}
#[inline]
fn column_type() -> ColumnType {
SeaOrmRepr::column_type()
}
}
impl From<Seconds> for Value {
#[inline]
fn from(value: Seconds) -> Self {
value.0.as_secs().into()
}
}
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
impl TryGetable for Seconds {
#[inline]
fn try_get_by<I: ColIdx>(res: &QueryResult, index: I) -> Result<Self, TryGetError> {
SeaOrmRepr::try_get_by(res, index)
.map(|i| DurationRepr::from_ne_bytes(i.to_ne_bytes()))
@@ -67,7 +81,18 @@ impl TryGetable for Seconds {
}
}
impl TryFromU64 for Seconds {
#[inline]
fn try_from_u64(n: u64) -> Result<Self, DbErr> {
SeaOrmRepr::try_from_u64(n)
.map(|i| DurationRepr::from_ne_bytes(i.to_ne_bytes()))
.map(Duration::from_secs)
.map(Self)
}
}
impl Nullable for Seconds {
#[inline]
fn null() -> Value {
SeaOrmRepr::null()
}
@@ -82,19 +107,19 @@ mod tests {
use super::Seconds;
#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "seconds")]
pub struct Model {
struct Model {
#[sea_orm(primary_key, auto_increment = false)]
id: u8,
#[sea_orm(primary_key, auto_increment = false)]
seconds: Seconds,
nullable: Option<Seconds>,
}
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
impl ActiveModelBehavior for ActiveModel {}
#[allow(dead_code)]
#[derive(Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
enum Relation {}
}
@@ -1,4 +1,4 @@
//! Typed IDs for use as primary keys
//! Typed IDs for use as primary keys.
use core::cmp::Ordering;
use core::fmt;
@@ -8,55 +8,70 @@ 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
/// The internal representation used by the database.
pub type SeaOrmRepr = i64;
/// An opaque type representing a row ID
/// An opaque type representing a row ID.
///
/// IDs should be tagged with `#[sea_orm(primary_key, auto_increment = false)]`.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
#[repr(transparent)]
pub struct Id<T> {
/// The ID.
id: SeaOrmRepr,
/// `PhantomData` for `T`.
#[cfg_attr(feature = "serde", serde(skip_serializing, default))]
_phantom: PhantomData<T>,
}
// Manual implementation since `T: Clone` is not required
// Manual implementation since `T: Clone` is not required.
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
impl<T> Clone for Id<T> {
#[inline]
fn clone(&self) -> Self {
*self
}
}
// Manual implementation since `T: Copy` is not required
// Manual implementation since `T: Copy` is not required.
impl<T> Copy for Id<T> {}
// Manual implementation since `T: PartialEq` is not required
// Manual implementation since `T: PartialEq` is not required.
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
impl<T> PartialEq for Id<T> {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.id == other.id
self.id.eq(&other.id)
}
}
// Manual implementation since `T: Eq` is not required
// Manual implementation since `T: Eq` is not required.
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
impl<T> Eq for Id<T> {}
// Manual implementation since `T: PartialOrd` is not required
// Manual implementation since `T: PartialOrd` is not required.
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
impl<T> PartialOrd for Id<T> {
#[inline]
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
// Manual implementation since `T: Ord` is not required
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
impl<T> Ord for Id<T> {
#[inline]
fn cmp(&self, other: &Self) -> Ordering {
self.id.cmp(&other.id)
}
}
// Manual implementation since `T: Hash` is not required
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
impl<T> Hash for Id<T> {
#[inline]
fn hash<H: Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
@@ -64,7 +79,9 @@ impl<T> Hash for Id<T> {
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 {
#[inline]
#[must_use]
pub const fn from_raw(raw: SeaOrmRepr) -> Self {
Self {
id: raw,
_phantom: PhantomData,
@@ -72,13 +89,16 @@ impl<T> Id<T> {
}
/// Allows extracting the raw value, though the use is discouraged.
pub fn into_raw(self) -> SeaOrmRepr {
#[inline]
#[must_use]
pub const fn into_raw(self) -> SeaOrmRepr {
self.id
}
}
impl<T> fmt::Debug for Id<T> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Id")
.field("T", &core::any::type_name::<T>())
.field("id", &self.id)
@@ -86,7 +106,9 @@ impl<T> fmt::Debug for Id<T> {
}
}
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
impl<T> ValueType for Id<T> {
#[inline]
fn try_from(v: Value) -> Result<Self, ValueTypeErr> {
<SeaOrmRepr as ValueType>::try_from(v).map(|id| Self {
id,
@@ -94,26 +116,32 @@ impl<T> ValueType for Id<T> {
})
}
#[inline]
fn type_name() -> String {
format!("Id<{}>", &core::any::type_name::<T>())
format!("Id<{}>", core::any::type_name::<T>())
}
#[inline]
fn array_type() -> ArrayType {
SeaOrmRepr::array_type()
}
#[inline]
fn column_type() -> ColumnType {
SeaOrmRepr::column_type()
}
}
impl<T> From<Id<T>> for Value {
#[inline]
fn from(value: Id<T>) -> Self {
value.id.into()
}
}
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
impl<T> TryGetable for Id<T> {
#[inline]
fn try_get_by<I: ColIdx>(res: &QueryResult, index: I) -> Result<Self, TryGetError> {
SeaOrmRepr::try_get_by(res, index).map(|id| Self {
id,
@@ -123,6 +151,7 @@ impl<T> TryGetable for Id<T> {
}
impl<T> TryFromU64 for Id<T> {
#[inline]
fn try_from_u64(n: u64) -> Result<Self, DbErr> {
SeaOrmRepr::try_from_u64(n).map(|id| Self {
id,
@@ -132,6 +161,7 @@ impl<T> TryFromU64 for Id<T> {
}
impl<T> Nullable for Id<T> {
#[inline]
fn null() -> Value {
SeaOrmRepr::null()
}
@@ -146,24 +176,32 @@ mod tests {
use super::Id;
#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "ids")]
pub struct Model {
#[expect(clippy::use_self, reason = "derive macros cause Self to be invalid")]
struct Model {
#[sea_orm(primary_key, auto_increment = false)]
id: Id<Model>,
nullable: Option<Id<Model>>,
}
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
impl ActiveModelBehavior for ActiveModel {}
#[allow(dead_code)]
#[derive(Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
enum Relation {}
#[test]
#[expect(clippy::missing_panics_doc, reason = "unit test")]
fn round_trip() {
let raw = 1234;
let id = Id::<()>::from_raw(raw);
assert_eq!(raw, id.into_raw());
}
#[test]
#[cfg(feature = "serde")]
fn test_serde() {
fn ser_de() {
let id: Id<()> = Id::from_raw(1234);
serde_test::assert_tokens(&id, &[serde_test::Token::I64(1234)]);
}
+6
View File
@@ -0,0 +1,6 @@
//! `SeaORM` column types for enforcing data consistency.
pub mod duration;
pub mod id;
pub mod net;
pub mod path;
+193
View File
@@ -0,0 +1,193 @@
//! [`IpAddr`], [`Ipv4Addr`], and [`Ipv6Addr`] utilities.
use core::net::{IpAddr, Ipv4Addr, Ipv6Addr};
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.
type SeaOrmRepr = String;
/// Wrapper around [`IpAddr`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
#[repr(transparent)]
#[expect(clippy::exhaustive_structs, reason = "unlikely to change")]
pub struct IpAddress(pub IpAddr);
/// Wrapper around [`Ipv4Addr`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
#[repr(transparent)]
#[expect(clippy::exhaustive_structs, reason = "unlikely to change")]
pub struct Ipv4Address(pub Ipv4Addr);
/// Wrapper around [`Ipv6Addr`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
#[repr(transparent)]
#[expect(clippy::exhaustive_structs, reason = "unlikely to change")]
pub struct Ipv6Address(pub Ipv6Addr);
/// Implements `SeaORM` traits for `IP` types.
macro_rules! impl_addr {
($t:ty, $inner:ty, $repr:ty) => {
impl From<$inner> for $t {
#[inline]
fn from(value: $inner) -> Self {
Self(value)
}
}
impl From<$t> for $inner {
#[inline]
fn from(value: $t) -> Self {
value.0
}
}
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
impl ValueType for $t {
#[inline]
fn try_from(v: Value) -> Result<Self, ValueTypeErr> {
<SeaOrmRepr as ValueType>::try_from(v)
.and_then(|s| s.parse().map_err(|_| ValueTypeErr))
.map(Self)
}
#[inline]
fn type_name() -> String {
core::any::type_name::<Self>().to_string()
}
#[inline]
fn array_type() -> ArrayType {
SeaOrmRepr::array_type()
}
#[inline]
fn column_type() -> ColumnType {
SeaOrmRepr::column_type()
}
}
impl From<$t> for Value {
#[inline]
fn from(value: $t) -> Self {
value.0.to_string().into()
}
}
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
impl TryGetable for $t {
#[inline]
fn try_get_by<I: ColIdx>(res: &QueryResult, index: I) -> Result<Self, TryGetError> {
SeaOrmRepr::try_get_by(res, index)
.and_then(|s| s.parse().map_err(|_| TryGetError::Null(Self::type_name())))
.map(Self)
}
}
impl TryFromU64 for $t {
#[inline]
fn try_from_u64(n: u64) -> Result<Self, DbErr> {
SeaOrmRepr::try_from_u64(n)
.and_then(|s| {
s.parse()
.map_err(|_| DbErr::ConvertFromU64(core::any::type_name::<Self>()))
})
.map(Self)
}
}
impl Nullable for $t {
#[inline]
fn null() -> Value {
SeaOrmRepr::null()
}
}
};
}
impl_addr!(IpAddress, IpAddr, SeaOrmRepr);
impl_addr!(Ipv4Address, Ipv4Addr, SeaOrmRepr);
impl_addr!(Ipv6Address, Ipv6Addr, SeaOrmRepr);
#[cfg(test)]
mod tests {
mod ipaddress {
use sea_orm::{
ActiveModelBehavior, DeriveEntityModel, DerivePrimaryKey, DeriveRelation, EntityTrait,
EnumIter, PrimaryKeyTrait,
};
use super::super::IpAddress;
#[derive(Debug, Clone, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "ipaddress")]
struct Model {
#[sea_orm(primary_key, auto_increment = false)]
id: u8,
#[sea_orm(primary_key, auto_increment = false)]
addr: IpAddress,
nullable: Option<IpAddress>,
}
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
impl ActiveModelBehavior for ActiveModel {}
#[derive(Debug, EnumIter, DeriveRelation)]
enum Relation {}
}
mod ipv4address {
use sea_orm::{
ActiveModelBehavior, DeriveEntityModel, DerivePrimaryKey, DeriveRelation, EntityTrait,
EnumIter, PrimaryKeyTrait,
};
use super::super::Ipv4Address;
#[derive(Debug, Clone, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "ipv4address")]
struct Model {
#[sea_orm(primary_key, auto_increment = false)]
id: u8,
#[sea_orm(primary_key, auto_increment = false)]
addr: Ipv4Address,
nullable: Option<Ipv4Address>,
}
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
impl ActiveModelBehavior for ActiveModel {}
#[derive(Debug, EnumIter, DeriveRelation)]
enum Relation {}
}
mod ipv6address {
use sea_orm::{
ActiveModelBehavior, DeriveEntityModel, DerivePrimaryKey, DeriveRelation, EntityTrait,
EnumIter, PrimaryKeyTrait,
};
use super::super::Ipv6Address;
#[derive(Debug, Clone, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "ipv6address")]
struct Model {
#[sea_orm(primary_key, auto_increment = false)]
id: u8,
#[sea_orm(primary_key, auto_increment = false)]
addr: Ipv6Address,
nullable: Option<Ipv6Address>,
}
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
impl ActiveModelBehavior for ActiveModel {}
#[derive(Debug, EnumIter, DeriveRelation)]
enum Relation {}
}
}
@@ -1,4 +1,4 @@
//! [Path] and [PathBuf] utilities
//! [Path] and [`PathBuf`] utilities.
use std::ffi::OsString;
use std::path::{Path, PathBuf};
@@ -9,36 +9,44 @@ use std::os::unix::ffi::OsStringExt as _;
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};
use sea_orm::{ColIdx, ColumnType, DbErr, QueryResult, TryFromU64, TryGetError, TryGetable, Value};
/// The internal representation used by the database.
type SeaOrmRepr = Vec<u8>;
/// Wrapper around [PathBuf] to store paths as bytes in SeaORM
/// 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)]
#[expect(clippy::exhaustive_structs, reason = "unlikely to change")]
#[expect(clippy::module_name_repetitions, reason = "Bytes wouldn't make sense")]
pub struct PathBytes(pub PathBuf);
impl From<PathBuf> for PathBytes {
#[inline]
fn from(value: PathBuf) -> Self {
Self(value)
}
}
impl From<PathBytes> for PathBuf {
#[inline]
fn from(value: PathBytes) -> Self {
value.0
}
}
impl AsRef<Path> for PathBytes {
#[inline]
fn as_ref(&self) -> &Path {
&self.0
}
}
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
impl ValueType for PathBytes {
#[inline]
fn try_from(v: Value) -> Result<Self, ValueTypeErr> {
<SeaOrmRepr as ValueType>::try_from(v)
.map(OsString::from_vec)
@@ -46,26 +54,32 @@ impl ValueType for PathBytes {
.map(Self)
}
#[inline]
fn type_name() -> String {
core::any::type_name::<PathBuf>().to_string()
core::any::type_name::<Self>().to_owned()
}
#[inline]
fn array_type() -> ArrayType {
SeaOrmRepr::array_type()
}
#[inline]
fn column_type() -> ColumnType {
SeaOrmRepr::column_type()
}
}
impl From<PathBytes> for Value {
#[inline]
fn from(value: PathBytes) -> Self {
value.0.into_os_string().into_vec().into()
}
}
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
impl TryGetable for PathBytes {
#[inline]
fn try_get_by<I: ColIdx>(res: &QueryResult, index: I) -> Result<Self, TryGetError> {
SeaOrmRepr::try_get_by(res, index)
.map(OsString::from_vec)
@@ -74,7 +88,18 @@ impl TryGetable for PathBytes {
}
}
impl TryFromU64 for PathBytes {
#[inline]
fn try_from_u64(n: u64) -> Result<Self, DbErr> {
SeaOrmRepr::try_from_u64(n)
.map(OsString::from_vec)
.map(PathBuf::from)
.map(Self)
}
}
impl Nullable for PathBytes {
#[inline]
fn null() -> Value {
SeaOrmRepr::null()
}
@@ -89,19 +114,19 @@ mod tests {
use super::PathBytes;
#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "paths")]
pub struct Model {
struct Model {
#[sea_orm(primary_key, auto_increment = false)]
id: u8,
#[sea_orm(primary_key, auto_increment = false)]
path: PathBytes,
nullable: Option<PathBytes>,
}
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
impl ActiveModelBehavior for ActiveModel {}
#[allow(dead_code)]
#[derive(Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
enum Relation {}
}
@@ -1,4 +1,4 @@
//! Helpers for working with SeaORM
//! Helpers for working with `SeaORM`.
mod upsert;
pub use upsert::UpsertTrait;
+66
View File
@@ -0,0 +1,66 @@
//! Provides [`UpsertTrait`] for performing an upsert into a database.
use sea_orm::sea_query::{IntoColumnRef as _, OnConflict};
use sea_orm::{ActiveModelTrait, EntityTrait, Insert, Iterable as _};
/// This trait add a method on [Insert] to allow for upsert behavior.
pub trait UpsertTrait: private::Sealed {
/// Set ON CONFLICT on primary key to update all other columns.
#[must_use]
fn on_conflict_upsert(self) -> Self;
}
/// Returns an [Iterator] over the `PrimaryKey` columns of an [`ActiveModelTrait`].
fn primary_key_iter<A: ActiveModelTrait>()
-> impl Iterator<Item = <A::Entity as EntityTrait>::PrimaryKey> {
<A::Entity as EntityTrait>::PrimaryKey::iter()
}
/// Returns an [Iterator] over the [`EntityTrait`] columns of an [`ActiveModelTrait`].
fn column_iter<A: ActiveModelTrait>() -> impl Iterator<Item = <A::Entity as EntityTrait>::Column> {
<<A as ActiveModelTrait>::Entity as EntityTrait>::Column::iter()
}
impl<A: ActiveModelTrait> private::Sealed for Insert<A> {}
impl<A: ActiveModelTrait> UpsertTrait for Insert<A> {
#[inline]
fn on_conflict_upsert(self) -> Self {
self.on_conflict(
OnConflict::columns(primary_key_iter::<A>())
.update_columns(column_iter::<A>().filter(|col| {
!primary_key_iter::<A>().any(|pk| pk.into_column_ref() == col.into_column_ref())
}))
.to_owned(),
)
}
}
/// Private module.
mod private {
/// Sealed trait to prevent implementations of `UpsertTrait`.
#[expect(unnameable_types, reason = "sealed trait")]
pub trait Sealed {}
}
#[cfg(test)]
mod tests {
use super::{UpsertTrait, private::Sealed};
#[test]
fn seal() {
struct S;
impl Sealed for S {}
impl UpsertTrait for S {
fn on_conflict_upsert(self) -> Self {
self
}
}
fn is_sealed<T: Sealed>() {}
fn is_upsert<T: UpsertTrait>() {}
is_sealed::<S>();
is_upsert::<S>();
}
}
@@ -1,4 +1,4 @@
//! Helpers for defining SeaORM schemas
//! Helpers for defining `SeaORM` schemas.
#[cfg(feature = "sqlite")]
mod sqlite;
+505
View File
@@ -0,0 +1,505 @@
//! Helper functions for declaring `SQLite` schemas.
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)]`.
///
/// When using the new entity format:
///
/// use sea_orm::entity::prelude::*;
/// use seamantic::model::id::Id;
///
/// #[sea_orm::model]
/// #[derive(Debug, Clone, DeriveEntityModel)]
/// #[sea_orm(table_name = "rowid_test")]
/// pub struct Model {
/// #[sea_orm(primary_key, auto_increment = false)]
/// id: Id<Model>,
/// }
/// impl ActiveModelBehavior for ActiveModel {}
#[inline]
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.
///
/// When using the new entity format:
///
/// use sea_orm::entity::prelude::*;
/// use seamantic::model::id::Id;
///
/// #[sea_orm::model]
/// #[derive(Debug, Clone, DeriveEntityModel)]
/// #[sea_orm(table_name = "nocase_test")]
/// pub struct Model {
/// #[sea_orm(primary_key, auto_increment = false)]
/// id: i64,
/// #[sea_orm(extra = "COLLATE NOCASE")]
/// nocase: String,
/// }
/// impl ActiveModelBehavior for ActiveModel {}
#[inline]
pub fn sqlite_case_insensitive_string<T: IntoIden>(name: T) -> ColumnDef {
string(name).extra("COLLATE NOCASE").take()
}
#[cfg(test)]
mod entity_tests {
use sea_orm::{ConnectOptions, Database, DatabaseConnection};
/// Create and initialize a new in-memory SQLite database.
///
/// # Panics
/// If the database driver fails to connect, or if the schema registry fails
/// to sync the database schema.
async fn new_initialized_memory_db() -> DatabaseConnection {
let options = ConnectOptions::new("sqlite::memory:");
let db = Database::connect(options)
.await
.expect("Database::connect()");
db.get_schema_registry("seamantic::schema::sqlite::*")
.sync(&db)
.await
.expect("db.get_schema_registry().sync()");
db
}
#[expect(
clippy::unused_async_trait_impl,
reason = "sea_orm::model generates unpolled futures"
)]
#[expect(
clippy::same_name_method,
reason = "sea_orm::model generates the same method names"
)]
#[expect(
clippy::allow_attributes,
reason = "under different conditions sea_orm::model generates different code"
)]
#[allow(
clippy::future_not_send,
reason = "sea_orm::model generates un-send futures"
)]
mod rowid_test {
use sea_orm::ActiveValue::{NotSet, Set};
use sea_orm::entity::prelude::*;
use crate::model::id::{Id, SeaOrmRepr};
#[sea_orm::model]
#[derive(Debug, Clone, DeriveEntityModel)]
#[sea_orm(table_name = "rowid_test")]
struct Model {
#[sea_orm(primary_key, auto_increment = false)]
id: Id<Model>,
}
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
impl ActiveModelBehavior for ActiveModel {}
#[cfg(not(miri))]
#[tokio::test]
#[expect(clippy::missing_panics_doc, reason = "unit test")]
#[expect(clippy::default_numeric_fallback, reason = "numbers are not ambiguous")]
async fn sqlite_rowid_alias() {
let db = super::new_initialized_memory_db().await;
// 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);
}
}
}
#[expect(
clippy::unused_async_trait_impl,
reason = "sea_orm::model generates unpolled futures"
)]
#[expect(
clippy::same_name_method,
reason = "sea_orm::model generates the same method names"
)]
#[expect(
clippy::allow_attributes,
reason = "under different conditions sea_orm::model generates different code"
)]
#[allow(
clippy::future_not_send,
reason = "sea_orm::model generates un-send futures"
)]
mod nocase_test {
use sea_orm::ActiveValue::Set;
use sea_orm::entity::prelude::*;
#[sea_orm::model]
#[derive(Debug, Clone, DeriveEntityModel)]
#[sea_orm(table_name = "nocase_test")]
struct Model {
#[sea_orm(primary_key, auto_increment = false)]
id: i64,
case: String,
#[sea_orm(extra = "COLLATE NOCASE")]
nocase: String,
}
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
impl ActiveModelBehavior for ActiveModel {}
#[cfg(not(miri))]
#[tokio::test]
#[expect(clippy::missing_panics_doc, reason = "unit test")]
async fn sqlite_case_insensitive_string() {
let db = super::new_initialized_memory_db().await;
let i = 50;
// Insert a lowercase string
{
let model = ActiveModel {
id: Set(i),
case: Set("abcd".to_owned()),
nocase: Set("abcd".to_owned()),
};
drop(model.insert(&db).await.expect("insert"));
}
// Query by uppercase string
{
Entity::find()
.filter(Column::Case.eq("ABCD"))
.one(&db)
.await
.expect("find by case insensitive string")
.ok_or(())
.expect_err("find by case insensitive string");
let model = Entity::find()
.filter(Column::Nocase.eq("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.nocase, "abcd");
}
}
}
}
#[cfg(test)]
mod tests {
use sea_orm::ActiveValue::{NotSet, Set};
use sea_orm::entity::prelude::*;
use sea_orm::{ConnectOptions, Database};
use sea_orm_migration::async_trait::async_trait;
use sea_orm_migration::prelude::*;
use crate::model::id::{Id, SeaOrmRepr};
use super::{sqlite_case_insensitive_string, sqlite_rowid_alias};
#[expect(clippy::missing_panics_doc, reason = "unit test")]
async fn new_memory_db() -> DatabaseConnection {
let options = ConnectOptions::new("sqlite::memory:");
Database::connect(options).await.expect("Database::connect")
}
#[cfg(not(miri))]
#[tokio::test]
#[expect(unused_qualifications, reason = "derive macro qualifies everything")]
#[expect(clippy::missing_panics_doc, reason = "unit test")]
#[expect(clippy::default_numeric_fallback, reason = "numbers are not ambiguous")]
#[expect(clippy::redundant_test_prefix, reason = "name collision")]
async fn test_sqlite_rowid_alias() {
struct Migrator;
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
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]
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
impl MigrationTrait for Migration {
#[expect(
elided_lifetimes_in_paths,
reason = "async_trait causes lifetimes to be strange"
)]
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")]
#[expect(clippy::use_self, reason = "derive macros cause Self to be invalid")]
struct Model {
#[sea_orm(primary_key, auto_increment = false)]
id: Id<Model>,
}
#[derive(Debug, EnumIter)]
enum Relation {}
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
impl RelationTrait for Relation {
#[expect(clippy::panic, reason = "no relation")]
fn def(&self) -> RelationDef {
panic!("No RelationDef")
}
}
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
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);
}
}
#[cfg(not(miri))]
#[tokio::test]
#[expect(unused_qualifications, reason = "derive macro qualifies everything")]
#[expect(clippy::missing_panics_doc, reason = "unit test")]
#[expect(clippy::redundant_test_prefix, reason = "name collision")]
async fn test_sqlite_case_insensitive_string() {
struct Migrator;
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
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]
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
impl MigrationTrait for Migration {
#[expect(
elided_lifetimes_in_paths,
reason = "async_trait causes lifetimes to be strange"
)]
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")]
struct Model {
#[sea_orm(primary_key, auto_increment = false)]
id: i64,
ci_str: String,
}
#[derive(Debug, EnumIter)]
enum Relation {}
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
impl RelationTrait for Relation {
#[expect(clippy::panic, reason = "no relation")]
fn def(&self) -> RelationDef {
panic!("No RelationDef")
}
}
#[expect(clippy::missing_trait_methods, reason = "accept default methods")]
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()),
};
drop(model.insert(&db).await.expect("insert"));
}
// Query by uppercase string
{
let model = Entity::find()
.filter(Column::CiStr.eq("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");
}
}
}
+43
View File
@@ -0,0 +1,43 @@
set minimum-version := '1.55.0'
set default-list
alias update := update-min
all:
just all-min
just all-max
all-min: update-min build lint test example doc format
all-max: update-max build lint test example doc format
update-min:
cargo +nightly update -Z direct-minimal-versions
update-max:
cargo update
build:
cargo hack --feature-powerset build
lint:
cargo hack --feature-powerset clippy -- -D warnings
cargo hack --feature-powerset clippy --tests -- -D warnings
test:
cargo hack --feature-powerset test
cargo +1.94 hack --feature-powerset test
cargo +nightly hack --feature-powerset miri test
example:
cargo run --example=migrations --features=sqlite
doc *args:
RUSTDOCFLAGS="--cfg docsrs -D warnings" cargo +nightly doc \
--workspace --all-features --document-private-items \
--lib --bins --no-deps --locked \
{{ args }}
format:
cargo fmt --check
install:
cargo install --path crates/lint-policy-cli
+1 -1
View File
@@ -1,2 +1,2 @@
[toolchain]
channel = "stable"
channel = "1.98"
-5
View File
@@ -1,5 +0,0 @@
//! SeaORM column types for enforcing data consistency
pub mod duration;
pub mod id;
pub mod path;
-34
View File
@@ -1,34 +0,0 @@
use sea_orm::sea_query::{IntoColumnRef, OnConflict};
use sea_orm::{ActiveModelTrait, EntityTrait, Insert, Iterable};
/// This trait add a method on [Insert] to allow for upsert behavior
pub trait UpsertTrait: private::Sealed {
/// Set ON CONFLICT on primary key to update all other columns
fn on_conflict_upsert(self) -> Self;
}
fn primary_key_iter<A: ActiveModelTrait>()
-> impl Iterator<Item = <A::Entity as EntityTrait>::PrimaryKey> {
<A::Entity as EntityTrait>::PrimaryKey::iter()
}
fn column_iter<A: ActiveModelTrait>() -> impl Iterator<Item = <A::Entity as EntityTrait>::Column> {
<<A as ActiveModelTrait>::Entity as EntityTrait>::Column::iter()
}
impl<A: ActiveModelTrait> UpsertTrait for Insert<A> {
fn on_conflict_upsert(self) -> Self {
self.on_conflict(
OnConflict::columns(primary_key_iter::<A>())
.update_columns(column_iter::<A>().filter(|col| {
!primary_key_iter::<A>().any(|pk| pk.into_column_ref() == col.into_column_ref())
}))
.to_owned(),
)
}
}
mod private {
pub trait Sealed {}
impl<A: ::sea_orm::ActiveModelTrait> Sealed for ::sea_orm::Insert<A> {}
}
-241
View File
@@ -1,241 +0,0 @@
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");
}
}
}