1use crate::identifier::QualifiedName;
13use crate::ir::constraint::{Constraint, ConstraintKind};
14use crate::ir::table::Table;
15use crate::plan::rewrite::sql as rewrite_sql;
16
17#[must_use]
26pub fn render_table(t: &Table) -> String {
27 let mut out = String::new();
28
29 let table_without_fks = Table {
32 qname: t.qname.clone(),
33 columns: t.columns.clone(),
34 constraints: t
35 .constraints
36 .iter()
37 .filter(|c| !matches!(c.kind, ConstraintKind::ForeignKey(_)))
38 .cloned()
39 .collect(),
40 partition_by: None,
41 partition_of: None,
42 comment: t.comment.clone(),
43 owner: None,
44 grants: vec![],
45 rls_enabled: false,
46 rls_forced: false,
47 policies: vec![],
48 storage: crate::ir::reloptions::TableStorageOptions::default(),
49 };
50
51 out.push_str(&rewrite_sql::create_table(&table_without_fks));
52 out.push('\n');
53
54 if let Some(comment) = &t.comment {
56 out.push_str(&rewrite_sql::comment_on_table(
57 &t.qname,
58 Some(comment.as_str()),
59 ));
60 out.push('\n');
61 }
62
63 for col in &t.columns {
65 if let Some(comment) = &col.comment {
66 out.push_str(&rewrite_sql::comment_on_column(
67 &t.qname,
68 &col.name,
69 Some(comment.as_str()),
70 ));
71 out.push('\n');
72 }
73 }
74
75 for c in &t.constraints {
77 if matches!(c.kind, ConstraintKind::ForeignKey(_)) {
78 continue;
79 }
80 if let Some(comment) = &c.comment {
81 out.push_str(&rewrite_sql::comment_on_constraint(
82 &t.qname,
83 &c.qname.name,
84 Some(comment.as_str()),
85 ));
86 out.push('\n');
87 }
88 }
89
90 out
91}
92
93#[must_use]
97pub fn render_add_fk(table_qname: &QualifiedName, c: &Constraint) -> String {
98 let mut out = String::new();
99 out.push_str(&rewrite_sql::alter_table_add_constraint(table_qname, c));
100 out.push('\n');
101 if let Some(comment) = &c.comment {
102 out.push_str(&rewrite_sql::comment_on_constraint(
103 table_qname,
104 &c.qname.name,
105 Some(comment.as_str()),
106 ));
107 out.push('\n');
108 }
109 out
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115 use crate::identifier::Identifier;
116 use crate::ir::column::Column;
117 use crate::ir::column_type::ColumnType;
118 use crate::ir::constraint::{
119 Constraint, ConstraintKind, Deferrable, FkMatchType, ForeignKey, ReferentialAction,
120 };
121 use crate::ir::default_expr::{DefaultExpr, LiteralValue};
122 use crate::ir::table::Table;
123
124 fn id(s: &str) -> Identifier {
125 Identifier::from_unquoted(s).unwrap()
126 }
127
128 fn qn(schema: &str, name: &str) -> QualifiedName {
129 QualifiedName::new(id(schema), id(name))
130 }
131
132 fn col(name: &str, ty: ColumnType) -> Column {
133 Column {
134 name: id(name),
135 ty,
136 nullable: false,
137 default: None,
138 identity: None,
139 generated: None,
140 collation: None,
141 storage: None,
142 compression: None,
143 comment: None,
144 }
145 }
146
147 fn assert_pg_parseable(sql: &str) {
148 let r = pg_query::parse(sql);
150 assert!(r.is_ok(), "pg_query rejected SQL:\n{sql}\nerr: {r:?}");
151 }
152
153 #[test]
154 fn simple_table_renders() {
155 let t = Table {
156 qname: qn("app", "users"),
157 columns: vec![
158 col("id", ColumnType::BigInt),
159 Column {
160 name: id("email"),
161 ty: ColumnType::Text,
162 nullable: true,
163 ..col("email", ColumnType::Text)
164 },
165 ],
166 constraints: vec![Constraint {
167 qname: qn("app", "users_pkey"),
168 kind: ConstraintKind::PrimaryKey {
169 columns: vec![id("id")],
170 include: vec![],
171 },
172 deferrable: Deferrable::NotDeferrable,
173 comment: None,
174 }],
175 partition_by: None,
176 partition_of: None,
177 comment: None,
178 owner: None,
179 grants: vec![],
180 rls_enabled: false,
181 rls_forced: false,
182 policies: vec![],
183 storage: crate::ir::reloptions::TableStorageOptions::default(),
184 };
185 let sql = render_table(&t);
186 assert!(sql.contains("CREATE TABLE app.users"));
187 assert!(sql.contains("id bigint NOT NULL"));
188 assert!(sql.contains("CONSTRAINT users_pkey PRIMARY KEY (id)"));
189 assert!(!sql.contains("FOREIGN KEY"), "FK must not appear inline");
190 assert_pg_parseable(&sql);
191 }
192
193 #[test]
194 fn fk_excluded_from_create_table() {
195 let t = Table {
196 qname: qn("app", "users"),
197 columns: vec![
198 col("id", ColumnType::BigInt),
199 col("org_id", ColumnType::BigInt),
200 ],
201 constraints: vec![
202 Constraint {
203 qname: qn("app", "users_pkey"),
204 kind: ConstraintKind::PrimaryKey {
205 columns: vec![id("id")],
206 include: vec![],
207 },
208 deferrable: Deferrable::NotDeferrable,
209 comment: None,
210 },
211 Constraint {
212 qname: qn("app", "users_org_fkey"),
213 kind: ConstraintKind::ForeignKey(ForeignKey {
214 columns: vec![id("org_id")],
215 referenced_table: qn("app", "orgs"),
216 referenced_columns: vec![id("id")],
217 on_update: ReferentialAction::NoAction,
218 on_delete: ReferentialAction::Cascade,
219 match_type: FkMatchType::Simple,
220 }),
221 deferrable: Deferrable::NotDeferrable,
222 comment: None,
223 },
224 ],
225 partition_by: None,
226 partition_of: None,
227 comment: None,
228 owner: None,
229 grants: vec![],
230 rls_enabled: false,
231 rls_forced: false,
232 policies: vec![],
233 storage: crate::ir::reloptions::TableStorageOptions::default(),
234 };
235 let sql = render_table(&t);
236 assert!(
237 !sql.contains("FOREIGN KEY"),
238 "FK must not appear in render_table output"
239 );
240 assert_pg_parseable(&sql);
241 }
242
243 #[test]
244 fn render_add_fk_emits_alter_table() {
245 let fk = Constraint {
246 qname: qn("app", "users_org_fkey"),
247 kind: ConstraintKind::ForeignKey(ForeignKey {
248 columns: vec![id("org_id")],
249 referenced_table: qn("app", "orgs"),
250 referenced_columns: vec![id("id")],
251 on_update: ReferentialAction::NoAction,
252 on_delete: ReferentialAction::Cascade,
253 match_type: FkMatchType::Simple,
254 }),
255 deferrable: Deferrable::NotDeferrable,
256 comment: None,
257 };
258 let sql = render_add_fk(&qn("app", "users"), &fk);
259 assert!(sql.contains("ALTER TABLE app.users ADD"));
260 assert!(sql.contains("FOREIGN KEY"));
261 assert!(sql.contains("REFERENCES app.orgs"));
262 assert_pg_parseable(&sql);
263 }
264
265 #[test]
266 fn table_comment_rendered() {
267 let t = Table {
268 qname: qn("app", "orgs"),
269 columns: vec![col("id", ColumnType::BigInt)],
270 constraints: vec![],
271 partition_by: None,
272 partition_of: None,
273 comment: Some("organization records".into()),
274 owner: None,
275 grants: vec![],
276 rls_enabled: false,
277 rls_forced: false,
278 policies: vec![],
279 storage: crate::ir::reloptions::TableStorageOptions::default(),
280 };
281 let sql = render_table(&t);
282 assert!(sql.contains("COMMENT ON TABLE app.orgs IS 'organization records';"));
283 assert_pg_parseable(&sql);
284 }
285
286 #[test]
287 fn column_comment_rendered() {
288 let mut c = col("email", ColumnType::Text);
289 c.comment = Some("email address".into());
290 let t = Table {
291 qname: qn("app", "users"),
292 columns: vec![c],
293 constraints: vec![],
294 partition_by: None,
295 partition_of: None,
296 comment: None,
297 owner: None,
298 grants: vec![],
299 rls_enabled: false,
300 rls_forced: false,
301 policies: vec![],
302 storage: crate::ir::reloptions::TableStorageOptions::default(),
303 };
304 let sql = render_table(&t);
305 assert!(sql.contains("COMMENT ON COLUMN app.users.email IS 'email address';"));
306 assert_pg_parseable(&sql);
307 }
308
309 #[test]
310 fn column_with_default_literal() {
311 let mut c = col("active", ColumnType::Boolean);
312 c.default = Some(DefaultExpr::Literal(LiteralValue::Bool(true)));
313 c.nullable = true;
314 let t = Table {
315 qname: qn("app", "users"),
316 columns: vec![c],
317 constraints: vec![],
318 partition_by: None,
319 partition_of: None,
320 comment: None,
321 owner: None,
322 grants: vec![],
323 rls_enabled: false,
324 rls_forced: false,
325 policies: vec![],
326 storage: crate::ir::reloptions::TableStorageOptions::default(),
327 };
328 let sql = render_table(&t);
329 assert!(sql.contains("DEFAULT true"));
330 assert_pg_parseable(&sql);
331 }
332
333 #[test]
334 fn nullable_column() {
335 let mut c = col("bio", ColumnType::Text);
336 c.nullable = true;
337 let t = Table {
338 qname: qn("app", "users"),
339 columns: vec![c],
340 constraints: vec![],
341 partition_by: None,
342 partition_of: None,
343 comment: None,
344 owner: None,
345 grants: vec![],
346 rls_enabled: false,
347 rls_forced: false,
348 policies: vec![],
349 storage: crate::ir::reloptions::TableStorageOptions::default(),
350 };
351 let sql = render_table(&t);
352 assert!(!sql.contains("NOT NULL"));
354 assert_pg_parseable(&sql);
355 }
356
357 #[test]
358 fn check_constraint_inline() {
359 use crate::ir::default_expr::NormalizedExpr;
360 let check = Constraint {
361 qname: qn("app", "users_email_check"),
362 kind: ConstraintKind::Check {
363 expression: NormalizedExpr::from_text("email ~* '^.+@.+$'"),
364 no_inherit: false,
365 },
366 deferrable: Deferrable::NotDeferrable,
367 comment: None,
368 };
369 let t = Table {
370 qname: qn("app", "users"),
371 columns: vec![col("email", ColumnType::Text)],
372 constraints: vec![check],
373 partition_by: None,
374 partition_of: None,
375 comment: None,
376 owner: None,
377 grants: vec![],
378 rls_enabled: false,
379 rls_forced: false,
380 policies: vec![],
381 storage: crate::ir::reloptions::TableStorageOptions::default(),
382 };
383 let sql = render_table(&t);
384 assert!(sql.contains("CHECK"), "expected CHECK in CREATE TABLE");
385 assert_pg_parseable(&sql);
386 }
387
388 #[test]
393 fn renders_inline_storage_external() {
394 use crate::ir::column::StorageKind;
395 let mut c = col("doc", ColumnType::Text);
396 c.storage = Some(StorageKind::External);
397 let t = Table {
398 qname: qn("app", "users"),
399 columns: vec![c],
400 constraints: vec![],
401 partition_by: None,
402 partition_of: None,
403 comment: None,
404 owner: None,
405 grants: vec![],
406 rls_enabled: false,
407 rls_forced: false,
408 policies: vec![],
409 storage: crate::ir::reloptions::TableStorageOptions::default(),
410 };
411 let sql = render_table(&t);
412 assert!(sql.contains("STORAGE EXTERNAL"), "got: {sql}");
413 assert_pg_parseable(&sql);
414 }
415
416 #[test]
417 fn renders_inline_compression_lz4() {
418 use crate::ir::column::Compression;
419 let mut c = col("blob", ColumnType::Bytea);
420 c.compression = Some(Compression::Lz4);
421 let t = Table {
422 qname: qn("app", "users"),
423 columns: vec![c],
424 constraints: vec![],
425 partition_by: None,
426 partition_of: None,
427 comment: None,
428 owner: None,
429 grants: vec![],
430 rls_enabled: false,
431 rls_forced: false,
432 policies: vec![],
433 storage: crate::ir::reloptions::TableStorageOptions::default(),
434 };
435 let sql = render_table(&t);
436 assert!(sql.contains("COMPRESSION lz4"), "got: {sql}");
437 assert_pg_parseable(&sql);
438 }
439
440 #[test]
441 fn renders_no_storage_compression_when_none() {
442 let c = col("plain", ColumnType::Text);
443 let t = Table {
444 qname: qn("app", "users"),
445 columns: vec![c],
446 constraints: vec![],
447 partition_by: None,
448 partition_of: None,
449 comment: None,
450 owner: None,
451 grants: vec![],
452 rls_enabled: false,
453 rls_forced: false,
454 policies: vec![],
455 storage: crate::ir::reloptions::TableStorageOptions::default(),
456 };
457 let sql = render_table(&t);
458 assert!(!sql.contains("STORAGE"), "got: {sql}");
459 assert!(!sql.contains("COMPRESSION"), "got: {sql}");
460 assert_pg_parseable(&sql);
461 }
462}