sqlx_core_oldapi/testing/
fixtures.rs1use crate::database::{Database, HasArguments};
4
5use crate::query_builder::QueryBuilder;
6
7use indexmap::set::IndexSet;
8use std::cmp;
9use std::collections::{BTreeMap, HashMap};
10use std::marker::PhantomData;
11use std::sync::Arc;
12
13pub type Result<T, E = FixtureError> = std::result::Result<T, E>;
14
15pub struct FixtureSnapshot<DB> {
21 tables: BTreeMap<TableName, Table>,
22 db: PhantomData<DB>,
23}
24
25#[derive(Debug, thiserror::Error)]
26#[error("could not create fixture: {0}")]
27pub struct FixtureError(String);
28
29pub struct Fixture<DB> {
30 ops: Vec<FixtureOp>,
31 db: PhantomData<DB>,
32}
33
34enum FixtureOp {
35 Insert {
36 table: TableName,
37 columns: Vec<ColumnName>,
38 rows: Vec<Vec<Value>>,
39 },
40 }
42
43type TableName = Arc<str>;
44type ColumnName = Arc<str>;
45type Value = String;
46
47struct Table {
48 name: TableName,
49 columns: IndexSet<ColumnName>,
50 rows: Vec<Vec<Value>>,
51 foreign_keys: HashMap<ColumnName, (TableName, ColumnName)>,
52}
53
54macro_rules! fixture_assert (
55 ($cond:expr, $msg:literal $($arg:tt)*) => {
56 if !($cond) {
57 return Err(FixtureError(format!($msg $($arg)*)))
58 }
59 }
60);
61
62impl<DB: Database> FixtureSnapshot<DB> {
63 pub fn additive_fixture(&self) -> Result<Fixture<DB>> {
74 let visit_order = self.calculate_visit_order()?;
75
76 let mut ops = Vec::new();
77
78 for table_name in visit_order {
79 let table = self.tables.get(&table_name).unwrap();
80
81 ops.push(FixtureOp::Insert {
82 table: table_name,
83 columns: table.columns.iter().cloned().collect(),
84 rows: table.rows.clone(),
85 });
86 }
87
88 Ok(Fixture { ops, db: self.db })
89 }
90
91 fn calculate_visit_order(&self) -> Result<Vec<TableName>> {
96 let mut table_depths = HashMap::with_capacity(self.tables.len());
97 let mut visited_set = IndexSet::with_capacity(self.tables.len());
98
99 for table in self.tables.values() {
100 foreign_key_depth(&self.tables, table, &mut table_depths, &mut visited_set)?;
101 visited_set.clear();
102 }
103
104 let mut table_names: Vec<TableName> = table_depths.keys().cloned().collect();
105 table_names.sort_by_key(|name| table_depths.get(name).unwrap());
106 Ok(table_names)
107 }
108}
109
110#[allow(clippy::to_string_trait_impl)]
113impl<DB: Database> ToString for Fixture<DB>
114where
115 for<'a> <DB as HasArguments<'a>>::Arguments: Default,
116{
117 fn to_string(&self) -> String {
118 let mut query = QueryBuilder::<DB>::new("");
119
120 for op in &self.ops {
121 match op {
122 FixtureOp::Insert {
123 table,
124 columns,
125 rows,
126 } => {
127 if columns.is_empty() || rows.is_empty() {
129 continue;
130 }
131
132 query.push(format_args!("INSERT INTO {} (", table));
133
134 let mut separated = query.separated(", ");
135
136 for column in columns {
137 separated.push(column);
138 }
139
140 query.push(")\n");
141
142 query.push_values(rows, |mut separated, row| {
143 for value in row {
144 separated.push(value);
145 }
146 });
147
148 query.push(";\n");
149 }
150 }
151 }
152
153 query.into_sql()
154 }
155}
156
157fn foreign_key_depth(
158 tables: &BTreeMap<TableName, Table>,
159 table: &Table,
160 depths: &mut HashMap<TableName, usize>,
161 visited_set: &mut IndexSet<TableName>,
162) -> Result<usize> {
163 if let Some(&depth) = depths.get(&table.name) {
164 return Ok(depth);
165 }
166
167 fixture_assert!(
169 visited_set.insert(table.name.clone()),
170 "foreign key cycle detected: {:?} -> {:?}",
171 visited_set,
172 table.name
173 );
174
175 let mut refdepth = 0;
176
177 for (colname, (refname, refcol)) in &table.foreign_keys {
178 let referenced = tables.get(refname).ok_or_else(|| {
179 FixtureError(format!(
180 "table {:?} in foreign key `{}.{} references {}.{}` does not exist",
181 refname, table.name, colname, refname, refcol
182 ))
183 })?;
184
185 refdepth = cmp::max(
186 refdepth,
187 foreign_key_depth(tables, referenced, depths, visited_set)?,
188 );
189 }
190
191 let depth = refdepth + 1;
192
193 depths.insert(table.name.clone(), depth);
194
195 Ok(depth)
196}
197
198#[test]
199#[cfg(feature = "postgres")]
200fn test_additive_fixture() -> Result<()> {
201 use crate::postgres::Postgres;
202
203 let mut snapshot = FixtureSnapshot {
204 tables: BTreeMap::new(),
205 db: PhantomData::<Postgres>,
206 };
207
208 snapshot.tables.insert(
209 "foo".into(),
210 Table {
211 name: "foo".into(),
212 columns: ["foo_id", "foo_a", "foo_b"]
213 .into_iter()
214 .map(Arc::<str>::from)
215 .collect(),
216 rows: vec![vec!["1".into(), "'asdf'".into(), "true".into()]],
217 foreign_keys: HashMap::new(),
218 },
219 );
220
221 snapshot.tables.insert(
224 "bar".into(),
225 Table {
226 name: "bar".into(),
227 columns: ["bar_id", "foo_id", "bar_a", "bar_b"]
228 .into_iter()
229 .map(Arc::<str>::from)
230 .collect(),
231 rows: vec![vec![
232 "1234".into(),
233 "1".into(),
234 "'2022-07-22 23:27:48.775113301+00:00'".into(),
235 "3.14".into(),
236 ]],
237 foreign_keys: [("foo_id".into(), ("foo".into(), "foo_id".into()))]
238 .into_iter()
239 .collect(),
240 },
241 );
242
243 snapshot.tables.insert(
245 "baz".into(),
246 Table {
247 name: "baz".into(),
248 columns: ["baz_id", "bar_id", "foo_id", "baz_a", "baz_b"]
249 .into_iter()
250 .map(Arc::<str>::from)
251 .collect(),
252 rows: vec![vec![
253 "5678".into(),
254 "1234".into(),
255 "1".into(),
256 "'2022-07-22 23:27:48.775113301+00:00'".into(),
257 "3.14".into(),
258 ]],
259 foreign_keys: [
260 ("foo_id".into(), ("foo".into(), "foo_id".into())),
261 ("bar_id".into(), ("bar".into(), "bar_id".into())),
262 ]
263 .into_iter()
264 .collect(),
265 },
266 );
267
268 let fixture = snapshot.additive_fixture()?;
269
270 assert_eq!(
271 fixture.to_string(),
272 "INSERT INTO foo (foo_id, foo_a, foo_b)\n\
273 VALUES (1, 'asdf', true);\n\
274 INSERT INTO bar (bar_id, foo_id, bar_a, bar_b)\n\
275 VALUES (1234, 1, '2022-07-22 23:27:48.775113301+00:00', 3.14);\n\
276 INSERT INTO baz (baz_id, bar_id, foo_id, baz_a, baz_b)\n\
277 VALUES (5678, 1234, 1, '2022-07-22 23:27:48.775113301+00:00', 3.14);\n"
278 );
279
280 Ok(())
281}