Skip to main content

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