Files
YG_FT/docs/postgres-schema.sql
wuyongtao 836343b29e feat: 新增 compute_gateway、compute_poller、agent 模块,重构前端 dist
- 新增 backend/app/modules/compute_gateway(client/sync)计算网关模块
- 新增 backend/app/workers/compute_poller 计算轮询 worker
- 新增 compute/agent/process_manager 进程管理器
- 新增 scripts/ 脚本目录
- 更新 Docker 部署配置(app/compute/nginx)
- 更新后端平台 API、数据库 SQL、core 配置
- 更新前端多个视图组件及 API 模块
- 重构 frontend/dist 构建产物(新 hash)
- 更新多项文档

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-22 20:26:16 +08:00

1514 lines
67 KiB
PL/PgSQL

-- PostgreSQL schema for the model fine-tuning platform.
-- Recommended PostgreSQL version: 14+.
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE EXTENSION IF NOT EXISTS citext;
CREATE SCHEMA IF NOT EXISTS ft_platform;
SET search_path TO ft_platform, public;
-- =========================
-- Common helpers
-- =========================
CREATE OR REPLACE FUNCTION ft_platform.set_updated_at()
RETURNS trigger AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION ft_platform.touch_updated_at(table_name regclass)
RETURNS void AS $$
BEGIN
EXECUTE format('DROP TRIGGER IF EXISTS trg_set_updated_at ON %s', table_name);
EXECUTE format(
'CREATE TRIGGER trg_set_updated_at BEFORE UPDATE ON %s
FOR EACH ROW EXECUTE FUNCTION ft_platform.set_updated_at()',
table_name
);
END;
$$ LANGUAGE plpgsql;
-- =========================
-- Enums
-- =========================
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'user_role') THEN
CREATE TYPE user_role AS ENUM ('admin', 'operator', 'viewer');
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'user_status') THEN
CREATE TYPE user_status AS ENUM ('active', 'disabled');
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'task_status') THEN
CREATE TYPE task_status AS ENUM ('pending', 'running', 'completed', 'failed', 'stopped');
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'model_type') THEN
CREATE TYPE model_type AS ENUM ('LLM', 'CV', 'NLP', 'Embedding', 'Other');
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'model_purpose') THEN
CREATE TYPE model_purpose AS ENUM ('training', 'inference', 'evaluation');
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'model_source') THEN
CREATE TYPE model_source AS ENUM ('local', 'api');
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'dataset_type') THEN
CREATE TYPE dataset_type AS ENUM ('train', 'test', 'eval', 'val', 'other');
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'dataset_storage') THEN
CREATE TYPE dataset_storage AS ENUM ('local', 'minio', 'cloud');
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'dataset_source') THEN
CREATE TYPE dataset_source AS ENUM ('upload', 'task');
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'train_type') THEN
CREATE TYPE train_type AS ENUM ('SFT', 'DPO', 'CPT');
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'train_method') THEN
CREATE TYPE train_method AS ENUM ('lora', 'qlora', 'full', 'prefix', 'adapter', 'peft', 'adalora', 'longlora');
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'process_type') THEN
CREATE TYPE process_type AS ENUM ('structured', 'unstructured', 'external');
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'eval_type') THEN
CREATE TYPE eval_type AS ENUM ('custom', 'baseline');
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'dimension_type') THEN
CREATE TYPE dimension_type AS ENUM ('classification', 'metric', 'text_similarity');
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'result_status') THEN
CREATE TYPE result_status AS ENUM ('valid', 'modified', 'invalid');
END IF;
END $$;
-- =========================
-- User center and RBAC
-- =========================
CREATE TABLE IF NOT EXISTS users (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
username citext NOT NULL UNIQUE,
display_name varchar(100) NOT NULL,
password_hash text NOT NULL,
role user_role NOT NULL DEFAULT 'viewer',
status user_status NOT NULL DEFAULT 'active',
protected boolean NOT NULL DEFAULT false,
last_login_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
deleted_at timestamptz
);
SELECT touch_updated_at('users');
CREATE TABLE IF NOT EXISTS permissions (
code varchar(64) PRIMARY KEY,
name varchar(100) NOT NULL,
description text,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS role_permissions (
role user_role NOT NULL,
permission_code varchar(64) NOT NULL REFERENCES permissions(code) ON DELETE CASCADE,
PRIMARY KEY (role, permission_code)
);
CREATE TABLE IF NOT EXISTS user_permissions (
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
permission_code varchar(64) NOT NULL REFERENCES permissions(code) ON DELETE CASCADE,
allowed boolean NOT NULL DEFAULT true,
created_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (user_id, permission_code)
);
CREATE TABLE IF NOT EXISTS login_sessions (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token_jti uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
ip inet,
user_agent text,
expires_at timestamptz NOT NULL,
revoked_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_login_sessions_user_created ON login_sessions(user_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_login_sessions_expires ON login_sessions(expires_at) WHERE revoked_at IS NULL;
INSERT INTO permissions(code, name, description) VALUES
('dashboard', '服务看板', '访问服务看板'),
('fine-tune', '模型训练', '创建和管理微调任务'),
('model-eval', '模型评测', '创建和管理评测任务'),
('model-inference', '模型推理', '创建推理与模型对比任务'),
('model-manage', '模型管理', '登记、编辑、删除模型'),
('dataset', '数据集管理', '上传、编辑、下载数据集'),
('data-process', '数据处理', '创建和管理数据处理任务'),
('data-convert', '数据转换/工具', '使用数据转换和工具中心'),
('hardware', '平台性能', '查看硬件和系统监控'),
('logs', '查看日志', '查看系统与训练日志'),
('user-settings', '用户设置', '管理用户和权限')
ON CONFLICT (code) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description;
INSERT INTO role_permissions(role, permission_code)
SELECT 'admin'::user_role, code FROM permissions
ON CONFLICT DO NOTHING;
INSERT INTO role_permissions(role, permission_code) VALUES
('operator', 'dashboard'),
('operator', 'fine-tune'),
('operator', 'model-eval'),
('operator', 'model-inference'),
('operator', 'model-manage'),
('operator', 'dataset'),
('operator', 'data-process'),
('operator', 'data-convert'),
('operator', 'hardware'),
('operator', 'logs'),
('viewer', 'dashboard'),
('viewer', 'model-eval'),
('viewer', 'model-inference'),
('viewer', 'dataset'),
('viewer', 'hardware'),
('viewer', 'logs')
ON CONFLICT DO NOTHING;
-- Replace this password hash during deployment.
INSERT INTO users(username, display_name, password_hash, role, status, protected)
VALUES ('admin', '系统管理员', '$argon2id$replace-with-real-hash', 'admin', 'active', true)
ON CONFLICT (username) DO NOTHING;
-- =========================
-- Audit and operation logs
-- =========================
CREATE TABLE IF NOT EXISTS audit_logs (
id bigserial PRIMARY KEY,
user_id uuid REFERENCES users(id) ON DELETE SET NULL,
username citext,
action varchar(100) NOT NULL,
resource_type varchar(80) NOT NULL,
resource_id text,
request_method varchar(12),
request_path text,
ip inet,
user_agent text,
success boolean NOT NULL DEFAULT true,
error_message text,
before_data jsonb,
after_data jsonb,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_audit_logs_user_created ON audit_logs(user_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_audit_logs_resource ON audit_logs(resource_type, resource_id);
CREATE INDEX IF NOT EXISTS idx_audit_logs_created ON audit_logs(created_at DESC);
CREATE TABLE IF NOT EXISTS web_logs (
id bigserial PRIMARY KEY,
user_id uuid REFERENCES users(id) ON DELETE SET NULL,
level varchar(20) NOT NULL,
message text NOT NULL,
page text,
context jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_web_logs_created ON web_logs(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_web_logs_level_created ON web_logs(level, created_at DESC);
-- =========================
-- Files and object storage metadata
-- =========================
CREATE TABLE IF NOT EXISTS storage_objects (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
storage_type dataset_storage NOT NULL DEFAULT 'local',
bucket varchar(128),
object_key text NOT NULL,
original_name text,
mime_type varchar(200),
byte_size bigint NOT NULL DEFAULT 0 CHECK (byte_size >= 0),
checksum_sha256 char(64),
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
created_by uuid REFERENCES users(id) ON DELETE SET NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX IF NOT EXISTS uq_storage_object_location
ON storage_objects(storage_type, COALESCE(bucket, ''), object_key);
CREATE INDEX IF NOT EXISTS idx_storage_objects_checksum ON storage_objects(checksum_sha256);
-- =========================
-- Model registry
-- =========================
CREATE TABLE IF NOT EXISTS models (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name varchar(150) NOT NULL,
type model_type NOT NULL DEFAULT 'LLM',
purpose model_purpose NOT NULL,
model_source model_source NOT NULL DEFAULT 'local',
description text,
path text,
api_url text,
api_key_encrypted text,
online_model_name varchar(200),
config jsonb NOT NULL DEFAULT '{}'::jsonb,
created_by uuid REFERENCES users(id) ON DELETE SET NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
deleted_at timestamptz,
CONSTRAINT ck_model_local_or_api CHECK (
(model_source = 'local' AND path IS NOT NULL)
OR
(model_source = 'api' AND api_url IS NOT NULL AND online_model_name IS NOT NULL)
)
);
SELECT touch_updated_at('models');
CREATE UNIQUE INDEX IF NOT EXISTS uq_models_name_alive ON models(name) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_models_purpose ON models(purpose) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_models_type_source ON models(type, model_source) WHERE deleted_at IS NULL;
CREATE TABLE IF NOT EXISTS trained_models (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name varchar(150) NOT NULL,
base_model_id uuid REFERENCES models(id) ON DELETE SET NULL,
fine_tune_task_id uuid,
train_method train_method,
adapter_path text,
merged boolean NOT NULL DEFAULT false,
merging boolean NOT NULL DEFAULT false,
merged_path text,
export_object_id uuid REFERENCES storage_objects(id) ON DELETE SET NULL,
metrics jsonb NOT NULL DEFAULT '{}'::jsonb,
created_by uuid REFERENCES users(id) ON DELETE SET NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
deleted_at timestamptz
);
SELECT touch_updated_at('trained_models');
CREATE UNIQUE INDEX IF NOT EXISTS uq_trained_models_name_alive ON trained_models(name) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_trained_models_task ON trained_models(fine_tune_task_id);
CREATE INDEX IF NOT EXISTS idx_trained_models_base ON trained_models(base_model_id);
-- =========================
-- Datasets and versions
-- =========================
CREATE TABLE IF NOT EXISTS datasets (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name varchar(150) NOT NULL,
type dataset_type NOT NULL,
storage_type dataset_storage NOT NULL DEFAULT 'local',
source dataset_source NOT NULL DEFAULT 'upload',
source_task_id uuid,
size_bytes bigint NOT NULL DEFAULT 0 CHECK (size_bytes >= 0),
record_count bigint NOT NULL DEFAULT 0 CHECK (record_count >= 0),
description text,
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
created_by uuid REFERENCES users(id) ON DELETE SET NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
deleted_at timestamptz
);
SELECT touch_updated_at('datasets');
CREATE UNIQUE INDEX IF NOT EXISTS uq_datasets_name_alive ON datasets(name) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_datasets_type_created ON datasets(type, created_at DESC) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_datasets_source_task ON datasets(source_task_id) WHERE source = 'task';
CREATE TABLE IF NOT EXISTS dataset_files (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
dataset_id uuid NOT NULL REFERENCES datasets(id) ON DELETE CASCADE,
name text NOT NULL,
storage_object_id uuid REFERENCES storage_objects(id) ON DELETE SET NULL,
current_version_id uuid,
size_bytes bigint NOT NULL DEFAULT 0 CHECK (size_bytes >= 0),
record_count bigint NOT NULL DEFAULT 0 CHECK (record_count >= 0),
file_format varchar(40),
created_by uuid REFERENCES users(id) ON DELETE SET NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
deleted_at timestamptz
);
SELECT touch_updated_at('dataset_files');
CREATE UNIQUE INDEX IF NOT EXISTS uq_dataset_files_name_alive
ON dataset_files(dataset_id, name) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_dataset_files_dataset ON dataset_files(dataset_id) WHERE deleted_at IS NULL;
CREATE TABLE IF NOT EXISTS dataset_file_versions (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
dataset_file_id uuid NOT NULL REFERENCES dataset_files(id) ON DELETE CASCADE,
version_no integer NOT NULL CHECK (version_no > 0),
storage_object_id uuid REFERENCES storage_objects(id) ON DELETE SET NULL,
content_preview text,
description text,
base_version_id uuid REFERENCES dataset_file_versions(id) ON DELETE SET NULL,
size_bytes bigint NOT NULL DEFAULT 0 CHECK (size_bytes >= 0),
record_count bigint NOT NULL DEFAULT 0 CHECK (record_count >= 0),
checksum_sha256 char(64),
created_by uuid REFERENCES users(id) ON DELETE SET NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX IF NOT EXISTS uq_dataset_file_versions_no
ON dataset_file_versions(dataset_file_id, version_no);
CREATE INDEX IF NOT EXISTS idx_dataset_file_versions_file_created
ON dataset_file_versions(dataset_file_id, created_at DESC);
ALTER TABLE dataset_files
DROP CONSTRAINT IF EXISTS fk_dataset_files_current_version;
ALTER TABLE dataset_files
ADD CONSTRAINT fk_dataset_files_current_version
FOREIGN KEY (current_version_id) REFERENCES dataset_file_versions(id) ON DELETE SET NULL;
CREATE TABLE IF NOT EXISTS dataset_records (
id bigserial PRIMARY KEY,
dataset_id uuid NOT NULL REFERENCES datasets(id) ON DELETE CASCADE,
dataset_file_id uuid REFERENCES dataset_files(id) ON DELETE CASCADE,
version_id uuid REFERENCES dataset_file_versions(id) ON DELETE CASCADE,
line_no integer,
split varchar(20),
instruction text,
input text,
output text,
raw jsonb NOT NULL DEFAULT '{}'::jsonb,
status result_status NOT NULL DEFAULT 'valid',
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_dataset_records_dataset_id ON dataset_records(dataset_id, id);
CREATE INDEX IF NOT EXISTS idx_dataset_records_file_version ON dataset_records(dataset_file_id, version_id, line_no);
CREATE INDEX IF NOT EXISTS idx_dataset_records_split ON dataset_records(dataset_id, split);
CREATE INDEX IF NOT EXISTS idx_dataset_records_raw_gin ON dataset_records USING gin(raw);
-- =========================
-- Data processing
-- =========================
CREATE TABLE IF NOT EXISTS data_process_tasks (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name varchar(150) NOT NULL,
description text,
status task_status NOT NULL DEFAULT 'pending',
process_type process_type NOT NULL,
source_dataset_id uuid REFERENCES datasets(id) ON DELETE SET NULL,
output_dataset_id uuid REFERENCES datasets(id) ON DELETE SET NULL,
config jsonb NOT NULL DEFAULT '{}'::jsonb,
progress numeric(5,2) NOT NULL DEFAULT 0 CHECK (progress >= 0 AND progress <= 100),
input_count bigint NOT NULL DEFAULT 0,
output_count bigint NOT NULL DEFAULT 0,
filtered_count bigint NOT NULL DEFAULT 0,
duplicate_count bigint NOT NULL DEFAULT 0,
error_count bigint NOT NULL DEFAULT 0,
failure_reason text,
created_by uuid REFERENCES users(id) ON DELETE SET NULL,
started_at timestamptz,
completed_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
deleted_at timestamptz
);
SELECT touch_updated_at('data_process_tasks');
CREATE UNIQUE INDEX IF NOT EXISTS uq_data_process_tasks_name_alive
ON data_process_tasks(name) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_data_process_tasks_status_created
ON data_process_tasks(status, created_at DESC) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_data_process_tasks_creator_created
ON data_process_tasks(created_by, created_at DESC) WHERE deleted_at IS NULL;
CREATE TABLE IF NOT EXISTS data_process_source_files (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
task_id uuid NOT NULL REFERENCES data_process_tasks(id) ON DELETE CASCADE,
storage_object_id uuid REFERENCES storage_objects(id) ON DELETE SET NULL,
name text NOT NULL,
size_bytes bigint NOT NULL DEFAULT 0,
record_count bigint NOT NULL DEFAULT 0,
content_preview text,
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_data_process_source_files_task ON data_process_source_files(task_id);
CREATE TABLE IF NOT EXISTS data_process_preview_items (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
task_id uuid NOT NULL REFERENCES data_process_tasks(id) ON DELETE CASCADE,
source_file_id uuid REFERENCES data_process_source_files(id) ON DELETE CASCADE,
original_content text NOT NULL DEFAULT '',
edited_content text NOT NULL DEFAULT '',
source_start integer,
source_end integer,
source_start_line integer,
source_end_line integer,
token_count integer NOT NULL DEFAULT 0,
status varchar(20) NOT NULL DEFAULT 'original',
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
SELECT touch_updated_at('data_process_preview_items');
CREATE INDEX IF NOT EXISTS idx_data_process_preview_task_file
ON data_process_preview_items(task_id, source_file_id, created_at);
CREATE TABLE IF NOT EXISTS data_process_results (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
task_id uuid NOT NULL REFERENCES data_process_tasks(id) ON DELETE CASCADE,
preview_item_id uuid REFERENCES data_process_preview_items(id) ON DELETE SET NULL,
instruction text NOT NULL,
input text NOT NULL DEFAULT '',
output text NOT NULL,
original_instruction text,
original_input text,
original_output text,
status result_status NOT NULL DEFAULT 'valid',
error text,
split varchar(20),
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
SELECT touch_updated_at('data_process_results');
CREATE INDEX IF NOT EXISTS idx_data_process_results_task_status
ON data_process_results(task_id, status, id);
CREATE INDEX IF NOT EXISTS idx_data_process_results_task_split
ON data_process_results(task_id, split);
-- =========================
-- Fine-tune tasks
-- =========================
CREATE TABLE IF NOT EXISTS fine_tune_tasks (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name varchar(150) NOT NULL,
description text,
status task_status NOT NULL DEFAULT 'pending',
train_type train_type NOT NULL,
train_method train_method NOT NULL DEFAULT 'lora',
template varchar(80) NOT NULL DEFAULT 'qwen',
base_model_id uuid NOT NULL REFERENCES models(id) ON DELETE RESTRICT,
train_dataset_id uuid REFERENCES datasets(id) ON DELETE SET NULL,
auto_merge boolean NOT NULL DEFAULT false,
output_model_name varchar(150),
gpus integer[] NOT NULL DEFAULT '{}',
params jsonb NOT NULL DEFAULT '{}'::jsonb,
progress numeric(5,2) NOT NULL DEFAULT 0 CHECK (progress >= 0 AND progress <= 100),
process_id integer,
command text,
output_dir text,
log_file text,
train_duration_seconds integer,
failure_reason text,
created_by uuid REFERENCES users(id) ON DELETE SET NULL,
started_at timestamptz,
completed_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
deleted_at timestamptz
);
SELECT touch_updated_at('fine_tune_tasks');
CREATE UNIQUE INDEX IF NOT EXISTS uq_fine_tune_tasks_name_alive
ON fine_tune_tasks(name) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_fine_tune_tasks_status_created
ON fine_tune_tasks(status, created_at DESC) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_fine_tune_tasks_dataset ON fine_tune_tasks(train_dataset_id);
CREATE INDEX IF NOT EXISTS idx_fine_tune_tasks_base_model ON fine_tune_tasks(base_model_id);
ALTER TABLE trained_models
DROP CONSTRAINT IF EXISTS fk_trained_models_fine_tune_task;
ALTER TABLE trained_models
ADD CONSTRAINT fk_trained_models_fine_tune_task
FOREIGN KEY (fine_tune_task_id) REFERENCES fine_tune_tasks(id) ON DELETE SET NULL;
CREATE TABLE IF NOT EXISTS fine_tune_metrics (
id bigserial PRIMARY KEY,
task_id uuid NOT NULL REFERENCES fine_tune_tasks(id) ON DELETE CASCADE,
step integer,
epoch numeric(10,4),
loss numeric(18,8),
learning_rate numeric(18,12),
grad_norm numeric(18,8),
metrics jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_fine_tune_metrics_task_step ON fine_tune_metrics(task_id, step);
CREATE INDEX IF NOT EXISTS idx_fine_tune_metrics_task_created ON fine_tune_metrics(task_id, created_at);
-- =========================
-- Inference and compare
-- =========================
CREATE TABLE IF NOT EXISTS inference_tasks (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name varchar(150) NOT NULL,
description text,
status task_status NOT NULL DEFAULT 'pending',
load_status jsonb NOT NULL DEFAULT '{"loaded_models":[]}'::jsonb,
created_by uuid REFERENCES users(id) ON DELETE SET NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
deleted_at timestamptz
);
SELECT touch_updated_at('inference_tasks');
CREATE INDEX IF NOT EXISTS idx_inference_tasks_status_created
ON inference_tasks(status, created_at DESC) WHERE deleted_at IS NULL;
CREATE TABLE IF NOT EXISTS inference_task_models (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
task_id uuid NOT NULL REFERENCES inference_tasks(id) ON DELETE CASCADE,
model_id uuid REFERENCES models(id) ON DELETE SET NULL,
trained_model_id uuid REFERENCES trained_models(id) ON DELETE SET NULL,
model_name varchar(150) NOT NULL,
model_path text,
gpu_id integer,
source varchar(40),
port integer,
pid integer,
status varchar(40) NOT NULL DEFAULT 'pending',
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
SELECT touch_updated_at('inference_task_models');
CREATE INDEX IF NOT EXISTS idx_inference_task_models_task ON inference_task_models(task_id);
CREATE INDEX IF NOT EXISTS idx_inference_task_models_pid ON inference_task_models(pid) WHERE pid IS NOT NULL;
CREATE UNIQUE INDEX IF NOT EXISTS uq_inference_task_models_port_alive
ON inference_task_models(port) WHERE port IS NOT NULL AND status IN ('loading', 'ready');
CREATE TABLE IF NOT EXISTS chat_sessions (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
inference_task_id uuid REFERENCES inference_tasks(id) ON DELETE SET NULL,
title varchar(200),
created_by uuid REFERENCES users(id) ON DELETE SET NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
SELECT touch_updated_at('chat_sessions');
CREATE INDEX IF NOT EXISTS idx_chat_sessions_task_created ON chat_sessions(inference_task_id, created_at DESC);
CREATE TABLE IF NOT EXISTS chat_messages (
id bigserial PRIMARY KEY,
session_id uuid NOT NULL REFERENCES chat_sessions(id) ON DELETE CASCADE,
model_id uuid REFERENCES models(id) ON DELETE SET NULL,
trained_model_id uuid REFERENCES trained_models(id) ON DELETE SET NULL,
role varchar(20) NOT NULL,
content text NOT NULL,
latency_ms integer,
token_usage jsonb NOT NULL DEFAULT '{}'::jsonb,
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_chat_messages_session_id ON chat_messages(session_id, id);
-- =========================
-- Evaluation
-- =========================
CREATE TABLE IF NOT EXISTS eval_dimensions (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name varchar(150) NOT NULL,
type dimension_type NOT NULL,
description text,
eval_model_id uuid REFERENCES models(id) ON DELETE SET NULL,
eval_method jsonb NOT NULL DEFAULT '[]'::jsonb,
eval_prompt text,
is_active boolean NOT NULL DEFAULT true,
is_default boolean NOT NULL DEFAULT false,
bleu_n integer,
output_precision integer NOT NULL DEFAULT 3,
score_min numeric(12,4),
score_max numeric(12,4),
pass_threshold numeric(12,4),
created_by uuid REFERENCES users(id) ON DELETE SET NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
deleted_at timestamptz
);
SELECT touch_updated_at('eval_dimensions');
CREATE UNIQUE INDEX IF NOT EXISTS uq_eval_dimensions_name_alive
ON eval_dimensions(name) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_eval_dimensions_active ON eval_dimensions(is_active) WHERE deleted_at IS NULL;
CREATE TABLE IF NOT EXISTS eval_tasks (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
eval_task_name varchar(150) NOT NULL,
eval_type eval_type NOT NULL DEFAULT 'custom',
model_id uuid REFERENCES models(id) ON DELETE SET NULL,
trained_model_id uuid REFERENCES trained_models(id) ON DELETE SET NULL,
dataset_id uuid REFERENCES datasets(id) ON DELETE SET NULL,
dimension_id uuid REFERENCES eval_dimensions(id) ON DELETE SET NULL,
gpu_id integer,
data_source varchar(40) NOT NULL DEFAULT 'dataset',
leaderboard boolean NOT NULL DEFAULT false,
basic_metrics jsonb NOT NULL DEFAULT '{}'::jsonb,
status task_status NOT NULL DEFAULT 'pending',
metric varchar(80),
score numeric(12,4),
overall_score numeric(12,4),
overall_score_max numeric(12,4),
overall_evaluation text,
improvement_suggestions jsonb NOT NULL DEFAULT '[]'::jsonb,
sample_count integer NOT NULL DEFAULT 0,
completed_count integer NOT NULL DEFAULT 0,
passed_count integer NOT NULL DEFAULT 0,
failure_reason text,
created_by uuid REFERENCES users(id) ON DELETE SET NULL,
started_at timestamptz,
completed_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
deleted_at timestamptz
);
SELECT touch_updated_at('eval_tasks');
CREATE UNIQUE INDEX IF NOT EXISTS uq_eval_tasks_name_alive
ON eval_tasks(eval_task_name) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_eval_tasks_status_created ON eval_tasks(status, created_at DESC) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_eval_tasks_model_dataset ON eval_tasks(model_id, dataset_id);
CREATE TABLE IF NOT EXISTS eval_dimension_summaries (
id bigserial PRIMARY KEY,
eval_task_id uuid NOT NULL REFERENCES eval_tasks(id) ON DELETE CASCADE,
name varchar(150) NOT NULL,
score numeric(12,4),
max_score numeric(12,4),
pass_rate numeric(6,2),
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_eval_dimension_summaries_task ON eval_dimension_summaries(eval_task_id);
CREATE TABLE IF NOT EXISTS eval_sample_results (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
eval_task_id uuid NOT NULL REFERENCES eval_tasks(id) ON DELETE CASCADE,
sample_index integer NOT NULL,
input text NOT NULL,
reference_answer text,
model_output text NOT NULL DEFAULT '',
score numeric(12,4),
max_score numeric(12,4),
passed boolean,
status task_status NOT NULL DEFAULT 'pending',
judgement varchar(40),
evaluation_reason text,
error_type varchar(80),
dimension_scores jsonb NOT NULL DEFAULT '[]'::jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
SELECT touch_updated_at('eval_sample_results');
CREATE UNIQUE INDEX IF NOT EXISTS uq_eval_sample_results_task_index
ON eval_sample_results(eval_task_id, sample_index);
CREATE INDEX IF NOT EXISTS idx_eval_sample_results_task_status
ON eval_sample_results(eval_task_id, status);
-- =========================
-- Data convert and custom tools
-- =========================
CREATE TABLE IF NOT EXISTS data_convert_jobs (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
convert_type varchar(40) NOT NULL DEFAULT 'json_to_jsonl',
status task_status NOT NULL DEFAULT 'pending',
output_name varchar(200) NOT NULL,
encoding varchar(40) NOT NULL DEFAULT 'UTF-8',
source_object_id uuid REFERENCES storage_objects(id) ON DELETE SET NULL,
result_object_id uuid REFERENCES storage_objects(id) ON DELETE SET NULL,
error_message text,
created_by uuid REFERENCES users(id) ON DELETE SET NULL,
started_at timestamptz,
completed_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
SELECT touch_updated_at('data_convert_jobs');
CREATE INDEX IF NOT EXISTS idx_data_convert_jobs_user_created ON data_convert_jobs(created_by, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_data_convert_jobs_status ON data_convert_jobs(status, created_at DESC);
CREATE TABLE IF NOT EXISTS custom_tools (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name varchar(100) NOT NULL,
description text,
url text NOT NULL,
icon varchar(80) NOT NULL DEFAULT 'fa-cog',
visibility varchar(20) NOT NULL DEFAULT 'private',
owner_id uuid REFERENCES users(id) ON DELETE CASCADE,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
deleted_at timestamptz
);
SELECT touch_updated_at('custom_tools');
CREATE INDEX IF NOT EXISTS idx_custom_tools_owner ON custom_tools(owner_id) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_custom_tools_visibility ON custom_tools(visibility) WHERE deleted_at IS NULL;
-- =========================
-- Metrics snapshots
-- =========================
CREATE TABLE IF NOT EXISTS system_metric_snapshots (
id bigserial PRIMARY KEY,
cpu jsonb NOT NULL DEFAULT '{}'::jsonb,
memory jsonb NOT NULL DEFAULT '{}'::jsonb,
disk jsonb NOT NULL DEFAULT '{}'::jsonb,
gpu jsonb NOT NULL DEFAULT '[]'::jsonb,
network jsonb NOT NULL DEFAULT '{}'::jsonb,
system jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_system_metric_snapshots_created ON system_metric_snapshots(created_at DESC);
CREATE TABLE IF NOT EXISTS service_status_snapshots (
id bigserial PRIMARY KEY,
service_name varchar(100) NOT NULL,
state varchar(30) NOT NULL,
instances_online integer NOT NULL DEFAULT 0,
instances_total integer NOT NULL DEFAULT 0,
detail jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_service_status_snapshots_name_created
ON service_status_snapshots(service_name, created_at DESC);
-- =========================
-- Useful views
-- =========================
CREATE OR REPLACE VIEW v_user_effective_permissions AS
SELECT
u.id AS user_id,
u.username,
p.code AS permission_code,
COALESCE(up.allowed, rp.permission_code IS NOT NULL, false) AS allowed
FROM users u
CROSS JOIN permissions p
LEFT JOIN role_permissions rp
ON rp.role = u.role AND rp.permission_code = p.code
LEFT JOIN user_permissions up
ON up.user_id = u.id AND up.permission_code = p.code
WHERE u.deleted_at IS NULL;
CREATE OR REPLACE VIEW v_dataset_summary AS
SELECT
d.id,
d.name,
d.type,
d.storage_type,
d.source,
d.source_task_id,
d.size_bytes,
d.record_count,
d.description,
count(df.id) FILTER (WHERE df.deleted_at IS NULL) AS file_count,
d.created_at,
d.updated_at
FROM datasets d
LEFT JOIN dataset_files df ON df.dataset_id = d.id
WHERE d.deleted_at IS NULL
GROUP BY d.id;
-- =========================
-- Maintenance notes
-- =========================
-- 1. For very large installations, convert audit_logs, web_logs,
-- system_metric_snapshots, fine_tune_metrics and eval_sample_results to
-- monthly/range partitions.
-- 2. Keep large file bodies in storage_objects, not in relational rows.
-- 3. Encrypt api_key_encrypted and external source secrets at the application layer
-- with KMS or a deployment secret.
-- 4. Use soft delete for user-facing resources to preserve audit trails.
-- =========================
-- Enterprise governance and compute extension
-- =========================
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'tenant_status') THEN
CREATE TYPE tenant_status AS ENUM ('active', 'disabled');
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'project_status') THEN
CREATE TYPE project_status AS ENUM ('active', 'archived', 'disabled');
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'project_role') THEN
CREATE TYPE project_role AS ENUM ('owner', 'maintainer', 'developer', 'reviewer', 'viewer');
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'approval_status') THEN
CREATE TYPE approval_status AS ENUM ('not_required', 'pending', 'approved', 'rejected', 'cancelled', 'expired');
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'compute_job_type') THEN
CREATE TYPE compute_job_type AS ENUM ('fine_tune', 'eval', 'data_process', 'inference', 'merge', 'convert', 'import');
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'job_priority') THEN
CREATE TYPE job_priority AS ENUM ('low', 'normal', 'high', 'urgent');
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'gpu_status') THEN
CREATE TYPE gpu_status AS ENUM ('idle', 'reserved', 'running', 'draining', 'offline', 'error');
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'service_level') THEN
CREATE TYPE service_level AS ENUM ('test', 'production');
END IF;
END $$;
CREATE TABLE IF NOT EXISTS tenants (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
code citext NOT NULL UNIQUE,
name varchar(150) NOT NULL,
status tenant_status NOT NULL DEFAULT 'active',
owner_id uuid REFERENCES users(id) ON DELETE SET NULL,
quota_config jsonb NOT NULL DEFAULT '{}'::jsonb,
retention_config jsonb NOT NULL DEFAULT '{}'::jsonb,
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
deleted_at timestamptz
);
SELECT touch_updated_at('tenants');
CREATE INDEX IF NOT EXISTS idx_tenants_status ON tenants(status) WHERE deleted_at IS NULL;
CREATE TABLE IF NOT EXISTS tenant_users (
tenant_id uuid NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
role user_role NOT NULL DEFAULT 'viewer',
is_tenant_admin boolean NOT NULL DEFAULT false,
created_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (tenant_id, user_id)
);
CREATE INDEX IF NOT EXISTS idx_tenant_users_user ON tenant_users(user_id);
CREATE TABLE IF NOT EXISTS projects (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id uuid NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
code citext NOT NULL,
name varchar(150) NOT NULL,
description text,
status project_status NOT NULL DEFAULT 'active',
owner_id uuid REFERENCES users(id) ON DELETE SET NULL,
quota_config jsonb NOT NULL DEFAULT '{}'::jsonb,
default_acl jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
archived_at timestamptz,
deleted_at timestamptz,
UNIQUE (tenant_id, code)
);
SELECT touch_updated_at('projects');
CREATE INDEX IF NOT EXISTS idx_projects_tenant_status ON projects(tenant_id, status) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_projects_owner ON projects(owner_id) WHERE deleted_at IS NULL;
CREATE TABLE IF NOT EXISTS project_members (
project_id uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
role project_role NOT NULL DEFAULT 'viewer',
created_by uuid REFERENCES users(id) ON DELETE SET NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (project_id, user_id)
);
SELECT touch_updated_at('project_members');
CREATE INDEX IF NOT EXISTS idx_project_members_user ON project_members(user_id);
CREATE TABLE IF NOT EXISTS storage_nodes (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
code citext NOT NULL UNIQUE,
name varchar(150) NOT NULL,
node_type varchar(40) NOT NULL DEFAULT 'compute_local',
base_path text NOT NULL,
total_bytes bigint,
used_bytes bigint NOT NULL DEFAULT 0,
status varchar(40) NOT NULL DEFAULT 'online',
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
SELECT touch_updated_at('storage_nodes');
CREATE TABLE IF NOT EXISTS training_engines (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
code citext NOT NULL UNIQUE,
name varchar(150) NOT NULL,
version varchar(80),
engine_type varchar(60) NOT NULL DEFAULT 'llama_factory',
executable_path text,
python_env_path text,
supported_task_types jsonb NOT NULL DEFAULT '[]'::jsonb,
supported_methods jsonb NOT NULL DEFAULT '[]'::jsonb,
supported_formats jsonb NOT NULL DEFAULT '[]'::jsonb,
schema jsonb NOT NULL DEFAULT '{}'::jsonb,
status varchar(40) NOT NULL DEFAULT 'enabled',
last_health_check_at timestamptz,
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
SELECT touch_updated_at('training_engines');
INSERT INTO training_engines(
code, name, engine_type, supported_task_types, supported_methods, supported_formats, status
) VALUES (
'llama_factory',
'LLaMA-Factory',
'llama_factory',
'["SFT", "DPO", "CPT"]'::jsonb,
'["lora", "qlora", "full"]'::jsonb,
'["alpaca", "sharegpt", "dpo_pair", "pretrain_text"]'::jsonb,
'enabled'
) ON CONFLICT (code) DO NOTHING;
CREATE TABLE IF NOT EXISTS quotas (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id uuid REFERENCES tenants(id) ON DELETE CASCADE,
project_id uuid REFERENCES projects(id) ON DELETE CASCADE,
subject_type varchar(40) NOT NULL,
subject_id uuid,
gpu_concurrency integer NOT NULL DEFAULT 0,
storage_bytes bigint NOT NULL DEFAULT 0,
max_running_jobs integer NOT NULL DEFAULT 0,
max_projects integer NOT NULL DEFAULT 0,
max_upload_file_bytes bigint NOT NULL DEFAULT 0,
config jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (subject_type, subject_id)
);
SELECT touch_updated_at('quotas');
CREATE INDEX IF NOT EXISTS idx_quotas_tenant_project ON quotas(tenant_id, project_id);
CREATE TABLE IF NOT EXISTS quota_usage (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id uuid REFERENCES tenants(id) ON DELETE CASCADE,
project_id uuid REFERENCES projects(id) ON DELETE CASCADE,
subject_type varchar(40) NOT NULL,
subject_id uuid,
gpu_running integer NOT NULL DEFAULT 0,
storage_used_bytes bigint NOT NULL DEFAULT 0,
running_jobs integer NOT NULL DEFAULT 0,
usage_detail jsonb NOT NULL DEFAULT '{}'::jsonb,
measured_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (subject_type, subject_id)
);
CREATE INDEX IF NOT EXISTS idx_quota_usage_tenant_project ON quota_usage(tenant_id, project_id);
CREATE TABLE IF NOT EXISTS retention_policies (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id uuid REFERENCES tenants(id) ON DELETE CASCADE,
project_id uuid REFERENCES projects(id) ON DELETE CASCADE,
audit_days integer NOT NULL DEFAULT 180,
login_log_days integer NOT NULL DEFAULT 180,
training_log_days integer NOT NULL DEFAULT 90,
metric_raw_days integer NOT NULL DEFAULT 30,
temp_file_days integer NOT NULL DEFAULT 1,
failed_job_workspace_days integer NOT NULL DEFAULT 14,
checkpoint_policy jsonb NOT NULL DEFAULT '{"keep_last":3,"keep_best":2,"failed_job_days":14}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (tenant_id, project_id)
);
SELECT touch_updated_at('retention_policies');
CREATE TABLE IF NOT EXISTS approval_templates (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id uuid REFERENCES tenants(id) ON DELETE CASCADE,
project_id uuid REFERENCES projects(id) ON DELETE CASCADE,
action varchar(80) NOT NULL,
name varchar(150) NOT NULL,
enabled boolean NOT NULL DEFAULT true,
approver_rules jsonb NOT NULL DEFAULT '[]'::jsonb,
risk_level varchar(40) NOT NULL DEFAULT 'medium',
expire_hours integer NOT NULL DEFAULT 24,
created_by uuid REFERENCES users(id) ON DELETE SET NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
SELECT touch_updated_at('approval_templates');
CREATE INDEX IF NOT EXISTS idx_approval_templates_scope_action
ON approval_templates(tenant_id, project_id, action);
CREATE TABLE IF NOT EXISTS approval_instances (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id uuid REFERENCES tenants(id) ON DELETE CASCADE,
project_id uuid REFERENCES projects(id) ON DELETE CASCADE,
template_id uuid REFERENCES approval_templates(id) ON DELETE SET NULL,
action varchar(80) NOT NULL,
resource_type varchar(80) NOT NULL,
resource_id text NOT NULL,
reason text,
payload jsonb NOT NULL DEFAULT '{}'::jsonb,
status approval_status NOT NULL DEFAULT 'pending',
requested_by uuid REFERENCES users(id) ON DELETE SET NULL,
decided_by uuid REFERENCES users(id) ON DELETE SET NULL,
decided_at timestamptz,
expires_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
SELECT touch_updated_at('approval_instances');
CREATE INDEX IF NOT EXISTS idx_approval_instances_status_created
ON approval_instances(status, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_approval_instances_scope
ON approval_instances(tenant_id, project_id, action, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_approval_instances_resource
ON approval_instances(resource_type, resource_id);
CREATE TABLE IF NOT EXISTS approval_steps (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
approval_id uuid NOT NULL REFERENCES approval_instances(id) ON DELETE CASCADE,
step_no integer NOT NULL,
approver_user_id uuid REFERENCES users(id) ON DELETE SET NULL,
approver_role varchar(80),
status approval_status NOT NULL DEFAULT 'pending',
comment text,
decided_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (approval_id, step_no)
);
CREATE INDEX IF NOT EXISTS idx_approval_steps_approver
ON approval_steps(approver_user_id, status);
CREATE TABLE IF NOT EXISTS compute_nodes (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
code citext NOT NULL UNIQUE,
name varchar(150) NOT NULL,
host varchar(200) NOT NULL,
api_base_url text NOT NULL,
file_gateway_url text NOT NULL DEFAULT '',
storage_node_id uuid REFERENCES storage_nodes(id) ON DELETE SET NULL,
status varchar(40) NOT NULL DEFAULT 'online',
scheduler_status varchar(40) NOT NULL DEFAULT 'online',
scheduler_weight integer NOT NULL DEFAULT 100,
enabled boolean NOT NULL DEFAULT true,
max_parallel_jobs integer NOT NULL DEFAULT 1,
data_root text NOT NULL DEFAULT '/data/yg-ft',
model_root text NOT NULL DEFAULT '/data/yg-ft/models',
log_root text NOT NULL DEFAULT '/opt/yg-ft/logs/training',
api_version varchar(40) NOT NULL DEFAULT 'v1',
capabilities jsonb NOT NULL DEFAULT '[]'::jsonb,
description text,
health_detail jsonb NOT NULL DEFAULT '{}'::jsonb,
agent_version varchar(80),
gpu_count integer NOT NULL DEFAULT 0,
last_heartbeat_at timestamptz,
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
SELECT touch_updated_at('compute_nodes');
CREATE INDEX IF NOT EXISTS idx_compute_nodes_status ON compute_nodes(status);
CREATE TABLE IF NOT EXISTS gpu_devices (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
compute_node_id uuid NOT NULL REFERENCES compute_nodes(id) ON DELETE CASCADE,
gpu_index integer NOT NULL,
uuid varchar(120) NOT NULL,
name varchar(150) NOT NULL,
status gpu_status NOT NULL DEFAULT 'idle',
memory_total_mb integer NOT NULL DEFAULT 0,
memory_used_mb integer NOT NULL DEFAULT 0,
utilization_percent numeric(5,2) NOT NULL DEFAULT 0,
temperature numeric(5,2),
power_w numeric(8,2),
driver_version varchar(80),
partition_type varchar(40) NOT NULL DEFAULT 'full',
parent_gpu_uuid varchar(120),
current_job_id uuid,
last_seen_at timestamptz,
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (compute_node_id, gpu_index),
UNIQUE (uuid)
);
SELECT touch_updated_at('gpu_devices');
CREATE INDEX IF NOT EXISTS idx_gpu_devices_node_status ON gpu_devices(compute_node_id, status);
CREATE TABLE IF NOT EXISTS compute_jobs (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
app_task_type varchar(80) NOT NULL,
app_task_id uuid,
tenant_id uuid REFERENCES tenants(id) ON DELETE SET NULL,
project_id uuid REFERENCES projects(id) ON DELETE SET NULL,
compute_node_id uuid REFERENCES compute_nodes(id) ON DELETE SET NULL,
engine_id uuid REFERENCES training_engines(id) ON DELETE SET NULL,
job_type compute_job_type NOT NULL,
status task_status NOT NULL DEFAULT 'pending',
priority job_priority NOT NULL DEFAULT 'normal',
resource_request jsonb NOT NULL DEFAULT '{}'::jsonb,
workspace_root text,
payload jsonb NOT NULL DEFAULT '{}'::jsonb,
result jsonb NOT NULL DEFAULT '{}'::jsonb,
progress numeric(5,2) NOT NULL DEFAULT 0 CHECK (progress >= 0 AND progress <= 100),
pid integer,
port integer,
failure_reason text,
requested_by uuid REFERENCES users(id) ON DELETE SET NULL,
started_at timestamptz,
completed_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
SELECT touch_updated_at('compute_jobs');
CREATE INDEX IF NOT EXISTS idx_compute_jobs_scope_status
ON compute_jobs(tenant_id, project_id, status, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_compute_jobs_node_status
ON compute_jobs(compute_node_id, status, priority, created_at);
CREATE INDEX IF NOT EXISTS idx_compute_jobs_app_task
ON compute_jobs(app_task_type, app_task_id);
CREATE TABLE IF NOT EXISTS gpu_allocations (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
compute_job_id uuid NOT NULL REFERENCES compute_jobs(id) ON DELETE CASCADE,
gpu_device_id uuid NOT NULL REFERENCES gpu_devices(id) ON DELETE RESTRICT,
tenant_id uuid REFERENCES tenants(id) ON DELETE SET NULL,
project_id uuid REFERENCES projects(id) ON DELETE SET NULL,
status varchar(40) NOT NULL DEFAULT 'reserved',
allocated_at timestamptz NOT NULL DEFAULT now(),
released_at timestamptz
);
CREATE INDEX IF NOT EXISTS idx_gpu_allocations_job ON gpu_allocations(compute_job_id);
CREATE INDEX IF NOT EXISTS idx_gpu_allocations_scope ON gpu_allocations(tenant_id, project_id, allocated_at DESC);
CREATE UNIQUE INDEX IF NOT EXISTS uq_gpu_allocations_active_gpu
ON gpu_allocations(gpu_device_id) WHERE released_at IS NULL;
-- Multi compute-node scheduling and local-cache metadata.
-- Each GPU server is modeled as one compute node. Nodes do not call each other;
-- the application platform schedules jobs and syncs resources through each node's File Gateway.
ALTER TABLE compute_nodes ADD COLUMN IF NOT EXISTS file_gateway_url text;
ALTER TABLE compute_nodes ADD COLUMN IF NOT EXISTS enabled boolean NOT NULL DEFAULT true;
ALTER TABLE compute_nodes ADD COLUMN IF NOT EXISTS scheduler_status varchar(40) NOT NULL DEFAULT 'online';
ALTER TABLE compute_nodes ADD COLUMN IF NOT EXISTS scheduler_weight integer NOT NULL DEFAULT 100 CHECK (scheduler_weight >= 0);
ALTER TABLE compute_nodes ADD COLUMN IF NOT EXISTS tags text[] NOT NULL DEFAULT '{}';
ALTER TABLE compute_nodes ADD COLUMN IF NOT EXISTS service_token_encrypted text;
ALTER TABLE compute_nodes ADD COLUMN IF NOT EXISTS data_root text NOT NULL DEFAULT '/data/yg-ft';
ALTER TABLE compute_nodes ADD COLUMN IF NOT EXISTS model_root text NOT NULL DEFAULT '/data/yg-ft/models';
ALTER TABLE compute_nodes ADD COLUMN IF NOT EXISTS log_root text NOT NULL DEFAULT '/opt/yg-ft/logs/compute';
ALTER TABLE compute_nodes ADD COLUMN IF NOT EXISTS max_parallel_jobs integer NOT NULL DEFAULT 1 CHECK (max_parallel_jobs >= 0);
ALTER TABLE compute_nodes ADD COLUMN IF NOT EXISTS current_running_jobs integer NOT NULL DEFAULT 0 CHECK (current_running_jobs >= 0);
ALTER TABLE compute_nodes ADD COLUMN IF NOT EXISTS last_health_check_at timestamptz;
ALTER TABLE compute_nodes ADD COLUMN IF NOT EXISTS health_detail jsonb NOT NULL DEFAULT '{}'::jsonb;
ALTER TABLE compute_nodes ADD COLUMN IF NOT EXISTS drain_reason text;
CREATE INDEX IF NOT EXISTS idx_compute_nodes_scheduler
ON compute_nodes(enabled, scheduler_status, scheduler_weight DESC, last_health_check_at DESC);
CREATE INDEX IF NOT EXISTS idx_compute_nodes_tags_gin
ON compute_nodes USING gin(tags);
ALTER TABLE compute_jobs ADD COLUMN IF NOT EXISTS scheduler_mode varchar(40) NOT NULL DEFAULT 'auto';
ALTER TABLE compute_jobs ADD COLUMN IF NOT EXISTS requested_node_id uuid REFERENCES compute_nodes(id) ON DELETE SET NULL;
ALTER TABLE compute_jobs ADD COLUMN IF NOT EXISTS assigned_at timestamptz;
ALTER TABLE compute_jobs ADD COLUMN IF NOT EXISTS scheduler_reason text;
CREATE INDEX IF NOT EXISTS idx_compute_jobs_requested_node
ON compute_jobs(requested_node_id, status, created_at DESC);
CREATE TABLE IF NOT EXISTS compute_node_engines (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
compute_node_id uuid NOT NULL REFERENCES compute_nodes(id) ON DELETE CASCADE,
engine_id uuid REFERENCES training_engines(id) ON DELETE SET NULL,
engine_code varchar(80) NOT NULL,
engine_version varchar(80),
home_path text,
status varchar(40) NOT NULL DEFAULT 'available',
capability jsonb NOT NULL DEFAULT '{}'::jsonb,
last_health_check_at timestamptz,
health_detail jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (compute_node_id, engine_code)
);
SELECT touch_updated_at('compute_node_engines');
CREATE INDEX IF NOT EXISTS idx_compute_node_engines_node_status
ON compute_node_engines(compute_node_id, status, engine_code);
CREATE TABLE IF NOT EXISTS resource_replicas (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id uuid REFERENCES tenants(id) ON DELETE SET NULL,
project_id uuid REFERENCES projects(id) ON DELETE SET NULL,
resource_type varchar(80) NOT NULL,
resource_id uuid NOT NULL,
storage_object_id uuid REFERENCES storage_objects(id) ON DELETE SET NULL,
compute_node_id uuid NOT NULL REFERENCES compute_nodes(id) ON DELETE CASCADE,
local_path text NOT NULL,
status varchar(40) NOT NULL DEFAULT 'available',
sync_status varchar(40) NOT NULL DEFAULT 'synced',
checksum_sha256 char(64),
byte_size bigint NOT NULL DEFAULT 0 CHECK (byte_size >= 0),
version varchar(120),
pinned boolean NOT NULL DEFAULT false,
last_verified_at timestamptz,
expires_at timestamptz,
failure_reason text,
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (resource_type, resource_id, compute_node_id)
);
SELECT touch_updated_at('resource_replicas');
CREATE INDEX IF NOT EXISTS idx_resource_replicas_resource
ON resource_replicas(resource_type, resource_id, status);
CREATE INDEX IF NOT EXISTS idx_resource_replicas_node_status
ON resource_replicas(compute_node_id, status, sync_status, updated_at DESC);
CREATE INDEX IF NOT EXISTS idx_resource_replicas_scope
ON resource_replicas(tenant_id, project_id, resource_type, updated_at DESC);
CREATE TABLE IF NOT EXISTS resource_sync_jobs (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id uuid REFERENCES tenants(id) ON DELETE SET NULL,
project_id uuid REFERENCES projects(id) ON DELETE SET NULL,
target_compute_node_id uuid NOT NULL REFERENCES compute_nodes(id) ON DELETE CASCADE,
source_compute_node_id uuid REFERENCES compute_nodes(id) ON DELETE SET NULL,
resource_type varchar(80) NOT NULL,
resource_id uuid NOT NULL,
storage_object_id uuid REFERENCES storage_objects(id) ON DELETE SET NULL,
status task_status NOT NULL DEFAULT 'pending',
transfer_mode varchar(40) NOT NULL DEFAULT 'app_proxy',
source_uri text,
target_path text NOT NULL,
byte_size bigint NOT NULL DEFAULT 0 CHECK (byte_size >= 0),
checksum_sha256 char(64),
progress numeric(5,2) NOT NULL DEFAULT 0 CHECK (progress >= 0 AND progress <= 100),
failure_reason text,
requested_by uuid REFERENCES users(id) ON DELETE SET NULL,
started_at timestamptz,
completed_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
SELECT touch_updated_at('resource_sync_jobs');
CREATE INDEX IF NOT EXISTS idx_resource_sync_jobs_status
ON resource_sync_jobs(status, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_resource_sync_jobs_target
ON resource_sync_jobs(target_compute_node_id, status, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_resource_sync_jobs_resource
ON resource_sync_jobs(resource_type, resource_id, status);
CREATE TABLE IF NOT EXISTS resource_acl (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id uuid REFERENCES tenants(id) ON DELETE CASCADE,
project_id uuid REFERENCES projects(id) ON DELETE CASCADE,
resource_type varchar(80) NOT NULL,
resource_id text NOT NULL,
subject_type varchar(40) NOT NULL,
subject_id text NOT NULL,
permissions text[] NOT NULL DEFAULT '{}',
created_by uuid REFERENCES users(id) ON DELETE SET NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (resource_type, resource_id, subject_type, subject_id)
);
SELECT touch_updated_at('resource_acl');
CREATE INDEX IF NOT EXISTS idx_resource_acl_resource
ON resource_acl(resource_type, resource_id);
CREATE INDEX IF NOT EXISTS idx_resource_acl_subject
ON resource_acl(subject_type, subject_id);
CREATE TABLE IF NOT EXISTS file_upload_sessions (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id uuid REFERENCES tenants(id) ON DELETE CASCADE,
project_id uuid REFERENCES projects(id) ON DELETE CASCADE,
storage_node_id uuid REFERENCES storage_nodes(id) ON DELETE SET NULL,
original_name text NOT NULL,
byte_size bigint NOT NULL DEFAULT 0,
checksum_sha256 char(64),
part_size_bytes integer NOT NULL DEFAULT 8388608,
uploaded_parts jsonb NOT NULL DEFAULT '[]'::jsonb,
status varchar(40) NOT NULL DEFAULT 'uploading',
created_by uuid REFERENCES users(id) ON DELETE SET NULL,
expires_at timestamptz NOT NULL DEFAULT now() + interval '1 day',
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
SELECT touch_updated_at('file_upload_sessions');
CREATE INDEX IF NOT EXISTS idx_file_upload_sessions_scope_status
ON file_upload_sessions(tenant_id, project_id, status, created_at DESC);
CREATE TABLE IF NOT EXISTS local_import_jobs (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id uuid REFERENCES tenants(id) ON DELETE CASCADE,
project_id uuid REFERENCES projects(id) ON DELETE CASCADE,
compute_node_id uuid REFERENCES compute_nodes(id) ON DELETE SET NULL,
import_type varchar(40) NOT NULL,
source_path text NOT NULL,
target_resource_id uuid,
status task_status NOT NULL DEFAULT 'pending',
scan_result jsonb NOT NULL DEFAULT '{}'::jsonb,
failure_reason text,
created_by uuid REFERENCES users(id) ON DELETE SET NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
SELECT touch_updated_at('local_import_jobs');
CREATE INDEX IF NOT EXISTS idx_local_import_jobs_scope_status
ON local_import_jobs(tenant_id, project_id, status, created_at DESC);
CREATE TABLE IF NOT EXISTS fine_tune_checkpoints (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
fine_tune_task_id uuid NOT NULL REFERENCES fine_tune_tasks(id) ON DELETE CASCADE,
storage_object_id uuid REFERENCES storage_objects(id) ON DELETE SET NULL,
checkpoint_name varchar(200) NOT NULL,
step integer,
metric_name varchar(80),
metric_value numeric(18,8),
is_best boolean NOT NULL DEFAULT false,
protected boolean NOT NULL DEFAULT false,
size_bytes bigint NOT NULL DEFAULT 0,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_fine_tune_checkpoints_task_step
ON fine_tune_checkpoints(fine_tune_task_id, step DESC);
CREATE INDEX IF NOT EXISTS idx_fine_tune_checkpoints_best
ON fine_tune_checkpoints(fine_tune_task_id, is_best) WHERE is_best;
CREATE TABLE IF NOT EXISTS model_services (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id uuid REFERENCES tenants(id) ON DELETE CASCADE,
project_id uuid REFERENCES projects(id) ON DELETE CASCADE,
model_id uuid REFERENCES models(id) ON DELETE SET NULL,
trained_model_id uuid REFERENCES trained_models(id) ON DELETE SET NULL,
compute_job_id uuid REFERENCES compute_jobs(id) ON DELETE SET NULL,
name varchar(150) NOT NULL,
service_level service_level NOT NULL DEFAULT 'test',
status task_status NOT NULL DEFAULT 'pending',
gpu_device_id uuid REFERENCES gpu_devices(id) ON DELETE SET NULL,
port integer,
max_concurrency integer NOT NULL DEFAULT 1,
timeout_seconds integer NOT NULL DEFAULT 120,
max_context_tokens integer,
approval_id uuid REFERENCES approval_instances(id) ON DELETE SET NULL,
created_by uuid REFERENCES users(id) ON DELETE SET NULL,
started_at timestamptz,
stopped_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
deleted_at timestamptz
);
SELECT touch_updated_at('model_services');
CREATE INDEX IF NOT EXISTS idx_model_services_scope_status
ON model_services(tenant_id, project_id, status, created_at DESC) WHERE deleted_at IS NULL;
CREATE TABLE IF NOT EXISTS cleanup_jobs (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id uuid REFERENCES tenants(id) ON DELETE CASCADE,
project_id uuid REFERENCES projects(id) ON DELETE CASCADE,
cleanup_type varchar(80) NOT NULL,
status task_status NOT NULL DEFAULT 'pending',
target jsonb NOT NULL DEFAULT '{}'::jsonb,
result jsonb NOT NULL DEFAULT '{}'::jsonb,
failure_reason text,
created_at timestamptz NOT NULL DEFAULT now(),
started_at timestamptz,
completed_at timestamptz
);
CREATE INDEX IF NOT EXISTS idx_cleanup_jobs_status_created ON cleanup_jobs(status, created_at DESC);
-- User identity provider extension. First release uses local accounts;
-- OIDC/LDAP can be enabled later without changing resource ownership tables.
ALTER TABLE users ADD COLUMN IF NOT EXISTS auth_provider varchar(40) NOT NULL DEFAULT 'local';
ALTER TABLE users ADD COLUMN IF NOT EXISTS external_id text;
ALTER TABLE users ADD COLUMN IF NOT EXISTS default_tenant_id uuid REFERENCES tenants(id) ON DELETE SET NULL;
CREATE INDEX IF NOT EXISTS idx_users_external_provider ON users(auth_provider, external_id);
CREATE INDEX IF NOT EXISTS idx_users_default_tenant ON users(default_tenant_id);
-- Add tenant/project/resource governance columns to existing resource tables.
ALTER TABLE storage_objects ADD COLUMN IF NOT EXISTS tenant_id uuid REFERENCES tenants(id) ON DELETE SET NULL;
ALTER TABLE storage_objects ADD COLUMN IF NOT EXISTS project_id uuid REFERENCES projects(id) ON DELETE SET NULL;
ALTER TABLE storage_objects ADD COLUMN IF NOT EXISTS storage_node_id uuid REFERENCES storage_nodes(id) ON DELETE SET NULL;
ALTER TABLE storage_objects ADD COLUMN IF NOT EXISTS visibility varchar(40) NOT NULL DEFAULT 'project';
ALTER TABLE storage_objects ADD COLUMN IF NOT EXISTS deleted_at timestamptz;
CREATE INDEX IF NOT EXISTS idx_storage_objects_scope
ON storage_objects(tenant_id, project_id, created_at DESC) WHERE deleted_at IS NULL;
ALTER TABLE models ADD COLUMN IF NOT EXISTS tenant_id uuid REFERENCES tenants(id) ON DELETE SET NULL;
ALTER TABLE models ADD COLUMN IF NOT EXISTS project_id uuid REFERENCES projects(id) ON DELETE SET NULL;
ALTER TABLE models ADD COLUMN IF NOT EXISTS owner_id uuid REFERENCES users(id) ON DELETE SET NULL;
ALTER TABLE models ADD COLUMN IF NOT EXISTS visibility varchar(40) NOT NULL DEFAULT 'project';
ALTER TABLE models ADD COLUMN IF NOT EXISTS approval_status approval_status NOT NULL DEFAULT 'not_required';
CREATE INDEX IF NOT EXISTS idx_models_scope_status
ON models(tenant_id, project_id, approval_status, created_at DESC) WHERE deleted_at IS NULL;
ALTER TABLE trained_models ADD COLUMN IF NOT EXISTS tenant_id uuid REFERENCES tenants(id) ON DELETE SET NULL;
ALTER TABLE trained_models ADD COLUMN IF NOT EXISTS project_id uuid REFERENCES projects(id) ON DELETE SET NULL;
ALTER TABLE trained_models ADD COLUMN IF NOT EXISTS owner_id uuid REFERENCES users(id) ON DELETE SET NULL;
ALTER TABLE trained_models ADD COLUMN IF NOT EXISTS visibility varchar(40) NOT NULL DEFAULT 'project';
ALTER TABLE trained_models ADD COLUMN IF NOT EXISTS approval_status approval_status NOT NULL DEFAULT 'not_required';
CREATE INDEX IF NOT EXISTS idx_trained_models_scope
ON trained_models(tenant_id, project_id, created_at DESC) WHERE deleted_at IS NULL;
ALTER TABLE datasets ADD COLUMN IF NOT EXISTS tenant_id uuid REFERENCES tenants(id) ON DELETE SET NULL;
ALTER TABLE datasets ADD COLUMN IF NOT EXISTS project_id uuid REFERENCES projects(id) ON DELETE SET NULL;
ALTER TABLE datasets ADD COLUMN IF NOT EXISTS owner_id uuid REFERENCES users(id) ON DELETE SET NULL;
ALTER TABLE datasets ADD COLUMN IF NOT EXISTS visibility varchar(40) NOT NULL DEFAULT 'project';
ALTER TABLE datasets ADD COLUMN IF NOT EXISTS approval_status approval_status NOT NULL DEFAULT 'not_required';
CREATE INDEX IF NOT EXISTS idx_datasets_scope_status
ON datasets(tenant_id, project_id, type, created_at DESC) WHERE deleted_at IS NULL;
ALTER TABLE dataset_files ADD COLUMN IF NOT EXISTS tenant_id uuid REFERENCES tenants(id) ON DELETE SET NULL;
ALTER TABLE dataset_files ADD COLUMN IF NOT EXISTS project_id uuid REFERENCES projects(id) ON DELETE SET NULL;
CREATE INDEX IF NOT EXISTS idx_dataset_files_scope
ON dataset_files(tenant_id, project_id, created_at DESC) WHERE deleted_at IS NULL;
ALTER TABLE data_process_tasks ADD COLUMN IF NOT EXISTS tenant_id uuid REFERENCES tenants(id) ON DELETE SET NULL;
ALTER TABLE data_process_tasks ADD COLUMN IF NOT EXISTS project_id uuid REFERENCES projects(id) ON DELETE SET NULL;
ALTER TABLE data_process_tasks ADD COLUMN IF NOT EXISTS owner_id uuid REFERENCES users(id) ON DELETE SET NULL;
ALTER TABLE data_process_tasks ADD COLUMN IF NOT EXISTS approval_status approval_status NOT NULL DEFAULT 'not_required';
ALTER TABLE data_process_tasks ADD COLUMN IF NOT EXISTS compute_job_id uuid REFERENCES compute_jobs(id) ON DELETE SET NULL;
CREATE INDEX IF NOT EXISTS idx_data_process_tasks_scope_status
ON data_process_tasks(tenant_id, project_id, status, created_at DESC) WHERE deleted_at IS NULL;
ALTER TABLE fine_tune_tasks ADD COLUMN IF NOT EXISTS tenant_id uuid REFERENCES tenants(id) ON DELETE SET NULL;
ALTER TABLE fine_tune_tasks ADD COLUMN IF NOT EXISTS project_id uuid REFERENCES projects(id) ON DELETE SET NULL;
ALTER TABLE fine_tune_tasks ADD COLUMN IF NOT EXISTS owner_id uuid REFERENCES users(id) ON DELETE SET NULL;
ALTER TABLE fine_tune_tasks ADD COLUMN IF NOT EXISTS approval_status approval_status NOT NULL DEFAULT 'not_required';
ALTER TABLE fine_tune_tasks ADD COLUMN IF NOT EXISTS compute_node_id uuid REFERENCES compute_nodes(id) ON DELETE SET NULL;
ALTER TABLE fine_tune_tasks ADD COLUMN IF NOT EXISTS compute_job_id uuid REFERENCES compute_jobs(id) ON DELETE SET NULL;
ALTER TABLE fine_tune_tasks ADD COLUMN IF NOT EXISTS resume_checkpoint_id uuid REFERENCES fine_tune_checkpoints(id) ON DELETE SET NULL;
CREATE INDEX IF NOT EXISTS idx_fine_tune_tasks_scope_status
ON fine_tune_tasks(tenant_id, project_id, status, created_at DESC) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_fine_tune_tasks_compute_job
ON fine_tune_tasks(compute_job_id) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_fine_tune_tasks_node_status
ON fine_tune_tasks(compute_node_id, status, created_at DESC) WHERE deleted_at IS NULL;
ALTER TABLE inference_tasks ADD COLUMN IF NOT EXISTS tenant_id uuid REFERENCES tenants(id) ON DELETE SET NULL;
ALTER TABLE inference_tasks ADD COLUMN IF NOT EXISTS project_id uuid REFERENCES projects(id) ON DELETE SET NULL;
ALTER TABLE inference_tasks ADD COLUMN IF NOT EXISTS owner_id uuid REFERENCES users(id) ON DELETE SET NULL;
ALTER TABLE inference_tasks ADD COLUMN IF NOT EXISTS approval_status approval_status NOT NULL DEFAULT 'not_required';
ALTER TABLE inference_tasks ADD COLUMN IF NOT EXISTS compute_job_id uuid REFERENCES compute_jobs(id) ON DELETE SET NULL;
CREATE INDEX IF NOT EXISTS idx_inference_tasks_scope_status
ON inference_tasks(tenant_id, project_id, status, created_at DESC) WHERE deleted_at IS NULL;
ALTER TABLE eval_tasks ADD COLUMN IF NOT EXISTS tenant_id uuid REFERENCES tenants(id) ON DELETE SET NULL;
ALTER TABLE eval_tasks ADD COLUMN IF NOT EXISTS project_id uuid REFERENCES projects(id) ON DELETE SET NULL;
ALTER TABLE eval_tasks ADD COLUMN IF NOT EXISTS owner_id uuid REFERENCES users(id) ON DELETE SET NULL;
ALTER TABLE eval_tasks ADD COLUMN IF NOT EXISTS approval_status approval_status NOT NULL DEFAULT 'not_required';
ALTER TABLE eval_tasks ADD COLUMN IF NOT EXISTS compute_job_id uuid REFERENCES compute_jobs(id) ON DELETE SET NULL;
CREATE INDEX IF NOT EXISTS idx_eval_tasks_scope_status
ON eval_tasks(tenant_id, project_id, status, created_at DESC) WHERE deleted_at IS NULL;
ALTER TABLE data_convert_jobs ADD COLUMN IF NOT EXISTS tenant_id uuid REFERENCES tenants(id) ON DELETE SET NULL;
ALTER TABLE data_convert_jobs ADD COLUMN IF NOT EXISTS project_id uuid REFERENCES projects(id) ON DELETE SET NULL;
ALTER TABLE data_convert_jobs ADD COLUMN IF NOT EXISTS owner_id uuid REFERENCES users(id) ON DELETE SET NULL;
ALTER TABLE data_convert_jobs ADD COLUMN IF NOT EXISTS compute_job_id uuid REFERENCES compute_jobs(id) ON DELETE SET NULL;
CREATE INDEX IF NOT EXISTS idx_data_convert_jobs_scope_status
ON data_convert_jobs(tenant_id, project_id, status, created_at DESC);
ALTER TABLE custom_tools ADD COLUMN IF NOT EXISTS tenant_id uuid REFERENCES tenants(id) ON DELETE SET NULL;
ALTER TABLE custom_tools ADD COLUMN IF NOT EXISTS project_id uuid REFERENCES projects(id) ON DELETE SET NULL;
CREATE INDEX IF NOT EXISTS idx_custom_tools_scope
ON custom_tools(tenant_id, project_id, visibility, created_at DESC) WHERE deleted_at IS NULL;
ALTER TABLE audit_logs ADD COLUMN IF NOT EXISTS tenant_id uuid REFERENCES tenants(id) ON DELETE SET NULL;
ALTER TABLE audit_logs ADD COLUMN IF NOT EXISTS project_id uuid REFERENCES projects(id) ON DELETE SET NULL;
ALTER TABLE audit_logs ADD COLUMN IF NOT EXISTS approval_id uuid REFERENCES approval_instances(id) ON DELETE SET NULL;
CREATE INDEX IF NOT EXISTS idx_audit_logs_scope_created
ON audit_logs(tenant_id, project_id, created_at DESC);
ALTER TABLE web_logs ADD COLUMN IF NOT EXISTS tenant_id uuid REFERENCES tenants(id) ON DELETE SET NULL;
ALTER TABLE web_logs ADD COLUMN IF NOT EXISTS project_id uuid REFERENCES projects(id) ON DELETE SET NULL;
CREATE INDEX IF NOT EXISTS idx_web_logs_scope_created
ON web_logs(tenant_id, project_id, created_at DESC);
CREATE OR REPLACE VIEW v_project_member_permissions AS
SELECT
p.tenant_id,
pm.project_id,
pm.user_id,
pm.role,
CASE pm.role
WHEN 'owner' THEN ARRAY['read','write','execute','download','delete','manage_acl']
WHEN 'maintainer' THEN ARRAY['read','write','execute','download','delete']
WHEN 'developer' THEN ARRAY['read','write','execute','download']
WHEN 'reviewer' THEN ARRAY['read','download']
ELSE ARRAY['read']
END AS permissions
FROM project_members pm
JOIN projects p ON p.id = pm.project_id
WHERE p.deleted_at IS NULL;