pgevolve-core 0.4.1

Postgres declarative schema management — core library (parser, IR, diff, planner) powering the pgevolve CLI.
Documentation
//! Integration smoke test for `parse_directory` against a hand-built fixture
//! directory laid down in a `tempdir`.

use std::fs;
use std::path::Path;

use pgevolve_core::ir::column::{Compression, StorageKind};
use pgevolve_core::parse;

fn write(path: &Path, contents: &str) {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent).unwrap();
    }
    fs::write(path, contents).unwrap();
}

#[test]
fn parses_a_two_table_two_index_project() {
    let tmp = tempfile::tempdir().unwrap();
    let root = tmp.path();

    write(&root.join("000_schema.sql"), "CREATE SCHEMA app;\n");
    write(
        &root.join("100_users.sql"),
        "-- @pgevolve schema=app\n\
         CREATE TABLE users (\n\
             id bigserial PRIMARY KEY,\n\
             email text NOT NULL\n\
         );\n\
         CREATE UNIQUE INDEX users_email_idx ON users (email);\n",
    );
    write(
        &root.join("200_orgs.sql"),
        "CREATE TABLE app.orgs (\n\
             id bigint PRIMARY KEY,\n\
             name text NOT NULL\n\
         );\n",
    );
    write(
        &root.join("300_links.sql"),
        "ALTER TABLE app.users ADD CONSTRAINT users_org_fkey \
         FOREIGN KEY (id) REFERENCES app.orgs (id);\n",
    );

    let catalog = parse::parse_directory(root, &[]).expect("parses");

    assert_eq!(catalog.schemas.len(), 1);
    assert_eq!(catalog.schemas[0].name.as_str(), "app");

    assert_eq!(catalog.tables.len(), 2);
    let users = catalog
        .tables
        .iter()
        .find(|t| t.qname.to_string() == "app.users")
        .expect("users present");
    let id = users
        .columns
        .iter()
        .find(|c| c.name.as_str() == "id")
        .expect("id column");
    // bigserial → BigInt + auto sequence + sequence default + NOT NULL.
    assert!(matches!(
        id.ty,
        pgevolve_core::ir::column_type::ColumnType::BigInt
    ));
    assert!(!id.nullable);
    assert!(matches!(
        id.default,
        Some(pgevolve_core::ir::default_expr::DefaultExpr::Sequence(_))
    ));

    // Constraints: PK + the FK from ALTER TABLE.
    assert!(users.constraints.iter().any(|c| matches!(
        c.kind,
        pgevolve_core::ir::constraint::ConstraintKind::PrimaryKey { .. }
    )));
    assert!(users.constraints.iter().any(|c| matches!(
        c.kind,
        pgevolve_core::ir::constraint::ConstraintKind::ForeignKey(_)
    )));

    assert_eq!(catalog.indexes.len(), 1);
    assert_eq!(catalog.indexes[0].qname.to_string(), "app.users_email_idx");

    // Synthesized sequence from bigserial.
    assert!(
        catalog
            .sequences
            .iter()
            .any(|s| s.qname.to_string() == "app.users_id_seq")
    );
}

#[test]
fn rejects_duplicate_table() {
    let tmp = tempfile::tempdir().unwrap();
    let root = tmp.path();

    write(&root.join("a.sql"), "CREATE TABLE app.t (id integer);\n");
    write(&root.join("b.sql"), "CREATE TABLE app.t (id integer);\n");

    let err = parse::parse_directory(root, &[]).unwrap_err();
    assert!(matches!(err, parse::ParseError::DuplicateObject { .. }));
}

#[test]
fn unsupported_object_kind_rejected() {
    let tmp = tempfile::tempdir().unwrap();
    let root = tmp.path();

    // CREATE EXTENSION became supported in v0.2 sub-spec #3 and
    // CREATE EVENT TRIGGER in v0.4.0; use a genuinely-unsupported
    // statement (CREATE RULE is ⛔ Not planned).
    write(
        &root.join("bad.sql"),
        "CREATE RULE notify_me AS ON UPDATE TO app.t DO NOTHING;\n",
    );
    let err = parse::parse_directory(root, &[]).unwrap_err();
    match err {
        parse::ParseError::UnsupportedObjectKind { .. } => {}
        other => panic!("expected UnsupportedObjectKind, got {other:?}"),
    }
}

#[test]
fn unqualified_name_without_directive_rejected() {
    let tmp = tempfile::tempdir().unwrap();
    let root = tmp.path();
    write(&root.join("bare.sql"), "CREATE TABLE bare (id integer);\n");
    let err = parse::parse_directory(root, &[]).unwrap_err();
    assert!(matches!(err, parse::ParseError::UnqualifiedName { .. }));
}

