use crate::schema_shared::{ColumnPair, IdKind, SharedSchemaError, validate_ident};
pub use crate::schema_shared::{ColumnPair as MssqlColumnPair, IdKind as MssqlIdKind};
pub const DESC_FLIP_TSQL: &str = include_str!("../sql/mssql/procedures/desc_flip.sql");
pub const DESC_GENERATORS_TSQL: &str = include_str!("../sql/mssql/procedures/desc_generators.sql");
pub const BULK_BACKFILL_TSQL: &str = include_str!("../sql/mssql/procedures/bulk_backfill.sql");
pub const INSTALL_ALL_DESC_TSQL: &str = concat!(
include_str!("../sql/mssql/procedures/desc_flip.sql"),
"\n",
include_str!("../sql/mssql/procedures/desc_generators.sql"),
"\n",
include_str!("../sql/mssql/procedures/bulk_backfill.sql"),
);
pub fn install_autofill_trigger_for_table_mssql(
table: &str,
pairs: &[ColumnPair<'_>],
kind: IdKind,
) -> Result<String, SharedSchemaError> {
assert!(!pairs.is_empty(), "at least one ColumnPair required");
validate_ident(table)?;
for p in pairs {
validate_ident(p.src)?;
validate_ident(p.dst)?;
}
let flip_fn = kind.flip_fn();
let trig_name = format!("zzz_{}_autofill_desc", table);
let mut body = String::new();
for p in pairs {
use std::fmt::Write as _;
writeln!(
body,
" UPDATE t\n \
SET t.{dst} = CASE WHEN t.{src} IS NULL THEN NULL ELSE dbo.{flip_fn}(t.{src}) END\n \
FROM {table} AS t\n \
WHERE t.{src} IN (SELECT {src} FROM inserted WHERE {src} IS NOT NULL);",
dst = p.dst,
src = p.src,
flip_fn = flip_fn,
table = table,
)
.expect("write! into String cannot fail");
}
let script = format!(
"CREATE OR ALTER TRIGGER {trig_name}\n\
ON {table}\n\
AFTER INSERT, UPDATE\n\
AS\n\
BEGIN\n \
SET NOCOUNT ON;\n\
{body}\
END;\n\
GO\n\
EXEC sp_settriggerorder @triggername = N'{trig_name}', @order = 'Last', @stmttype = 'INSERT';\n\
GO\n\
EXEC sp_settriggerorder @triggername = N'{trig_name}', @order = 'Last', @stmttype = 'UPDATE';\n\
GO\n",
trig_name = trig_name,
table = table,
body = body,
);
Ok(script)
}
pub fn drop_autofill_trigger_for_table_mssql(table: &str) -> Result<String, SharedSchemaError> {
validate_ident(table)?;
let trig_name = format!("zzz_{}_autofill_desc", table);
Ok(format!(
"IF OBJECT_ID(N'{trig_name}', N'TR') IS NOT NULL DROP TRIGGER {trig_name};\nGO\n",
trig_name = trig_name,
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn install_script_contains_required_statements() {
let script = install_autofill_trigger_for_table_mssql(
"events",
&[ColumnPair {
src: "id",
dst: "id_desc",
}],
IdKind::Heer,
)
.unwrap();
assert!(script.contains("CREATE OR ALTER TRIGGER zzz_events_autofill_desc"));
assert!(script.contains("AFTER INSERT, UPDATE"));
assert!(script.contains("sp_settriggerorder"));
assert!(script.contains("@stmttype = 'INSERT'"));
assert!(script.contains("@stmttype = 'UPDATE'"));
assert!(script.contains("dbo.heerid_to_desc"));
}
#[test]
fn install_script_uses_ranjid_flip_for_ranj_kind() {
let script = install_autofill_trigger_for_table_mssql(
"audit_log",
&[ColumnPair {
src: "id",
dst: "id_desc",
}],
IdKind::Ranj,
)
.unwrap();
assert!(script.contains("dbo.ranjid_to_desc"));
assert!(!script.contains("dbo.heerid_to_desc"));
}
#[test]
fn multi_pair_emits_branch_per_pair() {
let script = install_autofill_trigger_for_table_mssql(
"nodes",
&[
ColumnPair {
src: "id",
dst: "id_desc",
},
ColumnPair {
src: "parent_id",
dst: "parent_id_desc",
},
],
IdKind::Heer,
)
.unwrap();
assert_eq!(script.matches("UPDATE t").count(), 2);
assert!(script.contains("id_desc"));
assert!(script.contains("parent_id_desc"));
}
#[test]
fn rejects_invalid_table_identifier() {
let err = install_autofill_trigger_for_table_mssql(
"tbl; DROP TABLE users",
&[ColumnPair {
src: "id",
dst: "id_desc",
}],
IdKind::Heer,
)
.unwrap_err();
match err {
SharedSchemaError::InvalidIdentifier(s) => {
assert!(s.contains("DROP"));
}
}
}
#[test]
fn rejects_invalid_column_identifier() {
let err = install_autofill_trigger_for_table_mssql(
"events",
&[ColumnPair {
src: "id",
dst: "id_desc; --",
}],
IdKind::Heer,
)
.unwrap_err();
assert!(matches!(err, SharedSchemaError::InvalidIdentifier(_)));
}
#[test]
#[should_panic(expected = "at least one ColumnPair required")]
fn empty_pairs_panics() {
let _ = install_autofill_trigger_for_table_mssql("events", &[], IdKind::Heer);
}
#[test]
fn drop_script_is_idempotent_guarded() {
let script = drop_autofill_trigger_for_table_mssql("events").unwrap();
assert!(script.contains("IF OBJECT_ID"));
assert!(script.contains("DROP TRIGGER zzz_events_autofill_desc"));
}
#[test]
fn drop_rejects_invalid_identifier() {
let err = drop_autofill_trigger_for_table_mssql("bad name").unwrap_err();
assert!(matches!(err, SharedSchemaError::InvalidIdentifier(_)));
}
#[test]
fn install_all_desc_tsql_has_all_three_blobs() {
assert!(INSTALL_ALL_DESC_TSQL.contains("CREATE OR ALTER FUNCTION dbo.heerid_flip_mask"));
assert!(INSTALL_ALL_DESC_TSQL.contains("CREATE OR ALTER PROCEDURE dbo.heerid_next_desc"));
assert!(
INSTALL_ALL_DESC_TSQL.contains("CREATE OR ALTER PROCEDURE dbo.heeranjid_bulk_backfill")
);
}
#[test]
fn type_aliases_exported() {
let _pair: MssqlColumnPair<'_> = MssqlColumnPair {
src: "id",
dst: "id_desc",
};
let _kind: MssqlIdKind = MssqlIdKind::Heer;
}
}