ApplyLobsterWorkflowLobsterColumnsMigration.java 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. import java.sql.*;
  2. import java.util.ArrayList;
  3. import java.util.List;
  4. /**
  5. * Add missing company_workflow_lobster execution/template columns on tenant DB(s).
  6. * Usage: java -cp mysql-connector-j.jar ApplyLobsterWorkflowLobsterColumnsMigration [tenantId...]
  7. * Default tenantId: 33
  8. */
  9. public class ApplyLobsterWorkflowLobsterColumnsMigration {
  10. private static final String MASTER_HOST = getenv("DB_HOST", "cq-cdb-8fjmemkb.sql.tencentcdb.com");
  11. private static final int MASTER_PORT = Integer.parseInt(getenv("DB_PORT", "27220"));
  12. private static final String MASTER_USER = getenv("DB_USER", "root");
  13. private static final String MASTER_PASSWORD = getenv("DB_PASSWORD", "Ylrz_1q2w3e4r5t6y");
  14. private static final String MASTER_DB = getenv("DB_NAME", "ylrz_saas");
  15. public static void main(String[] args) throws Exception {
  16. if (args.length == 1 && ("master".equalsIgnoreCase(args[0]) || "all".equalsIgnoreCase(args[0]))) {
  17. patchMaster();
  18. if ("master".equalsIgnoreCase(args[0])) {
  19. return;
  20. }
  21. args = new String[] { "all-tenants" };
  22. }
  23. List<Long> tenantIds = new ArrayList<>();
  24. if (args.length == 0) {
  25. tenantIds.add(33L);
  26. } else if (args.length == 1 && "all-tenants".equalsIgnoreCase(args[0])) {
  27. try (Connection master = open(MASTER_HOST, MASTER_PORT, MASTER_DB, MASTER_USER, MASTER_PASSWORD)) {
  28. try (Statement st = master.createStatement();
  29. ResultSet rs = st.executeQuery("SELECT id FROM tenant_info WHERE status = 1 ORDER BY id")) {
  30. while (rs.next()) {
  31. tenantIds.add(rs.getLong("id"));
  32. }
  33. }
  34. }
  35. } else {
  36. for (String arg : args) {
  37. tenantIds.add(Long.parseLong(arg.trim()));
  38. }
  39. }
  40. try (Connection master = open(MASTER_HOST, MASTER_PORT, MASTER_DB, MASTER_USER, MASTER_PASSWORD)) {
  41. for (Long tenantId : tenantIds) {
  42. TenantDb tenant = loadTenant(master, tenantId);
  43. if (tenant == null) {
  44. System.err.println("[SKIP] tenant " + tenantId + " not found");
  45. continue;
  46. }
  47. System.out.println("[RUN] tenant " + tenantId + " -> " + tenant.host + ":" + tenant.port + "/" + tenant.database);
  48. try (Connection tenantConn = open(tenant.host, tenant.port, tenant.database, tenant.user, tenant.password)) {
  49. applyLobsterExecutionConfigTable(tenantConn);
  50. applyWorkflowLobsterColumns(tenantConn);
  51. applyTagBindingColumns(tenantConn);
  52. }
  53. System.out.println("[OK] tenant " + tenantId);
  54. }
  55. }
  56. }
  57. private static void patchMaster() throws SQLException {
  58. System.out.println("[RUN] master -> " + MASTER_DB);
  59. try (Connection conn = open(MASTER_HOST, MASTER_PORT, MASTER_DB, MASTER_USER, MASTER_PASSWORD)) {
  60. if (tableExists(conn, "company_workflow_lobster")) {
  61. applyWorkflowLobsterColumns(conn);
  62. } else {
  63. System.out.println(" skip: company_workflow_lobster not on master");
  64. }
  65. applyTagBindingColumns(conn);
  66. }
  67. System.out.println("[OK] master");
  68. }
  69. private static void applyLobsterExecutionConfigTable(Connection conn) throws SQLException {
  70. if (tableExists(conn, "lobster_execution_config")) {
  71. System.out.println(" table exists: lobster_execution_config");
  72. return;
  73. }
  74. try (Statement st = conn.createStatement()) {
  75. st.execute(
  76. "CREATE TABLE IF NOT EXISTS `lobster_execution_config` ("
  77. + "`id` bigint NOT NULL AUTO_INCREMENT,"
  78. + "`company_id` bigint NOT NULL COMMENT 'tenant company id',"
  79. + "`default_execution_mode` varchar(16) NOT NULL DEFAULT 'HYBRID',"
  80. + "`enable_content_personalization` tinyint NOT NULL DEFAULT 1,"
  81. + "`enable_flow_personalization` tinyint NOT NULL DEFAULT 1,"
  82. + "`strict_fixed_workflow` tinyint NOT NULL DEFAULT 0,"
  83. + "`auto_start_instance_on_tag` tinyint NOT NULL DEFAULT 0,"
  84. + "`create_by` varchar(64) DEFAULT NULL,"
  85. + "`create_time` datetime DEFAULT CURRENT_TIMESTAMP,"
  86. + "`update_by` varchar(64) DEFAULT NULL,"
  87. + "`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,"
  88. + "PRIMARY KEY (`id`),"
  89. + "UNIQUE KEY `uk_company_id` (`company_id`)"
  90. + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='lobster execution policy'"
  91. );
  92. System.out.println(" created table: lobster_execution_config");
  93. }
  94. }
  95. private static void applyWorkflowLobsterColumns(Connection conn) throws SQLException {
  96. addColumnIfMissing(conn, "company_workflow_lobster", "execution_mode",
  97. "ALTER TABLE company_workflow_lobster ADD COLUMN execution_mode varchar(16) DEFAULT NULL "
  98. + "COMMENT 'PROMPT/INSTANCE/HYBRID' AFTER canvas_data");
  99. addColumnIfMissing(conn, "company_workflow_lobster", "enable_content_personalization",
  100. "ALTER TABLE company_workflow_lobster ADD COLUMN enable_content_personalization tinyint DEFAULT NULL "
  101. + "COMMENT 'null=inherit tenant' AFTER execution_mode");
  102. addColumnIfMissing(conn, "company_workflow_lobster", "enable_flow_personalization",
  103. "ALTER TABLE company_workflow_lobster ADD COLUMN enable_flow_personalization tinyint DEFAULT NULL "
  104. + "COMMENT 'null=inherit tenant' AFTER enable_content_personalization");
  105. addColumnIfMissing(conn, "company_workflow_lobster", "strict_fixed_workflow",
  106. "ALTER TABLE company_workflow_lobster ADD COLUMN strict_fixed_workflow tinyint DEFAULT NULL "
  107. + "COMMENT 'null=inherit tenant' AFTER enable_flow_personalization");
  108. addColumnIfMissing(conn, "company_workflow_lobster", "auto_start_instance_on_tag",
  109. "ALTER TABLE company_workflow_lobster ADD COLUMN auto_start_instance_on_tag tinyint DEFAULT NULL "
  110. + "COMMENT 'null=inherit tenant' AFTER strict_fixed_workflow");
  111. addColumnIfMissing(conn, "company_workflow_lobster", "template_kind",
  112. "ALTER TABLE company_workflow_lobster ADD COLUMN template_kind varchar(20) NOT NULL DEFAULT 'MASTER' "
  113. + "COMMENT 'MASTER or TASK_COPY' AFTER auto_start_instance_on_tag");
  114. addColumnIfMissing(conn, "company_workflow_lobster", "source_template_id",
  115. "ALTER TABLE company_workflow_lobster ADD COLUMN source_template_id bigint DEFAULT NULL "
  116. + "COMMENT 'master template id when TASK_COPY' AFTER template_kind");
  117. try (Statement st = conn.createStatement()) {
  118. st.execute("UPDATE company_workflow_lobster SET template_kind = 'MASTER' "
  119. + "WHERE template_kind IS NULL OR template_kind = ''");
  120. }
  121. }
  122. private static void applyTagBindingColumns(Connection conn) throws SQLException {
  123. if (!tableExists(conn, "company_tag_template_binding")) {
  124. try (Statement st = conn.createStatement()) {
  125. st.execute(
  126. "CREATE TABLE IF NOT EXISTS company_tag_template_binding ("
  127. + "id bigint NOT NULL AUTO_INCREMENT,"
  128. + "company_id bigint NOT NULL,"
  129. + "tag_code varchar(128) NOT NULL,"
  130. + "tag_name varchar(128) NOT NULL,"
  131. + "template_id bigint NOT NULL,"
  132. + "template_name varchar(256) DEFAULT NULL,"
  133. + "priority int DEFAULT 0,"
  134. + "match_condition text,"
  135. + "exclude_tags varchar(2000) DEFAULT NULL,"
  136. + "target_channels varchar(500) DEFAULT NULL,"
  137. + "status tinyint DEFAULT 1,"
  138. + "del_flag tinyint DEFAULT 0,"
  139. + "create_by varchar(64) DEFAULT '',"
  140. + "create_time datetime DEFAULT NULL,"
  141. + "update_by varchar(64) DEFAULT '',"
  142. + "update_time datetime DEFAULT NULL,"
  143. + "PRIMARY KEY (id),"
  144. + "KEY idx_cttb_company_del (company_id, del_flag)"
  145. + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
  146. );
  147. System.out.println(" created table: company_tag_template_binding");
  148. }
  149. }
  150. if (!tableExists(conn, "company_tag_template_binding")) {
  151. return;
  152. }
  153. addColumnIfMissing(conn, "company_tag_template_binding", "source_template_id",
  154. "ALTER TABLE company_tag_template_binding ADD COLUMN source_template_id bigint DEFAULT NULL "
  155. + "COMMENT 'master template id' AFTER template_id");
  156. addColumnIfMissing(conn, "company_tag_template_binding", "task_template_id",
  157. "ALTER TABLE company_tag_template_binding ADD COLUMN task_template_id bigint DEFAULT NULL "
  158. + "COMMENT 'task copy template id' AFTER source_template_id");
  159. addColumnIfMissing(conn, "company_tag_template_binding", "exclude_tags",
  160. "ALTER TABLE company_tag_template_binding ADD COLUMN exclude_tags varchar(2000) DEFAULT NULL "
  161. + "COMMENT 'exclude tag codes' AFTER match_condition");
  162. addColumnIfMissing(conn, "company_tag_template_binding", "target_channels",
  163. "ALTER TABLE company_tag_template_binding ADD COLUMN target_channels varchar(500) DEFAULT NULL "
  164. + "COMMENT 'target channels' AFTER exclude_tags");
  165. try (Statement st = conn.createStatement()) {
  166. st.execute("UPDATE company_tag_template_binding SET source_template_id = template_id "
  167. + "WHERE source_template_id IS NULL AND template_id IS NOT NULL");
  168. }
  169. }
  170. private static void addColumnIfMissing(Connection conn, String table, String column, String ddl) throws SQLException {
  171. if (columnExists(conn, table, column)) {
  172. System.out.println(" column exists: " + table + "." + column);
  173. return;
  174. }
  175. try (Statement st = conn.createStatement()) {
  176. st.execute(ddl);
  177. System.out.println(" added column: " + table + "." + column);
  178. }
  179. }
  180. private static boolean tableExists(Connection conn, String table) throws SQLException {
  181. String sql = "SELECT COUNT(*) FROM information_schema.TABLES "
  182. + "WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?";
  183. try (PreparedStatement ps = conn.prepareStatement(sql)) {
  184. ps.setString(1, table);
  185. try (ResultSet rs = ps.executeQuery()) {
  186. return rs.next() && rs.getInt(1) > 0;
  187. }
  188. }
  189. }
  190. private static boolean columnExists(Connection conn, String table, String column) throws SQLException {
  191. String sql = "SELECT COUNT(*) FROM information_schema.COLUMNS "
  192. + "WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?";
  193. try (PreparedStatement ps = conn.prepareStatement(sql)) {
  194. ps.setString(1, table);
  195. ps.setString(2, column);
  196. try (ResultSet rs = ps.executeQuery()) {
  197. return rs.next() && rs.getInt(1) > 0;
  198. }
  199. }
  200. }
  201. private static TenantDb loadTenant(Connection master, long tenantId) throws SQLException {
  202. String sql = "SELECT db_url, db_account, db_pwd FROM tenant_info WHERE id = ? AND status = 1 LIMIT 1";
  203. try (PreparedStatement ps = master.prepareStatement(sql)) {
  204. ps.setLong(1, tenantId);
  205. try (ResultSet rs = ps.executeQuery()) {
  206. if (!rs.next()) {
  207. return null;
  208. }
  209. return parseJdbcUrl(rs.getString("db_url"), rs.getString("db_account"), rs.getString("db_pwd"));
  210. }
  211. }
  212. }
  213. private static TenantDb parseJdbcUrl(String jdbcUrl, String user, String password) {
  214. if (jdbcUrl == null || !jdbcUrl.startsWith("jdbc:mysql://")) {
  215. throw new IllegalArgumentException("Unsupported db_url: " + jdbcUrl);
  216. }
  217. String body = jdbcUrl.substring("jdbc:mysql://".length());
  218. int slash = body.indexOf('/');
  219. if (slash < 0) {
  220. throw new IllegalArgumentException("Invalid db_url: " + jdbcUrl);
  221. }
  222. String hostPort = body.substring(0, slash);
  223. String rest = body.substring(slash + 1);
  224. int q = rest.indexOf('?');
  225. String database = q >= 0 ? rest.substring(0, q) : rest;
  226. String host;
  227. int port;
  228. int colon = hostPort.indexOf(':');
  229. if (colon >= 0) {
  230. host = hostPort.substring(0, colon);
  231. port = Integer.parseInt(hostPort.substring(colon + 1));
  232. } else {
  233. host = hostPort;
  234. port = 3306;
  235. }
  236. TenantDb t = new TenantDb();
  237. t.host = host;
  238. t.port = port;
  239. t.database = database;
  240. t.user = user != null ? user : MASTER_USER;
  241. t.password = password != null ? password : MASTER_PASSWORD;
  242. return t;
  243. }
  244. private static Connection open(String host, int port, String database, String user, String password)
  245. throws SQLException {
  246. String url = "jdbc:mysql://" + host + ":" + port + "/" + database
  247. + "?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&allowMultiQueries=true";
  248. return DriverManager.getConnection(url, user, password);
  249. }
  250. private static String getenv(String key, String defaultValue) {
  251. String v = System.getenv(key);
  252. return v != null && !v.isEmpty() ? v : defaultValue;
  253. }
  254. private static final class TenantDb {
  255. String host;
  256. int port;
  257. String database;
  258. String user;
  259. String password;
  260. }
  261. }