use crate::checks::pg_helpers::{
NodeEnum, alter_table_cmds, cmd_def_as_column_def, column_type_name, is_json_type,
};
use crate::checks::{Check, Config, MigrationContext};
use crate::violation::Violation;
pub struct AddJsonColumnCheck;
impl Check for AddJsonColumnCheck {
fn check(&self, node: &NodeEnum, _config: &Config, _ctx: &MigrationContext) -> Vec<Violation> {
let Some((table_name, cmds)) = alter_table_cmds(node) else {
return vec![];
};
cmds.iter()
.filter_map(|cmd| {
let col = cmd_def_as_column_def(cmd)?;
if !is_json_type(&column_type_name(col)) {
return None;
}
let column_name = &col.colname;
Some(Violation::new(
"ADD COLUMN with JSON type",
format!(
"Adding column '{column_name}' with JSON type on table '{table_name}' can break existing SELECT DISTINCT queries. \
The JSON type has no equality operator, causing runtime errors for DISTINCT, GROUP BY, and UNION operations."
),
format!(
r"Use JSONB instead of JSON:
ALTER TABLE {table_name} ADD COLUMN {column_name} JSONB;
Benefits of JSONB over JSON:
- Has proper equality and comparison operators (supports DISTINCT, GROUP BY, UNION)
- Supports indexing (GIN indexes for efficient queries)
- Faster to process (binary format, no reparsing)
- Generally better performance for most use cases
Note: The only advantage of JSON over JSONB is that it preserves exact formatting and key order,
which is rarely needed in practice."
),
))
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{assert_allows, assert_detects_violation};
#[test]
fn test_detects_add_json_column() {
assert_detects_violation!(
AddJsonColumnCheck,
"ALTER TABLE users ADD COLUMN properties JSON;",
"ADD COLUMN with JSON type"
);
}
#[test]
fn test_detects_add_json_column_with_constraint() {
assert_detects_violation!(
AddJsonColumnCheck,
"ALTER TABLE users ADD COLUMN metadata JSON NOT NULL;",
"ADD COLUMN with JSON type"
);
}
#[test]
fn test_allows_add_jsonb_column() {
assert_allows!(
AddJsonColumnCheck,
"ALTER TABLE users ADD COLUMN properties JSONB;"
);
}
#[test]
fn test_allows_add_jsonb_column_with_constraint() {
assert_allows!(
AddJsonColumnCheck,
"ALTER TABLE users ADD COLUMN metadata JSONB NOT NULL;"
);
}
#[test]
fn test_allows_other_column_types() {
assert_allows!(
AddJsonColumnCheck,
"ALTER TABLE users ADD COLUMN name TEXT;"
);
}
#[test]
fn test_allows_create_table_with_json() {
assert_allows!(
AddJsonColumnCheck,
"CREATE TABLE users (id SERIAL PRIMARY KEY, data JSON);"
);
}
#[test]
fn test_ignores_other_alter_operations() {
assert_allows!(
AddJsonColumnCheck,
"ALTER TABLE users DROP COLUMN old_field;"
);
}
#[test]
fn test_ignores_other_statements() {
assert_allows!(AddJsonColumnCheck, "SELECT * FROM users;");
}
}