pgmold 0.33.5

PostgreSQL schema-as-code management tool
Documentation
mod common;
use common::*;

use pgmold::filter::Filter;
use pgmold::model::QualifiedName;
use pgmold::provider::load_schema_from_sources;

const NON_DESTRUCTIVE_LINT: LintOptions = LintOptions {
    allow_destructive: false,
    is_production: false,
};

#[test]
fn invalid_sql_produces_parse_error() {
    let result = parse_sql_string("NOT VALID SQL AT ALL");
    assert!(result.is_err());
}

#[test]
fn empty_sql_produces_empty_schema() {
    let schema = parse_sql_string("").unwrap();
    assert!(schema.tables.is_empty());
    assert!(schema.functions.is_empty());
    assert!(schema.views.is_empty());
}

#[test]
fn duplicate_table_in_sql_last_wins() {
    let result = parse_sql_string(
        "CREATE TABLE public.users (id serial PRIMARY KEY);
         CREATE TABLE public.users (id serial PRIMARY KEY, name text);",
    );
    let schema = result.unwrap();
    assert!(schema.tables.contains_key("public.users"));
}

#[test]
fn circular_fk_does_not_panic() {
    let schema_a = parse_sql_string(
        r#"
        CREATE TABLE public.a (
            id serial PRIMARY KEY,
            b_id integer
        );
        CREATE TABLE public.b (
            id serial PRIMARY KEY,
            a_id integer
        );
        ALTER TABLE public.a ADD CONSTRAINT a_b_fkey FOREIGN KEY (b_id) REFERENCES public.b(id);
        ALTER TABLE public.b ADD CONSTRAINT b_a_fkey FOREIGN KEY (a_id) REFERENCES public.a(id);
        "#,
    )
    .unwrap();

    let empty = Schema::new();
    let ops = compute_diff(&empty, &schema_a);
    assert!(
        ops.iter()
            .any(|op| matches!(op, MigrationOp::CreateTable(_))),
        "should produce CreateTable ops even with circular FKs"
    );
}

#[test]
fn destructive_drop_table_blocked_without_flag() {
    let ops = vec![MigrationOp::DropTable("public.old_table".to_string())];
    let results = lint_migration_plan(&ops, &NON_DESTRUCTIVE_LINT);
    assert!(has_errors(&results));
    assert!(results.iter().any(|r| r.rule == "deny_drop_table"));
}

#[test]
fn destructive_drop_column_blocked_without_flag() {
    let ops = vec![MigrationOp::DropColumn {
        table: QualifiedName::new("public", "users"),
        column: "email".to_string(),
    }];
    let results = lint_migration_plan(&ops, &NON_DESTRUCTIVE_LINT);
    assert!(has_errors(&results));
    assert!(results.iter().any(|r| r.rule == "deny_drop_column"));
}

#[test]
fn drop_index_allowed_without_flag() {
    let ops = vec![MigrationOp::DropIndex {
        table: QualifiedName::new("public", "users"),
        index_name: "users_email_idx".to_string(),
    }];
    let results = lint_migration_plan(&ops, &NON_DESTRUCTIVE_LINT);
    assert!(!has_errors(&results));
}

#[tokio::test]
async fn connection_to_nonexistent_db_returns_error() {
    let result = PgConnection::new("postgres://127.0.0.1:19999/nonexistent").await;
    assert!(result.is_err());
}

#[test]
fn schema_provider_unknown_prefix_returns_error() {
    let result = load_schema_from_sources(&["unknown:foo.sql".to_string()]);
    assert!(result.is_err());
}

#[test]
fn filter_invalid_glob_pattern_returns_error() {
    let result = Filter::new(&["[invalid".to_string()], &[], &[], &[]);
    assert!(result.is_err());
}

#[test]
fn diff_identical_schemas_produces_zero_ops() {
    let schema =
        parse_sql_string("CREATE TABLE public.users (id serial PRIMARY KEY, email text NOT NULL);")
            .unwrap();
    let ops = compute_diff(&schema, &schema);
    assert!(ops.is_empty());
}

#[test]
fn lint_empty_plan_produces_zero_results() {
    let results = lint_migration_plan(&[], &NON_DESTRUCTIVE_LINT);
    assert!(results.is_empty());
}