Skip to main content

Store

Struct Store 

Source
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

Source

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
§Panic

Does not panic.

Source

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
§Panic

Does not panic. Mutex poisoning is propagated as Err(MiniAppError::Storage(_)).

Source

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
§Panic

Does not panic.

Source

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
§Panic

Does not panic.

Source

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
Source

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
§Panic

Does not panic.

Source

pub async fn execute_under_savepoint<F, R>( &self, f: F, ) -> Result<R, MiniAppError>
where F: FnOnce(&mut Savepoint<'_>) -> Result<R, MiniAppError> + Send + 'static, R: Send + 'static,

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: closure FnOnce(&mut rusqlite::Savepoint<'_>) -> Result<R, MiniAppError> + Send + 'static.
  • R: return value, must be Send + 'static.
§Errors
§Panic

Does not panic.

Source

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
§Panic

Does not panic.

Source

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
§Panic

Does not panic.

Source

pub async fn alias_get(&self, name: &str) -> Result<AliasRecord, MiniAppError>

Retrieve a single alias by name.

§Errors
§Panic

Does not panic.

Source

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
§Panic

Does not panic.

Source

pub async fn alias_delete(&self, name: &str) -> Result<(), MiniAppError>

Delete the alias with the given name.

§Errors
§Panic

Does not panic.

Auto Trait Implementations§

§

impl Freeze for Store

§

impl RefUnwindSafe for Store

§

impl Send for Store

§

impl Sync for Store

§

impl Unpin for Store

§

impl UnsafeUnpin for Store

§

impl UnwindSafe for Store

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more