pub struct Store { /* private fields */ }Expand description
Async CRUD store backed by a single SQLite table.
The store wraps a rusqlite::Connection in an Arc<Mutex<_>> so it can
be cloned and shared across async tasks. rusqlite::Connection is
Send but !Sync; the Mutex provides the required exclusive access.
All database operations execute inside tokio::task::spawn_blocking so
the tokio runtime thread-pool is never blocked.
Implementations§
Source§impl Store
impl Store
Sourcepub async fn open(
db_path: &Path,
schema: SchemaConfig,
) -> Result<Self, MiniAppError>
pub async fn open( db_path: &Path, schema: SchemaConfig, ) -> Result<Self, MiniAppError>
Open the SQLite database at db_path and run CREATE TABLE IF NOT EXISTS rows.
§WAL journal mode
The connection is opened with PRAGMA journal_mode = WAL to enable safe
coexistence of old and new Store instances during schema hot-reload
(see crux-card.md Crux #1). WAL mode allows one writer and many readers
concurrently, preventing lock conflicts when dual registries are held.
Sidecar files <db>.db-wal and <db>.db-shm are created next to the
main DB file; this is expected and safe.
§Concurrency
Returns a Store that wraps Arc<Mutex<rusqlite::Connection>> and is
Send + Sync. rusqlite::Connection is Send but !Sync; the
std::sync::Mutex provides exclusive access. All subsequent CRUD calls
acquire the lock inside spawn_blocking closures and drop it before any
.await point — holding a MutexGuard across .await is never permitted.
If schema.dump.sync is Some(SyncMode::Bidirectional), a
tracing::warn! is emitted once here; the store behaves as write-only
until bidirectional sync is implemented.
§Cancel Safety
Not cancel-safe. Once the spawn_blocking closure has started (DDL
execution), calling abort on the JoinHandle or dropping the returned
Future has no effect — the DDL completes on the blocking thread pool.
§Errors
MiniAppError::Storage—Connection::open, WAL pragma, or DDL execute failure.MiniAppError::Schema— blocking thread panicked (JoinError).
§Panic
Does not panic.
Sourcepub fn db_path(&self) -> &Path
pub fn db_path(&self) -> &Path
Returns the filesystem path of the SQLite database file backing
this store, as captured at Store::open time.
Used by mini_app_core::aggregator::execute_aggregate to mount
each per-table .db file via ATTACH DATABASE for the
multi-table UNION ALL aggregation path (Crux #3).
Sourcepub fn conn(&self) -> Arc<Mutex<Connection>>
pub fn conn(&self) -> Arc<Mutex<Connection>>
Returns a clone of the Arc<Mutex<rusqlite::Connection>>
handle backing this store. Used by
crate::alias_storage::GlobalAliasStorage::migrate_from_per_table
to read the legacy per-table _aliases rows on registry mount.
The connection is shared (no copy); callers MUST acquire the
Mutex lock inside a spawn_blocking body to avoid blocking the
async runtime.
Sourcepub async fn create(&self, value: Value) -> Result<RowRecord, MiniAppError>
pub async fn create(&self, value: Value) -> Result<RowRecord, MiniAppError>
Validate value against the schema and insert a new row with a
generated UUID primary key.
§Concurrency
The rusqlite INSERT executes inside tokio::task::spawn_blocking.
Arc<Mutex<Connection>> is cloned before entering the blocking closure;
the std::sync::MutexGuard is acquired and dropped entirely within the
blocking closure — never held across an .await point.
After the spawn_blocking future resolves, dump::on_change is called
at the .await point. The MutexGuard is already dropped at this stage.
If dump::on_change fails (e.g. disk full), the error is propagated via
? and the caller receives Err(MiniAppError::Io(_)); the row remains
in the database (DB and file may be transiently inconsistent until the
next successful write).
§Cancel Safety
Not cancel-safe. Once the spawn_blocking closure has started, the
INSERT completes regardless of Future cancellation. If the caller
drops this Future after the INSERT but before dump::on_change
completes, the file may not be materialized while the DB row exists.
§Errors
MiniAppError::Validation— required field absent or type mismatch.MiniAppError::Storage— rusqlite error (constraint violation, I/O).MiniAppError::Schema— blocking thread panicked (JoinError).MiniAppError::Io— dump file write failure (only whendumpis configured).
§Panic
Does not panic. Mutex poisoning is propagated as Err(MiniAppError::Storage(_)).
Sourcepub async fn get(&self, id: &str) -> Result<RowRecord, MiniAppError>
pub async fn get(&self, id: &str) -> Result<RowRecord, MiniAppError>
Fetch the row with the given id.
§Concurrency
The SELECT executes inside tokio::task::spawn_blocking. The
std::sync::MutexGuard is acquired and released within the blocking
closure; no lock is held across .await.
§Cancel Safety
Once the blocking closure has started the SELECT will complete
regardless of Future cancellation.
§Errors
MiniAppError::NotFound— no row with the givenid.MiniAppError::Storage— rusqlite error.MiniAppError::Schema— blocking thread panicked (JoinError).
§Panic
Does not panic.
Sourcepub async fn list(
&self,
limit: Option<u32>,
offset: Option<u32>,
filter: Option<ListFilter>,
) -> Result<Vec<RowRecord>, MiniAppError>
pub async fn list( &self, limit: Option<u32>, offset: Option<u32>, filter: Option<ListFilter>, ) -> Result<Vec<RowRecord>, MiniAppError>
Return rows ordered by created_at DESC.
limit defaults to 100 (max 1000). offset defaults to 0.
§Concurrency
The SELECT executes inside tokio::task::spawn_blocking. The
std::sync::MutexGuard is held only within the blocking closure.
§Cancel Safety
Once the blocking closure has started the query runs to completion
regardless of Future cancellation.
§Errors
MiniAppError::Storage— rusqlite error.MiniAppError::Schema— blocking thread panicked (JoinError).MiniAppError::Validation—build_sqlonfilterfails (defensive; callers should callfilter.validate()first).
§Panic
Does not panic.
Sourcepub async fn row_count(&self) -> Result<u64, MiniAppError>
pub async fn row_count(&self) -> Result<u64, MiniAppError>
Count all rows in the table.
Used by schema_delete in dry_run mode to report how many rows
would be orphaned when the schema is removed.
§Returns
The total row count as u64.
§Errors
MiniAppError::Schema— if the mutex is poisoned or the blocking task panics.MiniAppError::Storage— if the SQL query fails.
Sourcepub async fn update(
&self,
id: &str,
value: Value,
mode: UpdateMode,
) -> Result<RowRecord, MiniAppError>
pub async fn update( &self, id: &str, value: Value, mode: UpdateMode, ) -> Result<RowRecord, MiniAppError>
Validate value and update the row identified by id.
updated_at is refreshed; created_at is unchanged.
§Concurrency
The UPDATE executes inside tokio::task::spawn_blocking. The
std::sync::MutexGuard is held only within the blocking closure and is
dropped before any .await point. Concurrent calls with the same id
are serialized by the Mutex.
After the spawn_blocking future resolves, dump::on_change is called
at the .await point. The MutexGuard is already dropped at this stage.
If dump::on_change fails (e.g. disk full), the error is propagated via
? and the caller receives Err(MiniAppError::Io(_)); the row update
remains in the database (DB and file may be transiently inconsistent
until the next successful write).
Same-id concurrent update is not order-preserving with respect to
file content. The DB UPDATE is serialised by the connection
Mutex, but dump::on_change runs outside the lock. Two concurrent
update(id, A) / update(id, B) calls may finalise the DB row as B
while the dump file ends up holding A’s content (whichever
spawn_blocking write completes last wins on disk). Callers that
require strict file-DB ordering must serialise updates by id at the
caller side.
§Cancel Safety
Not cancel-safe. Once the blocking closure has started the UPDATE will
complete regardless of Future cancellation. Idempotent at the SQL
level: calling with the same id and value results in the same final
DB state. If the caller drops this Future after the UPDATE but before
dump::on_change completes, the file may not be re-materialized while
the DB row already reflects the new value.
§Errors
MiniAppError::NotFound— no row with the givenid.MiniAppError::Validation— required field absent or type mismatch, or a null patch value targets a required field (Merge mode only).MiniAppError::Storage— rusqlite error.MiniAppError::Schema— blocking thread panicked (JoinError).MiniAppError::Io— dump file write failure (only whendumpis configured).
§Panic
Does not panic.
Sourcepub async fn execute_under_savepoint<F, R>(
&self,
f: F,
) -> Result<R, MiniAppError>
pub async fn execute_under_savepoint<F, R>( &self, f: F, ) -> Result<R, MiniAppError>
Execute a closure under a SQLite SAVEPOINT for all-or-nothing semantics.
The closure receives &mut rusqlite::Savepoint<'_> and may run arbitrary
SQL inside the SAVEPOINT. On success, the SAVEPOINT is committed. On
failure, the SAVEPOINT is rolled back automatically when it is dropped
(enforced via set_drop_behavior(DropBehavior::Rollback)).
§Crux compliance
This method is the implementation backing schema_batch’s
schema_batch SAVEPOINT atomicity Crux constraint. All ops inside a
batch share the same SAVEPOINT; any failure causes the SAVEPOINT to
roll back, leaving the DB unchanged.
§Concurrency
The Mutex is acquired and the entire SAVEPOINT + ops execute inside a
single tokio::task::spawn_blocking closure. Savepoint<'_> borrows
the Connection, so both must remain in the same closure scope — they
cannot straddle an .await point (K-103, K-110).
§Cancel Safety
Not cancel-safe. Once the spawn_blocking closure has started, the
SAVEPOINT runs to completion (commit or rollback) regardless of Future
cancellation.
§Type Parameters
F: closureFnOnce(&mut rusqlite::Savepoint<'_>) -> Result<R, MiniAppError> + Send + 'static.R: return value, must beSend + 'static.
§Errors
MiniAppError::Schema— Mutex poisoned or blocking thread panicked.MiniAppError::Storage— rusqlite SAVEPOINT creation or commit failed.- Any error returned by the closure
f.
§Panic
Does not panic.
Sourcepub async fn delete(&self, id: &str) -> Result<(), MiniAppError>
pub async fn delete(&self, id: &str) -> Result<(), MiniAppError>
Delete the row identified by id.
§Concurrency
The DELETE executes inside tokio::task::spawn_blocking. The
std::sync::MutexGuard is held only within the blocking closure and
is dropped before any .await point. Idempotent at the SQL level:
deleting a non-existent id returns MiniAppError::NotFound, so
calling twice with the same id returns Err(MiniAppError::NotFound)
on the second call.
After the spawn_blocking future resolves, dump::on_delete is called
at the .await point. The MutexGuard is already dropped at this stage.
In the current implementation on_delete is a no-op (Ok(())) and the
dump file is preserved on disk by default. The Result<(), MiniAppError>
signature is retained because a future schema flag (e.g.
dump.on_delete: keep | remove) may switch this to an actual file
removal that can fail with MiniAppError::Io. Today the value-level
behaviour is infallible, but the type-level contract (and the
?-propagation site in Store::delete) is preserved so that flipping
the future flag does not require changing the call site.
§Cancel Safety
Not cancel-safe with respect to the spawn_blocking portion: once the
blocking closure has started the DELETE runs to completion regardless
of Future cancellation. The current on_delete no-op is itself
cancel-safe (no .await, no I/O), so dropping this Future after the
DELETE has no observable file-system effect today. When on_delete
gains real file removal in the future, this paragraph must be updated
in lockstep with the new contract.
§Errors
MiniAppError::NotFound— no row with the givenid.MiniAppError::Storage— rusqlite error.MiniAppError::Schema— blocking thread panicked (JoinError).MiniAppError::Io— reserved for a futureon_deleteimplementation that performs file removal (currently never returned, but the variant is part of the public contract so the call site does not need to change when the flag is added).
§Panic
Does not panic.
Sourcepub async fn alias_create(
&self,
name: &str,
filter_json: &str,
default_limit: Option<u32>,
description: Option<String>,
params_schema: Option<String>,
) -> Result<(), MiniAppError>
pub async fn alias_create( &self, name: &str, filter_json: &str, default_limit: Option<u32>, description: Option<String>, params_schema: Option<String>, ) -> Result<(), MiniAppError>
Register a named query alias in _aliases.
The filter_json value is stored verbatim (serialize before calling).
default_limit, description, and params_schema are optional.
params_schema is a JSON array of parameter name strings
(e.g. ["project","owner"]); pass None for parameter-free aliases.
§Errors
MiniAppError::AliasAlreadyExists— an alias withnamealready exists. Delete it first or choose a different name.MiniAppError::Storage— rusqlite error.MiniAppError::Schema— blocking thread panicked (JoinError).
§Panic
Does not panic.
Sourcepub async fn alias_get(&self, name: &str) -> Result<AliasRecord, MiniAppError>
pub async fn alias_get(&self, name: &str) -> Result<AliasRecord, MiniAppError>
Retrieve a single alias by name.
§Errors
MiniAppError::AliasNotFound— no alias withnameexists.MiniAppError::Storage— rusqlite error.MiniAppError::Schema— blocking thread panicked (JoinError).
§Panic
Does not panic.
Sourcepub async fn alias_list(&self) -> Result<Vec<AliasRecord>, MiniAppError>
pub async fn alias_list(&self) -> Result<Vec<AliasRecord>, MiniAppError>
List all aliases registered for this table, ordered by name.
Returns an empty Vec when no aliases exist.
§Errors
MiniAppError::Storage— rusqlite error.MiniAppError::Schema— blocking thread panicked (JoinError).
§Panic
Does not panic.
Sourcepub async fn alias_delete(&self, name: &str) -> Result<(), MiniAppError>
pub async fn alias_delete(&self, name: &str) -> Result<(), MiniAppError>
Delete the alias with the given name.
§Errors
MiniAppError::AliasNotFound— no alias withnameexists.MiniAppError::Storage— rusqlite error.MiniAppError::Schema— blocking thread panicked (JoinError).
§Panic
Does not panic.