Skip to content

Dọn D1 (17.10.2026) ​

Chủ repo duyệt 16.10.2026, sau kiểm toán D1. Hai PR:

  1. Dọn D1 (1/2): lưu trữ rồi xoá dữ liệu trùng và bảng chết (trang này): migration 20261117a..20261117f.
  2. Dọn D1 (2/2): ảnh bài nộp sang R2: learner_product_submission_images.data_url ra khỏi D1, ba migration 20261117g..20261117i ở repo này + ba PR nối tiếp ở repo sutucon.com (mục cuối trang; bản một PR #2 đã hoàn tác).

Mọi thứ bị xoá đều có bản lưu ở R2 riêng tư conan-archive (không bật r2.dev, không gắn tên miền), dưới d1/2026-10-17/, kèm sổ băm api.conan.school/migrations/archive/2026-10-17-d1-cleanup.tsv. Mỗi tệp đã được tải ngược về và so sha256 + số dòng trước khi viết migration. Mốc time travel chụp TRƯỚC khi dọn: 00000d72-00000caa-00005107-416a38cd46ba222aa20b1589cdda865a.

Con số ​

Đo 17.10.2026 (byte dữ liệu length(CAST(c AS BLOB)), không tính chỉ mục và trang trống).

MigrationViệcDòngMB
20261117alegacy_book_ops_findings: giữ dòng upsert + hai lượt heartbeat gần nhất mỗi trường66.409 xoá, 924 giữ21,5
20261117bcourse_content_candidates: xoá bản giống hệt, giữ bản mới nhất3.615 xoá, 449 giữ49,5
20261117c79 bảng chết (31 có dòng, 48 rỗng)9.5255,3
20261117dcột book_illustrations.prompt5.395 ô7,8
20261117ecột legacy_ebooks.cover_prompt, cover_meta_json216 ô x 21,2
20261117fcột legacy_courses.art_prompt, art_meta_json217 ô x 20,6
Cộng~86

Khoảng 28% của 304 MB dữ liệu đo được. File SQLite không chắc co lại ngay (trang trống được tái dùng), nên con số wrangler d1 info có thể giảm chậm hơn; mục tiêu chính là chặn hai bảng tự đầy (đã chặn ghi lặp ở #458) và làm nhẹ đường đọc.

Cách xác định "không ai dùng": D1 này DÙNG CHUNG ​

Kiểm toán 16.10 chỉ nhìn hai repo và kết luận "chỉ hai Worker gắn D1 này". Sai: đọc known-risks. Ai gắn conan-platform-production (database_id 897de981-...) trên máy, 17.10.2026:

Worker / appNguồnCòn chạy?
api.conan.school, conan-tool-planerepo nàycó
api-sutucon-com (api.sutucon.com), Simba (Pages simba-sutucon), Rafikisutucon.com/có (deploy 10.09)
garage-api (api.conan.media), Pages garage-growth-os (agasa.conan.media)conan.media/agasa-platform/ (không phải git)có (deploy 28.07)
admin.inside6.com (PLATFORM_DB, cấu hình sinh ở scripts/prepare-worker-config.mjs)inside6.com/có
chuyenchon-admin-servicekhông có trên máy, chỉ có tài liệu chuyenchon/docs/admin/SDD.mdcó (deploy 03.08)

Mỗi bảng gỡ phải qua cả ba:

  1. Không truy vấn nào của bất kỳ Worker nào chạm tới trong 30 ngày: wrangler d1 insights conan-platform-production --time-period 30d --sort-by count --limit 5000 --json (3.591 câu; 31d trả rỗng). Đây là bằng chứng mạnh nhất vì nó nhìn mọi Worker kể cả mã không có trên máy. Loại các lượt quét bảo trì trước khi đếm: đếm dòng toàn bảng, char(8212) (quét gạch dài), ' AS loc,' (quét đường ảnh), ) AS c0,.
  2. Không mã nào đọc/ghi trong sáu cây trên (bỏ migrations/, docs, src/generated/). Với họ legacy_* phải tính cả d1Client: getD1Client()/createD1AdminClient() tự thêm legacy_, nên .from('events') là legacy_events, quan hệ '*, news_categories(*)' là legacy_news_categories, ${TABLE_PREFIX}x là legacy_x. Ngược lại SQL thô FROM events là bảng KHÔNG tiền tố. Lẫn hai chiều này thì hoặc giữ thừa, hoặc gỡ nhầm bảng đang sống.
  3. Không khoá ngoại nào từ bảng giữ lại trỏ vào. D1 bật foreign_keys; DROP TABLE chạy một DELETE ngầm, và ON DELETE CASCADE từ một bảng giữ lại sẽ xoá dữ liệu của bảng đó. Không có cặp nào như thế; trong danh sách gỡ thì con trước cha (seo_content_scores -> versions -> briefs -> categories, news_articles -> news_categories).

Test api.conan.school/tests/d1-cleanup.test.mts (npm run test:d1-cleanup, job api của CI) phát lại các migration và chạy lại phép kiểm 2 trên src/ cho danh sách gỡ: ai thêm lại một lượt đọc bảng đã gỡ thì đỏ ở CI, trước khi production trả 500.

Đã gỡ (migration 20261117c) ​

  • legacy_sb_* (10 bảng, 8.995 dòng, 4,9 MB): bản sao sách thời Supabase, mới nhất 05.07.2026. Kệ sách đọc legacy_ebook*.
  • compass27_* (8 bảng, 26 dòng): ứng dụng cũ 23.07.2026; compass.conan1.com dùng D1 riêng conan-compass.
  • simba_auth_identities, simba_sessions (43 dòng): chỉ _worker.bundle cũ ghi, đã gỡ khỏi sutucon 10.08.2026 (commit 5edc3ee); bản Simba đang chạy dựng từ 22b0a5a (đã gồm bản sửa) không có tên bảng; lần ghi cuối 07.08, phiên cuối hết hạn 04.09. Sutucon đăng nhập qua D1 riêng sutucon-auth.
  • Bản không tiền tố của tin tức và playbook: news_articles, news_categories, seo_categories, seo_content_briefs, seo_content_versions, seo_content_scores (126 dòng). Site đọc legacy_*. Gỡ chúng biến bẫy "ghi nhầm họ bảng, site không thấy" (skill playbook-pack) thành lỗi no such table ngay lúc ghi; bất biến đêm playbook-wrong-family giờ bắt bảng nào được dựng lại.
  • Bản không tiền tố rỗng mà mã chỉ chạm qua d1Client: book_packages, class_attendance, feature_artifacts, feature_findings, workshop_registrations.
  • Bản legacy_ chết của họ đã chuyển sang bảng không tiền tố: legacy_events (28), legacy_event_series (4), legacy_announcements (140), legacy_announcement_categories (24), legacy_workshop_recordings (3), legacy_workshop_resources (2), legacy_affiliate_visits (8), legacy_referral_events (0).
  • Bảng rỗng không ai dùng (còn lại của 48 bảng rỗng): legacy_ako_*, legacy_content_factory_*, legacy_course_* (bookmarks, competency_areas, faqs, insights, jtbd, learner_insights, purchase_journeys), legacy_seo_article_tags, legacy_seo_content_research, legacy_quiz_attempts, legacy_quiz_progress, legacy_zalo_*... Danh sách đủ, kèm số dòng từng bảng, ở chính file migration.

Giữ lại, và vì sao ​

BảngLý do giữ
garage_* (38 bảng, 1.911 dòng), agasa_* (5 bảng, 13 dòng)garage-api (api.conan.media) vẫn deploy và gắn D1 này; mã đọc 29 bảng garage_*. Bản đang chạy có thể khác bản trên đĩa (không phải git), nên giữ cả họ, kể cả 9 bảng chỉ có trong migration của nó.
d1_migrations (83)Sổ của wrangler d1 migrations apply, có 4 dòng của garage/agasa (0001_garage_foundation.sql...). agasa-platform/services/api/wrangler.jsonc khai migrations_dir: gỡ sổ thì một lượt migrations apply từ đó dội lại toàn bộ migration của nó.
migration_runs (3), migration_manifest (123)Sổ chuyển Supabase sang D1; admin.inside6.com/docs/requirements.md bắt ghi migration_runs trước khi tắt nguồn cũ. Nhỏ.
event_invitations (0)Bảng dự kiến trong yêu cầu của admin.inside6.com.
learning_integrity_*Dữ liệu của chuyenchon-admin-service theo SDD của nó; mã không có trên máy.
Book engine v1: legacy_book_packages, _package_revisions, _revision_proposals, _candidate_quality_reports, _audit_reports, _engine_usage, _ops_events (~17,5 MB)Còn đọc/ghi: màn hình Book Engine của admin (book-engine/repository.ts), review-repository.ts, workflow book-ops-repair, book-candidate-quality, book-remediation; ops-control.ts ghi legacy_book_ops_events (1.326 dòng trong 30 ngày); legacy_book_packages 142 lượt đọc, _package_revisions 46 lượt trong 30 ngày. Gỡ là việc đổi mã, không phải dọn dữ liệu.
Bản không tiền tố còn sống: book_engine_runs, feature_workflow_runs, content_runs, event_attendanceadmin.inside6.com đọc thẳng (vòng "Loop runs") và admin/system_control.ts dùng SQL thô.
ebook_learning_events, changelogs, book_favorites, event_rsvps... (24 bảng rỗng)Có truy vấn thật trong 30 ngày.
98 bảng rỗng khác (live_*, library_*, forum_*, concept_*, learner_session_feedback*, legacy_feature_*, legacy_lesson_outcomes...)Có mã đọc/ghi: 72 ở api/admin (kể cả qua d1Client), 26 ở api.sutucon.com / simba. Rỗng vì tính năng chưa có dữ liệu, không phải bảng chết.
legacy_class_attendance (4)admin/learning.ts đọc qua d1Client.
mentor_model_runs + hai bảng snapshotRafiki (/mentor-model) đọc qua api.sutucon.com; cron của api.conan.school là chỗ duy nhất làm mới.
Ngoài phạm vi, không đụnglegacy_ai_messages, legacy_ai_conversations, course_concept_interventions, learning_integrity_runs, image_recipes.prompt, legacy_ebooks.cover_svg_prompt, legacy_courses.cover_prompt (course_covers_to_sql.py còn ghi), book_art_styles.prompt.

Cột prompt chép từ git (migration 20261117d..f) ​

Mỗi cột đã được so với nguồn trong git TRƯỚC khi gỡ: 5.395/5.395 prompt ảnh minh hoạ trùng từng chữ với book_illustrations.ts prompts <sách> (dựng từ content/books/illustrations/*.json), 216/216 cover_prompt trùng content/books/art/*.json, 217/217 art_prompt trùng content/courses/art/*.json. Cùng PR:

  • scripts/book_illustrations.ts sql thôi chèn prompt; thêm lệnh prompts <sách> để người soạn duyệt bằng lời.
  • scripts/book_art.py sql thôi ghi cover_prompt, cover_meta_json.
  • scripts/book_pack_to_sql.py thôi ghi cover_prompt (vẫn kiểm nó trong JSON); dấu "chưa chép" của quyển cũ đổi từ cover_prompt IS NULL sang authoring_meta_json IS NULL.
  • scripts/course_art_to_sql.py thôi ghi art_prompt, art_meta_json.
  • scripts/audit_books.py tiêu chí B5 đọc prompt bìa từ git.

DROP COLUMN không có IF EXISTS: file chỉ chạy một lần nhờ sổ schema_migrations (dòng sổ nằm trong cùng lần gửi).

Khôi phục ​

Time travel (trong 30 ngày, đưa CẢ cơ sở dữ liệu về mốc, mất mọi ghi sau mốc):

bash
npx wrangler d1 time-travel restore conan-platform-production \
  --bookmark=00000d72-00000caa-00005107-416a38cd46ba222aa20b1589cdda865a

Chỉ chủ repo chạy, và chỉ khi cần trả lại cả cơ sở dữ liệu. Muốn lấy lại một bảng thì dùng bản R2:

bash
npx wrangler r2 object get conan-archive/d1/2026-10-17/_schema.sql --file /tmp/schema.sql --remote
npx wrangler r2 object get conan-archive/d1/2026-10-17/<bang>.jsonl.gz --file /tmp/<bang>.jsonl.gz --remote
sha256sum /tmp/<bang>.jsonl.gz   # so với cột sha256_gzip trong migrations/archive/2026-10-17-d1-cleanup.tsv

Rồi viết một migration mới: CREATE TABLE của bảng đó (chép từ _schema.sql), và INSERT từ JSONL (bỏ khoá _rowid_, chia lô dưới trần ~100 KB một câu). Cột: ALTER TABLE <bảng> ADD COLUMN <cột> TEXT rồi UPDATE ... WHERE id= theo <bảng>.<cột>.jsonl.gz. Đừng wrangler d1 execute chèn từ máy: dữ liệu khôi phục cũng đi đường "repo trước, D1 sau".

Kiểm sau deploy ​

bash
gh run list --workflow=deploy-api.yml --limit 3
npx wrangler d1 execute conan-platform-production --remote --command \
  "SELECT filename FROM schema_migrations WHERE filename LIKE '20261117%'"           # đủ 6 file
npx wrangler d1 execute conan-platform-production --remote --command \
  "SELECT COUNT(*) n, SUM(last_seen_at IS NOT NULL) cur FROM legacy_book_ops_findings" # ~1.000, không phải 67.000
npx wrangler d1 execute conan-platform-production --remote --command \
  "SELECT COUNT(*) n FROM course_content_candidates"                                   # ~450 + bản mới
npx wrangler d1 execute conan-platform-production --remote --command \
  "SELECT COUNT(*) n FROM sqlite_master WHERE type='table' AND name LIKE 'legacy_sb_%'" # 0

Rồi mở: màn hình Book Engine ở admin (lỗi heartbeat của lượt mới nhất vẫn hiện), một trang /playbooks/<slug> và /news ở www (đọc legacy_*, không đổi), một ảnh minh hoạ chương /api/book-art/...svg, và Simba/Rafiki/api.conan.media (không bảng nào của chúng bị đụng, nhưng D1 là của chung). Bất biến đêm playbook-wrong-family phải ra 0.

Ảnh bài nộp sang R2 (2/2) ​

learner_product_submission_images.data_url (71 ảnh base64, 11,5 MB, 17 học viên, 28.07 tới 14.10.2026) chuyển sang bucket R2 riêng tư simba-submissions (tạo 17.10.2026; r2.dev tắt, không tên miền: npx wrangler r2 bucket dev-url get simba-submissions, npx wrangler r2 bucket domain list simba-submissions). Không dùng conan-cms (công khai) và không phục vụ từ conan-archive (chỉ để lưu).

Ai ghi, ai đọc ​

Đường sống KHÔNG nằm ở api.conan.school:

  • /api/learner/* và /api/mentor/* của api này trả 410 từ 16.10.2026 (mountRetiredPortal); mã ghi/đọc ảnh ở src/domains/learner/index.ts là mã chết. Mở lại cổng đó sau bước 4 thì câu INSERT ... data_url của nó hỏng: phải ghi R2 như api.sutucon.com.
  • Người ghi và đọc thật là api.sutucon.com (repo sutucon.com): 30 ngày tới 17.10 có 18 câu INSERT (90 dòng ghi), 116 lượt đọc tóm tắt ảnh của chính học viên, 124 lượt đọc data_url của bài mẫu (exemplarImages).
  • Ai xem: học viên Simba thấy ảnh bằng chứng của bài mẫu cùng sản phẩm (ảnh của học viên khác). Mentor (Rafiki) chỉ thấy cờ has_evidence_image; workflow chấm bài chỉ đọc byte_size. Không bảng nào có khoá ngoại trỏ vào bảng này.

Các bước, theo đúng thứ tự ​

#Ở đâuViệcPR
1repo này, 20261117gALTER TABLE ... ADD COLUMN r2_key TEXT (NULL được)#463, đã merge và áp
2Asutucon.com, chỉ apiroute ảnh có xác thực + ghi kép: bytes lên R2, khoá vào r2_key, VÀ base64 vẫn vào data_url; GET .../products vẫn trả data_url, thêm id, versionsutucon.com#4, đã deploy 17.10
2Bsutucon.com, chỉ Simba<img src> của bài mẫu sang BFF của route ảnhsutucon.com#5, đã deploy 17.10 (sau khi route 2A đã thử)
2Csutucon.com, chỉ apithôi ghi base64 (data_url=''), thôi trả data_urlsutucon.com#6, đã deploy 17.10
3repo này, 20261117hđiền r2_key cho 71 hàng cũ từ bản chép đã so sha256#464, đã áp 17.10 13:10 UTC (71/71 có khoá)
4repo này, 20261117iALTER TABLE ... DROP COLUMN data_url#465, đã áp 17.10 13:19 UTC sau khi verify ra 71/71 khớp

Xong 17.10.2026. Cột data_url đã gỡ, bảng còn 13 cột, 71/71 hàng có r2_key; D1 xuống 298 MB. Mốc time-travel ngay trước khi gỡ: 00000d73-000021b8-00005107-1c827cb5c62e94fdba0aa7fbbe3884b6. Đường lùi ưu tiên vẫn là ADD COLUMN rồi điền từ conan-archive/d1/2026-10-17/learner_product_submission_images.jsonl.gz: restore cả D1 cuốn mất mọi lượt ghi mới của các ứng dụng dùng chung database này.

Kiểm route ảnh mà không cần đăng nhập Simba. Đăng nhập Simba là Google Identity Services dạng popup, agent không bấm được. Thay vào đó chạy đúng mã api của commit đã deploy bằng wrangler dev với một config tạm chỉ có hai binding, cả hai remote = true (D1 conan-platform-production, R2 simba-submissions), truyền --var SIMBA_GATEWAY_SECRET:<giá trị thử> rồi tự ký HMAC GET\n<path>\n<user_id>\n<timestamp> như BFF của Simba. Chỉ gọi route GET, không gọi gì ghi. Kết quả 17.10: ảnh bài mẫu 200 đúng byte_size, id lạ 404, lớp sai 403, chữ ký sai 401; sau bước 3 ảnh đi từ R2 (có ETag) và byte trùng khít bản base64. Bẫy: route bắn nhiều truy vấn song song (GET .../products) hay chết vì proxy binding từ xa ("Network connection lost", 500 sau vài phút); đó là bản chạy thử, không phải lỗi thật, kiểm thẳng câu SQL bằng wrangler d1 execute thay vì tin con số 500 đó.

Vì sao bước 2 thành ba PR. Bản đầu, sutucon.com#2, đổi api và Simba trong một lượt push. deploy.yml của sutucon chạy một job mỗi app, không job nào chờ job nào: Simba lên và trỏ ảnh tới route mới, api đỏ vì token CI thiếu quyền R2, ảnh bài mẫu vỡ khoảng 20 phút tới khi hoàn tác ở #3. Xem known-risks. Ba PR thì mỗi lượt push chỉ đổi một app, và lượt nào đỏ cũng để nguyên bản đang chạy: 2A đỏ thì Worker cũ ở lại; 2A lên thì Simba cũ vẫn đọc data_url (ghi kép); 2B đọc route đã có; 2C chỉ bỏ thứ không ai đọc nữa.

Token CI của sutucon phải có "Workers R2 Storage: Read" (ít nhất) trước khi merge 2A: wrangler deploy kiểm mọi binding R2 bằng GET /accounts/<id>/r2/buckets/<tên>, thiếu quyền là Authentication error [code: 10000] và Worker không được tải lên. Ghi ở DEPLOY.md của repo đó.

Bước 1 và 2A merge theo thứ tự nào cũng được: bản api.sutucon.com mới hỏi pragma_table_info ở MỖI lượt ghi (không lưu đệm, một isolate sống hàng giờ), chưa có r2_key thì ghi base64 như cũ. Bước 3 thì không: nếu bản api.sutucon.com CŨ (trước 2A) còn chạy sau khi hàng đã có r2_key, một học viên thay ảnh sẽ ghi data_url mới mà không đổi r2_key, và bản mới sẽ phục vụ ảnh cũ từ R2. Từ 2A mọi lượt thay ảnh đổi cả hai cột, nên về lý bước 3 an toàn ngay sau 2A; giữ nó sau 2C cho một thứ tự thẳng.

Hàng do bản api CŨ ghi sau lượt chép 17.10 (trước khi 2A lên) có r2_key NULL và không có trong 20261117h. verify liệt kê chúng; chạy lại copy (nó chỉ chọn hàng r2_key IS NULL) để sinh một migration điền khoá thứ hai, tên MỚI (đừng dùng lại tên đã có, xem known-risks), áp nó, verify lại, rồi mới bước 4. Sót thì gác của 20261117i dừng file, cột không bị gỡ.

Bước 4 không cần sửa mã ở api.sutucon.com: lượt ghi chỉ nhắc data_url khi cột còn, lượt đọc dùng SELECT i.*. DROP COLUMN chạy được trên bảng STRICT với cột NOT NULL (cột không nằm trong chỉ mục, ràng buộc hay trigger nào), nên KHÔNG cần dựng lại bảng; dựng lại bằng DROP TABLE còn nguy hiểm hơn vì D1 bật foreign_keys và DROP TABLE chạy một DELETE ngầm. Migration có gác: còn một hàng thiếu r2_key là nó dừng ở câu đầu, không gỡ gì.

Test: npm run test:submission-images-r2 (job api của CI) phát lại các migration trên lược đồ production và chạy cả câu ghi CŨ lẫn MỚI của api.sutucon.com: repo khác ghi bảng này, nên test đó là chỗ duy nhất ở đây giữ hình dạng câu ghi của họ. Bên sutucon: npm test trong api.sutucon.com (ghi, phục vụ, gác, đường lùi, thay ảnh, sau DROP COLUMN; ở 2A thêm ghi kép và danh sách còn data_url, ở 2C danh sách không còn data_url), chạy trên PR (checks.yml) và trước deploy.

Bản chép 71 ảnh cũ (17.10.2026) ​

sutucon.com/api.sutucon.com/scripts/copy-submission-images-to-r2.mjs copy: chỉ ĐỌC D1 (SELECT theo lô 8 hàng), giải base64, so số byte với byte_size, wrangler r2 object put --remote vào simba-submissions dưới khoá submissions/<submission_id>/<image_id>-<sha256 16 ký tự>.<ext>, rồi get --remote tải NGƯỢC về và so sha256. Kèm một bản lưu JSONL gzip của nguyên bảng (cả data_url) vào conan-archive/d1/2026-10-17/, cũng tải ngược so băm. Sổ: api.conan.school/migrations/archive/2026-10-17-submission-images-r2.tsv (không chứa nội dung ảnh).

Migration 20261117h là một UPDATE mỗi hàng, có gác r2_key IS NULL AND updated_at=<lúc chép> AND length(data_url)=<lúc chép>: hàng nào bị thay ảnh sau lượt chép thì KHÔNG bị gán khoá của ảnh cũ, nó giữ đường lùi data_url (hoặc đã có khoá mới do api.sutucon.com ghi). Chạy lại vô hại.

Kiểm sau từng bước ​

bash
# Sau bước 1 (CI của repo này áp 20261117g):
npx wrangler d1 execute conan-platform-production --remote --command \
  "SELECT name FROM pragma_table_info('learner_product_submission_images') WHERE name IN ('r2_key','data_url')"  # 2 dòng

# Mỗi bước 2A/2B/2C: job của đúng app đó xanh cho đúng SHA merge
gh run list -R dac2205/sutucon.com --workflow=deploy.yml --limit 3

# Sau 2A (chỉ api): route ảnh trả 200 image/* cho một bài mẫu thật, Cache-Control: private, nosniff; trang Sản phẩm
# trên Simba CŨ vẫn hiện ảnh (từ data_url). Một lượt nộp bài có ảnh mới:
npx wrangler d1 execute conan-platform-production --remote --command \
  "SELECT id, r2_key, length(data_url) n FROM learner_product_submission_images ORDER BY updated_at DESC LIMIT 3"  # r2_key có, n > 0 (ghi kép)

# Sau 2B (chỉ Simba): ảnh bài mẫu hiện, URL ảnh là /api/classes/<slug>/submission-images/<id>?v=...

# Sau 2C (chỉ api): câu trên với một lượt nộp mới cho r2_key có, n = 0; GET .../products không còn data_url.

# Sau bước 3 (20261117h):
npx wrangler d1 execute conan-platform-production --remote --command \
  "SELECT COUNT(*) n, SUM(r2_key IS NULL) chua_co_khoa FROM learner_product_submission_images"  # chua_co_khoa = 0
# Trước bước 4, kiểm R2 từng hàng (chỉ đọc): có object, khớp byte_size + băm, khớp data_url nếu còn
node scripts/copy-submission-images-to-r2.mjs verify     # ở sutucon.com/api.sutucon.com

Khôi phục bước 4: ALTER TABLE learner_product_submission_images ADD COLUMN data_url TEXT rồi điền từ conan-archive/d1/2026-10-17/learner_product_submission_images.jsonl.gz theo id (cột mới sẽ NULL được, khác bản gốc).

Tài liệu nội bộ nền tảng Conan School.