feat: 增加 MVP-1 可逆迁移与 admin 基线 (#20)
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
-- Keep this guard durable across a failed down attempt. It rejects a rollback before
|
||||
-- any baseline table is dropped when an administrator has created business data.
|
||||
CREATE TABLE IF NOT EXISTS chorus_admin_baseline_down_guard (
|
||||
id TINYINT NOT NULL,
|
||||
CONSTRAINT chk_chorus_admin_baseline_down_guard CHECK (id = 0)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
DELETE FROM chorus_admin_baseline_down_guard;
|
||||
INSERT INTO chorus_admin_baseline_down_guard (id)
|
||||
SELECT 1
|
||||
WHERE EXISTS (SELECT 1 FROM sys_user)
|
||||
OR EXISTS (SELECT 1 FROM sys_role)
|
||||
OR EXISTS (SELECT 1 FROM sys_dept)
|
||||
OR EXISTS (SELECT 1 FROM sys_post)
|
||||
OR EXISTS (SELECT 1 FROM sys_menu)
|
||||
OR EXISTS (SELECT 1 FROM sys_api)
|
||||
OR EXISTS (SELECT 1 FROM sys_config)
|
||||
OR EXISTS (SELECT 1 FROM sys_dict_type)
|
||||
OR EXISTS (SELECT 1 FROM sys_dict_data)
|
||||
OR EXISTS (SELECT 1 FROM sys_job)
|
||||
OR EXISTS (SELECT 1 FROM sys_login_log)
|
||||
OR EXISTS (SELECT 1 FROM sys_opera_log)
|
||||
OR EXISTS (SELECT 1 FROM sys_tables)
|
||||
OR EXISTS (SELECT 1 FROM sys_columns)
|
||||
OR EXISTS (SELECT 1 FROM sys_casbin_rule);
|
||||
|
||||
DROP TABLE sys_columns;
|
||||
DROP TABLE sys_tables;
|
||||
DROP TABLE sys_opera_log;
|
||||
DROP TABLE sys_login_log;
|
||||
DROP TABLE sys_job;
|
||||
DROP TABLE sys_dict_data;
|
||||
DROP TABLE sys_dict_type;
|
||||
DROP TABLE sys_config;
|
||||
DROP TABLE sys_menu_api_rule;
|
||||
DROP TABLE sys_role_dept;
|
||||
DROP TABLE sys_role_menu;
|
||||
DROP TABLE sys_casbin_rule;
|
||||
DROP TABLE sys_api;
|
||||
DROP TABLE sys_menu;
|
||||
DROP TABLE sys_user;
|
||||
DROP TABLE sys_post;
|
||||
DROP TABLE sys_dept;
|
||||
DROP TABLE sys_role;
|
||||
DROP TABLE chorus_admin_baseline_down_guard;
|
||||
@@ -0,0 +1,401 @@
|
||||
-- The audited minimum schema required by go-admin f06540883b41d03782bb6b2c4150f298f328c6b6.
|
||||
-- It intentionally contains no administrator account or default system data; 000004 owns
|
||||
-- Chorus-specific roles, menus, API entries, and permissions.
|
||||
|
||||
CREATE TABLE sys_role (
|
||||
role_id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
role_name VARCHAR(128) NOT NULL,
|
||||
status VARCHAR(4) NOT NULL,
|
||||
role_key VARCHAR(128) NOT NULL,
|
||||
role_sort INT NOT NULL DEFAULT 0,
|
||||
flag VARCHAR(128) NOT NULL DEFAULT '',
|
||||
remark VARCHAR(255) NOT NULL DEFAULT '',
|
||||
admin BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
data_scope VARCHAR(128) NOT NULL DEFAULT '',
|
||||
create_by BIGINT NOT NULL DEFAULT 0,
|
||||
update_by BIGINT NOT NULL DEFAULT 0,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
deleted_at DATETIME(3) NULL,
|
||||
PRIMARY KEY (role_id),
|
||||
UNIQUE KEY uq_sys_role_role_key (role_key),
|
||||
KEY idx_sys_role_deleted_at (deleted_at),
|
||||
KEY idx_sys_role_create_by (create_by),
|
||||
KEY idx_sys_role_update_by (update_by)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE sys_dept (
|
||||
dept_id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
parent_id BIGINT NOT NULL DEFAULT 0,
|
||||
dept_path VARCHAR(255) NOT NULL DEFAULT '',
|
||||
dept_name VARCHAR(128) NOT NULL,
|
||||
sort INT NOT NULL DEFAULT 0,
|
||||
leader VARCHAR(128) NOT NULL DEFAULT '',
|
||||
phone VARCHAR(11) NOT NULL DEFAULT '',
|
||||
email VARCHAR(64) NOT NULL DEFAULT '',
|
||||
status INT NOT NULL DEFAULT 1,
|
||||
create_by BIGINT NOT NULL DEFAULT 0,
|
||||
update_by BIGINT NOT NULL DEFAULT 0,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
deleted_at DATETIME(3) NULL,
|
||||
PRIMARY KEY (dept_id),
|
||||
KEY idx_sys_dept_deleted_at (deleted_at),
|
||||
KEY idx_sys_dept_create_by (create_by),
|
||||
KEY idx_sys_dept_update_by (update_by)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE sys_post (
|
||||
post_id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
post_name VARCHAR(128) NOT NULL,
|
||||
post_code VARCHAR(128) NOT NULL,
|
||||
sort INT NOT NULL DEFAULT 0,
|
||||
status INT NOT NULL DEFAULT 1,
|
||||
remark VARCHAR(255) NOT NULL DEFAULT '',
|
||||
create_by BIGINT NOT NULL DEFAULT 0,
|
||||
update_by BIGINT NOT NULL DEFAULT 0,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
deleted_at DATETIME(3) NULL,
|
||||
PRIMARY KEY (post_id),
|
||||
KEY idx_sys_post_deleted_at (deleted_at),
|
||||
KEY idx_sys_post_create_by (create_by),
|
||||
KEY idx_sys_post_update_by (update_by)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE sys_user (
|
||||
user_id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
username VARCHAR(64) NOT NULL,
|
||||
password VARCHAR(128) NOT NULL,
|
||||
nick_name VARCHAR(128) NOT NULL DEFAULT '',
|
||||
phone VARCHAR(11) NOT NULL DEFAULT '',
|
||||
role_id BIGINT NOT NULL,
|
||||
salt VARCHAR(255) NOT NULL DEFAULT '',
|
||||
avatar VARCHAR(255) NOT NULL DEFAULT '',
|
||||
sex VARCHAR(255) NOT NULL DEFAULT '',
|
||||
email VARCHAR(128) NOT NULL DEFAULT '',
|
||||
dept_id BIGINT NOT NULL DEFAULT 0,
|
||||
post_id BIGINT NOT NULL DEFAULT 0,
|
||||
remark VARCHAR(255) NOT NULL DEFAULT '',
|
||||
status VARCHAR(4) NOT NULL,
|
||||
create_by BIGINT NOT NULL DEFAULT 0,
|
||||
update_by BIGINT NOT NULL DEFAULT 0,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
deleted_at DATETIME(3) NULL,
|
||||
PRIMARY KEY (user_id),
|
||||
UNIQUE KEY uq_sys_user_username (username),
|
||||
KEY idx_sys_user_deleted_at (deleted_at),
|
||||
KEY idx_sys_user_create_by (create_by),
|
||||
KEY idx_sys_user_update_by (update_by),
|
||||
CONSTRAINT fk_sys_user_role FOREIGN KEY (role_id) REFERENCES sys_role (role_id) ON DELETE RESTRICT
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE sys_menu (
|
||||
menu_id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
menu_name VARCHAR(128) NOT NULL,
|
||||
title VARCHAR(128) NOT NULL,
|
||||
icon VARCHAR(128) NOT NULL DEFAULT '',
|
||||
path VARCHAR(128) NOT NULL,
|
||||
paths VARCHAR(128) NOT NULL DEFAULT '',
|
||||
menu_type VARCHAR(1) NOT NULL,
|
||||
action VARCHAR(16) NOT NULL DEFAULT '',
|
||||
permission VARCHAR(255) NOT NULL DEFAULT '',
|
||||
parent_id BIGINT NOT NULL DEFAULT 0,
|
||||
no_cache BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
breadcrumb VARCHAR(255) NOT NULL DEFAULT '',
|
||||
component VARCHAR(255) NOT NULL DEFAULT '',
|
||||
sort INT NOT NULL DEFAULT 0,
|
||||
visible VARCHAR(1) NOT NULL DEFAULT '0',
|
||||
is_frame VARCHAR(1) NOT NULL DEFAULT '0',
|
||||
create_by BIGINT NOT NULL DEFAULT 0,
|
||||
update_by BIGINT NOT NULL DEFAULT 0,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
deleted_at DATETIME(3) NULL,
|
||||
PRIMARY KEY (menu_id),
|
||||
UNIQUE KEY uq_sys_menu_path (path),
|
||||
KEY idx_sys_menu_deleted_at (deleted_at),
|
||||
KEY idx_sys_menu_create_by (create_by),
|
||||
KEY idx_sys_menu_update_by (update_by)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE sys_api (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
handle VARCHAR(128) NOT NULL,
|
||||
title VARCHAR(128) NOT NULL,
|
||||
path VARCHAR(128) NOT NULL,
|
||||
type VARCHAR(16) NOT NULL,
|
||||
action VARCHAR(16) NOT NULL,
|
||||
create_by BIGINT NOT NULL DEFAULT 0,
|
||||
update_by BIGINT NOT NULL DEFAULT 0,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
deleted_at DATETIME(3) NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_sys_api_path_action (path, action),
|
||||
UNIQUE KEY uq_sys_api_handle (handle),
|
||||
KEY idx_sys_api_deleted_at (deleted_at),
|
||||
KEY idx_sys_api_create_by (create_by),
|
||||
KEY idx_sys_api_update_by (update_by)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE sys_role_menu (
|
||||
role_id BIGINT NOT NULL,
|
||||
menu_id BIGINT NOT NULL,
|
||||
PRIMARY KEY (role_id, menu_id),
|
||||
CONSTRAINT fk_sys_role_menu_role FOREIGN KEY (role_id) REFERENCES sys_role (role_id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_sys_role_menu_menu FOREIGN KEY (menu_id) REFERENCES sys_menu (menu_id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE sys_role_dept (
|
||||
role_id BIGINT NOT NULL,
|
||||
dept_id BIGINT NOT NULL,
|
||||
PRIMARY KEY (role_id, dept_id),
|
||||
CONSTRAINT fk_sys_role_dept_role FOREIGN KEY (role_id) REFERENCES sys_role (role_id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_sys_role_dept_dept FOREIGN KEY (dept_id) REFERENCES sys_dept (dept_id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE sys_menu_api_rule (
|
||||
menu_id BIGINT NOT NULL,
|
||||
sys_api_id BIGINT NOT NULL,
|
||||
PRIMARY KEY (menu_id, sys_api_id),
|
||||
CONSTRAINT fk_sys_menu_api_rule_menu FOREIGN KEY (menu_id) REFERENCES sys_menu (menu_id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_sys_menu_api_rule_api FOREIGN KEY (sys_api_id) REFERENCES sys_api (id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE sys_casbin_rule (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
ptype VARCHAR(512) NOT NULL,
|
||||
v0 VARCHAR(512) NOT NULL DEFAULT '',
|
||||
v1 VARCHAR(512) NOT NULL DEFAULT '',
|
||||
v2 VARCHAR(512) NOT NULL DEFAULT '',
|
||||
v3 VARCHAR(512) NOT NULL DEFAULT '',
|
||||
v4 VARCHAR(512) NOT NULL DEFAULT '',
|
||||
v5 VARCHAR(512) NOT NULL DEFAULT '',
|
||||
rule_hash BINARY(32) GENERATED ALWAYS AS (
|
||||
UNHEX(SHA2(CONCAT_WS(CHAR(31), ptype, v0, v1, v2, v3, v4, v5), 256))
|
||||
) STORED,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_sys_casbin_rule (rule_hash)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE sys_config (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
config_name VARCHAR(128) NOT NULL,
|
||||
config_key VARCHAR(128) NOT NULL,
|
||||
config_value VARCHAR(255) NOT NULL DEFAULT '',
|
||||
config_type VARCHAR(64) NOT NULL DEFAULT '',
|
||||
is_frontend INT NOT NULL DEFAULT 0,
|
||||
remark VARCHAR(128) NOT NULL DEFAULT '',
|
||||
create_by BIGINT NOT NULL DEFAULT 0,
|
||||
update_by BIGINT NOT NULL DEFAULT 0,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
deleted_at DATETIME(3) NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_sys_config_deleted_at (deleted_at),
|
||||
KEY idx_sys_config_create_by (create_by),
|
||||
KEY idx_sys_config_update_by (update_by)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE sys_dict_type (
|
||||
dict_id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
dict_name VARCHAR(128) NOT NULL,
|
||||
dict_type VARCHAR(128) NOT NULL,
|
||||
status INT NOT NULL DEFAULT 1,
|
||||
remark VARCHAR(255) NOT NULL DEFAULT '',
|
||||
create_by BIGINT NOT NULL DEFAULT 0,
|
||||
update_by BIGINT NOT NULL DEFAULT 0,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
deleted_at DATETIME(3) NULL,
|
||||
PRIMARY KEY (dict_id),
|
||||
KEY idx_sys_dict_type_deleted_at (deleted_at),
|
||||
KEY idx_sys_dict_type_create_by (create_by),
|
||||
KEY idx_sys_dict_type_update_by (update_by)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE sys_dict_data (
|
||||
dict_code BIGINT NOT NULL AUTO_INCREMENT,
|
||||
dict_sort INT NOT NULL DEFAULT 0,
|
||||
dict_label VARCHAR(128) NOT NULL,
|
||||
dict_value VARCHAR(255) NOT NULL,
|
||||
dict_type VARCHAR(64) NOT NULL,
|
||||
css_class VARCHAR(128) NOT NULL DEFAULT '',
|
||||
list_class VARCHAR(128) NOT NULL DEFAULT '',
|
||||
is_default VARCHAR(8) NOT NULL DEFAULT '',
|
||||
status INT NOT NULL DEFAULT 1,
|
||||
`default` VARCHAR(8) NOT NULL DEFAULT '',
|
||||
remark VARCHAR(255) NOT NULL DEFAULT '',
|
||||
create_by BIGINT NOT NULL DEFAULT 0,
|
||||
update_by BIGINT NOT NULL DEFAULT 0,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
deleted_at DATETIME(3) NULL,
|
||||
PRIMARY KEY (dict_code),
|
||||
KEY idx_sys_dict_data_deleted_at (deleted_at),
|
||||
KEY idx_sys_dict_data_create_by (create_by),
|
||||
KEY idx_sys_dict_data_update_by (update_by)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE sys_job (
|
||||
job_id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
job_name VARCHAR(255) NOT NULL,
|
||||
job_group VARCHAR(255) NOT NULL,
|
||||
job_type INT NOT NULL DEFAULT 0,
|
||||
cron_expression VARCHAR(255) NOT NULL DEFAULT '',
|
||||
invoke_target VARCHAR(255) NOT NULL DEFAULT '',
|
||||
args VARCHAR(255) NOT NULL DEFAULT '',
|
||||
misfire_policy INT NOT NULL DEFAULT 0,
|
||||
concurrent INT NOT NULL DEFAULT 0,
|
||||
status INT NOT NULL DEFAULT 1,
|
||||
entry_id INT NOT NULL DEFAULT 0,
|
||||
create_by BIGINT NOT NULL DEFAULT 0,
|
||||
update_by BIGINT NOT NULL DEFAULT 0,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
deleted_at DATETIME(3) NULL,
|
||||
PRIMARY KEY (job_id),
|
||||
KEY idx_sys_job_deleted_at (deleted_at),
|
||||
KEY idx_sys_job_create_by (create_by),
|
||||
KEY idx_sys_job_update_by (update_by)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE sys_login_log (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
username VARCHAR(128) NOT NULL DEFAULT '',
|
||||
status VARCHAR(4) NOT NULL DEFAULT '',
|
||||
ipaddr VARCHAR(255) NOT NULL DEFAULT '',
|
||||
login_location VARCHAR(255) NOT NULL DEFAULT '',
|
||||
browser VARCHAR(255) NOT NULL DEFAULT '',
|
||||
os VARCHAR(255) NOT NULL DEFAULT '',
|
||||
platform VARCHAR(255) NOT NULL DEFAULT '',
|
||||
login_time TIMESTAMP NULL,
|
||||
remark VARCHAR(255) NOT NULL DEFAULT '',
|
||||
msg VARCHAR(255) NOT NULL DEFAULT '',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
create_by BIGINT NOT NULL DEFAULT 0,
|
||||
update_by BIGINT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_sys_login_log_create_by (create_by),
|
||||
KEY idx_sys_login_log_update_by (update_by)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE sys_opera_log (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
title VARCHAR(255) NOT NULL DEFAULT '',
|
||||
business_type VARCHAR(128) NOT NULL DEFAULT '',
|
||||
business_types VARCHAR(128) NOT NULL DEFAULT '',
|
||||
method VARCHAR(128) NOT NULL DEFAULT '',
|
||||
request_method VARCHAR(128) NOT NULL DEFAULT '',
|
||||
operator_type VARCHAR(128) NOT NULL DEFAULT '',
|
||||
oper_name VARCHAR(128) NOT NULL DEFAULT '',
|
||||
dept_name VARCHAR(128) NOT NULL DEFAULT '',
|
||||
oper_url VARCHAR(255) NOT NULL DEFAULT '',
|
||||
oper_ip VARCHAR(128) NOT NULL DEFAULT '',
|
||||
oper_location VARCHAR(128) NOT NULL DEFAULT '',
|
||||
oper_param TEXT NOT NULL,
|
||||
status VARCHAR(4) NOT NULL DEFAULT '',
|
||||
oper_time TIMESTAMP NULL,
|
||||
json_result VARCHAR(255) NOT NULL DEFAULT '',
|
||||
remark VARCHAR(255) NOT NULL DEFAULT '',
|
||||
latency_time VARCHAR(128) NOT NULL DEFAULT '',
|
||||
user_agent VARCHAR(255) NOT NULL DEFAULT '',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
create_by BIGINT NOT NULL DEFAULT 0,
|
||||
update_by BIGINT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_sys_opera_log_create_by (create_by),
|
||||
KEY idx_sys_opera_log_update_by (update_by)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE sys_tables (
|
||||
table_id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
table_name VARCHAR(255) NOT NULL,
|
||||
table_comment VARCHAR(255) NOT NULL DEFAULT '',
|
||||
class_name VARCHAR(255) NOT NULL DEFAULT '',
|
||||
tpl_category VARCHAR(255) NOT NULL DEFAULT '',
|
||||
package_name VARCHAR(255) NOT NULL DEFAULT '',
|
||||
module_name VARCHAR(255) NOT NULL DEFAULT '',
|
||||
module_front_name VARCHAR(255) NOT NULL DEFAULT '',
|
||||
business_name VARCHAR(255) NOT NULL DEFAULT '',
|
||||
function_name VARCHAR(255) NOT NULL DEFAULT '',
|
||||
function_author VARCHAR(255) NOT NULL DEFAULT '',
|
||||
pk_column VARCHAR(255) NOT NULL DEFAULT '',
|
||||
pk_go_field VARCHAR(255) NOT NULL DEFAULT '',
|
||||
pk_json_field VARCHAR(255) NOT NULL DEFAULT '',
|
||||
options VARCHAR(255) NOT NULL DEFAULT '',
|
||||
tree_code VARCHAR(255) NOT NULL DEFAULT '',
|
||||
tree_parent_code VARCHAR(255) NOT NULL DEFAULT '',
|
||||
tree_name VARCHAR(255) NOT NULL DEFAULT '',
|
||||
tree BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
crud BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
remark VARCHAR(255) NOT NULL DEFAULT '',
|
||||
is_data_scope INT NOT NULL DEFAULT 0,
|
||||
is_actions INT NOT NULL DEFAULT 0,
|
||||
is_auth INT NOT NULL DEFAULT 0,
|
||||
is_logical_delete VARCHAR(1) NOT NULL DEFAULT '0',
|
||||
logical_delete BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
logical_delete_column VARCHAR(128) NOT NULL DEFAULT '',
|
||||
create_by BIGINT NOT NULL DEFAULT 0,
|
||||
update_by BIGINT NOT NULL DEFAULT 0,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
deleted_at DATETIME(3) NULL,
|
||||
PRIMARY KEY (table_id),
|
||||
UNIQUE KEY uq_sys_tables_table_name (table_name),
|
||||
KEY idx_sys_tables_deleted_at (deleted_at),
|
||||
KEY idx_sys_tables_create_by (create_by),
|
||||
KEY idx_sys_tables_update_by (update_by)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE sys_columns (
|
||||
column_id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
table_id BIGINT NOT NULL,
|
||||
column_name VARCHAR(128) NOT NULL,
|
||||
column_comment VARCHAR(128) NOT NULL DEFAULT '',
|
||||
column_type VARCHAR(128) NOT NULL DEFAULT '',
|
||||
go_type VARCHAR(128) NOT NULL DEFAULT '',
|
||||
go_field VARCHAR(128) NOT NULL DEFAULT '',
|
||||
json_field VARCHAR(128) NOT NULL DEFAULT '',
|
||||
is_pk VARCHAR(4) NOT NULL DEFAULT '',
|
||||
is_increment VARCHAR(4) NOT NULL DEFAULT '',
|
||||
is_required VARCHAR(4) NOT NULL DEFAULT '',
|
||||
is_insert VARCHAR(4) NOT NULL DEFAULT '',
|
||||
is_edit VARCHAR(4) NOT NULL DEFAULT '',
|
||||
is_list VARCHAR(4) NOT NULL DEFAULT '',
|
||||
is_query VARCHAR(4) NOT NULL DEFAULT '',
|
||||
query_type VARCHAR(128) NOT NULL DEFAULT '',
|
||||
html_type VARCHAR(128) NOT NULL DEFAULT '',
|
||||
dict_type VARCHAR(128) NOT NULL DEFAULT '',
|
||||
sort INT NOT NULL DEFAULT 0,
|
||||
`list` VARCHAR(1) NOT NULL DEFAULT '0',
|
||||
pk BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
required BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
super_column BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
usable_column BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
increment BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
`insert` BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
edit BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
query BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
remark VARCHAR(255) NOT NULL DEFAULT '',
|
||||
fk_table_name VARCHAR(255) NOT NULL DEFAULT '',
|
||||
fk_table_name_class VARCHAR(255) NOT NULL DEFAULT '',
|
||||
fk_table_name_package VARCHAR(255) NOT NULL DEFAULT '',
|
||||
fk_label_id VARCHAR(255) NOT NULL DEFAULT '',
|
||||
fk_label_name VARCHAR(255) NOT NULL DEFAULT '',
|
||||
create_by BIGINT NOT NULL DEFAULT 0,
|
||||
update_by BIGINT NOT NULL DEFAULT 0,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
deleted_at DATETIME(3) NULL,
|
||||
PRIMARY KEY (column_id),
|
||||
UNIQUE KEY uq_sys_columns_table_column (table_id, column_name),
|
||||
KEY idx_sys_columns_deleted_at (deleted_at),
|
||||
KEY idx_sys_columns_create_by (create_by),
|
||||
KEY idx_sys_columns_update_by (update_by),
|
||||
CONSTRAINT fk_sys_columns_table FOREIGN KEY (table_id) REFERENCES sys_tables (table_id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
@@ -0,0 +1,63 @@
|
||||
-- MVP-0 did not understand x-goog-api-key. Reject the rollback before changing any
|
||||
-- application table if a post-MVP-0 provider would lose its original invariant.
|
||||
CREATE TABLE IF NOT EXISTS chorus_mvp1_routing_down_guard (
|
||||
id TINYINT NOT NULL,
|
||||
CONSTRAINT chk_chorus_mvp1_routing_down_guard CHECK (id = 0)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
DELETE FROM chorus_mvp1_routing_down_guard;
|
||||
INSERT INTO chorus_mvp1_routing_down_guard (id)
|
||||
SELECT 1 WHERE EXISTS (SELECT 1 FROM providers WHERE auth_type = 'x-goog-api-key');
|
||||
|
||||
ALTER TABLE generations
|
||||
DROP FOREIGN KEY fk_generations_route_pool,
|
||||
DROP FOREIGN KEY fk_generations_prompt_template,
|
||||
DROP CHECK chk_generations_route_snapshot,
|
||||
DROP CHECK chk_generations_provider_attempt_count,
|
||||
DROP INDEX idx_generations_route_pool,
|
||||
DROP INDEX idx_generations_prompt_template,
|
||||
DROP COLUMN provider_attempt_count,
|
||||
DROP COLUMN role_rule,
|
||||
DROP COLUMN route_snapshot,
|
||||
DROP COLUMN prompt_template_id,
|
||||
DROP COLUMN route_pool_version,
|
||||
DROP COLUMN route_pool_id;
|
||||
|
||||
ALTER TABLE generation_inputs
|
||||
DROP CHECK chk_generation_inputs_note,
|
||||
DROP COLUMN note;
|
||||
|
||||
ALTER TABLE prompt_templates
|
||||
DROP CHECK chk_prompt_templates_capability,
|
||||
DROP COLUMN default_role_rule,
|
||||
DROP COLUMN capability;
|
||||
|
||||
ALTER TABLE providers
|
||||
DROP CHECK chk_providers_auth_mvp1;
|
||||
|
||||
UPDATE providers p
|
||||
JOIN provider_credentials c ON c.id = p.active_credential_id
|
||||
SET p.api_key_enc = c.api_key_enc;
|
||||
|
||||
ALTER TABLE providers
|
||||
DROP FOREIGN KEY fk_providers_active_credential,
|
||||
DROP INDEX uq_providers_active_credential,
|
||||
DROP COLUMN active_credential_id,
|
||||
ADD CONSTRAINT chk_providers_auth CHECK (
|
||||
(auth_type = 'none' AND api_key_enc IS NULL) OR
|
||||
(auth_type = 'bearer' AND api_key_enc IS NOT NULL)
|
||||
);
|
||||
|
||||
ALTER TABLE provider_models
|
||||
DROP FOREIGN KEY fk_provider_models_provider_mvp1,
|
||||
ADD CONSTRAINT fk_provider_models_provider FOREIGN KEY (provider_id) REFERENCES providers (id) ON DELETE CASCADE;
|
||||
|
||||
DROP TABLE admin_audit_events;
|
||||
DROP TABLE provider_connectivity_checks;
|
||||
DROP TABLE route_member_runtime;
|
||||
DROP TABLE active_routes;
|
||||
DROP TABLE route_pool_members;
|
||||
DROP TABLE route_pools;
|
||||
DROP TABLE provider_model_capabilities;
|
||||
DROP TABLE provider_credentials;
|
||||
DROP TABLE chorus_mvp1_routing_down_guard;
|
||||
@@ -0,0 +1,246 @@
|
||||
-- Validate that a pre-MVP-1 Gemini template or model has not been configured without
|
||||
-- the explicit capability that MVP-1 requires. The guard remains only after a failed run.
|
||||
CREATE TABLE IF NOT EXISTS chorus_mvp1_routing_up_guard (
|
||||
id TINYINT NOT NULL,
|
||||
CONSTRAINT chk_chorus_mvp1_routing_up_guard CHECK (id = 0)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
DELETE FROM chorus_mvp1_routing_up_guard;
|
||||
INSERT INTO chorus_mvp1_routing_up_guard (id)
|
||||
SELECT 1
|
||||
WHERE EXISTS (SELECT 1 FROM provider_models WHERE api_type = 'gemini')
|
||||
OR EXISTS (SELECT 1 FROM prompt_templates WHERE api_type = 'gemini');
|
||||
DROP TABLE chorus_mvp1_routing_up_guard;
|
||||
|
||||
CREATE TABLE provider_credentials (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
provider_id BIGINT UNSIGNED NOT NULL,
|
||||
credential_version INT UNSIGNED NOT NULL,
|
||||
api_key_enc JSON NOT NULL,
|
||||
key_id VARCHAR(128) NOT NULL,
|
||||
status VARCHAR(16) NOT NULL,
|
||||
created_by BIGINT NULL,
|
||||
updated_by BIGINT NULL,
|
||||
created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
updated_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
|
||||
retired_at DATETIME(6) NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_provider_credentials_version (provider_id, credential_version),
|
||||
KEY idx_provider_credentials_status (provider_id, status, created_at),
|
||||
CONSTRAINT fk_provider_credentials_provider FOREIGN KEY (provider_id) REFERENCES providers (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT chk_provider_credentials_status CHECK (status IN ('active', 'retired')),
|
||||
CONSTRAINT chk_provider_credentials_envelope CHECK (
|
||||
JSON_TYPE(api_key_enc) = 'OBJECT' AND
|
||||
JSON_CONTAINS_PATH(api_key_enc, 'all', '$.version', '$.key_id', '$.nonce', '$.ciphertext')
|
||||
)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE provider_model_capabilities (
|
||||
provider_model_id BIGINT UNSIGNED NOT NULL,
|
||||
capability VARCHAR(32) NOT NULL,
|
||||
PRIMARY KEY (provider_model_id, capability),
|
||||
CONSTRAINT fk_provider_model_capabilities_model FOREIGN KEY (provider_model_id) REFERENCES provider_models (id) ON DELETE CASCADE,
|
||||
CONSTRAINT chk_provider_model_capabilities_capability CHECK (capability IN ('text', 'image_generate', 'image_edit'))
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE route_pools (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
slug VARCHAR(80) NOT NULL,
|
||||
name VARCHAR(120) NOT NULL,
|
||||
capability VARCHAR(32) NOT NULL,
|
||||
prompt_template_id BIGINT UNSIGNED NOT NULL,
|
||||
max_failover SMALLINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
version INT UNSIGNED NOT NULL DEFAULT 1,
|
||||
enabled BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_by BIGINT NULL,
|
||||
updated_by BIGINT NULL,
|
||||
created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
updated_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_route_pools_slug (slug),
|
||||
KEY idx_route_pools_capability_enabled (capability, enabled),
|
||||
KEY idx_route_pools_prompt_template (prompt_template_id),
|
||||
CONSTRAINT fk_route_pools_template FOREIGN KEY (prompt_template_id) REFERENCES prompt_templates (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT chk_route_pools_capability CHECK (capability IN ('text', 'image_generate', 'image_edit')),
|
||||
CONSTRAINT chk_route_pools_version CHECK (version > 0)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE active_routes (
|
||||
capability VARCHAR(32) NOT NULL,
|
||||
route_pool_id BIGINT UNSIGNED NOT NULL,
|
||||
operator_id BIGINT NULL,
|
||||
created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
updated_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
|
||||
PRIMARY KEY (capability),
|
||||
KEY idx_active_routes_pool (route_pool_id),
|
||||
CONSTRAINT fk_active_routes_pool FOREIGN KEY (route_pool_id) REFERENCES route_pools (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT chk_active_routes_capability CHECK (capability IN ('text', 'image_generate', 'image_edit'))
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE route_pool_members (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
route_pool_id BIGINT UNSIGNED NOT NULL,
|
||||
provider_model_id BIGINT UNSIGNED NOT NULL,
|
||||
weight SMALLINT UNSIGNED NOT NULL,
|
||||
failure_threshold SMALLINT UNSIGNED NOT NULL DEFAULT 3,
|
||||
open_seconds INT UNSIGNED NOT NULL DEFAULT 60,
|
||||
half_open_max SMALLINT UNSIGNED NOT NULL DEFAULT 1,
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
position SMALLINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
created_by BIGINT NULL,
|
||||
updated_by BIGINT NULL,
|
||||
created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
updated_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_route_pool_members_model (route_pool_id, provider_model_id),
|
||||
KEY idx_route_pool_members_list (route_pool_id, enabled, position),
|
||||
CONSTRAINT fk_route_pool_members_pool FOREIGN KEY (route_pool_id) REFERENCES route_pools (id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_route_pool_members_model FOREIGN KEY (provider_model_id) REFERENCES provider_models (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT chk_route_pool_members_weight CHECK (weight > 0),
|
||||
CONSTRAINT chk_route_pool_members_failure_threshold CHECK (failure_threshold > 0),
|
||||
CONSTRAINT chk_route_pool_members_open_seconds CHECK (open_seconds > 0),
|
||||
CONSTRAINT chk_route_pool_members_half_open CHECK (half_open_max > 0)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE route_member_runtime (
|
||||
route_pool_member_id BIGINT UNSIGNED NOT NULL,
|
||||
state VARCHAR(16) NOT NULL DEFAULT 'closed',
|
||||
consecutive_failures INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
open_until DATETIME(6) NULL,
|
||||
probe_owner VARCHAR(128) NULL,
|
||||
probe_token CHAR(36) NULL,
|
||||
probe_until DATETIME(6) NULL,
|
||||
last_error_code VARCHAR(64) NULL,
|
||||
last_error_message VARCHAR(1024) NULL,
|
||||
last_observed_at DATETIME(6) NULL,
|
||||
updated_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
|
||||
PRIMARY KEY (route_pool_member_id),
|
||||
KEY idx_route_member_runtime_state (state, open_until),
|
||||
CONSTRAINT fk_route_member_runtime_member FOREIGN KEY (route_pool_member_id) REFERENCES route_pool_members (id) ON DELETE CASCADE,
|
||||
CONSTRAINT chk_route_member_runtime_state CHECK (state IN ('closed', 'open', 'half_open')),
|
||||
CONSTRAINT chk_route_member_runtime_probe CHECK (
|
||||
(probe_owner IS NULL AND probe_token IS NULL AND probe_until IS NULL) OR
|
||||
(probe_owner IS NOT NULL AND probe_token IS NOT NULL AND probe_until IS NOT NULL)
|
||||
)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE provider_connectivity_checks (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
provider_model_id BIGINT UNSIGNED NOT NULL,
|
||||
operator_id BIGINT NULL,
|
||||
status VARCHAR(16) NOT NULL,
|
||||
error_code VARCHAR(64) NULL,
|
||||
error_message VARCHAR(1024) NULL,
|
||||
latency_ms INT UNSIGNED NULL,
|
||||
started_at DATETIME(6) NOT NULL,
|
||||
completed_at DATETIME(6) NULL,
|
||||
cooldown_until DATETIME(6) NOT NULL,
|
||||
created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_provider_connectivity_checks_model (provider_model_id, started_at DESC),
|
||||
KEY idx_provider_connectivity_checks_status (status, started_at),
|
||||
CONSTRAINT fk_provider_connectivity_checks_model FOREIGN KEY (provider_model_id) REFERENCES provider_models (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT chk_provider_connectivity_checks_status CHECK (status IN ('running', 'succeeded', 'failed'))
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE admin_audit_events (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
operator_id BIGINT NULL,
|
||||
action VARCHAR(128) NOT NULL,
|
||||
target_type VARCHAR(64) NOT NULL,
|
||||
target_id VARCHAR(128) NOT NULL,
|
||||
result VARCHAR(16) NOT NULL,
|
||||
request_id VARCHAR(128) NOT NULL,
|
||||
summary JSON NOT NULL,
|
||||
created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_admin_audit_events_request_action (request_id, action),
|
||||
KEY idx_admin_audit_events_target (target_type, target_id, created_at),
|
||||
KEY idx_admin_audit_events_operator (operator_id, created_at),
|
||||
CONSTRAINT chk_admin_audit_events_result CHECK (result IN ('succeeded', 'failed')),
|
||||
CONSTRAINT chk_admin_audit_events_summary CHECK (JSON_TYPE(summary) = 'OBJECT')
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- Backfill only envelopes that already passed the MVP-0 provider check constraint.
|
||||
INSERT INTO provider_credentials (provider_id, credential_version, api_key_enc, key_id, status, created_at, updated_at)
|
||||
SELECT id,
|
||||
1,
|
||||
api_key_enc,
|
||||
JSON_UNQUOTE(JSON_EXTRACT(api_key_enc, '$.key_id')),
|
||||
'active',
|
||||
created_at,
|
||||
updated_at
|
||||
FROM providers
|
||||
WHERE api_key_enc IS NOT NULL;
|
||||
|
||||
INSERT INTO provider_model_capabilities (provider_model_id, capability)
|
||||
SELECT id,
|
||||
CASE api_type
|
||||
WHEN 'chat' THEN 'text'
|
||||
WHEN 'images' THEN 'image_generate'
|
||||
WHEN 'images_edits' THEN 'image_edit'
|
||||
END
|
||||
FROM provider_models;
|
||||
|
||||
ALTER TABLE providers
|
||||
ADD COLUMN active_credential_id BIGINT UNSIGNED NULL,
|
||||
ADD UNIQUE KEY uq_providers_active_credential (active_credential_id);
|
||||
|
||||
UPDATE providers p
|
||||
JOIN provider_credentials c ON c.provider_id = p.id AND c.status = 'active'
|
||||
SET p.active_credential_id = c.id;
|
||||
|
||||
ALTER TABLE providers
|
||||
DROP CHECK chk_providers_auth,
|
||||
ADD CONSTRAINT fk_providers_active_credential FOREIGN KEY (active_credential_id) REFERENCES provider_credentials (id) ON DELETE RESTRICT;
|
||||
|
||||
UPDATE providers SET api_key_enc = NULL WHERE active_credential_id IS NOT NULL;
|
||||
|
||||
ALTER TABLE providers
|
||||
ADD CONSTRAINT chk_providers_auth_mvp1 CHECK (
|
||||
auth_type IN ('none', 'bearer', 'x-goog-api-key') AND
|
||||
((auth_type = 'none' AND active_credential_id IS NULL AND api_key_enc IS NULL) OR
|
||||
(auth_type IN ('bearer', 'x-goog-api-key') AND active_credential_id IS NOT NULL AND api_key_enc IS NULL))
|
||||
);
|
||||
|
||||
ALTER TABLE provider_models
|
||||
DROP FOREIGN KEY fk_provider_models_provider,
|
||||
ADD CONSTRAINT fk_provider_models_provider_mvp1 FOREIGN KEY (provider_id) REFERENCES providers (id) ON DELETE RESTRICT;
|
||||
|
||||
ALTER TABLE prompt_templates
|
||||
ADD COLUMN capability VARCHAR(32) NULL,
|
||||
ADD COLUMN default_role_rule TEXT NULL;
|
||||
|
||||
UPDATE prompt_templates
|
||||
SET capability = CASE api_type
|
||||
WHEN 'chat' THEN 'text'
|
||||
WHEN 'images' THEN 'image_generate'
|
||||
WHEN 'images_edits' THEN 'image_edit'
|
||||
END,
|
||||
default_role_rule = '';
|
||||
|
||||
ALTER TABLE prompt_templates
|
||||
MODIFY COLUMN capability VARCHAR(32) NOT NULL,
|
||||
MODIFY COLUMN default_role_rule TEXT NOT NULL,
|
||||
ADD CONSTRAINT chk_prompt_templates_capability CHECK (capability IN ('text', 'image_generate', 'image_edit'));
|
||||
|
||||
ALTER TABLE generations
|
||||
ADD COLUMN route_pool_id BIGINT UNSIGNED NULL,
|
||||
ADD COLUMN route_pool_version INT UNSIGNED NULL,
|
||||
ADD COLUMN prompt_template_id BIGINT UNSIGNED NULL,
|
||||
ADD COLUMN route_snapshot JSON NULL,
|
||||
ADD COLUMN role_rule TEXT NULL,
|
||||
ADD COLUMN provider_attempt_count INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
ADD KEY idx_generations_route_pool (route_pool_id),
|
||||
ADD KEY idx_generations_prompt_template (prompt_template_id),
|
||||
ADD CONSTRAINT fk_generations_route_pool FOREIGN KEY (route_pool_id) REFERENCES route_pools (id) ON DELETE RESTRICT,
|
||||
ADD CONSTRAINT fk_generations_prompt_template FOREIGN KEY (prompt_template_id) REFERENCES prompt_templates (id) ON DELETE RESTRICT,
|
||||
ADD CONSTRAINT chk_generations_route_snapshot CHECK (
|
||||
(route_pool_id IS NULL AND route_pool_version IS NULL AND prompt_template_id IS NULL AND route_snapshot IS NULL) OR
|
||||
(route_pool_id IS NOT NULL AND route_pool_version IS NOT NULL AND prompt_template_id IS NOT NULL AND route_snapshot IS NOT NULL AND JSON_TYPE(route_snapshot) = 'OBJECT')
|
||||
),
|
||||
ADD CONSTRAINT chk_generations_provider_attempt_count CHECK (provider_attempt_count <= attempt_count);
|
||||
|
||||
ALTER TABLE generation_inputs
|
||||
ADD COLUMN note VARCHAR(500) NULL,
|
||||
ADD CONSTRAINT chk_generation_inputs_note CHECK (note IS NULL OR CHAR_LENGTH(note) <= 500);
|
||||
@@ -0,0 +1,30 @@
|
||||
DELETE c
|
||||
FROM sys_casbin_rule c
|
||||
JOIN sys_api a ON c.ptype = 'p' AND c.v0 = 'chorus_operator' AND c.v1 = a.path AND c.v2 = a.action
|
||||
WHERE a.handle LIKE 'chorus.%';
|
||||
|
||||
DELETE r
|
||||
FROM sys_menu_api_rule r
|
||||
JOIN sys_menu m ON r.menu_id = m.menu_id
|
||||
JOIN sys_api a ON r.sys_api_id = a.id
|
||||
WHERE m.path IN ('/chorus/providers', '/chorus/models', '/chorus/routes', '/chorus/templates', '/chorus/generations', '/chorus/users')
|
||||
AND a.handle LIKE 'chorus.%';
|
||||
|
||||
DELETE r
|
||||
FROM sys_role_menu r
|
||||
JOIN sys_role role_record ON r.role_id = role_record.role_id
|
||||
JOIN sys_menu m ON r.menu_id = m.menu_id
|
||||
WHERE role_record.role_key = 'chorus_operator'
|
||||
AND m.path IN ('/chorus', '/chorus/providers', '/chorus/models', '/chorus/routes', '/chorus/templates', '/chorus/generations', '/chorus/users');
|
||||
|
||||
DELETE FROM sys_api WHERE handle IN (
|
||||
'chorus.providers.list', 'chorus.providers.create', 'chorus.providers.get', 'chorus.providers.update',
|
||||
'chorus.providers.credential.rotate', 'chorus.providers.credential.activate', 'chorus.models.list',
|
||||
'chorus.models.create', 'chorus.models.update', 'chorus.models.connectivity', 'chorus.routes.list',
|
||||
'chorus.routes.create', 'chorus.routes.update', 'chorus.templates.list', 'chorus.templates.create',
|
||||
'chorus.templates.update', 'chorus.generations.list', 'chorus.users.list'
|
||||
);
|
||||
|
||||
DELETE FROM sys_menu WHERE path IN ('/chorus/providers', '/chorus/models', '/chorus/routes', '/chorus/templates', '/chorus/generations', '/chorus/users');
|
||||
DELETE FROM sys_menu WHERE path = '/chorus';
|
||||
DELETE FROM sys_role WHERE role_key = 'chorus_operator';
|
||||
@@ -0,0 +1,89 @@
|
||||
-- Chorus-owned records use stable role keys, menu paths, and API handles. The seed
|
||||
-- deliberately creates no sys_user row and therefore never introduces a known password.
|
||||
INSERT INTO sys_role (role_name, status, role_key, role_sort, flag, remark, admin, data_scope)
|
||||
VALUES ('Chorus Operator', '2', 'chorus_operator', 100, '', 'Chorus operations role', FALSE, '1')
|
||||
ON DUPLICATE KEY UPDATE
|
||||
role_name = VALUES(role_name), status = VALUES(status), role_sort = VALUES(role_sort),
|
||||
remark = VALUES(remark), admin = VALUES(admin), data_scope = VALUES(data_scope), deleted_at = NULL;
|
||||
|
||||
INSERT INTO sys_menu
|
||||
(menu_name, title, icon, path, paths, menu_type, action, permission, parent_id, no_cache, breadcrumb, component, sort, visible, is_frame)
|
||||
VALUES
|
||||
('chorus', 'Chorus', 'ri:magic-line', '/chorus', '', 'M', '', 'chorus:menu:view', 0, FALSE, 'Chorus', 'Layout', 900, '0', '0')
|
||||
ON DUPLICATE KEY UPDATE
|
||||
menu_name = VALUES(menu_name), title = VALUES(title), icon = VALUES(icon), menu_type = VALUES(menu_type),
|
||||
action = VALUES(action), permission = VALUES(permission), no_cache = VALUES(no_cache), breadcrumb = VALUES(breadcrumb),
|
||||
component = VALUES(component), sort = VALUES(sort), visible = VALUES(visible), is_frame = VALUES(is_frame), deleted_at = NULL;
|
||||
|
||||
UPDATE sys_menu SET paths = CONCAT('/0/', menu_id) WHERE path = '/chorus';
|
||||
|
||||
INSERT INTO sys_menu
|
||||
(menu_name, title, icon, path, paths, menu_type, action, permission, parent_id, no_cache, breadcrumb, component, sort, visible, is_frame)
|
||||
SELECT menu_name, title, icon, path, '', menu_type, action, permission,
|
||||
(SELECT menu_id FROM sys_menu WHERE path = '/chorus'), no_cache, breadcrumb, component, sort, visible, is_frame
|
||||
FROM (
|
||||
SELECT 'chorus-providers' AS menu_name, 'Providers' AS title, 'ri:server-line' AS icon,
|
||||
'/chorus/providers' AS path, 'C' AS menu_type, '' AS action, 'chorus:providers:view' AS permission,
|
||||
FALSE AS no_cache, 'Chorus / Providers' AS breadcrumb, 'chorus/providers/index' AS component, 10 AS sort, '0' AS visible, '0' AS is_frame
|
||||
UNION ALL SELECT 'chorus-models', 'Models', 'ri:cpu-line', '/chorus/models', 'C', '', 'chorus:models:view', FALSE, 'Chorus / Models', 'chorus/models/index', 20, '0', '0'
|
||||
UNION ALL SELECT 'chorus-routes', 'Route Pools', 'ri:git-branch-line', '/chorus/routes', 'C', '', 'chorus:routes:view', FALSE, 'Chorus / Route Pools', 'chorus/routes/index', 30, '0', '0'
|
||||
UNION ALL SELECT 'chorus-templates', 'Prompt Templates', 'ri:file-text-line', '/chorus/templates', 'C', '', 'chorus:templates:view', FALSE, 'Chorus / Prompt Templates', 'chorus/templates/index', 40, '0', '0'
|
||||
UNION ALL SELECT 'chorus-generations', 'Generations', 'ri:history-line', '/chorus/generations', 'C', '', 'chorus:generations:view', FALSE, 'Chorus / Generations', 'chorus/generations/index', 50, '0', '0'
|
||||
UNION ALL SELECT 'chorus-users', 'Users', 'ri:user-line', '/chorus/users', 'C', '', 'chorus:users:view', FALSE, 'Chorus / Users', 'chorus/users/index', 60, '0', '0'
|
||||
) AS chorus_menus
|
||||
ON DUPLICATE KEY UPDATE
|
||||
menu_name = VALUES(menu_name), title = VALUES(title), icon = VALUES(icon), menu_type = VALUES(menu_type),
|
||||
action = VALUES(action), permission = VALUES(permission), parent_id = VALUES(parent_id), no_cache = VALUES(no_cache),
|
||||
breadcrumb = VALUES(breadcrumb), component = VALUES(component), sort = VALUES(sort), visible = VALUES(visible),
|
||||
is_frame = VALUES(is_frame), deleted_at = NULL;
|
||||
|
||||
UPDATE sys_menu child
|
||||
JOIN sys_menu parent ON parent.path = '/chorus'
|
||||
SET child.paths = CONCAT(parent.paths, '/', child.menu_id)
|
||||
WHERE child.path IN ('/chorus/providers', '/chorus/models', '/chorus/routes', '/chorus/templates', '/chorus/generations', '/chorus/users');
|
||||
|
||||
INSERT INTO sys_api (handle, title, path, type, action)
|
||||
VALUES
|
||||
('chorus.providers.list', 'List providers', '/api/v1/chorus/providers', 'BUS', 'GET'),
|
||||
('chorus.providers.create', 'Create provider', '/api/v1/chorus/providers', 'BUS', 'POST'),
|
||||
('chorus.providers.get', 'Get provider', '/api/v1/chorus/providers/:id', 'BUS', 'GET'),
|
||||
('chorus.providers.update', 'Update provider', '/api/v1/chorus/providers/:id', 'BUS', 'PUT'),
|
||||
('chorus.providers.credential.rotate', 'Rotate provider credential', '/api/v1/chorus/providers/:id/credential', 'BUS', 'PUT'),
|
||||
('chorus.providers.credential.activate', 'Activate provider credential', '/api/v1/chorus/providers/:id/credential/:version/activate', 'BUS', 'POST'),
|
||||
('chorus.models.list', 'List provider models', '/api/v1/chorus/provider-models', 'BUS', 'GET'),
|
||||
('chorus.models.create', 'Create provider model', '/api/v1/chorus/provider-models', 'BUS', 'POST'),
|
||||
('chorus.models.update', 'Update provider model', '/api/v1/chorus/provider-models/:id', 'BUS', 'PUT'),
|
||||
('chorus.models.connectivity', 'Run connectivity check', '/api/v1/chorus/provider-models/:id/connectivity-checks', 'BUS', 'POST'),
|
||||
('chorus.routes.list', 'List route pools', '/api/v1/chorus/route-pools', 'BUS', 'GET'),
|
||||
('chorus.routes.create', 'Create route pool', '/api/v1/chorus/route-pools', 'BUS', 'POST'),
|
||||
('chorus.routes.update', 'Update route pool', '/api/v1/chorus/route-pools/:id', 'BUS', 'PUT'),
|
||||
('chorus.templates.list', 'List prompt templates', '/api/v1/chorus/prompt-templates', 'BUS', 'GET'),
|
||||
('chorus.templates.create', 'Create prompt template', '/api/v1/chorus/prompt-templates', 'BUS', 'POST'),
|
||||
('chorus.templates.update', 'Update prompt template', '/api/v1/chorus/prompt-templates/:id', 'BUS', 'PUT'),
|
||||
('chorus.generations.list', 'List generations', '/api/v1/chorus/generations', 'BUS', 'GET'),
|
||||
('chorus.users.list', 'List portal users', '/api/v1/chorus/users', 'BUS', 'GET')
|
||||
ON DUPLICATE KEY UPDATE
|
||||
title = VALUES(title), type = VALUES(type), deleted_at = NULL;
|
||||
|
||||
INSERT IGNORE INTO sys_role_menu (role_id, menu_id)
|
||||
SELECT r.role_id, m.menu_id
|
||||
FROM sys_role r
|
||||
JOIN sys_menu m ON m.path IN ('/chorus', '/chorus/providers', '/chorus/models', '/chorus/routes', '/chorus/templates', '/chorus/generations', '/chorus/users')
|
||||
WHERE r.role_key = 'chorus_operator';
|
||||
|
||||
INSERT IGNORE INTO sys_menu_api_rule (menu_id, sys_api_id)
|
||||
SELECT m.menu_id, a.id
|
||||
FROM sys_menu m
|
||||
JOIN sys_api a ON (
|
||||
(m.path = '/chorus/providers' AND a.handle LIKE 'chorus.providers.%') OR
|
||||
(m.path = '/chorus/models' AND a.handle LIKE 'chorus.models.%') OR
|
||||
(m.path = '/chorus/routes' AND a.handle LIKE 'chorus.routes.%') OR
|
||||
(m.path = '/chorus/templates' AND a.handle LIKE 'chorus.templates.%') OR
|
||||
(m.path = '/chorus/generations' AND a.handle = 'chorus.generations.list') OR
|
||||
(m.path = '/chorus/users' AND a.handle = 'chorus.users.list')
|
||||
);
|
||||
|
||||
INSERT IGNORE INTO sys_casbin_rule (ptype, v0, v1, v2, v3, v4, v5)
|
||||
SELECT 'p', 'chorus_operator', path, action, '', '', ''
|
||||
FROM sys_api
|
||||
WHERE handle LIKE 'chorus.%';
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -20,8 +21,25 @@ func TestMigrationPairsAndProductionTables(t *testing.T) {
|
||||
if len(upFiles) == 0 || len(upFiles) != len(downFiles) {
|
||||
t.Fatalf("migration pairs mismatch: up=%d down=%d", len(upFiles), len(downFiles))
|
||||
}
|
||||
wantFiles := []string{
|
||||
"000001_mvp0_base.up.sql",
|
||||
"000002_admin_baseline.up.sql",
|
||||
"000003_mvp1_routing.up.sql",
|
||||
"000004_mvp1_admin_seed.up.sql",
|
||||
}
|
||||
slices.Sort(upFiles)
|
||||
if !slices.Equal(upFiles, wantFiles) {
|
||||
t.Fatalf("up migrations = %v, want %v", upFiles, wantFiles)
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(upFiles[0])
|
||||
for _, upFile := range upFiles {
|
||||
downFile := strings.TrimSuffix(upFile, ".up.sql") + ".down.sql"
|
||||
if _, err := os.Stat(downFile); err != nil {
|
||||
t.Errorf("%s has no matching down migration: %v", upFile, err)
|
||||
}
|
||||
}
|
||||
|
||||
content, err := os.ReadFile("000001_mvp0_base.up.sql")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -38,3 +56,64 @@ func TestMigrationPairsAndProductionTables(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMVP1MigrationContracts(t *testing.T) {
|
||||
read := func(name string) string {
|
||||
t.Helper()
|
||||
content, err := os.ReadFile(name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return string(content)
|
||||
}
|
||||
|
||||
adminBaseline := read("000002_admin_baseline.up.sql")
|
||||
for _, table := range []string{
|
||||
"sys_user", "sys_role", "sys_menu", "sys_api", "sys_role_menu", "sys_menu_api_rule",
|
||||
"sys_casbin_rule", "sys_tables", "sys_columns",
|
||||
} {
|
||||
if !regexp.MustCompile(`(?i)CREATE TABLE\s+` + regexp.QuoteMeta(table) + `\s*\(`).MatchString(adminBaseline) {
|
||||
t.Errorf("admin baseline does not create %s", table)
|
||||
}
|
||||
}
|
||||
if strings.Contains(strings.ToLower(adminBaseline), "automigrate") {
|
||||
t.Error("admin baseline must not use AutoMigrate")
|
||||
}
|
||||
|
||||
routing := read("000003_mvp1_routing.up.sql")
|
||||
for _, required := range []string{
|
||||
"provider_credentials", "provider_model_capabilities", "route_pools", "active_routes",
|
||||
"route_pool_members", "route_member_runtime", "provider_connectivity_checks", "admin_audit_events",
|
||||
"active_credential_id", "route_snapshot", "provider_attempt_count", "default_role_rule", "note",
|
||||
"uq_provider_credentials_version", "uq_route_pools_slug", "uq_route_pool_members_model",
|
||||
"idx_provider_connectivity_checks_model", "uq_admin_audit_events_request_action",
|
||||
"DROP CHECK chk_providers_auth", "UPDATE providers SET api_key_enc = NULL",
|
||||
} {
|
||||
if !strings.Contains(routing, required) {
|
||||
t.Errorf("MVP-1 routing migration is missing %s", required)
|
||||
}
|
||||
}
|
||||
|
||||
seed := read("000004_mvp1_admin_seed.up.sql")
|
||||
for _, required := range []string{"chorus_operator", "chorus.providers.credential.rotate", "sys_role_menu", "sys_menu_api_rule", "sys_casbin_rule"} {
|
||||
if !strings.Contains(seed, required) {
|
||||
t.Errorf("admin seed is missing %s", required)
|
||||
}
|
||||
}
|
||||
if strings.Contains(seed, "INSERT INTO sys_user") {
|
||||
t.Error("admin seed must not create a known administrator account")
|
||||
}
|
||||
adminDown := read("000002_admin_baseline.down.sql")
|
||||
for _, required := range []string{"chorus_admin_baseline_down_guard", "INSERT INTO chorus_admin_baseline_down_guard", "EXISTS (SELECT 1 FROM sys_user)"} {
|
||||
if !strings.Contains(adminDown, required) {
|
||||
t.Errorf("admin baseline down migration is missing rollback guard %s", required)
|
||||
}
|
||||
}
|
||||
|
||||
for _, name := range []string{"000002_admin_baseline.down.sql", "000003_mvp1_routing.down.sql", "000004_mvp1_admin_seed.down.sql"} {
|
||||
down := read(name)
|
||||
if strings.Contains(strings.ToLower(down), "delete from sys_menu where title") || strings.Contains(strings.ToLower(down), "delete from sys_api where title") {
|
||||
t.Errorf("%s must delete Chorus seed data by stable keys, not titles", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
// This test resets only the explicitly designated disposable migration database.
|
||||
// It is opt-in so normal package tests cannot erase a developer's local data.
|
||||
func TestMVP1MigrationsUpDownUpMySQL(t *testing.T) {
|
||||
if os.Getenv("CHORUS_RUN_MIGRATION_TESTS") != "1" {
|
||||
t.Skip("set CHORUS_RUN_MIGRATION_TESTS=1 for the disposable MySQL migration database")
|
||||
}
|
||||
migrationURL := strings.TrimSpace(os.Getenv("CHORUS_MIGRATE_URL"))
|
||||
dsn := strings.TrimSpace(os.Getenv("CHORUS_DSN"))
|
||||
if migrationURL == "" || dsn == "" {
|
||||
t.Fatal("CHORUS_MIGRATE_URL and CHORUS_DSN are required for migration integration tests")
|
||||
}
|
||||
if _, err := exec.LookPath("migrate"); err != nil {
|
||||
t.Fatalf("migrate executable is required: %v", err)
|
||||
}
|
||||
|
||||
db, err := sql.Open("mysql", dsn)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
|
||||
defer cancel()
|
||||
if err := db.PingContext(ctx); err != nil {
|
||||
t.Fatalf("connect disposable migration database: %v", err)
|
||||
}
|
||||
|
||||
runMigrate := func(args ...string) {
|
||||
t.Helper()
|
||||
commandArgs := append([]string{"-path", ".", "-database", migrationURL}, args...)
|
||||
command := exec.CommandContext(ctx, "migrate", commandArgs...)
|
||||
output, err := command.CombinedOutput()
|
||||
if err != nil && !strings.Contains(string(output), "no change") {
|
||||
t.Fatalf("migrate %s: %v\n%s", strings.Join(args, " "), err, output)
|
||||
}
|
||||
}
|
||||
|
||||
runMigrate("down", "-all")
|
||||
runMigrate("goto", "1")
|
||||
seedMVP0MigrationFixture(t, ctx, db)
|
||||
|
||||
runMigrate("up")
|
||||
assertTableExists(t, ctx, db, "provider_credentials", true)
|
||||
assertTableExists(t, ctx, db, "route_pools", true)
|
||||
assertTableExists(t, ctx, db, "sys_user", true)
|
||||
|
||||
var credentialCount int
|
||||
if err := db.QueryRowContext(ctx, `SELECT COUNT(*) FROM provider_credentials WHERE provider_id = 1 AND credential_version = 1 AND key_id = 'migration-test-key'`).Scan(&credentialCount); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if credentialCount != 1 {
|
||||
t.Fatalf("credential backfill count = %d, want 1", credentialCount)
|
||||
}
|
||||
var activeCredential sql.NullInt64
|
||||
var oldEnvelope sql.NullString
|
||||
if err := db.QueryRowContext(ctx, `SELECT active_credential_id, api_key_enc FROM providers WHERE id = 1`).Scan(&activeCredential, &oldEnvelope); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !activeCredential.Valid || oldEnvelope.Valid {
|
||||
t.Fatalf("provider credential migration did not activate the new credential safely")
|
||||
}
|
||||
assertCount(t, ctx, db, `SELECT COUNT(*) FROM provider_model_capabilities WHERE provider_model_id = 1 AND capability = 'text'`, 1)
|
||||
assertCount(t, ctx, db, `SELECT COUNT(*) FROM sys_role WHERE role_key = 'chorus_operator'`, 1)
|
||||
assertCount(t, ctx, db, `SELECT COUNT(*) FROM sys_api WHERE handle = 'chorus.providers.credential.rotate'`, 1)
|
||||
|
||||
runMigrate("down", "3")
|
||||
assertTableExists(t, ctx, db, "provider_credentials", false)
|
||||
assertTableExists(t, ctx, db, "sys_user", false)
|
||||
assertCount(t, ctx, db, `SELECT COUNT(*) FROM providers WHERE id = 1 AND api_key_enc IS NOT NULL`, 1)
|
||||
assertCount(t, ctx, db, `SELECT COUNT(*) FROM generations WHERE id = 1`, 1)
|
||||
|
||||
runMigrate("up")
|
||||
assertTableExists(t, ctx, db, "provider_credentials", true)
|
||||
assertCount(t, ctx, db, `SELECT COUNT(*) FROM provider_credentials WHERE provider_id = 1 AND credential_version = 1`, 1)
|
||||
assertCount(t, ctx, db, `SELECT COUNT(*) FROM sys_role WHERE role_key = 'chorus_operator'`, 1)
|
||||
}
|
||||
|
||||
func seedMVP0MigrationFixture(t *testing.T, ctx context.Context, db *sql.DB) {
|
||||
t.Helper()
|
||||
statements := []string{
|
||||
`INSERT INTO users (id, email, password_hash, display_name, status) VALUES (1, 'migration-user@chorus.invalid', 'synthetic', 'Migration User', 'active')`,
|
||||
`INSERT INTO providers (id, slug, name, base_url, auth_type, api_key_enc, enabled) VALUES (1, 'migration-provider', 'Migration Provider', 'https://provider.invalid', 'bearer', JSON_OBJECT('version', 1, 'key_id', 'migration-test-key', 'nonce', 'AA==', 'ciphertext', 'AA=='), FALSE)`,
|
||||
`INSERT INTO provider_models (id, provider_id, name, model_id, api_type, kind, extra_body, timeout_ms, weight, enabled) VALUES (1, 1, 'Migration Model', 'migration-model', 'chat', 'text', JSON_OBJECT(), 30000, 100, TRUE)`,
|
||||
`INSERT INTO prompt_templates (id, template_key, kind, api_type, name, version, template_text, enabled) VALUES (1, 'migration-template', 'text', 'chat', 'Migration Template', 1, '{{.UserPrompt}}', TRUE)`,
|
||||
`INSERT INTO generations (id, user_id, provider_model_id, kind, status, idempotency_key, user_prompt, rendered_prompt, attempts, attempt_count) VALUES (1, 1, 1, 'text', 'pending', 'migration-key', 'migration input', 'migration input', JSON_ARRAY(), 0)`,
|
||||
}
|
||||
for _, statement := range statements {
|
||||
if _, err := db.ExecContext(ctx, statement); err != nil {
|
||||
t.Fatalf("seed MVP-0 fixture: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func assertTableExists(t *testing.T, ctx context.Context, db *sql.DB, table string, want bool) {
|
||||
t.Helper()
|
||||
var count int
|
||||
if err := db.QueryRowContext(ctx, `SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = ?`, table).Scan(&count); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if (count == 1) != want {
|
||||
t.Fatalf("table %s exists = %t, want %t", table, count == 1, want)
|
||||
}
|
||||
}
|
||||
|
||||
func assertCount(t *testing.T, ctx context.Context, db *sql.DB, query string, want int) {
|
||||
t.Helper()
|
||||
var got int
|
||||
if err := db.QueryRowContext(ctx, query).Scan(&got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != want {
|
||||
t.Fatal(fmt.Sprintf("query count = %d, want %d", got, want))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user