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