Skip to main content

nautilus_schema/
ir.rs

1//! Intermediate representation (IR) of a validated schema.
2//!
3//! This module defines a provider-agnostic IR that represents a schema after
4//! semantic validation. All type references are resolved, relations are validated,
5//! and both logical and physical names are stored explicitly.
6
7pub use crate::ast::ComputedKind;
8use crate::ast::{ReferentialAction, StorageStrategy};
9use crate::span::Span;
10use std::collections::HashMap;
11use std::fmt;
12use std::str::FromStr;
13
14/// Validated intermediate representation of a complete schema.
15#[derive(Debug, Clone, PartialEq)]
16pub struct SchemaIr {
17    /// The datasource declaration (if present).
18    pub datasource: Option<DatasourceIr>,
19    /// The generator declaration (if present).
20    pub generator: Option<GeneratorIr>,
21    /// All models in the schema, indexed by logical name.
22    pub models: HashMap<String, ModelIr>,
23    /// All enums in the schema, indexed by logical name.
24    pub enums: HashMap<String, EnumIr>,
25    /// All composite types in the schema, indexed by logical name.
26    pub composite_types: HashMap<String, CompositeTypeIr>,
27}
28
29impl SchemaIr {
30    /// Creates a new empty schema IR.
31    pub fn new() -> Self {
32        Self {
33            datasource: None,
34            generator: None,
35            models: HashMap::new(),
36            enums: HashMap::new(),
37            composite_types: HashMap::new(),
38        }
39    }
40
41    /// Gets a model by logical name.
42    pub fn get_model(&self, name: &str) -> Option<&ModelIr> {
43        self.models.get(name)
44    }
45
46    /// Gets an enum by logical name.
47    pub fn get_enum(&self, name: &str) -> Option<&EnumIr> {
48        self.enums.get(name)
49    }
50
51    /// Gets a composite type by logical name.
52    pub fn get_composite_type(&self, name: &str) -> Option<&CompositeTypeIr> {
53        self.composite_types.get(name)
54    }
55}
56
57impl Default for SchemaIr {
58    fn default() -> Self {
59        Self::new()
60    }
61}
62
63/// Validated datasource configuration.
64#[derive(Debug, Clone, PartialEq)]
65pub struct DatasourceIr {
66    /// The datasource name (e.g., "db").
67    pub name: String,
68    /// The provider (e.g., "postgresql", "mysql", "sqlite").
69    pub provider: String,
70    /// The connection URL (may contain env() references).
71    pub url: String,
72    /// Span of the datasource block.
73    pub span: Span,
74}
75
76/// Whether the generated client API uses async or sync methods.
77#[derive(Debug, Clone, PartialEq, Eq, Default)]
78pub enum InterfaceKind {
79    /// Synchronous API (default). Methods are plain `fn`, Rust uses
80    /// `tokio::task::block_in_place` internally; Python uses `asyncio.run()`.
81    #[default]
82    Sync,
83    /// Asynchronous API. Methods are `async fn` in Rust and `async def` in Python.
84    Async,
85}
86
87/// Validated generator configuration.
88#[derive(Debug, Clone, PartialEq)]
89pub struct GeneratorIr {
90    /// The generator name (e.g., "client").
91    pub name: String,
92    /// The provider (e.g., "nautilus-client-rs").
93    pub provider: String,
94    /// The output path (if specified).
95    pub output: Option<String>,
96    /// Whether to generate a sync or async client interface.
97    /// Defaults to [`InterfaceKind::Sync`] when the `interface` field is omitted.
98    pub interface: InterfaceKind,
99    /// Depth of recursive include TypedDicts generated for the Python client.
100    pub recursive_type_depth: usize,
101    /// Span of the generator block.
102    pub span: Span,
103}
104
105/// Validated model with fully resolved fields and metadata.
106#[derive(Debug, Clone, PartialEq)]
107pub struct ModelIr {
108    /// The logical name as defined in the schema (e.g., "User").
109    pub logical_name: String,
110    /// The physical database table name (from @@map or logical_name).
111    pub db_name: String,
112    /// All fields in the model.
113    pub fields: Vec<FieldIr>,
114    /// Primary key metadata.
115    pub primary_key: PrimaryKeyIr,
116    /// Unique constraints (from @unique and @@unique).
117    pub unique_constraints: Vec<UniqueConstraintIr>,
118    /// Indexes (from @@index).
119    pub indexes: Vec<IndexIr>,
120    /// Table-level CHECK constraint expressions (SQL strings).
121    pub check_constraints: Vec<String>,
122    /// Span of the model declaration.
123    pub span: Span,
124}
125
126impl ModelIr {
127    /// Finds a field by logical name.
128    pub fn find_field(&self, name: &str) -> Option<&FieldIr> {
129        self.fields.iter().find(|f| f.logical_name == name)
130    }
131
132    /// Returns an iterator over scalar fields (non-relations).
133    pub fn scalar_fields(&self) -> impl Iterator<Item = &FieldIr> {
134        self.fields
135            .iter()
136            .filter(|f| !matches!(f.field_type, ResolvedFieldType::Relation(_)))
137    }
138
139    /// Returns an iterator over relation fields.
140    pub fn relation_fields(&self) -> impl Iterator<Item = &FieldIr> {
141        self.fields
142            .iter()
143            .filter(|f| matches!(f.field_type, ResolvedFieldType::Relation(_)))
144    }
145}
146
147/// Validated field with resolved type.
148#[derive(Debug, Clone, PartialEq)]
149pub struct FieldIr {
150    /// The logical field name as defined in the schema (e.g., "userId").
151    pub logical_name: String,
152    /// The physical database column name (from @map or logical_name).
153    pub db_name: String,
154    /// The resolved field type (scalar, enum, or relation).
155    pub field_type: ResolvedFieldType,
156    /// Whether the field is required (not optional and not array).
157    pub is_required: bool,
158    /// Whether the field is an array.
159    pub is_array: bool,
160    /// Storage strategy for array fields (None for non-arrays or native support).
161    pub storage_strategy: Option<StorageStrategy>,
162    /// Default value (if specified via @default).
163    pub default_value: Option<DefaultValue>,
164    /// Whether the field has @unique.
165    pub is_unique: bool,
166    /// Whether the field has @updatedAt — auto-set to now() on every write.
167    pub is_updated_at: bool,
168    /// Computed column expression and kind — `None` for regular fields.
169    pub computed: Option<(String, ComputedKind)>,
170    /// Column-level CHECK constraint expression (SQL string). `None` for unconstrained fields.
171    pub check: Option<String>,
172    /// Span of the field declaration.
173    pub span: Span,
174}
175
176/// Resolved field type after validation.
177#[derive(Debug, Clone, PartialEq)]
178pub enum ResolvedFieldType {
179    /// A scalar type (String, Int, etc.).
180    Scalar(ScalarType),
181    /// An enum type with the enum's logical name.
182    Enum {
183        /// The logical name of the enum.
184        enum_name: String,
185    },
186    /// A relation to another model.
187    Relation(RelationIr),
188    /// A composite type (embedded struct).
189    CompositeType {
190        /// The logical name of the composite type.
191        type_name: String,
192    },
193}
194
195/// Scalar type enumeration.
196#[derive(Debug, Clone, Copy, PartialEq, Eq)]
197pub enum ScalarType {
198    /// UTF-8 string type.
199    String,
200    /// Boolean type (true/false).
201    Boolean,
202    /// 32-bit integer.
203    Int,
204    /// 64-bit integer.
205    BigInt,
206    /// 64-bit floating point.
207    Float,
208    /// Fixed-precision decimal number.
209    Decimal {
210        /// Number of total digits.
211        precision: u32,
212        /// Number of digits after decimal point.
213        scale: u32,
214    },
215    /// Date and time.
216    DateTime,
217    /// Binary data.
218    Bytes,
219    /// JSON value.
220    Json,
221    /// UUID value.
222    Uuid,
223    /// JSONB value (PostgreSQL only).
224    Jsonb,
225    /// XML value (PostgreSQL only).
226    Xml,
227    /// Fixed-length character type.
228    Char {
229        /// Column length.
230        length: u32,
231    },
232    /// Variable-length character type.
233    VarChar {
234        /// Maximum column length.
235        length: u32,
236    },
237}
238
239impl ScalarType {
240    /// Returns the Rust type name for this scalar type.
241    pub fn rust_type(&self) -> &'static str {
242        match self {
243            ScalarType::String => "String",
244            ScalarType::Boolean => "bool",
245            ScalarType::Int => "i32",
246            ScalarType::BigInt => "i64",
247            ScalarType::Float => "f64",
248            ScalarType::Decimal { .. } => "rust_decimal::Decimal",
249            ScalarType::DateTime => "chrono::NaiveDateTime",
250            ScalarType::Bytes => "Vec<u8>",
251            ScalarType::Json => "serde_json::Value",
252            ScalarType::Uuid => "uuid::Uuid",
253            ScalarType::Jsonb => "serde_json::Value",
254            ScalarType::Xml | ScalarType::Char { .. } | ScalarType::VarChar { .. } => "String",
255        }
256    }
257
258    /// Returns `true` when this scalar type is supported by the given database provider.
259    pub fn supported_by(self, provider: DatabaseProvider) -> bool {
260        match self {
261            ScalarType::Jsonb | ScalarType::Xml => provider == DatabaseProvider::Postgres,
262            ScalarType::Char { .. } | ScalarType::VarChar { .. } => {
263                matches!(
264                    provider,
265                    DatabaseProvider::Postgres | DatabaseProvider::Mysql
266                )
267            }
268            _ => true,
269        }
270    }
271
272    /// Human-readable list of supported providers (for diagnostics).
273    pub fn supported_providers(self) -> &'static str {
274        match self {
275            ScalarType::Jsonb | ScalarType::Xml => "PostgreSQL only",
276            ScalarType::Char { .. } | ScalarType::VarChar { .. } => "PostgreSQL and MySQL",
277            _ => "all databases",
278        }
279    }
280}
281
282/// Validated relation metadata.
283#[derive(Debug, Clone, PartialEq)]
284pub struct RelationIr {
285    /// Optional relation name (required for multiple relations between same models).
286    pub name: Option<String>,
287    /// The logical name of the target model.
288    pub target_model: String,
289    /// Foreign key field names in the current model (logical names).
290    pub fields: Vec<String>,
291    /// Referenced field names in the target model (logical names).
292    pub references: Vec<String>,
293    /// Referential action on delete.
294    pub on_delete: Option<ReferentialAction>,
295    /// Referential action on update.
296    pub on_update: Option<ReferentialAction>,
297}
298
299/// Default value for a field.
300#[derive(Debug, Clone, PartialEq)]
301pub enum DefaultValue {
302    /// A literal string value.
303    String(String),
304    /// A literal number value (stored as string to preserve precision).
305    Number(String),
306    /// A literal boolean value.
307    Boolean(bool),
308    /// An enum variant name.
309    EnumVariant(String),
310    /// A function call (autoincrement, uuid, now, etc.).
311    Function(FunctionCall),
312}
313
314/// Function call in a default value.
315#[derive(Debug, Clone, PartialEq)]
316pub struct FunctionCall {
317    /// The function name (e.g., "autoincrement", "uuid", "now").
318    pub name: String,
319    /// Function arguments (if any).
320    pub args: Vec<String>,
321}
322
323/// Primary key metadata.
324#[derive(Debug, Clone, PartialEq)]
325pub enum PrimaryKeyIr {
326    /// Single-field primary key (from @id).
327    Single(String),
328    /// Composite primary key (from @@id).
329    Composite(Vec<String>),
330}
331
332impl PrimaryKeyIr {
333    /// Returns the field names that form the primary key.
334    pub fn fields(&self) -> Vec<&str> {
335        match self {
336            PrimaryKeyIr::Single(field) => vec![field.as_str()],
337            PrimaryKeyIr::Composite(fields) => fields.iter().map(|s| s.as_str()).collect(),
338        }
339    }
340
341    /// Returns true if this is a single-field primary key.
342    pub fn is_single(&self) -> bool {
343        matches!(self, PrimaryKeyIr::Single(_))
344    }
345
346    /// Returns true if this is a composite primary key.
347    pub fn is_composite(&self) -> bool {
348        matches!(self, PrimaryKeyIr::Composite(_))
349    }
350}
351
352/// Unique constraint metadata.
353#[derive(Debug, Clone, PartialEq)]
354pub struct UniqueConstraintIr {
355    /// Field names (logical) that form the unique constraint.
356    pub fields: Vec<String>,
357}
358
359/// Index access method / algorithm.
360///
361/// The default (when `None` is stored on [`IndexIr`]) lets the DBMS choose
362/// (BTree on every supported database).
363#[derive(Debug, Clone, Copy, PartialEq, Eq)]
364pub enum IndexType {
365    /// B-Tree (default on all databases).
366    BTree,
367    /// Hash index — PostgreSQL and MySQL 8+.
368    Hash,
369    /// Generalized Inverted Index — PostgreSQL only (arrays, JSONB, full-text).
370    Gin,
371    /// Generalized Search Tree — PostgreSQL only (geometry, range types).
372    Gist,
373    /// Block Range Index — PostgreSQL only (large ordered tables).
374    Brin,
375    /// Full-text index — MySQL only.
376    FullText,
377}
378
379/// Error returned when parsing an unknown index type string.
380#[derive(Debug, Clone, PartialEq, Eq)]
381pub struct ParseIndexTypeError;
382
383impl fmt::Display for ParseIndexTypeError {
384    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
385        f.write_str("unknown index type")
386    }
387}
388
389impl std::error::Error for ParseIndexTypeError {}
390
391impl FromStr for IndexType {
392    type Err = ParseIndexTypeError;
393
394    fn from_str(s: &str) -> Result<Self, Self::Err> {
395        match s.to_ascii_lowercase().as_str() {
396            "btree" => Ok(IndexType::BTree),
397            "hash" => Ok(IndexType::Hash),
398            "gin" => Ok(IndexType::Gin),
399            "gist" => Ok(IndexType::Gist),
400            "brin" => Ok(IndexType::Brin),
401            "fulltext" => Ok(IndexType::FullText),
402            _ => Err(ParseIndexTypeError),
403        }
404    }
405}
406
407impl IndexType {
408    /// Returns `true` when this index type is supported by the given database provider.
409    pub fn supported_by(self, provider: DatabaseProvider) -> bool {
410        match self {
411            IndexType::BTree => true,
412            IndexType::Hash => matches!(
413                provider,
414                DatabaseProvider::Postgres | DatabaseProvider::Mysql
415            ),
416            IndexType::Gin | IndexType::Gist | IndexType::Brin => {
417                provider == DatabaseProvider::Postgres
418            }
419            IndexType::FullText => provider == DatabaseProvider::Mysql,
420        }
421    }
422
423    /// Human-readable list of supported providers (for diagnostics).
424    pub fn supported_providers(self) -> &'static str {
425        match self {
426            IndexType::BTree => "all databases",
427            IndexType::Hash => "PostgreSQL and MySQL",
428            IndexType::Gin => "PostgreSQL only",
429            IndexType::Gist => "PostgreSQL only",
430            IndexType::Brin => "PostgreSQL only",
431            IndexType::FullText => "MySQL only",
432        }
433    }
434
435    /// The canonical display name used in schema files.
436    pub fn as_str(self) -> &'static str {
437        match self {
438            IndexType::BTree => "BTree",
439            IndexType::Hash => "Hash",
440            IndexType::Gin => "Gin",
441            IndexType::Gist => "Gist",
442            IndexType::Brin => "Brin",
443            IndexType::FullText => "FullText",
444        }
445    }
446}
447
448/// The three datasource providers recognised by the Nautilus schema language.
449///
450/// Obtained by parsing the `provider` field of a `datasource` block:
451/// ```text
452/// datasource db {
453///     provider = "postgresql"  // -> DatabaseProvider::Postgres
454/// }
455/// ```
456#[derive(Debug, Clone, Copy, PartialEq, Eq)]
457pub enum DatabaseProvider {
458    /// PostgreSQL (provider string: `"postgresql"`).
459    Postgres,
460    /// MySQL / MariaDB (provider string: `"mysql"`).
461    Mysql,
462    /// SQLite (provider string: `"sqlite"`).
463    Sqlite,
464}
465
466/// Error returned when parsing an unknown database provider string.
467#[derive(Debug, Clone, PartialEq, Eq)]
468pub struct ParseDatabaseProviderError;
469
470impl fmt::Display for ParseDatabaseProviderError {
471    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
472        f.write_str("unknown database provider")
473    }
474}
475
476impl std::error::Error for ParseDatabaseProviderError {}
477
478impl FromStr for DatabaseProvider {
479    type Err = ParseDatabaseProviderError;
480
481    fn from_str(s: &str) -> Result<Self, Self::Err> {
482        match s {
483            "postgresql" => Ok(DatabaseProvider::Postgres),
484            "mysql" => Ok(DatabaseProvider::Mysql),
485            "sqlite" => Ok(DatabaseProvider::Sqlite),
486            _ => Err(ParseDatabaseProviderError),
487        }
488    }
489}
490
491impl DatabaseProvider {
492    /// All valid datasource provider strings.
493    pub const ALL: &'static [&'static str] = &["postgresql", "mysql", "sqlite"];
494
495    /// The canonical provider string used in `.nautilus` schema files.
496    pub fn as_str(self) -> &'static str {
497        match self {
498            DatabaseProvider::Postgres => "postgresql",
499            DatabaseProvider::Mysql => "mysql",
500            DatabaseProvider::Sqlite => "sqlite",
501        }
502    }
503
504    /// Human-readable display name (for diagnostic messages).
505    pub fn display_name(self) -> &'static str {
506        match self {
507            DatabaseProvider::Postgres => "PostgreSQL",
508            DatabaseProvider::Mysql => "MySQL",
509            DatabaseProvider::Sqlite => "SQLite",
510        }
511    }
512}
513
514impl std::fmt::Display for DatabaseProvider {
515    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
516        f.write_str(self.as_str())
517    }
518}
519
520/// The generator (client) providers recognised by the Nautilus schema language.
521///
522/// Obtained by parsing the `provider` field of a `generator` block:
523/// ```text
524/// generator client {
525///     provider = "nautilus-client-rs"  // -> ClientProvider::Rust
526/// }
527/// ```
528#[derive(Debug, Clone, Copy, PartialEq, Eq)]
529pub enum ClientProvider {
530    /// Rust client (provider string: `"nautilus-client-rs"`).
531    Rust,
532    /// Python client (provider string: `"nautilus-client-py"`).
533    Python,
534    /// JavaScript/TypeScript client (provider string: `"nautilus-client-js"`).
535    JavaScript,
536}
537
538/// Error returned when parsing an unknown client provider string.
539#[derive(Debug, Clone, PartialEq, Eq)]
540pub struct ParseClientProviderError;
541
542impl fmt::Display for ParseClientProviderError {
543    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
544        f.write_str("unknown client provider")
545    }
546}
547
548impl std::error::Error for ParseClientProviderError {}
549
550impl FromStr for ClientProvider {
551    type Err = ParseClientProviderError;
552
553    fn from_str(s: &str) -> Result<Self, Self::Err> {
554        match s {
555            "nautilus-client-rs" => Ok(ClientProvider::Rust),
556            "nautilus-client-py" => Ok(ClientProvider::Python),
557            "nautilus-client-js" => Ok(ClientProvider::JavaScript),
558            _ => Err(ParseClientProviderError),
559        }
560    }
561}
562
563impl ClientProvider {
564    /// All valid generator provider strings.
565    pub const ALL: &'static [&'static str] = &[
566        "nautilus-client-rs",
567        "nautilus-client-py",
568        "nautilus-client-js",
569    ];
570
571    /// The canonical provider string used in `.nautilus` schema files.
572    pub fn as_str(self) -> &'static str {
573        match self {
574            ClientProvider::Rust => "nautilus-client-rs",
575            ClientProvider::Python => "nautilus-client-py",
576            ClientProvider::JavaScript => "nautilus-client-js",
577        }
578    }
579}
580
581impl fmt::Display for ClientProvider {
582    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
583        f.write_str(self.as_str())
584    }
585}
586
587/// Index metadata.
588#[derive(Debug, Clone, PartialEq)]
589pub struct IndexIr {
590    /// Field names (logical) that form the index.
591    pub fields: Vec<String>,
592    /// Optional index type (access method).  `None` -> let the DBMS decide
593    /// (BTree on all supported databases).
594    pub index_type: Option<IndexType>,
595    /// Logical name — for developer reference only.
596    pub name: Option<String>,
597    /// Physical DDL name.  When set this is used as the `CREATE INDEX` name
598    /// instead of the auto-generated `idx_{table}_{cols}` name.
599    pub map: Option<String>,
600}
601
602/// Validated enum type.
603#[derive(Debug, Clone, PartialEq)]
604pub struct EnumIr {
605    /// The logical enum name (e.g., "Role").
606    pub logical_name: String,
607    /// Enum variant names.
608    pub variants: Vec<String>,
609    /// Span of the enum declaration.
610    pub span: Span,
611}
612
613impl EnumIr {
614    /// Checks if a variant exists.
615    pub fn has_variant(&self, name: &str) -> bool {
616        self.variants.iter().any(|v| v == name)
617    }
618}
619
620/// A single field within a composite type.
621///
622/// Only scalar and enum field types are allowed — no relations or nested composite types.
623#[derive(Debug, Clone, PartialEq)]
624pub struct CompositeFieldIr {
625    /// The logical field name as defined in the type block.
626    pub logical_name: String,
627    /// The physical name (from @map or logical_name).
628    pub db_name: String,
629    /// The resolved field type (Scalar or Enum only).
630    pub field_type: ResolvedFieldType,
631    /// Whether the field is required (not optional).
632    pub is_required: bool,
633    /// Whether the field is an array.
634    pub is_array: bool,
635    /// Storage strategy for array fields.
636    pub storage_strategy: Option<StorageStrategy>,
637    /// Span of the field declaration.
638    pub span: Span,
639}
640
641/// Validated composite type (embedded struct).
642#[derive(Debug, Clone, PartialEq)]
643pub struct CompositeTypeIr {
644    /// The logical type name as defined in the schema (e.g., "Address").
645    pub logical_name: String,
646    /// All fields of the composite type.
647    pub fields: Vec<CompositeFieldIr>,
648    /// Span of the type declaration.
649    pub span: Span,
650}