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