Skip to main content

reinhardt_db/migrations/operations/
postgres.rs

1//! PostgreSQL-specific migration operations
2//!
3//! This module provides PostgreSQL-specific operations inspired by Django's
4//! `django/contrib/postgres/operations.py`. These operations allow you to:
5//!
6//! - Create and manage PostgreSQL extensions
7//! - Use PostgreSQL-specific index types (GIN, GiST, BRIN, etc.)
8//! - Work with PostgreSQL functions and triggers
9//!
10//! # Example
11//!
12//! ```rust
13//! use reinhardt_db::migrations::operations::postgres::{CreateExtension, CreateCollation};
14//!
15//! // Create the hstore extension
16//! let ext = CreateExtension::new("hstore");
17//!
18//! // Create a custom collation
19//! let collation = CreateCollation::new("german", "de_DE");
20//! ```
21
22use crate::backends::schema::BaseDatabaseSchemaEditor;
23use crate::migrations::ProjectState;
24use pg_escape::quote_literal;
25use serde::{Deserialize, Serialize};
26
27/// Create a PostgreSQL extension
28///
29/// PostgreSQL extensions add additional functionality to the database.
30/// Common extensions include hstore, postgis, pg_trgm, and many others.
31///
32/// # Example
33///
34/// ```rust
35/// use reinhardt_db::migrations::operations::postgres::CreateExtension;
36/// use reinhardt_db::migrations::ProjectState;
37///
38/// let mut state = ProjectState::new();
39/// let ext = CreateExtension::new("hstore");
40///
41/// // Extensions don't modify project state
42/// ext.state_forwards("myapp", &mut state);
43/// ```
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct CreateExtension {
46	/// The name.
47	pub name: String,
48	/// The schema.
49	pub schema: Option<String>,
50	/// The version.
51	pub version: Option<String>,
52}
53
54impl CreateExtension {
55	/// Create a new CreateExtension operation
56	///
57	/// # Example
58	///
59	/// ```rust
60	/// use reinhardt_db::migrations::operations::postgres::CreateExtension;
61	///
62	/// let ext = CreateExtension::new("hstore");
63	/// assert_eq!(ext.name, "hstore");
64	/// assert!(ext.schema.is_none());
65	/// ```
66	pub fn new(name: impl Into<String>) -> Self {
67		Self {
68			name: name.into(),
69			schema: None,
70			version: None,
71		}
72	}
73
74	/// Set the schema where the extension should be created
75	pub fn with_schema(mut self, schema: impl Into<String>) -> Self {
76		self.schema = Some(schema.into());
77		self
78	}
79
80	/// Set a specific version of the extension
81	pub fn with_version(mut self, version: impl Into<String>) -> Self {
82		self.version = Some(version.into());
83		self
84	}
85
86	/// Apply to project state (extensions don't modify state)
87	pub fn state_forwards(&self, _app_label: &str, _state: &mut ProjectState) {
88		// Extensions are database-level and don't affect the application schema
89	}
90
91	/// Generate SQL using schema editor
92	///
93	/// # Example
94	///
95	/// ```rust,no_run
96	/// use reinhardt_db::migrations::operations::postgres::CreateExtension;
97	/// use reinhardt_db::backends::schema::factory::{SchemaEditorFactory, DatabaseType};
98	///
99	/// let ext = CreateExtension::new("hstore").with_schema("public");
100	/// let factory = SchemaEditorFactory::new();
101	/// let editor = factory.create_for_database(DatabaseType::PostgreSQL);
102	///
103	/// let sql = ext.database_forwards(editor.as_ref());
104	/// assert_eq!(sql.len(), 1);
105	/// assert!(sql[0].contains("CREATE EXTENSION"));
106	/// assert!(sql[0].contains("hstore"));
107	/// ```
108	pub fn database_forwards(&self, _schema_editor: &dyn BaseDatabaseSchemaEditor) -> Vec<String> {
109		let mut parts = vec!["CREATE EXTENSION IF NOT EXISTS".to_string()];
110		// Always use double quotes for PostgreSQL identifier safety
111		parts.push(format!("\"{}\"", self.name));
112
113		if let Some(ref schema) = self.schema {
114			parts.push("SCHEMA".to_string());
115			parts.push(format!("\"{}\"", schema));
116		}
117
118		if let Some(ref version) = self.version {
119			parts.push("VERSION".to_string());
120			parts.push(quote_literal(version));
121		}
122
123		vec![format!("{};", parts.join(" "))]
124	}
125
126	/// Generate reverse SQL
127	///
128	/// # Example
129	///
130	/// ```rust,no_run
131	/// use reinhardt_db::migrations::operations::postgres::CreateExtension;
132	/// use reinhardt_db::backends::schema::factory::{SchemaEditorFactory, DatabaseType};
133	///
134	/// let ext = CreateExtension::new("hstore");
135	/// let factory = SchemaEditorFactory::new();
136	/// let editor = factory.create_for_database(DatabaseType::PostgreSQL);
137	///
138	/// let sql = ext.database_backwards(editor.as_ref());
139	/// assert_eq!(sql.len(), 1);
140	/// assert!(sql[0].contains("DROP EXTENSION"));
141	/// ```
142	pub fn database_backwards(&self, _schema_editor: &dyn BaseDatabaseSchemaEditor) -> Vec<String> {
143		vec![format!("DROP EXTENSION IF EXISTS \"{}\";", self.name)]
144	}
145}
146
147/// Drop a PostgreSQL extension
148///
149/// # Example
150///
151/// ```rust
152/// use reinhardt_db::migrations::operations::postgres::DropExtension;
153///
154/// let drop = DropExtension::new("hstore");
155/// ```
156#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct DropExtension {
158	/// The name.
159	pub name: String,
160}
161
162impl DropExtension {
163	/// Create a new DropExtension operation
164	pub fn new(name: impl Into<String>) -> Self {
165		Self { name: name.into() }
166	}
167
168	/// Apply to project state (extensions don't modify state)
169	pub fn state_forwards(&self, _app_label: &str, _state: &mut ProjectState) {}
170
171	/// Generate SQL
172	///
173	/// # Example
174	///
175	/// ```rust,no_run
176	/// use reinhardt_db::migrations::operations::postgres::DropExtension;
177	/// use reinhardt_db::backends::schema::factory::{SchemaEditorFactory, DatabaseType};
178	///
179	/// let drop = DropExtension::new("hstore");
180	/// let factory = SchemaEditorFactory::new();
181	/// let editor = factory.create_for_database(DatabaseType::PostgreSQL);
182	///
183	/// let sql = drop.database_forwards(editor.as_ref());
184	/// assert_eq!(sql.len(), 1);
185	/// assert!(sql[0].contains("DROP EXTENSION"));
186	/// ```
187	pub fn database_forwards(&self, _schema_editor: &dyn BaseDatabaseSchemaEditor) -> Vec<String> {
188		vec![format!("DROP EXTENSION IF EXISTS \"{}\";", self.name)]
189	}
190
191	/// Generate reverse SQL (recreate extension)
192	pub fn database_backwards(&self, _schema_editor: &dyn BaseDatabaseSchemaEditor) -> Vec<String> {
193		vec![format!("CREATE EXTENSION IF NOT EXISTS \"{}\";", self.name)]
194	}
195}
196
197/// Create a PostgreSQL collation
198///
199/// Collations define how text is sorted and compared in the database.
200///
201/// # Example
202///
203/// ```rust
204/// use reinhardt_db::migrations::operations::postgres::CreateCollation;
205///
206/// let collation = CreateCollation::new("german", "de_DE");
207/// ```
208#[derive(Debug, Clone, Serialize, Deserialize)]
209pub struct CreateCollation {
210	/// The name.
211	pub name: String,
212	/// The locale.
213	pub locale: String,
214	/// The provider.
215	pub provider: Option<String>,
216}
217
218impl CreateCollation {
219	/// Create a new collation
220	///
221	/// # Example
222	///
223	/// ```rust
224	/// use reinhardt_db::migrations::operations::postgres::CreateCollation;
225	///
226	/// let collation = CreateCollation::new("german", "de_DE");
227	/// assert_eq!(collation.name, "german");
228	/// assert_eq!(collation.locale, "de_DE");
229	/// ```
230	pub fn new(name: impl Into<String>, locale: impl Into<String>) -> Self {
231		Self {
232			name: name.into(),
233			locale: locale.into(),
234			provider: None,
235		}
236	}
237
238	/// Set the collation provider (icu or libc)
239	pub fn with_provider(mut self, provider: impl Into<String>) -> Self {
240		self.provider = Some(provider.into());
241		self
242	}
243
244	/// Apply to project state
245	pub fn state_forwards(&self, _app_label: &str, _state: &mut ProjectState) {}
246
247	/// Generate SQL
248	///
249	/// # Example
250	///
251	/// ```rust,no_run
252	/// use reinhardt_db::migrations::operations::postgres::CreateCollation;
253	/// use reinhardt_db::backends::schema::factory::{SchemaEditorFactory, DatabaseType};
254	///
255	/// let collation = CreateCollation::new("german", "de_DE");
256	/// let factory = SchemaEditorFactory::new();
257	/// let editor = factory.create_for_database(DatabaseType::PostgreSQL);
258	///
259	/// let sql = collation.database_forwards(editor.as_ref());
260	/// assert_eq!(sql.len(), 1);
261	/// assert!(sql[0].contains("CREATE COLLATION"));
262	/// assert!(sql[0].contains("german"));
263	/// ```
264	pub fn database_forwards(&self, _schema_editor: &dyn BaseDatabaseSchemaEditor) -> Vec<String> {
265		// Always use double quotes for PostgreSQL identifier safety
266		let mut sql = format!(
267			"CREATE COLLATION IF NOT EXISTS \"{}\" (LOCALE = {}",
268			self.name,
269			quote_literal(&self.locale)
270		);
271
272		if let Some(ref provider) = self.provider {
273			sql.push_str(&format!(", PROVIDER = {}", quote_literal(provider)));
274		}
275
276		sql.push_str(");");
277		vec![sql]
278	}
279
280	/// Generate reverse SQL
281	pub fn database_backwards(&self, _schema_editor: &dyn BaseDatabaseSchemaEditor) -> Vec<String> {
282		vec![format!("DROP COLLATION IF EXISTS \"{}\";", self.name)]
283	}
284}
285
286/// Commonly used PostgreSQL extensions
287pub mod extensions {
288	use super::CreateExtension;
289
290	/// Create the hstore extension for key-value storage
291	///
292	/// # Example
293	///
294	/// ```rust
295	/// use reinhardt_db::migrations::operations::postgres::extensions::hstore;
296	///
297	/// let ext = hstore();
298	/// assert_eq!(ext.name, "hstore");
299	/// ```
300	pub fn hstore() -> CreateExtension {
301		CreateExtension::new("hstore")
302	}
303
304	/// Create the pg_trgm extension for trigram matching
305	pub fn pg_trgm() -> CreateExtension {
306		CreateExtension::new("pg_trgm")
307	}
308
309	/// Create the uuid-ossp extension for UUID generation
310	pub fn uuid_ossp() -> CreateExtension {
311		CreateExtension::new("uuid-ossp")
312	}
313
314	/// Create the postgis extension for geographic data
315	pub fn postgis() -> CreateExtension {
316		CreateExtension::new("postgis")
317	}
318
319	/// Create the btree_gin extension for B-tree GIN indexes
320	pub fn btree_gin() -> CreateExtension {
321		CreateExtension::new("btree_gin")
322	}
323
324	/// Create the btree_gist extension for B-tree GiST indexes
325	pub fn btree_gist() -> CreateExtension {
326		CreateExtension::new("btree_gist")
327	}
328
329	/// Create the citext extension for case-insensitive text
330	pub fn citext() -> CreateExtension {
331		CreateExtension::new("citext")
332	}
333
334	/// Create the unaccent extension for removing accents
335	pub fn unaccent() -> CreateExtension {
336		CreateExtension::new("unaccent")
337	}
338}
339
340// MigrationOperation trait implementation for Django-style naming
341use crate::migrations::operation_trait::MigrationOperation;
342
343impl MigrationOperation for CreateExtension {
344	fn migration_name_fragment(&self) -> Option<String> {
345		Some(format!("create_extension_{}", self.name.to_lowercase()))
346	}
347
348	fn describe(&self) -> String {
349		format!("Create PostgreSQL extension {}", self.name)
350	}
351}
352
353impl MigrationOperation for DropExtension {
354	fn migration_name_fragment(&self) -> Option<String> {
355		Some(format!("drop_extension_{}", self.name.to_lowercase()))
356	}
357
358	fn describe(&self) -> String {
359		format!("Drop PostgreSQL extension {}", self.name)
360	}
361}
362
363impl MigrationOperation for CreateCollation {
364	fn migration_name_fragment(&self) -> Option<String> {
365		Some(format!("create_collation_{}", self.name.to_lowercase()))
366	}
367
368	fn describe(&self) -> String {
369		format!("Create collation {}", self.name)
370	}
371}
372
373#[cfg(test)]
374mod tests {
375	use super::*;
376
377	#[test]
378	fn test_create_extension_basic() {
379		let ext = CreateExtension::new("hstore");
380		assert_eq!(ext.name, "hstore");
381		assert!(ext.schema.is_none());
382		assert!(ext.version.is_none());
383	}
384
385	#[test]
386	fn test_create_extension_with_schema() {
387		let ext = CreateExtension::new("hstore").with_schema("public");
388		assert_eq!(ext.name, "hstore");
389		assert_eq!(ext.schema, Some("public".to_string()));
390	}
391
392	#[test]
393	fn test_create_extension_with_version() {
394		let ext = CreateExtension::new("postgis").with_version("3.0.0");
395		assert_eq!(ext.version, Some("3.0.0".to_string()));
396	}
397
398	#[cfg(feature = "postgres")]
399	#[test]
400	fn test_create_extension_database_forwards() {
401		use crate::backends::schema::test_utils::MockSchemaEditor;
402
403		let ext = CreateExtension::new("hstore");
404		let editor = MockSchemaEditor::new();
405
406		let sql = ext.database_forwards(&editor);
407		assert_eq!(sql.len(), 1);
408		assert!(sql[0].contains("CREATE EXTENSION IF NOT EXISTS"));
409		assert!(sql[0].contains("\"hstore\""));
410	}
411
412	#[cfg(feature = "postgres")]
413	#[test]
414	fn test_create_extension_with_schema_sql() {
415		use crate::backends::schema::test_utils::MockSchemaEditor;
416
417		let ext = CreateExtension::new("hstore").with_schema("public");
418		let editor = MockSchemaEditor::new();
419
420		let sql = ext.database_forwards(&editor);
421		assert!(sql[0].contains("SCHEMA"));
422		assert!(sql[0].contains("\"public\""));
423	}
424
425	#[cfg(feature = "postgres")]
426	#[test]
427	fn test_drop_extension() {
428		use crate::backends::schema::test_utils::MockSchemaEditor;
429
430		let drop = DropExtension::new("hstore");
431		let editor = MockSchemaEditor::new();
432
433		let sql = drop.database_forwards(&editor);
434		assert_eq!(sql.len(), 1);
435		assert!(sql[0].contains("DROP EXTENSION IF EXISTS"));
436		assert!(sql[0].contains("\"hstore\""));
437	}
438
439	#[cfg(feature = "postgres")]
440	#[test]
441	fn test_create_collation() {
442		use crate::backends::schema::test_utils::MockSchemaEditor;
443
444		let collation = CreateCollation::new("german", "de_DE");
445		let editor = MockSchemaEditor::new();
446
447		let sql = collation.database_forwards(&editor);
448		assert_eq!(sql.len(), 1);
449		assert!(sql[0].contains("CREATE COLLATION IF NOT EXISTS"));
450		assert!(sql[0].contains("\"german\""));
451		assert!(sql[0].contains("de_DE"));
452	}
453
454	#[test]
455	fn test_extension_helpers() {
456		let hstore = extensions::hstore();
457		assert_eq!(hstore.name, "hstore");
458
459		let pg_trgm = extensions::pg_trgm();
460		assert_eq!(pg_trgm.name, "pg_trgm");
461
462		let postgis = extensions::postgis();
463		assert_eq!(postgis.name, "postgis");
464	}
465}