nounsql_core/codegen/
pg.rs1use crate::dialect::Dialect;
2use crate::ir::{Schema, Table, Val};
3
4fn quote_in(d: Dialect, name: &str) -> String {
5 let plain = !name.is_empty()
6 && name
7 .chars()
8 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
9 && !name.starts_with(|c: char| c.is_ascii_digit());
10 if plain && !d.is_reserved(name) {
11 name.to_string()
12 } else {
13 format!("\"{}\"", name.replace('"', "\"\""))
14 }
15}
16
17fn literal(s: &str) -> String {
18 format!("'{}'", s.replace('\'', "''"))
19}
20
21fn val(v: &Val) -> String {
22 match v {
23 Val::Literal(s) => s.clone(),
24 Val::Eval(e) => e.clone(),
25 }
26}
27
28fn action(a: &str) -> &'static str {
29 match a {
30 "restrict" => "RESTRICT",
31 "set_null" => "SET NULL",
32 "no_action" => "NO ACTION",
33 _ => "CASCADE",
34 }
35}
36
37pub fn emit(dialect: Dialect, schema: &Schema) -> String {
38 let quote = |n: &str| quote_in(dialect, n);
39 let mut out = String::new();
40 for table in &schema.tables {
41 out.push_str(&create_table(dialect, table));
42 out.push('\n');
43 }
44 for table in &schema.tables {
45 for idx in &table.indexes {
46 let kind = if idx.unique { "UNIQUE INDEX" } else { "INDEX" };
47 let cols = idx
48 .columns
49 .iter()
50 .map(|c| quote(c))
51 .collect::<Vec<_>>()
52 .join(", ");
53 out.push_str(&format!(
54 "CREATE {kind} {} ON {} ({cols});\n",
55 quote(&idx.name),
56 quote(&table.name)
57 ));
58 }
59 }
60 if schema.tables.iter().any(|t| !t.foreign_keys.is_empty()) {
61 out.push('\n');
62 }
63 for table in &schema.tables {
64 for fk in &table.foreign_keys {
65 let cols = fk
66 .columns
67 .iter()
68 .map(|c| quote(c))
69 .collect::<Vec<_>>()
70 .join(", ");
71 let refs = fk
72 .ref_columns
73 .iter()
74 .map(|c| quote(c))
75 .collect::<Vec<_>>()
76 .join(", ");
77 let name = format!("{}_{}_fkey", table.name, fk.columns.join("_"));
78 out.push_str(&format!(
79 "ALTER TABLE {} ADD CONSTRAINT {} FOREIGN KEY ({cols}) REFERENCES {} ({refs}) ON DELETE {} ON UPDATE {};\n",
80 quote(&table.name),
81 quote(&name),
82 quote(&fk.ref_table),
83 action(&fk.on_delete),
84 action(&fk.on_update),
85 ));
86 }
87 }
88
89 let triggers = emit_on_update_triggers(dialect, schema);
90 if !triggers.is_empty() {
91 out.push('\n');
92 out.push_str(&triggers);
93 }
94 let comments = emit_comments(dialect, schema);
95 if !comments.is_empty() {
96 out.push('\n');
97 out.push_str(&comments);
98 }
99 out
100}
101
102fn create_table(dialect: Dialect, table: &Table) -> String {
103 let quote = |n: &str| quote_in(dialect, n);
104 let mut lines: Vec<String> = Vec::new();
105 for col in table.columns.values() {
106 let mut line = format!(" {} {}", quote(&col.name), col.ty);
107 if !col.null || table.pk.contains(&col.name) {
109 line.push_str(" NOT NULL");
110 }
111 if let Some(d) = &col.default {
112 line.push_str(&format!(" DEFAULT {}", val(d)));
113 }
114 lines.push(line);
115 }
116 if !table.pk.is_empty() {
117 let cols = table
118 .pk
119 .iter()
120 .map(|c| quote(c))
121 .collect::<Vec<_>>()
122 .join(", ");
123 lines.push(format!(
124 " CONSTRAINT {} PRIMARY KEY ({cols})",
125 quote(&format!("{}_pkey", table.name))
126 ));
127 }
128 format!(
129 "CREATE TABLE {} (\n{}\n);\n",
130 quote(&table.name),
131 lines.join(",\n")
132 )
133}
134
135fn emit_on_update_triggers(dialect: Dialect, schema: &Schema) -> String {
137 let quote = |n: &str| quote_in(dialect, n);
138 let mut out = String::new();
139 for table in &schema.tables {
140 for col in table.columns.values() {
141 let Some(v) = &col.on_update else { continue };
142 let fname = format!("{}_{}_on_update", table.name, col.name);
143 out.push_str(&format!(
144 "CREATE OR REPLACE FUNCTION {}() RETURNS trigger AS $$\nBEGIN\n NEW.{} := {};\n RETURN NEW;\nEND;\n$$ LANGUAGE plpgsql;\n",
145 quote(&fname),
146 quote(&col.name),
147 val(v)
148 ));
149 out.push_str(&format!(
150 "CREATE TRIGGER {} BEFORE UPDATE ON {} FOR EACH ROW EXECUTE FUNCTION {}();\n",
151 quote(&format!("{fname}_trg")),
152 quote(&table.name),
153 quote(&fname)
154 ));
155 }
156 }
157 out
158}
159
160fn emit_comments(dialect: Dialect, schema: &Schema) -> String {
161 let quote = |n: &str| quote_in(dialect, n);
162 let mut out = String::new();
163 for table in &schema.tables {
164 if let Some(c) = &table.comment {
165 out.push_str(&format!(
166 "COMMENT ON TABLE {} IS {};\n",
167 quote(&table.name),
168 literal(c)
169 ));
170 }
171 for col in table.columns.values() {
172 if let Some(c) = &col.comment {
173 out.push_str(&format!(
174 "COMMENT ON COLUMN {}.{} IS {};\n",
175 quote(&table.name),
176 quote(&col.name),
177 literal(c)
178 ));
179 }
180 }
181 }
182 out
183}