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 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 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)] 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}