datasources-postgres 0.1.18

Postgres datasource provider
Documentation
use datasources_code_generators::{generate_sql, SqlGenerateDriver};
use std::sync::Once;

fn register_sql() {
    static ONCE: Once = Once::new();
    ONCE.call_once(|| {
        datasources_code_generators::register_dialect(
            "postgres",
            SqlGenerateDriver {
                quote_left: crate::PostgresDriver::QUOTE_LEFT,
                quote_right: crate::PostgresDriver::QUOTE_RIGHT,
                type_converter: crate::PostgresDriver::types(),
                default_value_converter: crate::PostgresDriver::defaults(),
                statement_renderer: crate::PostgresDriver::statement_text_renderer(),
            },
        )
        .unwrap();
    });
}
use regex::Regex;
use std::collections::HashMap;

const TYPES_YAML: &str = r#"
types:
  - user:
      tags: [datasource_type]
      inherits: set
      fields:
        - email:
            type: string
        - created:
            type: datetime
        - updated:
            type: datetime
"#;

fn files(types: &str, datasource: Option<&str>, seeds: Option<&str>) -> HashMap<String, String> {
    register_sql();
    let mut files = HashMap::from([("types.yaml".into(), types.to_string())]);
    if let Some(ds) = datasource {
        files.insert("datasource.yaml".into(), ds.into());
    }
    if let Some(seeds) = seeds {
        files.insert("datasource_seeds.yaml".into(), seeds.into());
    }
    files
}

fn names(entries: &[datasources_code_generators::GenerateEntry]) -> Vec<String> {
    let mut names: Vec<_> = entries.iter().map(|e| e.filename.clone()).collect();
    names.sort();
    names
}

#[test]
fn emits_stored_procedure_migrations_when_the_flag_is_true_on_postgres() {
    let listed = names(
        &generate_sql(
            &files(TYPES_YAML, None, None),
            &HashMap::from([("datasource.use_stored_procedures".into(), "true".into())]),
            &["postgres".into()],
        )
        .unwrap(),
    );
    assert!(listed.iter().any(|f| f == "postgres/migrations/0001_initial_up.sql"));
    assert!(listed.iter().any(|f| f == "postgres/migrations/0002_stored_procedures_up.sql"));
    assert!(listed.iter().any(|f| f == "postgres/migrations/0002_stored_procedures_down.sql"));
    let up = generate_sql(
        &files(TYPES_YAML, None, None),
        &HashMap::from([("datasource.use_stored_procedures".into(), "true".into())]),
        &["postgres".into()],
    )
    .unwrap()
    .into_iter()
    .find(|e| e.filename.ends_with("_stored_procedures_up.sql"))
    .unwrap()
    .content;
    assert!(up.contains("read_users"));
    assert!(up.contains("bulk_create_users"));
    assert!(up.contains("CREATE OR REPLACE"));
}

#[test]
fn omits_stored_procedure_migrations_when_the_flag_is_absent() {
    let files = names(&generate_sql(&files(TYPES_YAML, None, None), &HashMap::new(), &["postgres".into()]).unwrap());
    assert!(files.iter().any(|f| f == "postgres/migrations/0001_initial_up.sql"));
    assert!(!files.iter().any(|f| f.contains("stored_procedures")));
}

const IDENTITY_YAML: &str = r#"
types:
  - person:
      tags: [datasource_type]
      inherits: set
      fields:
        - id:
            type: integer
            is_id: true
        - code:
            type: integer
        - email:
            type: string
  - link:
      tags: [datasource_type]
      inherits: set
      ids: [left_id, right_id]
      fields:
        - left_id:
            type: integer
        - right_id:
            type: integer
  - country:
      tags: [datasource_type]
      inherits: set
      fields:
        - name:
            type: string
"#;

const IDENTITY_DATASOURCE: &str = r#"
includes:
  - types:
      filter: tag == "datasource_type"
types:
  - country:
      fields:
        - id:
            is_fixed_id: true
            is_readonly: true
"#;

fn up_body() -> String {
    generate_sql(
        &files(IDENTITY_YAML, Some(IDENTITY_DATASOURCE), None),
        &HashMap::new(),
        &["postgres".into()],
    )
    .unwrap()
    .into_iter()
    .find(|e| e.filename.ends_with("0001_initial_up.sql"))
    .unwrap()
    .content
}

