Skip to main content

dactyl_db/
contract.rs

1//! Backend-neutral physical operations.
2//!
3//! These types deliberately stop at the storage boundary. Callers own schema
4//! policy, migration ordering, retry policy, and domain meaning; Dactyl owns
5//! only execution, atomicity, access mode, and result normalization.
6
7use std::time::Duration;
8
9use serde::{Deserialize, Serialize};
10
11use crate::rows::{Parameter, Rows};
12
13/// The first version of the opaque storage-context envelope.
14pub const STORAGE_CONTEXT_VERSION: u16 = 1;
15
16/// Caller-owned context forwarded to a remote storage service.
17///
18/// Dactyl validates only the envelope: the version must be non-zero and the
19/// payload must be a JSON object. The payload's fields and meaning belong to
20/// the caller and the remote service; Dactyl does not interpret organization,
21/// repository, membership, or authorization semantics.
22#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
23pub struct StorageContext {
24    version: u16,
25    payload: serde_json::Value,
26}
27
28impl StorageContext {
29    /// Build a versioned opaque context without adopting its domain schema.
30    pub fn new(
31        version: u16,
32        payload: serde_json::Value,
33    ) -> Result<Self, crate::error::DactylError> {
34        let context = Self { version, payload };
35        context.validate()?;
36        Ok(context)
37    }
38
39    pub fn version(&self) -> u16 {
40        self.version
41    }
42
43    /// Return the untouched caller-owned payload.
44    pub fn payload(&self) -> &serde_json::Value {
45        &self.payload
46    }
47
48    pub(crate) fn validate(&self) -> Result<(), crate::error::DactylError> {
49        if self.version == 0 {
50            return Err(crate::error::DactylError::adapter_with_code(
51                crate::error::AdapterErrorKind::Protocol,
52                "invalid_context",
53                "storage context version must be non-zero",
54            ));
55        }
56        if !self.payload.is_object() {
57            return Err(crate::error::DactylError::adapter_with_code(
58                crate::error::AdapterErrorKind::Protocol,
59                "invalid_context",
60                "storage context payload must be a JSON object",
61            ));
62        }
63        Ok(())
64    }
65}
66
67/// Whether an opened route may mutate durable state.
68#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
69#[serde(rename_all = "snake_case")]
70pub enum AccessMode {
71    #[default]
72    ReadWrite,
73    ReadOnly,
74}
75
76/// The physical kind of an operation. Schema operations are caller-supplied
77/// and are not migrations: Dactyl never assigns ids or ordering to them.
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
79#[serde(rename_all = "snake_case")]
80pub enum OperationKind {
81    Read,
82    Write,
83    Schema,
84}
85
86/// An opaque, backend-neutral operation accepted by [`crate::Connection`].
87#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
88pub struct Operation {
89    pub(crate) kind: OperationKind,
90    pub(crate) sql: String,
91    #[serde(default)]
92    pub(crate) params: Vec<Parameter>,
93}
94
95impl Operation {
96    pub fn read(sql: impl Into<String>, params: impl Into<Vec<Parameter>>) -> Self {
97        Self {
98            kind: OperationKind::Read,
99            sql: sql.into(),
100            params: params.into(),
101        }
102    }
103
104    pub fn write(sql: impl Into<String>, params: impl Into<Vec<Parameter>>) -> Self {
105        Self {
106            kind: OperationKind::Write,
107            sql: sql.into(),
108            params: params.into(),
109        }
110    }
111
112    pub fn schema(sql: impl Into<String>, params: impl Into<Vec<Parameter>>) -> Self {
113        Self {
114            kind: OperationKind::Schema,
115            sql: sql.into(),
116            params: params.into(),
117        }
118    }
119
120    pub fn kind(&self) -> OperationKind {
121        self.kind
122    }
123
124    pub fn sql(&self) -> &str {
125        &self.sql
126    }
127
128    pub fn params(&self) -> &[Parameter] {
129        &self.params
130    }
131}
132
133/// A generated key explicitly returned by a write, never an ambient handle.
134#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
135#[serde(untagged)]
136pub enum GeneratedKey {
137    Integer(i64),
138    Text(String),
139}
140
141/// The normalized result of one physical write.
142#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
143pub struct WriteResult {
144    pub affected_rows: u64,
145    #[serde(default)]
146    pub generated_keys: Vec<GeneratedKey>,
147}
148
149impl WriteResult {
150    pub fn generated_key(&self) -> Option<&GeneratedKey> {
151        self.generated_keys.first()
152    }
153}
154
155/// A result in an atomic batch, preserving operation order.
156#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
157pub enum OperationResult {
158    Rows(Rows),
159    Write(WriteResult),
160}
161
162/// The result of an opaque atomic batch.
163#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
164pub struct AtomicResult {
165    pub results: Vec<OperationResult>,
166}
167
168/// Options that affect physical opening, not migration or application policy.
169#[derive(Debug, Clone, Copy, PartialEq, Eq)]
170pub struct OpenOptions {
171    pub access_mode: AccessMode,
172    pub lock_timeout: Duration,
173}
174
175impl Default for OpenOptions {
176    fn default() -> Self {
177        Self {
178            access_mode: AccessMode::ReadWrite,
179            lock_timeout: Duration::from_millis(250),
180        }
181    }
182}