Skip to main content

keelson_gen/
schema.rs

1//! The introspected schema — the generator's intermediate representation.
2//!
3//! Every introspector (and every test that wants to skip the database)
4//! produces this; the resolver and emitter consume only this. The IR is
5//! deliberately plain data with `PartialEq`, so "live introspection equals
6//! the hand-built IR" is a single `assert_eq!`.
7
8/// A whole schema: every table and view the generator will consider, sorted
9/// by name (the introspectors guarantee the order; determinism starts here).
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct Schema {
12    /// Tables and views, sorted by [`TableDef::name`].
13    pub tables: Vec<TableDef>,
14}
15
16/// What the catalog says a relation *is*. Three answers, because writability
17/// and viewness are separate facts: a base table is always writable, a view
18/// is writable only when the engine says so, and the engines disagree about
19/// when that is (see [`TableKind::UpdatableView`]).
20///
21/// This is the catalog's answer, not the generator's decision. Whether a
22/// model ends up with the `Table` surface is resolved from this *plus* the
23/// configuration, in `resolve`.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum TableKind {
26    /// A base table: gets the full `Table` surface when it has a primary key.
27    Table,
28    /// A view (or materialised view) the engine will not write through:
29    /// `SELECT`-only, unconditionally.
30    View,
31    /// A view the engine reports as writable — PostgreSQL's auto-updatable
32    /// views and any view with the right `INSTEAD OF` triggers, MySQL's
33    /// `IS_UPDATABLE = 'YES'`, SQLite's views carrying all three `INSTEAD OF`
34    /// triggers. Still keyless: a view has no primary key, so the write
35    /// surface needs a `[tables.<name>] key` before it can be generated.
36    UpdatableView,
37}
38
39impl TableKind {
40    /// Whether the catalog calls this a view (updatable or not).
41    pub fn is_view(self) -> bool {
42        !matches!(self, TableKind::Table)
43    }
44
45    /// Whether the engine will accept `INSERT`/`UPDATE`/`DELETE` against it.
46    /// A base table always will; a view only when the catalog says so.
47    pub fn is_updatable(self) -> bool {
48        matches!(self, TableKind::Table | TableKind::UpdatableView)
49    }
50
51    /// The word to use for this relation in an error message.
52    pub(crate) fn noun(self) -> &'static str {
53        match self {
54            TableKind::Table => "table",
55            TableKind::View => "view",
56            TableKind::UpdatableView => "updatable view",
57        }
58    }
59}
60
61/// One table or view.
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct TableDef {
64    /// The unqualified name as the catalog spells it.
65    pub name: String,
66    /// Table or view.
67    pub kind: TableKind,
68    /// Columns in catalog (ordinal) order — the order the generated model
69    /// lists them, matching the hand-written spec.
70    pub columns: Vec<ColumnDef>,
71    /// Primary-key column names in key order; empty when there is none (all
72    /// views, and keyless tables — both emit `View`-only models unless the
73    /// configuration declares a key, see `[tables.<name>] key`).
74    pub primary_key: Vec<String>,
75    /// Foreign keys in a stable catalog order.
76    pub foreign_keys: Vec<ForeignKey>,
77    /// Declared `UNIQUE` constraints, each a column list in key order,
78    /// sorted by column list; the primary key is **not** repeated here.
79    ///
80    /// Read by the factory emitter, which backs a unique column with a
81    /// [`Sequence`](https://docs.rs/keelson-factory) value so
82    /// `create_many(&db, 100)` cannot collide. Only *declared constraints*
83    /// are introspected — a bare `CREATE UNIQUE INDEX` is an index, not a
84    /// constraint, and is deliberately not read (the generator would have no
85    /// honest way to tell a partial or expression index from a plain one).
86    pub unique_keys: Vec<Vec<String>>,
87}
88
89impl TableDef {
90    /// The column named `name`, if any.
91    pub fn column(&self, name: &str) -> Option<&ColumnDef> {
92        self.columns.iter().find(|c| c.name == name)
93    }
94}
95
96/// One column.
97#[derive(Debug, Clone, PartialEq, Eq)]
98pub struct ColumnDef {
99    /// The name as the catalog spells it.
100    pub name: String,
101    /// The declared database type, as the dialect's catalog reports it
102    /// (PostgreSQL: `format_type` output like `timestamp with time zone`;
103    /// SQLite: the declared type text). The type map normalises it.
104    pub db_type: String,
105    /// Whether NULL is allowed.
106    pub nullable: bool,
107    /// The default expression's text, when there is one.
108    pub default: Option<String>,
109    /// Auto-increment (PostgreSQL identity/serial; SQLite rowid alias).
110    pub autoincrement: bool,
111    /// The column comment, where the dialect has them (PostgreSQL).
112    pub comment: Option<String>,
113}
114
115/// One foreign key. Multi-column keys are carried faithfully but relation
116/// emission covers single-column keys only (recorded in the crate docs).
117#[derive(Debug, Clone, PartialEq, Eq)]
118pub struct ForeignKey {
119    /// Referencing columns, in key order.
120    pub columns: Vec<String>,
121    /// The referenced table.
122    pub ref_table: String,
123    /// Referenced columns, in the same order as [`ForeignKey::columns`].
124    pub ref_columns: Vec<String>,
125}