Skip to main content

nounsql_core/codegen/
pg.rs

1use 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        // 主キーの列は定義上 NOT NULL。null_default に関わらず明示する。
108        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
135/// `on_update=` は PostgreSQL に対応する構文が無いのでトリガで実現する。
136fn 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}