Skip to content

Commit d283af5

Browse files
committed
Use shared DB handles; fix model serialization
Refactor database handling to use a shared internal DatabaseHandle in thread-local scope and resolve the active connection/backend directly from the current scope. Harden transaction semantics (consistent failure on leaked handles) and rename init helper usage in docs/examples (Database::init). Replace previous no-op serialization stubs for translations and file attachments with real implementations that operate on the model state and return explicit errors for unsupported operations (e.g. loading all translations into scalar fields). Update call sites (queries, fulltext, eager loading, nested upserts, generated macro code) to use the shared internal connection and conn.connection() where appropriate. Add tests for translation and file attachment round-trips and refresh docs/README and macros crate version to 0.8.4.
1 parent 5f07e8e commit d283af5

17 files changed

Lines changed: 327 additions & 142 deletions

CHANGELOG.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,26 @@ All notable changes to TideORM will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.8.4] - 2026-03-20
9+
10+
### Fixed
11+
12+
- Replaced silent translation and file-attachment serialization stubs so `Model::load_language_translations()`, `Model::get_files_attribute()`, and `Model::set_files_attribute()` now operate on the model state instead of succeeding without effect.
13+
- Made `Model::load_all_translations()` fail loudly with a clear unsupported error instead of silently pretending to load all translations into scalar model fields.
14+
- Hardened `Database::transaction()` so leaked transaction handles now fail consistently on both commit and rollback paths instead of silently relying on drop-based rollback in the error path.
15+
- Ensured `NestedSaveBuilder::save()` persists related models with the parent foreign key instead of returning only FK-patched JSON payloads.
16+
17+
### Changed
18+
19+
- Reduced database-access overhead on model hot paths by resolving the current connection/backend directly from the active scope instead of repeatedly cloning the outer `Database` wrapper.
20+
- Changed transaction-scoped thread-local overrides to store `DatabaseHandle` directly and updated `ConnectionRef::Database` to carry the shared internal connection handle instead of cloning `DatabaseConnection` per lookup.
21+
- Refreshed public docs and examples to use the current global-database initialization API and the 0.8.4 crate version.
22+
23+
### Internal
24+
25+
- Kept the workspace warning-free after the connection-handle refactor by updating generated macro code, full-text execution paths, eager loading, nested bulk upserts, and query helpers to use shared internal connections correctly.
26+
- Verified the release with `cargo test --lib` and `cargo clippy --workspace --all-targets -- -D warnings`.
27+
828
## [0.8.1] - 2026-03-18
929

