Skip to main content

drizzle_migrations/sqlite/
snapshot.rs

1//! `SQLite` snapshot type matching drizzle-kit format.
2//!
3//! `SQLiteSnapshot` is a type alias of the generic [`crate::snapshot::Snapshot`]
4//! — the CRUD / serde IO surface lives once in that module. This file
5//! supplies only:
6//!
7//! * the [`SnapshotEntity`] impl that pins the SQLite dialect / version
8//!   constants used by `Snapshot::new()`;
9//! * the legacy v6 types preserved for reading old snapshots.
10
11use super::ddl::SqliteEntity;
12use crate::snapshot::{Snapshot, SnapshotEntity};
13use crate::version::SQLITE_SNAPSHOT_VERSION;
14use serde::{Deserialize, Serialize};
15
16impl SnapshotEntity for SqliteEntity {
17    const DIALECT: &'static str = "sqlite";
18    const SNAPSHOT_VERSION: &'static str = SQLITE_SNAPSHOT_VERSION;
19}
20
21/// `SQLite` schema snapshot — drizzle-kit beta v7 format.
22///
23/// Type alias of [`Snapshot<SqliteEntity>`]; see the generic type's docs
24/// for the field set and IO surface.
25pub type SQLiteSnapshot = Snapshot<SqliteEntity>;
26
27// =============================================================================
28// Legacy V6 Snapshot Format (for reading old snapshots)
29// =============================================================================
30
31use super::ddl::{Table, View};
32use std::collections::HashMap;
33
34/// Schema metadata for tracking renames (legacy v6 format)
35#[derive(Serialize, Deserialize, Clone, Debug, Default)]
36pub struct Meta {
37    /// Table renames: `old_name` -> `new_name`
38    #[serde(default)]
39    pub tables: HashMap<String, String>,
40    /// Column renames: "`table.old_column`" -> "`table.new_column`"
41    #[serde(default)]
42    pub columns: HashMap<String, String>,
43}
44
45/// Internal kit metadata (legacy v6 format)
46#[derive(Serialize, Deserialize, Clone, Debug, Default)]
47pub struct Internal {
48    /// Index-specific internals
49    #[serde(default)]
50    pub indexes: HashMap<String, IndexInternal>,
51}
52
53/// Internal index metadata (legacy v6 format)
54#[derive(Serialize, Deserialize, Clone, Debug, Default)]
55pub struct IndexInternal {
56    /// Column-specific metadata
57    #[serde(default)]
58    pub columns: HashMap<String, ColumnInternal>,
59}
60
61/// Internal column metadata (legacy v6 format)
62#[derive(Serialize, Deserialize, Clone, Debug, Default)]
63#[serde(rename_all = "camelCase")]
64pub struct ColumnInternal {
65    /// Is this column an expression?
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub is_expression: Option<bool>,
68}
69
70/// Legacy `SQLite` schema snapshot - v6 format (for reading old snapshots)
71#[derive(Serialize, Deserialize, Clone, Debug)]
72#[serde(rename_all = "camelCase")]
73pub struct SnapshotV6 {
74    /// Schema version ("6")
75    pub version: String,
76    /// Dialect identifier
77    pub dialect: String,
78    /// Unique ID for this snapshot
79    pub id: String,
80    /// ID of the previous snapshot in the chain
81    pub prev_id: String,
82    /// Tables in the schema
83    pub tables: HashMap<String, Table>,
84    /// Views in the schema
85    #[serde(default)]
86    pub views: HashMap<String, View>,
87    /// Enums (empty for `SQLite`, kept for compatibility with drizzle-kit's
88    /// cross-dialect v6 format).
89    #[serde(default)]
90    pub enums: HashMap<String, serde_json::Value>,
91    /// Metadata for tracking renames
92    #[serde(rename = "_meta")]
93    pub meta: Meta,
94    /// Internal kit metadata
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub internal: Option<Internal>,
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102    use crate::sqlite::ddl::{Column, Index, IndexColumn, Table};
103
104    #[test]
105    fn test_new_snapshot() {
106        let snapshot = SQLiteSnapshot::new();
107        assert_eq!(snapshot.version, "7");
108        assert_eq!(snapshot.dialect, "sqlite");
109        assert_eq!(snapshot.prev_ids[0], crate::ORIGIN_UUID);
110        assert!(snapshot.ddl.is_empty());
111    }
112
113    #[test]
114    fn test_add_entity() {
115        let mut snapshot = SQLiteSnapshot::new();
116
117        // Add a table entity
118        let table = Table::new("users");
119        snapshot.add_entity(SqliteEntity::Table(table));
120
121        // Add column entities
122        let id_col = Column::new("users", "id", "integer")
123            .not_null()
124            .autoincrement();
125        let name_col = Column::new("users", "name", "text").not_null();
126
127        snapshot.add_entity(SqliteEntity::Column(id_col));
128        snapshot.add_entity(SqliteEntity::Column(name_col));
129
130        assert_eq!(snapshot.ddl.len(), 3);
131    }
132
133    #[test]
134    fn test_snapshot_serialization() {
135        let mut snapshot = SQLiteSnapshot::new();
136
137        let table = Table::new("users");
138        snapshot.add_entity(SqliteEntity::Table(table));
139
140        let col = Column::new("users", "id", "integer").not_null();
141        snapshot.add_entity(SqliteEntity::Column(col));
142
143        let json = snapshot.to_json().unwrap();
144
145        // Verify round-trip via structured comparison
146        let parsed = SQLiteSnapshot::from_json(&json).unwrap();
147        assert_eq!(parsed.version, "7");
148        assert_eq!(parsed.dialect, "sqlite");
149        assert_eq!(parsed.ddl.len(), 2);
150
151        // Verify JSON structure via serde_json::Value
152        let value: serde_json::Value = serde_json::from_str(&json).unwrap();
153        assert_eq!(value["version"], "7");
154        assert_eq!(value["dialect"], "sqlite");
155        assert_eq!(value["ddl"][0]["entityType"], "tables");
156        assert_eq!(value["ddl"][1]["entityType"], "columns");
157    }
158
159    #[test]
160    fn partial_index_predicate_round_trips_through_snapshot_json() {
161        let mut snapshot = SQLiteSnapshot::new();
162        let mut index = Index::new("jobs", "idx_jobs_unclaimed", vec![IndexColumn::new("id")]);
163        index.where_clause = Some("builder IS NULL".into());
164        snapshot.add_entity(SqliteEntity::Index(index));
165
166        let json = snapshot.to_json().unwrap();
167        assert_eq!(
168            serde_json::from_str::<serde_json::Value>(&json).unwrap()["ddl"][0]["where"],
169            "builder IS NULL"
170        );
171        let parsed = SQLiteSnapshot::from_json(&json).unwrap();
172        let index = parsed
173            .ddl
174            .iter()
175            .find_map(|entity| match entity {
176                SqliteEntity::Index(index) => Some(index),
177                _ => None,
178            })
179            .expect("partial index");
180        assert_eq!(index.where_clause.as_deref(), Some("builder IS NULL"));
181    }
182
183    #[test]
184    fn test_column_json_format_matches_drizzle_kit() {
185        // Create a column with autoincrement to verify field naming
186        let col = Column::new("users", "id", "integer")
187            .not_null()
188            .autoincrement();
189
190        let value: serde_json::Value = serde_json::to_value(&col).unwrap();
191
192        // Verify field names match drizzle-kit exactly:
193        // - autoincrement (not autoIncrement)
194        // - notNull (camelCase)
195        // - type (renamed from sql_type)
196        assert_eq!(value["autoincrement"], serde_json::json!(true));
197        assert_eq!(value["notNull"], serde_json::json!(true));
198        assert_eq!(value["type"], "integer");
199        assert_eq!(value["table"], "users");
200        assert_eq!(value["name"], "id");
201
202        // Verify it doesn't contain snake_case versions
203        assert!(
204            value.get("sql_type").is_none(),
205            "Should not contain 'sql_type'"
206        );
207        assert!(
208            value.get("not_null").is_none(),
209            "Should not contain 'not_null'"
210        );
211    }
212}