use crate::catalog::id::DbObjectId;
use crate::catalog::index::Index;
use crate::diff::operations::IndexOperation;
use crate::render::{RenderedSql, Safety, SqlRenderer, quote_ident};
impl SqlRenderer for IndexOperation {
fn to_sql(&self) -> Vec<RenderedSql> {
match self {
IndexOperation::Create(index) => vec![render_create_index(index)],
IndexOperation::Drop { schema, name } => vec![RenderedSql {
sql: format!("DROP INDEX {}.{};", quote_ident(schema), quote_ident(name)),
safety: Safety::Safe,
}],
IndexOperation::Cluster {
table_schema,
table_name,
index_schema,
index_name,
} => vec![RenderedSql {
sql: format!(
"CLUSTER {}.{} USING {}.{};",
quote_ident(table_schema),
quote_ident(table_name),
quote_ident(index_schema),
quote_ident(index_name)
),
safety: Safety::Safe,
}],
IndexOperation::SetWithoutCluster { schema, name } => vec![RenderedSql {
sql: format!(
"ALTER TABLE {}.{} SET WITHOUT CLUSTER;",
quote_ident(schema),
quote_ident(name)
),
safety: Safety::Safe,
}],
IndexOperation::Reindex {
schema,
name,
concurrently,
} => vec![RenderedSql {
sql: format!(
"REINDEX INDEX{} {}.{};",
if *concurrently { " CONCURRENTLY" } else { "" },
quote_ident(schema),
quote_ident(name)
),
safety: Safety::Safe,
}],
}
}
fn db_object_id(&self) -> DbObjectId {
match self {
IndexOperation::Create(index) => index.id(),
IndexOperation::Drop { schema, name } => DbObjectId::Index {
schema: schema.clone(),
name: name.clone(),
},
IndexOperation::Cluster {
index_schema,
index_name,
..
} => DbObjectId::Index {
schema: index_schema.clone(),
name: index_name.clone(),
},
IndexOperation::SetWithoutCluster { schema, name } => DbObjectId::Table {
schema: schema.clone(),
name: name.clone(),
},
IndexOperation::Reindex { schema, name, .. } => DbObjectId::Index {
schema: schema.clone(),
name: name.clone(),
},
}
}
}
fn render_create_index(index: &Index) -> RenderedSql {
RenderedSql::new(crate::render::sql::render_create_index(index))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::catalog::index::{IndexColumn, IndexType};
#[test]
fn test_render_simple_btree_index() {
let index = Index {
schema: "public".to_string(),
name: "idx_users_email".to_string(),
table_schema: "public".to_string(),
table_name: "users".to_string(),
index_type: IndexType::Btree,
is_unique: false,
is_clustered: false,
is_valid: true,
columns: vec![IndexColumn {
expression: "email".to_string(),
collation: None,
opclass: None,
ordering: Some("ASC".to_string()),
nulls_ordering: Some("NULLS LAST".to_string()),
}],
include_columns: vec![],
predicate: None,
tablespace: None,
storage_parameters: vec![],
comment: None,
depends_on: vec![],
};
let rendered = render_create_index(&index);
assert_eq!(
rendered.sql,
"CREATE INDEX \"idx_users_email\" ON \"public\".\"users\" USING btree (email);"
);
}
#[test]
fn test_render_unique_index_with_desc_ordering() {
let index = Index {
schema: "public".to_string(),
name: "idx_users_created_desc".to_string(),
table_schema: "public".to_string(),
table_name: "users".to_string(),
index_type: IndexType::Btree,
is_unique: true,
is_clustered: false,
is_valid: true,
columns: vec![IndexColumn {
expression: "created_at".to_string(),
collation: None,
opclass: None,
ordering: Some("DESC".to_string()),
nulls_ordering: Some("NULLS FIRST".to_string()),
}],
include_columns: vec![],
predicate: None,
tablespace: None,
storage_parameters: vec![],
comment: None,
depends_on: vec![],
};
let rendered = render_create_index(&index);
assert_eq!(
rendered.sql,
"CREATE UNIQUE INDEX \"idx_users_created_desc\" ON \"public\".\"users\" USING btree (created_at DESC NULLS FIRST);"
);
}
#[test]
fn test_render_partial_index_with_include() {
let index = Index {
schema: "public".to_string(),
name: "idx_orders_active".to_string(),
table_schema: "public".to_string(),
table_name: "orders".to_string(),
index_type: IndexType::Btree,
is_unique: false,
is_clustered: false,
is_valid: true,
columns: vec![IndexColumn {
expression: "customer_id".to_string(),
collation: None,
opclass: None,
ordering: Some("ASC".to_string()),
nulls_ordering: Some("NULLS LAST".to_string()),
}],
include_columns: vec!["\"total_amount\"".to_string(), "\"created_at\"".to_string()],
predicate: Some("status = 'active'".to_string()),
tablespace: None,
storage_parameters: vec![],
comment: None,
depends_on: vec![],
};
let rendered = render_create_index(&index);
assert_eq!(
rendered.sql,
"CREATE INDEX \"idx_orders_active\" ON \"public\".\"orders\" USING btree (customer_id) INCLUDE (\"total_amount\", \"created_at\") WHERE status = 'active';"
);
}
#[test]
fn test_render_gin_index_with_storage_params() {
let index = Index {
schema: "public".to_string(),
name: "idx_documents_content".to_string(),
table_schema: "public".to_string(),
table_name: "documents".to_string(),
index_type: IndexType::Gin,
is_unique: false,
is_clustered: false,
is_valid: true,
columns: vec![IndexColumn {
expression: "to_tsvector('english', content)".to_string(),
collation: None,
opclass: None,
ordering: None,
nulls_ordering: None,
}],
include_columns: vec![],
predicate: None,
tablespace: None,
storage_parameters: vec![
("fastupdate".to_string(), "off".to_string()),
("gin_pending_list_limit".to_string(), "1MB".to_string()),
],
comment: None,
depends_on: vec![],
};
let rendered = render_create_index(&index);
assert_eq!(
rendered.sql,
"CREATE INDEX \"idx_documents_content\" ON \"public\".\"documents\" USING gin (to_tsvector('english', content)) WITH (fastupdate = off, gin_pending_list_limit = 1MB);"
);
}
#[test]
fn test_render_cluster_operation() {
let op = IndexOperation::Cluster {
table_schema: "public".to_string(),
table_name: "users".to_string(),
index_schema: "public".to_string(),
index_name: "idx_users_email".to_string(),
};
let rendered = op.to_sql();
assert_eq!(rendered.len(), 1);
assert_eq!(
rendered[0].sql,
"CLUSTER \"public\".\"users\" USING \"public\".\"idx_users_email\";"
);
assert_eq!(rendered[0].safety, Safety::Safe);
}
#[test]
fn test_render_set_without_cluster_operation() {
let op = IndexOperation::SetWithoutCluster {
schema: "public".to_string(),
name: "users".to_string(),
};
let rendered = op.to_sql();
assert_eq!(rendered.len(), 1);
assert_eq!(
rendered[0].sql,
"ALTER TABLE \"public\".\"users\" SET WITHOUT CLUSTER;"
);
assert_eq!(rendered[0].safety, Safety::Safe);
}
#[test]
fn test_render_reindex_operation() {
let op = IndexOperation::Reindex {
schema: "public".to_string(),
name: "idx_users_email".to_string(),
concurrently: false,
};
let rendered = op.to_sql();
assert_eq!(rendered.len(), 1);
assert_eq!(
rendered[0].sql,
"REINDEX INDEX \"public\".\"idx_users_email\";"
);
assert_eq!(rendered[0].safety, Safety::Safe);
}
#[test]
fn test_render_reindex_concurrently_operation() {
let op = IndexOperation::Reindex {
schema: "public".to_string(),
name: "idx_users_email".to_string(),
concurrently: true,
};
let rendered = op.to_sql();
assert_eq!(rendered.len(), 1);
assert_eq!(
rendered[0].sql,
"REINDEX INDEX CONCURRENTLY \"public\".\"idx_users_email\";"
);
assert_eq!(rendered[0].safety, Safety::Safe);
}
#[test]
fn test_cluster_operation_not_destructive() {
let op = IndexOperation::Cluster {
table_schema: "public".to_string(),
table_name: "users".to_string(),
index_schema: "public".to_string(),
index_name: "idx_users_email".to_string(),
};
assert!(!op.to_sql().iter().any(|s| s.safety == Safety::Destructive));
}
#[test]
fn test_reindex_operation_not_destructive() {
let op = IndexOperation::Reindex {
schema: "public".to_string(),
name: "idx_users_email".to_string(),
concurrently: true,
};
assert!(!op.to_sql().iter().any(|s| s.safety == Safety::Destructive));
}
}