Skip to content

Commit ca06ff2

Browse files
committed
Return owned Database handles and propagate errors
Refactor global DB access to return owned Database handles and make internal connection access fallible. Many call sites now propagate Result errors (use ?), and Database internal accessors (__internal_connection, __internal_backend, __get_connection) return Result to avoid silently bypassing transaction scopes or panicking on disconnected handles. TideConfig schema path storage was changed from &'static str to owned String to prevent leaks and allow safe reconfiguration, and TideConfig::schema_file_path() now returns Option<String>. Restored Model::to_hash_map() behavior that omits structured presenter `params` (documented and tested), added profiling short-circuit to skip Instant::now() when global profiling is disabled, and reworked SelfRefMany::load_tree() to fetch trees with a single recursive CTE (honors configured local_key). Updated docs and changelog and added/adjusted tests to cover schema path replacement, profiling, presenter serialization, self-ref SQL, and disconnected-handle behavior.
1 parent 918afbc commit ca06ff2

22 files changed

Lines changed: 580 additions & 181 deletions

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Fixed
1111

12+
- Restored transaction scoping for TideORM model and query helpers so `save()`, `update()`, `delete()`, eager loading, nested operations, full-text reads, and aggregate helpers now honor the active transaction instead of bypassing it through the global pooled connection.
13+
- Hardened raw SQL builder escape hatches by rejecting obvious injection markers in `where_raw`, raw column expressions, and nested subqueries before execution.
14+
- Restored the original `Model::to_hash_map()` behavior that hides structured presenter `params` payloads entirely; `params` remains a reserved presenter key and is now documented as such.
15+
- Removed disabled-path profiling overhead in `__profile_future()` by skipping `Instant::now()` when global profiling is off.
16+
- Replaced leaked schema-path storage in `TideConfig` so repeated `apply()` and `connect()` calls no longer leak each configured schema file path.
17+
- Reworked `SelfRefMany::load_tree()` to use a single recursive CTE query, eliminating per-node descendant lookups and honoring the configured `local_key` when walking self-referential trees.
1218
- Repaired all-features build breakage after the reconfigurable global-database refactor by updating direct SeaORM call sites to borrow owned internal connections correctly.
1319
- Restored consistent full-text SQL parameterization coverage across the query and full-text test suites, including PostgreSQL ranked search placeholders and SQLite FTS pagination bindings.
1420
- Tightened encrypted-field missing-key coverage so integration tests now assert the actionable startup-configuration error message returned by `Encrypted<T>`.
1521

1622
### Changed
1723

24+
- `require_db()`, `try_db()`, `TideConfig::db()`, `TideConfig::try_db()`, `Model::db()`, and `Model::database()` now return owned `Database` handles for consistency with transaction-aware current-connection access.
25+
- `TideConfig::schema_file_path()` now returns `Option<String>` instead of `Option<&'static str>` so schema path state can be replaced safely without leaking memory across reconfiguration.
1826
- Documented resettable global configuration, tokenization override reset behavior, and the batched nested many-model save/update/delete paths in the README and mdBook chapters.
1927

2028
### Internal

docs/models.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@ The `#[tideorm::model]` macro automatically implements:
2525
- `Serialize` - for JSON serialization
2626
- `Deserialize` - for JSON deserialization
2727

28+
### Reserved Attribute Names
29+
30+
`params` is reserved for presenter payloads.
31+
32+
When TideORM builds `to_hash_map()` output and the serialized `params` value is
33+
an object or array, it is omitted from the resulting map. Avoid using `params`
34+
for presenter-facing structured model attributes if you need that data to appear
35+
in `to_hash_map()` output.
36+
2837
### Custom Implementations (When Needed)
2938

3039
If you need custom implementations, use `skip_derives` and provide your own:
@@ -860,12 +869,15 @@ let has_manager = manager_rel.exists().await?;
860869
let reports = reports_rel.load().await?;
861870
let count = reports_rel.count().await?;
862871

863-
// Load entire subtree recursively
872+
// Load entire subtree recursively in one recursive CTE query
864873
let tree = reports_rel.load_tree(3).await?; // 3 levels deep
865874
```
866875

867876
Note: `SelfRef` and `SelfRefMany` are runtime wrappers today. The derive macro does not yet provide `#[tideorm(self_ref = ...)]` or `#[tideorm(self_ref_many = ...)]` field wiring.
868877

878+
`SelfRefMany::load_tree()` respects the configured `local_key` and fetches the
879+
tree in one query, which avoids one SELECT per node on large hierarchies.
880+
869881
### Nested Save (Cascade Operations)
870882

871883
Save parent and related models together with automatic foreign key handling:

src/config.rs

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ use crate::tide_info;
3838
static GLOBAL_CONFIG: OnceLock<RwLock<Config>> = OnceLock::new();
3939

4040
/// Global schema file path (set via TideConfig::schema_file())
41-
static SCHEMA_FILE_PATH: OnceLock<RwLock<Option<&'static str>>> = OnceLock::new();
41+
static SCHEMA_FILE_PATH: OnceLock<RwLock<Option<String>>> = OnceLock::new();
4242

4343
/// File URL generator function type
4444
///
@@ -66,7 +66,7 @@ thread_local! {
6666
static LOCAL_CONFIG: RefCell<Option<Config>> = const { RefCell::new(None) };
6767
static LOCAL_DB_TYPE: Cell<Option<DatabaseType>> = const { Cell::new(None) };
6868
static LOCAL_POOL_CONFIG: RefCell<Option<PoolConfig>> = const { RefCell::new(None) };
69-
static LOCAL_SCHEMA_FILE_PATH: Cell<Option<&'static str>> = const { Cell::new(None) };
69+
static LOCAL_SCHEMA_FILE_PATH: RefCell<Option<String>> = const { RefCell::new(None) };
7070
#[cfg(feature = "attachments")]
7171
static LOCAL_FILE_URL_GENERATOR: Cell<Option<FileUrlGenerator>> = const { Cell::new(None) };
7272
}
@@ -79,7 +79,7 @@ fn global_pool_config_state() -> &'static RwLock<Option<PoolConfig>> {
7979
GLOBAL_POOL_CONFIG.get_or_init(|| RwLock::new(None))
8080
}
8181

