byteorm_lib/
lib.rs

1mod ast;
2mod parser;
3
4use pest::Parser;
5use pest_derive::Parser;
6
7
8pub mod rustgen;
9#[derive(Parser)]
10#[grammar = "grammar.pest"]
11pub struct SchemaParser;
12
13pub use ast::*;
14pub use parser::parse_schema;
15
16
17pub mod snapshot {
18    use tokio_postgres::Client;
19    use crate::Schema;
20
21    pub async fn init_snapshot_table(client: &Client) -> Result<(), Box<dyn std::error::Error>> {
22        let create_table = "CREATE TABLE IF NOT EXISTS _byteorm_schema ( id SERIAL PRIMARY KEY, schema_json JSONB NOT NULL, created_at TIMESTAMP DEFAULT now(), updated_at TIMESTAMP DEFAULT now() )";
23
24        client.execute(create_table, &[]).await?;
25        Ok(())
26    }
27
28    pub async fn save_snapshot(client: &Client, schema: &Schema) -> Result<(), Box<dyn std::error::Error>> {
29        let json_value = serde_json::to_value(schema)?;
30
31        client.execute(
32            "DELETE FROM _byteorm_schema WHERE id != 0",
33            &[],
34        ).await.ok();
35
36        client.execute(
37            "INSERT INTO _byteorm_schema (schema_json, updated_at) VALUES ($1, now())",
38            &[&json_value], 
39        ).await?;
40
41        println!("Snapshot saved to database");
42        Ok(())
43    }
44
45
46    pub async fn load_snapshot(client: &Client) -> Result<Option<Schema>, Box<dyn std::error::Error>> {
47        let rows = client.query(
48            "SELECT schema_json FROM _byteorm_schema ORDER BY updated_at DESC LIMIT 1",
49            &[],
50        ).await?;
51
52        if rows.is_empty() {
53            return Ok(None);
54        }
55
56        let json_value: serde_json::Value = rows[0].get(0);
57        Ok(Some(serde_json::from_value(json_value)?))
58    }
59
60}
61
62
63
64pub mod diff {
65    use crate::{Schema, Model, Field, Modifier};
66
67    #[derive(Debug, Clone)]
68    pub enum Change {
69        CreateTable(Model),
70        AddColumn { table: String, field: Field },
71        RemoveColumn { table: String, column: String },
72        AlterColumn { table: String, column: String, old: Field, new: Field },
73        RemoveTable(String),
74    }
75
76    pub fn diff_schemas(previous: Option<&Schema>, current: &Schema) -> Vec<Change> {
77        let mut changes = Vec::new();
78
79        if let Some(prev) = previous {
80            for prev_model in &prev.models {
81                if !current.models.iter().any(|m| m.name == prev_model.name) {
82                    changes.push(Change::RemoveTable(prev_model.name.clone()));
83                }
84            }
85
86            for curr_model in &current.models {
87                if let Some(prev_model) = prev.models.iter().find(|m| m.name == curr_model.name) {
88                    for curr_field in &curr_model.fields {
89                        if !prev_model.fields.iter().any(|f| f.name == curr_field.name) {
90                            changes.push(Change::AddColumn {
91                                table: curr_model.name.clone(),
92                                field: curr_field.clone(),
93                            });
94                        }
95                    }
96
97                    for prev_field in &prev_model.fields {
98                        if !curr_model.fields.iter().any(|f| f.name == prev_field.name) {
99                            changes.push(Change::RemoveColumn {
100                                table: curr_model.name.clone(),
101                                column: prev_field.name.clone(),
102                            });
103                        }
104                    }
105                }
106            }
107        } else {
108            for model in &current.models {
109                changes.push(Change::CreateTable(model.clone()));
110            }
111        }
112
113        changes
114    }
115}
116
117pub mod codegen {
118    use crate::{Field, Modifier, diff::Change};
119
120    pub fn postgres_type(type_name: &str) -> &'static str {
121        match type_name {
122            "BigInt" => "BIGINT",
123            "Int" => "INTEGER",
124            "String" => "TEXT",
125            "JsonB" => "JSONB",
126            "TimestamptZ" => "TIMESTAMP WITH TIME ZONE",
127            "Timestamp" => "TIMESTAMP",
128            "Boolean" => "BOOLEAN",
129            "Real" => "REAL",
130            "Serial" => "SERIAL",
131            _ => "TEXT",
132        }
133    }
134
135    pub fn field_to_sql(field: &Field) -> String {
136        let mut sql = format!("{} {}", field.name, postgres_type(&field.type_name));
137
138        for modifier in &field.modifiers {
139            match modifier {
140                Modifier::PrimaryKey => sql.push_str(" PRIMARY KEY"),
141                Modifier::NotNull => sql.push_str(" NOT NULL"),
142                Modifier::Nullable => sql.push_str(" NULL"),
143                Modifier::Unique => sql.push_str(" UNIQUE"),
144                Modifier::ForeignKey { model, field } => {
145                    let fk_field = field.as_deref().unwrap_or("id");
146                    sql.push_str(&format!(" REFERENCES {} ({})", model, fk_field));
147                }
148            }
149        }
150
151        if field.is_sql_default() {
152            if let Some(value) = field.get_default_value() {
153                let sql_type = postgres_type(&field.type_name);
154                if matches!(
155                sql_type,
156                "BOOLEAN" | "REAL" | "INTEGER" | "BIGINT" | "SERIAL"
157            ) || value == "now()"
158                {
159                    sql.push_str(&format!(" DEFAULT {}", value));
160                } else {
161                    sql.push_str(&format!(" DEFAULT '{}'", value));
162                }
163            }
164        }
165
166        sql
167    }
168
169
170
171
172    pub fn change_to_sql(change: &Change) -> String {
173        match change {
174            Change::CreateTable(model) => {
175                let mut sql = format!("CREATE TABLE IF NOT EXISTS {} ( ", model.name);
176                let pk_columns: Vec<String> = model.fields
177                    .iter()
178                    .filter(|f| f.modifiers.iter().any(|m| matches!(m, Modifier::PrimaryKey)))
179                    .map(|f| f.name.clone())
180                    .collect();
181
182                for (idx, field) in model.fields.iter().enumerate() {
183                    let mut field_sql = field_to_sql(field);
184                    if field.modifiers.iter().any(|m| matches!(m, Modifier::PrimaryKey)) && pk_columns.len() > 1 {
185                        field_sql = field_sql.replace(" PRIMARY KEY", "");
186                    }
187                    sql.push_str(&field_sql);
188                    if idx < model.fields.len() - 1 {
189                        sql.push_str(", ");
190                    }
191                }
192
193                if pk_columns.len() > 1 {
194                    sql.push_str(&format!(", PRIMARY KEY ({}) ", pk_columns.join(", ")));
195                }
196                sql.push_str(");");
197                sql
198            }
199            Change::AddColumn { table, field } => {
200                format!("ALTER TABLE {} ADD COLUMN {};", table, field_to_sql(field))
201            }
202            Change::RemoveColumn { table, column } => {
203                format!("ALTER TABLE {} DROP COLUMN {};", table, column)
204            }
205            Change::RemoveTable(name) => {
206                format!("DROP TABLE IF EXISTS {};", name)
207            }
208            _ => String::new(),
209        }
210    }
211
212
213    pub fn generate_migration_sql(changes: &[Change]) -> String {
214        let mut sql = String::new();
215        for change in changes {
216            sql.push_str(&change_to_sql(change));
217        }
218        sql
219    }
220}
221
222
223pub mod db {
224    use tokio_postgres::Client;
225    use std::env;
226    use crate::Schema;
227
228    pub async fn connect() -> Result<Client, Box<dyn std::error::Error>> {
229        let db_url = env::var("DATABASE_URL")
230            .unwrap_or_else(|_| "host=localhost user=postgres dbname=byteorm".to_string());
231
232        let (client, connection) = tokio_postgres::connect(&db_url, tokio_postgres::tls::NoTls).await?;
233
234        tokio::spawn(async move {
235            if let Err(e) = connection.await {
236                eprintln!("Connection error: {}", e);
237            }
238        });
239
240        Ok(client)
241    }
242
243    pub async fn reset_database(client: &tokio_postgres::Client, schema: &Schema) -> Result<(), Box<dyn std::error::Error>> {
244        for model in &schema.models {
245            let table_name = model.name.to_lowercase();
246            let sql = format!("DROP TABLE IF EXISTS {} CASCADE;", table_name);
247            client.execute(&sql, &[]).await?;
248        }
249
250        client.execute("TRUNCATE _byteorm_schema;", &[]).await.ok();
251
252        Ok(())
253    }
254
255
256
257    pub async fn execute_sql(client: &Client, sql: &str) -> Result<(), Box<dyn std::error::Error>> {
258        for statement in sql.split(';').filter(|s| !s.trim().is_empty()) {
259            let normalized = statement
260                .lines()
261                .map(|l| l.trim())
262                .filter(|l| !l.is_empty())
263                .collect::<Vec<_>>()
264                .join(" ");
265
266            println!("Executing: {}", normalized.trim());
267            match client.execute(&normalized, &[]).await {
268                Ok(_) => println!("  ✅ OK"),
269                Err(e) => {
270                    eprintln!("  ❌ Error: {}", e);
271                    eprintln!("  📍 Error details:");
272                    eprintln!("     Code: {:?}", e.code());
273                    eprintln!("     Message: {}", e);
274                    return Err(e.into());
275                }
276            }
277        }
278        Ok(())
279    }
280
281}
282