1use crate::SeedError;
4use crate::generator::{Generator, GeneratorKind};
5use crate::identity::{ColumnId, TableId};
6use drizzle_core::{Relation, SQLSchemaImpl, SQLTableInfo, SchemaHasTable, TableRef};
7#[cfg(any(feature = "sqlite", feature = "postgres", feature = "mysql"))]
8use drizzle_core::{SQLColumn, SQLColumnInfo};
9use std::collections::{HashMap, HashSet};
10use std::marker::PhantomData;
11use std::sync::Arc;
12
13#[cfg(feature = "sqlite")]
14use crate::Sqlite;
15#[cfg(feature = "sqlite")]
16use drizzle_sqlite::traits::SQLiteColumn;
17#[cfg(feature = "sqlite")]
18use drizzle_sqlite::values::SQLiteValue;
19
20#[cfg(feature = "postgres")]
21use crate::Postgres;
22#[cfg(feature = "postgres")]
23use drizzle_postgres::traits::PostgresColumn;
24#[cfg(feature = "postgres")]
25use drizzle_postgres::values::PostgresValue;
26
27#[cfg(feature = "mysql")]
28use crate::MySql;
29#[cfg(feature = "mysql")]
30use drizzle_mysql::traits::MySQLColumn;
31#[cfg(feature = "mysql")]
32use drizzle_mysql::values::MySQLValue;
33
34pub struct SeedConfig<'a, D, S> {
44 pub(crate) schema: &'a S,
46 pub(crate) skipped_tables: HashSet<TableId>,
48 pub(crate) seed: u64,
50 pub(crate) default_count: usize,
52 pub(crate) table_counts: HashMap<TableId, usize>,
54 pub(crate) column_generators: HashMap<ColumnId, Arc<dyn Generator>>,
56 pub(crate) column_kinds: HashMap<ColumnId, GeneratorKind>,
58 pub(crate) relation_counts: HashMap<(TableId, TableId), usize>,
60 pub(crate) max_params_per_batch: Option<usize>,
62 pub(crate) name_errors: Vec<SeedError>,
65 _dialect: PhantomData<D>,
66 _schema: PhantomData<&'a S>,
67}
68
69impl<'a, D, S> SeedConfig<'a, D, S> {
70 fn with_defaults(schema: &'a S) -> Self {
71 Self {
72 schema,
73 skipped_tables: HashSet::new(),
74 seed: 0,
75 default_count: 10,
76 table_counts: HashMap::new(),
77 column_generators: HashMap::new(),
78 column_kinds: HashMap::new(),
79 relation_counts: HashMap::new(),
80 max_params_per_batch: None,
81 name_errors: Vec::new(),
82 _dialect: PhantomData,
83 _schema: PhantomData,
84 }
85 }
86
87 #[must_use]
89 pub const fn seed(mut self, seed: u64) -> Self {
90 self.seed = seed;
91 self
92 }
93
94 #[must_use]
97 pub const fn default_count(mut self, count: usize) -> Self {
98 self.default_count = count;
99 self
100 }
101
102 #[must_use]
111 pub fn max_params(mut self, limit: usize) -> Self {
112 assert!(limit > 0, "max_params must be > 0");
113 self.max_params_per_batch = Some(limit);
114 self
115 }
116
117 pub(crate) fn count_for(&self, table: TableId) -> usize {
118 self.table_counts
119 .get(&table)
120 .copied()
121 .unwrap_or(self.default_count)
122 }
123}
124
125impl<D, S> SeedConfig<'_, D, S>
126where
127 S: SQLSchemaImpl,
128{
129 pub(crate) fn active_tables(&self) -> Vec<&'static TableRef> {
130 self.schema
131 .table_refs()
132 .iter()
133 .copied()
134 .filter(|t| !self.skipped_tables.contains(&TableId::from_ref(t)))
135 .collect()
136 }
137
138 #[must_use]
143 pub fn count<T>(mut self, table: &T, count: usize) -> Self
144 where
145 T: SQLTableInfo,
146 S: SchemaHasTable<T>,
147 {
148 self.table_counts.insert(TableId::from_info(table), count);
149 self
150 }
151
152 #[must_use]
159 pub fn relation<P, C>(mut self, parent: &P, child: &C, count: usize) -> Self
160 where
161 P: SQLTableInfo,
162 C: SQLTableInfo + Relation<P>,
163 S: SchemaHasTable<P> + SchemaHasTable<C>,
164 {
165 self.relation_counts.insert(
166 (TableId::from_info(parent), TableId::from_info(child)),
167 count,
168 );
169 self
170 }
171}
172
173impl<D, S> SeedConfig<'_, D, S> {
174 #[must_use]
180 pub fn skip<T>(mut self, table: &T) -> Self
181 where
182 T: SQLTableInfo,
183 S: SchemaHasTable<T>,
184 {
185 self.skipped_tables.insert(TableId::from_info(table));
186 self
187 }
188}
189
190impl<D, S> SeedConfig<'_, D, S>
199where
200 S: SQLSchemaImpl,
201{
202 #[must_use]
205 pub fn count_by_name(mut self, table: &str, count: usize) -> Self {
206 if let Some(table) = self.resolve_table(table) {
207 self.table_counts.insert(TableId::from_ref(table), count);
208 }
209 self
210 }
211
212 #[must_use]
216 pub fn relation_by_name(mut self, parent: &str, child: &str, count: usize) -> Self {
217 let (Some(parent_ref), Some(child_ref)) =
218 (self.resolve_table(parent), self.resolve_table(child))
219 else {
220 return self;
221 };
222 let parent_id = TableId::from_ref(parent_ref);
223 let related = child_ref
224 .foreign_keys
225 .iter()
226 .any(|fk| TableId::foreign_target(child_ref, fk) == parent_id);
227 if related {
228 self.relation_counts
229 .insert((parent_id, TableId::from_ref(child_ref)), count);
230 } else {
231 self.name_errors.push(SeedError::NotRelated {
232 parent: parent_id.to_string(),
233 child: TableId::from_ref(child_ref).to_string(),
234 });
235 }
236 self
237 }
238
239 #[must_use]
241 pub fn skip_by_name(mut self, table: &str) -> Self {
242 if let Some(table) = self.resolve_table(table) {
243 self.skipped_tables.insert(TableId::from_ref(table));
244 }
245 self
246 }
247
248 #[must_use]
251 pub fn kind_by_name(mut self, table: &str, column: &str, kind: GeneratorKind) -> Self {
252 if let Some(column) = self.resolve_column(table, column) {
253 self.column_kinds.insert(column, kind);
254 }
255 self
256 }
257
258 #[must_use]
260 pub fn generator_by_name(
261 mut self,
262 table: &str,
263 column: &str,
264 generator: impl Generator + 'static,
265 ) -> Self {
266 if let Some(column) = self.resolve_column(table, column) {
267 self.column_generators.insert(column, Arc::new(generator));
268 }
269 self
270 }
271
272 fn resolve_table(&mut self, name: &str) -> Option<&'static TableRef> {
275 let tables = self.schema.table_refs();
276 let exact: Vec<&'static TableRef> = tables
277 .iter()
278 .copied()
279 .filter(|table| table.name == name)
280 .collect();
281 let candidates = if exact.is_empty() {
282 name.split_once('.')
283 .map_or_else(Vec::new, |(schema, table_name)| {
284 tables
285 .iter()
286 .copied()
287 .filter(|table| {
288 table.name == table_name
289 && (table.schema == Some(schema)
290 || (schema == "public" && table.schema.is_none()))
291 })
292 .collect()
293 })
294 } else {
295 exact
296 };
297 match candidates.as_slice() {
298 [table] => Some(table),
299 [] => {
300 self.name_errors.push(SeedError::UnknownTable {
301 table: name.to_string(),
302 known: tables
303 .iter()
304 .map(|table| TableId::from_ref(table).to_string())
305 .collect(),
306 });
307 None
308 }
309 _ => {
310 self.name_errors.push(SeedError::AmbiguousTable {
311 table: name.to_string(),
312 candidates: candidates
313 .iter()
314 .map(|table| TableId::from_ref(table).to_string())
315 .collect(),
316 });
317 None
318 }
319 }
320 }
321
322 fn resolve_column(&mut self, table: &str, column: &str) -> Option<ColumnId> {
323 let table = self.resolve_table(table)?;
324 let table_id = TableId::from_ref(table);
325 if let Some(found) = table
326 .columns
327 .iter()
328 .find(|candidate| candidate.name == column)
329 {
330 return Some(ColumnId::new(table_id, found.name));
331 }
332 self.name_errors.push(SeedError::UnknownColumn {
333 table: table_id.to_string(),
334 column: column.to_string(),
335 known: table
336 .columns
337 .iter()
338 .map(|column| column.name.to_string())
339 .collect(),
340 });
341 None
342 }
343
344 pub(crate) fn check_names(&self) -> Result<(), SeedError> {
346 self.name_errors.first().cloned().map_or(Ok(()), Err)
347 }
348}
349
350macro_rules! dialect_seed_config {
351 (
352 feature = $feature:literal,
353 dialect = $dialect:literal,
354 marker = $marker:ty,
355 constructor = $constructor:ident,
356 column = $column_trait:path,
357 value = $value:ty,
358 seed_statement = $seed_statement:path,
359 reset_statement = $reset_statement:path,
360 generate = $generate:ident,
361 reset = $reset:ident,
362 reset_note = $reset_note:literal
363 ) => {
364 #[cfg(feature = $feature)]
365 impl<'a> SeedConfig<'a, $marker, ()> {
366 #[doc = concat!("Creates a ", $dialect, " seed config for `schema` (usually a `#[derive(...Schema)]` struct).")]
367 pub fn $constructor<Schema>(schema: &'a Schema) -> SeedConfig<'a, $marker, Schema>
368 where
369 Schema: SQLSchemaImpl,
370 {
371 SeedConfig::<'a, $marker, Schema>::with_defaults(schema)
372 }
373 }
374
375 #[cfg(feature = $feature)]
376 impl<S> SeedConfig<'_, $marker, S>
377 where
378 S: SQLSchemaImpl,
379 {
380 #[must_use]
383 pub fn kind<C>(mut self, column: &C, kind: GeneratorKind) -> Self
384 where
385 C: SQLColumnInfo + $column_trait,
386 S: SchemaHasTable<<C as SQLColumn<'static, $value>>::Table>,
387 {
388 self.column_kinds.insert(ColumnId::from_info(column), kind);
389 self
390 }
391
392 #[must_use]
400 pub fn generator<C>(mut self, column: &C, generator: impl Generator + 'static) -> Self
401 where
402 C: SQLColumnInfo + $column_trait,
403 S: SchemaHasTable<<C as SQLColumn<'static, $value>>::Table>,
404 {
405 self.column_generators
406 .insert(ColumnId::from_info(column), Arc::new(generator));
407 self
408 }
409
410 #[must_use]
423 pub fn generate(&self) -> Vec<$seed_statement> {
424 self.try_generate()
425 .unwrap_or_else(|error| panic!("invalid seed plan: {error}"))
426 }
427
428 pub fn try_generate(&self) -> Result<Vec<$seed_statement>, crate::SeedError> {
438 crate::Seeder::new(self).$generate()
439 }
440
441 pub fn try_generate_script(&self) -> Result<String, crate::SeedError> {
453 let mut script = String::new();
454 for statement in self.try_generate()? {
455 script.push_str(&statement.inline_sql()?);
456 script.push_str(";\n");
457 }
458 Ok(script)
459 }
460
461 pub fn try_generate_rows(&self) -> Result<Vec<crate::SeedRows>, crate::SeedError> {
474 crate::Seeder::new(self).generate_rows()
475 }
476
477 #[doc = $reset_note]
483 pub fn reset_plan(&self) -> Result<Vec<$reset_statement>, crate::SeedError> {
490 crate::Seeder::new(self).$reset()
491 }
492 }
493 };
494}
495
496dialect_seed_config!(
497 feature = "sqlite",
498 dialect = "SQLite",
499 marker = Sqlite,
500 constructor = sqlite,
501 column = SQLiteColumn<'static>,
502 value = SQLiteValue<'static>,
503 seed_statement = crate::SQLiteSeedStatement,
504 reset_statement = crate::SQLiteResetStatement,
505 generate = generate_sqlite,
506 reset = reset_sqlite,
507 reset_note = "Wrap execution in a transaction when the connection API supports one."
508);
509
510dialect_seed_config!(
511 feature = "postgres",
512 dialect = "PostgreSQL",
513 marker = Postgres,
514 constructor = postgres,
515 column = PostgresColumn<'static>,
516 value = PostgresValue<'static>,
517 seed_statement = crate::PostgresSeedStatement,
518 reset_statement = crate::PostgresResetStatement,
519 generate = generate_postgres,
520 reset = reset_postgres,
521 reset_note = "Wrap execution in a transaction when the connection API supports one."
522);
523
524dialect_seed_config!(
525 feature = "mysql",
526 dialect = "MySQL",
527 marker = MySql,
528 constructor = mysql,
529 column = MySQLColumn<'static>,
530 value = MySQLValue<'static>,
531 seed_statement = crate::MySQLSeedStatement,
532 reset_statement = crate::MySQLResetStatement,
533 generate = generate_mysql,
534 reset = reset_mysql,
535 reset_note = "The plan appends `ALTER TABLE ... AUTO_INCREMENT = 1` for auto-increment tables. Those statements implicitly commit in MySQL, so callers must not assume the whole reset is transactional."
536);