feat(education): complete Flyway migration and atomic submit
This commit is contained in:
105
sql/postgresql/education/002-education-practice-session.sql
Normal file
105
sql/postgresql/education/002-education-practice-session.sql
Normal file
@@ -0,0 +1,105 @@
|
||||
-- =============================================
|
||||
-- Education 模块 — 练习会话与题目快照 DDL
|
||||
-- Ticket #6: 练习会话创建、题目快照、恢复与状态机
|
||||
-- Migration: 002
|
||||
-- Prerequisites: 000-education-schema.sql (database creation)
|
||||
-- 001-education-tenant-seed.sql (tenant seed data)
|
||||
-- =============================================
|
||||
|
||||
-- =============================================
|
||||
-- Preconditions
|
||||
-- =============================================
|
||||
-- This migration MUST fail if either table already exists (no IF NOT EXISTS).
|
||||
-- Operator is expected to verify:
|
||||
-- SELECT COUNT(*) FROM information_schema.tables
|
||||
-- WHERE table_schema = current_database()
|
||||
-- AND table_name IN ('education_practice_session', 'education_practice_question');
|
||||
-- Result MUST be 0 before executing this migration.
|
||||
|
||||
-- =============================================
|
||||
-- 练习会话表
|
||||
-- =============================================
|
||||
-- Indexes:
|
||||
-- uk_tenant_client_session — per-tenant uniqueness for clientSessionId idempotency.
|
||||
-- Used by: selectByTenantAndClientSessionId (idempotent create check),
|
||||
-- INSERT ... ON CONFLICT ... for concurrent-create race resolution.
|
||||
-- idx_tenant_user_status — covers getCurrentSession (latest ACTIVE by tenant+user)
|
||||
-- and ownership queries. Column order: (tenant_id, user_id, status) so the
|
||||
-- index supports both filtering by tenant+user and tenant+user+status.
|
||||
-- Lock impact: INSERT acquires a row-level lock on the unique index entry;
|
||||
-- concurrent inserts with same (tenant_id, client_session_id) serialize naturally.
|
||||
-- No additional table-level locks required.
|
||||
CREATE TABLE education_practice_session (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY /* 会话主键 */,
|
||||
tenant_id BIGINT NOT NULL /* 租户编号 */,
|
||||
user_id BIGINT NOT NULL /* Member 用户编号 */,
|
||||
client_session_id VARCHAR(36) NOT NULL /* 客户端生成的会话标识(UUID),用于幂等创建 */,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE'
|
||||
/* 会话状态:ACTIVE-进行中, SUBMITTED-已提交, EXPIRED-已过期, CANCELLED-已取消 */,
|
||||
question_count INT NOT NULL DEFAULT 0 /* 题目总数 */,
|
||||
collection_id VARCHAR(64) DEFAULT NULL /* 源题集 ID */,
|
||||
node_id VARCHAR(64) DEFAULT NULL /* 源目录节点 ID */,
|
||||
type VARCHAR(32) DEFAULT NULL /* 筛选题型 */,
|
||||
difficulty VARCHAR(32) DEFAULT NULL /* 筛选难度 */,
|
||||
version INT NOT NULL DEFAULT 0 /* 乐观锁版本号 */,
|
||||
creator VARCHAR(64) DEFAULT '' /* 创建者 */,
|
||||
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP /* 创建时间 */,
|
||||
updater VARCHAR(64) DEFAULT '' /* 更新者 */,
|
||||
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP /* 更新时间 */,
|
||||
deleted BOOLEAN NOT NULL DEFAULT false /* 是否删除 */
|
||||
);
|
||||
COMMENT ON TABLE education_practice_session IS '教育-练习会话';
|
||||
|
||||
CREATE UNIQUE INDEX uk_tenant_client_session ON education_practice_session (tenant_id, client_session_id);
|
||||
CREATE INDEX idx_tenant_user_status ON education_practice_session (tenant_id, user_id, status);
|
||||
|
||||
-- =============================================
|
||||
-- 练习会话题目快照表
|
||||
-- =============================================
|
||||
-- Indexes:
|
||||
-- uk_session_sequence — per-session uniqueness for question sequence numbers.
|
||||
-- Used by: insertBatch to ensure no duplicate sequences within a session.
|
||||
-- idx_session_id — covers selectBySessionIdOrderBySequence (load all questions
|
||||
-- for a session, ordered by sequence). Also used by cascade delete lookups.
|
||||
-- Lock impact: INSERT acquires gap locks within session_id range on uk_session_sequence;
|
||||
-- concurrent inserts into different sessions are independent.
|
||||
-- Options column: JSONB data type stores only label, content, order — never isCorrect.
|
||||
-- Application layer (optionsToSafeJson) strips correctness before storage.
|
||||
CREATE TABLE education_practice_question (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY /* 主键 */,
|
||||
tenant_id BIGINT NOT NULL /* 租户编号 */,
|
||||
session_id BIGINT NOT NULL /* 会话 ID */,
|
||||
sequence INT NOT NULL /* 题目序号(1-based,服务端固定) */,
|
||||
question_id VARCHAR(64) NOT NULL /* 原始题目 ID */,
|
||||
content_version VARCHAR(64) NOT NULL DEFAULT '' /* 快照时的题目内容版本 */,
|
||||
stem TEXT NOT NULL /* 题干快照 */,
|
||||
type VARCHAR(32) NOT NULL /* 题型快照 */,
|
||||
difficulty VARCHAR(32) DEFAULT NULL /* 难度快照 */,
|
||||
options JSONB NOT NULL /* 选项快照 JSON(不含 isCorrect) */,
|
||||
correct_answer TEXT DEFAULT NULL /* 正确答案快照 */,
|
||||
explanation TEXT DEFAULT NULL /* 解析快照 */,
|
||||
selected_answer TEXT DEFAULT NULL /* 学生已选答案 */,
|
||||
is_answered BOOLEAN NOT NULL DEFAULT false /* 是否已作答 */,
|
||||
creator VARCHAR(64) DEFAULT '' /* 创建者 */,
|
||||
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP /* 创建时间 */,
|
||||
updater VARCHAR(64) DEFAULT '' /* 更新者 */,
|
||||
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP /* 更新时间 */,
|
||||
deleted BOOLEAN NOT NULL DEFAULT false /* 是否删除 */
|
||||
);
|
||||
COMMENT ON TABLE education_practice_question IS '教育-练习会话题目快照';
|
||||
|
||||
CREATE UNIQUE INDEX uk_session_sequence ON education_practice_question (session_id, sequence);
|
||||
CREATE INDEX idx_session_id ON education_practice_question (session_id);
|
||||
|
||||
-- =============================================
|
||||
-- Post-migration verification queries
|
||||
-- =============================================
|
||||
-- Verify tables exist with correct structure:
|
||||
-- \d education_practice_session
|
||||
-- \d education_practice_question
|
||||
-- Verify unique keys are enforced:
|
||||
-- SELECT * FROM pg_indexes WHERE tablename = 'education_practice_session' AND indexname = 'uk_tenant_client_session';
|
||||
-- SELECT * FROM pg_indexes WHERE tablename = 'education_practice_question' AND indexname = 'uk_session_sequence';
|
||||
-- Verify no orphan data (should be 0 after fresh migration):
|
||||
-- SELECT COUNT(*) FROM education_practice_session;
|
||||
-- SELECT COUNT(*) FROM education_practice_question;
|
||||
Reference in New Issue
Block a user