Skip to main content

reinhardt_query/query/database/
attach_database.rs

1//! ATTACH DATABASE statement builder
2//!
3//! This module provides the `AttachDatabaseStatement` type for building SQL ATTACH DATABASE queries.
4//! ATTACH DATABASE is a SQLite-specific feature for connecting additional database files.
5
6use crate::types::{DynIden, IntoIden};
7use crate::value::Value;
8
9use crate::query::traits::{QueryBuilderTrait, QueryStatementBuilder, QueryStatementWriter};
10
11/// ATTACH DATABASE statement builder (SQLite-specific)
12///
13/// This struct provides a fluent API for constructing ATTACH DATABASE queries.
14/// ATTACH DATABASE allows attaching additional database files to the current connection.
15///
16/// # Examples
17///
18/// ```rust
19/// use reinhardt_query::prelude::*;
20///
21/// // ATTACH DATABASE 'path/to/db.sqlite' AS auxiliary
22/// let query = Query::attach_database()
23///     .file_path("path/to/db.sqlite")
24///     .as_name("auxiliary");
25/// ```
26#[derive(Debug, Clone)]
27pub struct AttachDatabaseStatement {
28	pub(crate) file_path: Option<String>,
29	pub(crate) database_name: Option<DynIden>,
30}
31
32impl AttachDatabaseStatement {
33	/// Create a new ATTACH DATABASE statement
34	///
35	/// # Examples
36	///
37	/// ```rust
38	/// use reinhardt_query::prelude::*;
39	///
40	/// let query = Query::attach_database();
41	/// ```
42	pub fn new() -> Self {
43		Self {
44			file_path: None,
45			database_name: None,
46		}
47	}
48
49	/// Take the ownership of data in the current [`AttachDatabaseStatement`]
50	pub fn take(&mut self) -> Self {
51		Self {
52			file_path: self.file_path.take(),
53			database_name: self.database_name.take(),
54		}
55	}
56
57	/// Set the file path to the database file
58	///
59	/// # Examples
60	///
61	/// ```rust
62	/// use reinhardt_query::prelude::*;
63	///
64	/// let query = Query::attach_database()
65	///     .file_path("path/to/db.sqlite");
66	/// ```
67	pub fn file_path<S>(&mut self, path: S) -> &mut Self
68	where
69		S: Into<String>,
70	{
71		self.file_path = Some(path.into());
72		self
73	}
74
75	/// Set the alias name for the attached database
76	///
77	/// # Examples
78	///
79	/// ```rust
80	/// use reinhardt_query::prelude::*;
81	///
82	/// let query = Query::attach_database()
83	///     .file_path("path/to/db.sqlite")
84	///     .as_name("auxiliary");
85	/// ```
86	pub fn as_name<N>(&mut self, name: N) -> &mut Self
87	where
88		N: IntoIden,
89	{
90		self.database_name = Some(name.into_iden());
91		self
92	}
93}
94
95impl Default for AttachDatabaseStatement {
96	fn default() -> Self {
97		Self::new()
98	}
99}
100
101impl QueryStatementBuilder for AttachDatabaseStatement {
102	fn build_any(&self, query_builder: &dyn QueryBuilderTrait) -> (String, crate::value::Values) {
103		use std::any::Any;
104		if (query_builder as &dyn Any)
105			.downcast_ref::<crate::backend::PostgresQueryBuilder>()
106			.is_some()
107		{
108			panic!("ATTACH DATABASE is SQLite-specific and not supported in PostgreSQL");
109		}
110		if (query_builder as &dyn Any)
111			.downcast_ref::<crate::backend::MySqlQueryBuilder>()
112			.is_some()
113		{
114			panic!("ATTACH DATABASE is SQLite-specific and not supported in MySQL");
115		}
116		if let Some(sqlite_builder) =
117			(query_builder as &dyn Any).downcast_ref::<crate::backend::SqliteQueryBuilder>()
118		{
119			use crate::backend::QueryBuilder as _;
120			let file_path = self
121				.file_path
122				.as_deref()
123				.expect("ATTACH DATABASE requires a file path");
124			let db_name = self
125				.database_name
126				.as_ref()
127				.expect("ATTACH DATABASE requires a schema name (AS clause)");
128			// Reuse Value::to_sql_literal for proper string literal escaping
129			let escaped_file_path =
130				Value::String(Some(Box::new(file_path.to_string()))).to_sql_literal();
131			// Reuse escape_identifier for proper identifier escaping
132			let escaped_db_name = sqlite_builder.escape_identifier(&db_name.to_string());
133			let sql = format!(
134				"ATTACH DATABASE {} AS {}",
135				escaped_file_path, escaped_db_name,
136			);
137			return (sql, crate::value::Values::new());
138		}
139		if (query_builder as &dyn Any)
140			.downcast_ref::<crate::backend::CockroachDBQueryBuilder>()
141			.is_some()
142		{
143			panic!("ATTACH DATABASE is SQLite-specific and not supported in CockroachDB");
144		}
145		panic!("Unsupported query builder type");
146	}
147}
148
149impl QueryStatementWriter for AttachDatabaseStatement {}
150
151#[cfg(test)]
152mod tests {
153	use super::*;
154	use rstest::*;
155
156	#[rstest]
157	fn test_attach_database_new() {
158		let stmt = AttachDatabaseStatement::new();
159		assert!(stmt.file_path.is_none());
160		assert!(stmt.database_name.is_none());
161	}
162
163	#[rstest]
164	fn test_attach_database_with_file_path() {
165		let mut stmt = AttachDatabaseStatement::new();
166		stmt.file_path("path/to/db.sqlite");
167		assert_eq!(stmt.file_path.as_ref().unwrap(), "path/to/db.sqlite");
168	}
169
170	#[rstest]
171	fn test_attach_database_with_as_name() {
172		let mut stmt = AttachDatabaseStatement::new();
173		stmt.as_name("auxiliary");
174		assert_eq!(
175			stmt.database_name.as_ref().unwrap().to_string(),
176			"auxiliary"
177		);
178	}
179
180	#[rstest]
181	fn test_attach_database_full() {
182		let mut stmt = AttachDatabaseStatement::new();
183		stmt.file_path("path/to/db.sqlite").as_name("auxiliary");
184		assert_eq!(stmt.file_path.as_ref().unwrap(), "path/to/db.sqlite");
185		assert_eq!(
186			stmt.database_name.as_ref().unwrap().to_string(),
187			"auxiliary"
188		);
189	}
190
191	#[rstest]
192	fn test_attach_database_take() {
193		let mut stmt = AttachDatabaseStatement::new();
194		stmt.file_path("path/to/db.sqlite").as_name("auxiliary");
195		let taken = stmt.take();
196		assert!(stmt.file_path.is_none());
197		assert!(stmt.database_name.is_none());
198		assert_eq!(taken.file_path.as_ref().unwrap(), "path/to/db.sqlite");
199		assert_eq!(
200			taken.database_name.as_ref().unwrap().to_string(),
201			"auxiliary"
202		);
203	}
204
205	#[rstest]
206	fn test_attach_database_default() {
207		let stmt = AttachDatabaseStatement::default();
208		assert!(stmt.file_path.is_none());
209		assert!(stmt.database_name.is_none());
210	}
211
212	#[rstest]
213	fn test_attach_database_build_sql() {
214		// Arrange
215		let mut stmt = AttachDatabaseStatement::new();
216		stmt.file_path("path/to/db.sqlite").as_name("auxiliary");
217
218		// Act
219		let (sql, values) = stmt.build_any(&crate::backend::SqliteQueryBuilder);
220
221		// Assert
222		assert_eq!(sql, r#"ATTACH DATABASE 'path/to/db.sqlite' AS "auxiliary""#);
223		assert!(values.0.is_empty());
224	}
225
226	#[rstest]
227	fn test_attach_database_file_path_with_single_quotes() {
228		// Arrange
229		let mut stmt = AttachDatabaseStatement::new();
230		stmt.file_path("/path/to/file's.db").as_name("auxiliary");
231
232		// Act
233		let (sql, _) = stmt.build_any(&crate::backend::SqliteQueryBuilder);
234
235		// Assert
236		assert_eq!(
237			sql,
238			r#"ATTACH DATABASE '/path/to/file''s.db' AS "auxiliary""#
239		);
240	}
241
242	#[rstest]
243	fn test_attach_database_db_name_with_double_quotes() {
244		// Arrange
245		let mut stmt = AttachDatabaseStatement::new();
246		stmt.file_path("path/to/db.sqlite").as_name(r#"my"db"#);
247
248		// Act
249		let (sql, _) = stmt.build_any(&crate::backend::SqliteQueryBuilder);
250
251		// Assert
252		assert_eq!(sql, r#"ATTACH DATABASE 'path/to/db.sqlite' AS "my""db""#);
253	}
254
255	#[rstest]
256	fn test_attach_database_both_special_chars() {
257		// Arrange
258		let mut stmt = AttachDatabaseStatement::new();
259		stmt.file_path("/tmp/user's data/test.db")
260			.as_name(r#"special"name"#);
261
262		// Act
263		let (sql, _) = stmt.build_any(&crate::backend::SqliteQueryBuilder);
264
265		// Assert
266		assert_eq!(
267			sql,
268			r#"ATTACH DATABASE '/tmp/user''s data/test.db' AS "special""name""#
269		);
270	}
271}