package joist.codegen.passes;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import javax.sql.DataSource;
import joist.codegen.InformationSchemaColumn;
import joist.codegen.Schema;
import joist.codegen.dtos.Entity;
import joist.jdbc.Jdbc;
import joist.util.StringBuilderr;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Automatically adds INSERT/UPDATE/DELETE triggers to all entity tables in the schema that log changes
* to an {@code history_entry} table.
*
* @author larry
*/
public class MySqlHistoryTriggersPass implements Pass<Schema> {
private static final Logger log = LoggerFactory.getLogger(MySqlHistoryTriggersPass.class);
private final Set<Pattern> skippedTables = new HashSet<Pattern>();
private final Set<Pattern> skippedColumns = new HashSet<Pattern>();
private final String historyTableName;
private DataSource ds;
public MySqlHistoryTriggersPass() {
this("history_entry");
}
public MySqlHistoryTriggersPass(String historyTableName) {
this.historyTableName = historyTableName;
this.skipTable(historyTableName);
}
public void pass(Schema schema) {
log.info("Updating history triggers");
this.ds = schema.getConfig().dbAppSaSettings.getDataSource();
for (Entity entity : schema.getEntities().values()) {
// always try to drop the trigger
this.dropExistingTriggers(entity.getTableName());
if (!this.shouldSkipTable(entity.getTableName())) {
if (entity.isRoot()) {
this.createInsertTrigger(entity.getTableName());
}
this.createUpdateTrigger(schema, entity.getTableName(), entity.getRootEntity().getTableName());
this.createDeleteTrigger(schema, entity.getTableName(), entity.getRootEntity().getTableName());
}
}
}
/** @param regex regular expression of tables to ignore, {@code some_table} */
public void skipTable(String regex) {
this.skippedTables.add(Pattern.compile(regex));
}
/** @param regex regular expression of columns to ignore, {@code some_table.the_column} */
public void skipColumn(String regex) {
this.skippedColumns.add(Pattern.compile(regex));
}
private boolean shouldSkipTable(String tableName) {
for (Pattern pattern : this.skippedTables) {
if (pattern.matcher(tableName).matches()) {
return true;
}
}
return false;
}
private boolean shouldSkipColumn(String tableName, String columnName) {
if ("version".equals(columnName) || "id".equals(columnName)) {
return true;
}
String matchAgainst = tableName + "." + columnName;
for (Pattern pattern : this.skippedColumns) {
if (pattern.matcher(matchAgainst).matches()) {
return true;
}
}
return false;
}
private void dropExistingTriggers(String table) {
Jdbc.update(this.ds, "DROP TRIGGER IF EXISTS " + table + "_history_update");
Jdbc.update(this.ds, "DROP TRIGGER IF EXISTS " + table + "_history_insert");
Jdbc.update(this.ds, "DROP TRIGGER IF EXISTS " + table + "_history_delete");
}
private void createInsertTrigger(String tableName) {
StringBuilderr sql = new StringBuilderr();
sql.line("CREATE TRIGGER {}_history_insert AFTER INSERT ON {}", tableName, tableName);
sql.line("FOR EACH ROW");
sql.line("BEGIN");
sql.line(" INSERT INTO {}", this.historyTableName);
sql.line(" (type, root_table_name, primary_key, property_name, old_value, new_value, updater, update_time, version)");
sql.line(" VALUES");
sql.line(" ('insert', '{}', NEW.id, null, null, null, @updater, now(), 1);", tableName);
sql.line("END;");
Jdbc.update(this.ds, sql.toString());
}
private void createDeleteTrigger(Schema schema, String tableName, String rootTableName) {
StringBuilderr sql = new StringBuilderr();
sql.line("CREATE TRIGGER {}_history_delete AFTER DELETE ON {}", tableName, tableName);
sql.line("FOR EACH ROW");
sql.line("BEGIN");
for (InformationSchemaColumn c : this.columnsForTable(schema, tableName)) {
if (this.shouldSkipColumn(tableName, c.name)) {
continue;
}
sql.line(" INSERT INTO {}", this.historyTableName);
sql.line(" (type, root_table_name, primary_key, property_name, old_value, new_value, updater, update_time, version)");
sql.line(" VALUES");
sql.line(" ('delete', '{}', OLD.id, '{}', {}, null, @updater, now(), 1);", rootTableName, c.name, snippet("OLD", c));
}
sql.line("END;");
Jdbc.update(this.ds, sql.toString());
}
private void createUpdateTrigger(Schema schema, String tableName, String rootTableName) {
StringBuilderr sql = new StringBuilderr();
sql.line("CREATE TRIGGER {}_history_update AFTER UPDATE ON {}", tableName, tableName);
sql.line("FOR EACH ROW");
sql.line("BEGIN");
for (InformationSchemaColumn c : this.columnsForTable(schema, tableName)) {
if (this.shouldSkipColumn(tableName, c.name)) {
continue;
}
sql.line("IF NOT BINARY NEW.{} <=> BINARY OLD.{} THEN", c.name, c.name);
sql.line(" INSERT INTO {}", this.historyTableName);
sql.line(" (type, root_table_name, primary_key, property_name, old_value, new_value, updater, update_time, version)");
sql.line(" VALUES");
sql.line(" ('update', '{}', NEW.id, '{}', {}, {}, @updater, now(), 1);", //
rootTableName,
c.name,
snippet("OLD", c),
snippet("NEW", c));
sql.line("END IF;");
}
sql.line("END;");
Jdbc.update(this.ds, sql.toString());
}
private static String snippet(String prefix, InformationSchemaColumn c) {
String fullName = prefix + "." + c.name;
if ("bit".equals(c.dataType)) {
// switch byte 0/byte 1 to true/false, assuming this is a BooleanColumn
return "if(" + fullName + " = 1, 'true', 'false')";
} else {
// use default to string
return "left(" + fullName + ", 255)";
}
}
private List<InformationSchemaColumn> columnsForTable(Schema schema, String table) {
List<InformationSchemaColumn> cols = new ArrayList<InformationSchemaColumn>();
for (InformationSchemaColumn c : schema.getColumns()) {
if (c.tableName.equals(table)) {
cols.add(c);
}
}
return cols;
}
}