Skip to main content

dibs_proto/
lib.rs

1//! Protocol definitions for dibs CLI-to-service communication.
2//!
3//! This crate defines the vox service interface between the `dibs` CLI
4//! and the user's db crate (e.g., `my-app-db`).
5//!
6//! The db crate runs as a short-lived vox service, responding to
7//! schema and migration queries from the CLI.
8
9// The `vox::service` macro expands to handler functions whose Result Err
10// type is `vox::VoxError<DibsError>` — large enough that newer clippy
11// flags `result_large_err`. The shape comes from upstream `vox`, not
12// anything we can box without touching that crate.
13#![allow(clippy::result_large_err)]
14
15use facet::Facet;
16use vox::service;
17
18/// Schema information for a table.
19#[derive(Debug, Clone, Facet)]
20pub struct TableInfo {
21    /// Table name
22    pub name: String,
23    /// Column definitions
24    pub columns: Vec<ColumnInfo>,
25    /// Foreign key constraints
26    pub foreign_keys: Vec<ForeignKeyInfo>,
27    /// Indices
28    pub indices: Vec<IndexInfo>,
29    /// Source file (if known)
30    pub source_file: Option<String>,
31    /// Source line (if known)
32    pub source_line: Option<u32>,
33    /// Doc comment (if any)
34    pub doc: Option<String>,
35    /// Lucide icon name for display in admin UI
36    pub icon: Option<String>,
37}
38
39/// Column information.
40#[derive(Debug, Clone, Facet)]
41pub struct ColumnInfo {
42    /// Column name
43    pub name: String,
44    /// SQL type (e.g., "BIGINT", "TEXT")
45    pub sql_type: String,
46    /// Rust type name (e.g., "i64", "String", "jiff::Timestamp")
47    pub rust_type: Option<String>,
48    /// Whether the column is nullable
49    pub nullable: bool,
50    /// Default value expression (if any)
51    pub default: Option<String>,
52    /// Whether this is a primary key
53    pub primary_key: bool,
54    /// Whether this has a unique constraint
55    pub unique: bool,
56    /// Whether this column is auto-generated (serial, uuid default, etc.)
57    pub auto_generated: bool,
58    /// Whether this is a long text field (use textarea instead of input)
59    pub long: bool,
60    /// Whether this column should be used as the display label for the row
61    pub label: bool,
62    /// Enum variants (if this is an enum type)
63    pub enum_variants: Vec<String>,
64    /// Doc comment (if any)
65    pub doc: Option<String>,
66    /// Language/format for code editor (e.g., "markdown", "json")
67    pub lang: Option<String>,
68    /// Lucide icon name for display in admin UI
69    pub icon: Option<String>,
70    /// Semantic subtype of the column (e.g., "email", "url", "password")
71    pub subtype: Option<String>,
72}
73
74/// Foreign key information.
75#[derive(Debug, Clone, Facet)]
76pub struct ForeignKeyInfo {
77    /// Columns in this table
78    pub columns: Vec<String>,
79    /// Referenced table
80    pub references_table: String,
81    /// Referenced columns
82    pub references_columns: Vec<String>,
83}
84
85/// A column in an index with optional sort order and nulls ordering.
86#[derive(Debug, Clone, Facet)]
87pub struct IndexColumnInfo {
88    /// Column name
89    pub name: String,
90    /// Sort order: "asc" or "desc"
91    pub order: String,
92    /// Nulls ordering: "default", "first", or "last"
93    pub nulls: String,
94}
95
96/// Index information.
97#[derive(Debug, Clone, Facet)]
98pub struct IndexInfo {
99    /// Index name
100    pub name: String,
101    /// Columns in the index with sort order
102    pub columns: Vec<IndexColumnInfo>,
103    /// Whether this is a unique index
104    pub unique: bool,
105    /// Optional WHERE clause for partial indexes
106    pub where_clause: Option<String>,
107}
108
109/// The full schema (list of tables).
110#[derive(Debug, Clone, Facet)]
111pub struct SchemaInfo {
112    /// All tables in the schema
113    pub tables: Vec<TableInfo>,
114}
115
116/// A single schema change.
117#[derive(Debug, Clone, Facet)]
118pub struct ChangeInfo {
119    /// Human-readable description of the change
120    pub description: String,
121    /// Change kind (for coloring/icons)
122    pub kind: ChangeKind,
123}
124
125/// Kind of schema change.
126#[derive(Debug, Clone, Copy, Facet)]
127#[repr(u8)]
128pub enum ChangeKind {
129    /// Something is being added
130    Add = 0,
131    /// Something is being removed
132    Drop = 1,
133    /// Something is being modified
134    Alter = 2,
135}
136
137/// Diff result for a single table.
138#[derive(Debug, Clone, Facet)]
139pub struct TableDiffInfo {
140    /// Table name
141    pub table: String,
142    /// Changes for this table
143    pub changes: Vec<ChangeInfo>,
144}
145
146/// Full diff result.
147#[derive(Debug, Clone, Facet)]
148pub struct DiffResult {
149    /// Diffs organized by table
150    pub table_diffs: Vec<TableDiffInfo>,
151}
152
153/// Migration status.
154#[derive(Debug, Clone, Facet)]
155pub struct MigrationInfo {
156    /// Migration version/name
157    pub version: String,
158    /// Human-readable name
159    pub name: String,
160    /// Whether this migration has been applied
161    pub applied: bool,
162    /// When it was applied (if applied)
163    pub applied_at: Option<String>,
164    /// Source file path (if known)
165    pub source_file: Option<String>,
166    /// Source code (if available)
167    pub source: Option<String>,
168}
169
170/// Request to diff schema against a database.
171#[derive(Debug, Clone, Facet)]
172pub struct DiffRequest {
173    /// Database connection URL
174    pub database_url: String,
175}
176
177/// Request to get migration status.
178#[derive(Debug, Clone, Facet)]
179pub struct MigrationStatusRequest {
180    /// Database connection URL
181    pub database_url: String,
182}
183
184/// Request to run migrations.
185#[derive(Debug, Clone, Facet)]
186pub struct MigrateRequest {
187    /// Database connection URL
188    pub database_url: String,
189    /// Specific migration to run (if None, run all pending)
190    pub migration: Option<String>,
191}
192
193/// A migration that was already applied before this run.
194#[derive(Debug, Clone, Facet)]
195pub struct AppliedMigration {
196    /// Migration version
197    pub version: String,
198    /// When it was applied
199    pub applied_at: String,
200}
201
202/// A migration that was just run.
203#[derive(Debug, Clone, Facet)]
204pub struct RanMigration {
205    /// Migration version
206    pub version: String,
207    /// How long it took to run in milliseconds
208    pub duration_ms: u64,
209}
210
211/// Result of running migrations.
212#[derive(Debug, Clone, Facet)]
213pub struct MigrateResult {
214    /// Total number of migrations defined
215    pub total_defined: u32,
216    /// Migrations that were already applied before this run
217    pub already_applied: Vec<AppliedMigration>,
218    /// Migrations that were applied in this run
219    pub applied: Vec<RanMigration>,
220    /// Time spent establishing which migrations to run (init + query) in milliseconds
221    pub setup_ms: u64,
222    /// Total execution time in milliseconds (setup + all migrations)
223    pub total_time_ms: u64,
224}
225
226/// Log message streamed during migration.
227#[derive(Debug, Clone, Facet)]
228pub struct MigrationLog {
229    /// Log level
230    pub level: LogLevel,
231    /// Message
232    pub message: String,
233    /// Migration this log is from (if applicable)
234    pub migration: Option<String>,
235}
236
237/// Log level.
238#[derive(Debug, Clone, Copy, Facet)]
239#[repr(u8)]
240pub enum LogLevel {
241    /// Debug information
242    Debug = 0,
243    /// Informational message
244    Info = 1,
245    /// Warning
246    Warn = 2,
247    /// Error
248    Error = 3,
249}
250
251/// SQL error with context for rich error display.
252#[derive(Debug, Clone, Facet)]
253pub struct SqlError {
254    /// The error message
255    pub message: String,
256    /// The SQL that caused the error (if available)
257    pub sql: Option<String>,
258    /// Position in the SQL where the error occurred (1-indexed byte offset)
259    pub position: Option<u32>,
260    /// Hint from postgres (if any)
261    pub hint: Option<String>,
262    /// Detail from postgres (if any)
263    pub detail: Option<String>,
264    /// Source location where the error occurred (file:line:col)
265    pub caller: Option<String>,
266}
267
268/// Error from the dibs service.
269#[derive(Debug, Clone, Facet)]
270#[repr(u8)]
271pub enum DibsError {
272    /// Database connection failed
273    ConnectionFailed(String) = 0,
274    /// Migration failed with SQL context
275    MigrationFailed(SqlError) = 1,
276    /// Invalid request
277    InvalidRequest(String) = 2,
278    /// Unknown table
279    UnknownTable(String) = 3,
280    /// Unknown column
281    UnknownColumn(String) = 4,
282    /// Query error
283    QueryError(String) = 5,
284}
285
286// =============================================================================
287// Backoffice types
288// =============================================================================
289
290/// A runtime value for backoffice queries.
291///
292/// Mirrors the internal dibs::query::Value type for wire transmission.
293#[derive(Debug, Clone, Facet)]
294#[repr(u8)]
295pub enum Value {
296    /// NULL
297    Null = 0,
298    /// Boolean
299    Bool(bool) = 1,
300    /// 16-bit integer
301    I16(i16) = 2,
302    /// 32-bit integer
303    I32(i32) = 3,
304    /// 64-bit integer
305    I64(i64) = 4,
306    /// 32-bit float
307    F32(f32) = 5,
308    /// 64-bit float
309    F64(f64) = 6,
310    /// String
311    String(String) = 7,
312    /// Binary data
313    Bytes(Vec<u8>) = 8,
314}
315
316/// A row of data as field name → value pairs.
317#[derive(Debug, Clone, Facet)]
318pub struct Row {
319    /// Fields in the row
320    pub fields: Vec<RowField>,
321}
322
323/// A single field in a row.
324#[derive(Debug, Clone, Facet)]
325pub struct RowField {
326    /// Field name
327    pub name: String,
328    /// Field value
329    pub value: Value,
330}
331
332/// Filter operator for backoffice queries.
333#[derive(Debug, Clone, Copy, Facet)]
334#[repr(u8)]
335pub enum FilterOp {
336    /// Equal (=)
337    Eq,
338    /// Not equal (!=)
339    Ne,
340    /// Less than (<)
341    Lt,
342    /// Less than or equal (<=)
343    Lte,
344    /// Greater than (>)
345    Gt,
346    /// Greater than or equal (>=)
347    Gte,
348    /// LIKE pattern match
349    Like,
350    /// Case-insensitive LIKE
351    ILike,
352    /// IS NULL
353    IsNull,
354    /// IS NOT NULL
355    IsNotNull,
356    /// IN (value1, value2, ...) - uses `values` field instead of `value`
357    In,
358    /// JSONB get object operator (->)
359    JsonGet,
360    /// JSONB get text operator (->>)
361    JsonGetText,
362    /// Contains operator (@>)
363    Contains,
364    /// Key exists operator (?)
365    KeyExists,
366}
367
368/// A single filter condition.
369#[derive(Debug, Clone, Facet)]
370pub struct Filter {
371    /// Column name
372    pub field: String,
373    /// Operator
374    pub op: FilterOp,
375    /// Value to compare (ignored for IsNull/IsNotNull/In)
376    pub value: Value,
377    /// Values for IN operator
378    pub values: Vec<Value>,
379}
380
381/// Sort direction.
382#[derive(Debug, Clone, Copy, Facet)]
383#[repr(u8)]
384pub enum SortDir {
385    /// Ascending
386    Asc = 0,
387    /// Descending
388    Desc = 1,
389}
390
391/// A sort clause.
392#[derive(Debug, Clone, Facet)]
393pub struct Sort {
394    /// Column name
395    pub field: String,
396    /// Direction
397    pub dir: SortDir,
398}
399
400/// Request to list rows from a table.
401#[derive(Debug, Clone, Facet)]
402pub struct ListRequest {
403    /// Table name
404    pub table: String,
405    /// Filter conditions (ANDed together)
406    pub filters: Vec<Filter>,
407    /// Sort order
408    pub sort: Vec<Sort>,
409    /// Maximum rows to return
410    pub limit: Option<u32>,
411    /// Offset for pagination
412    pub offset: Option<u32>,
413    /// Columns to select (empty = all)
414    pub select: Vec<String>,
415}
416
417/// Response from listing rows.
418#[derive(Debug, Clone, Facet)]
419pub struct ListResponse {
420    /// The rows
421    pub rows: Vec<Row>,
422    /// Total count (if requested)
423    pub total: Option<u64>,
424}
425
426/// Request to get a single row by primary key.
427#[derive(Debug, Clone, Facet)]
428pub struct GetRequest {
429    /// Table name
430    pub table: String,
431    /// Primary key value
432    pub pk: Value,
433}
434
435/// Request to create a new row.
436#[derive(Debug, Clone, Facet)]
437pub struct CreateRequest {
438    /// Table name
439    pub table: String,
440    /// Row data
441    pub data: Row,
442}
443
444/// Request to update a row.
445#[derive(Debug, Clone, Facet)]
446pub struct UpdateRequest {
447    /// Table name
448    pub table: String,
449    /// Primary key value
450    pub pk: Value,
451    /// Fields to update
452    pub data: Row,
453}
454
455/// Request to delete a row.
456#[derive(Debug, Clone, Facet)]
457pub struct DeleteRequest {
458    /// Table name
459    pub table: String,
460    /// Primary key value
461    pub pk: Value,
462}
463
464/// The dibs service trait.
465///
466/// Implemented by the user's db crate, called by the dibs CLI.
467#[service]
468pub trait DibsService {
469    /// Get the schema defined in Rust code.
470    async fn schema(&self) -> SchemaInfo;
471
472    /// Diff the Rust schema against a live database.
473    async fn diff(&self, request: DiffRequest) -> Result<DiffResult, DibsError>;
474
475    /// Generate migration SQL from a diff against the database.
476    async fn generate_migration_sql(&self, request: DiffRequest) -> Result<String, DibsError>;
477
478    /// Get migration status (applied vs pending).
479    async fn migration_status(
480        &self,
481        request: MigrationStatusRequest,
482    ) -> Result<Vec<MigrationInfo>, DibsError>;
483
484    /// Run migrations, streaming logs back.
485    async fn migrate(
486        &self,
487        request: MigrateRequest,
488        logs: vox::Tx<MigrationLog>,
489    ) -> Result<MigrateResult, DibsError>;
490}
491
492/// The Squel service trait - the data plane.
493///
494/// Provides generic CRUD operations for any registered table.
495/// Used by admin UIs that dynamically discover and interact with the schema.
496///
497/// Named "Squel" as a cute play on SQL.
498#[service]
499pub trait SquelService {
500    /// Get the schema for all registered tables.
501    async fn schema(&self) -> SchemaInfo;
502
503    /// List rows from a table with filtering, sorting, and pagination.
504    async fn list(&self, request: ListRequest) -> Result<ListResponse, DibsError>;
505
506    /// Get a single row by primary key.
507    async fn get(&self, request: GetRequest) -> Result<Option<Row>, DibsError>;
508
509    /// Create a new row.
510    async fn create(&self, request: CreateRequest) -> Result<Row, DibsError>;
511
512    /// Update an existing row.
513    async fn update(&self, request: UpdateRequest) -> Result<Row, DibsError>;
514
515    /// Delete a row.
516    async fn delete(&self, request: DeleteRequest) -> Result<u64, DibsError>;
517}