From cee908406eea1301986a967659facfacb3238b54 Mon Sep 17 00:00:00 2001 From: mikkeldamsgaard Date: Tue, 24 Feb 2026 22:10:24 +0100 Subject: [PATCH 1/4] feat: add integration tests with docker-compose (#7) - wait-for, render, fetch, exec, seed PostgreSQL/MySQL with cross-table refs, idempotency, reset, create database/schema, create-if-missing tests --- .github/workflows/integration.yml | 61 ++ CHANGELOG.md | 5 + src/seed/db.rs | 19 +- src/seed/executor.rs | 3 +- tests/README.md | 52 +- tests/docker-compose.yml | 39 ++ tests/fixtures/seed-mysql.yaml | 37 ++ tests/fixtures/seed-postgres.yaml | 36 ++ tests/fixtures/template.conf.tmpl | 6 + tests/integration_test.rs | 952 ++++++++++++++++++++++++++++++ 10 files changed, 1194 insertions(+), 16 deletions(-) create mode 100644 .github/workflows/integration.yml create mode 100644 tests/docker-compose.yml create mode 100644 tests/fixtures/seed-mysql.yaml create mode 100644 tests/fixtures/seed-postgres.yaml create mode 100644 tests/fixtures/template.conf.tmpl create mode 100644 tests/integration_test.rs diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml new file mode 100644 index 0000000..80c39bd --- /dev/null +++ b/.github/workflows/integration.yml @@ -0,0 +1,61 @@ +name: Integration Tests +on: + workflow_dispatch: + push: + branches: [main] + pull_request: + branches: [main] +permissions: + contents: read +jobs: + integration: + runs-on: ubuntu-latest + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_USER: initium + POSTGRES_PASSWORD: initium + POSTGRES_DB: initium_test + ports: + - 15432:5432 + options: >- + --health-cmd "pg_isready -U initium" + --health-interval 2s + --health-timeout 5s + --health-retries 10 + mysql: + image: mysql:8.0 + env: + MYSQL_ROOT_PASSWORD: rootpass + MYSQL_USER: initium + MYSQL_PASSWORD: initium + MYSQL_DATABASE: initium_test + ports: + - 13306:3306 + options: >- + --health-cmd "mysqladmin ping -h localhost -u root -prootpass" + --health-interval 2s + --health-timeout 5s + --health-retries 15 + http-server: + image: nginx:1-alpine + ports: + - 18080:80 + options: >- + --health-cmd "wget --spider -q http://localhost:80/" + --health-interval 2s + --health-timeout 5s + --health-retries 5 + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: Install psql and mysql clients + run: | + sudo apt-get update -qq + sudo apt-get install -y -qq postgresql-client default-mysql-client + - name: Run integration tests + env: + INTEGRATION: "1" + run: cargo test --test integration_test -- --test-threads=1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f42f4c..f2eb6f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- Integration tests with docker-compose for end-to-end testing against real Postgres 16, MySQL 8.0, and nginx services (`tests/integration_test.rs`): wait-for TCP/HTTP/timeout/multiple targets, render template, fetch HTTP, exec command, seed PostgreSQL and MySQL with cross-table reference verification, create database/schema, idempotency, and reset mode +- Additional create-if-missing integration tests: 2 PostgreSQL and 2 MySQL tests using known non-existing database names (`initium_noexist_alpha`, `initium_noexist_beta`) to verify database creation, existence checks, and idempotent re-runs +- `tests/docker-compose.yml` with Postgres, MySQL, and HTTP health-check server definitions +- `tests/fixtures/` with seed spec files and template for integration tests +- Separate GitHub Actions workflow (`.github/workflows/integration.yml`) for integration tests with service containers - Helm chart unit tests using helm-unittest plugin (`charts/initium/tests/deployment_test.yaml`) covering deployment rendering, securityContext enforcement, disabled sampleDeployment, multiple initContainers, extraVolumes/extraVolumeMounts, image configuration, workdir mount, and labels - `helm unittest` step added to CI helm-lint job with automatic plugin installation - Duration unit support for all time parameters (`--timeout`, `--initial-delay`, `--max-delay`, seed phase `timeout`, seed wait-for `timeout`): accepts `ms`, `s`, `m`, `h` suffixes with decimal values (e.g. `1.5m`, `2.7s`) and combined units (e.g. `1m30s`, `2s700ms`, `18h36m4s200ms`); bare numbers default to seconds diff --git a/src/seed/db.rs b/src/seed/db.rs index 4d34d91..433707d 100644 --- a/src/seed/db.rs +++ b/src/seed/db.rs @@ -8,6 +8,7 @@ pub trait Database: Send { table: &str, columns: &[String], values: &[String], + auto_id_column: Option<&str>, ) -> Result, String>; fn row_exists( &mut self, @@ -104,6 +105,7 @@ impl Database for SqliteDb { table: &str, columns: &[String], values: &[String], + _auto_id_column: Option<&str>, ) -> Result, String> { let col_list: Vec = columns .iter() @@ -310,18 +312,20 @@ impl Database for PostgresDb { table: &str, columns: &[String], values: &[String], + auto_id_column: Option<&str>, ) -> Result, String> { let col_list: Vec = columns .iter() .map(|c| format!("\"{}\"", sanitize_identifier(c))) .collect(); let placeholders: Vec = (1..=values.len()).map(|i| format!("${}", i)).collect(); + let returning_col = sanitize_identifier(auto_id_column.unwrap_or("id")); let sql = format!( "INSERT INTO \"{}\" ({}) VALUES ({}) RETURNING COALESCE(CAST(\"{}\" AS BIGINT), 0)", sanitize_identifier(table), col_list.join(", "), placeholders.join(", "), - sanitize_identifier(columns.first().map(|s| s.as_str()).unwrap_or("id")) + returning_col ); let params: Vec<&(dyn postgres::types::ToSql + Sync)> = values .iter() @@ -543,6 +547,7 @@ impl Database for MysqlDb { table: &str, columns: &[String], values: &[String], + _auto_id_column: Option<&str>, ) -> Result, String> { let col_list: Vec = columns .iter() @@ -747,7 +752,7 @@ mod tests { let columns = vec!["name".into(), "email".into()]; let values = vec!["Alice".into(), "alice@example.com".into()]; - let id = db.insert_row("users", &columns, &values).unwrap(); + let id = db.insert_row("users", &columns, &values, None).unwrap(); assert!(id.is_some()); assert_eq!(id.unwrap(), 1); @@ -765,9 +770,9 @@ mod tests { db.conn .execute("CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)", []) .unwrap(); - db.insert_row("items", &["name".into()], &["item1".into()]) + db.insert_row("items", &["name".into()], &["item1".into()], None) .unwrap(); - db.insert_row("items", &["name".into()], &["item2".into()]) + db.insert_row("items", &["name".into()], &["item2".into()], None) .unwrap(); let count = db.delete_rows("items").unwrap(); assert_eq!(count, 2); @@ -780,7 +785,8 @@ mod tests { .execute("CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT)", []) .unwrap(); db.begin_transaction().unwrap(); - db.insert_row("t", &["v".into()], &["a".into()]).unwrap(); + db.insert_row("t", &["v".into()], &["a".into()], None) + .unwrap(); db.rollback_transaction().unwrap(); let count: i64 = db .conn @@ -789,7 +795,8 @@ mod tests { assert_eq!(count, 0); db.begin_transaction().unwrap(); - db.insert_row("t", &["v".into()], &["b".into()]).unwrap(); + db.insert_row("t", &["v".into()], &["b".into()], None) + .unwrap(); db.commit_transaction().unwrap(); let count: i64 = db .conn diff --git a/src/seed/executor.rs b/src/seed/executor.rs index 64a20da..a51d270 100644 --- a/src/seed/executor.rs +++ b/src/seed/executor.rs @@ -242,7 +242,8 @@ impl<'a> SeedExecutor<'a> { continue; } - let generated_id = self.db.insert_row(table, &columns, &values)?; + let auto_id_col = ts.auto_id.as_ref().map(|a| a.column.as_str()); + let generated_id = self.db.insert_row(table, &columns, &values, auto_id_col)?; if let Some(ref_key) = ref_name { let mut ref_map = HashMap::new(); diff --git a/tests/README.md b/tests/README.md index ab4afa4..2a11133 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,17 +1,51 @@ -# Integration Tests +# Tests -This directory contains integration tests for Initium. +## Unit Tests -## Running +Run unit tests with: -Integration tests require external services (Postgres, HTTP servers, etc.) and are not run in standard `cargo test`. +```bash +cargo test +``` + +## Integration Tests + +Integration tests require Docker Compose services (Postgres 16, MySQL 8.0, nginx). ```bash -# Run unit tests only (default) -make test +# Start services +docker compose -f tests/docker-compose.yml up -d --wait -# Integration tests require docker-compose (future) -# docker-compose -f tests/docker-compose.yml up -d -# cargo test --test integration +# Run integration tests +INTEGRATION=1 cargo test --test integration_test -- --test-threads=1 + +# Stop services +docker compose -f tests/docker-compose.yml down ``` +### Test scenarios + +| Test | Description | +| ------------------------------------- | -------------------------------------------------------- | +| `test_waitfor_tcp_postgres` | wait-for TCP against Postgres succeeds | +| `test_waitfor_tcp_mysql` | wait-for TCP against MySQL succeeds | +| `test_waitfor_http_server` | wait-for HTTP against nginx returns 200 | +| `test_waitfor_nonexistent_service` | wait-for against closed port fails with exit code 1 | +| `test_waitfor_multiple_targets` | wait-for with Postgres + MySQL + HTTP all reachable | +| `test_render_template` | render envsubst template produces correct output | +| `test_fetch_from_http_server` | fetch from nginx writes HTML to file | +| `test_exec_command` | exec echo captures output in logs | +| `test_exec_failing_command` | exec false returns exit code 1 | +| `test_seed_postgres` | seed PostgreSQL with refs, idempotency, and reset | +| `test_seed_mysql` | seed MySQL with refs and idempotency | +| `test_seed_postgres_create_database` | seed creates a PostgreSQL database via create_if_missing | +| `test_seed_postgres_create_schema` | seed creates a PostgreSQL schema via create_if_missing | +| `test_seed_mysql_create_database` | seed creates a MySQL database via create_if_missing | +| `test_seed_postgres_create_nonexistent_db_alpha` | create-if-missing with known non-existing PG database | +| `test_seed_postgres_create_nonexistent_db_beta` | create-if-missing with second non-existing PG database + idempotency | +| `test_seed_mysql_create_nonexistent_db_alpha` | create-if-missing with known non-existing MySQL database | +| `test_seed_mysql_create_nonexistent_db_beta` | create-if-missing with second non-existing MySQL database + idempotency | + +### CI + +Integration tests run automatically via `.github/workflows/integration.yml` using GitHub Actions service containers. diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml new file mode 100644 index 0000000..32bd49a --- /dev/null +++ b/tests/docker-compose.yml @@ -0,0 +1,39 @@ +services: + postgres: + image: postgres:16-alpine + environment: + POSTGRES_USER: initium + POSTGRES_PASSWORD: initium + POSTGRES_DB: initium_test + ports: + - "15432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U initium"] + interval: 2s + timeout: 5s + retries: 10 + + mysql: + image: mysql:8.0 + environment: + MYSQL_ROOT_PASSWORD: rootpass + MYSQL_USER: initium + MYSQL_PASSWORD: initium + MYSQL_DATABASE: initium_test + ports: + - "13306:3306" + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-prootpass"] + interval: 2s + timeout: 5s + retries: 15 + + http-server: + image: nginx:1-alpine + ports: + - "18080:80" + healthcheck: + test: ["CMD", "wget", "--spider", "-q", "http://localhost:80/"] + interval: 2s + timeout: 5s + retries: 5 diff --git a/tests/fixtures/seed-mysql.yaml b/tests/fixtures/seed-mysql.yaml new file mode 100644 index 0000000..1566e68 --- /dev/null +++ b/tests/fixtures/seed-mysql.yaml @@ -0,0 +1,37 @@ +database: + driver: mysql + url_env: MYSQL_URL + tracking_table: initium_seed + +phases: + - name: setup + order: 1 + seed_sets: + - name: products + order: 1 + tables: + - table: products + unique_key: [sku] + auto_id: + column: id + rows: + - _ref: prod_widget + sku: WIDGET-001 + name: Widget + price: "9.99" + - _ref: prod_gadget + sku: GADGET-001 + name: Gadget + price: "19.99" + + - name: orders + order: 2 + tables: + - table: orders + auto_id: + column: id + rows: + - product_id: "@ref:prod_widget.id" + quantity: "2" + - product_id: "@ref:prod_gadget.id" + quantity: "1" diff --git a/tests/fixtures/seed-postgres.yaml b/tests/fixtures/seed-postgres.yaml new file mode 100644 index 0000000..b5817d0 --- /dev/null +++ b/tests/fixtures/seed-postgres.yaml @@ -0,0 +1,36 @@ +database: + driver: postgres + url_env: POSTGRES_URL + tracking_table: initium_seed + +phases: + - name: setup + order: 1 + seed_sets: + - name: departments + order: 1 + tables: + - table: departments + unique_key: [name] + auto_id: + column: id + rows: + - _ref: dept_eng + name: Engineering + - _ref: dept_sales + name: Sales + + - name: employees + order: 2 + tables: + - table: employees + unique_key: [email] + auto_id: + column: id + rows: + - name: Alice + email: alice@example.com + department_id: "@ref:dept_eng.id" + - name: Bob + email: bob@example.com + department_id: "@ref:dept_sales.id" diff --git a/tests/fixtures/template.conf.tmpl b/tests/fixtures/template.conf.tmpl new file mode 100644 index 0000000..76edff3 --- /dev/null +++ b/tests/fixtures/template.conf.tmpl @@ -0,0 +1,6 @@ +server { + host = ${DB_HOST} + port = ${DB_PORT} + database = ${DB_NAME} + max_connections = ${MAX_CONN} +} diff --git a/tests/integration_test.rs b/tests/integration_test.rs new file mode 100644 index 0000000..2d16faa --- /dev/null +++ b/tests/integration_test.rs @@ -0,0 +1,952 @@ +//! Integration tests requiring docker-compose services. +//! +//! These tests are ignored by default and only run when the +//! `INTEGRATION` environment variable is set to `1`. +//! +//! To run: +//! docker compose -f tests/docker-compose.yml up -d --wait +//! INTEGRATION=1 cargo test --test integration_test -- --test-threads=1 +//! docker compose -f tests/docker-compose.yml down + +use std::process::Command; + +fn initium_bin() -> String { + env!("CARGO_BIN_EXE_initium").to_string() +} + +fn integration_enabled() -> bool { + std::env::var("INTEGRATION").map_or(false, |v| v == "1") +} + +fn fixtures_dir() -> String { + let manifest = env!("CARGO_MANIFEST_DIR"); + format!("{}/tests/fixtures", manifest) +} + +const PG_URL: &str = "postgres://initium:initium@localhost:15432/initium_test"; +const MYSQL_URL_STR: &str = "mysql://initium:initium@localhost:13306/initium_test"; +const MYSQL_ROOT_URL_STR: &str = "mysql://root:rootpass@localhost:13306/initium_test"; + +fn pg_client() -> postgres::Client { + postgres::Client::connect(PG_URL, postgres::NoTls).expect("failed to connect to postgres") +} + +fn mysql_conn() -> mysql::PooledConn { + let pool = mysql::Pool::new(MYSQL_URL_STR).expect("failed to connect to mysql"); + pool.get_conn().expect("failed to get mysql connection") +} + +fn mysql_root_conn() -> mysql::PooledConn { + let pool = mysql::Pool::new(MYSQL_ROOT_URL_STR).expect("failed to connect to mysql as root"); + pool.get_conn() + .expect("failed to get mysql root connection") +} + +// --------------------------------------------------------------------------- +// wait-for: TCP against Postgres +// --------------------------------------------------------------------------- +#[test] +fn test_waitfor_tcp_postgres() { + if !integration_enabled() { + return; + } + let out = Command::new(initium_bin()) + .args([ + "wait-for", + "--target", + "tcp://localhost:15432", + "--timeout", + "30s", + "--max-attempts", + "30", + ]) + .output() + .expect("failed to run initium"); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!( + out.status.success(), + "wait-for tcp postgres should succeed: {}", + stderr + ); + assert!( + stderr.contains("target is reachable"), + "expected reachable log: {}", + stderr + ); +} + +// --------------------------------------------------------------------------- +// wait-for: HTTP against nginx health-check server +// --------------------------------------------------------------------------- +#[test] +fn test_waitfor_http_server() { + if !integration_enabled() { + return; + } + let out = Command::new(initium_bin()) + .args([ + "wait-for", + "--target", + "http://localhost:18080/", + "--timeout", + "30s", + "--max-attempts", + "30", + ]) + .output() + .expect("failed to run initium"); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!( + out.status.success(), + "wait-for http should succeed: {}", + stderr + ); + assert!( + stderr.contains("target is reachable"), + "expected reachable log: {}", + stderr + ); +} + +// --------------------------------------------------------------------------- +// wait-for: non-existent service times out with proper exit code +// --------------------------------------------------------------------------- +#[test] +fn test_waitfor_nonexistent_service_timeout() { + if !integration_enabled() { + return; + } + let out = Command::new(initium_bin()) + .args([ + "wait-for", + "--target", + "tcp://localhost:19999", + "--timeout", + "2s", + "--max-attempts", + "2", + "--initial-delay", + "500ms", + ]) + .output() + .expect("failed to run initium"); + assert!(!out.status.success(), "wait-for non-existent should fail"); + let code = out.status.code().unwrap_or(-1); + assert_eq!(code, 1, "expected exit code 1, got {}", code); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!( + stderr.contains("not reachable"), + "expected 'not reachable' in error: {}", + stderr + ); +} + +// --------------------------------------------------------------------------- +// wait-for: TCP against MySQL +// --------------------------------------------------------------------------- +#[test] +fn test_waitfor_tcp_mysql() { + if !integration_enabled() { + return; + } + let out = Command::new(initium_bin()) + .args([ + "wait-for", + "--target", + "tcp://localhost:13306", + "--timeout", + "30s", + "--max-attempts", + "30", + ]) + .output() + .expect("failed to run initium"); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!( + out.status.success(), + "wait-for tcp mysql should succeed: {}", + stderr + ); +} + +// --------------------------------------------------------------------------- +// wait-for: multiple targets at once +// --------------------------------------------------------------------------- +#[test] +fn test_waitfor_multiple_targets() { + if !integration_enabled() { + return; + } + let out = Command::new(initium_bin()) + .args([ + "wait-for", + "--target", + "tcp://localhost:15432", + "--target", + "tcp://localhost:13306", + "--target", + "http://localhost:18080/", + "--timeout", + "30s", + "--max-attempts", + "30", + ]) + .output() + .expect("failed to run initium"); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!( + out.status.success(), + "wait-for multiple should succeed: {}", + stderr + ); + assert!( + stderr.contains("all targets reachable"), + "expected all targets reachable: {}", + stderr + ); +} + +// --------------------------------------------------------------------------- +// render: template with env vars produces correct output +// --------------------------------------------------------------------------- +#[test] +fn test_render_template() { + if !integration_enabled() { + return; + } + let workdir = tempfile::TempDir::new().expect("failed to create tempdir"); + let template = format!("{}/template.conf.tmpl", fixtures_dir()); + + let out = Command::new(initium_bin()) + .args([ + "render", + "--template", + &template, + "--output", + "app.conf", + "--workdir", + workdir.path().to_str().unwrap(), + ]) + .env("DB_HOST", "postgres.prod") + .env("DB_PORT", "5432") + .env("DB_NAME", "myapp") + .env("MAX_CONN", "100") + .output() + .expect("failed to run initium"); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!(out.status.success(), "render should succeed: {}", stderr); + + let rendered = std::fs::read_to_string(workdir.path().join("app.conf")) + .expect("failed to read rendered output"); + assert!( + rendered.contains("host = postgres.prod"), + "expected host: {}", + rendered + ); + assert!( + rendered.contains("port = 5432"), + "expected port: {}", + rendered + ); + assert!( + rendered.contains("database = myapp"), + "expected database: {}", + rendered + ); + assert!( + rendered.contains("max_connections = 100"), + "expected max_conn: {}", + rendered + ); +} + +// --------------------------------------------------------------------------- +// fetch: from HTTP server writes response to file +// --------------------------------------------------------------------------- +#[test] +fn test_fetch_from_http_server() { + if !integration_enabled() { + return; + } + let workdir = tempfile::TempDir::new().expect("failed to create tempdir"); + + let out = Command::new(initium_bin()) + .args([ + "fetch", + "--url", + "http://localhost:18080/", + "--output", + "index.html", + "--workdir", + workdir.path().to_str().unwrap(), + "--timeout", + "30s", + ]) + .output() + .expect("failed to run initium"); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!(out.status.success(), "fetch should succeed: {}", stderr); + + let fetched = std::fs::read_to_string(workdir.path().join("index.html")) + .expect("failed to read fetched file"); + assert!(!fetched.is_empty(), "fetched file should not be empty"); + assert!( + fetched.contains("nginx") || fetched.contains("Welcome") || fetched.contains("html"), + "fetched content should contain html: {}", + &fetched[..fetched.len().min(200)] + ); +} + +// --------------------------------------------------------------------------- +// exec: runs command, captures output and exit code +// --------------------------------------------------------------------------- +#[test] +fn test_exec_command() { + if !integration_enabled() { + return; + } + let out = Command::new(initium_bin()) + .args(["exec", "--", "echo", "hello-from-initium"]) + .output() + .expect("failed to run initium"); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!(out.status.success(), "exec echo should succeed: {}", stderr); + assert!( + stderr.contains("hello-from-initium"), + "expected captured output in logs: {}", + stderr + ); +} + +#[test] +fn test_exec_failing_command() { + if !integration_enabled() { + return; + } + let out = Command::new(initium_bin()) + .args(["exec", "--", "false"]) + .output() + .expect("failed to run initium"); + assert!(!out.status.success(), "exec false should fail"); + let code = out.status.code().unwrap_or(-1); + assert_eq!(code, 1, "expected exit code 1, got {}", code); +} + +// --------------------------------------------------------------------------- +// seed: PostgreSQL — create tables, seed, verify +// --------------------------------------------------------------------------- +#[test] +fn test_seed_postgres() { + if !integration_enabled() { + return; + } + + let mut client = pg_client(); + client + .batch_execute( + "DROP TABLE IF EXISTS employees; + DROP TABLE IF EXISTS departments; + DROP TABLE IF EXISTS initium_seed; + CREATE TABLE departments (id SERIAL PRIMARY KEY, name TEXT UNIQUE); + CREATE TABLE employees (id SERIAL PRIMARY KEY, name TEXT, email TEXT UNIQUE, department_id INTEGER REFERENCES departments(id));", + ) + .expect("failed to create postgres tables"); + + let spec = format!("{}/seed-postgres.yaml", fixtures_dir()); + let out = Command::new(initium_bin()) + .args(["seed", "--spec", &spec]) + .env("POSTGRES_URL", PG_URL) + .output() + .expect("failed to run seed"); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!( + out.status.success(), + "seed postgres should succeed: {}", + stderr + ); + assert!( + stderr.contains("seed execution completed"), + "expected completion log: {}", + stderr + ); + + // Verify data + let dept_count: i64 = client + .query_one("SELECT COUNT(*) FROM departments", &[]) + .unwrap() + .get(0); + assert_eq!(dept_count, 2, "expected 2 departments"); + + let emp_count: i64 = client + .query_one("SELECT COUNT(*) FROM employees", &[]) + .unwrap() + .get(0); + assert_eq!(emp_count, 2, "expected 2 employees"); + + // Verify cross-table references + let rows = client + .query( + "SELECT e.name, d.name FROM employees e JOIN departments d ON e.department_id = d.id ORDER BY e.name", + &[], + ) + .unwrap(); + assert_eq!(rows.len(), 2); + let alice_dept: &str = rows[0].get(1); + let bob_dept: &str = rows[1].get(1); + assert_eq!(alice_dept, "Engineering", "Alice should be in Engineering"); + assert_eq!(bob_dept, "Sales", "Bob should be in Sales"); + + // Test idempotency + let out = Command::new(initium_bin()) + .args(["seed", "--spec", &spec]) + .env("POSTGRES_URL", PG_URL) + .output() + .expect("failed to re-run seed"); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!( + out.status.success(), + "idempotent seed should succeed: {}", + stderr + ); + assert!( + stderr.contains("already applied"), + "expected skip log on re-run: {}", + stderr + ); + + let dept_count: i64 = client + .query_one("SELECT COUNT(*) FROM departments", &[]) + .unwrap() + .get(0); + assert_eq!(dept_count, 2, "idempotent re-run should not duplicate"); + + // Test reset mode + let out = Command::new(initium_bin()) + .args(["seed", "--spec", &spec, "--reset"]) + .env("POSTGRES_URL", PG_URL) + .output() + .expect("failed to run seed --reset"); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!( + out.status.success(), + "seed --reset should succeed: {}", + stderr + ); + assert!( + stderr.contains("reset mode"), + "expected reset log: {}", + stderr + ); + + let dept_count: i64 = client + .query_one("SELECT COUNT(*) FROM departments", &[]) + .unwrap() + .get(0); + assert_eq!(dept_count, 2, "reset should re-seed 2 departments"); +} + +// --------------------------------------------------------------------------- +// seed: MySQL — create tables, seed, verify +// --------------------------------------------------------------------------- +#[test] +fn test_seed_mysql() { + if !integration_enabled() { + return; + } + use mysql::prelude::Queryable; + + let mut conn = mysql_conn(); + conn.query_drop("DROP TABLE IF EXISTS orders").unwrap(); + conn.query_drop("DROP TABLE IF EXISTS products").unwrap(); + conn.query_drop("DROP TABLE IF EXISTS initium_seed") + .unwrap(); + conn.query_drop( + "CREATE TABLE products (id INT AUTO_INCREMENT PRIMARY KEY, sku VARCHAR(255) UNIQUE, name VARCHAR(255), price VARCHAR(50))", + ) + .unwrap(); + conn.query_drop( + "CREATE TABLE orders (id INT AUTO_INCREMENT PRIMARY KEY, product_id INT, quantity VARCHAR(50), FOREIGN KEY (product_id) REFERENCES products(id))", + ) + .unwrap(); + + let spec = format!("{}/seed-mysql.yaml", fixtures_dir()); + let out = Command::new(initium_bin()) + .args(["seed", "--spec", &spec]) + .env("MYSQL_URL", MYSQL_URL_STR) + .output() + .expect("failed to run seed"); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!( + out.status.success(), + "seed mysql should succeed: {}", + stderr + ); + assert!( + stderr.contains("seed execution completed"), + "expected completion log: {}", + stderr + ); + + // Verify data + let prod_count: Option = conn + .exec_first("SELECT COUNT(*) FROM products", ()) + .unwrap(); + assert_eq!(prod_count, Some(2), "expected 2 products"); + + let order_count: Option = conn.exec_first("SELECT COUNT(*) FROM orders", ()).unwrap(); + assert_eq!(order_count, Some(2), "expected 2 orders"); + + // Verify cross-table references + let rows: Vec<(String, String)> = conn + .exec( + "SELECT p.name, o.quantity FROM orders o JOIN products p ON o.product_id = p.id ORDER BY p.name", + (), + ) + .unwrap(); + assert_eq!(rows.len(), 2); + assert_eq!(rows[0].0, "Gadget"); + assert_eq!(rows[0].1, "1"); + assert_eq!(rows[1].0, "Widget"); + assert_eq!(rows[1].1, "2"); + + // Test idempotency + let out = Command::new(initium_bin()) + .args(["seed", "--spec", &spec]) + .env("MYSQL_URL", MYSQL_URL_STR) + .output() + .expect("failed to re-run seed"); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!( + out.status.success(), + "idempotent seed should succeed: {}", + stderr + ); + assert!( + stderr.contains("already applied"), + "expected skip log on re-run: {}", + stderr + ); + + let prod_count: Option = conn + .exec_first("SELECT COUNT(*) FROM products", ()) + .unwrap(); + assert_eq!( + prod_count, + Some(2), + "idempotent re-run should not duplicate" + ); +} + +// --------------------------------------------------------------------------- +// seed: PostgreSQL — create database via seed phase +// --------------------------------------------------------------------------- +#[test] +fn test_seed_postgres_create_database() { + if !integration_enabled() { + return; + } + + let mut client = pg_client(); + let _ = client.batch_execute("DROP DATABASE IF EXISTS initium_created_db"); + + let workdir = tempfile::TempDir::new().expect("failed to create tempdir"); + let spec_path = workdir.path().join("create-db-seed.yaml"); + std::fs::write( + &spec_path, + "database:\n driver: postgres\n url_env: POSTGRES_URL\n tracking_table: initium_seed\n\nphases:\n - name: create_db\n order: 1\n database: initium_created_db\n create_if_missing: true\n seed_sets:\n - name: placeholder\n tables:\n - table: departments\n rows: []\n", + ) + .expect("failed to write spec"); + + let out = Command::new(initium_bin()) + .args(["seed", "--spec", spec_path.to_str().unwrap()]) + .env("POSTGRES_URL", PG_URL) + .output() + .expect("failed to run seed"); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!( + out.status.success(), + "seed create database should succeed: {}", + stderr + ); + assert!( + stderr.contains("creating database if missing"), + "expected create database log: {}", + stderr + ); + + let count: i64 = client + .query_one( + "SELECT COUNT(*) FROM pg_database WHERE datname = 'initium_created_db'", + &[], + ) + .unwrap() + .get(0); + assert_eq!(count, 1, "expected database to exist"); + + // Idempotent re-run + let out = Command::new(initium_bin()) + .args(["seed", "--spec", spec_path.to_str().unwrap()]) + .env("POSTGRES_URL", PG_URL) + .output() + .expect("failed to re-run seed"); + assert!( + out.status.success(), + "idempotent create database should succeed" + ); + + let _ = client.batch_execute("DROP DATABASE IF EXISTS initium_created_db"); +} + +// --------------------------------------------------------------------------- +// seed: PostgreSQL — create schema via seed phase +// --------------------------------------------------------------------------- +#[test] +fn test_seed_postgres_create_schema() { + if !integration_enabled() { + return; + } + + let mut client = pg_client(); + let _ = client.batch_execute("DROP SCHEMA IF EXISTS test_analytics CASCADE"); + + let workdir = tempfile::TempDir::new().expect("failed to create tempdir"); + let spec_path = workdir.path().join("create-schema-seed.yaml"); + std::fs::write( + &spec_path, + "database:\n driver: postgres\n url_env: POSTGRES_URL\n tracking_table: initium_seed\n\nphases:\n - name: create_schema\n order: 1\n schema: test_analytics\n create_if_missing: true\n seed_sets:\n - name: placeholder\n tables:\n - table: departments\n rows: []\n", + ) + .expect("failed to write spec"); + + let out = Command::new(initium_bin()) + .args(["seed", "--spec", spec_path.to_str().unwrap()]) + .env("POSTGRES_URL", PG_URL) + .output() + .expect("failed to run seed"); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!( + out.status.success(), + "seed create schema should succeed: {}", + stderr + ); + assert!( + stderr.contains("creating schema if missing"), + "expected create schema log: {}", + stderr + ); + + let count: i64 = client + .query_one( + "SELECT COUNT(*) FROM information_schema.schemata WHERE schema_name = 'test_analytics'", + &[], + ) + .unwrap() + .get(0); + assert_eq!(count, 1, "expected schema to exist"); + + let _ = client.batch_execute("DROP SCHEMA IF EXISTS test_analytics CASCADE"); +} + +// --------------------------------------------------------------------------- +// seed: MySQL — create database via seed phase +// --------------------------------------------------------------------------- +#[test] +fn test_seed_mysql_create_database() { + if !integration_enabled() { + return; + } + use mysql::prelude::Queryable; + + let mut conn = mysql_root_conn(); + let _ = conn.query_drop("DROP DATABASE IF EXISTS initium_created_db"); + + let workdir = tempfile::TempDir::new().expect("failed to create tempdir"); + let spec_path = workdir.path().join("create-db-seed.yaml"); + std::fs::write( + &spec_path, + "database:\n driver: mysql\n url_env: MYSQL_URL\n tracking_table: initium_seed\n\nphases:\n - name: create_db\n order: 1\n database: initium_created_db\n create_if_missing: true\n seed_sets:\n - name: placeholder\n tables:\n - table: products\n rows: []\n", + ) + .expect("failed to write spec"); + + let out = Command::new(initium_bin()) + .args(["seed", "--spec", spec_path.to_str().unwrap()]) + .env("MYSQL_URL", MYSQL_ROOT_URL_STR) + .output() + .expect("failed to run seed"); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!( + out.status.success(), + "seed create database should succeed: {}", + stderr + ); + + let count: Option = conn + .exec_first( + "SELECT COUNT(*) FROM information_schema.schemata WHERE SCHEMA_NAME = 'initium_created_db'", + (), + ) + .unwrap(); + assert_eq!(count, Some(1), "expected database to exist"); + + // Idempotent re-run + let out = Command::new(initium_bin()) + .args(["seed", "--spec", spec_path.to_str().unwrap()]) + .env("MYSQL_URL", MYSQL_ROOT_URL_STR) + .output() + .expect("failed to re-run seed"); + assert!( + out.status.success(), + "idempotent create database should succeed" + ); + + let _ = conn.query_drop("DROP DATABASE IF EXISTS initium_created_db"); +} + +// --------------------------------------------------------------------------- +// seed: PostgreSQL — create non-existing database and seed data into it +// --------------------------------------------------------------------------- +#[test] +fn test_seed_postgres_create_nonexistent_db_alpha() { + if !integration_enabled() { + return; + } + + let mut client = pg_client(); + let _ = client.batch_execute("DROP DATABASE IF EXISTS initium_noexist_alpha"); + + // Verify the database does NOT exist before seeding + let count: i64 = client + .query_one( + "SELECT COUNT(*) FROM pg_database WHERE datname = 'initium_noexist_alpha'", + &[], + ) + .unwrap() + .get(0); + assert_eq!(count, 0, "database should not exist before test"); + + let workdir = tempfile::TempDir::new().expect("failed to create tempdir"); + let spec_path = workdir.path().join("create-db-alpha.yaml"); + std::fs::write( + &spec_path, + "database:\n driver: postgres\n url_env: POSTGRES_URL\n tracking_table: initium_seed\n\nphases:\n - name: create_alpha\n order: 1\n database: initium_noexist_alpha\n create_if_missing: true\n seed_sets:\n - name: placeholder\n tables:\n - table: departments\n rows: []\n", + ) + .expect("failed to write spec"); + + let out = Command::new(initium_bin()) + .args(["seed", "--spec", spec_path.to_str().unwrap()]) + .env("POSTGRES_URL", PG_URL) + .output() + .expect("failed to run seed"); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!( + out.status.success(), + "seed create nonexistent db alpha should succeed: {}", + stderr + ); + assert!( + stderr.contains("creating database if missing"), + "expected create database log: {}", + stderr + ); + + // Verify the database was created + let count: i64 = client + .query_one( + "SELECT COUNT(*) FROM pg_database WHERE datname = 'initium_noexist_alpha'", + &[], + ) + .unwrap() + .get(0); + assert_eq!(count, 1, "database should now exist"); + + let _ = client.batch_execute("DROP DATABASE IF EXISTS initium_noexist_alpha"); +} + +// --------------------------------------------------------------------------- +// seed: PostgreSQL — create a second non-existing database with different name +// --------------------------------------------------------------------------- +#[test] +fn test_seed_postgres_create_nonexistent_db_beta() { + if !integration_enabled() { + return; + } + + let mut client = pg_client(); + let _ = client.batch_execute("DROP DATABASE IF EXISTS initium_noexist_beta"); + + // Verify the database does NOT exist before seeding + let count: i64 = client + .query_one( + "SELECT COUNT(*) FROM pg_database WHERE datname = 'initium_noexist_beta'", + &[], + ) + .unwrap() + .get(0); + assert_eq!(count, 0, "database should not exist before test"); + + let workdir = tempfile::TempDir::new().expect("failed to create tempdir"); + let spec_path = workdir.path().join("create-db-beta.yaml"); + std::fs::write( + &spec_path, + "database:\n driver: postgres\n url_env: POSTGRES_URL\n tracking_table: initium_seed\n\nphases:\n - name: create_beta\n order: 1\n database: initium_noexist_beta\n create_if_missing: true\n seed_sets:\n - name: placeholder\n tables:\n - table: departments\n rows: []\n", + ) + .expect("failed to write spec"); + + let out = Command::new(initium_bin()) + .args(["seed", "--spec", spec_path.to_str().unwrap()]) + .env("POSTGRES_URL", PG_URL) + .output() + .expect("failed to run seed"); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!( + out.status.success(), + "seed create nonexistent db beta should succeed: {}", + stderr + ); + assert!( + stderr.contains("creating database if missing"), + "expected create database log: {}", + stderr + ); + + // Verify the database was created + let count: i64 = client + .query_one( + "SELECT COUNT(*) FROM pg_database WHERE datname = 'initium_noexist_beta'", + &[], + ) + .unwrap() + .get(0); + assert_eq!(count, 1, "database should now exist"); + + // Re-run to verify idempotency — should not fail + let out = Command::new(initium_bin()) + .args(["seed", "--spec", spec_path.to_str().unwrap()]) + .env("POSTGRES_URL", PG_URL) + .output() + .expect("failed to re-run seed"); + assert!( + out.status.success(), + "idempotent create nonexistent db beta should succeed" + ); + + let _ = client.batch_execute("DROP DATABASE IF EXISTS initium_noexist_beta"); +} + +// --------------------------------------------------------------------------- +// seed: MySQL — create non-existing database and verify +// --------------------------------------------------------------------------- +#[test] +fn test_seed_mysql_create_nonexistent_db_alpha() { + if !integration_enabled() { + return; + } + use mysql::prelude::Queryable; + + let mut conn = mysql_root_conn(); + let _ = conn.query_drop("DROP DATABASE IF EXISTS initium_noexist_alpha"); + + // Verify the database does NOT exist before seeding + let count: Option = conn + .exec_first( + "SELECT COUNT(*) FROM information_schema.schemata WHERE SCHEMA_NAME = 'initium_noexist_alpha'", + (), + ) + .unwrap(); + assert_eq!(count, Some(0), "database should not exist before test"); + + let workdir = tempfile::TempDir::new().expect("failed to create tempdir"); + let spec_path = workdir.path().join("create-db-alpha.yaml"); + std::fs::write( + &spec_path, + "database:\n driver: mysql\n url_env: MYSQL_URL\n tracking_table: initium_seed\n\nphases:\n - name: create_alpha\n order: 1\n database: initium_noexist_alpha\n create_if_missing: true\n seed_sets:\n - name: placeholder\n tables:\n - table: products\n rows: []\n", + ) + .expect("failed to write spec"); + + let out = Command::new(initium_bin()) + .args(["seed", "--spec", spec_path.to_str().unwrap()]) + .env("MYSQL_URL", MYSQL_ROOT_URL_STR) + .output() + .expect("failed to run seed"); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!( + out.status.success(), + "seed create nonexistent db alpha should succeed: {}", + stderr + ); + + // Verify the database was created + let count: Option = conn + .exec_first( + "SELECT COUNT(*) FROM information_schema.schemata WHERE SCHEMA_NAME = 'initium_noexist_alpha'", + (), + ) + .unwrap(); + assert_eq!(count, Some(1), "database should now exist"); + + let _ = conn.query_drop("DROP DATABASE IF EXISTS initium_noexist_alpha"); +} + +// --------------------------------------------------------------------------- +// seed: MySQL — create a second non-existing database with different name +// --------------------------------------------------------------------------- +#[test] +fn test_seed_mysql_create_nonexistent_db_beta() { + if !integration_enabled() { + return; + } + use mysql::prelude::Queryable; + + let mut conn = mysql_root_conn(); + let _ = conn.query_drop("DROP DATABASE IF EXISTS initium_noexist_beta"); + + // Verify the database does NOT exist before seeding + let count: Option = conn + .exec_first( + "SELECT COUNT(*) FROM information_schema.schemata WHERE SCHEMA_NAME = 'initium_noexist_beta'", + (), + ) + .unwrap(); + assert_eq!(count, Some(0), "database should not exist before test"); + + let workdir = tempfile::TempDir::new().expect("failed to create tempdir"); + let spec_path = workdir.path().join("create-db-beta.yaml"); + std::fs::write( + &spec_path, + "database:\n driver: mysql\n url_env: MYSQL_URL\n tracking_table: initium_seed\n\nphases:\n - name: create_beta\n order: 1\n database: initium_noexist_beta\n create_if_missing: true\n seed_sets:\n - name: placeholder\n tables:\n - table: products\n rows: []\n", + ) + .expect("failed to write spec"); + + let out = Command::new(initium_bin()) + .args(["seed", "--spec", spec_path.to_str().unwrap()]) + .env("MYSQL_URL", MYSQL_ROOT_URL_STR) + .output() + .expect("failed to run seed"); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!( + out.status.success(), + "seed create nonexistent db beta should succeed: {}", + stderr + ); + + // Verify the database was created + let count: Option = conn + .exec_first( + "SELECT COUNT(*) FROM information_schema.schemata WHERE SCHEMA_NAME = 'initium_noexist_beta'", + (), + ) + .unwrap(); + assert_eq!(count, Some(1), "database should now exist"); + + // Re-run to verify idempotency — should not fail + let out = Command::new(initium_bin()) + .args(["seed", "--spec", spec_path.to_str().unwrap()]) + .env("MYSQL_URL", MYSQL_ROOT_URL_STR) + .output() + .expect("failed to re-run seed"); + assert!( + out.status.success(), + "idempotent create nonexistent db beta should succeed" + ); + + let _ = conn.query_drop("DROP DATABASE IF EXISTS initium_noexist_beta"); +} From df49dab485fd19a57560c34db8960d08db8ed69d Mon Sep 17 00:00:00 2001 From: mikkeldamsgaard Date: Tue, 24 Feb 2026 22:15:53 +0100 Subject: [PATCH 2/4] fix: use inline escaped values in postgres insert_row/row_exists to fix TEXT-to-INTEGER coercion --- src/seed/db.rs | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/seed/db.rs b/src/seed/db.rs index 433707d..e8ba35d 100644 --- a/src/seed/db.rs +++ b/src/seed/db.rs @@ -318,22 +318,18 @@ impl Database for PostgresDb { .iter() .map(|c| format!("\"{}\"", sanitize_identifier(c))) .collect(); - let placeholders: Vec = (1..=values.len()).map(|i| format!("${}", i)).collect(); + let value_list: Vec = values.iter().map(|v| escape_sql_value(v)).collect(); let returning_col = sanitize_identifier(auto_id_column.unwrap_or("id")); let sql = format!( "INSERT INTO \"{}\" ({}) VALUES ({}) RETURNING COALESCE(CAST(\"{}\" AS BIGINT), 0)", sanitize_identifier(table), col_list.join(", "), - placeholders.join(", "), + value_list.join(", "), returning_col ); - let params: Vec<&(dyn postgres::types::ToSql + Sync)> = values - .iter() - .map(|v| v as &(dyn postgres::types::ToSql + Sync)) - .collect(); let row = self .client - .query_one(&sql, params.as_slice()) + .query_one(&sql, &[]) .map_err(|e| format!("inserting row into '{}': {}", table, e))?; let id: i64 = row.get(0); Ok(Some(id)) @@ -350,21 +346,17 @@ impl Database for PostgresDb { } let conditions: Vec = unique_columns .iter() - .enumerate() - .map(|(i, c)| format!("\"{}\" = ${}", sanitize_identifier(c), i + 1)) + .zip(unique_values.iter()) + .map(|(c, v)| format!("\"{}\" = {}", sanitize_identifier(c), escape_sql_value(v))) .collect(); let sql = format!( "SELECT COUNT(*) FROM \"{}\" WHERE {}", sanitize_identifier(table), conditions.join(" AND ") ); - let params: Vec<&(dyn postgres::types::ToSql + Sync)> = unique_values - .iter() - .map(|v| v as &(dyn postgres::types::ToSql + Sync)) - .collect(); let row = self .client - .query_one(&sql, params.as_slice()) + .query_one(&sql, &[]) .map_err(|e| format!("checking row existence in '{}': {}", table, e))?; let count: i64 = row.get(0); Ok(count > 0) @@ -717,6 +709,10 @@ fn sanitize_identifier(name: &str) -> String { .collect() } +fn escape_sql_value(val: &str) -> String { + format!("'{}'", val.replace('\'', "''")) +} + #[cfg(test)] mod tests { use super::*; From 5d86f7de585f4911dce5ff941191ae17a0612fd1 Mon Sep 17 00:00:00 2001 From: mikkeldamsgaard Date: Tue, 24 Feb 2026 22:19:04 +0100 Subject: [PATCH 3/4] fix: reverse seed set order during reset to respect foreign key constraints --- src/seed/executor.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/seed/executor.rs b/src/seed/executor.rs index a51d270..7980824 100644 --- a/src/seed/executor.rs +++ b/src/seed/executor.rs @@ -76,7 +76,11 @@ impl<'a> SeedExecutor<'a> { } let mut seed_sets: Vec<&SeedSet> = phase.seed_sets.iter().collect(); - seed_sets.sort_by_key(|s| s.order); + if self.reset { + seed_sets.sort_by_key(|s| std::cmp::Reverse(s.order)); + } else { + seed_sets.sort_by_key(|s| s.order); + } for ss in &seed_sets { self.execute_seed_set(ss)?; } From fa5d5d37e549bcb2df94fefffa1a278909dc0ff1 Mon Sep 17 00:00:00 2001 From: mikkeldamsgaard Date: Tue, 24 Feb 2026 22:22:46 +0100 Subject: [PATCH 4/4] fix: separate reset (reverse) and seed (forward) passes for FK-safe reset mode --- src/seed/executor.rs | 41 +++++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/src/seed/executor.rs b/src/seed/executor.rs index 7980824..2594e9b 100644 --- a/src/seed/executor.rs +++ b/src/seed/executor.rs @@ -76,11 +76,14 @@ impl<'a> SeedExecutor<'a> { } let mut seed_sets: Vec<&SeedSet> = phase.seed_sets.iter().collect(); + seed_sets.sort_by_key(|s| s.order); + if self.reset { - seed_sets.sort_by_key(|s| std::cmp::Reverse(s.order)); - } else { - seed_sets.sort_by_key(|s| s.order); + for ss in seed_sets.iter().rev() { + self.reset_seed_set(ss)?; + } } + for ss in &seed_sets { self.execute_seed_set(ss)?; } @@ -144,25 +147,27 @@ impl<'a> SeedExecutor<'a> { } } + fn reset_seed_set(&mut self, ss: &SeedSet) -> Result<(), String> { + let name = &ss.name; + self.log + .info("reset mode: clearing seed set data", &[("seed_set", name)]); + let mut tables: Vec<&TableSeed> = ss.tables.iter().collect(); + tables.sort_by_key(|t| std::cmp::Reverse(t.order)); + for ts in &tables { + let count = self.db.delete_rows(&ts.table)?; + self.log.info( + "deleted rows", + &[("table", &ts.table), ("count", &count.to_string())], + ); + } + self.db.remove_seed_mark(&self.tracking_table, name)?; + Ok(()) + } + fn execute_seed_set(&mut self, ss: &SeedSet) -> Result<(), String> { let name = &ss.name; self.log.info("processing seed set", &[("seed_set", name)]); - if self.reset { - self.log - .info("reset mode: clearing seed set data", &[("seed_set", name)]); - let mut tables: Vec<&TableSeed> = ss.tables.iter().collect(); - tables.sort_by_key(|t| std::cmp::Reverse(t.order)); - for ts in &tables { - let count = self.db.delete_rows(&ts.table)?; - self.log.info( - "deleted rows", - &[("table", &ts.table), ("count", &count.to_string())], - ); - } - self.db.remove_seed_mark(&self.tracking_table, name)?; - } - if self.db.is_seed_applied(&self.tracking_table, name)? { self.log .info("seed set already applied, skipping", &[("seed_set", name)]);