#[test]
fn uses_is_id_as_a_generated_primary_key() {
    let sql = up_body();
    assert!(Regex::new(r#"CREATE TABLE "people""#).unwrap().is_match(&sql));
    assert!(Regex::new(r#""id" INTEGER GENERATED BY DEFAULT AS IDENTITY"#)
        .unwrap()
        .is_match(&sql));
    assert!(Regex::new(r#"CONSTRAINT "people_primary_key" PRIMARY KEY"#)
        .unwrap()
        .is_match(&sql));
    assert!(Regex::new(r#""code" INTEGER NOT NULL"#).unwrap().is_match(&sql));
    assert!(Regex::new(r#""email""#).unwrap().is_match(&sql));
}

#[test]
fn emits_a_composite_primary_key_when_ids_lists_multiple_columns() {
    let sql = up_body();
    assert!(Regex::new(r#"CREATE TABLE "links""#).unwrap().is_match(&sql));
    assert!(Regex::new(r#""left_id" INTEGER NOT NULL"#).unwrap().is_match(&sql));
    assert!(Regex::new(r#""right_id" INTEGER NOT NULL"#).unwrap().is_match(&sql));
    assert!(Regex::new(r#"CONSTRAINT "links_primary_key" PRIMARY KEY \("left_id", "right_id"\)"#)
        .unwrap()
        .is_match(&sql));
    let table = Regex::new(r#"CREATE TABLE "links"[\s\S]*?(CREATE TABLE|$)"#)
        .unwrap()
        .find(&sql)
        .map(|m| m.as_str().to_string())
        .unwrap_or_default();
    assert!(!table.contains("GENERATED BY DEFAULT AS IDENTITY"));
}

#[test]
fn emits_a_plain_primary_key_when_is_fixed_id_is_set() {
    let sql = up_body();
    assert!(Regex::new(r#"CREATE TABLE "countries""#).unwrap().is_match(&sql));
    assert!(Regex::new(r#""id" INTEGER NOT NULL"#).unwrap().is_match(&sql));
    assert!(Regex::new(r#"CONSTRAINT "countries_primary_key" PRIMARY KEY \("id"\)"#)
        .unwrap()
        .is_match(&sql));
    let table = Regex::new(r#"CREATE TABLE "countries"[\s\S]*?(CREATE TABLE|$)"#)
        .unwrap()
        .find(&sql)
        .map(|m| m.as_str().to_string())
        .unwrap_or_default();
    assert!(!table.contains("GENERATED BY DEFAULT AS IDENTITY"));
}

fn frozen_diff(types_yaml: &str) -> String {
    let types: serde_yaml::Value = serde_yaml::from_str(types_yaml).unwrap();
    let mut envelope = serde_yaml::Mapping::new();
    envelope.insert(serde_yaml::Value::String("added".into()), serde_yaml::Value::Bool(true));
    envelope.insert(serde_yaml::Value::String("content".into()), types);
    let mut doc = serde_yaml::Mapping::new();
    doc.insert(
        serde_yaml::Value::String("types.yaml".into()),
        serde_yaml::Value::Mapping(envelope),
    );
    serde_yaml::to_string(&serde_yaml::Value::Mapping(doc)).unwrap()
}

fn replacement_diff(types_yaml: &str) -> String {
    let types: serde_yaml::Value = serde_yaml::from_str(types_yaml).unwrap();
    let mut doc = serde_yaml::Mapping::new();
    doc.insert(
        serde_yaml::Value::String("types.yaml".into()),
        types,
    );
    serde_yaml::to_string(&serde_yaml::Value::Mapping(doc)).unwrap()
}

#[test]
fn replaces_stored_procedures_when_a_later_version_adds_a_column() {
    let first = "version-20261016T000000Z-001/diff.yaml";
    let second = "version-20261016T000000Z-002/diff.yaml";
    let next = r#"
types:
  - user:
      tags: [datasource_type]
      inherits: set
      fields:
        - email:
            type: string
        - created:
            type: datetime
        - updated:
            type: datetime
        - notes:
            type: string
"#;
    let mut files = files(
        next,
        Some("version: 1.0.0\nincludes:\n  - types:\n      filter: tag == \"datasource_type\" || tag == \"view_type\"\ntypes: []\n"),
        None,
    );
    files.insert(first.into(), frozen_diff(TYPES_YAML));
    files.insert(second.into(), replacement_diff(next));
    let entries = generate_sql(
        &files,
        &HashMap::from([("datasource.use_stored_procedures".into(), "true".into())]),
        &["postgres".into()],
    )
    .unwrap();
    let up2 = entries
        .iter()
        .find(|e| e.filename == "postgres/migrations/0002_up.sql")
        .unwrap()
        .content
        .clone();
    let down2 = entries
        .iter()
        .find(|e| e.filename == "postgres/migrations/0002_down.sql")
        .unwrap()
        .content
        .clone();
    assert!(up2.contains("notes"));
    assert!(up2.contains("CREATE OR REPLACE"));
    assert!(down2.contains("CREATE OR REPLACE"));
}

#[test]
fn throws_when_the_selected_dialect_has_neither_an_engine_file_nor_datasource_yaml() {
    let path = "version-20261016T000000Z-001/diff.yaml";
    let mut files = files(TYPES_YAML, None, None);
    files.insert(
        "datasource.sqlite.yaml".into(),
        r#"
version: 1.0.0
types:
  - user:
      fields:
        - email:
            type: string
"#
        .into(),
    );
    files.insert(path.into(), frozen_diff(TYPES_YAML));
    let error = generate_sql(&files, &HashMap::new(), &["postgres".into()]).unwrap_err();
    assert!(error
        .to_string()
        .contains("postgres requires datasource.postgres.yaml or datasource.yaml"));
}

#[test]
fn infers_a_reference_column_type_from_the_parent_id() {
    let sql = generate_sql(
        &files(
            r#"
types:
  - gender:
      tags: [datasource_type]
      inherits: set
      fields:
        - name:
            type: string
            size: 128
  - contact:
      tags: [datasource_type]
      inherits: set
      fields:
        - gender_type_id:
            references: gender.id
"#,
            None,
            None,
        ),
        &HashMap::new(),
        &["postgres".into()],
    )
    .unwrap()
    .into_iter()
    .find(|e| e.filename.ends_with("0001_initial_up.sql"))
    .unwrap()
    .content;
    let contacts = Regex::new(r#"CREATE TABLE "contacts" \(([\s\S]*?)\);"#)
        .unwrap()
        .captures(&sql)
        .unwrap()[1]
        .to_string();
    assert!(Regex::new(r#""gender_type_id" INTEGER"#)
        .unwrap()
        .is_match(&contacts));
    assert!(!Regex::new(r#""gender_type_id" TEXT"#)
        .unwrap()
        .is_match(&contacts));
}

#[test]
fn infers_a_versioned_reference_column_type_from_an_inherited_parent_id() {
    let types = r#"
types:
  - base:
      fields:
        - id:
            type: integer
            is_id: true
  - gender:
      tags: [datasource_type]
      inherits: base
      fields:
        - name:
            type: string
            size: 128
  - contact:
      tags: [datasource_type]
      inherits: base
      fields:
        - gender_type_id:
            references: gender.id
"#;
    let path = "version-20261016T000000Z-001/diff.yaml";
    let mut files = files(
        types,
        Some("version: 1.0.0\nincludes:\n  - types:\n      filter: tag == \"datasource_type\" || tag == \"view_type\"\ntypes: []\n"),
        None,
    );
    files.insert(path.into(), frozen_diff(types));
    let sql = generate_sql(&files, &HashMap::new(), &["postgres".into()])
        .unwrap()
        .into_iter()
        .find(|e| e.filename.ends_with("0001_up.sql"))
        .unwrap()
        .content;
    let contacts = Regex::new(r#"CREATE TABLE "contacts" \(([\s\S]*?)\);"#)
        .unwrap()
        .captures(&sql)
        .unwrap()[1]
        .to_string();
    assert!(Regex::new(r#""gender_type_id" INTEGER"#)
        .unwrap()
        .is_match(&contacts));
    assert!(!Regex::new(r#""gender_type_id" TEXT"#)
        .unwrap()
        .is_match(&contacts));
}