82-
fn global_schema_file_path_state() -> &'static RwLock<Option<&'static str>> {
82+
fn global_schema_file_path_state() -> &'static RwLock<Option<String>> {
8383
SCHEMA_FILE_PATH.get_or_init(|| RwLock::new(None))
8484
}
8585

@@ -88,10 +88,6 @@ fn global_file_url_generator_state() -> &'static RwLock<Option<FileUrlGenerator>
8888
GLOBAL_FILE_URL_GENERATOR.get_or_init(|| RwLock::new(None))
8989
}
9090

91-
fn leak_schema_path(path: String) -> &'static str {
92-
Box::leak(path.into_boxed_str())
93-
}
94-
9591
/// Supported database types
9692
///
9793
/// TideORM supports multiple database backends. Each has its own
@@ -1363,15 +1359,14 @@ impl TideConfig {
13631359
// Generate schema file if configured
13641360
if let Some(path) = &self.schema_file {
13651361
// Store schema path
1366-
let leaked_path = leak_schema_path(path.clone());
1367-
*global_schema_file_path_state().write() = Some(leaked_path);
1368-
LOCAL_SCHEMA_FILE_PATH.with(|slot| slot.set(Some(leaked_path)));
1362+
*global_schema_file_path_state().write() = Some(path.clone());
1363+
LOCAL_SCHEMA_FILE_PATH.with(|slot| *slot.borrow_mut() = Some(path.clone()));
13691364

13701365
// Auto-generate schema file from database introspection
13711366
crate::schema::SchemaWriter::write_schema(path).await?;
13721367
} else {
13731368
*global_schema_file_path_state().write() = None;
1374-
LOCAL_SCHEMA_FILE_PATH.with(|slot| slot.set(None));
1369+
LOCAL_SCHEMA_FILE_PATH.with(|slot| slot.borrow_mut().take());
13751370
}
13761371

13771372
Ok(db_ref)
@@ -1402,9 +1397,8 @@ impl TideConfig {
14021397
*global_pool_config_state().write() = Some(self.pool.clone());
14031398
LOCAL_POOL_CONFIG.with(|slot| *slot.borrow_mut() = Some(self.pool));
14041399

1405-
let leaked_path = self.schema_file.map(leak_schema_path);
1406-
*global_schema_file_path_state().write() = leaked_path;
1407-
LOCAL_SCHEMA_FILE_PATH.with(|slot| slot.set(leaked_path));
1400+
*global_schema_file_path_state().write() = self.schema_file.clone();
1401+
LOCAL_SCHEMA_FILE_PATH.with(|slot| *slot.borrow_mut() = self.schema_file);
14081402
}
14091403

14101404
/// Reset global and current-thread TideORM configuration state.
@@ -1420,7 +1414,7 @@ impl TideConfig {
14201414
LOCAL_POOL_CONFIG.with(|slot| slot.borrow_mut().take());
14211415

14221416
*global_schema_file_path_state().write() = None;
1423-
LOCAL_SCHEMA_FILE_PATH.with(|slot| slot.set(None));
1417+
LOCAL_SCHEMA_FILE_PATH.with(|slot| slot.borrow_mut().take());
14241418

14251419
#[cfg(feature = "attachments")]
14261420
{
@@ -1442,7 +1436,7 @@ impl TideConfig {
14421436
/// ```rust,ignore
14431437
/// let db = TideConfig::db()?;
14441438
/// ```
1445-
pub fn db() -> crate::error::Result<&'static Database> {
1439+
pub fn db() -> crate::error::Result<Database> {
14461440
crate::database::require_db()
14471441
}
14481442

@@ -1457,7 +1451,7 @@ impl TideConfig {
14571451
/// // use db...
14581452
/// }
14591453
/// ```
1460-
pub fn try_db() -> Option<&'static Database> {
1454+
pub fn try_db() -> Option<Database> {
14611455
crate::database::try_db()
14621456
}
14631457

@@ -1536,11 +1530,14 @@ impl TideConfig {
15361530
.unwrap_or_default()
15371531
}
15381532

1539-
/// Get the configured schema file path (if any)
1540-
pub fn schema_file_path() -> Option<&'static str> {
1533+
/// Get the configured schema file path (if any).
1534+
///
1535+
/// Returns an owned path so repeated `apply()` or `connect()` calls can
1536+
/// replace the stored schema path without leaking memory.
1537+
pub fn schema_file_path() -> Option<String> {
15411538
LOCAL_SCHEMA_FILE_PATH
1542-
.with(|slot| slot.get())
1543-
.or_else(|| *global_schema_file_path_state().read())
1539+
.with(|slot| slot.borrow().clone())
1540+
.or_else(|| global_schema_file_path_state().read().clone())
15441541
}
15451542

15461543
/// Write schema to the configured file
@@ -1605,7 +1602,7 @@ impl TideConfig {
16051602
async fn detect_server_version(db: &Database) -> Result<String> {
16061603
use crate::internal::{ConnectionTrait, DbBackend, Statement};
16071604

1608-
let conn = db.__internal_connection();
1605+
let conn = db.__internal_connection()?;
16091606
let backend = conn.get_database_backend();
16101607

16111608
// Only probe MySQL-type connections

0 commit comments

Comments
 (0)