1030
### Fixed
@@ -551,7 +571,8 @@ This is the first public release of TideORM, a developer-friendly ORM for Rust w
551571
- **Repository:** [https://github.com/mohamadzoh/tideorm](https://github.com/mohamadzoh/tideorm)
552572
- **Documentation:** See README.md and examples/
553573

554-
[Unreleased]: https://github.com/mohamadzoh/tideorm/compare/v0.8.1...HEAD
574+
[Unreleased]: https://github.com/mohamadzoh/tideorm/compare/v0.8.4...HEAD
575+
[0.8.4]: https://github.com/mohamadzoh/tideorm/compare/v0.8.1...v0.8.4
555576
[0.8.1]: https://github.com/mohamadzoh/tideorm/compare/v0.8.0...v0.8.1
556577
[0.8.0]: https://github.com/mohamadzoh/tideorm/compare/v0.7.3...v0.8.0
557578
[0.7.3]: https://github.com/mohamadzoh/tideorm/compare/v0.7.2...v0.7.3

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "tideorm"
3-
version = "0.8.3"
3+
version = "0.8.4"
44
edition = "2024"
55
authors = ["Mohamad Al Zohbie <alzoubi528@gmail.com>"]
66
description = "A developer-friendly ORM for Rust with clean, expressive syntax"
@@ -48,7 +48,7 @@ rust_decimal = { version = "1.40.0", features = ["serde"] }
4848
thiserror = "2.0.18"
4949

5050
# Derive macros (our own crate)
51-
tideorm-macros = { version = "0.8.1", path = "tideorm-macros" }
51+
tideorm-macros = { version = "0.8.4", path = "tideorm-macros" }
5252

5353
# Utils
5454
parking_lot = "0.12.5"

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -192,22 +192,22 @@ let recent_posts = user.posts.load_with(|q| {
192192
```toml
193193
[dependencies]
194194
# PostgreSQL (default)
195-
tideorm = { version = "0.8.1", features = ["postgres"] }
195+
tideorm = { version = "0.8.4", features = ["postgres"] }
196196

197197
# MySQL
198-
tideorm = { version = "0.8.1", features = ["mysql"] }
198+
tideorm = { version = "0.8.4", features = ["mysql"] }
199199

200200
# SQLite
201-
tideorm = { version = "0.8.1", features = ["sqlite"] }
201+
tideorm = { version = "0.8.4", features = ["sqlite"] }
202202

203203
# Enable attachments support explicitly
204-
tideorm = { version = "0.8.1", features = ["postgres", "attachments"] }
204+
tideorm = { version = "0.8.4", features = ["postgres", "attachments"] }
205205

206206
# Enable translations support explicitly
207-
tideorm = { version = "0.8.1", features = ["postgres", "translations"] }
207+
tideorm = { version = "0.8.4", features = ["postgres", "translations"] }
208208

209209
# Enable full-text search support explicitly
210-
tideorm = { version = "0.8.1", features = ["postgres", "fulltext"] }
210+
tideorm = { version = "0.8.4", features = ["postgres", "fulltext"] }
211211
```
212212

213213
### Feature Flags

docs/queries.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -514,7 +514,7 @@ TideORM provides full-text search capabilities across PostgreSQL (tsvector/tsque
514514
Enable the feature explicitly when you need the full-text search API:
515515

516516
```toml
517-
tideorm = { version = "0.8.1", features = ["postgres", "fulltext"] }
517+
tideorm = { version = "0.8.4", features = ["postgres", "fulltext"] }
518518
```
519519

520520
### Search Basics

docs/relations.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ Enable the feature first:
210210

211211
```toml
212212
[dependencies]
213-
tideorm = { version = "0.8.1", features = ["postgres", "attachments"] }
213+
tideorm = { version = "0.8.4", features = ["postgres", "attachments"] }
214214
```
215215

216216
### Model Setup
@@ -545,7 +545,7 @@ Enable the feature first:
545545

546546
```toml
547547
[dependencies]
548-
tideorm = { version = "0.8.1", features = ["postgres", "translations"] }
548+
tideorm = { version = "0.8.4", features = ["postgres", "translations"] }
549549
```
550550

551551
### Model Setup

src/database.rs

Lines changed: 72 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
//!
3636
//! ```rust,ignore
3737
//! // Initialize global connection (call once at startup)
38-
//! Database::connect_global("postgres://localhost/myapp").await?;
38+
//! Database::init("postgres://localhost/myapp").await?;
3939
//!
4040
//! // Now models can use the global connection automatically
4141
//! let user = User {
@@ -66,7 +66,7 @@ use crate::tide_warn;
6666
static GLOBAL_DB: OnceLock<Database> = OnceLock::new();
6767

6868
thread_local! {
69-
static THREAD_DB_OVERRIDE: RefCell<Option<Database>> = const { RefCell::new(None) };
69+
static THREAD_DB_OVERRIDE: RefCell<Option<DatabaseHandle>> = const { RefCell::new(None) };
7070
}
7171

7272
#[derive(Clone)]
@@ -86,7 +86,7 @@ fn panic_missing_global_db(message: &str) -> ! {
8686
/// Get a reference to the global database connection
8787
///
8888
/// This function returns the global database connection that was initialized
89-
/// with `Database::connect_global()` or `Database::set_global()`.
89+
/// with `Database::init()` or `Database::set_global()`.
9090
///
9191
/// # Panics
9292
///
@@ -96,7 +96,7 @@ fn panic_missing_global_db(message: &str) -> ! {
9696
/// # Example
9797
///
9898
/// ```rust,ignore
99-
/// // After initializing with connect_global()
99+
/// // After initializing with Database::init()
100100
/// let users = User::all().await?;
101101
/// ```
102102
pub fn db() -> &'static Database {
@@ -159,13 +159,39 @@ pub fn has_global_db() -> bool {
159159

160160
#[doc(hidden)]
161161
pub fn __current_db() -> Result<Database> {
162-
if let Some(db) = THREAD_DB_OVERRIDE.with(|slot| slot.borrow().clone()) {
163-
return Ok(db);
162+
if let Some(handle) = THREAD_DB_OVERRIDE.with(|slot| slot.borrow().clone()) {
163+
return Ok(Database::from_handle(handle));
164164
}
165165

166166
require_db()
167167
}
168168

169+
fn current_scope_handle() -> Result<DatabaseHandle> {
170+
if let Some(handle) = THREAD_DB_OVERRIDE.with(|slot| slot.borrow().clone()) {
171+
return Ok(handle);
172+
}
173+
174+
global_db_handle().current_handle()
175+
}
176+
177+
#[doc(hidden)]
178+
pub fn __current_connection() -> Result<ConnectionRef> {
179+
Ok(match current_scope_handle()? {
180+
DatabaseHandle::Connection(inner) => ConnectionRef::Database(inner),
181+
DatabaseHandle::Transaction(tx) => ConnectionRef::Transaction(tx),
182+
})
183+
}
184+
185+
#[doc(hidden)]
186+
pub fn __current_backend() -> Result<crate::internal::DbBackend> {
187+
use crate::internal::ConnectionTrait;
188+
189+
Ok(match current_scope_handle()? {
190+
DatabaseHandle::Connection(inner) => inner.connection().get_database_backend(),
191+
DatabaseHandle::Transaction(tx) => tx.as_ref().get_database_backend(),
192+
})
193+
}
194+
169195
/// Database connection handle
170196
///
171197
/// This is the main entry point for all database operations in TideORM.
@@ -187,18 +213,14 @@ impl Database {
187213
}
188214
}
189215

190-
fn from_internal_connection(inner: InternalConnection) -> Self {
216+
fn from_handle(handle: DatabaseHandle) -> Self {
191217
Self {
192-
inner: Arc::new(RwLock::new(Some(DatabaseHandle::Connection(Arc::new(
193-
inner,
194-
))))),
218+
inner: Arc::new(RwLock::new(Some(handle))),
195219
}
196220
}
197221

198-
fn from_internal_transaction(inner: Arc<crate::internal::DatabaseTransaction>) -> Self {
199-
Self {
200-
inner: Arc::new(RwLock::new(Some(DatabaseHandle::Transaction(inner)))),
201-
}
222+
fn from_internal_connection(inner: InternalConnection) -> Self {
223+
Self::from_handle(DatabaseHandle::Connection(Arc::new(inner)))
202224
}
203225

204226
fn current_handle(&self) -> Result<DatabaseHandle> {
@@ -229,12 +251,12 @@ impl Database {
229251
self.inner.write().take();
230252
}
231253

232-
fn replace_thread_override(db: Option<Self>) -> Option<Self> {
233-
THREAD_DB_OVERRIDE.with(|slot| slot.replace(db))
254+
fn replace_thread_override(handle: Option<DatabaseHandle>) -> Option<DatabaseHandle> {
255+
THREAD_DB_OVERRIDE.with(|slot| slot.replace(handle))
234256
}
235257

236-
fn set_thread_override(db: Option<Self>) {
237-
THREAD_DB_OVERRIDE.with(|slot| *slot.borrow_mut() = db);
258+
fn set_thread_override(handle: Option<DatabaseHandle>) {
259+
THREAD_DB_OVERRIDE.with(|slot| *slot.borrow_mut() = handle);
238260
}
239261

240262
fn is_connected(&self) -> bool {
@@ -308,8 +330,8 @@ impl Database {
308330
pub fn set_global(db: Self) -> Result<&'static Self> {
309331
let inner = db.current_inner()?;
310332
let global = global_db_handle();
311-
global.replace_inner(inner);
312-
Self::set_thread_override(Some(db));
333+
global.replace_inner(inner.clone());
334+
Self::set_thread_override(Some(DatabaseHandle::Connection(inner)));
313335
Ok(global)
314336
}
315337

@@ -390,6 +412,7 @@ impl Database {
390412

391413
let txn = match self.__get_connection()? {
392414
ConnectionRef::Database(conn) => conn
415+
.connection()
393416
.begin()
394417
.await
395418
.map_err(|e| Error::transaction(e.to_string()))?,
@@ -403,7 +426,7 @@ impl Database {
403426
let txn = Arc::new(txn);
404427
let tx = Transaction { inner: txn.clone() };
405428
let previous_override =
406-
Self::replace_thread_override(Some(Self::from_internal_transaction(txn.clone())));
429+
Self::replace_thread_override(Some(DatabaseHandle::Transaction(txn.clone())));
407430

408431
let outcome = f(&tx).await;
409432

@@ -505,13 +528,12 @@ impl Database {
505528
pub async fn raw<T: crate::model::Model>(sql: &str) -> Result<Vec<T>> {
506529
use crate::internal::{ConnectionTrait, FromQueryResult, Statement};
507530

508-
let db = crate::database::__current_db()?;
509-
let backend = db.__internal_backend()?;
531+
let backend = crate::database::__current_backend()?;
510532
let stmt = Statement::from_string(backend, sql.to_string());
511533

512-
let results = match db.__get_connection()? {
534+
let results = match crate::database::__current_connection()? {
513535
ConnectionRef::Database(conn) => {
514-
crate::profiling::__profile_future(conn.query_all_raw(stmt)).await
536+
crate::profiling::__profile_future(conn.connection().query_all_raw(stmt)).await
515537
}
516538
ConnectionRef::Transaction(tx) => {
517539
crate::profiling::__profile_future(tx.as_ref().query_all_raw(stmt)).await
@@ -563,8 +585,12 @@ impl Database {
563585

564586
let results = match self.__get_connection()? {
565587
ConnectionRef::Database(conn) => {
566-
let stmt = Statement::from_sql_and_values(conn.get_database_backend(), sql, params);
567-
crate::profiling::__profile_future(conn.query_all_raw(stmt)).await
588+
let stmt = Statement::from_sql_and_values(
589+
conn.connection().get_database_backend(),
590+
sql,
591+
params,
592+
);
593+
crate::profiling::__profile_future(conn.connection().query_all_raw(stmt)).await
568594
}
569595
ConnectionRef::Transaction(tx) => {
570596
let stmt =
@@ -597,10 +623,9 @@ impl Database {
597623
pub async fn execute(sql: &str) -> Result<u64> {
598624
use crate::internal::ConnectionTrait;
599625

600-
let db = crate::database::__current_db()?;
601-
let result = match db.__get_connection()? {
626+
let result = match crate::database::__current_connection()? {
602627
ConnectionRef::Database(conn) => {
603-
crate::profiling::__profile_future(conn.execute_unprepared(sql)).await
628+
crate::profiling::__profile_future(conn.connection().execute_unprepared(sql)).await
604629
}
605630
ConnectionRef::Transaction(tx) => {
606631
crate::profiling::__profile_future(tx.as_ref().execute_unprepared(sql)).await
@@ -640,8 +665,12 @@ impl Database {
640665

641666
let result = match self.__get_connection()? {
642667
ConnectionRef::Database(conn) => {
643-
let stmt = Statement::from_sql_and_values(conn.get_database_backend(), sql, params);
644-
crate::profiling::__profile_future(conn.execute_raw(stmt)).await
668+
let stmt = Statement::from_sql_and_values(
669+
conn.connection().get_database_backend(),
670+
sql,
671+
params,
672+
);
673+
crate::profiling::__profile_future(conn.connection().execute_raw(stmt)).await
645674
}
646675
ConnectionRef::Transaction(tx) => {
647676
let stmt =
@@ -679,13 +708,12 @@ impl Database {
679708
pub async fn raw_json(sql: &str) -> Result<Vec<serde_json::Value>> {
680709
use crate::internal::{ConnectionTrait, Statement};
681710

682-
let db = crate::database::__current_db()?;
683-
let backend = db.__internal_backend()?;
711+
let backend = crate::database::__current_backend()?;
684712
let stmt = Statement::from_string(backend, sql.to_string());
685713

686-
let results = match db.__get_connection()? {
714+
let results = match crate::database::__current_connection()? {
687715
ConnectionRef::Database(conn) => {
688-
crate::profiling::__profile_future(conn.query_all_raw(stmt)).await
716+
crate::profiling::__profile_future(conn.connection().query_all_raw(stmt)).await
689717
}
690718
ConnectionRef::Transaction(tx) => {
691719
crate::profiling::__profile_future(tx.as_ref().query_all_raw(stmt)).await
@@ -716,8 +744,12 @@ impl Database {
716744

717745
let results = match self.__get_connection()? {
718746
ConnectionRef::Database(conn) => {
719-
let stmt = Statement::from_sql_and_values(conn.get_database_backend(), sql, params);
720-
crate::profiling::__profile_future(conn.query_all_raw(stmt)).await
747+
let stmt = Statement::from_sql_and_values(
748+
conn.connection().get_database_backend(),
749+
sql,
750+
params,
751+
);
752+
crate::profiling::__profile_future(conn.connection().query_all_raw(stmt)).await
721753
}
722754
ConnectionRef::Transaction(tx) => {
723755
let stmt =
@@ -1011,16 +1043,14 @@ pub trait Connection: Send + Sync {
10111043
/// Internal connection reference (hidden from users)
10121044
#[doc(hidden)]
10131045
pub enum ConnectionRef {
1014-
Database(crate::internal::DatabaseConnection),
1046+
Database(Arc<crate::internal::InternalConnection>),
10151047
Transaction(Arc<crate::internal::DatabaseTransaction>),
10161048
}
10171049

10181050
impl Connection for Database {
10191051
fn __get_connection(&self) -> Result<ConnectionRef> {
10201052
Ok(match self.current_handle()? {
1021-
DatabaseHandle::Connection(inner) => {
1022-
ConnectionRef::Database(inner.connection().clone())
1023-
}
1053+
DatabaseHandle::Connection(inner) => ConnectionRef::Database(inner),
10241054
DatabaseHandle::Transaction(tx) => ConnectionRef::Transaction(tx),
10251055
})
10261056
}

0 commit comments

Comments
 (0)