#![cfg(any(feature = "rusqlite", feature = "turso", feature = "libsql"))]
use crate::common::schema::sqlite::*;
use crate::common::seed::{
ConstantName, NameGenerator, Param, RelatedOptions, SeedContract, SimpleOptions, Statement,
};
use crate::sqlite::foreign_keys::{CompositeFkSchema, FkCascadeSchema};
use drizzle::sqlite::prelude::*;
use drizzle_seed::{Generator, GeneratorKind, RngCore, SeedConfig, SeedError, SeedValue};
#[SQLiteTable(NAME = "seed_simple")]
struct ContractSimple {
#[column(PRIMARY)]
id: i32,
name: String,
}
#[SQLiteTable(NAME = "seed_parent")]
struct ContractParent {
#[column(PRIMARY)]
id: i32,
name: String,
}
#[SQLiteTable(NAME = "seed_child")]
struct ContractChild {
#[column(PRIMARY)]
id: i32,
#[column(REFERENCES = ContractParent::id)]
parent_id: i32,
value: String,
}
#[SQLiteTable(NAME = "seed_profile")]
struct ContractProfile {
#[column(PRIMARY)]
id: i32,
email: String,
name: String,
description: String,
}
#[SQLiteTable(NAME = "seed_self_reference")]
struct ContractSelfReference {
#[column(PRIMARY)]
id: i32,
#[column(REFERENCES = ContractSelfReference::id)]
parent_id: Option<i32>,
}
#[derive(SQLiteSchema)]
struct ContractSimpleSchema {
simple: ContractSimple,
}
#[derive(SQLiteSchema)]
struct ContractRelatedSchema {
parent: ContractParent,
child: ContractChild,
}
#[derive(SQLiteSchema)]
struct ContractProfileSchema {
profile: ContractProfile,
}
#[derive(SQLiteSchema)]
struct ContractAllSchema {
simple: ContractSimple,
parent: ContractParent,
child: ContractChild,
profile: ContractProfile,
}
#[derive(SQLiteSchema)]
struct ContractSelfReferenceSchema {
nodes: ContractSelfReference,
}
struct SQLiteSeedContract;
impl SeedContract for SQLiteSeedContract {
fn simple(options: SimpleOptions) -> Vec<Statement> {
let schema = ContractSimpleSchema::new();
let mut config = SeedConfig::sqlite(&schema).seed(options.seed);
if let Some(count) = options.count {
config = config.count(&schema.simple, count);
}
if let Some(count) = options.default_count {
config = config.default_count(count);
}
if let Some(max_params) = options.max_params {
config = config.max_params(max_params);
}
config = match options.name_generator {
NameGenerator::Inferred => config,
NameGenerator::Email => config.kind(&ContractSimple::name, GeneratorKind::Email),
NameGenerator::Constant => config.generator(&ContractSimple::name, ConstantName),
NameGenerator::Column => config.generator(&ContractSimple::name, &ContractSimple::name),
};
config.generate().into_iter().map(normalize).collect()
}
fn related(options: RelatedOptions) -> Vec<Statement> {
let schema = ContractRelatedSchema::new();
let mut config = SeedConfig::sqlite(&schema).seed(options.seed);
if let Some(count) = options.parent_count {
config = config.count(&schema.parent, count);
}
if let Some(count) = options.child_count {
config = config.count(&schema.child, count);
}
if let Some(count) = options.children_per_parent {
config = config.relation(&schema.parent, &schema.child, count);
}
if options.skip_parent {
config = config.skip(&schema.parent);
}
if options.skip_child {
config = config.skip(&schema.child);
}
config.generate().into_iter().map(normalize).collect()
}
fn reset_related() -> Vec<String> {
let schema = ContractRelatedSchema::new();
SeedConfig::sqlite(&schema)
.reset_plan()
.unwrap()
.into_iter()
.map(|statement| statement.sql())
.collect()
}
fn reset_self_referential() -> Vec<String> {
let schema = ContractSelfReferenceSchema::new();
SeedConfig::sqlite(&schema)
.reset_plan()
.unwrap()
.into_iter()
.map(|statement| statement.sql())
.collect()
}
fn parameter_limit_error() -> SeedError {
let schema = ContractSimpleSchema::new();
SeedConfig::sqlite(&schema)
.count(&schema.simple, 1)
.max_params(1)
.try_generate()
.unwrap_err()
}
fn unsafe_reset_error() -> SeedError {
let schema = ContractRelatedSchema::new();
SeedConfig::sqlite(&schema)
.skip(&schema.child)
.reset_plan()
.unwrap_err()
}
fn all_tables(seed: u64, count: usize) -> Vec<Statement> {
let schema = ContractAllSchema::new();
SeedConfig::sqlite(&schema)
.seed(seed)
.default_count(count)
.generate()
.into_iter()
.map(normalize)
.collect()
}
fn profiles(seed: u64, count: usize) -> Vec<Statement> {
let schema = ContractProfileSchema::new();
SeedConfig::sqlite(&schema)
.seed(seed)
.count(&schema.profile, count)
.generate()
.into_iter()
.map(normalize)
.collect()
}
}
fn normalize(statement: drizzle_seed::SQLiteSeedStatement) -> Statement {
let (sql, params) = statement.build();
Statement {
sql,
params: params
.into_iter()
.map(|param| match param {
drizzle::sqlite::values::OwnedSQLiteValue::Integer(value) => {
Param::Integer(i128::from(value))
}
drizzle::sqlite::values::OwnedSQLiteValue::Text(value) => Param::Text(value),
other => Param::Other(format!("{other:?}")),
})
.collect(),
}
}
crate::common::seed::seed_contract_tests!(SQLiteSeedContract);
#[test]
fn config_count_extracts_table_name() {
let schema = FkCascadeSchema::new();
let stmts = SeedConfig::sqlite(&schema)
.count(&schema.fk_parent, 50)
.count(&schema.fk_cascade, 200)
.generate();
let parent_sql = stmts
.iter()
.map(|s| s.sql())
.find(|s| s.contains("INSERT INTO") && s.contains("fk_parent"))
.unwrap();
let values_section = &parent_sql[parent_sql.find("VALUES ").unwrap() + 7..];
let parent_rows = values_section.matches('(').count();
assert_eq!(parent_rows, 50, "fk_parent should have 50 rows");
}
#[test]
fn config_kind_override_via_column_ref() {
let schema = SimpleSchema::new();
let stmts = SeedConfig::sqlite(&schema)
.seed(42)
.count(&schema.simple, 5)
.kind(&Simple::name, GeneratorKind::Email)
.generate();
let (sql, params) = stmts[0].build();
assert!(sql.starts_with("INSERT INTO") && sql.contains("simple"));
for name_param in params.iter().skip(1).step_by(2) {
let drizzle::sqlite::values::OwnedSQLiteValue::Text(name_val) = name_param else {
panic!("expected TEXT for name column");
};
assert!(
name_val.contains('@') && name_val.contains('.'),
"with Email override, name column should produce emails, got: {name_val}"
);
}
}
#[test]
fn config_kind_override_via_schema_config() {
let schema = SimpleSchema::new();
let stmts = SeedConfig::sqlite(&schema)
.seed(42)
.count(&schema.simple, 5)
.kind(&Simple::name, GeneratorKind::Email)
.generate();
let (_sql, params) = stmts[0].build();
for name_param in params.iter().skip(1).step_by(2) {
let drizzle::sqlite::values::OwnedSQLiteValue::Text(name_val) = name_param else {
panic!("expected TEXT for name column");
};
assert!(
name_val.contains('@') && name_val.contains('.'),
"with Email override, name column should produce emails, got: {name_val}"
);
}
}
#[test]
fn config_custom_generator_via_column_ref() {
struct ConstGen;
impl Generator for ConstGen {
fn generate(&self, _rng: &mut dyn RngCore, _index: usize, _sql_type: &str) -> SeedValue {
SeedValue::Text("FIXED".to_string())
}
fn name(&self) -> &'static str {
"Const"
}
}
let schema = SimpleSchema::new();
let stmts = SeedConfig::sqlite(&schema)
.seed(1)
.count(&schema.simple, 4)
.generator(&Simple::name, ConstGen)
.generate();
let (_sql, params) = stmts[0].build();
let fixed_count = params
.iter()
.skip(1)
.step_by(2)
.filter(|v| {
matches!(
v,
drizzle::sqlite::values::OwnedSQLiteValue::Text(s) if s == "FIXED"
)
})
.count();
assert_eq!(fixed_count, 4, "custom generator should produce FIXED");
}
#[test]
fn config_generator_accepts_column_generator() {
let schema = SimpleSchema::new();
let stmts = SeedConfig::sqlite(&schema)
.seed(1)
.count(&schema.simple, 4)
.generator(&Simple::name, &Simple::name)
.generate();
let (_sql, params) = stmts[0].build();
let generated_text_count = params
.iter()
.skip(1)
.step_by(2)
.filter(|v| matches!(v, drizzle::sqlite::values::OwnedSQLiteValue::Text(_)))
.count();
assert_eq!(generated_text_count, 4);
}
#[test]
fn config_skip_parent_allows_explicit_unconstrained_generation() {
let schema = FkCascadeSchema::new();
let stmts = SeedConfig::sqlite(&schema)
.skip(&schema.fk_parent)
.count(&schema.fk_cascade, 2)
.generate();
let sqls: Vec<String> = stmts.iter().map(|s| s.sql()).collect();
assert!(
!sqls
.iter()
.any(|sql| sql.contains("INSERT INTO") && sql.contains("fk_parent"))
);
}
#[test]
fn config_skip_child_is_allowed() {
let schema = FkCascadeSchema::new();
let stmts = SeedConfig::sqlite(&schema)
.skip(&schema.fk_cascade)
.count(&schema.fk_parent, 3)
.generate();
let sqls: Vec<String> = stmts.iter().map(|s| s.sql()).collect();
assert!(
sqls.iter()
.any(|sql| sql.contains("INSERT INTO") && sql.contains("fk_parent"))
);
assert!(
!sqls
.iter()
.any(|sql| sql.contains("INSERT INTO") && sql.contains("fk_cascade"))
);
}
#[test]
fn config_relation_via_table_refs() {
let schema = FkCascadeSchema::new();
let _config = SeedConfig::sqlite(&schema)
.seed(1)
.count(&schema.fk_parent, 10)
.count(&schema.fk_cascade, 50)
.relation(&schema.fk_parent, &schema.fk_cascade, 5);
}
#[test]
fn config_relation_derives_child_count_when_unset() {
let schema = FkCascadeSchema::new();
let stmts = SeedConfig::sqlite(&schema)
.seed(7)
.count(&schema.fk_parent, 4)
.relation(&schema.fk_parent, &schema.fk_cascade, 3)
.generate();
let child_sql = stmts
.iter()
.map(|s| s.sql())
.find(|s| s.contains("INSERT INTO") && s.contains("fk_cascade"))
.unwrap();
let values_start = child_sql.find("VALUES ").unwrap() + 7;
let values_section = &child_sql[values_start..child_sql.len() - 1];
let child_rows = values_section.matches('(').count();
assert_eq!(
child_rows, 12,
"relation(3) and parent count 4 should derive 12 child rows"
);
}
#[test]
fn config_schema_config_includes_all_schema_tables() {
let schema = FullBlogSchema::new();
let stmts = SeedConfig::sqlite(&schema)
.seed(7)
.count(&schema.simple, 2)
.count(&schema.complex, 2)
.count(&schema.post, 2)
.count(&schema.category, 2)
.count(&schema.post_category, 2)
.generate();
let sqls: Vec<String> = stmts.iter().map(|s| s.sql()).collect();
for table in [
"simple",
"complex",
"posts",
"categories",
"post_categories",
] {
assert!(
sqls.iter()
.any(|sql| sql.contains("INSERT INTO") && sql.contains(table)),
"expected INSERT for table {table}"
);
}
}
#[test]
fn fk_child_count_derives_automatically_without_relation() {
let schema = FkCascadeSchema::new();
let stmts = SeedConfig::sqlite(&schema)
.seed(7)
.count(&schema.fk_parent, 4)
.generate();
let child_sql = stmts
.iter()
.map(|s| s.sql())
.find(|s| s.contains("INSERT INTO") && s.contains("fk_cascade"))
.unwrap();
let values_start = child_sql.find("VALUES ").unwrap() + 7;
let values_section = &child_sql[values_start..child_sql.len() - 1];
let child_rows = values_section.matches('(').count();
assert_eq!(
child_rows, 4,
"without relation, FK child count should default to 1:1 with parent count"
);
}
#[test]
fn config_relation_groups_fk_values_per_parent() {
let schema = FkCascadeSchema::new();
let stmts = SeedConfig::sqlite(&schema)
.seed(7)
.count(&schema.fk_parent, 3)
.relation(&schema.fk_parent, &schema.fk_cascade, 2)
.generate();
let (_sql, params) = stmts
.iter()
.map(|s| s.build())
.find(|(sql, _)| sql.contains("INSERT INTO") && sql.contains("fk_cascade"))
.unwrap();
let mut parent_ids = Vec::new();
for parent_param in params.iter().skip(1).step_by(3) {
let drizzle::sqlite::values::OwnedSQLiteValue::Integer(v) = parent_param else {
panic!("expected integer parent_id param");
};
parent_ids.push(*v);
}
assert_eq!(parent_ids, vec![1, 1, 2, 2, 3, 3]);
}
#[test]
fn config_relation_groups_composite_fk_values_per_parent() {
let schema = CompositeFkSchema::new();
let stmts = SeedConfig::sqlite(&schema)
.seed(11)
.count(&schema.composite_fk_parent, 3)
.relation(&schema.composite_fk_parent, &schema.composite_fk_child, 2)
.generate();
let (_sql, params) = stmts
.iter()
.map(|s| s.build())
.find(|(sql, _)| sql.contains("INSERT INTO") && sql.contains("composite_fk_child"))
.unwrap();
let mut fk_pairs = Vec::new();
for row_params in params.as_chunks::<4>().0 {
let drizzle::sqlite::values::OwnedSQLiteValue::Integer(parent_a) = row_params[1] else {
panic!("expected integer parent_a param");
};
let drizzle::sqlite::values::OwnedSQLiteValue::Integer(parent_b) = row_params[2] else {
panic!("expected integer parent_b param");
};
fk_pairs.push((parent_a, parent_b));
}
assert_eq!(
fk_pairs,
vec![(1, 1), (1, 1), (2, 2), (2, 2), (3, 3), (3, 3)]
);
}
#[test]
fn seeder_simple_table_pk_sequential_and_name_inferred() {
let schema = SimpleSchema::new();
let stmts = SeedConfig::sqlite(&schema)
.seed(42)
.count(&schema.simple, 5)
.generate();
assert_eq!(stmts.len(), 1);
let (sql, params) = stmts[0].build();
assert!(sql.contains("INSERT INTO") && sql.contains("simple"));
assert!(sql.contains("VALUES"));
let mut ids = Vec::new();
for id_param in params.iter().step_by(2) {
let drizzle::sqlite::values::OwnedSQLiteValue::Integer(id) = id_param else {
panic!("expected integer id");
};
ids.push(*id);
}
assert_eq!(ids, vec![1, 2, 3, 4, 5]);
for name_param in params.iter().skip(1).step_by(2) {
let drizzle::sqlite::values::OwnedSQLiteValue::Text(name) = name_param else {
panic!("expected text name");
};
assert!(!name.is_empty());
}
}
#[test]
fn seeder_deterministic_output() {
let schema = SimpleSchema::new();
let config = SeedConfig::sqlite(&schema)
.seed(123)
.count(&schema.simple, 20);
let sql_a: Vec<String> = config.generate().iter().map(|s| s.sql()).collect();
let sql_b: Vec<String> = config.generate().iter().map(|s| s.sql()).collect();
assert_eq!(sql_a, sql_b, "same seed must produce identical output");
}
#[test]
fn seeder_different_seeds_produce_different_output() {
let schema = SimpleSchema::new();
let stmts_a = SeedConfig::sqlite(&schema)
.seed(1)
.count(&schema.simple, 10)
.generate();
let stmts_b = SeedConfig::sqlite(&schema)
.seed(2)
.count(&schema.simple, 10)
.generate();
let params_a = stmts_a[0].build().1;
let params_b = stmts_b[0].build().1;
assert_ne!(
params_a, params_b,
"different seeds must produce different values"
);
}
#[test]
fn seeder_fk_parent_before_child() {
let schema = FkCascadeSchema::new();
let stmts = SeedConfig::sqlite(&schema)
.seed(42)
.count(&schema.fk_parent, 3)
.count(&schema.fk_cascade, 10)
.generate();
let sqls: Vec<String> = stmts.iter().map(|s| s.sql()).collect();
assert!(sqls.len() >= 2, "should have at least 2 statements");
assert!(
sqls[0].contains("INSERT INTO") && sqls[0].contains("fk_parent"),
"first INSERT should be parent table, got: {}",
&sqls[0][..60.min(sqls[0].len())]
);
assert!(
sqls[1].contains("INSERT INTO") && sqls[1].contains("fk_cascade"),
"second INSERT should be child table, got: {}",
&sqls[1][..60.min(sqls[1].len())]
);
}
#[test]
fn seeder_fk_values_are_valid_parent_pks() {
let schema = FkCascadeSchema::new();
let stmts = SeedConfig::sqlite(&schema)
.seed(42)
.count(&schema.fk_parent, 5)
.count(&schema.fk_cascade, 30)
.generate();
let valid_pks: Vec<i64> = (1..=5).collect();
let (_sql, params) = stmts
.iter()
.map(|s| s.build())
.find(|(sql, _)| sql.contains("INSERT INTO") && sql.contains("fk_cascade"))
.unwrap();
for parent_param in params.iter().skip(1).step_by(3) {
if let drizzle::sqlite::values::OwnedSQLiteValue::Integer(parent_id) = parent_param {
assert!(
valid_pks.contains(parent_id),
"FK parent_id={parent_id} not in valid parent PKs {valid_pks:?}"
);
}
}
}
#[test]
fn seeder_complex_table_column_heuristics() {
let schema = ComplexSchema::new();
let stmts = SeedConfig::sqlite(&schema)
.seed(42)
.count(&schema.complex, 10)
.generate();
assert!(!stmts.is_empty(), "should produce at least one statement");
let sql = stmts[0].sql();
assert!(
sql.contains("INSERT INTO") && sql.contains("complex"),
"should insert into complex table"
);
assert!(sql.contains("id"), "should have id column");
assert!(sql.contains("email"), "should have email column");
assert!(sql.contains("name"), "should have name column");
assert!(
sql.contains("description"),
"should have description column"
);
}
#[test]
fn seeder_zero_count_produces_nothing() {
let schema = SimpleSchema::new();
let stmts = SeedConfig::sqlite(&schema)
.seed(1)
.count(&schema.simple, 0)
.generate();
assert!(stmts.is_empty(), "count=0 should produce no statements");
}
#[test]
fn seeder_default_count_used_when_not_overridden() {
let schema = SimpleSchema::new();
let stmts = SeedConfig::sqlite(&schema)
.seed(1)
.default_count(7)
.generate();
let sql = stmts[0].sql();
let value_section = &sql[sql.find("VALUES ").unwrap() + 7..];
let row_count = value_section.matches('(').count();
assert_eq!(
row_count, 7,
"default_count=7 should produce 7 rows, got {row_count}"
);
}
#[test]
fn seeder_multi_table_blog_schema() {
let schema = FullBlogSchema::new();
let stmts = SeedConfig::sqlite(&schema)
.seed(42)
.count(&schema.simple, 3)
.count(&schema.complex, 5)
.count(&schema.post, 10)
.count(&schema.category, 4)
.count(&schema.post_category, 15)
.generate();
let sqls: Vec<String> = stmts.iter().map(|s| s.sql()).collect();
let table_names = [
"simple",
"complex",
"posts",
"categories",
"post_categories",
];
for name in &table_names {
assert!(
sqls.iter()
.any(|s| s.contains("INSERT INTO") && s.contains(name)),
"should have INSERT for table {name}"
);
}
let complex_idx = sqls
.iter()
.position(|s| s.contains("INSERT INTO") && s.contains("complex"))
.unwrap();
let posts_idx = sqls
.iter()
.position(|s| s.contains("INSERT INTO") && s.contains("posts"))
.unwrap();
assert!(
complex_idx < posts_idx,
"complex (parent) must be seeded before posts (child)"
);
}
#[test]
fn seeder_statements_return_sql_and_params() {
let schema = SimpleSchema::new();
let stmts = SeedConfig::sqlite(&schema)
.seed(42)
.count(&schema.simple, 3)
.generate();
assert_eq!(stmts.len(), 1);
let (sql, params) = stmts[0].build();
assert!(sql.starts_with("INSERT INTO"));
assert!(sql.contains('?'));
assert_eq!(params.len(), 6);
}
#[test]
fn seeder_respects_max_params() {
let schema = SimpleSchema::new();
let stmts = SeedConfig::sqlite(&schema)
.seed(42)
.count(&schema.simple, 10)
.max_params(4)
.generate();
assert_eq!(stmts.len(), 5);
for stmt in &stmts {
let (_sql, params) = stmt.build();
assert!(params.len() <= 4);
}
}
#[test]
fn seeder_email_column_produces_emails() {
let schema = ComplexSchema::new();
let stmts = SeedConfig::sqlite(&schema)
.seed(42)
.count(&schema.complex, 10)
.generate();
let (_sql, params) = stmts[0].build();
let mut email_count = 0;
for param in ¶ms {
if let drizzle::sqlite::values::OwnedSQLiteValue::Text(email_text) = param
&& email_text.contains('@')
&& email_text.contains('.')
{
email_count += 1;
}
}
assert!(
email_count > 0,
"should have produced at least one non-NULL email"
);
}
mod executed {
use drizzle::sqlite::prelude::*;
use drizzle::sqlite::values::OwnedSQLiteValue;
use drizzle_seed::SeedConfig;
#[derive(SQLiteEnum, Default, Debug, Clone, Copy, PartialEq, Eq)]
pub enum SeedRole {
#[default]
Guest,
Member,
Admin,
}
#[derive(SQLiteEnum, Default, Debug, Clone, Copy, PartialEq, Eq)]
pub enum SeedStatus {
Suspended = -1,
#[default]
Inactive = 3,
Active,
}
#[SQLiteTable(NAME = "seed_typed", STRICT)]
pub struct SeedTyped {
#[column(PRIMARY)]
pub id: i64,
#[column(UNIQUE)]
pub username: String,
#[column(ENUM)]
pub role: SeedRole,
#[column(integer, ENUM)]
pub status: SeedStatus,
pub position: i64,
pub email_count: i64,
pub hotel_id: i64,
pub is_active: bool,
}
#[derive(SQLiteSchema)]
pub struct SeedTypedSchema {
pub typed: SeedTyped,
}
#[SQLiteTable(NAME = "seed_member")]
pub struct SeedMember {
#[column(PRIMARY)]
pub id: i64,
pub name: String,
}
#[SQLiteTable(NAME = "seed_group")]
pub struct SeedGroup {
#[column(PRIMARY)]
pub id: i64,
pub title: String,
}
#[SQLiteTable(NAME = "seed_membership")]
pub struct SeedMembership {
#[column(PRIMARY, REFERENCES = SeedMember::id)]
pub member_id: i64,
#[column(PRIMARY, REFERENCES = SeedGroup::id)]
pub group_id: i64,
}
#[derive(SQLiteSchema)]
pub struct SeedMembershipSchema {
pub member: SeedMember,
pub group: SeedGroup,
pub membership: SeedMembership,
}
#[drizzle::test]
fn join_table_rows_never_repeat_the_composite_key(db: &mut TestDb<SeedMembershipSchema>) {
let SeedMembershipSchema {
member,
group,
membership,
} = schema;
for statement in SeedConfig::sqlite(&schema)
.count(&member, 10)
.count(&group, 10)
.relation(&member, &membership, 2)
.relation(&group, &membership, 5)
.generate()
{
db.execute(statement);
}
let rows: Vec<SelectSeedMembership> = db.select(()).from(membership).all();
let mut pairs: Vec<(i64, i64)> = rows
.iter()
.map(|row| (row.member_id, row.group_id))
.collect();
assert!(!pairs.is_empty() && pairs.len() < 50);
pairs.sort_unstable();
pairs.dedup();
assert_eq!(pairs.len(), rows.len());
}
#[drizzle::test]
fn seeded_rows_insert_and_decode(db: &mut TestDb<SeedTypedSchema>) {
let SeedTypedSchema { typed } = schema;
for statement in SeedConfig::sqlite(&schema)
.seed(3)
.count(&typed, 200)
.generate()
{
db.execute(statement);
}
let rows: Vec<SelectSeedTyped> = db.select(()).from(typed).all();
assert_eq!(rows.len(), 200);
let mut usernames: Vec<&str> = rows.iter().map(|row| row.username.as_str()).collect();
usernames.sort_unstable();
usernames.dedup();
assert_eq!(usernames.len(), 200, "UNIQUE username values repeat");
for role in [SeedRole::Guest, SeedRole::Member, SeedRole::Admin] {
assert!(
rows.iter().any(|row| row.role == role),
"{role:?} never seeded"
);
}
for status in [
SeedStatus::Suspended,
SeedStatus::Inactive,
SeedStatus::Active,
] {
assert!(
rows.iter().any(|row| row.status == status),
"{status:?} never seeded"
);
}
}
fn runtime_schema() -> drizzle_seed::schema::Schema {
use drizzle_seed::schema::{Column, Schema, Table};
Schema::sqlite()
.table(
Table::new("seed_typed")
.column(Column::new("id", "INTEGER").primary_key())
.column(Column::new("username", "TEXT").not_null().unique())
.column(
Column::new("role", "TEXT")
.not_null()
.enum_values(["Guest", "Member", "Admin"]),
)
.column(Column::new("status", "INTEGER").not_null())
.column(Column::new("position", "INTEGER").not_null())
.column(Column::new("email_count", "INTEGER").not_null())
.column(Column::new("hotel_id", "INTEGER").not_null())
.column(Column::new("is_active", "INTEGER").not_null()),
)
.table(
Table::new("seed_member")
.column(Column::new("id", "INTEGER").primary_key())
.column(Column::new("name", "TEXT").not_null()),
)
.table(
Table::new("seed_group")
.column(Column::new("id", "INTEGER").primary_key())
.column(Column::new("title", "TEXT").not_null()),
)
.table(
Table::new("seed_membership")
.column(
Column::new("member_id", "INTEGER")
.not_null()
.references("seed_member", "id"),
)
.column(
Column::new("group_id", "INTEGER")
.not_null()
.references("seed_group", "id"),
)
.primary_key(["member_id", "group_id"]),
)
}
#[derive(SQLiteSchema)]
pub struct SeedRuntimeMirror {
pub typed: SeedTyped,
pub member: SeedMember,
pub group: SeedGroup,
pub membership: SeedMembership,
}
#[test]
fn runtime_schema_and_names_match_the_macro_schema() {
use drizzle_seed::generators;
let schema = SeedRuntimeMirror::new();
let typed: Vec<(String, Vec<OwnedSQLiteValue>)> = SeedConfig::sqlite(&schema)
.seed(9)
.count(&schema.typed, 30)
.count(&schema.member, 6)
.count(&schema.group, 4)
.relation(&schema.member, &schema.membership, 2)
.generator(&schema.typed.status, generators::one_of([-1, 3, 4]))
.generate()
.iter()
.map(|statement| statement.build())
.collect();
let runtime = runtime_schema();
let named: Vec<(String, Vec<OwnedSQLiteValue>)> = SeedConfig::sqlite(&runtime)
.seed(9)
.count_by_name("seed_typed", 30)
.count_by_name("seed_member", 6)
.count_by_name("seed_group", 4)
.relation_by_name("seed_member", "seed_membership", 2)
.generator_by_name("seed_typed", "status", generators::one_of([-1, 3, 4]))
.generate()
.iter()
.map(|statement| statement.build())
.collect();
assert!(!typed.is_empty());
assert_eq!(typed, named);
}
#[drizzle::test]
fn runtime_schema_rows_insert_and_decode(db: &mut TestDb<SeedRuntimeMirror>) {
let runtime = runtime_schema();
for statement in SeedConfig::sqlite(&runtime)
.seed(4)
.count_by_name("seed_typed", 50)
.skip_by_name("seed_membership")
.kind_by_name("seed_group", "title", drizzle_seed::GeneratorKind::Title)
.generator_by_name(
"seed_typed",
"status",
drizzle_seed::generators::one_of([-1, 3, 4]),
)
.generate()
{
db.execute(statement);
}
let rows: Vec<SelectSeedTyped> = db.select(()).from(schema.typed).all();
assert_eq!(rows.len(), 50);
for role in [SeedRole::Guest, SeedRole::Member, SeedRole::Admin] {
assert!(
rows.iter().any(|row| row.role == role),
"{role:?} never seeded"
);
}
let groups: Vec<SelectSeedGroup> = db.select(()).from(schema.group).all();
assert_eq!(groups.len(), 10);
assert!(
groups
.iter()
.all(|group| group.title.chars().next().is_some_and(char::is_uppercase))
);
let memberships: Vec<SelectSeedMembership> = db.select(()).from(schema.membership).all();
assert!(memberships.is_empty(), "skipped table was seeded");
}
#[test]
fn unknown_names_are_reported_with_the_known_ones() {
use drizzle_seed::SeedError;
let runtime = runtime_schema();
let error = SeedConfig::sqlite(&runtime)
.count_by_name("seed_typo", 3)
.try_generate()
.unwrap_err();
assert!(
matches!(&error, SeedError::UnknownTable { table, known }
if table == "seed_typo" && known.contains(&"seed_member".to_string())),
"{error:?}"
);
assert!(error.to_string().contains("seed_member"), "{error}");
let error = SeedConfig::sqlite(&runtime)
.generator_by_name(
"seed_member",
"nmae",
drizzle_seed::generators::constant("x"),
)
.try_generate_rows()
.unwrap_err();
assert!(
matches!(&error, SeedError::UnknownColumn { column, known, .. }
if column == "nmae" && known == &["id", "name"]),
"{error:?}"
);
let error = SeedConfig::sqlite(&runtime)
.relation_by_name("seed_group", "seed_member", 2)
.reset_plan()
.unwrap_err();
assert!(
matches!(&error, SeedError::NotRelated { parent, child }
if parent == "seed_group" && child == "seed_member"),
"{error:?}"
);
let schema = SeedRuntimeMirror::new();
assert!(
SeedConfig::sqlite(&schema)
.count_by_name("seed_member", 2)
.try_generate()
.is_ok()
);
}
#[test]
fn snapshot_schema_matches_the_macro_schema() {
use drizzle::migrations::Schema as _;
use drizzle_seed::schema::Schema;
use drizzle_seed::{Sqlite, generators};
fn configure<S: drizzle::core::SQLSchemaImpl>(schema: &S) -> SeedConfig<'_, Sqlite, S> {
SeedConfig::sqlite(schema)
.seed(21)
.count_by_name("seed_typed", 25)
.count_by_name("seed_member", 5)
.relation_by_name("seed_member", "seed_membership", 3)
.generator_by_name("seed_typed", "role", generators::one_of(["Guest", "Admin"]))
.generator_by_name("seed_typed", "status", generators::one_of([-1, 3, 4]))
}
let macro_schema = SeedRuntimeMirror::new();
let from_snapshot = Schema::from_snapshot(¯o_schema.to_snapshot()).unwrap();
let build = |statements: Vec<drizzle_seed::SQLiteSeedStatement>| {
statements
.iter()
.map(|statement| statement.build())
.collect::<Vec<_>>()
};
let expected = build(configure(¯o_schema).generate());
assert!(!expected.is_empty());
assert_eq!(build(configure(&from_snapshot).generate()), expected);
}
const LIVE_TABLES: &str = r"
CREATE TABLE live_authors (
id INTEGER PRIMARY KEY AUTOINCREMENT,
handle TEXT NOT NULL UNIQUE,
status TEXT NOT NULL,
bio TEXT,
score REAL NOT NULL DEFAULT 0,
avatar BLOB,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE live_posts (
id INTEGER PRIMARY KEY,
author_id INTEGER NOT NULL REFERENCES live_authors(id),
slug TEXT NOT NULL,
title TEXT NOT NULL,
rating REAL,
body TEXT
);
CREATE UNIQUE INDEX live_posts_author_slug ON live_posts (author_id, slug);
CREATE TABLE live_tags (
post_id INTEGER NOT NULL REFERENCES live_posts(id),
tag TEXT NOT NULL,
PRIMARY KEY (post_id, tag)
);
";
#[derive(SQLiteFromRow)]
struct Text(String);
fn fingerprint_sql(table: &str, columns: &[&str]) -> String {
let row = columns
.iter()
.map(|column| format!("quote({column})"))
.collect::<Vec<_>>()
.join(" || ',' || ");
format!("SELECT group_concat(r, '|') FROM (SELECT {row} AS r FROM {table} ORDER BY 1)")
}
#[drizzle::test]
fn introspected_database_seeds_with_bound_and_inline_values(db: &mut TestDb<SeedTypedSchema>) {
use drizzle_seed::schema::Schema;
for statement in LIVE_TABLES.split(';').filter(|s| !s.trim().is_empty()) {
db.execute(SQL::raw(statement));
}
let generated = !cfg!(feature = "turso");
if generated {
db.execute(SQL::raw(
"ALTER TABLE live_authors ADD COLUMN handle_length INTEGER \
GENERATED ALWAYS AS (length(handle)) VIRTUAL",
));
}
let snapshot = result!(db.introspect()).expect("introspect");
let schema = Schema::from_snapshot(&snapshot)
.unwrap()
.retain(|table| table.name().starts_with("live_"));
assert_eq!(schema.tables().len(), 3);
let config = SeedConfig::sqlite(&schema)
.seed(5)
.count_by_name("live_authors", 12)
.relation_by_name("live_authors", "live_posts", 3)
.relation_by_name("live_posts", "live_tags", 2);
let tables: [(&str, &[&str]); 3] = [
(
"live_authors",
if generated {
&[
"id",
"handle",
"status",
"bio",
"score",
"avatar",
"handle_length",
]
} else {
&["id", "handle", "status", "bio", "score", "avatar"]
},
),
(
"live_posts",
&["id", "author_id", "slug", "title", "rating", "body"],
),
("live_tags", &["post_id", "tag"]),
];
for statement in config.generate() {
db.execute(statement);
}
let mut bound = Vec::new();
for (table, columns) in tables {
let Text(rows) = db.get(SQL::raw(fingerprint_sql(table, columns)));
bound.push(rows);
}
for statement in config.reset_plan().unwrap() {
db.execute(statement);
}
let script = config.try_generate_script().unwrap();
for statement in config.generate() {
let inline = statement.inline_sql().unwrap();
assert!(script.contains(&inline));
db.execute(SQL::raw(inline));
}
let mut inline = Vec::new();
for (table, columns) in tables {
let Text(rows) = db.get(SQL::raw(fingerprint_sql(table, columns)));
inline.push(rows);
}
assert_eq!(inline, bound);
}
#[SQLiteTable(NAME = "seed_defaults")]
pub struct SeedDefaults {
#[column(PRIMARY)]
pub id: i64,
#[column(DEFAULT = 7)]
pub score: i64,
pub name: String,
}
#[derive(SQLiteSchema)]
pub struct SeedDefaultsSchema {
pub defaults: SeedDefaults,
}
#[drizzle::test]
fn columns_with_defaults_take_them(db: &mut TestDb<SeedDefaultsSchema>) {
let SeedDefaultsSchema { defaults } = schema;
for statement in SeedConfig::sqlite(&schema).count(&defaults, 5).generate() {
assert!(!statement.sql().contains("DEFAULT"), "{}", statement.sql());
db.execute(statement);
}
let rows: Vec<SelectSeedDefaults> = db.select(()).from(defaults).all();
assert_eq!(rows.len(), 5);
assert!(rows.iter().all(|row| row.score == 7));
}
}
#[test]
fn rows_output_matches_generated_statements() {
let schema = ContractRelatedSchema::new();
let config = SeedConfig::sqlite(&schema)
.seed(5)
.count(&schema.parent, 3)
.relation(&schema.parent, &schema.child, 2);
let tables = config.try_generate_rows().unwrap();
let names: Vec<&str> = tables.iter().map(|rows| rows.table).collect();
assert_eq!(names, ["seed_parent", "seed_child"]);
assert_eq!(tables[0].columns, ["id", "name"]);
assert_eq!(tables[1].rows.len(), 6);
let params: Vec<OwnedSQLiteValue> = config
.generate()
.into_iter()
.flat_map(|statement| statement.build().1)
.collect();
let values: Vec<&SeedValue> = tables
.iter()
.flat_map(|table| table.rows.iter().flatten())
.collect();
assert_eq!(params.len(), values.len());
for (param, value) in params.iter().zip(values) {
match (param, value) {
(OwnedSQLiteValue::Integer(a), SeedValue::Integer(b)) => assert_eq!(a, b),
(OwnedSQLiteValue::Text(a), SeedValue::Text(b)) => assert_eq!(a, b),
other => panic!("unexpected pair {other:?}"),
}
}
}
#[test]
fn generators_module_plugs_into_columns() {
use drizzle_seed::generators::{self, GeneratorExt};
let schema = ContractSimpleSchema::new();
let statements = SeedConfig::sqlite(&schema)
.count(&schema.simple, 4)
.generator(
&schema.simple.name,
generators::one_of(["ada", "grace"]).nullable(0.0),
)
.generate();
let (_, params) = statements[0].build();
let names: Vec<&OwnedSQLiteValue> = params.iter().skip(1).step_by(2).collect();
assert_eq!(names.len(), 4);
assert!(names.iter().all(|name| matches!(
name,
OwnedSQLiteValue::Text(text) if text == "ada" || text == "grace"
)));
}