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, None, 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 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));
}