datacraft-core 0.2.0

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
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
//! The command language (step 4.1, item H13; ARCHITECTURE §5): every
//! user-visible action — GUI or agent — is exactly one command. The enum
//! is `#[non_exhaustive]` (M22, ADR-7): adding a command is not a breaking
//! change for clients compiled against a published engine — out-of-crate
//! consumers match with a wildcard arm; wire (serde) consumers are
//! unaffected.

use crate::ObjectId;
use crate::canvas::Rect;
use crate::form::ControlDef;
use crate::ids::TabId;
use crate::qbe::{QbeRow, QbeValue, SortOrder};
use crate::schema::TableDef;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;

mod contexts;
pub use contexts::ExpressionContext;
mod source;
pub use source::{SourceSpec, effective_path};

/// One user-visible action.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum Command {
    /// Opens the database file at `path` (closes nothing implicitly —
    /// rejected while another database is open).
    OpenDatabase {
        /// Filesystem path of the `.mdb`/`.accdb` file.
        path: PathBuf,
        /// Database password for protected files (step 16.2, M5); absent
        /// or `None` for unprotected files.
        #[serde(default)]
        password: Option<String>,
        /// Source override (step 65.1, L8; ADR-7): `None` opens the plain
        /// `path` byte-identically; `Some` is authoritative and resolves
        /// through [`SourceSpec`] (`file://` opens like its path, other
        /// schemes answer the no-server-driver boundary).
        #[serde(default)]
        source: Option<SourceSpec>,
    },
    /// Closes the open database and all of its tabs.
    CloseDatabase,
    /// Creates a brand-new, empty database file (phase 29, H44). The file
    /// must not exist; dispatch `OpenDatabase` to open it afterwards.
    CreateDatabase {
        /// Filesystem path of the new file.
        path: PathBuf,
        /// Target format family (`Jet4` or `Sqlite`).
        format: crate::port::DatabaseFormat,
    },
    /// Materializes a shipped template into a brand-new file (phase 29,
    /// H44); the new database is not opened. Names come from `templates`.
    CreateDatabaseFromTemplate {
        /// Filesystem path of the new file.
        path: PathBuf,
        /// Registry name of the template (case-insensitive).
        template: String,
    },
    /// Writes a copy under `path` (step 27.2, M14): the original stays
    /// untouched; gated on the driver's `passwords` capability.
    SaveDatabaseAs {
        /// Filesystem path of the copy; it must not exist yet.
        path: PathBuf,
        /// New password, or `None` for an unencrypted copy.
        #[serde(default)]
        password: Option<String>,
    },
    /// Replaces the password in place (step 27.2, M14); the old password
    /// is the one the session was opened with.
    ChangePassword {
        /// New password, or `None` to remove the protection.
        #[serde(default)]
        new_password: Option<String>,
    },
    /// Creates a table from a validated definition.
    CreateTable {
        /// Full field-level definition.
        definition: TableDef,
    },
    /// Imports an external CSV file as a new table (step 58.1, M50):
    /// inferred kinds, filled over the write path — the source is never
    /// touched; GUI, CLI and MCP share this chain.
    ImportFile {
        /// The CSV file to read (header row + records).
        source: std::path::PathBuf,
        /// Target table name.
        table: String,
    },
    /// Drops a table (name matched case-insensitively); dependent tabs
    /// close first.
    DropTable {
        /// Table name as shown in the navigation pane.
        table: String,
    },
    /// Appends a field to a table (step 24.3, item M9). Existing rows get
    /// Null in the new column; the field must be nullable (or carry no
    /// engine-evaluable default).
    AddField {
        /// Target table (case-insensitive).
        table: String,
        /// Full definition of the new field.
        field: crate::schema::FieldDef,
    },
    /// Removes a field from a table (step 24.3, item M9).
    DropField {
        /// Target table (case-insensitive).
        table: String,
        /// Field name (case-insensitive).
        field: String,
    },
    /// Renames a field (step 24.3, item M9); row data is untouched.
    RenameField {
        /// Target table (case-insensitive).
        table: String,
        /// Current field name (case-insensitive).
        field: String,
        /// New field name.
        new_name: String,
    },
    /// Replaces a field's properties (step 24.3, item M9); the storage
    /// shape (type, size, kind) must not change.
    SetFieldProperties {
        /// Target table (case-insensitive).
        table: String,
        /// Field name (case-insensitive).
        field: String,
        /// The new property set.
        properties: crate::schema::FieldProperties,
    },
    /// Appends one row to a table (step 13.2, H36). Values are positional
    /// and validated against the table schema; `Null` on an AutoNumber
    /// field means "engine-assigned".
    InsertRow {
        /// Target table name (case-insensitive).
        table: String,
        /// Full positional value list (arity must match the table).
        values: Vec<crate::schema::FieldValue>,
    },
    /// Replaces one row by its zero-based live-row index (step 11.10,
    /// H40). `Null` on an AutoNumber field keeps the engine-assigned value;
    /// any other AutoNumber value must equal the current one.
    UpdateRow {
        /// Target table name (case-insensitive).
        table: String,
        /// Zero-based index in the table's live-row order.
        row: usize,
        /// Full positional value list (arity must match the table).
        values: Vec<crate::schema::FieldValue>,
    },
    /// Deletes one row by its zero-based live-row index (step 11.10, H40).
    DeleteRow {
        /// Target table name (case-insensitive).
        table: String,
        /// Zero-based index in the table's live-row order.
        row: usize,
    },
    /// Previews a journaled write batch and stores it for confirmation
    /// (step 11.11, M2). Nothing is written until `ApplyWrite`.
    /// Headless-only (L26): the GUI's equivalent is the immediate row
    /// commands plus Undo/Redo — a separate history, not journal tokens.
    PreviewWrite {
        /// Target table name (case-insensitive).
        table: String,
        /// The batch (indices address the evolving row set).
        ops: Vec<crate::journal::WriteOp>,
    },
    /// Applies the pending journal identified by `token` (step 11.11, M2).
    /// Headless-only — see `PreviewWrite` (L26).
    ApplyWrite {
        /// Token from the preview.
        token: u64,
    },
    /// Rolls the last applied journal back (inverse operations).
    /// Headless-only — see `PreviewWrite` (L26).
    RollbackWrite {
        /// Token of the applied journal.
        token: u64,
    },
    /// Discards the pending journal without writing anything.
    /// Headless-only — see `PreviewWrite` (L26).
    DiscardWrite {
        /// Token from the preview.
        token: u64,
    },
    /// Renames one table, query, form or report (step 72.1, item M81);
    /// tabs, sidecar references and relationships follow the new name.
    RenameObject {
        /// The object to rename (kind + current name).
        object: ObjectId,
        /// The new name, validated against the kind's namespace.
        new_name: String,
    },
    /// Opens the object in a tab (re-activates an existing tab instead of
    /// opening a second one for the same object).
    OpenObject(ObjectId),
    /// Opens the object in the given view (step 21.5, item U9) — the
    /// navigation pane's Open/Design context menu in one command.
    OpenObjectInMode {
        /// The object to open.
        object: ObjectId,
        /// The view the tab starts (or switches to) in.
        mode: crate::state::TabMode,
    },
    /// Closes one document tab; closing the active tab activates its
    /// next-lower neighbor.
    CloseTab {
        /// The tab to close.
        tab_id: TabId,
    },
    /// Closes every tab except `tab_id` and activates the kept tab
    /// (step 21.6, item U10).
    CloseOtherTabs {
        /// The tab to keep.
        tab_id: TabId,
    },
    /// Closes every document tab (step 21.6, item U10).
    CloseAllTabs,
    /// Makes an existing tab the active one.
    SetActiveTab {
        /// The tab to activate.
        tab_id: TabId,
    },
    /// Undoes the last row write of the open database (step 28.1, item U14).
    Undo,
    /// Re-applies the last undone row write (step 28.1, item U14).
    Redo,
    /// Switches one tab between its datasheet and design view (step 21.1,
    /// item U5) — the Views split-button's single action.
    SetTabMode {
        /// The tab whose view changes.
        tab_id: TabId,
        /// The target view.
        mode: crate::state::TabMode,
    },
    /// Shows or hides the navigation pane.
    ToggleNavPane,
    /// Shows or hides the relationship mapper (Access opens it as a
    /// document; the headless core models it as a pane flag).
    ToggleRelationshipMapper,
    /// Creates a referential relationship (step 32.3, item M16). The
    /// referencing and referenced tables/columns must exist; duplicates are
    /// refused.
    CreateRelationship {
        /// The relationship to create.
        relationship: crate::port::Relationship,
    },
    /// Deletes one referential relationship (step 32.3, item M16).
    DeleteRelationship {
        /// The relationship to delete.
        relationship: crate::port::Relationship,
    },
    /// Shows or hides the SQL window (step 13.3, H37).
    ToggleSqlWindow,
    /// Opens or closes the Import File window (step 58.1, item M50).
    ToggleImportWindow,
    /// Parses an external CSV file and stores its import preview for the
    /// Import File window (step 58.1, item M50). The source is only read.
    PreviewImportFile {
        /// The CSV file to read.
        source: std::path::PathBuf,
    },
    /// Shows or hides the expression window (step 41.2, item M46).
    ToggleExpressionWindow,
    /// Runs exactly one SELECT/WITH statement over the loaded tables in a
    /// throwaway in-memory SQLite database (step 13.3, H37).
    RunSql {
        /// The statement; anything but one SELECT/WITH is rejected.
        sql: String,
    },
    /// Evaluates one expression (step 15.2, H41) — over the bus, the MCP
    /// endpoint and the CLI alike. With a row context the expression sees
    /// the table's fields; without one it evaluates over an empty scope.
    RunExpression {
        /// Optional row context (table + zero-based live-row index).
        context: Option<ExpressionContext>,
        /// The expression source.
        expression: String,
    },
    /// Creates an empty query (name unique, case-insensitive).
    CreateQuery {
        /// Query name; rejected on duplicates.
        name: String,
    },
    /// Sidecar view design command (step 71.1, M93): create, set SQL, delete.
    View(crate::view::ViewCommand),
    /// Edits one QBE grid cell (identifiers are schema-whitelisted, LESSONS #5).
    SetQbeCell {
        /// Target query name (case-insensitive).
        query: String,
        /// Grid row.
        row: QbeRow,
        /// Grid column index.
        col: usize,
        /// New cell value.
        value: QbeValue,
    },
    /// Clears every QBE criterion cell of a query (step 11.5).
    ClearQbeCriteria {
        /// Target query name (case-insensitive).
        query: String,
    },
    /// Sorts a table's datasheet view (step 44.5, item M54): core view
    /// state projected into the tab snapshot; `SortOrder::None` clears.
    /// The field is whitelisted against the table's schema (LESSONS #5).
    SetTableSort {
        /// Target table name (case-insensitive).
        table: String,
        /// Sort field name.
        field: String,
        /// Sort direction; `None` clears the sort.
        order: SortOrder,
    },
    /// Filters a table's datasheet view by one column's criteria
    /// (step 44.5, item M54): parsed eagerly against the column's kind
    /// so garbage fails at set time; `None` clears that column's
    /// filter. Multiple filters AND together.
    SetTableFilter {
        /// Target table name (case-insensitive).
        table: String,
        /// Filtered field name.
        field: String,
        /// Criteria text, engine dialect; `None` clears the field.
        criteria: Option<String>,
    },
    /// Clears every column filter of a table's datasheet view
    /// (step 44.5, item M54) — the status bar chip's one-click clear.
    ClearTableFilters {
        /// Target table name (case-insensitive).
        table: String,
    },
    /// Deletes a query (dependent tabs close first).
    DeleteQuery {
        /// Query name (case-insensitive).
        name: String,
    },
    /// Executes a query; the row count is observable, the data itself is
    /// fetched via `Core::run_query`.
    RunQuery {
        /// Query name (case-insensitive).
        query: String,
    },
    /// Creates an empty form (name unique, case-insensitive).
    CreateForm {
        /// Form name; rejected on duplicates.
        name: String,
    },
    /// Deletes a form.
    DeleteForm {
        /// Form name (case-insensitive).
        name: String,
    },
    /// Places a control on a form (rect snapped to the form grid at
    /// dispatch, bounds-checked against the canvas).
    AddFormControl {
        /// Target form name (case-insensitive).
        form: String,
        /// The control definition.
        control: ControlDef,
    },
    /// Moves/resizes a control (snapped and bounds-checked).
    SetFormControlRect {
        /// Target form name (case-insensitive).
        form: String,
        /// Control name (case-insensitive).
        control: String,
        /// New canvas rect.
        rect: Rect,
    },
    /// Sets the content property a placed control carries (step 63.1,
    /// item H56): the label text, the button caption, the bound source
    /// field or the embedded form — one property per kind, by the model's
    /// design. An empty value clears a binding.
    SetFormControlContent {
        /// Target form name (case-insensitive).
        form: String,
        /// Control name (case-insensitive).
        control: String,
        /// The content value.
        value: String,
    },
    /// Sets the kind-specific design data of a placed form control (ADR-8).
    SetFormControlData(crate::form::SetControlData),
    /// Applies or clears the form's own WHERE filter (phase 64, M71; ADR-8).
    SetFormFilter(crate::form::FormFilter),
    /// Removes a control.
    DeleteFormControl {
        /// Target form name (case-insensitive).
        form: String,
        /// Control name (case-insensitive).
        control: String,
    },
    /// Adds a record-source field from the designer's field list as a
    /// bound control at the next free slot (step 11.6).
    AddFormField {
        /// Target form name (case-insensitive).
        form: String,
        /// Source field name (case-insensitive).
        field: String,
    },
    /// Assigns a form's source table (whitelisted; existing bindings
    /// re-validated).
    SetFormSource {
        /// Target form name (case-insensitive).
        form: String,
        /// Source table name.
        table: String,
    },
    /// Creates an empty report with print-oriented page defaults
    /// (step 10.4, item H31).
    CreateReport {
        /// Report name; rejected on duplicates.
        name: String,
    },
    /// Deletes a report.
    DeleteReport {
        /// Report name (case-insensitive).
        name: String,
    },
    /// Places a control on a report (page-bounds-checked, snapped).
    AddReportControl {
        /// Target report name (case-insensitive).
        report: String,
        /// The control definition.
        control: ControlDef,
    },
    /// Moves/resizes a report control (snapped, page-bounds-checked).
    SetReportControlRect {
        /// Target report name (case-insensitive).
        report: String,
        /// Control name (case-insensitive).
        control: String,
        /// New page rect.
        rect: Rect,
    },
    /// Sets the content property a placed report control carries (step
    /// 63.1, item H56): the label text, the button caption, the bound
    /// source field (or a `=expression`) or the embedded form. An empty
    /// value clears a binding.
    SetReportControlContent {
        /// Target report name (case-insensitive).
        report: String,
        /// Control name (case-insensitive).
        control: String,
        /// The content value.
        value: String,
    },
    /// Removes a report control.
    DeleteReportControl {
        /// Target report name (case-insensitive).
        report: String,
        /// Control name (case-insensitive).
        control: String,
    },
    /// Adds a record-source field from the designer's field list as a
    /// bound control at the next free page slot (step 11.6).
    AddReportField {
        /// Target report name (case-insensitive).
        report: String,
        /// Source field name (case-insensitive).
        field: String,
    },
    /// Assigns a report's source table (whitelisted; bindings re-validated).
    SetReportSource {
        /// Target report name (case-insensitive).
        report: String,
        /// Source table name.
        table: String,
    },
    /// Sets or clears a report's grouping field (step 15.2, H41): the
    /// field must exist in the source; `None` clears the grouping.
    SetReportGrouping {
        /// Target report name (case-insensitive).
        report: String,
        /// The grouping field, or `None` to ungroup.
        group_by: Option<String>,
    },
    /// Loads one VBA module's extracted source (step 17.2, L5). Read-only:
    /// the source is returned in `Event::VbaModuleLoaded`, never executed.
    LoadVbaModule {
        /// Module name (case-insensitive).
        module: String,
    },
    /// Runs the headless static VBA analysis (step 17.3, L5): call graph,
    /// embedded SQL, table/query references and unused modules.
    AnalyzeVba,
    /// Writes the design layer (forms + queries + reports) to a
    /// `.datacraft` sidecar file (ADR-1).
    SaveDesignDocument {
        /// Target file path.
        path: std::path::PathBuf,
    },
    /// Loads forms + queries from a sidecar file (version gate enforced;
    /// the database name must match the open database).
    LoadDesignDocument {
        /// Source file path.
        path: std::path::PathBuf,
    },
}