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}