feat: 增加 MVP-1 可逆迁移与 admin 基线 (#20)

This commit is contained in:
ila
2026-08-21 16:33:00 +08:00
parent c8fa274611
commit 6035ac4d9a
8 changed files with 1082 additions and 1 deletions
+45
View File
@@ -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;
+401
View File
@@ -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;
+63
View File
@@ -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;
+246
View File
@@ -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';
+89
View File
@@ -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.%';
+80 -1
View File
@@ -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)
}
}
}
+128
View File
@@ -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))
}
}