asn1rs_model/gen/
sql.rs

1use crate::gen::Generator;
2use crate::model::sql::Column;
3use crate::model::sql::Constraint;
4use crate::model::sql::Sql;
5use crate::model::Definition;
6use crate::model::Model;
7use std::fmt::Write;
8
9#[derive(Debug)]
10pub enum Error {
11    Fmt(::std::fmt::Error),
12}
13
14impl From<::std::fmt::Error> for Error {
15    fn from(e: ::std::fmt::Error) -> Self {
16        Error::Fmt(e)
17    }
18}
19
20#[derive(Debug)]
21pub enum TableOptimizationHint {
22    WritePerformance,
23}
24
25#[derive(Debug)]
26pub enum PrimaryKeyHint {
27    WrapOnOverflow,
28}
29
30#[allow(clippy::module_name_repetitions)]
31#[derive(Debug, Default)]
32pub struct SqlDefGenerator {
33    models: Vec<Model<Sql>>,
34    optimize_tables_for: Option<TableOptimizationHint>,
35    primary_key_hint: Option<PrimaryKeyHint>,
36}
37
38impl SqlDefGenerator {
39    pub fn reset(&mut self) {
40        self.models.clear();
41    }
42}
43
44impl Generator<Sql> for SqlDefGenerator {
45    type Error = Error;
46
47    fn add_model(&mut self, model: Model<Sql>) {
48        self.models.push(model);
49    }
50
51    fn models(&self) -> &[Model<Sql>] {
52        &self.models
53    }
54
55    fn models_mut(&mut self) -> &mut [Model<Sql>] {
56        &mut self.models
57    }
58
59    fn to_string(&self) -> Result<Vec<(String, String)>, <Self as Generator<Sql>>::Error> {
60        let mut files = Vec::with_capacity(self.models.len());
61        for model in &self.models {
62            let mut drop = String::new();
63            let mut create = String::new();
64            for Definition(name, sql) in &model.definitions {
65                writeln!(create)?;
66                match sql {
67                    Sql::Table(columns, constraints) => {
68                        // TODO
69                        writeln!(drop, "DROP TABLE IF EXISTS {} CASCADE;", name)?;
70                        self.append_create_table(&mut create, name, columns, constraints)?;
71                        self.apply_primary_key_hints(&mut create, name, columns)?;
72                    }
73                    Sql::Enum(variants) => {
74                        // TODO
75                        writeln!(drop, "DROP TABLE IF EXISTS {} CASCADE;", name)?;
76                        self.append_create_enum(&mut create, name, variants)?
77                    }
78                    Sql::Index(table, columns) => {
79                        Self::append_index(&mut create, name, table, &columns[..])?;
80                    }
81                    Sql::AbandonChildrenFunction(table, children) => {
82                        Self::append_abandon_children(&mut create, table, name, &children[..])?;
83                    }
84                    Sql::SilentlyPreventAnyDelete(table) => {
85                        Self::append_silently_prevent_any_delete(&mut create, name, table)?;
86                    }
87                }
88            }
89            drop.push_str(&create);
90            files.push((format!("{}.sql", model.name), drop));
91        }
92        Ok(files)
93    }
94}
95
96impl SqlDefGenerator {
97    pub fn optimize_tables_for_write_performance(mut self) -> Self {
98        self.optimize_tables_for = Some(TableOptimizationHint::WritePerformance);
99        self
100    }
101
102    pub const fn no_table_write_optimization(mut self) -> Self {
103        self.optimize_tables_for = None;
104        self
105    }
106
107    pub const fn wrap_primary_key_on_overflow(mut self) -> Self {
108        self.primary_key_hint = Some(PrimaryKeyHint::WrapOnOverflow);
109        self
110    }
111
112    pub const fn no_wrap_of_primary_key_on_overflow(mut self) -> Self {
113        self.primary_key_hint = None;
114        self
115    }
116
117    fn append_create_table(
118        &self,
119        target: &mut dyn Write,
120        name: &str,
121        columns: &[Column],
122        constraints: &[Constraint],
123    ) -> Result<(), Error> {
124        writeln!(
125            target,
126            "CREATE{}TABLE {} (",
127            match self.optimize_tables_for {
128                Some(TableOptimizationHint::WritePerformance) => " UNLOGGED ",
129                None => " ",
130            },
131            name
132        )?;
133        for (index, column) in columns.iter().enumerate() {
134            Self::append_column_statement(target, column)?;
135            if index + 1 < columns.len() || !constraints.is_empty() {
136                write!(target, ",")?;
137            }
138            writeln!(target)?;
139        }
140        for (index, constraint) in constraints.iter().enumerate() {
141            Self::append_constraint(target, constraint)?;
142            if index + 1 < constraints.len() {
143                write!(target, ",")?;
144            }
145            writeln!(target)?;
146        }
147        writeln!(target, ");")?;
148        Ok(())
149    }
150
151    #[allow(clippy::single_match)] // to get a compiler error on a new variant in PrimaryKeyHint
152    fn apply_primary_key_hints(
153        &self,
154        target: &mut dyn Write,
155        table: &str,
156        columns: &[Column],
157    ) -> Result<(), Error> {
158        let column_name = columns.iter().find_map(|column| {
159            if column.primary_key {
160                Some(column.name.clone())
161            } else {
162                None
163            }
164        });
165        if let Some(column) = column_name {
166            match self.primary_key_hint {
167                Some(PrimaryKeyHint::WrapOnOverflow) => {
168                    writeln!(target, "ALTER SEQUENCE {}_{}_seq CYCLE;", table, column)?;
169                }
170                None => {}
171            }
172        }
173        Ok(())
174    }
175
176    pub fn append_column_statement(target: &mut dyn Write, column: &Column) -> Result<(), Error> {
177        write!(target, "    {} {}", column.name, column.sql.to_string())?;
178        if column.primary_key {
179            write!(target, " PRIMARY KEY")?;
180        }
181        Ok(())
182    }
183
184    fn append_create_enum(
185        &self,
186        target: &mut dyn Write,
187        name: &str,
188        variants: &[String],
189    ) -> Result<(), Error> {
190        writeln!(
191            target,
192            "CREATE{}TABLE {} (",
193            match self.optimize_tables_for {
194                Some(TableOptimizationHint::WritePerformance) => " UNLOGGED ",
195                None => " ",
196            },
197            name
198        )?;
199        writeln!(target, "    id SERIAL PRIMARY KEY,")?;
200        writeln!(target, "    name TEXT NOT NULL")?;
201        writeln!(target, ");")?;
202
203        writeln!(target, "INSERT INTO {} (id, name) VALUES", name)?;
204        for (index, variant) in variants.iter().enumerate() {
205            write!(target, "    ({}, '{}')", index, variant)?;
206            if index + 1 < variants.len() {
207                write!(target, ", ")?;
208            } else {
209                write!(target, ";")?;
210            }
211            writeln!(target)?;
212        }
213        Ok(())
214    }
215
216    fn append_constraint(target: &mut dyn Write, constraint: &Constraint) -> Result<(), Error> {
217        match constraint {
218            Constraint::CombinedPrimaryKey(columns) => {
219                write!(target, "    PRIMARY KEY({})", columns.join(", "))?;
220            }
221            Constraint::OneNotNull(columns) => {
222                write!(
223                    target,
224                    "    CHECK (num_nonnulls({}) = 1)",
225                    columns.join(", ")
226                )?;
227            }
228        }
229        Ok(())
230    }
231
232    fn append_index(
233        target: &mut dyn Write,
234        name: &str,
235        table: &str,
236        columns: &[String],
237    ) -> Result<(), Error> {
238        writeln!(
239            target,
240            "CREATE INDEX {} ON {}({});",
241            name,
242            table,
243            columns.join(", ")
244        )?;
245        Ok(())
246    }
247
248    fn append_abandon_children(
249        target: &mut dyn Write,
250        table: &str,
251        name: &str,
252        children: &[(String, String, String)],
253    ) -> Result<(), Error> {
254        writeln!(
255            target,
256            "CREATE OR REPLACE FUNCTION {}() RETURNS TRIGGER AS",
257            name
258        )?;
259        writeln!(target, "$$ BEGIN")?;
260        for (column, other_table, other_column) in children {
261            writeln!(
262                target,
263                "    DELETE FROM {} WHERE {} = OLD.{};",
264                other_table, other_column, column
265            )?;
266        }
267        writeln!(target, "    RETURN NULL;")?;
268        writeln!(target, "END; $$ LANGUAGE plpgsql;")?;
269        writeln!(
270            target,
271            "CREATE TRIGGER OnDelete{} AFTER DELETE ON {}",
272            name, table
273        )?;
274        writeln!(target, "    FOR EACH ROW")?;
275        writeln!(target, "    EXECUTE PROCEDURE {}();", name)?;
276        Ok(())
277    }
278
279    fn append_silently_prevent_any_delete(
280        target: &mut dyn Write,
281        name: &str,
282        table: &str,
283    ) -> Result<(), Error> {
284        writeln!(target, "CREATE RULE {} AS ON DELETE TO {}", name, table)?;
285        writeln!(target, "    DO INSTEAD NOTHING;")?;
286        Ok(())
287    }
288}