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