diff --git a/README.md b/README.md index 491edc6..2e7109b 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ LLM disclaimer: I (Mark) estimate that 90% of the code/design/architecture has b [![License](https://img.shields.io/badge/license-AGPL-blue.svg)](LICENSE) [![Docs](https://img.shields.io/badge/docs-spawn.dev-green)](https://docs.spawn.dev) -**Stop treating your database like a script runner. Start treating it like a codebase.** +**Treat your database code like a codebase.** I like to lean heavily on the database. I don't like tools that abstract away the raw power of databases like PostgreSQL. Spawn is designed for developers who want to use the full breadth of modern database features: Functions, Views, Triggers, RLS – while keeping the maintenance nightmares to a minimum. @@ -243,7 +243,7 @@ Spawn supports **Provider Commands** – configure it to use `gcloud`, `aws`, or ```toml # spawn.toml -[databases.prod] +[targets.prod] command = { kind = "provider", provider = ["gcloud", "compute", "ssh", "--dry-run", ...], diff --git a/docs/src/components/cli-options.ts b/docs/src/components/cli-options.ts index ebf7f9d..98200ae 100644 --- a/docs/src/components/cli-options.ts +++ b/docs/src/components/cli-options.ts @@ -12,11 +12,11 @@ export const globalOptions: CLIOption[] = [ { flag: "-d, --debug", description: "Turn on debug output." }, ]; -/** The --database flag. Relevant to commands that read or validate the database config. */ -export const databaseOption: CLIOption[] = [ +/** The --target flag. Relevant to commands that read or validate the target config. */ +export const targetOption: CLIOption[] = [ { - flag: "--database ", - description: "Select which database from spawn.toml to use.", + flag: "--target ", + description: "Select which target from spawn.toml to use.", }, ]; @@ -24,7 +24,7 @@ export const databaseOption: CLIOption[] = [ export const environmentOption: CLIOption[] = [ { flag: "-e, --environment ", - description: "Override the environment for the database config.", + description: "Override the environment for the target config.", }, ]; diff --git a/docs/src/content/docs/cli/check.mdx b/docs/src/content/docs/cli/check.mdx index c57355a..6f161b1 100644 --- a/docs/src/content/docs/cli/check.mdx +++ b/docs/src/content/docs/cli/check.mdx @@ -4,11 +4,11 @@ description: Check your project for potential issues. --- import CLICommand from "../../../components/CLICommand.astro"; -import { globalOptions, databaseOption } from "../../../components/cli-options"; +import { globalOptions, targetOption } from "../../../components/cli-options"; ", description: "Reason for adoption (recorded in history)" }, ...environmentOption, - ...databaseOption, + ...targetOption, ...globalOptions ]} > @@ -32,6 +36,7 @@ Records a migration as applied in `_spawn.migration_history` without executing t ## Behavior Adoption creates a history entry with: + - Status: `success` - Activity: `ADOPT` - Optional description explaining why it was adopted diff --git a/docs/src/content/docs/cli/migration-apply.mdx b/docs/src/content/docs/cli/migration-apply.mdx index f9e3cb8..35d5f0e 100644 --- a/docs/src/content/docs/cli/migration-apply.mdx +++ b/docs/src/content/docs/cli/migration-apply.mdx @@ -6,7 +6,7 @@ description: Apply migrations to the database. import CLICommand from "../../../components/CLICommand.astro"; import { globalOptions, - databaseOption, + targetOption, environmentOption, variablesOption, } from "../../../components/cli-options"; @@ -19,7 +19,7 @@ import { { flag: "--yes", description: "Skip confirmation prompt" }, { flag: "--retry", description: "Retry a previous migration" }, ...environmentOption, - ...databaseOption, + ...targetOption, ...globalOptions ]} > diff --git a/docs/src/content/docs/cli/migration-build.mdx b/docs/src/content/docs/cli/migration-build.mdx index b256367..59cb6ca 100644 --- a/docs/src/content/docs/cli/migration-build.mdx +++ b/docs/src/content/docs/cli/migration-build.mdx @@ -6,7 +6,7 @@ description: Build a migration into final SQL. import CLICommand from "../../../components/CLICommand.astro"; import { globalOptions, - databaseOption, + targetOption, environmentOption, variablesOption, } from "../../../components/cli-options"; @@ -17,7 +17,7 @@ import { { flag: "--pinned", description: "Use pinned component versions from lock.toml" }, ...variablesOption, ...environmentOption, - ...databaseOption, + ...targetOption, ...globalOptions ]} > diff --git a/docs/src/content/docs/cli/migration-new.mdx b/docs/src/content/docs/cli/migration-new.mdx index 3cb2f2a..10714a0 100644 --- a/docs/src/content/docs/cli/migration-new.mdx +++ b/docs/src/content/docs/cli/migration-new.mdx @@ -4,11 +4,15 @@ description: Create a new migration. --- import CLICommand from "../../../components/CLICommand.astro"; -import { globalOptions, databaseOption, environmentOption } from "../../../components/cli-options"; +import { + globalOptions, + targetOption, + environmentOption, +} from "../../../components/cli-options"; Creates a new timestamped migration directory with a template `up.sql` file. diff --git a/docs/src/content/docs/cli/migration-pin.mdx b/docs/src/content/docs/cli/migration-pin.mdx index fdb92c0..eb46f97 100644 --- a/docs/src/content/docs/cli/migration-pin.mdx +++ b/docs/src/content/docs/cli/migration-pin.mdx @@ -4,11 +4,15 @@ description: Pin a migration to current component versions. --- import CLICommand from "../../../components/CLICommand.astro"; -import { globalOptions, databaseOption, environmentOption } from "../../../components/cli-options"; +import { + globalOptions, + targetOption, + environmentOption, +} from "../../../components/cli-options"; Creates a snapshot of all components referenced by the migration and stores them in the content-addressable `pinned/` directory. Writes a `lock.toml` file in the migration folder with the snapshot hash. diff --git a/docs/src/content/docs/cli/migration-status.mdx b/docs/src/content/docs/cli/migration-status.mdx index a5a3e28..9231c35 100644 --- a/docs/src/content/docs/cli/migration-status.mdx +++ b/docs/src/content/docs/cli/migration-status.mdx @@ -4,11 +4,15 @@ description: Show migration status across filesystem and database. --- import CLICommand from "../../../components/CLICommand.astro"; -import { globalOptions, databaseOption, environmentOption } from "../../../components/cli-options"; +import { + globalOptions, + targetOption, + environmentOption, +} from "../../../components/cli-options"; Displays a table showing the status of all migrations from both the filesystem and database. diff --git a/docs/src/content/docs/cli/test-build.mdx b/docs/src/content/docs/cli/test-build.mdx index d825ede..9edb689 100644 --- a/docs/src/content/docs/cli/test-build.mdx +++ b/docs/src/content/docs/cli/test-build.mdx @@ -4,11 +4,11 @@ description: Build a test into final SQL. --- import CLICommand from "../../../components/CLICommand.astro"; -import { globalOptions, databaseOption } from "../../../components/cli-options"; +import { globalOptions, targetOption } from "../../../components/cli-options"; Renders a test's `test.sql` template into final SQL, resolving component includes and template variables. Outputs to stdout. diff --git a/docs/src/content/docs/cli/test-compare.mdx b/docs/src/content/docs/cli/test-compare.mdx index 68c6023..4cfb9db 100644 --- a/docs/src/content/docs/cli/test-compare.mdx +++ b/docs/src/content/docs/cli/test-compare.mdx @@ -4,11 +4,11 @@ description: Run tests and compare output to expected results. --- import CLICommand from "../../../components/CLICommand.astro"; -import { globalOptions, databaseOption } from "../../../components/cli-options"; +import { globalOptions, targetOption } from "../../../components/cli-options"; Runs tests and compares their output against saved expected results, reporting any differences. diff --git a/docs/src/content/docs/cli/test-expect.mdx b/docs/src/content/docs/cli/test-expect.mdx index 61e57e4..82bc5cf 100644 --- a/docs/src/content/docs/cli/test-expect.mdx +++ b/docs/src/content/docs/cli/test-expect.mdx @@ -4,11 +4,11 @@ description: Update expected test output. --- import CLICommand from "../../../components/CLICommand.astro"; -import { globalOptions, databaseOption } from "../../../components/cli-options"; +import { globalOptions, targetOption } from "../../../components/cli-options"; Runs a test and saves its output as the new expected result for future comparisons. diff --git a/docs/src/content/docs/cli/test-new.mdx b/docs/src/content/docs/cli/test-new.mdx index 64466c7..568a193 100644 --- a/docs/src/content/docs/cli/test-new.mdx +++ b/docs/src/content/docs/cli/test-new.mdx @@ -4,11 +4,11 @@ description: Create a new test. --- import CLICommand from "../../../components/CLICommand.astro"; -import { globalOptions, databaseOption } from "../../../components/cli-options"; +import { globalOptions, targetOption } from "../../../components/cli-options"; Creates a new test directory with a template `test.sql` file. diff --git a/docs/src/content/docs/cli/test-run.mdx b/docs/src/content/docs/cli/test-run.mdx index da3c9e9..a3bd3c3 100644 --- a/docs/src/content/docs/cli/test-run.mdx +++ b/docs/src/content/docs/cli/test-run.mdx @@ -4,11 +4,11 @@ description: Run tests against the database. --- import CLICommand from "../../../components/CLICommand.astro"; -import { globalOptions, databaseOption } from "../../../components/cli-options"; +import { globalOptions, targetOption } from "../../../components/cli-options"; Executes one or all tests against the configured database and displays the results. diff --git a/docs/src/content/docs/getting-started/magic.mdx b/docs/src/content/docs/getting-started/magic.mdx index e147add..2432d6b 100644 --- a/docs/src/content/docs/getting-started/magic.mdx +++ b/docs/src/content/docs/getting-started/magic.mdx @@ -582,11 +582,11 @@ $$ LANGUAGE plpgsql; COMMIT; ``` -But if we edit the database in `spawn.toml` to specify that it is a dev environment, like so: +But if we edit the target in `spawn.toml` to specify that it is a dev environment, like so: ```toml {6} ... -[databases.postgres_psql] +[targets.postgres_psql] engine = "postgres-psql" spawn_database = "postgres" spawn_schema = "_spawn" diff --git a/docs/src/content/docs/guides/manage-databases.md b/docs/src/content/docs/guides/manage-databases.md index f841407..0d9d7f1 100644 --- a/docs/src/content/docs/guides/manage-databases.md +++ b/docs/src/content/docs/guides/manage-databases.md @@ -3,15 +3,15 @@ title: Database Connections description: How to configure database connections in spawn.toml --- -Spawn requires a database connection to apply migrations and run tests. Database connections are configured in your `spawn.toml` file under the `[databases]` section. See the [configuration reference](/reference/config/#database-configurations) for the full list of database fields. +Spawn requires a database connection to apply migrations and run tests. Database connections are configured in your `spawn.toml` file under the `[targets]` section. See the [configuration reference](/reference/config/#target-configurations) for the full list of target fields. ## Basic Configuration -Each database configuration requires: +Each target configuration requires: - `engine`: The database engine type (currently only `"postgres-psql"`) -- `spawn_database`: The database name to connect to -- `spawn_schema`: The schema where spawn stores migration tracking (default: `"_spawn"`) +- `spawn_database`: The database where spawn stores migration tracking (defaults to using whichever database your command connects to by default). This database must already exist. +- `spawn_schema`: The schema where spawn stores migration tracking (default: `"_spawn"`). This schema will be created if it does not exist. - `environment`: Environment name (e.g., `"dev"`, `"prod"`) - `command`: How to execute SQL commands (see below) @@ -24,7 +24,7 @@ The `command` field specifies how spawn should execute SQL against your database Use a direct command when you have a straightforward way to connect to your database. ```toml -[databases.local] +[targets.local] engine = "postgres-psql" spawn_database = "myapp" spawn_schema = "_spawn" @@ -37,7 +37,7 @@ command = { kind = "direct", direct = ["psql", "-U", "postgres", "myapp"] } For databases running in Docker: ```toml -[databases.docker_local] +[targets.docker_local] engine = "postgres-psql" spawn_database = "myapp" spawn_schema = "_spawn" @@ -56,7 +56,7 @@ command = { Provider commands are useful when the connection details need to be resolved dynamically, such as with cloud providers where connection setup is slow but the underlying connection is fast. ```toml -[databases.staging] +[targets.staging] engine = "postgres-psql" spawn_database = "myapp" spawn_schema = "_spawn" @@ -93,7 +93,7 @@ The provider pattern resolves the SSH command once using `gcloud compute ssh --d You can use the gcloud command directly, but this will be called multiple times during a single `spawn migration apply` command. ```toml -[databases.staging_slow] +[targets.staging_slow] engine = "postgres-psql" spawn_database = "myapp" spawn_schema = "_spawn" @@ -118,7 +118,7 @@ command = { You can also use gcloud to provide the underlying ssh command needed to connect, which will be resolved just once, and then every connection to the database will use the provided ssh command directly. ```toml -[databases.staging] +[targets.staging] engine = "postgres-psql" spawn_database = "myapp" spawn_schema = "_spawn" @@ -178,10 +178,10 @@ ssh-add ~/.ssh/your_key ```toml # spawn.toml spawn_folder = "spawn" -database = "local" # Default database +target = "local" # Default target # Local development (Docker) -[databases.local] +[targets.local] engine = "postgres-psql" spawn_database = "myapp" spawn_schema = "_spawn" @@ -192,7 +192,7 @@ command = { } # Staging (Google Cloud via provider - fast!) -[databases.staging] +[targets.staging] engine = "postgres-psql" spawn_database = "myapp" spawn_schema = "_spawn" @@ -210,7 +210,7 @@ command = { } # Production (Google Cloud via provider - fast!) -[databases.production] +[targets.production] engine = "postgres-psql" spawn_database = "myapp" spawn_schema = "_spawn" @@ -231,28 +231,28 @@ command = { Usage: ```bash -# Use local database (default) +# Use local target (default) spawn migration apply -# Use staging database -spawn --database staging migration apply +# Use staging target +spawn --target staging migration apply -# Use production database -spawn --database production migration apply +# Use production target +spawn --target production migration apply ``` ## Advanced Configuration -### Multiple Databases +### Multiple Targets -You can configure multiple databases and switch between them: +You can configure multiple targets and switch between them: ```bash -# Use specific database -spawn --database staging migration build my-migration +# Use specific target +spawn --target staging migration build my-migration # Override in environment variable -export SPAWN_DATABASE=production +export SPAWN_TARGET=production spawn migration apply ``` diff --git a/docs/src/content/docs/reference/config.md b/docs/src/content/docs/reference/config.md index 02d0ea5..f48f582 100644 --- a/docs/src/content/docs/reference/config.md +++ b/docs/src/content/docs/reference/config.md @@ -33,22 +33,22 @@ This would expect the following directory layout: - `./database/spawn/tests/` - `./database/spawn/pinned/` -### `database` +### `target` **Type:** String **Required:** No **Default:** None -The default database to use for commands. Must match a key in `[databases]`. +The default target to use for commands. Must match a key in `[targets]`. ```toml -database = "local" +target = "local" ``` -Override per-command with `--database`: +Override per-command with `--target`: ```bash -spawn --database production migration status +spawn --target production migration status ``` ### `environment` @@ -57,13 +57,13 @@ spawn --database production migration status **Required:** No **Default:** None -Global environment override. Overrides the `environment` field in database configs. +Global environment override. Overrides the `environment` field in target configs. ```toml environment = "dev" ``` -This is rarely set at the top level. Usually each database defines its own environment. +This is rarely set at the top level. Usually each target defines its own environment. ### `project_id` @@ -91,9 +91,9 @@ telemetry = false Set the `DO_NOT_TRACK` environment variable to disable telemetry globally. -## Database configurations +## Target configurations -The `[databases]` section defines one or more database connections. Each database is a table with the following fields. For practical setup examples including Docker and Google Cloud SQL, see the [Database Connections guide](/guides/manage-databases/). +The `[targets]` section defines one or more database connections. Each target is a table with the following fields. For practical setup examples including Docker and Google Cloud SQL, see the [Database Connections guide](/guides/manage-databases/). ### `engine` @@ -104,16 +104,16 @@ The `[databases]` section defines one or more database connections. Each databas The database engine type. Currently only PostgreSQL via psql is supported. ```toml -[databases.local] +[targets.local] engine = "postgres-psql" ``` ### `spawn_database` **Type:** String -**Required:** Yes +**Required:** No -The database name where Spawn stores migration tracking tables (in the `spawn_schema`). +The database name where Spawn stores migration tracking tables (in the `spawn_schema`). If not provided, defaults to using the same database that your connection command uses. This database must already exist. ```toml spawn_database = "spawn" @@ -125,7 +125,7 @@ spawn_database = "spawn" **Required:** No **Default:** `"_spawn"` -The schema where Spawn creates its internal tracking tables (`migration_history`, etc.). +The schema where Spawn creates its internal tracking tables (`migration_history`, etc.). This schema will be created if it does not yet exist. ```toml spawn_schema = "_spawn" @@ -156,7 +156,7 @@ environment = "dev" **Type:** Table (CommandSpec) **Required:** Yes -Specifies how to execute SQL against the database. Two modes: `direct` and `provider`. For now, only connection via PostgreSQL psql is supported, so this should be the command that allows piping changes to the database. See the [Database Connections guide](/guides/manage-databases/#command-configuration) for detailed examples of both modes. +Specifies how to execute SQL against the target. Two modes: `direct` and `provider`. For now, only connection via PostgreSQL psql is supported, so this should be the command that allows piping changes to the database. See the [Database Connections guide](/guides/manage-databases/#command-configuration) for detailed examples of both modes. #### Direct command @@ -204,10 +204,10 @@ The `--dry-run` flag makes `gcloud` output the SSH command as a string instead o ```toml spawn_folder = "./database/spawn" -database = "local" +target = "local" project_id = -[databases.local] +[targets.local] spawn_database = "spawn" spawn_schema = "_spawn" environment = "dev" @@ -217,7 +217,7 @@ command = { direct = ["docker", "exec", "-i", "mydb", "psql", "-U", "postgres", "postgres"] } -[databases.staging] +[targets.staging] spawn_database = "spawn" spawn_schema = "_spawn" engine = "postgres-psql" @@ -233,7 +233,7 @@ command = { append = ["-T", "sudo", "-u", "postgres", "psql", "mydb"] } -[databases.production] +[targets.production] spawn_database = "spawn" spawn_schema = "_spawn" engine = "postgres-psql" @@ -255,8 +255,8 @@ command = { Spawn supports environment variable overrides with the `SPAWN_` prefix: ```bash -export SPAWN_DATABASE=production +export SPAWN_TARGET=production spawn migration status ``` -This is equivalent to `spawn --database production migration status`. +This is equivalent to `spawn --target production migration status`. diff --git a/docs/src/content/docs/reference/roadmap.md b/docs/src/content/docs/reference/roadmap.md index 377c5c9..ff9a395 100644 --- a/docs/src/content/docs/reference/roadmap.md +++ b/docs/src/content/docs/reference/roadmap.md @@ -34,7 +34,7 @@ This is a high-level overview of where Spawn is headed. Items here are subject t - ✅ **TOML configuration** - `spawn.toml` for project settings - ✅ **Organized folder structure** - `/components`, `/migrations`, `/tests`, `/pinned` -- ✅ **Database targeting** - `--database` flag for multiple database configurations +- ✅ **Target selection** - `--target` flag for multiple target configurations - ✅ **PostgreSQL focus** - Optimized for PostgreSQL features and workflows ## Roadmap diff --git a/docs/src/content/docs/reference/templating.md b/docs/src/content/docs/reference/templating.md index bbba6a7..e066499 100644 --- a/docs/src/content/docs/reference/templating.md +++ b/docs/src/content/docs/reference/templating.md @@ -21,7 +21,7 @@ Templates are used in: ### `env` -The environment from the database config (e.g., `"dev"`, `"prod"`). +The environment from the target config (e.g., `"dev"`, `"prod"`). ```sql {% if env == "dev" %} @@ -223,7 +223,7 @@ SELECT 1; ``` :::note -These parse filters complement the `--variables` CLI flag. Use `--variables` to pass a single variables file into the `variables` context. This is intended for situations where you want to provide data that is specific to a particular database target, or contains information that should not be committed to your repo. Use `read_file` with a parse filter when you need to load additional structured data from `components/`, either for tests or data that is applicable to all database targets. +These parse filters complement the `--variables` CLI flag. Use `--variables` to pass a single variables file into the `variables` context. This is intended for situations where you want to provide data that is specific to a particular target, or contains information that should not be committed to your repo. Use `read_file` with a parse filter when you need to load additional structured data from `components/`, either for tests or data that is applicable to all targets. ::: ### `escape_identifier` diff --git a/mise.toml b/mise.toml index db8dcba..eac2aa0 100644 --- a/mise.toml +++ b/mise.toml @@ -9,6 +9,10 @@ pnpm = "10" # Pinned to major version # Ensure pnpm uses local node_modules _.path = ["./node_modules/.bin"] +[tasks.test] +description = "Run unit tests then integration tests" +run = ["cargo test", "cargo test -- --ignored"] + [tasks.docs] description = "Run the documentation dev server" dir = "docs" diff --git a/src/cli.rs b/src/cli.rs index cb9c20b..c526402 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -20,7 +20,7 @@ pub struct Cli { pub config_file: String, #[arg(global = true, long)] - pub database: Option, + pub target: Option, /// Internal flag for telemetry child process (hidden) #[arg(long, hide = true)] @@ -256,8 +256,7 @@ pub async fn run_cli(cli: Cli, base_op: &Operator) -> CliResult { let config_exists = base_op.exists(&cli.config_file).await.unwrap_or(false); // Load config from file (required for all other commands) - let mut main_config = match Config::load(&cli.config_file, base_op, cli.database.clone()).await - { + let mut main_config = match Config::load(&cli.config_file, base_op, cli.target.clone()).await { Ok(cfg) => cfg, Err(e) => { // If config doesn't exist, show helpful message diff --git a/src/commands/check.rs b/src/commands/check.rs index 5212cd7..e3a12c8 100644 --- a/src/commands/check.rs +++ b/src/commands/check.rs @@ -14,9 +14,9 @@ impl TelemetryDescribe for Check { impl Command for Check { async fn execute(&self, config: &Config) -> Result { - // Validate the database reference if one was provided - if config.database.is_some() { - config.db_config()?; + // Validate the target reference if one was provided + if config.target.is_some() { + config.target_config()?; } let mut warnings: Vec = Vec::new(); diff --git a/src/commands/init.rs b/src/commands/init.rs index 5197d9e..bee3131 100644 --- a/src/commands/init.rs +++ b/src/commands/init.rs @@ -1,6 +1,6 @@ use crate::commands::{Outcome, TelemetryDescribe, TelemetryInfo}; use crate::config::ConfigLoaderSaver; -use crate::engine::{CommandSpec, DatabaseConfig, EngineType}; +use crate::engine::{CommandSpec, EngineType, TargetConfig}; use anyhow::{anyhow, Result}; use opendal::Operator; use std::collections::HashMap; @@ -50,13 +50,13 @@ impl Init { "postgres-db".to_string() }; - // Create example database config - let mut databases = HashMap::new(); - databases.insert( + // Create example target config + let mut targets = HashMap::new(); + targets.insert( "postgres_psql".to_string(), - DatabaseConfig { + TargetConfig { engine: EngineType::PostgresPSQL, - spawn_database: db_name.clone(), + spawn_database: Some(db_name.clone()), spawn_schema: "_spawn".to_string(), environment: "dev".to_string(), command: Some(CommandSpec::Direct { @@ -77,9 +77,9 @@ impl Init { // Create default config let config = ConfigLoaderSaver { spawn_folder: "spawn".to_string(), - database: Some("postgres_psql".to_string()), + target: Some("postgres_psql".to_string()), environment: None, - databases: Some(databases), + targets: Some(targets), project_id: Some(project_id.clone()), telemetry: None, }; diff --git a/src/commands/migration/mod.rs b/src/commands/migration/mod.rs index 2ae2277..24dcf93 100644 --- a/src/commands/migration/mod.rs +++ b/src/commands/migration/mod.rs @@ -106,9 +106,9 @@ pub async fn get_pending_and_confirm( return Ok(None); } - let db_config = config.db_config()?; - let target = config.database.as_deref().unwrap_or("unknown"); - let env = &db_config.environment; + let target_config = config.target_config()?; + let target = config.target.as_deref().unwrap_or("unknown"); + let env = &target_config.environment; println!(); println!("TARGET: {}", target); diff --git a/src/config.rs b/src/config.rs index c6f9f2b..47bdff7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,4 +1,4 @@ -use crate::engine::{postgres_psql::PSQL, DatabaseConfig, Engine, EngineType}; +use crate::engine::{postgres_psql::PSQL, Engine, EngineType, TargetConfig}; use crate::pinfile::LockData; use crate::variables::Variables; use anyhow::{anyhow, Context, Result}; @@ -13,9 +13,9 @@ static PINFILE_LOCK_NAME: &str = "lock.toml"; #[derive(Clone, Debug, Deserialize, Serialize)] pub struct ConfigLoaderSaver { pub spawn_folder: String, - pub database: Option, + pub target: Option, pub environment: Option, - pub databases: Option>, + pub targets: Option>, /// Unique project identifier for telemetry (UUID string) pub project_id: Option, /// Set to false to disable telemetry @@ -32,9 +32,9 @@ impl ConfigLoaderSaver { pub fn build(self, base_fs: Operator, spawn_fs: Option) -> Config { Config { spawn_folder: self.spawn_folder, - database: self.database, + target: self.target, environment: self.environment, - databases: self.databases.unwrap_or_default(), + targets: self.targets.unwrap_or_default(), project_id: self.project_id, telemetry: self.telemetry.unwrap_or(true), base_fs, @@ -45,7 +45,7 @@ impl ConfigLoaderSaver { pub async fn load( path: &str, op: &Operator, - database: Option, + target: Option, ) -> Result { let bytes = op .read(path) @@ -76,7 +76,7 @@ impl ConfigLoaderSaver { // Add in settings from the environment (with a prefix of APP) // Eg.. `APP_DEBUG=1 ./target/app` would set the `debug` key .add_source(config::Environment::with_prefix("SPAWN")) - .set_override_option("database", database)? + .set_override_option("target", target)? .set_default("environment", "prod") .context("could not set default environment")? .build()? @@ -165,9 +165,9 @@ impl FolderPather { #[derive(Debug, Clone)] pub struct Config { spawn_folder: String, - pub database: Option, - pub environment: Option, // Override the environment for the db config - pub databases: HashMap, + pub target: Option, + pub environment: Option, // Override the environment for the target config + pub targets: HashMap, /// Unique project identifier for telemetry (UUID string) pub project_id: Option, /// Whether telemetry is enabled in config @@ -190,22 +190,19 @@ impl Config { } pub async fn new_engine(&self) -> Result> { - let db_config = self.db_config()?; + let target_config = self.target_config()?; - match db_config.engine { - EngineType::PostgresPSQL => Ok(PSQL::new(&db_config).await?), + match target_config.engine { + EngineType::PostgresPSQL => Ok(PSQL::new(&target_config).await?), } } - pub fn db_config(&self) -> Result { - let db_name = self - .database - .as_ref() - .ok_or(anyhow!("no database selected"))?; + pub fn target_config(&self) -> Result { + let target_name = self.target.as_ref().ok_or(anyhow!("no target selected"))?; let mut conf = self - .databases - .get(db_name) - .ok_or(anyhow!("no database defined with name '{}'", db_name,))? + .targets + .get(target_name) + .ok_or(anyhow!("no target defined with name '{}'", target_name,))? .clone(); if let Some(env) = &self.environment { @@ -215,8 +212,8 @@ impl Config { Ok(conf) } - pub async fn load(path: &str, op: &Operator, database: Option) -> Result { - let config_loader = ConfigLoaderSaver::load(path, op, database).await?; + pub async fn load(path: &str, op: &Operator, target: Option) -> Result { + let config_loader = ConfigLoaderSaver::load(path, op, target).await?; Ok(config_loader.build(op.clone(), None)) } diff --git a/src/engine/mod.rs b/src/engine/mod.rs index 4b4d0c3..17cb180 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -231,9 +231,9 @@ pub enum CommandSpec { } #[derive(Clone, Debug, Deserialize, Serialize)] -pub struct DatabaseConfig { +pub struct TargetConfig { pub engine: EngineType, - pub spawn_database: String, + pub spawn_database: Option, #[serde(default = "default_schema")] pub spawn_schema: String, #[serde(default = "default_environment")] diff --git a/src/engine/postgres_psql.rs b/src/engine/postgres_psql.rs index e9c96b8..823159f 100644 --- a/src/engine/postgres_psql.rs +++ b/src/engine/postgres_psql.rs @@ -4,9 +4,9 @@ use crate::config::FolderPather; use crate::engine::{ - resolve_command_spec, DatabaseConfig, Engine, EngineError, ExistingMigrationInfo, - MigrationActivity, MigrationError, MigrationHistoryStatus, MigrationResult, MigrationStatus, - StdoutWriter, WriterFn, + resolve_command_spec, Engine, EngineError, ExistingMigrationInfo, MigrationActivity, + MigrationError, MigrationHistoryStatus, MigrationResult, MigrationStatus, StdoutWriter, + TargetConfig, WriterFn, }; use crate::escape::{EscapedIdentifier, EscapedLiteral, EscapedQuery, InsecureRawSql}; use crate::sql_query; @@ -35,27 +35,24 @@ pub fn migration_lock_key() -> i64 { #[derive(Debug)] pub struct PSQL { psql_command: Vec, - /// Schema name as an escaped identifier (for use as schema.table) - spawn_schema: String, - db_config: DatabaseConfig, + target_config: TargetConfig, } static PROJECT_DIR: Dir<'_> = include_dir!("./static/engine-migrations/postgres-psql"); static SPAWN_NAMESPACE: &str = "spawn"; impl PSQL { - pub async fn new(config: &DatabaseConfig) -> Result> { + pub async fn new(config: &TargetConfig) -> Result> { let command_spec = config .command .clone() - .ok_or(anyhow!("Command for database config must be defined"))?; + .ok_or(anyhow!("Command for target config must be defined"))?; let psql_command = resolve_command_spec(command_spec).await?; let eng = Box::new(Self { psql_command, - spawn_schema: config.spawn_schema.clone(), - db_config: config.clone(), + target_config: config.clone(), }); // Ensure we have latest schema: @@ -67,16 +64,106 @@ impl PSQL { } fn spawn_schema_literal(&self) -> EscapedLiteral { - EscapedLiteral::new(&self.spawn_schema) + EscapedLiteral::new(&self.target_config.spawn_schema) } fn spawn_schema_ident(&self) -> EscapedIdentifier { - EscapedIdentifier::new(&self.spawn_schema) + EscapedIdentifier::new(&self.target_config.spawn_schema) } fn safe_spawn_namespace(&self) -> EscapedLiteral { EscapedLiteral::new(SPAWN_NAMESPACE) } + + /// Returns a psql `\c` command to switch to the given database, or + /// an empty string if `database` is None. + fn db_connect_command(database: Option<&str>) -> InsecureRawSql { + if let Some(db) = database { + InsecureRawSql::new(&format!("\\c {}\n", EscapedIdentifier::new(db))) + } else { + InsecureRawSql::new("") + } + } + + /// Returns a psql `\c` command to switch to the spawn_database, if configured. + /// Used to ensure internal queries and schema migrations target the correct database. + fn spawn_db_connect_command(&self) -> InsecureRawSql { + Self::db_connect_command(self.target_config.spawn_database.as_deref()) + } + + fn build_record_migration_sql( + &self, + migration_name: &str, + namespace: &EscapedLiteral, + status: MigrationStatus, + activity: MigrationActivity, + checksum: Option<&str>, + execution_time: Option, + pin_hash: Option<&str>, + description: Option<&str>, + ) -> EscapedQuery { + let safe_migration_name = EscapedLiteral::new(migration_name); + let safe_status = EscapedLiteral::new(status.as_str()); + let safe_activity = EscapedLiteral::new(activity.as_str()); + let safe_description = EscapedLiteral::new(description.unwrap_or("")); + // If no checksum provided, use empty bytea (decode returns empty bytea for empty string) + let checksum_expr = checksum + .map(|c| format!("decode('{}', 'hex')", c)) + .unwrap_or_else(|| "decode('', 'hex')".to_string()); + let checksum_raw = InsecureRawSql::new(&checksum_expr); + let safe_pin_hash = pin_hash.map(|h| EscapedLiteral::new(h)); + + let duration_interval = execution_time + .map(|d| InsecureRawSql::new(&format!("INTERVAL '{} second'", d))) + .unwrap_or_else(|| InsecureRawSql::new("INTERVAL '0 second'")); + + let qry = sql_query!( + r#" + {} + BEGIN; + WITH inserted_migration AS ( + INSERT INTO {}.migration (name, namespace) VALUES ({}, {}) + ON CONFLICT (name, namespace) DO UPDATE SET name = EXCLUDED.name + RETURNING migration_id + ) + INSERT INTO {}.migration_history ( + migration_id_migration, + activity_id_activity, + created_by, + description, + status_note, + status_id_status, + checksum, + execution_time, + pin_hash + ) + SELECT + migration_id, + {}, + 'unused', + {}, + '', + {}, + {}, + {}, + {} + FROM inserted_migration; + COMMIT; + "#, + self.spawn_db_connect_command(), + self.spawn_schema_ident(), + safe_migration_name, + namespace, + self.spawn_schema_ident(), + safe_activity, + safe_description, + safe_status, + checksum_raw, + duration_interval, + safe_pin_hash, + ); + qry + } } #[async_trait] @@ -307,7 +394,11 @@ impl Engine for PSQL { ); let output = self - .execute_sql(&query, Some("unaligned")) + .execute_sql( + &query, + Some("unaligned"), + self.target_config.spawn_database.as_deref(), + ) .await .map_err(MigrationError::Database)?; @@ -420,80 +511,6 @@ impl Write for TeeWriter { } } -/// Free function to build the SQL query for recording a migration. -/// This can be used from both async and sync contexts. -fn build_record_migration_sql( - spawn_schema: &str, - migration_name: &str, - namespace: &EscapedLiteral, - status: MigrationStatus, - activity: MigrationActivity, - checksum: Option<&str>, - execution_time: Option, - pin_hash: Option<&str>, - description: Option<&str>, -) -> EscapedQuery { - let schema_ident = EscapedIdentifier::new(spawn_schema); - let safe_migration_name = EscapedLiteral::new(migration_name); - let safe_status = EscapedLiteral::new(status.as_str()); - let safe_activity = EscapedLiteral::new(activity.as_str()); - let safe_description = EscapedLiteral::new(description.unwrap_or("")); - // If no checksum provided, use empty bytea (decode returns empty bytea for empty string) - let checksum_expr = checksum - .map(|c| format!("decode('{}', 'hex')", c)) - .unwrap_or_else(|| "decode('', 'hex')".to_string()); - let checksum_raw = InsecureRawSql::new(&checksum_expr); - let safe_pin_hash = pin_hash.map(|h| EscapedLiteral::new(h)); - - let duration_interval = execution_time - .map(|d| InsecureRawSql::new(&format!("INTERVAL '{} second'", d))) - .unwrap_or_else(|| InsecureRawSql::new("INTERVAL '0 second'")); - - sql_query!( - r#" -BEGIN; -WITH inserted_migration AS ( - INSERT INTO {}.migration (name, namespace) VALUES ({}, {}) - ON CONFLICT (name, namespace) DO UPDATE SET name = EXCLUDED.name - RETURNING migration_id -) -INSERT INTO {}.migration_history ( - migration_id_migration, - activity_id_activity, - created_by, - description, - status_note, - status_id_status, - checksum, - execution_time, - pin_hash -) -SELECT - migration_id, - {}, - 'unused', - {}, - '', - {}, - {}, - {}, - {} -FROM inserted_migration; -COMMIT; -"#, - schema_ident, - safe_migration_name, - namespace, - schema_ident, - safe_activity, - safe_description, - safe_status, - checksum_raw, - duration_interval, - safe_pin_hash, - ) -} - impl PSQL { pub async fn update_schema(&self) -> Result<()> { // Create a memory operator from the included directory containing @@ -537,8 +554,8 @@ impl PSQL { .await .context("Failed to load config for postgres psql")?; let dbengtype = "psql".to_string(); - cfg.database = Some(dbengtype.clone()); - cfg.databases = HashMap::from([(dbengtype, self.db_config.clone())]); + cfg.target = Some(dbengtype.clone()); + cfg.targets = HashMap::from([(dbengtype, self.target_config.clone())]); // Apply each migration that hasn't been applied yet for migration_path in available_migrations { @@ -559,7 +576,7 @@ impl PSQL { // Load and render the migration let variables = crate::variables::Variables::from_str( "json", - &serde_json::json!({"schema": &self.spawn_schema}).to_string(), + &serde_json::json!({"schema": &self.target_config.spawn_schema}).to_string(), )?; let gen = migrator.generate_streaming(Some(variables)).await?; let mut buffer = Vec::new(); @@ -570,8 +587,13 @@ impl PSQL { // Apply the migration and record it // Note: even for bootstrap, the first migration creates the tables, // so they exist by the time we record the migration. - let write_fn: WriterFn = - Box::new(move |writer: &mut dyn Write| writer.write_all(content.as_bytes())); + // Prefix with \c to spawn_database if configured, so the internal + // schema is created in the correct database. + let db_connect = self.spawn_db_connect_command(); + let write_fn: WriterFn = Box::new(move |writer: &mut dyn Write| { + writer.write_all(db_connect.as_str().as_bytes())?; + writer.write_all(content.as_bytes()) + }); match self .apply_and_record_migration_v1( migration_name, @@ -595,9 +617,16 @@ impl PSQL { /// Execute SQL and return stdout as a String. /// Used for internal queries where we need to parse results. - async fn execute_sql(&self, query: &EscapedQuery, format: Option<&str>) -> Result { + /// If `database` is Some, a `\c` command is prepended to switch databases first. + async fn execute_sql( + &self, + query: &EscapedQuery, + format: Option<&str>, + database: Option<&str>, + ) -> Result { let query_str = query.as_str().to_string(); let format_owned = format.map(|s| s.to_string()); + let db_connect = Self::db_connect_command(database); // Create a shared buffer to capture stdout let stdout_buf = Arc::new(Mutex::new(Vec::new())); @@ -605,6 +634,8 @@ impl PSQL { self.execute_with_writer( Box::new(move |writer| { + // Switch database if requested + writer.write_all(db_connect.as_str().as_bytes())?; // Format settings if requested (QUIET is already set globally) if let Some(fmt) = format_owned { writer.write_all(b"\\pset tuples_only on\n")?; @@ -624,14 +655,14 @@ impl PSQL { } async fn migration_table_exists(&self) -> Result { - self.table_exists("migration").await + self.spawn_table_exists("migration").await } async fn migration_history_table_exists(&self) -> Result { - self.table_exists("migration_history").await + self.spawn_table_exists("migration_history").await } - async fn table_exists(&self, table_name: &str) -> Result { + async fn spawn_table_exists(&self, table_name: &str) -> Result { let safe_table_name = EscapedLiteral::new(table_name); // Use type-safe escaped types - escaping happens at construction time let query = sql_query!( @@ -646,7 +677,13 @@ impl PSQL { safe_table_name ); - let output = self.execute_sql(&query, Some("csv")).await?; + let output = self + .execute_sql( + &query, + Some("csv"), + self.target_config.spawn_database.as_deref(), + ) + .await?; // With tuples_only mode, output is just "t" or "f" Ok(output.trim() == "t") } @@ -661,7 +698,13 @@ impl PSQL { namespace, ); - let output = self.execute_sql(&query, Some("csv")).await?; + let output = self + .execute_sql( + &query, + Some("csv"), + self.target_config.spawn_database.as_deref(), + ) + .await?; let mut migrations = HashSet::new(); // With tuples_only mode, we get just the data rows (no headers) @@ -698,7 +741,13 @@ impl PSQL { namespace ); - let output = self.execute_sql(&query, Some("csv")).await?; + let output = self + .execute_sql( + &query, + Some("csv"), + self.target_config.spawn_database.as_deref(), + ) + .await?; // With tuples_only mode, we get just the data row (no headers). // Parse CSV: name,namespace,status_id_status,activity_id_activity,checksum @@ -742,8 +791,7 @@ impl PSQL { pin_hash: Option<&str>, description: Option<&str>, ) -> MigrationResult<()> { - let record_query = build_record_migration_sql( - &self.spawn_schema, + let record_query = self.build_record_migration_sql( migration_name, namespace, status, diff --git a/src/template.rs b/src/template.rs index d647555..968f16e 100644 --- a/src/template.rs +++ b/src/template.rs @@ -335,15 +335,15 @@ pub async fn generate_streaming( let store = Store::new(pinner, cfg.operator().clone(), cfg.pather()) .context("could not create new store for generate")?; - let db_config = cfg - .db_config() - .context("could not get db config for generate")?; + let target_config = cfg + .target_config() + .context("could not get target config for generate")?; generate_streaming_with_store( name, variables, - &db_config.environment, - &db_config.engine, + &target_config.environment, + &target_config.engine, store, ) .await diff --git a/tests/integration_postgres.rs b/tests/integration_postgres.rs index 45322ec..254995e 100644 --- a/tests/integration_postgres.rs +++ b/tests/integration_postgres.rs @@ -43,7 +43,7 @@ use opendal::Operator; use spawn_db::{ commands::{AdoptMigration, ApplyMigration, Command, CompareTests, ExpectTest, Outcome}, config::ConfigLoaderSaver, - engine::{CommandSpec, DatabaseConfig, EngineType}, + engine::{CommandSpec, EngineType, TargetConfig}, }; use std::collections::HashMap; use std::env; @@ -200,7 +200,7 @@ impl IntegrationTestHelper { } }; - // Create the database config for this test + // Create the target config for this test let config_loader = Self::create_config(&db_name, &connection_mode); // Use MigrationTestHelper for filesystem and config management @@ -218,14 +218,14 @@ impl IntegrationTestHelper { Ok(helper) } - /// Creates a database config that points to our isolated test database + /// Creates a target config that points to our isolated test database fn create_config(db_name: &str, connection_mode: &ConnectionMode) -> ConfigLoaderSaver { - let mut databases = HashMap::new(); - databases.insert( + let mut targets = HashMap::new(); + targets.insert( "postgres_psql".to_string(), - DatabaseConfig { + TargetConfig { engine: EngineType::PostgresPSQL, - spawn_database: db_name.to_string(), + spawn_database: Some(db_name.to_string()), spawn_schema: "_spawn".to_string(), environment: "test".to_string(), command: Some(CommandSpec::Direct { @@ -236,9 +236,9 @@ impl IntegrationTestHelper { ConfigLoaderSaver { spawn_folder: "/db".to_string(), - database: Some("postgres_psql".to_string()), + target: Some("postgres_psql".to_string()), environment: None, - databases: Some(databases), + targets: Some(targets), project_id: None, telemetry: Some(false), } @@ -1583,3 +1583,170 @@ COMMIT;"#; Ok(()) } + +/// Tests that spawn_database config controls where migration tracking is recorded. +/// +/// When spawn_database is set to a different database, the migration +/// SQL runs against the psql-connected database, but tracking tables are +/// recorded in the spawn_database via a \c switch. +#[tokio::test] +#[ignore] +async fn test_spawn_database_config() -> Result<()> { + require_postgres()?; + + let connection_mode = ConnectionMode::from_env(); + let keep_db = should_keep_db(); + + // We need two databases: + // - migration_db: where the migration SQL runs (psql connects here) + // - tracking_db: where spawn records migration history (spawn_database config) + let migration_db = format!("spawn_test_{}", Uuid::new_v4().simple()); + let tracking_db = format!("spawn_test_{}", Uuid::new_v4().simple()); + + println!(); + println!("======================================================="); + println!("Test: test_spawn_database_config"); + println!("Migration DB: {}", migration_db); + println!("Tracking DB: {}", tracking_db); + if keep_db { + println!("SPAWN_TEST_KEEP_DB is set - databases will be preserved"); + } + println!("======================================================="); + + IntegrationTestHelper::create_test_database(&migration_db, &connection_mode)?; + + let mem_service = Memory::default(); + let mem_op = Operator::new(mem_service)?.finish(); + + // Config: psql connects to migration_db, but spawn_database = tracking_db + let mut targets = HashMap::new(); + targets.insert( + "postgres_psql".to_string(), + TargetConfig { + engine: EngineType::PostgresPSQL, + spawn_database: Some(tracking_db.to_string()), + spawn_schema: "_spawn".to_string(), + environment: "test".to_string(), + command: Some(CommandSpec::Direct { + direct: connection_mode.psql_command(&migration_db), + }), + }, + ); + + let config_loader = ConfigLoaderSaver { + spawn_folder: "/db".to_string(), + target: Some("postgres_psql".to_string()), + environment: None, + targets: Some(targets), + project_id: None, + telemetry: Some(false), + }; + + let migration_helper = + MigrationTestHelper::new_from_operator_with_config(mem_op, config_loader).await?; + + let _ = connection_mode.execute_sql( + &migration_db, + format!("CREATE DATABASE {}", tracking_db).as_str(), + )?; + + let migration_name = migration_helper + .create_migration_manual( + "split-db-test", + r#"BEGIN; +CREATE TABLE split_db_test (id SERIAL PRIMARY KEY); +COMMIT;"# + .to_string(), + ) + .await?; + + let config = migration_helper.load_config().await?; + let cmd = ApplyMigration { + migration: Some(migration_name.clone()), + pinned: false, + variables: None, + yes: true, + retry: false, + }; + cmd.execute(&config).await?; + + // The migration SQL should have run in migration_db (the psql-connected database) + let table_check = connection_mode.execute_sql( + &migration_db, + "SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'split_db_test');", + )?; + let table_output = String::from_utf8_lossy(&table_check.stdout); + assert!( + table_output.contains(" t"), + "split_db_test table should exist in migration_db, got: {}", + table_output + ); + + // The tracking record should be in tracking_db (spawn_database), NOT migration_db + let tracking_in_target = connection_mode.execute_sql( + &tracking_db, + "SELECT COUNT(*) FROM _spawn.migration WHERE namespace = 'default';", + )?; + let tracking_target_output = String::from_utf8_lossy(&tracking_in_target.stdout); + assert!( + tracking_target_output.contains('1'), + "tracking should be in tracking_db when spawn_database is set, got: {}", + tracking_target_output + ); + + // Verify the table was NOT created in tracking_db (migration runs in migration_db) + let no_table_check = connection_mode.execute_sql( + &tracking_db, + "SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'split_db_test');", + )?; + let no_table_output = String::from_utf8_lossy(&no_table_check.stdout); + assert!( + no_table_output.contains(" f"), + "split_db_test table should NOT exist in tracking_db, got: {}", + no_table_output + ); + + // Re-apply the same migration. Because tracking is in tracking_db, + // the engine must read from tracking_db to detect it's already applied. + // If execute_sql doesn't prepend \c to the tracking database, the read + // queries hit migration_db (where _spawn tables don't exist), the + // engine won't find the prior apply, and it will re-run the SQL — + // which fails because split_db_test already exists. + let config2 = migration_helper.load_config().await?; + let cmd2 = ApplyMigration { + migration: Some(migration_name.clone()), + pinned: false, + variables: None, + yes: true, + retry: false, + }; + cmd2.execute(&config2).await.expect( + "Re-applying the same migration should succeed (detected as already applied), \ + but it failed — likely because execute_sql reads from the wrong database \ + when spawn_database is configured", + ); + + // ========================================================================= + // Cleanup + // ========================================================================= + if keep_db { + println!("KEEPING databases: {} and {}", migration_db, tracking_db); + } else { + // Terminate connections and drop both databases + for db in [&migration_db, &tracking_db] { + let _ = connection_mode.execute_sql( + DEFAULT_NEUTRAL_DATABASE, + &format!( + "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '{}'", + db + ), + ); + let _ = connection_mode.execute_sql( + DEFAULT_NEUTRAL_DATABASE, + &format!("DROP DATABASE IF EXISTS \"{}\"", db), + ); + } + } + + Ok(()) +} diff --git a/tests/migration_build.rs b/tests/migration_build.rs index 7e07107..92d5e05 100644 --- a/tests/migration_build.rs +++ b/tests/migration_build.rs @@ -6,7 +6,7 @@ use pretty_assertions::assert_eq; use spawn_db::{ commands::{BuildMigration, Check, Command, NewMigration, Outcome, PinMigration}, config::{Config, ConfigLoaderSaver}, - engine::{CommandSpec, DatabaseConfig, EngineType}, + engine::{CommandSpec, EngineType, TargetConfig}, store, }; use std::collections::HashMap; @@ -71,12 +71,12 @@ impl MigrationTestHelper { } fn default_config_loadersaver() -> ConfigLoaderSaver { - let mut databases = HashMap::new(); - databases.insert( + let mut targets = HashMap::new(); + targets.insert( "postgres_psql".to_string(), - DatabaseConfig { + TargetConfig { engine: EngineType::PostgresPSQL, - spawn_database: "spawn".to_string(), + spawn_database: Some("spawn".to_string()), spawn_schema: "public".to_string(), environment: "dev".to_string(), command: Some(CommandSpec::Direct { @@ -96,9 +96,9 @@ impl MigrationTestHelper { ConfigLoaderSaver { spawn_folder: "/db".to_string(), - database: Some("postgres_psql".to_string()), + target: Some("postgres_psql".to_string()), environment: Some("dev".to_string()), - databases: Some(databases), + targets: Some(targets), project_id: None, telemetry: Some(false), }