#[test]
fn ignored_paths_are_skipped() {
    let tmp = tempfile::tempdir().unwrap();
    let root = tmp.path();
    write(&root.join("good.sql"), "CREATE SCHEMA app;\n");
    write(
        &root.join("ignore_me/bad.sql"),
        "CREATE VIEW app.v AS SELECT 1;\n",
    );
    let pat = glob::Pattern::new(&format!("{}/ignore_me/**/*", root.display())).unwrap();
    let catalog = parse::parse_directory(root, &[pat]).expect("parses without bad");
    assert_eq!(catalog.schemas.len(), 1);
}

// ------------------------------------------------------------------
// ALTER COLUMN SET STORAGE / SET COMPRESSION integration tests
// ------------------------------------------------------------------

#[test]
fn alter_column_set_storage_external_applies_to_column() {
    let tmp = tempfile::tempdir().unwrap();
    let root = tmp.path();
    write(
        &root.join("schema.sql"),
        "CREATE SCHEMA app;\n\
         CREATE TABLE app.t (doc text);\n\
         ALTER TABLE app.t ALTER COLUMN doc SET STORAGE EXTERNAL;\n",
    );
    let catalog = parse::parse_directory(root, &[]).expect("parses");
    let col = &catalog.tables[0].columns[0];
    assert_eq!(col.storage, Some(StorageKind::External));
    assert_eq!(col.compression, None);
}

#[test]
fn alter_column_set_compression_lz4_applies_to_column() {
    let tmp = tempfile::tempdir().unwrap();
    let root = tmp.path();
    write(
        &root.join("schema.sql"),
        "CREATE SCHEMA app;\n\
         CREATE TABLE app.t (blob bytea);\n\
         ALTER TABLE app.t ALTER COLUMN blob SET COMPRESSION lz4;\n",
    );
    let catalog = parse::parse_directory(root, &[]).expect("parses");
    let col = &catalog.tables[0].columns[0];
    assert_eq!(col.compression, Some(Compression::Lz4));
    assert_eq!(col.storage, None);
}

#[test]
fn alter_column_set_compression_default_clears_compression() {
    let tmp = tempfile::tempdir().unwrap();
    let root = tmp.path();
    // Inline COMPRESSION lz4 then ALTER to DEFAULT: result must be None.
    write(
        &root.join("schema.sql"),
        "CREATE SCHEMA app;\n\
         CREATE TABLE app.t (blob bytea COMPRESSION lz4);\n\
         ALTER TABLE app.t ALTER COLUMN blob SET COMPRESSION DEFAULT;\n",
    );
    let catalog = parse::parse_directory(root, &[]).expect("parses");
    let col = &catalog.tables[0].columns[0];
    assert_eq!(
        col.compression, None,
        "DEFAULT should revert to cluster GUC (None)"
    );
}

#[test]
fn alter_column_set_storage_and_compression_together() {
    let tmp = tempfile::tempdir().unwrap();
    let root = tmp.path();
    write(
        &root.join("schema.sql"),
        "CREATE SCHEMA app;\n\
         CREATE TABLE app.t (doc text);\n\
         ALTER TABLE app.t ALTER COLUMN doc SET STORAGE EXTERNAL;\n\
         ALTER TABLE app.t ALTER COLUMN doc SET COMPRESSION lz4;\n",
    );
    let catalog = parse::parse_directory(root, &[]).expect("parses");
    let col = &catalog.tables[0].columns[0];
    assert_eq!(col.storage, Some(StorageKind::External));
    assert_eq!(col.compression, Some(Compression::Lz4));
}

#[test]
fn alter_column_unknown_table_errors() {
    let tmp = tempfile::tempdir().unwrap();
    let root = tmp.path();
    write(
        &root.join("schema.sql"),
        "CREATE SCHEMA app;\n\
         ALTER TABLE app.nonexistent ALTER COLUMN doc SET STORAGE EXTERNAL;\n",
    );
    let err = parse::parse_directory(root, &[]).unwrap_err();
    assert!(
        matches!(err, parse::ParseError::Structural { .. }),
        "expected Structural error for unknown table, got {err:?}"
    );
}

#[test]
fn alter_column_unknown_column_errors() {
    let tmp = tempfile::tempdir().unwrap();
    let root = tmp.path();
    write(
        &root.join("schema.sql"),
        "CREATE SCHEMA app;\n\
         CREATE TABLE app.t (id integer);\n\
         ALTER TABLE app.t ALTER COLUMN nonexistent SET STORAGE EXTERNAL;\n",
    );
    let err = parse::parse_directory(root, &[]).unwrap_err();
    assert!(
        matches!(err, parse::ParseError::Structural { .. }),
        "expected Structural error for unknown column, got {err:?}"
    );
}