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}