schema_sync/snapshot.rs
1//! Developer: s4gor
2//! Github: https://github.com/s4gor
3//!
4//! Schema snapshot system
5//!
6//! Snapshots are normalized, deterministic representations of a schema
7//! at a point in time. They enable:
8//! - Diffing schema version A vs B
9//! - Storing expected schema state
10//! - Version control integration
11//! - Deterministic hash-based versioning
12
13use async_trait::async_trait;
14use serde::{Deserialize, Serialize};
15use std::collections::HashMap;
16
17use crate::errors::Result;
18
19/// A normalized snapshot of a database schema
20///
21/// This structure is database-agnostic and represents the logical
22/// structure of a schema, not the database-specific SQL.
23///
24/// Snapshots are deterministic: the same schema always produces
25/// the same snapshot (order-independent where possible).
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27pub struct SchemaSnapshot {
28 /// Unique identifier for this snapshot (deterministic hash)
29 pub version_hash: String,
30
31 /// Timestamp when snapshot was created (ISO 8601 string)
32 pub created_at: String,
33
34 /// Tables in this schema
35 pub tables: HashMap<String, TableSnapshot>,
36
37 /// Views in this schema
38 pub views: HashMap<String, ViewSnapshot>,
39
40 /// Functions/procedures in this schema
41 pub functions: HashMap<String, FunctionSnapshot>,
42
43 /// Extensions/enums/types in this schema
44 pub types: HashMap<String, TypeSnapshot>,
45}
46
47/// Snapshot of a single table
48#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
49pub struct TableSnapshot {
50 /// Table name
51 pub name: String,
52
53 /// Columns in this table
54 pub columns: Vec<ColumnSnapshot>,
55
56 /// Primary key constraint
57 pub primary_key: Option<PrimaryKeySnapshot>,
58
59 /// Foreign key constraints
60 pub foreign_keys: Vec<ForeignKeySnapshot>,
61
62 /// Unique constraints
63 pub unique_constraints: Vec<UniqueConstraintSnapshot>,
64
65 /// Indexes on this table
66 pub indexes: Vec<IndexSnapshot>,
67
68 /// Check constraints
69 pub check_constraints: Vec<CheckConstraintSnapshot>,
70}
71
72/// Snapshot of a single column
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
74pub struct ColumnSnapshot {
75 /// Column name
76 pub name: String,
77
78 /// Data type (normalized, database-agnostic representation)
79 pub data_type: String,
80
81 /// Whether column is nullable
82 pub nullable: bool,
83
84 /// Default value (if any)
85 pub default_value: Option<String>,
86
87 /// Whether column has auto-increment/sequence
88 pub auto_increment: bool,
89}
90
91/// Snapshot of a primary key
92#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
93pub struct PrimaryKeySnapshot {
94 /// Name of the constraint
95 pub name: String,
96
97 /// Column names that make up the primary key
98 pub columns: Vec<String>,
99}
100
101/// Snapshot of a foreign key
102#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
103pub struct ForeignKeySnapshot {
104 /// Name of the constraint
105 pub name: String,
106
107 /// Columns in this table
108 pub columns: Vec<String>,
109
110 /// Referenced table
111 pub referenced_table: String,
112
113 /// Referenced columns
114 pub referenced_columns: Vec<String>,
115
116 /// On delete action
117 pub on_delete: Option<String>,
118
119 /// On update action
120 pub on_update: Option<String>,
121}
122
123/// Snapshot of a unique constraint
124#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
125pub struct UniqueConstraintSnapshot {
126 /// Name of the constraint
127 pub name: String,
128
129 /// Column names that make up the unique constraint
130 pub columns: Vec<String>,
131}
132
133/// Snapshot of an index
134#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
135pub struct IndexSnapshot {
136 /// Index name
137 pub name: String,
138
139 /// Column names in this index
140 pub columns: Vec<String>,
141
142 /// Whether index is unique
143 pub unique: bool,
144
145 /// Index type (e.g., "btree", "hash", "gin")
146 pub index_type: Option<String>,
147}
148
149/// Snapshot of a check constraint
150#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
151pub struct CheckConstraintSnapshot {
152 /// Name of the constraint
153 pub name: String,
154
155 /// Check expression (normalized)
156 pub expression: String,
157}
158
159/// Snapshot of a view
160#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
161pub struct ViewSnapshot {
162 /// View name
163 pub name: String,
164
165 /// View definition (normalized SQL)
166 pub definition: String,
167}
168
169/// Snapshot of a function/procedure
170#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
171pub struct FunctionSnapshot {
172 /// Function name
173 pub name: String,
174
175 /// Function signature (parameters)
176 pub signature: String,
177
178 /// Function body/definition
179 pub body: String,
180
181 /// Return type
182 pub return_type: String,
183}
184
185/// Snapshot of a custom type (enum, composite, etc.)
186#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
187pub struct TypeSnapshot {
188 /// Type name
189 pub name: String,
190
191 /// Type kind (enum, composite, domain, etc.)
192 pub kind: String,
193
194 /// Type definition (normalized)
195 pub definition: String,
196}
197
198/// Trait for storing and retrieving schema snapshots
199///
200/// Implementations can store snapshots in:
201/// - File system
202/// - Database
203/// - Version control
204/// - In-memory (for testing)
205#[async_trait]
206pub trait SnapshotStore: Send + Sync {
207 /// Store a snapshot for a tenant
208 async fn store(&self, tenant: &crate::cli::TenantContext, snapshot: &SchemaSnapshot) -> Result<()>;
209
210 /// Retrieve the latest snapshot for a tenant
211 async fn get_latest(&self, tenant: &crate::cli::TenantContext) -> Result<Option<SchemaSnapshot>>;
212
213 /// Retrieve a specific snapshot by version hash
214 async fn get_by_hash(
215 &self,
216 tenant: &crate::cli::TenantContext,
217 version_hash: &str,
218 ) -> Result<Option<SchemaSnapshot>>;
219
220 /// List all snapshots for a tenant (ordered by creation time, newest first)
221 async fn list(&self, tenant: &crate::cli::TenantContext) -> Result<Vec<SchemaSnapshot>>;
222
223 /// Compare two snapshots and return their version hashes
224 async fn compare(
225 &self,
226 tenant: &crate::cli::TenantContext,
227 hash_a: &str,
228 hash_b: &str,
229 ) -> Result<crate::diff::SchemaDiff>;
230}
231
232/// Calculate a deterministic hash for a snapshot
233///
234/// This function ensures that the same schema always produces the same hash,
235/// regardless of the order of elements or other non-semantic differences.
236pub fn calculate_version_hash(snapshot: &SchemaSnapshot) -> String {
237 // In a real implementation, this would:
238 // 1. Normalize the snapshot (sort maps, etc.)
239 // 2. Serialize to a canonical format
240 // 3. Hash using SHA-256 or similar
241 // 4. Return hex-encoded hash
242
243 // For now, return a placeholder
244 format!("hash_{}", snapshot.tables.len())
245}
246