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;