pub mod index;
pub mod schema;
pub mod sequence;
pub mod table;
pub mod view;
use crate::ir::catalog::Catalog;
use crate::ir::constraint::ConstraintKind;
#[must_use]
pub fn render_catalog(catalog: &Catalog) -> String {
let mut out = String::new();
for s in &catalog.schemas {
out.push_str(&schema::render_schema(s));
out.push('\n');
}
if !catalog.schemas.is_empty() {
out.push('\n');
}
for t in &catalog.tables {
out.push_str(&table::render_table(t));
out.push('\n');
}
if !catalog.tables.is_empty() {
out.push('\n');
}
let mut had_fk = false;
for t in &catalog.tables {
for c in &t.constraints {
if matches!(c.kind, ConstraintKind::ForeignKey(_)) {
out.push_str(&table::render_add_fk(&t.qname, c));
out.push('\n');
had_fk = true;
}
}
}
if had_fk {
out.push('\n');
}
for i in &catalog.indexes {
out.push_str(&index::render_index(i));
out.push('\n');
}
if !catalog.indexes.is_empty() {
out.push('\n');
}
for s in &catalog.sequences {
out.push_str(&sequence::render_sequence(s));
out.push('\n');
}
if !catalog.sequences.is_empty() {
out.push('\n');
}
for v in &catalog.views {
out.push_str(&view::render_view(v));
out.push('\n');
}
if !catalog.views.is_empty() {
out.push('\n');
}
for mv in &catalog.materialized_views {
out.push_str(&view::render_materialized_view(mv));
out.push('\n');
}
while out.ends_with("\n\n") {
out.pop();
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::identifier::{Identifier, QualifiedName};
use crate::ir::catalog::Catalog;
use crate::ir::column::Column;
use crate::ir::column_type::ColumnType;
use crate::ir::constraint::{
Constraint, ConstraintKind, Deferrable, FkMatchType, ForeignKey, ReferentialAction,
};
use crate::ir::index::{
Index, IndexColumn, IndexColumnExpr, IndexMethod, IndexParent, NullsOrder, SortOrder,
};
use crate::ir::schema::Schema;
use crate::ir::sequence::Sequence;
use crate::ir::table::Table;
fn id(s: &str) -> Identifier {
Identifier::from_unquoted(s).unwrap()
}
fn qn(schema: &str, name: &str) -> QualifiedName {
QualifiedName::new(id(schema), id(name))
}
#[test]
fn empty_catalog_renders_empty() {
let rendered = render_catalog(&Catalog::empty());
assert!(rendered.trim().is_empty());
}
#[test]
fn schema_renders_first() {
let mut cat = Catalog::empty();
cat.schemas.push(Schema::new(id("app")));
cat.tables.push(Table {
qname: qn("app", "users"),
columns: vec![Column {
name: id("id"),
ty: ColumnType::BigInt,
nullable: false,
default: None,
identity: None,
generated: None,
collation: None,
storage: None,
compression: None,
comment: None,
}],
constraints: vec![],
partition_by: None,
partition_of: None,
comment: None,
owner: None,
grants: vec![],
rls_enabled: false,
rls_forced: false,
policies: vec![],
storage: crate::ir::reloptions::TableStorageOptions::default(),
});
let rendered = render_catalog(&cat);
let schema_pos = rendered.find("CREATE SCHEMA").unwrap();
let table_pos = rendered.find("CREATE TABLE").unwrap();
assert!(schema_pos < table_pos, "schema must come before table");
}
#[test]
#[allow(clippy::too_many_lines)] fn fk_rendered_after_table() {
let mut cat = Catalog::empty();
cat.schemas.push(Schema::new(id("app")));
cat.tables.push(Table {
qname: qn("app", "orgs"),
columns: vec![Column {
name: id("id"),
ty: ColumnType::BigInt,
nullable: false,
default: None,
identity: None,
generated: None,
collation: None,
storage: None,
compression: None,
comment: None,
}],
constraints: vec![Constraint {
qname: qn("app", "orgs_pkey"),
kind: ConstraintKind::PrimaryKey {
columns: vec![id("id")],
include: vec![],
},
deferrable: Deferrable::NotDeferrable,
comment: None,
}],
partition_by: None,
partition_of: None,
comment: None,
owner: None,
grants: vec![],
rls_enabled: false,
rls_forced: false,
policies: vec![],
storage: crate::ir::reloptions::TableStorageOptions::default(),
});
cat.tables.push(Table {
qname: qn("app", "users"),
columns: vec![
Column {
name: id("id"),
ty: ColumnType::BigInt,
nullable: false,
default: None,
identity: None,
generated: None,
collation: None,
storage: None,
compression: None,
comment: None,
},
Column {
name: id("org_id"),
ty: ColumnType::BigInt,
nullable: false,
default: None,
identity: None,
generated: None,
collation: None,
storage: None,
compression: None,
comment: None,
},
],
constraints: vec![
Constraint {
qname: qn("app", "users_pkey"),
kind: ConstraintKind::PrimaryKey {
columns: vec![id("id")],
include: vec![],
},
deferrable: Deferrable::NotDeferrable,
comment: None,
},
Constraint {
qname: qn("app", "users_org_fkey"),
kind: ConstraintKind::ForeignKey(ForeignKey {
columns: vec![id("org_id")],
referenced_table: qn("app", "orgs"),
referenced_columns: vec![id("id")],
on_update: ReferentialAction::NoAction,
on_delete: ReferentialAction::NoAction,
match_type: FkMatchType::Simple,
}),
deferrable: Deferrable::NotDeferrable,
comment: None,
},
],
partition_by: None,
partition_of: None,
comment: None,
owner: None,
grants: vec![],
rls_enabled: false,
rls_forced: false,
policies: vec![],
storage: crate::ir::reloptions::TableStorageOptions::default(),
});
let rendered = render_catalog(&cat);
assert!(
rendered.contains("ALTER TABLE"),
"expected ALTER TABLE for FK"
);
let create_table_pos = rendered.rfind("CREATE TABLE").unwrap();
let alter_pos = rendered.find("ALTER TABLE").unwrap();
assert!(
create_table_pos < alter_pos,
"ALTER TABLE must come after last CREATE TABLE"
);
let create_table_end = rendered.find("ALTER TABLE").unwrap();
let create_table_section = &rendered[..create_table_end];
assert!(
!create_table_section.contains("FOREIGN KEY"),
"FK should not be inline in CREATE TABLE"
);
}
#[test]
fn indexes_rendered_after_tables() {
let mut cat = Catalog::empty();
cat.tables.push(Table {
qname: qn("app", "users"),
columns: vec![Column {
name: id("email"),
ty: ColumnType::Text,
nullable: false,
default: None,
identity: None,
generated: None,
collation: None,
storage: None,
compression: None,
comment: None,
}],
constraints: vec![],
partition_by: None,
partition_of: None,
comment: None,
owner: None,
grants: vec![],
rls_enabled: false,
rls_forced: false,
policies: vec![],
storage: crate::ir::reloptions::TableStorageOptions::default(),
});
cat.indexes.push(Index {
qname: qn("app", "users_email_idx"),
on: IndexParent::Table(qn("app", "users")),
method: IndexMethod::BTree,
columns: vec![IndexColumn {
expr: IndexColumnExpr::Column(id("email")),
collation: None,
opclass: None,
sort_order: SortOrder::Asc,
nulls_order: NullsOrder::NullsLast,
}],
include: vec![],
unique: true,
nulls_not_distinct: false,
predicate: None,
tablespace: None,
comment: None,
storage: crate::ir::reloptions::IndexStorageOptions::default(),
});
let rendered = render_catalog(&cat);
let table_pos = rendered.find("CREATE TABLE").unwrap();
let index_pos = rendered.find("CREATE UNIQUE INDEX").unwrap();
assert!(table_pos < index_pos, "index must come after table");
}
#[test]
fn sequences_rendered_last() {
let mut cat = Catalog::empty();
cat.sequences.push(Sequence {
qname: qn("app", "users_id_seq"),
data_type: ColumnType::BigInt,
start: 1,
increment: 1,
min_value: None,
max_value: None,
cache: 1,
cycle: false,
owned_by: None,
comment: None,
owner: None,
grants: vec![],
});
cat.tables.push(Table {
qname: qn("app", "users"),
columns: vec![Column {
name: id("id"),
ty: ColumnType::BigInt,
nullable: false,
default: None,
identity: None,
generated: None,
collation: None,
storage: None,
compression: None,
comment: None,
}],
constraints: vec![],
partition_by: None,
partition_of: None,
comment: None,
owner: None,
grants: vec![],
rls_enabled: false,
rls_forced: false,
policies: vec![],
storage: crate::ir::reloptions::TableStorageOptions::default(),
});
let rendered = render_catalog(&cat);
let table_pos = rendered.find("CREATE TABLE").unwrap();
let seq_pos = rendered.find("CREATE SEQUENCE").unwrap();
assert!(table_pos < seq_pos, "sequence must come after table");
}
#[test]
fn parseable_by_pg_query() {
let mut cat = Catalog::empty();
cat.schemas.push(Schema::new(id("app")));
cat.tables.push(Table {
qname: qn("app", "users"),
columns: vec![
Column {
name: id("id"),
ty: ColumnType::BigInt,
nullable: false,
default: None,
identity: None,
generated: None,
collation: None,
storage: None,
compression: None,
comment: None,
},
Column {
name: id("email"),
ty: ColumnType::Text,
nullable: false,
default: None,
identity: None,
generated: None,
collation: None,
storage: None,
compression: None,
comment: None,
},
],
constraints: vec![Constraint {
qname: qn("app", "users_pkey"),
kind: ConstraintKind::PrimaryKey {
columns: vec![id("id")],
include: vec![],
},
deferrable: Deferrable::NotDeferrable,
comment: None,
}],
partition_by: None,
partition_of: None,
comment: Some("user accounts".into()),
owner: None,
grants: vec![],
rls_enabled: false,
rls_forced: false,
policies: vec![],
storage: crate::ir::reloptions::TableStorageOptions::default(),
});
cat.indexes.push(Index {
qname: qn("app", "users_email_idx"),
on: IndexParent::Table(qn("app", "users")),
method: IndexMethod::BTree,
columns: vec![IndexColumn {
expr: IndexColumnExpr::Column(id("email")),
collation: None,
opclass: None,
sort_order: SortOrder::Asc,
nulls_order: NullsOrder::NullsLast,
}],
include: vec![],
unique: true,
nulls_not_distinct: false,
predicate: None,
tablespace: None,
comment: None,
storage: crate::ir::reloptions::IndexStorageOptions::default(),
});
let rendered = render_catalog(&cat);
let r = pg_query::parse(&rendered);
assert!(
r.is_ok(),
"pg_query rejected rendered catalog:\n{rendered}\nerr: {r:?}"
);
}
#[test]
fn views_rendered_after_sequences() {
use crate::ir::view::View;
use crate::parse::normalize_body::NormalizedBody;
let mut cat = Catalog::empty();
cat.sequences.push(Sequence {
qname: qn("app", "users_id_seq"),
data_type: ColumnType::BigInt,
start: 1,
increment: 1,
min_value: None,
max_value: None,
cache: 1,
cycle: false,
owned_by: None,
comment: None,
owner: None,
grants: vec![],
});
cat.views.push(View {
qname: qn("app", "active_users"),
columns: vec![],
body_canonical: NormalizedBody::from_sql("SELECT 1").unwrap(),
body_dependencies: vec![],
security_barrier: None,
security_invoker: None,
comment: None,
raw_body: String::new(),
owner: None,
grants: vec![],
});
let rendered = render_catalog(&cat);
let seq_pos = rendered.find("CREATE SEQUENCE").unwrap();
let view_pos = rendered.find("CREATE VIEW").unwrap();
assert!(seq_pos < view_pos, "views must come after sequences");
}
#[test]
fn materialized_views_rendered_in_catalog() {
use crate::ir::view::MaterializedView;
use crate::parse::normalize_body::NormalizedBody;
let mut cat = Catalog::empty();
cat.materialized_views.push(MaterializedView {
qname: qn("app", "summary"),
columns: vec![],
body_canonical: NormalizedBody::from_sql("SELECT count(*) FROM app.users").unwrap(),
body_dependencies: vec![],
comment: None,
raw_body: String::new(),
owner: None,
grants: vec![],
storage: crate::ir::reloptions::MaterializedViewStorageOptions::default(),
});
let rendered = render_catalog(&cat);
assert!(
rendered.contains("CREATE MATERIALIZED VIEW"),
"expected CREATE MATERIALIZED VIEW: {rendered}"
);
}
}