Skip to main content

mongreldb_core/
handle.rs

1//! `DatabaseHandle`: a lightweight caller-specific object referencing one
2//! `DatabaseCore` (spec §10.1, S1A-001).
3//!
4//! Handles are issued by [`crate::manager::DatabaseManager::open_shared`].
5//! Every handle carries its own [`HandleIdentity`] and [`HandleAccess`], while
6//! recovery, WAL opening, open-generation advancement, and table mounting all
7//! happened exactly once on the shared core. Dropping one handle never closes
8//! storage; the core closes when the last reference drops or when
9//! [`DatabaseHandle::shutdown`] drains it (S1A-004).
10//!
11//! Handles expose only principal-aware operations. They never dereference to
12//! the raw [`Database`] facade or return mutable table handles.
13
14use std::sync::Arc;
15use std::time::Duration;
16
17use crate::core::{LifecycleState, OperationGuard};
18use crate::database::Database;
19use crate::error::{MongrelError, Result};
20use crate::service_principal::ServicePrincipalDefinition;
21
22/// A password that zeroizes its allocation and never reveals itself through
23/// `Debug` output.
24pub struct SecretString(zeroize::Zeroizing<String>);
25
26impl SecretString {
27    pub fn new(secret: impl Into<String>) -> Self {
28        Self(zeroize::Zeroizing::new(secret.into()))
29    }
30
31    pub(crate) fn expose(&self) -> &str {
32        self.0.as_str()
33    }
34}
35
36impl std::fmt::Debug for SecretString {
37    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38        f.write_str("SecretString([REDACTED])")
39    }
40}
41
42/// The per-caller identity bound to one handle (spec §10.1, S1A-001).
43///
44/// Catalog identities pin one user generation and are re-resolved against the
45/// live catalog on each authorized operation. Service identities pin a
46/// registered token generation (`token_id` + `principal_id` +
47/// `creation_version`) and re-resolve live scopes from the shared core's
48/// service-principal store on each authorize (P0.1).
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub enum HandleIdentity {
51    /// No credentials were supplied for this handle.
52    Credentialless,
53    /// A user resolved from the database catalog. `user_id` +
54    /// `created_version` pin the exact catalog generation so username reuse
55    /// cannot revive a stale identity.
56    CatalogUser {
57        username: String,
58        user_id: u64,
59        created_version: u64,
60    },
61    /// An authenticated service principal. Authority is re-resolved live from
62    /// the shared core; the handle never freezes a caller-supplied permission
63    /// vector.
64    ServicePrincipal {
65        token_id: String,
66        principal_id: [u8; 16],
67        creation_version: u64,
68    },
69}
70
71/// The identity requested when attaching a shared handle (spec §2.2).
72///
73/// Catalog credentials are verified against the live shared catalog before a
74/// handle is issued. Service credentials are verified against the core's
75/// service-principal store (P0.1). The secret allocation is zeroized when
76/// attach returns.
77///
78/// Callers cannot supply a permission vector on open — that was the P0.1
79/// authority gap (`ScopedServicePrincipal`). Service authority is assigned
80/// only by admin registration on the shared core.
81#[derive(Debug)]
82pub enum OpenIdentity {
83    /// Attach without credentials. Rejected (fail closed) when the database
84    /// catalog has `require_auth` enabled.
85    Credentialless,
86    /// Authenticate one registered service principal by token id + secret.
87    ServiceCredentials {
88        token_id: String,
89        secret: SecretString,
90    },
91    /// Authenticate one catalog user on the existing shared core.
92    CatalogCredentials {
93        username: String,
94        password: SecretString,
95    },
96}
97
98/// Crate-private service capability for trusted internal actors (P0.1-T6).
99///
100/// Not part of [`OpenIdentity`], not FFI/Kit/serializable from untrusted
101/// input, and not constructible outside `mongreldb-core`. Use this only for
102/// in-process engine paths that must act as a service principal without
103/// going through public credentials.
104#[derive(Debug, Clone)]
105pub(crate) struct InternalServiceCapability {
106    pub principal_id: [u8; 16],
107    pub permissions: Vec<crate::auth::Permission>,
108}
109
110/// Per-handle access restriction (spec §2.2: "each handle may have its own
111/// principal and read-only restriction").
112///
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
114pub struct HandleAccess {
115    read_only: bool,
116}
117
118impl HandleAccess {
119    /// A read-write handle (the Stage 1A default).
120    pub fn read_write() -> Self {
121        Self { read_only: false }
122    }
123
124    /// A read-only handle.
125    pub fn read_only() -> Self {
126        Self { read_only: true }
127    }
128
129    /// Whether this handle is restricted to reads.
130    pub fn is_read_only(&self) -> bool {
131        self.read_only
132    }
133}
134
135/// A lightweight caller-specific handle onto one shared [`DatabaseCore`]
136/// (spec §10.1, S1A-001).
137///
138/// Obtained from [`crate::manager::DatabaseManager::open_shared`]. Cloning or
139/// dropping a handle has no storage side effects; storage closes when the
140/// last core reference drops or [`Self::shutdown`] runs.
141pub struct DatabaseHandle {
142    /// The facade carrying this handle's live authorization context.
143    database: Database,
144    identity: HandleIdentity,
145    access: HandleAccess,
146    /// Frozen scopes for crate-private [`InternalServiceCapability`] only.
147    /// Authenticated service principals re-resolve live and leave this `None`.
148    internal_capability: Option<InternalServiceCapability>,
149}
150
151impl DatabaseHandle {
152    pub(crate) fn new(
153        database: Database,
154        identity: HandleIdentity,
155        access: HandleAccess,
156        internal_capability: Option<InternalServiceCapability>,
157    ) -> Self {
158        Self {
159            database,
160            identity,
161            access,
162            internal_capability,
163        }
164    }
165
166    /// Issue a handle bound to a crate-private internal service capability.
167    /// Not reachable from public attach paths.
168    #[allow(dead_code)] // used by trusted in-crate callers / future wiring
169    pub(crate) fn with_internal_capability(
170        database: Database,
171        capability: InternalServiceCapability,
172        access: HandleAccess,
173    ) -> Self {
174        let identity = HandleIdentity::ServicePrincipal {
175            token_id: format!("internal:{:02x?}", capability.principal_id),
176            principal_id: capability.principal_id,
177            creation_version: 0,
178        };
179        Self::new(database, identity, access, Some(capability))
180    }
181
182    /// The identity bound to this handle.
183    pub fn identity(&self) -> &HandleIdentity {
184        &self.identity
185    }
186
187    /// The access restriction bound to this handle.
188    pub fn access(&self) -> HandleAccess {
189        self.access
190    }
191
192    /// Whether two handles reference the exact same process-local core.
193    pub fn shares_core_with(&self, other: &Self) -> bool {
194        Arc::ptr_eq(&self.database.core(), &other.database.core())
195    }
196
197    /// The current lifecycle state of the shared core (S1A-004).
198    pub fn lifecycle_state(&self) -> LifecycleState {
199        self.database.lifecycle_state()
200    }
201
202    /// Admit one operation against the shared core (S1A-004). The RAII guard
203    /// releases the operation slot on drop; new operations are rejected once
204    /// the core leaves [`LifecycleState::Open`].
205    pub fn operation_guard(&self) -> Result<OperationGuard> {
206        self.database.operation_guard()
207    }
208
209    fn authorize_write(&self, operation: &'static str) -> Result<()> {
210        if self.access.is_read_only() {
211            Err(MongrelError::ReadOnlyHandle { operation })
212        } else {
213            Ok(())
214        }
215    }
216
217    /// Re-resolve authenticated service authority from the shared core and
218    /// rebind the facade principal so subsequent require/txn checks see live
219    /// scopes (P0.1-T4/T5).
220    fn resolve_service_authority(&self) -> Result<()> {
221        if self.internal_capability.is_some() {
222            return Ok(());
223        }
224        let HandleIdentity::ServicePrincipal {
225            token_id,
226            principal_id,
227            creation_version,
228        } = &self.identity
229        else {
230            return Ok(());
231        };
232        let def = self.database.core().service_principals.resolve_live(
233            token_id,
234            *principal_id,
235            *creation_version,
236        )?;
237        self.database.rebind_service_principal(&def);
238        Ok(())
239    }
240
241    fn authorize_permission(&self, permission: &crate::auth::Permission) -> Result<()> {
242        if let Some(capability) = &self.internal_capability {
243            if !capability
244                .permissions
245                .iter()
246                .any(|granted| granted.satisfies(permission))
247            {
248                return Err(MongrelError::PermissionDenied {
249                    required: permission.clone(),
250                    principal: format!("service:{:02x?}", capability.principal_id),
251                });
252            }
253            return Ok(());
254        }
255        if let HandleIdentity::ServicePrincipal {
256            token_id,
257            principal_id,
258            creation_version,
259        } = &self.identity
260        {
261            let def = self.database.core().service_principals.resolve_live(
262                token_id,
263                *principal_id,
264                *creation_version,
265            )?;
266            self.database.rebind_service_principal(&def);
267            if !def
268                .permissions
269                .iter()
270                .any(|granted| granted.satisfies(permission))
271            {
272                return Err(MongrelError::PermissionDenied {
273                    required: permission.clone(),
274                    principal: format!("service:{token_id}"),
275                });
276            }
277            return Ok(());
278        }
279        self.database.require(permission)
280    }
281
282    /// Execute an authorized native query with live roles, RLS, masks, and
283    /// column grants.
284    pub fn query(
285        &self,
286        table: &str,
287        query: &crate::query::Query,
288        projection: Option<&[u16]>,
289    ) -> Result<Vec<crate::memtable::Row>> {
290        self.resolve_service_authority()?;
291        self.database
292            .query_for_current_principal(table, query, projection)
293    }
294
295    /// Count rows visible to this handle after live authorization and RLS.
296    pub fn count(&self, table: &str) -> Result<u64> {
297        self.resolve_service_authority()?;
298        self.database
299            .count_for(table, self.database.principal().as_ref())
300    }
301
302    /// Return all rows visible to this handle after RLS and masks.
303    pub fn rows(&self, table: &str) -> Result<Vec<crate::memtable::Row>> {
304        self.resolve_service_authority()?;
305        self.database
306            .rows_for(table, self.database.principal().as_ref())
307    }
308
309    /// Create a table through this handle's live principal.
310    pub fn create_table(&self, name: &str, schema: crate::schema::Schema) -> Result<u64> {
311        self.authorize_write("create table")?;
312        self.authorize_permission(&crate::auth::Permission::Ddl)?;
313        self.database.create_table(name, schema)
314    }
315
316    /// Atomically insert one row through this handle's live principal.
317    pub fn put(
318        &self,
319        table: &str,
320        cells: Vec<(u16, crate::memtable::Value)>,
321    ) -> Result<Option<i64>> {
322        self.authorize_write("put")?;
323        self.resolve_service_authority()?;
324        self.database
325            .transaction_for_current_principal(|transaction| transaction.put(table, cells))
326    }
327
328    /// Atomically insert many rows through this handle's live principal (P1.4).
329    pub fn put_batch(
330        &self,
331        table: &str,
332        rows: Vec<Vec<(u16, crate::memtable::Value)>>,
333    ) -> Result<Vec<Option<i64>>> {
334        self.authorize_write("put_batch")?;
335        self.resolve_service_authority()?;
336        self.database
337            .transaction_for_current_principal(|transaction| transaction.put_batch(table, rows))
338    }
339
340    /// Atomically update one row through this handle's live principal (P1.4).
341    pub fn update(
342        &self,
343        table: &str,
344        row_id: crate::RowId,
345        cells: Vec<(u16, crate::memtable::Value)>,
346    ) -> Result<crate::txn::OwnedRow> {
347        self.authorize_write("update")?;
348        self.resolve_service_authority()?;
349        self.database
350            .transaction_for_current_principal(|transaction| {
351                let mut images = transaction.update_many(table, vec![(row_id, cells)])?;
352                images.pop().ok_or_else(|| {
353                    MongrelError::NotFound(format!("row {row_id:?} not found for update"))
354                })
355            })
356    }
357
358    /// Open an authorized session bound to this handle's principal (P1.4-T1).
359    ///
360    /// Full SQL `MongrelSession` lives in the query crate (avoids core→query
361    /// dependency). This session is the authorized CRUD/transaction wrapper
362    /// without raw core escape.
363    pub fn session(&self) -> Result<AuthorizedMongrelSession<'_>> {
364        self.resolve_service_authority()?;
365        Ok(AuthorizedMongrelSession { handle: self })
366    }
367
368    /// Begin an authorized multi-statement transaction (P1.4-T2).
369    ///
370    /// Writes through the returned [`AuthorizedTransaction`] re-check the
371    /// handle's read-only restriction. Commit/rollback are explicit.
372    pub fn begin(&self) -> Result<AuthorizedTransaction<'_>> {
373        self.authorize_write("begin")?;
374        self.resolve_service_authority()?;
375        let principal = self.database.principal();
376        let txn = self.database.begin_as(principal);
377        Ok(AuthorizedTransaction {
378            handle: self,
379            txn: Some(txn),
380        })
381    }
382
383    /// Atomically delete one row through this handle's live principal.
384    pub fn delete(&self, table: &str, row_id: crate::RowId) -> Result<()> {
385        self.authorize_write("delete")?;
386        self.resolve_service_authority()?;
387        self.database
388            .transaction_for_current_principal(|transaction| transaction.delete(table, row_id))
389    }
390
391    /// Create a secondary index through this handle's live principal (P1.4-T3).
392    ///
393    /// Requires DDL permission (and fails on a read-only handle). Uses the
394    /// same product path as SQL `CREATE INDEX` (`Database::create_index`).
395    pub fn create_index(&self, table: &str, definition: crate::schema::IndexDef) -> Result<u64> {
396        self.authorize_write("create index")?;
397        self.authorize_permission(&crate::auth::Permission::Ddl)?;
398        self.database.create_index(table, definition)
399    }
400
401    /// Drop a secondary index by name through this handle's live principal (P1.4-T3).
402    pub fn drop_index(&self, table: &str, name: &str) -> Result<()> {
403        self.authorize_write("drop index")?;
404        self.authorize_permission(&crate::auth::Permission::Ddl)?;
405        self.database.drop_index(table, name)
406    }
407
408    /// Create a stored procedure through this handle's live principal (P1.4-X6).
409    pub fn create_procedure(
410        &self,
411        procedure: crate::procedure::StoredProcedure,
412    ) -> Result<crate::procedure::StoredProcedure> {
413        self.authorize_write("create procedure")?;
414        self.authorize_permission(&crate::auth::Permission::Ddl)?;
415        self.resolve_service_authority()?;
416        self.database.create_procedure(procedure)
417    }
418
419    /// Drop a stored procedure by name (P1.4-X6).
420    pub fn drop_procedure(&self, name: &str) -> Result<()> {
421        self.authorize_write("drop procedure")?;
422        self.authorize_permission(&crate::auth::Permission::Ddl)?;
423        self.resolve_service_authority()?;
424        self.database.drop_procedure(name)
425    }
426
427    /// Call a stored procedure through this handle's live principal (P1.4-X6).
428    pub fn call_procedure(
429        &self,
430        name: &str,
431        args: std::collections::HashMap<String, crate::memtable::Value>,
432    ) -> Result<crate::procedure::ProcedureCallResult> {
433        // Mode-dependent write authorization is enforced inside Database when
434        // the procedure body mutates; the handle still re-resolves authority.
435        self.resolve_service_authority()?;
436        self.database.call_procedure(name, args)
437    }
438
439    /// Create a trigger through this handle's live principal (P1.4-X6).
440    pub fn create_trigger(
441        &self,
442        trigger: crate::trigger::StoredTrigger,
443    ) -> Result<crate::trigger::StoredTrigger> {
444        self.authorize_write("create trigger")?;
445        self.authorize_permission(&crate::auth::Permission::Ddl)?;
446        self.resolve_service_authority()?;
447        self.database.create_trigger(trigger)
448    }
449
450    /// Drop a trigger by name (P1.4-X6).
451    pub fn drop_trigger(&self, name: &str) -> Result<()> {
452        self.authorize_write("drop trigger")?;
453        self.authorize_permission(&crate::auth::Permission::Ddl)?;
454        self.resolve_service_authority()?;
455        self.database.drop_trigger(name)
456    }
457
458    /// Historical rows visible to this principal at `snapshot` (P1.4-X7).
459    pub fn rows_at_epoch(
460        &self,
461        table: &str,
462        snapshot: crate::epoch::Snapshot,
463    ) -> Result<Vec<crate::memtable::Row>> {
464        self.resolve_service_authority()?;
465        self.database
466            .rows_at_epoch_for_current_principal(table, snapshot)
467    }
468
469    /// Create a catalog user. Admin only.
470    pub fn create_user(&self, username: &str, password: &str) -> Result<crate::auth::UserEntry> {
471        self.authorize_write("create user")?;
472        self.authorize_permission(&crate::auth::Permission::Admin)?;
473        self.database.create_user(username, password)
474    }
475
476    /// Drop a catalog user. Admin only.
477    pub fn drop_user(&self, username: &str) -> Result<()> {
478        self.authorize_write("drop user")?;
479        self.authorize_permission(&crate::auth::Permission::Admin)?;
480        self.database.drop_user(username)
481    }
482
483    /// Create a role. Admin only.
484    pub fn create_role(&self, role: &str) -> Result<crate::auth::RoleEntry> {
485        self.authorize_write("create role")?;
486        self.authorize_permission(&crate::auth::Permission::Admin)?;
487        self.database.create_role(role)
488    }
489
490    /// Grant a role to a catalog user. Admin only.
491    pub fn grant_role(&self, username: &str, role: &str) -> Result<()> {
492        self.authorize_write("grant role")?;
493        self.authorize_permission(&crate::auth::Permission::Admin)?;
494        self.database.grant_role(username, role)
495    }
496
497    /// Revoke a role from a catalog user. Admin only.
498    pub fn revoke_role(&self, username: &str, role: &str) -> Result<()> {
499        self.authorize_write("revoke role")?;
500        self.authorize_permission(&crate::auth::Permission::Admin)?;
501        self.database.revoke_role(username, role)
502    }
503
504    /// Grant one permission to a role. Admin only.
505    pub fn grant_permission(&self, role: &str, permission: crate::auth::Permission) -> Result<()> {
506        self.authorize_write("grant permission")?;
507        self.authorize_permission(&crate::auth::Permission::Admin)?;
508        self.database.grant_permission(role, permission)
509    }
510
511    /// Revoke one permission from a role. Admin only.
512    pub fn revoke_permission(&self, role: &str, permission: crate::auth::Permission) -> Result<()> {
513        self.authorize_write("revoke permission")?;
514        self.authorize_permission(&crate::auth::Permission::Admin)?;
515        self.database.revoke_permission(role, permission)
516    }
517
518    /// Register an authenticated service principal on the shared core (P0.1).
519    /// Admin only. The returned definition's secret is never re-exported;
520    /// callers must retain the raw secret they supplied.
521    pub fn register_service_principal(
522        &self,
523        token_id: impl Into<String>,
524        principal_id: [u8; 16],
525        permissions: Vec<crate::auth::Permission>,
526        raw_secret: &str,
527        expires_unix: u64,
528    ) -> Result<ServicePrincipalDefinition> {
529        self.authorize_write("register service principal")?;
530        self.authorize_permission(&crate::auth::Permission::Admin)?;
531        self.database.core().service_principals.register(
532            token_id,
533            principal_id,
534            permissions,
535            raw_secret,
536            expires_unix,
537        )
538    }
539
540    /// Revoke a registered service principal. Admin only. Existing handles
541    /// bound to the token fail on the next authorized operation.
542    pub fn revoke_service_principal(&self, token_id: &str) -> Result<()> {
543        self.authorize_write("revoke service principal")?;
544        self.authorize_permission(&crate::auth::Permission::Admin)?;
545        self.database.core().service_principals.revoke(token_id)
546    }
547
548    /// Replace the live permission set for a registered service principal.
549    /// Admin only. Scope reduction takes effect on the next authorize without
550    /// reopening handles.
551    pub fn set_service_principal_permissions(
552        &self,
553        token_id: &str,
554        permissions: Vec<crate::auth::Permission>,
555    ) -> Result<()> {
556        self.authorize_write("set service principal permissions")?;
557        self.authorize_permission(&crate::auth::Permission::Admin)?;
558        self.database
559            .core()
560            .service_principals
561            .set_permissions(token_id, permissions)
562    }
563
564    /// Shut the shared core down (spec §10.1, S1A-004): drain in-flight
565    /// operations within `drain_deadline`, sync durable state, release the
566    /// file lock, and mark the core `Closed`. Every handle — including this
567    /// one — then rejects further operations. Dropping a handle never closes
568    /// storage; only this method or the last core drop does.
569    pub fn shutdown(&self, drain_deadline: Duration) -> Result<()> {
570        self.authorize_write("shutdown")?;
571        self.authorize_permission(&crate::auth::Permission::Admin)?;
572        self.database.core().shutdown(drain_deadline)
573    }
574}
575
576/// Principal-bound authorized session without raw core escape (P1.4-T1).
577///
578/// Obtained from [`DatabaseHandle::session`]. Delegates to the handle's
579/// authorized surface; never exposes raw [`crate::database::Database`] /
580/// table handles.
581pub struct AuthorizedMongrelSession<'a> {
582    handle: &'a DatabaseHandle,
583}
584
585/// Alias used by some call sites.
586pub type AuthorizedSession<'a> = AuthorizedMongrelSession<'a>;
587
588impl<'a> AuthorizedMongrelSession<'a> {
589    pub fn begin(&self) -> Result<AuthorizedTransaction<'a>> {
590        self.handle.begin()
591    }
592
593    pub fn put(
594        &self,
595        table: &str,
596        cells: Vec<(u16, crate::memtable::Value)>,
597    ) -> Result<Option<i64>> {
598        self.handle.put(table, cells)
599    }
600
601    pub fn update(
602        &self,
603        table: &str,
604        row_id: crate::RowId,
605        cells: Vec<(u16, crate::memtable::Value)>,
606    ) -> Result<crate::txn::OwnedRow> {
607        self.handle.update(table, row_id, cells)
608    }
609
610    pub fn delete(&self, table: &str, row_id: crate::RowId) -> Result<()> {
611        self.handle.delete(table, row_id)
612    }
613
614    pub fn query(
615        &self,
616        table: &str,
617        query: &crate::query::Query,
618        projection: Option<&[u16]>,
619    ) -> Result<Vec<crate::memtable::Row>> {
620        self.handle.query(table, query, projection)
621    }
622
623    pub fn count(&self, table: &str) -> Result<u64> {
624        self.handle.count(table)
625    }
626
627    pub fn put_batch(
628        &self,
629        table: &str,
630        rows: Vec<Vec<(u16, crate::memtable::Value)>>,
631    ) -> Result<Vec<Option<i64>>> {
632        self.handle.put_batch(table, rows)
633    }
634
635    /// Create a secondary index (P1.4-X5).
636    pub fn create_index(&self, table: &str, definition: crate::schema::IndexDef) -> Result<u64> {
637        self.handle.create_index(table, definition)
638    }
639
640    /// Drop a secondary index (P1.4-X5).
641    pub fn drop_index(&self, table: &str, name: &str) -> Result<()> {
642        self.handle.drop_index(table, name)
643    }
644
645    /// Create a stored procedure through the authorized surface (P1.4-X6).
646    pub fn create_procedure(
647        &self,
648        procedure: crate::procedure::StoredProcedure,
649    ) -> Result<crate::procedure::StoredProcedure> {
650        self.handle.create_procedure(procedure)
651    }
652
653    /// Drop a stored procedure by name (P1.4-X6).
654    pub fn drop_procedure(&self, name: &str) -> Result<()> {
655        self.handle.drop_procedure(name)
656    }
657
658    /// Execute a stored procedure with live principal authorization (P1.4-X6).
659    pub fn call_procedure(
660        &self,
661        name: &str,
662        args: std::collections::HashMap<String, crate::memtable::Value>,
663    ) -> Result<crate::procedure::ProcedureCallResult> {
664        self.handle.call_procedure(name, args)
665    }
666
667    /// Create a trigger through the authorized surface (P1.4-X6).
668    pub fn create_trigger(
669        &self,
670        trigger: crate::trigger::StoredTrigger,
671    ) -> Result<crate::trigger::StoredTrigger> {
672        self.handle.create_trigger(trigger)
673    }
674
675    /// Drop a trigger by name (P1.4-X6).
676    pub fn drop_trigger(&self, name: &str) -> Result<()> {
677        self.handle.drop_trigger(name)
678    }
679
680    /// Historical rows at a snapshot under the session principal (P1.4-X7).
681    pub fn rows_at_epoch(
682        &self,
683        table: &str,
684        snapshot: crate::epoch::Snapshot,
685    ) -> Result<Vec<crate::memtable::Row>> {
686        self.handle.rows_at_epoch(table, snapshot)
687    }
688
689    /// Authorized aggregate count (P1.4-X7).
690    pub fn aggregate_count(&self, table: &str) -> Result<u64> {
691        self.handle.count(table)
692    }
693}
694
695/// Authorized multi-statement transaction bound to one [`DatabaseHandle`] (P1.4).
696///
697/// Obtained from [`DatabaseHandle::begin`]. Mutations re-check the handle's
698/// read-only restriction; principal authorization is enforced by the underlying
699/// [`crate::txn::Transaction`].
700pub struct AuthorizedTransaction<'a> {
701    handle: &'a DatabaseHandle,
702    txn: Option<crate::txn::Transaction<'a>>,
703}
704
705impl<'a> AuthorizedTransaction<'a> {
706    fn txn_mut(&mut self) -> Result<&mut crate::txn::Transaction<'a>> {
707        self.txn
708            .as_mut()
709            .ok_or_else(|| MongrelError::InvalidArgument("transaction already finished".into()))
710    }
711
712    /// Stage an insert.
713    pub fn put(
714        &mut self,
715        table: &str,
716        cells: Vec<(u16, crate::memtable::Value)>,
717    ) -> Result<Option<i64>> {
718        self.handle.authorize_write("put")?;
719        self.handle.resolve_service_authority()?;
720        self.txn_mut()?.put(table, cells)
721    }
722
723    /// Stage many inserts.
724    pub fn put_batch(
725        &mut self,
726        table: &str,
727        rows: Vec<Vec<(u16, crate::memtable::Value)>>,
728    ) -> Result<Vec<Option<i64>>> {
729        self.handle.authorize_write("put_batch")?;
730        self.handle.resolve_service_authority()?;
731        self.txn_mut()?.put_batch(table, rows)
732    }
733
734    /// Stage an update of one row.
735    pub fn update(
736        &mut self,
737        table: &str,
738        row_id: crate::RowId,
739        cells: Vec<(u16, crate::memtable::Value)>,
740    ) -> Result<crate::txn::OwnedRow> {
741        self.handle.authorize_write("update")?;
742        self.handle.resolve_service_authority()?;
743        let mut images = self.txn_mut()?.update_many(table, vec![(row_id, cells)])?;
744        images
745            .pop()
746            .ok_or_else(|| MongrelError::NotFound(format!("row {row_id:?} not found for update")))
747    }
748
749    /// Stage a delete of one row.
750    pub fn delete(&mut self, table: &str, row_id: crate::RowId) -> Result<()> {
751        self.handle.authorize_write("delete")?;
752        self.handle.resolve_service_authority()?;
753        self.txn_mut()?.delete(table, row_id)
754    }
755
756    /// Commit the transaction.
757    pub fn commit(mut self) -> Result<crate::epoch::Epoch> {
758        self.handle.authorize_write("commit")?;
759        let txn = self
760            .txn
761            .take()
762            .ok_or_else(|| MongrelError::InvalidArgument("transaction already finished".into()))?;
763        txn.commit()
764    }
765
766    /// Roll back the transaction.
767    pub fn rollback(mut self) {
768        if let Some(txn) = self.txn.take() {
769            txn.rollback();
770        }
771    }
772}
773
774impl Drop for AuthorizedTransaction<'_> {
775    fn drop(&mut self) {
776        if let Some(txn) = self.txn.take() {
777            txn.rollback();
778        }
779    }
780}
781
782impl std::fmt::Debug for DatabaseHandle {
783    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
784        f.debug_struct("DatabaseHandle")
785            .field("identity", &self.identity)
786            .field("access", &self.access)
787            .field("database", &self.database)
788            .finish()
789    }
790}