datacraft-core 0.1.3

Headless domain model, command bus and application state for DataCraft
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
//! Storage port (step 4.1, item H13).
//!
//! The command core performs storage I/O only through this core-owned
//! trait: real Jet/ACE backends implement it (dependency direction
//! `storage → core`), headless tests use [`MockPort`]. No storage type
//! crosses into the core vocabulary.

use serde::{Deserialize, Serialize};

use crate::schema::{FieldValue, TableDef};

mod capabilities;
mod native_query;
mod native_view;
mod source_objects;
mod write_op;

pub use capabilities::{DriverCapabilities, DriverId, DriverInfo, Support};
pub use native_query::{
    NativeJoin, NativeJoinKind, NativeOrder, NativeQuery, NativeQueryColumn, NativeQueryKind,
};
pub use native_view::NativeView;
pub use source_objects::{
    NativeObject, NativeObjectKind, SourceFact, SourceFacts, SourceIndex, SourceTrigger,
};

/// Binary format family of an opened database (domain vocabulary; the
/// backends map their physical version detection onto it).
///
/// `#[non_exhaustive]` (M32): ADR-7's server path adds format families;
/// clients match with a wildcard arm.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum DatabaseFormat {
    /// Jet 3.x (Access 97 and earlier).
    Jet3,
    /// Jet 4.x (Access 2000–2003).
    Jet4,
    /// ACE (Access 2007 and later, `.accdb`).
    Ace,
    /// SQLite 3 (`.sqlite`, `.db`) — the non-Jet source family (H35).
    Sqlite,
    /// A directory of `*.csv` files (phase 30, M18; ADR-7 driver proof).
    Csv,
}

/// Errors a storage backend reports through the port.
///
/// `#[non_exhaustive]` (M22, ADR-7): adding a variant is not a breaking
/// change for drivers or clients compiled against a published core —
/// out-of-crate consumers match with a wildcard arm.
#[derive(Debug)]
#[non_exhaustive]
pub enum PortError {
    /// Opening the database file failed.
    OpenFailed {
        /// Concrete, locatable detail.
        detail: String,
    },
    /// The requested table does not exist.
    TableNotFound(String),
    /// A write operation failed.
    WriteFailed {
        /// Concrete, locatable detail.
        detail: String,
    },
    /// The file is valid but uses a feature the backend does not support.
    Unsupported {
        /// What is unsupported and why.
        reason: String,
    },
    /// A row window starts beyond the table's rows.
    OutOfRange {
        /// Name of the table addressed.
        table: String,
        /// First requested row index.
        start: usize,
        /// Requested window length.
        len: usize,
        /// Actual row count of the table.
        row_count: usize,
    },
    /// The database is password-protected and no password was supplied.
    PasswordRequired {
        /// Filesystem path of the database.
        path: String,
    },
    /// The supplied password does not match the stored database password.
    WrongPassword {
        /// Filesystem path of the database.
        path: String,
    },
}

impl std::fmt::Display for PortError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::OpenFailed { detail } => write!(f, "database open failed: {detail}"),
            Self::TableNotFound(name) => write!(f, "table {name:?} not found"),
            Self::WriteFailed { detail } => write!(f, "write failed: {detail}"),
            Self::Unsupported { reason } => write!(f, "unsupported: {reason}"),
            Self::OutOfRange {
                table,
                start,
                len,
                row_count,
            } => write!(
                f,
                "row window {start}+{len} of {table:?} is out of range ({row_count} rows)"
            ),
            Self::PasswordRequired { path } => {
                write!(f, "{path}: the database is password-protected")
            }
            Self::WrongPassword { path } => {
                write!(f, "{path}: the database password is wrong")
            }
        }
    }
}

impl std::error::Error for PortError {}

/// One referential-integrity relationship between two tables.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Relationship {
    /// Table holding the referencing column.
    pub table: String,
    /// Referencing column.
    pub column: String,
    /// Referenced (one-side) table.
    pub related_table: String,
    /// Referenced column.
    pub related_column: String,
}

