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 ¤t.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 ¤t.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