Data Dictionary · Khác (phần 10/11)
⚙️ Trang này sinh tự động từ source code bằng
scripts/gen-reference.mjs— đừng sửa tay, sửa code rồi chạynpm run gen:reference.
Thuộc Data Dictionary. 33 bảng, referral_rewards tới speak_rooms.
referral_rewards
Sổ quyền lợi. Mỗi lần ghi công sinh HAI dòng: một cho người giới thiệu, một cho người được giới thiệu — để hai bên đọc cùng một sự thật thay vì mỗi bên tính một kiểu. amount_minor để NULL khi chưa biết tiền: phase 1 chưa có bảng giá (Q-014), nên dòng nằm ở trạng thái 'pending' cho tới khi có người thật sự trả tiền. KHÔNG bịa số 0 vào đây — 0 nghĩa là "đã tính ra và bằng không", khác hẳn "chưa tính".
migration: 0064_referrals.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
id | TEXT | PRIMARY KEY |
referred_user_id | TEXT | NOT NULL REFERENCES users(id) |
referrer_user_id | TEXT | NOT NULL REFERENCES users(id) |
kind | TEXT | NOT NULL CHECK (kind IN ('referrer_credit','referee_discount')) |
percent | INTEGER | NOT NULL, -- chụp lại tỷ lệ TẠI THỜI ĐIỂM ghi công: đổi chính sách |
status | TEXT | NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','earned','settled','void')) |
amount_minor | INTEGER | , -- đơn vị nhỏ nhất (VND: đồng). NULL = chưa có tiền để tính |
currency | TEXT | NOT NULL DEFAULT 'VND' |
note | TEXT | — |
created_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
settled_at | TEXT | — |
Khóa ngoại: referred_user_id → users · referrer_user_id → users
Index: idx_referral_rewards_referrer(referrer_user_id, status) · idx_referral_rewards_referred(referred_user_id, status)
sat_answers
migration: 0288_sat_practice_core.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
session_id | TEXT | NOT NULL REFERENCES sat_sessions(id) ON DELETE CASCADE |
item_id | TEXT | NOT NULL REFERENCES sat_items(id) |
answer | TEXT | — |
marked | INTEGER | NOT NULL DEFAULT 0 CHECK (marked IN (0,1)) |
correct | INTEGER | CHECK (correct IN (0,1)) |
answered_at | TEXT | — |
cause | TEXT | CHECK (cause IN ('knowledge','misread','pacing','careless')) |
| — | table constraint | PRIMARY KEY (session_id, item_id) |
revealed_at | TEXT | — thêm ở 0291_sat_practice_wave2.sql |
Khóa ngoại: session_id → sat_sessions · item_id → sat_items
Index: idx_sat_answers_time(answered_at)
sat_form_modules
migration: 0288_sat_practice_core.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
form_id | TEXT | NOT NULL REFERENCES sat_forms(id) ON DELETE CASCADE |
section | TEXT | NOT NULL CHECK (section IN ('rw','math')) |
stage | TEXT | NOT NULL CHECK (stage IN ('m1','m2_easy','m2_hard')) |
position | INTEGER | NOT NULL CHECK (position > 0) |
item_id | TEXT | NOT NULL REFERENCES sat_items(id) |
| — | table constraint | PRIMARY KEY (form_id, section, stage, position) |
Khóa ngoại: form_id → sat_forms · item_id → sat_items
sat_forms
Một đề đầy đủ: 6 module (mỗi phần M1, M2 dễ, M2 khó). routing_json giữ ngưỡng M1 → M2 khó.
migration: 0288_sat_practice_core.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
id | TEXT | PRIMARY KEY |
title | TEXT | NOT NULL |
status | TEXT | NOT NULL DEFAULT 'draft' CHECK (status IN ('draft','published','retired')) |
routing_json | TEXT | NOT NULL DEFAULT '{"rw":15,"math":13}' |
created_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
sat_goals
REQ-SAT-02. Một dòng mỗi learner.
migration: 0288_sat_practice_core.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
learner_id | TEXT | PRIMARY KEY REFERENCES learners(id) ON DELETE CASCADE |
school_group | TEXT | CHECK (school_group IN ('vn','us_merit','us_top','custom')) |
target_total | INTEGER | NOT NULL CHECK (target_total BETWEEN 400 AND 1600 AND target_total % 10 = 0) |
target_rw | INTEGER | NOT NULL CHECK (target_rw BETWEEN 200 AND 800 AND target_rw % 10 = 0) |
target_math | INTEGER | NOT NULL CHECK (target_math BETWEEN 200 AND 800 AND target_math % 10 = 0) |
test_date | TEXT | — |
deadline | TEXT | — |
updated_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
Khóa ngoại: learner_id → learners
sat_items
migration: 0288_sat_practice_core.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
id | TEXT | PRIMARY KEY |
section | TEXT | NOT NULL CHECK (section IN ('rw','math')) |
domain | TEXT | NOT NULL CHECK (domain IN ( |
skill | TEXT | NOT NULL DEFAULT '' |
node_id | TEXT | — |
difficulty | INTEGER | NOT NULL CHECK (difficulty BETWEEN 1 AND 3) |
passage_id | TEXT | REFERENCES sat_passages(id) |
stem_md | TEXT | NOT NULL |
format | TEXT | NOT NULL CHECK (format IN ('mcq','spr')) |
choices_json | TEXT | — |
answer_json | TEXT | NOT NULL |
rationale_md | TEXT | NOT NULL DEFAULT '' |
source | TEXT | NOT NULL |
license | TEXT | NOT NULL |
status | TEXT | NOT NULL DEFAULT 'draft' CHECK (status IN ('draft','review','approved','retired')) |
author_id | TEXT | — |
reviewer_id | TEXT | — |
attempts_n | INTEGER | NOT NULL DEFAULT 0 |
correct_n | INTEGER | NOT NULL DEFAULT 0 |
created_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
updated_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
| — | table constraint | CHECK (lower(source) NOT LIKE '%collegeboard%' AND lower(source) NOT LIKE '%college board%' AND lower(source) NOT LIKE '%bluebook%') |
| — | table constraint | CHECK (reviewer_id IS NULL OR author_id IS NULL OR reviewer_id <> author_id) |
| — | table constraint | CHECK (status <> 'approved' OR reviewer_id IS NOT NULL) |
| — | table constraint | CHECK ((format = 'mcq' AND choices_json IS NOT NULL) OR (format = 'spr' AND choices_json IS NULL)) |
| — | table constraint | CHECK ((section = 'rw' AND domain IN ('craft_structure','information_ideas','conventions','expression_ideas')) |
Khóa ngoại: passage_id → sat_passages
Index: idx_sat_items_pick(section, domain, status, difficulty)
sat_mistakes
Đợt 2 (REQ-SAT-12). Tạo sẵn để đợt 2 không cần một migration chen giữa.
migration: 0288_sat_practice_core.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
learner_id | TEXT | NOT NULL REFERENCES learners(id) ON DELETE CASCADE |
item_id | TEXT | NOT NULL REFERENCES sat_items(id) |
cause | TEXT | — |
due_at | TEXT | — |
streak | INTEGER | NOT NULL DEFAULT 0 |
cleared_at | TEXT | — |
| — | table constraint | PRIMARY KEY (learner_id, item_id) |
last_correct_at | TEXT | — thêm ở 0291_sat_practice_wave2.sql |
created_at | TEXT | — thêm ở 0291_sat_practice_wave2.sql |
Khóa ngoại: learner_id → learners · item_id → sat_items
Index: idx_sat_mistakes_due(learner_id, cleared_at, due_at)
sat_passages
Đoạn văn dùng chung cho một hoặc vài câu (R&W). Math phần lớn không có đoạn.
migration: 0288_sat_practice_core.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
id | TEXT | PRIMARY KEY |
section | TEXT | NOT NULL CHECK (section IN ('rw','math')) |
body_md | TEXT | NOT NULL |
source | TEXT | NOT NULL |
license | TEXT | NOT NULL |
created_by | TEXT | — |
created_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
| — | table constraint | CHECK (lower(source) NOT LIKE '%collegeboard%' AND lower(source) NOT LIKE '%college board%' AND lower(source) NOT LIKE '%bluebook%') |
sat_plans
Đợt 3 (REQ-SAT-15).
migration: 0288_sat_practice_core.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
learner_id | TEXT | NOT NULL REFERENCES learners(id) ON DELETE CASCADE |
version | INTEGER | NOT NULL |
plan_json | TEXT | NOT NULL |
created_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
| — | table constraint | PRIMARY KEY (learner_id, version) |
Khóa ngoại: learner_id → learners
sat_session_items
Migration 0291 — NEMO SAT đợt 2: phiên luyện và sổ lỗi. SRC-1024, SDD-044 §5. Phiên luyện (luyện theo miền, module có giờ, ôn sổ lỗi) không lấy câu từ một đề đã ghép sẵn như bài đo, nên cần danh sách câu riêng của từng phiên. Sổ lỗi cần thêm mốc lần làm đúng gần nhất để thi hành luật "đúng hai lần cách nhau ít nhất 3 ngày thì ra khỏi sổ" (REQ-SAT-12). Idempotent: bảng mới dùng IF NOT EXISTS; cột mới thêm bằng ALTER, và ALTER chạy lại sẽ lỗi "duplicate column" nên migration này, như mọi migration, chỉ chạy đúng một lần qua d1_migrations.
migration: 0291_sat_practice_wave2.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
session_id | TEXT | NOT NULL REFERENCES sat_sessions(id) ON DELETE CASCADE |
position | INTEGER | NOT NULL CHECK (position > 0) |
item_id | TEXT | NOT NULL REFERENCES sat_items(id) |
| — | table constraint | PRIMARY KEY (session_id, position) |
Khóa ngoại: session_id → sat_sessions · item_id → sat_items
Index: idx_sat_session_items_item(item_id)
sat_sessions
Một phiên: bài đo (cha) và từng module của nó (con, parent_id), hoặc một phiên luyện lẻ.
migration: 0288_sat_practice_core.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
id | TEXT | PRIMARY KEY |
learner_id | TEXT | NOT NULL REFERENCES learners(id) ON DELETE CASCADE |
parent_id | TEXT | REFERENCES sat_sessions(id) ON DELETE CASCADE |
kind | TEXT | NOT NULL CHECK (kind IN ('diagnostic','retest','domain','module','review')) |
form_id | TEXT | REFERENCES sat_forms(id) |
section | TEXT | CHECK (section IN ('rw','math')) |
stage | TEXT | CHECK (stage IN ('m1','m2_easy','m2_hard')) |
started_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
deadline_at | TEXT | — |
submitted_at | TEXT | — |
result_json | TEXT | — |
Khóa ngoại: learner_id → learners · parent_id → sat_sessions · form_id → sat_forms
Index: idx_sat_sessions_learner(learner_id, kind, started_at) · idx_sat_sessions_parent(parent_id)
school_achievements
Thành tích gắn NĂM HỌC dạng "2024-2025" (chuỗi, không phải số): năm học Việt Nam vắt qua hai năm dương lịch; ép về số nguyên là buộc mọi chỗ đọc tự nhớ quy ước, và sớm muộn có chỗ nhớ khác.
migration: 0047_school_registry.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
id | TEXT | PRIMARY KEY |
school_id | TEXT | NOT NULL REFERENCES target_schools(id) |
school_year | TEXT | NOT NULL |
category | TEXT | NOT NULL CHECK (category IN ('hsg_quoc_gia','hsg_tinh','olympic_quoc_te','dai_hoc','khac')) |
title | TEXT | NOT NULL |
detail | TEXT | — |
quantity | INTEGER | — |
source_url | TEXT | — |
source_note | TEXT | — |
status | TEXT | NOT NULL DEFAULT 'draft' CHECK (status IN ('draft','published','archived')) |
display_order | INTEGER | NOT NULL DEFAULT 100 |
created_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
Khóa ngoại: school_id → target_schools
Index: idx_school_achievements_school(school_id, school_year DESC)
school_code_redemptions
migration: 0308_school_codes_and_outreach.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
id | TEXT | PRIMARY KEY |
code | TEXT | NOT NULL REFERENCES school_codes(code) |
program | TEXT | NOT NULL CHECK (program IN ('ielts', 'sat')) |
learner_id | TEXT | NOT NULL REFERENCES learners(id) ON DELETE CASCADE |
user_id | TEXT | , -- người bấm nhập mã (learner hoặc bố mẹ) |
access_ends_on | TEXT | NOT NULL |
redeemed_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
Khóa ngoại: code → school_codes · learner_id → learners
Index: uq_school_redemption_program_learner(program, learner_id) UNIQUE · idx_school_redemptions_code(code, program)
school_codes
── Mã trường ─────────────────────────────────────────────────────────────────────────────────
migration: 0308_school_codes_and_outreach.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
code | TEXT | PRIMARY KEY, -- chữ HOA, vd NEMO-7K3QX9 |
school_name | TEXT | NOT NULL |
org_id | TEXT | , -- outreach_orgs.id nếu trường đến từ outreach |
ielts_seats | INTEGER | NOT NULL DEFAULT 100 CHECK (ielts_seats >= 0) |
sat_seats | INTEGER | NOT NULL DEFAULT 100 CHECK (sat_seats >= 0) |
valid_from | TEXT | NOT NULL DEFAULT '2026-10-01' |
valid_to | TEXT | NOT NULL DEFAULT '2026-10-31' |
access_days | INTEGER | NOT NULL DEFAULT 90 CHECK (access_days BETWEEN 1 AND 730) |
status | TEXT | NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'revoked')) |
note | TEXT | — |
created_by | TEXT | — |
created_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
revoked_at | TEXT | — |
revoked_by | TEXT | — |
Index: idx_school_codes_org(org_id)
school_students
Người được nêu tên công khai của trường (Q-136) — KHÔNG phải sổ điểm danh học sinh đang học. Hai cổng chặn publish, cả hai thi hành trong service chứ không chỉ nhắc trên UI: consent = 1 (đã đồng ý được nêu tên) verification_status = 'verified' (đã kiểm chứng, chỉ đạo chủ dự án 2026-08-17)
migration: 0047_school_registry.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
id | TEXT | PRIMARY KEY |
school_id | TEXT | NOT NULL REFERENCES target_schools(id) |
full_name | TEXT | NOT NULL |
program_id | TEXT | REFERENCES specialized_programs(id) |
program_label | TEXT | — |
birth_year | INTEGER | — |
cohort_year | INTEGER | — |
grad_year | INTEGER | — |
kind | TEXT | NOT NULL DEFAULT 'alumni' CHECK (kind IN ('alumni','current','teacher')) |
highlight | TEXT | — |
achievements | TEXT | — |
current_org | TEXT | — |
photo_media_id | TEXT | REFERENCES media(id) |
mentor_profile_id | TEXT | REFERENCES mentor_profiles(id) |
sources_json | TEXT | NOT NULL DEFAULT '[]' |
verification_status | TEXT | NOT NULL DEFAULT 'unverified' |
| — | table constraint | CHECK (verification_status IN ('unverified','verified','disputed')) |
verification_note | TEXT | , -- NỘI BỘ: đã đối chiếu gì, chỗ nào còn chưa chắc |
verified_at | TEXT | — |
verified_by | TEXT | REFERENCES users(id) |
consent | INTEGER | NOT NULL DEFAULT 0 CHECK (consent IN (0,1)) |
consent_note | TEXT | , -- NỘI BỘ |
featured | INTEGER | NOT NULL DEFAULT 0 CHECK (featured IN (0,1)) |
status | TEXT | NOT NULL DEFAULT 'draft' CHECK (status IN ('draft','published','archived')) |
display_order | INTEGER | NOT NULL DEFAULT 100 |
created_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
updated_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
Khóa ngoại: school_id → target_schools · program_id → specialized_programs · photo_media_id → media · mentor_profile_id → mentor_profiles · verified_by → users
Index: idx_school_students_school(school_id, status, display_order) · idx_school_students_mentor(mentor_profile_id)
school_year_stats
Số liệu tuyển sinh so sánh được. Không cột số nào NOT NULL: một trường có thể công bố điểm chuẩn mà không công bố số dự thi. Ô trống là câu trả lời hợp lệ; điền 0 là nói sai (SDD-020 §1.3).
migration: 0047_school_registry.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
school_id | TEXT | NOT NULL REFERENCES target_schools(id) |
school_year | TEXT | NOT NULL |
program_id | TEXT | NOT NULL DEFAULT '' , -- '' = mức toàn trường; PK không nhận NULL |
applicants | INTEGER | — |
seats | INTEGER | — |
cutoff_score | REAL | — |
ratio | REAL | — |
source_url | TEXT | — |
source_note | TEXT | — |
updated_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
| — | table constraint | PRIMARY KEY (school_id, school_year, program_id) |
Khóa ngoại: school_id → target_schools
slot_bookings
Một dòng = một learner ghi tên vào MỘT buổi của MỘT ngày. session_date là ngày lịch Việt Nam và phải rơi đúng vào weekday của khung — điều đó CSDL không kiểm được (không có biểu thức nào đọc sang bảng khác trong CHECK), nên service.ts kiểm, và có test cho đúng chỗ đó. meeting_url chép lại vào đây lúc đăng ký (chứ không đọc sang mentor_slots lúc hiển thị): đổi link của khung vào tuần sau thì buổi tuần này vẫn giữ link mà learner đã nhận. Đây là chép có chủ ý của một giá trị tại thời điểm, không phải hai nguồn sự thật.
migration: 0223_mentor_slots.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
id | TEXT | PRIMARY KEY |
slot_id | TEXT | NOT NULL REFERENCES mentor_slots(id) |
learner_id | TEXT | NOT NULL REFERENCES learners(id) ON DELETE CASCADE |
session_date | TEXT | NOT NULL, -- YYYY-MM-DD giờ Việt Nam |
status | TEXT | NOT NULL DEFAULT 'booked' CHECK (status IN ('booked','cancelled','attended','absent')) |
meeting_url | TEXT | — |
booked_by_user_id | TEXT | REFERENCES users(id) |
cancelled_at | TEXT | — |
created_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
Khóa ngoại: slot_id → mentor_slots · learner_id → learners · booked_by_user_id → users
Index: idx_slot_booking_once(slot_id, session_date, learner_id) UNIQUE · idx_slot_booking_date(session_date, slot_id) · idx_slot_booking_learner(learner_id, session_date DESC)
slot_preferences
rank 1 = thích nhất, 2 = thích nhì. Khoá chính (learner_id, rank) ép "mỗi hạng đúng một khung"; UNIQUE (learner_id, slot_id) ép "không chọn cùng một khung cho cả hai hạng" — một learner khai cùng một khung ở cả hai chỗ là không nói thêm được gì, mà lại làm hỏng phép xếp lớp.
migration: 0223_mentor_slots.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
learner_id | TEXT | NOT NULL REFERENCES learners(id) ON DELETE CASCADE |
rank | INTEGER | NOT NULL CHECK (rank IN (1,2)) |
slot_id | TEXT | NOT NULL REFERENCES mentor_slots(id) |
updated_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
| — | table constraint | PRIMARY KEY (learner_id, rank) |
Khóa ngoại: learner_id → learners · slot_id → mentor_slots
Index: idx_slot_pref_unique(learner_id, slot_id) UNIQUE
speak_course_enrollments
migration: 0312_speak_course_orders_and_enrollments.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
course_id | TEXT | NOT NULL CHECK (course_id IN ('speak-2', 'speak-3')) |
learner_id | TEXT | NOT NULL REFERENCES learners(id) |
activation_code | TEXT | NOT NULL UNIQUE |
first_session_index | INTEGER | NOT NULL CHECK (first_session_index >= 0) |
activated_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
| — | table constraint | PRIMARY KEY (course_id, learner_id) |
Khóa ngoại: learner_id → learners
speak_course_orders
Migration 0312 - speak course orders and enrollments. SRC-1130, SRC-1131, SDD-038. ## Vì sao migration này tồn tại Chủ dự án 29.09.2026: NEMO SPEAK 2 cần trang đăng ký + thanh toán riêng, và đăng ký xong thì hệ thống gửi MÃ KÍCH HOẠT để vào lớp. Tới bản này repo không có chỗ nào giữ "ai đã đặt khoá nào, chuyển khoản với nội dung gì, đã trả chưa", cũng không có sổ ghi danh một khoá có ngày khai giảng. Không có sổ đơn thì admin đối chiếu sao kê bằng trí nhớ; không có sổ ghi danh thì mã kích hoạt không mở ra được thứ gì. Hai bảng tách nhau vì NGƯỜI TRẢ TIỀN và NGƯỜI HỌC có thể khác nhau: bố mẹ đặt đơn trên hồ sơ của mình rồi đưa mã cho con nhập. Đơn thuộc learner đặt; ghi danh thuộc learner nhập mã. SRC-1131 (cùng ngày, trước khi gộp): khoá chạy CUỐN CHIẾU, learner vào lúc nào cũng được và học 5 buổi kế tiếp. first_session_index giữ chỉ số buổi đầu của người ấy trong dòng buổi liên tục của khoá (xem workers/api/src/modules/speakCourses/cohort.ts), nên 5 ngày của họ cố định kể cả khi lịch chung chạy tiếp. Thêm khoá speak-3. ## Chạy lại không đổi gì CREATE TABLE IF NOT EXISTS, CREATE INDEX IF NOT EXISTS.
migration: 0312_speak_course_orders_and_enrollments.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
id | TEXT | PRIMARY KEY |
learner_id | TEXT | NOT NULL REFERENCES learners(id) |
course_id | TEXT | NOT NULL CHECK (course_id IN ('speak-2', 'speak-3')) |
amount_vnd | INTEGER | NOT NULL CHECK (amount_vnd >= 0) |
transfer_ref | TEXT | NOT NULL UNIQUE |
status | TEXT | NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'paid', 'cancelled')) |
activation_code | TEXT | UNIQUE |
created_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
paid_at | TEXT | — |
paid_by | TEXT | — |
Khóa ngoại: learner_id → learners
Index: idx_speak_course_orders_learner(learner_id, course_id) · idx_speak_course_orders_status(status, created_at)
speak_pair_observers
Người vào sau hai người đầu: xem cùng bài, không được cộng giờ.
migration: 0302_speak_pair_sessions.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
session_id | TEXT | NOT NULL REFERENCES speak_pair_sessions(id) ON DELETE CASCADE |
learner_id | TEXT | NOT NULL REFERENCES learners(id) ON DELETE CASCADE |
joined_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
| — | table constraint | PRIMARY KEY (session_id, learner_id) |
Khóa ngoại: session_id → speak_pair_sessions · learner_id → learners
speak_pair_sessions
Migration 0302 - Buổi luyện nói theo cặp ("Speak together"). SRC-1078. Chủ dự án 27.09.2026: vài learner học cùng nhau qua Zoom, mở cùng một bài nói, đóng vai A (hỏi) và B (trả lời) qua bốn lượt rồi chia sẻ; hệ thống phải ghi nhận "A và B đã luyện xong bài này" và cộng thời gian học cho CẢ HAI. ## Vì sao cần một bảng Hai learner ở hai máy khác nhau. Muốn biết "cả hai cùng xong một bài" thì phải có một chỗ chung cho hai máy cùng trỏ vào: một buổi, có mã 4 số (một người tạo, đọc mã qua Zoom, người kia nhập), ghi ai là chủ, ai tham gia, đang ở bước nào, và mỗi người bấm Xong lúc nào. Phút học vẫn ghi vào learning_events như mọi hoạt động khác (skill 'speaking', ref 'pair:<bài>'), nên tổng giờ, chuỗi ngày, bảng xếp hạng và thưởng tự cộng; bảng này chỉ giữ trạng thái của buổi. ## Bản 2 (chủ dự án 27.09.2026, cùng ngày, TRƯỚC khi migration này từng được áp) Sửa thẳng file này thay cho thêm migration mới vì nó chưa chạm môi trường nào: Actions bị chặn từ trước khi nó lên main. Bốn luật mới: · chỉ ĐÚNG HAI người được cộng giờ; người vào sau là quan sát viên (speak_pair_observers); · vào nhầm phòng thì ra được (ended_at khi chủ buổi rời, hoặc trả chỗ khi người kia rời); · giờ chỉ bắt đầu khi CẢ HAI đã xem bài và bấm Bắt đầu (host_ready_at, guest_ready_at, started_at); bước 0 là bước xem bài; · ngưỡng 8 phút đo từ started_at, không từ lúc tạo mã. ## Chạy lại không đổi gì CREATE TABLE / INDEX IF NOT EXISTS.
migration: 0302_speak_pair_sessions.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
id | TEXT | PRIMARY KEY |
code | TEXT | NOT NULL |
topic | TEXT | NOT NULL |
host_learner_id | TEXT | NOT NULL REFERENCES learners(id) ON DELETE CASCADE |
guest_learner_id | TEXT | REFERENCES learners(id) ON DELETE CASCADE |
step | INTEGER | NOT NULL DEFAULT 0 CHECK (step BETWEEN 0 AND 5) |
host_ready_at | TEXT | — |
guest_ready_at | TEXT | — |
started_at | TEXT | — |
ended_at | TEXT | — |
host_done_at | TEXT | — |
guest_done_at | TEXT | — |
credited_at | TEXT | — |
created_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
Khóa ngoại: host_learner_id → learners · guest_learner_id → learners
Index: idx_speak_pair_open_code(code) · idx_speak_pair_host(host_learner_id, created_at DESC) · idx_speak_pair_guest(guest_learner_id, created_at DESC)
speak_recording_comments
migration: 0318_speak_rooms_and_recordings.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
id | TEXT | PRIMARY KEY |
recording_id | TEXT | NOT NULL REFERENCES speak_recordings(id) ON DELETE CASCADE |
author_user_id | TEXT | NOT NULL |
at_ms | INTEGER | — |
body | TEXT | NOT NULL |
created_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
Khóa ngoại: recording_id → speak_recordings
Index: idx_speak_recording_comments_rec(recording_id, created_at)
speak_recording_deletions
Audit of every deletion (by a participant, by expiry, by admin). Kept after the file is gone.
migration: 0318_speak_rooms_and_recordings.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
id | TEXT | PRIMARY KEY |
recording_id | TEXT | NOT NULL |
reason | TEXT | NOT NULL CHECK (reason IN ('participant','expired','admin')) |
learner_id | TEXT | — |
actor_user_id | TEXT | — |
deleted_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
Index: idx_speak_recording_deletions_rec(recording_id)
speak_recording_feedback
migration: 0318_speak_rooms_and_recordings.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
id | TEXT | PRIMARY KEY |
recording_id | TEXT | NOT NULL REFERENCES speak_recordings(id) ON DELETE CASCADE |
learner_id | TEXT | NOT NULL REFERENCES learners(id) ON DELETE CASCADE |
criterion | TEXT | NOT NULL CHECK (criterion IN ('fc','lr','gra','p')) |
band | REAL | — |
comment | TEXT | NOT NULL |
source | TEXT | NOT NULL DEFAULT 'ai' CHECK (source IN ('ai','mentor')) |
model | TEXT | — |
created_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
Khóa ngoại: recording_id → speak_recordings · learner_id → learners
Index: idx_speak_recording_feedback_rec(recording_id, learner_id)
speak_recording_participants
Whose voice is on the recording: everyone present at any moment while it was recording.
migration: 0318_speak_rooms_and_recordings.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
recording_id | TEXT | NOT NULL REFERENCES speak_recordings(id) ON DELETE CASCADE |
learner_id | TEXT | NOT NULL REFERENCES learners(id) ON DELETE CASCADE |
| — | table constraint | PRIMARY KEY (recording_id, learner_id) |
Khóa ngoại: recording_id → speak_recordings · learner_id → learners
Index: idx_speak_recording_participants_learner(learner_id)
speak_recording_parts
migration: 0319_speak_room_host_and_segments.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
id | TEXT | PRIMARY KEY |
recording_id | TEXT | NOT NULL REFERENCES speak_recordings(id) ON DELETE CASCADE |
seq | INTEGER | NOT NULL |
status | TEXT | NOT NULL DEFAULT 'recording' CHECK (status IN ('recording','processing','ready','failed')) |
provider_recording_id | TEXT | — |
content_type | TEXT | — |
bytes | INTEGER | — |
started_at | TEXT | NOT NULL |
stopped_at | TEXT | — |
duration_seconds | INTEGER | — |
| — | table constraint | UNIQUE (recording_id, seq) |
Khóa ngoại: recording_id → speak_recordings
Index: idx_speak_recording_parts_provider(provider_recording_id)
speak_recording_segments
migration: 0318_speak_rooms_and_recordings.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
id | TEXT | PRIMARY KEY |
recording_id | TEXT | NOT NULL REFERENCES speak_recordings(id) ON DELETE CASCADE |
learner_id | TEXT | NOT NULL REFERENCES learners(id) ON DELETE CASCADE |
start_ms | INTEGER | NOT NULL |
end_ms | INTEGER | NOT NULL |
text | TEXT | NOT NULL |
model | TEXT | — |
Khóa ngoại: recording_id → speak_recordings · learner_id → learners
Index: idx_speak_recording_segments_rec(recording_id, start_ms)
speak_recording_tracks
One file per speaker (RealtimeKit track recording), the input for speaker-split transcripts.
migration: 0318_speak_rooms_and_recordings.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
id | TEXT | PRIMARY KEY |
recording_id | TEXT | NOT NULL REFERENCES speak_recordings(id) ON DELETE CASCADE |
learner_id | TEXT | NOT NULL REFERENCES learners(id) ON DELETE CASCADE |
offset_ms | INTEGER | NOT NULL DEFAULT 0 |
duration_seconds | INTEGER | — |
created_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
Khóa ngoại: recording_id → speak_recordings · learner_id → learners
Index: idx_speak_recording_tracks_rec(recording_id)
speak_recordings
migration: 0318_speak_rooms_and_recordings.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
id | TEXT | PRIMARY KEY |
room_id | TEXT | NOT NULL REFERENCES speak_rooms(id) ON DELETE CASCADE |
status | TEXT | NOT NULL DEFAULT 'scheduled' |
| — | table constraint | CHECK (status IN ('scheduled','recording','processing','ready','skipped','failed','deleted')) |
skip_reason | TEXT | — |
planned_start_at | TEXT | NOT NULL |
planned_end_at | TEXT | NOT NULL |
provider_recording_id | TEXT | — |
content_type | TEXT | — |
bytes | INTEGER | — |
started_at | TEXT | — |
stopped_at | TEXT | — |
duration_seconds | INTEGER | — |
expires_at | TEXT | — |
deleted_at | TEXT | — |
created_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
Khóa ngoại: room_id → speak_rooms
Index: idx_speak_recordings_room(room_id) UNIQUE · idx_speak_recordings_status(status, planned_end_at) · idx_speak_recordings_expiry(expires_at) · idx_speak_recordings_provider(provider_recording_id)
speak_room_events
migration: 0319_speak_room_host_and_segments.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
id | TEXT | PRIMARY KEY |
room_id | TEXT | NOT NULL REFERENCES speak_rooms(id) ON DELETE CASCADE |
kind | TEXT | NOT NULL CHECK (kind IN ('host_transfer','host_auto_pass','record','pause','resume','stop','auto_stop','room_end')) |
actor_learner_id | TEXT | — |
target_learner_id | TEXT | — |
detail | TEXT | — |
created_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
Khóa ngoại: room_id → speak_rooms
Index: idx_speak_room_events_room(room_id, created_at)
speak_room_participants
migration: 0318_speak_rooms_and_recordings.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
room_id | TEXT | NOT NULL REFERENCES speak_rooms(id) ON DELETE CASCADE |
learner_id | TEXT | NOT NULL REFERENCES learners(id) ON DELETE CASCADE |
joined_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
consent_at | TEXT | — |
left_at | TEXT | — |
seen_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
| — | table constraint | PRIMARY KEY (room_id, learner_id) |
Khóa ngoại: room_id → speak_rooms · learner_id → learners
Index: idx_speak_room_participants_learner(learner_id)
speak_rooms
Migration 0318 - speak rooms and recordings. SRC-1162, SDD-052. ## Why this migration exists Owner request 30.09.2026: pair talk moves from Zoom breakout rooms into voice rooms on learn.nemo12.com/speak. A room is joined by a short code; everyone present is recorded automatically from +30 s after the room starts, for 10 minutes; everyone who was present can listen back; audio lives in R2 and is deleted after 90 days. Access to a recording is derived from speak_recording_participants (who was present, i.e. whose voice is on the file), never from the room creator alone. Without that table a learner who left before the recording started could still play it, and a parent could not be matched to the child whose voice is on it. Phase 2 tables (transcripts per speaker, AI feedback per IELTS criterion, mentor comments) are created now but no code writes them yet (SDD-052 §9). Creating them now keeps phase 2 a code change, not a schema change on a table that already holds children's recordings. ## Re-running changes nothing CREATE TABLE / INDEX IF NOT EXISTS only.
migration: 0318_speak_rooms_and_recordings.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
id | TEXT | PRIMARY KEY |
code | TEXT | NOT NULL |
created_by | TEXT | NOT NULL REFERENCES learners(id) ON DELETE CASCADE |
context_kind | TEXT | NOT NULL DEFAULT 'none' CHECK (context_kind IN ('none','session','topic')) |
context_ref | TEXT | — |
status | TEXT | NOT NULL DEFAULT 'open' CHECK (status IN ('open','live','ended')) |
provider | TEXT | NOT NULL DEFAULT 'mock' |
provider_meeting_id | TEXT | — |
created_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
started_at | TEXT | — |
ended_at | TEXT | — |
host_learner_id | TEXT | REFERENCES learners(id) ON DELETE SET NULL — thêm ở 0319_speak_room_host_and_segments.sql |
Khóa ngoại: created_by → learners
Index: idx_speak_rooms_code(code, status) · idx_speak_rooms_status(status, created_at)