Skip to main content

datacraft_core/
port.rs

1//! Storage port (step 4.1, item H13).
2//!
3//! The command core performs storage I/O only through this core-owned
4//! trait: real Jet/ACE backends implement it (dependency direction
5//! `storage → core`), headless tests use [`MockPort`]. No storage type
6//! crosses into the core vocabulary.
7
8use serde::{Deserialize, Serialize};
9
10use crate::schema::{FieldValue, TableDef};
11
12mod capabilities;
13mod native_view;
14mod source_objects;
15mod write_op;
16
17pub use capabilities::{DriverCapabilities, DriverId, DriverInfo, Support};
18pub use datacraft_query::native_query::{
19    NativeJoin, NativeJoinKind, NativeOrder, NativeQuery, NativeQueryColumn, NativeQueryKind,
20};
21pub use native_view::NativeView;
22pub use source_objects::{
23    NativeObject, NativeObjectKind, SourceFact, SourceFacts, SourceIndex, SourceTrigger,
24};
25
26/// Binary format family of an opened database (domain vocabulary; the
27/// backends map their physical version detection onto it).
28///
29/// `#[non_exhaustive]` (M32): ADR-7's server path adds format families;
30/// clients match with a wildcard arm.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
32#[serde(rename_all = "snake_case")]
33#[non_exhaustive]
34pub enum DatabaseFormat {
35    /// Jet 3.x (Access 97 and earlier).
36    Jet3,
37    /// Jet 4.x (Access 2000–2003).
38    Jet4,
39    /// ACE (Access 2007 and later, `.accdb`).
40    Ace,
41    /// SQLite 3 (`.sqlite`, `.db`) — the non-Jet source family (H35).
42    Sqlite,
43    /// A directory of `*.csv` files (phase 30, M18; ADR-7 driver proof).
44    Csv,
45}
46
47impl DatabaseFormat {
48    /// The canonical extension of a family this build can create — without
49    /// the dot (step 75.3, item L43). The creation pickers suggest it and
50    /// the create commands compare against it.
51    pub fn create_extension(self) -> Option<&'static str> {
52        match self {
53            Self::Jet4 => Some("mdb"),
54            Self::Sqlite => Some("sqlite"),
55            Self::Jet3 | Self::Ace | Self::Csv => None,
56        }
57    }
58
59    /// The suffix conflict of a creation path (step 75.3, item L43): the
60    /// extension of the *other* creatable family, compared
61    /// case-insensitively. `None` accepts matching, unknown and missing
62    /// extensions — the create command carries the format explicitly, so
63    /// only a known extension of the other family is a contradiction.
64    pub fn creation_suffix_conflict(self, path: &std::path::Path) -> Option<&'static str> {
65        let extension = path.extension()?.to_str()?;
66        [Self::Jet4, Self::Sqlite]
67            .into_iter()
68            .filter(|family| *family != self)
69            .filter_map(Self::create_extension)
70            .find(|candidate| extension.eq_ignore_ascii_case(candidate))
71    }
72}
73
74/// Errors a storage backend reports through the port.
75///
76/// `#[non_exhaustive]` (M22, ADR-7): adding a variant is not a breaking
77/// change for drivers or clients compiled against a published core —
78/// out-of-crate consumers match with a wildcard arm.
79#[derive(Debug)]
80#[non_exhaustive]
81pub enum PortError {
82    /// Opening the database file failed.
83    OpenFailed {
84        /// Concrete, locatable detail.
85        detail: String,
86    },
87    /// The requested table does not exist.
88    TableNotFound(String),
89    /// A write operation failed.
90    WriteFailed {
91        /// Concrete, locatable detail.
92        detail: String,
93    },
94    /// The file is valid but uses a feature the backend does not support.
95    Unsupported {
96        /// What is unsupported and why.
97        reason: String,
98    },
99    /// A row window starts beyond the table's rows.
100    OutOfRange {
101        /// Name of the table addressed.
102        table: String,
103        /// First requested row index.
104        start: usize,
105        /// Requested window length.
106        len: usize,
107        /// Actual row count of the table.
108        row_count: usize,
109    },
110    /// The database is password-protected and no password was supplied.
111    PasswordRequired {
112        /// Filesystem path of the database.
113        path: String,
114    },
115    /// The supplied password does not match the stored database password.
116    WrongPassword {
117        /// Filesystem path of the database.
118        path: String,
119    },
120}
121
122impl std::fmt::Display for PortError {
123    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124        match self {
125            Self::OpenFailed { detail } => write!(f, "database open failed: {detail}"),
126            Self::TableNotFound(name) => write!(f, "table {name:?} not found"),
127            Self::WriteFailed { detail } => write!(f, "write failed: {detail}"),
128            Self::Unsupported { reason } => write!(f, "unsupported: {reason}"),
129            Self::OutOfRange {
130                table,
131                start,
132                len,
133                row_count,
134            } => write!(
135                f,
136                "row window {start}+{len} of {table:?} is out of range ({row_count} rows)"
137            ),
138            Self::PasswordRequired { path } => {
139                write!(f, "{path}: the database is password-protected")
140            }
141            Self::WrongPassword { path } => {
142                write!(f, "{path}: the database password is wrong")
143            }
144        }
145    }
146}
147
148impl std::error::Error for PortError {}
149
150/// One referential-integrity relationship between two tables.
151#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
152pub struct Relationship {
153    /// Table holding the referencing column.
154    pub table: String,
155    /// Referencing column.
156    pub column: String,
157    /// Referenced (one-side) table.
158    pub related_table: String,
159    /// Referenced column.
160    pub related_column: String,
161}
162
163/// Window selector for row reads.
164#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
165pub struct RowRange {
166    /// Zero-based index of the first row to read.
167    pub start: usize,
168    /// Maximum number of rows to read.
169    pub len: usize,
170}
171
172/// Metadata of a successfully opened database.
173#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
174pub struct OpenedDatabase {
175    /// Database name (file stem).
176    pub name: String,
177    /// Binary format family.
178    pub format: DatabaseFormat,
179    /// A sibling Access lock file detected at open (step 53.1, item M59):
180    /// the file NAME as data — Access may hold the database open, so the
181    /// session opened read-only. Language stays UI-owned (ADR-4); clients
182    /// localize the notice from this field. `None` = no lock file seen.
183    pub lock_file: Option<String>,
184}
185
186/// The storage seam of the command core. Implementors must be deterministic
187/// and must not block indefinitely.
188pub trait StoragePort {
189    /// Stable identity of this driver (phase 30, item M17; ADR-7).
190    /// Required (M22): a driver names itself — there is no default
191    /// identity, so a driver that forgets its declaration does not
192    /// compile instead of hiding behind `"generic"`.
193    fn driver_id(&self) -> DriverId;
194    /// Declarative capabilities of the open data source (phase 30, item
195    /// M17; ADR-7) — per opened session, because they depend on runtime
196    /// state. Required (M22): there is no default — the former
197    /// optimistic `DriverCapabilities::full()` fallback made a forgetful
198    /// driver *lie* about what it can do; now the compiler asks for the
199    /// declaration. Start from [`DriverCapabilities::none`] or
200    /// [`DriverCapabilities::full`] and state the deviations.
201    fn capabilities(&self) -> DriverCapabilities;
202    /// The source family this driver opens, when fixed for the driver
203    /// (M23). The default is `None` — a single-purpose driver names its
204    /// family; a multi-family driver (the Jet/ACE adapter) may too, with
205    /// its primary family. The detecting composite reads this for its
206    /// inventory's fallback entries.
207    fn source_format(&self) -> Option<DatabaseFormat> {
208        None
209    }
210    /// The driver inventory of this composition (M23): every driver it
211    /// can open with id, fixed family and offline capability summary.
212    /// The default is empty — a single-purpose port names itself via
213    /// [`StoragePort::driver_id`]; the detecting composite reports its
214    /// full registry so agents can pre-flight an open.
215    fn driver_inventory(&self) -> Vec<DriverInfo> {
216        Vec::new()
217    }
218    /// Opens the database at `path`; `password` is `None` for unprotected
219    /// files (step 16.2, M5). Backends without passwords ignore it.
220    fn open(
221        &mut self,
222        path: &std::path::Path,
223        password: Option<&str>,
224    ) -> Result<OpenedDatabase, PortError>;
225    /// Closes the open database (idempotent).
226    fn close(&mut self);
227    /// Creates a new, empty database file at `path` (phase 29, item H44).
228    /// The file must not exist yet; creating never opens the database —
229    /// the caller dispatches [`StoragePort::open`] afterwards. Backends
230    /// that cannot create files answer a precise `Unsupported` (the
231    /// default).
232    fn create_database(
233        &mut self,
234        path: &std::path::Path,
235        format: DatabaseFormat,
236    ) -> Result<(), PortError> {
237        let _ = (path, format);
238        Err(PortError::Unsupported {
239            reason: "creating database files is not supported by this backend".to_owned(),
240        })
241    }
242    /// Writes a copy of the open database to `path` (step 27.2, item
243    /// M14). `password` protects the copy; `None` writes an unencrypted
244    /// copy. The original stays untouched. Backends without a password
245    /// surface answer a precise `Unsupported` (the default).
246    fn save_as(&mut self, path: &std::path::Path, password: Option<&str>) -> Result<(), PortError> {
247        let _ = (path, password);
248        Err(PortError::Unsupported {
249            reason: "saving database copies is not supported by this backend".to_owned(),
250        })
251    }
252    /// Replaces the open database's password in place (step 27.2, item
253    /// M14); `None` removes the protection. Backends without a password
254    /// surface answer a precise `Unsupported` (the default).
255    fn change_password(&mut self, new_password: Option<&str>) -> Result<(), PortError> {
256        let _ = new_password;
257        Err(PortError::Unsupported {
258            reason: "changing database passwords is not supported by this backend".to_owned(),
259        })
260    }
261    /// Catalog of table definitions in storage order.
262    fn tables(&self) -> Vec<TableDef>;
263    /// Views that live in the opened file itself (phase 73, step 73.1;
264    /// item M90) — read-only, never DataCraft's design layer. The default
265    /// is empty: Jet/ACE and CSV carry none.
266    fn native_views(&self) -> Result<Vec<NativeView>, PortError> {
267        Ok(Vec::new())
268    }
269    /// Indexes that live in the opened file itself (phase 73, step 73.2;
270    /// item M90) — read-only inventory. The default is empty.
271    fn indexes(&self) -> Result<Vec<SourceIndex>, PortError> {
272        Ok(Vec::new())
273    }
274    /// Triggers that live in the opened file itself (phase 73, step 73.2;
275    /// item M90) — inventory only, never created or executed. The default
276    /// is empty.
277    fn triggers(&self) -> Result<Vec<SourceTrigger>, PortError> {
278        Ok(Vec::new())
279    }
280    /// Driver facts and the file's own health verdict (phase 73, step
281    /// 73.3; item M91). Read at open: the check is bounded and runs once.
282    /// The default reports no facts and a healthy file.
283    fn source_facts(&self) -> SourceFacts {
284        SourceFacts::default()
285    }
286    /// Native Access forms/reports named by the catalog (phase 74, step
287    /// 74.1; item M87) — read-only inventory, definitions never decoded.
288    /// The default is empty.
289    fn native_objects(&self) -> Result<Vec<NativeObject>, PortError> {
290        Ok(Vec::new())
291    }
292    /// Creates a table; errors on duplicate names.
293    fn create_table(&mut self, definition: &TableDef) -> Result<(), PortError>;
294    /// Drops a table; returns its canonical name.
295    fn drop_table(&mut self, name: &str) -> Result<String, PortError>;
296    /// Reads a window of rows as domain values. A window starting at
297    /// `row_count` is empty; anything beyond fails with
298    /// [`PortError::OutOfRange`].
299    fn read_rows(&self, table: &str, range: RowRange) -> Result<Vec<Vec<FieldValue>>, PortError>;
300    /// Appends one row (positional values, arity must match the table).
301    /// `Null` on an AutoNumber field means "engine-assigned".
302    fn insert_row(&mut self, table: &str, values: &[FieldValue]) -> Result<(), PortError>;
303    /// Appends a row image verbatim, including engine-assigned AutoNumber
304    /// values (step 28.1, item U14 — undo/redo restoration). The default
305    /// delegates to [`StoragePort::insert_row`]; backends that reject
306    /// user-supplied AutoNumbers override it with a validated restore path.
307    fn restore_row(&mut self, table: &str, values: &[FieldValue]) -> Result<(), PortError> {
308        self.insert_row(table, values)
309    }
310    /// Replaces the row at `row` (zero-based live-row index) with `values`
311    /// (step 11.10, H40). AutoNumber values are preserved by the caller.
312    fn update_row(
313        &mut self,
314        table: &str,
315        row: usize,
316        values: &[FieldValue],
317    ) -> Result<(), PortError>;
318    /// Deletes the row at `row` (zero-based live-row index).
319    fn delete_row(&mut self, table: &str, row: usize) -> Result<(), PortError>;
320    /// Applies a batch of row operations in order (step 11.11, M2). Indices
321    /// address the row set as it evolves through the batch. Backends apply
322    /// the batch atomically where they can (Jet does); the default applies
323    /// the operations one by one.
324    fn write_row_ops(&mut self, table: &str, ops: &[RowWriteOp]) -> Result<(), PortError> {
325        for op in ops {
326            match op {
327                RowWriteOp::Insert(values) => self.insert_row(table, values)?,
328                RowWriteOp::Update { row, values } => self.update_row(table, *row, values)?,
329                RowWriteOp::Delete { row } => self.delete_row(table, *row)?,
330            }
331        }
332        Ok(())
333    }
334    /// Alters one table's schema (step 24.3, item M9). Backends that cannot
335    /// rewrite their catalog answer a precise `Unsupported`.
336    fn alter_table(&mut self, table: &str, change: SchemaChange) -> Result<(), PortError> {
337        let _ = (table, change);
338        Err(PortError::Unsupported {
339            reason: "schema alteration is not supported by this backend".to_owned(),
340        })
341    }
342    /// Renames one table in place (phase 72, item M81); rows, indexes and
343    /// relationships follow the name. Gated on `alter_table` by the
344    /// dispatcher; backends that cannot rewrite their catalog answer a
345    /// precise `Unsupported` (the default).
346    fn rename_table(&mut self, table: &str, new_name: &str) -> Result<String, PortError> {
347        let _ = (table, new_name);
348        Err(PortError::Unsupported {
349            reason: "table renames are not supported by this backend".to_owned(),
350        })
351    }
352    /// Whether the backend can apply sidecar views natively (step 71.3,
353    /// item M93): SQLite compiles them to `CREATE VIEW`, while Jet/ACE/CSV
354    /// rely on the shadow SQL engine and answer `false`.
355    fn supports_native_views(&self) -> bool {
356        false
357    }
358    /// Creates one native view in the open database (step 71.3, M93).
359    /// Backends without a view engine answer a precise `Unsupported`.
360    fn create_native_view(&mut self, name: &str, sql: &str) -> Result<(), PortError> {
361        let _ = (name, sql);
362        Err(PortError::Unsupported {
363            reason: "native views are not supported by this backend".to_owned(),
364        })
365    }
366    /// Drops one native view (step 71.3, M93).
367    fn drop_native_view(&mut self, name: &str) -> Result<(), PortError> {
368        let _ = name;
369        Err(PortError::Unsupported {
370            reason: "native views are not supported by this backend".to_owned(),
371        })
372    }
373    /// Referential relationships of the open database.
374    fn relationships(&self) -> Result<Vec<Relationship>, PortError>;
375    /// Creates one referential relationship (step 32.3, item M16).
376    /// Backends without a relationship write path answer a precise
377    /// `Unsupported` (the default).
378    fn create_relationship(&mut self, relationship: &Relationship) -> Result<(), PortError> {
379        let _ = relationship;
380        Err(PortError::Unsupported {
381            reason: "creating relationships is not supported by this backend".to_owned(),
382        })
383    }
384    /// Deletes one referential relationship (step 32.3, item M16).
385    fn delete_relationship(&mut self, relationship: &Relationship) -> Result<(), PortError> {
386        let _ = relationship;
387        Err(PortError::Unsupported {
388            reason: "deleting relationships is not supported by this backend".to_owned(),
389        })
390    }
391    /// Native saved queries of the open database (step 15.3, M3).
392    /// Backends without native queries return an empty list.
393    fn native_queries(&self) -> Result<Vec<NativeQuery>, PortError> {
394        Ok(Vec::new())
395    }
396    /// Named macros of the open database (step 17.1, L4). Backends without
397    /// macros return an empty list; definitions are read-only.
398    fn macros(&self) -> Result<Vec<crate::macro_def::MacroDef>, PortError> {
399        Ok(Vec::new())
400    }
401    /// Extracted VBA project of the open database (step 17.2, L5).
402    /// Backends without a decoded project return `None`; extraction errors
403    /// are reported so the core can surface them without blocking the open.
404    fn vba_project(&self) -> Result<Option<crate::vba::VbaProject>, PortError> {
405        Ok(None)
406    }
407}
408
409/// One schema change of an `alter_table` call (step 24.3, item M9).
410///
411/// `#[non_exhaustive]` (M32): new alter operations are additive for
412/// out-of-crate drivers.
413#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
414#[non_exhaustive]
415pub enum SchemaChange {
416    /// Appends one field (the new last column).
417    AddField {
418        /// Full definition of the new field.
419        field: crate::schema::FieldDef,
420    },
421    /// Removes one field (name matched case-insensitively).
422    DropField {
423        /// Field name.
424        field: String,
425    },
426    /// Renames one field; row data is untouched.
427    RenameField {
428        /// Current field name (case-insensitive).
429        field: String,
430        /// New name.
431        new_name: String,
432    },
433    /// Replaces one field's properties (storage shape must not change).
434    SetFieldProperties {
435        /// Field name (case-insensitive).
436        field: String,
437        /// The new property set.
438        properties: crate::schema::FieldProperties,
439    },
440}
441
442pub use write_op::{RowWriteOp, window_slice};
443
444pub use crate::port_mock::MockPort;
445
446#[cfg(test)]
447mod tests;