/// Window selector for row reads.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct RowRange {
    /// Zero-based index of the first row to read.
    pub start: usize,
    /// Maximum number of rows to read.
    pub len: usize,
}

/// Metadata of a successfully opened database.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OpenedDatabase {
    /// Database name (file stem).
    pub name: String,
    /// Binary format family.
    pub format: DatabaseFormat,
    /// A sibling Access lock file detected at open (step 53.1, item M59):
    /// the file NAME as data — Access may hold the database open, so the
    /// session opened read-only. Language stays UI-owned (ADR-4); clients
    /// localize the notice from this field. `None` = no lock file seen.
    pub lock_file: Option<String>,
}

/// The storage seam of the command core. Implementors must be deterministic
/// and must not block indefinitely.
pub trait StoragePort {
    /// Stable identity of this driver (phase 30, item M17; ADR-7).
    /// Required (M22): a driver names itself — there is no default
    /// identity, so a driver that forgets its declaration does not
    /// compile instead of hiding behind `"generic"`.
    fn driver_id(&self) -> DriverId;
    /// Declarative capabilities of the open data source (phase 30, item
    /// M17; ADR-7) — per opened session, because they depend on runtime
    /// state. Required (M22): there is no default — the former
    /// optimistic `DriverCapabilities::full()` fallback made a forgetful
    /// driver *lie* about what it can do; now the compiler asks for the
    /// declaration. Start from [`DriverCapabilities::none`] or
    /// [`DriverCapabilities::full`] and state the deviations.
    fn capabilities(&self) -> DriverCapabilities;
    /// The source family this driver opens, when fixed for the driver
    /// (M23). The default is `None` — a single-purpose driver names its
    /// family; a multi-family driver (the Jet/ACE adapter) may too, with
    /// its primary family. The detecting composite reads this for its
    /// inventory's fallback entries.
    fn source_format(&self) -> Option<DatabaseFormat> {
        None
    }
    /// The driver inventory of this composition (M23): every driver it
    /// can open with id, fixed family and offline capability summary.
    /// The default is empty — a single-purpose port names itself via
    /// [`StoragePort::driver_id`]; the detecting composite reports its
    /// full registry so agents can pre-flight an open.
    fn driver_inventory(&self) -> Vec<DriverInfo> {
        Vec::new()
    }
    /// Opens the database at `path`; `password` is `None` for unprotected
    /// files (step 16.2, M5). Backends without passwords ignore it.
    fn open(
        &mut self,
        path: &std::path::Path,
        password: Option<&str>,
    ) -> Result<OpenedDatabase, PortError>;
    /// Closes the open database (idempotent).
    fn close(&mut self);
    /// Creates a new, empty database file at `path` (phase 29, item H44).
    /// The file must not exist yet; creating never opens the database —
    /// the caller dispatches [`StoragePort::open`] afterwards. Backends
    /// that cannot create files answer a precise `Unsupported` (the
    /// default).
    fn create_database(
        &mut self,
        path: &std::path::Path,
        format: DatabaseFormat,
    ) -> Result<(), PortError> {
        let _ = (path, format);
        Err(PortError::Unsupported {
            reason: "creating database files is not supported by this backend".to_owned(),
        })
    }
    /// Writes a copy of the open database to `path` (step 27.2, item
    /// M14). `password` protects the copy; `None` writes an unencrypted
    /// copy. The original stays untouched. Backends without a password
    /// surface answer a precise `Unsupported` (the default).
    fn save_as(&mut self, path: &std::path::Path, password: Option<&str>) -> Result<(), PortError> {
        let _ = (path, password);
        Err(PortError::Unsupported {
            reason: "saving database copies is not supported by this backend".to_owned(),
        })
    }
    /// Replaces the open database's password in place (step 27.2, item
    /// M14); `None` removes the protection. Backends without a password
    /// surface answer a precise `Unsupported` (the default).
    fn change_password(&mut self, new_password: Option<&str>) -> Result<(), PortError> {
        let _ = new_password;
        Err(PortError::Unsupported {
            reason: "changing database passwords is not supported by this backend".to_owned(),
        })
    }
    /// Catalog of table definitions in storage order.
    fn tables(&self) -> Vec<TableDef>;
    /// Views that live in the opened file itself (phase 73, step 73.1;
    /// item M90) — read-only, never DataCraft's design layer. The default
    /// is empty: Jet/ACE and CSV carry none.
    fn native_views(&self) -> Result<Vec<NativeView>, PortError> {
        Ok(Vec::new())
    }
    /// Indexes that live in the opened file itself (phase 73, step 73.2;
    /// item M90) — read-only inventory. The default is empty.
    fn indexes(&self) -> Result<Vec<SourceIndex>, PortError> {
        Ok(Vec::new())
    }
    /// Triggers that live in the opened file itself (phase 73, step 73.2;
    /// item M90) — inventory only, never created or executed. The default
    /// is empty.
    fn triggers(&self) -> Result<Vec<SourceTrigger>, PortError> {
        Ok(Vec::new())
    }
    /// Driver facts and the file's own health verdict (phase 73, step
    /// 73.3; item M91). Read at open: the check is bounded and runs once.
    /// The default reports no facts and a healthy file.
    fn source_facts(&self) -> SourceFacts {
        SourceFacts::default()
    }
    /// Native Access forms/reports named by the catalog (phase 74, step
    /// 74.1; item M87) — read-only inventory, definitions never decoded.
    /// The default is empty.
    fn native_objects(&self) -> Result<Vec<NativeObject>, PortError> {
        Ok(Vec::new())
    }
    /// Creates a table; errors on duplicate names.
    fn create_table(&mut self, definition: &TableDef) -> Result<(), PortError>;
    /// Drops a table; returns its canonical name.
    fn drop_table(&mut self, name: &str) -> Result<String, PortError>;
    /// Reads a window of rows as domain values. A window starting at
    /// `row_count` is empty; anything beyond fails with
    /// [`PortError::OutOfRange`].
    fn read_rows(&self, table: &str, range: RowRange) -> Result<Vec<Vec<FieldValue>>, PortError>;
    /// Appends one row (positional values, arity must match the table).
    /// `Null` on an AutoNumber field means "engine-assigned".
    fn insert_row(&mut self, table: &str, values: &[FieldValue]) -> Result<(), PortError>;
    /// Appends a row image verbatim, including engine-assigned AutoNumber
    /// values (step 28.1, item U14 — undo/redo restoration). The default
    /// delegates to [`StoragePort::insert_row`]; backends that reject
    /// user-supplied AutoNumbers override it with a validated restore path.
    fn restore_row(&mut self, table: &str, values: &[FieldValue]) -> Result<(), PortError> {
        self.insert_row(table, values)
    }
    /// Replaces the row at `row` (zero-based live-row index) with `values`
    /// (step 11.10, H40). AutoNumber values are preserved by the caller.
    fn update_row(
        &mut self,
        table: &str,
        row: usize,
        values: &[FieldValue],
    ) -> Result<(), PortError>;
    /// Deletes the row at `row` (zero-based live-row index).
    fn delete_row(&mut self, table: &str, row: usize) -> Result<(), PortError>;
    /// Applies a batch of row operations in order (step 11.11, M2). Indices
    /// address the row set as it evolves through the batch. Backends apply
    /// the batch atomically where they can (Jet does); the default applies
    /// the operations one by one.
    fn write_row_ops(&mut self, table: &str, ops: &[RowWriteOp]) -> Result<(), PortError> {
        for op in ops {
            match op {
                RowWriteOp::Insert(values) => self.insert_row(table, values)?,
                RowWriteOp::Update { row, values } => self.update_row(table, *row, values)?,
                RowWriteOp::Delete { row } => self.delete_row(table, *row)?,
            }
        }
        Ok(())
    }
    /// Alters one table's schema (step 24.3, item M9). Backends that cannot
    /// rewrite their catalog answer a precise `Unsupported`.
    fn alter_table(&mut self, table: &str, change: SchemaChange) -> Result<(), PortError> {
        let _ = (table, change);
        Err(PortError::Unsupported {
            reason: "schema alteration is not supported by this backend".to_owned(),
        })
    }
    /// Renames one table in place (phase 72, item M81); rows, indexes and
    /// relationships follow the name. Gated on `alter_table` by the
    /// dispatcher; backends that cannot rewrite their catalog answer a
    /// precise `Unsupported` (the default).
    fn rename_table(&mut self, table: &str, new_name: &str) -> Result<String, PortError> {
        let _ = (table, new_name);
        Err(PortError::Unsupported {
            reason: "table renames are not supported by this backend".to_owned(),
        })
    }
    /// Whether the backend can apply sidecar views natively (step 71.3,
    /// item M93): SQLite compiles them to `CREATE VIEW`, while Jet/ACE/CSV
    /// rely on the shadow SQL engine and answer `false`.
    fn supports_native_views(&self) -> bool {
        false
    }
    /// Creates one native view in the open database (step 71.3, M93).
    /// Backends without a view engine answer a precise `Unsupported`.
    fn create_native_view(&mut self, name: &str, sql: &str) -> Result<(), PortError> {
        let _ = (name, sql);
        Err(PortError::Unsupported {
            reason: "native views are not supported by this backend".to_owned(),
        })
    }
    /// Drops one native view (step 71.3, M93).
    fn drop_native_view(&mut self, name: &str) -> Result<(), PortError> {
        let _ = name;
        Err(PortError::Unsupported {
            reason: "native views are not supported by this backend".to_owned(),
        })
    }
    /// Referential relationships of the open database.
    fn relationships(&self) -> Result<Vec<Relationship>, PortError>;
    /// Creates one referential relationship (step 32.3, item M16).
    /// Backends without a relationship write path answer a precise
    /// `Unsupported` (the default).
    fn create_relationship(&mut self, relationship: &Relationship) -> Result<(), PortError> {
        let _ = relationship;
        Err(PortError::Unsupported {
            reason: "creating relationships is not supported by this backend".to_owned(),
        })
    }
    /// Deletes one referential relationship (step 32.3, item M16).
    fn delete_relationship(&mut self, relationship: &Relationship) -> Result<(), PortError> {
        let _ = relationship;
        Err(PortError::Unsupported {
            reason: "deleting relationships is not supported by this backend".to_owned(),
        })
    }
    /// Native saved queries of the open database (step 15.3, M3).
    /// Backends without native queries return an empty list.
    fn native_queries(&self) -> Result<Vec<NativeQuery>, PortError> {
        Ok(Vec::new())
    }
    /// Named macros of the open database (step 17.1, L4). Backends without
    /// macros return an empty list; definitions are read-only.
    fn macros(&self) -> Result<Vec<crate::macro_def::MacroDef>, PortError> {
        Ok(Vec::new())
    }
    /// Extracted VBA project of the open database (step 17.2, L5).
    /// Backends without a decoded project return `None`; extraction errors
    /// are reported so the core can surface them without blocking the open.
    fn vba_project(&self) -> Result<Option<crate::vba::VbaProject>, PortError> {
        Ok(None)
    }
}

/// One schema change of an `alter_table` call (step 24.3, item M9).
///
/// `#[non_exhaustive]` (M32): new alter operations are additive for
/// out-of-crate drivers.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum SchemaChange {
    /// Appends one field (the new last column).
    AddField {
        /// Full definition of the new field.
        field: crate::schema::FieldDef,
    },
    /// Removes one field (name matched case-insensitively).
    DropField {
        /// Field name.
        field: String,
    },
    /// Renames one field; row data is untouched.
    RenameField {
        /// Current field name (case-insensitive).
        field: String,
        /// New name.
        new_name: String,
    },
    /// Replaces one field's properties (storage shape must not change).
    SetFieldProperties {
        /// Field name (case-insensitive).
        field: String,
        /// The new property set.
        properties: crate::schema::FieldProperties,
    },
}

pub use write_op::{RowWriteOp, window_slice};

pub use crate::port_mock::MockPort;

#[cfg(test)]
mod tests;