---
url: https://docs.nemo12.com/ops/migration-reconciliation.md
description: >-
  Nhật ký đối soát d1_migrations production với migrations/*.sql trên đĩa
  (AS-04.6.1), biến lệch không ai biết thành nợ đã khai báo.
---

# Migration Reconciliation Log

Đối soát hai chiều `d1_migrations` (production, remote — nguồn sự thật) ↔ `migrations/*.sql` (đĩa), theo [Audit Standard AS-04.6.1](../quality/audit-standard/index.md). Mục đích: biến "lệch không ai biết" thành "nợ đã khai báo" — PASS không đòi 0 lệch tuyệt đối (đó là AS-04.6.2), chỉ đòi mọi lệch đã được liệt kê và giải thích, ngày đối soát ≤ 7 ngày.

## Đối soát 2026-08-20 (Audit #004 — AS-04.6.1, AS-04.6.2, AS-04.6.3)

Lệnh chạy (cùng bộ lệnh của lần trước, thêm `--config` vì repo nay có 9 `wrangler.jsonc`):

```bash
npx wrangler d1 execute nemo12-platform --remote --json --config workers/api/wrangler.jsonc \
  --command "SELECT name FROM d1_migrations ORDER BY name"
ls migrations/*.sql | xargs -n1 basename | sort
```

Kết quả: `d1_migrations` remote **58 dòng**, đĩa **57 file**. Lệch = **1 dòng mồ côi**, 0 file chưa áp.

| Tên trong `d1_migrations` (production) | File hiện tại trên đĩa | Vì sao lệch | Có nguy hiểm không |
| --- | --- | --- | --- |
| `0046_context_event_temporal.sql` | đã đổi tên thành `0052_context_event_temporal.sql` (commit `d4d582c`, gỡ trùng số 0046 với `0046_mentor_profiles_albums.sql`) | Cùng nguyên nhân với hai dòng mồ côi của lần đối soát trước: đổi tên file trên đĩa không đổi được dòng đã ghi trong `d1_migrations`. Nội dung đã áp hai lần dưới hai tên (một lần dưới `0046_…` trước khi đổi, một lần dưới `0052_…` sau khi đổi) và không hỏng gì, vì file này dựng lại bảng bằng `CREATE TABLE IF NOT EXISTS` + `INSERT … SELECT` từ bảng cũ | Không — `disk - remote` rỗng, không migration nào đang chờ áp, và schema-diff dưới đây khớp tuyệt đối |

**Kết luận AS-04.6.1:** 1/1 lệch đã biết, đã giải thích → PASS kể từ 2026-08-20.

**AS-04.6.2 (0 dòng mồ côi) vẫn KHÔNG PASS.** Câu dọn nợ dưới đây **cố ý chưa chạy** — nó là một lệnh ghi thẳng vào D1 production, mà luật hiện hành của dự án là mọi thay đổi production đi qua CI (AS-03.5.3). Để chủ dự án quyết định thời điểm:

```bash
npx wrangler d1 execute nemo12-platform --remote --config workers/api/wrangler.jsonc \
  --command "DELETE FROM d1_migrations WHERE name = '0046_context_event_temporal.sql'"
```

## Schema-diff production ↔ `migrate:dryrun` — 2026-08-20 (AS-04.6.3)

Lần trước chạy 2026-08-15, tức đã cách 18 migration. Chạy lại:

```bash
npm run migrate:dryrun          # dựng lại toàn bộ 57 migration từ D1 rỗng
# rồi dump sqlite_master cả hai phía và so theo TẬP CỘT của từng bảng
```

| | Production | `migrate:dryrun` |
| --- | --- | --- |
| Số bảng (bỏ `sqlite_*`, `_cf_*`) | **92** | **92** |
| Bảng chỉ có ở một phía | 0 | 0 |
| Bảng lệch tập cột | **0** | **0** |

So sánh theo **tập cột** chứ không theo text DDL thô, vì cùng một schema dựng bằng hai đường khác nhau (`CREATE TABLE` gộp so với `CREATE` rồi `ALTER ADD COLUMN`) in ra DDL khác nhau — hạn chế đã ghi ở phần đối soát 2026-08-15 và vẫn đúng. **AS-04.6.3 = PASS.**

## Đối soát 2026-08-15

Lệnh chạy:

```bash
npx wrangler d1 execute nemo12-platform --remote --json \
  --command "SELECT name FROM d1_migrations ORDER BY name"
ls migrations/*.sql | xargs -n1 basename | sort
```

Kết quả: `d1_migrations` remote có **41 dòng**, đĩa có **39 file**. Lệch = 2 dòng mồ côi (remote có, đĩa không):

| Tên trong `d1_migrations` (production) | File hiện tại trên đĩa | Vì sao lệch | Có nguy hiểm không |
| --- | --- | --- | --- |
| `0022_whale_country_detail.sql` | đã đổi tên thành `0031_whale_country_detail.sql` (đợt sửa trùng số 0022 — AS-04.1.1) | Đổi tên file trên đĩa không đổi được dòng đã ghi trong `d1_migrations` production; nội dung y hệt đã áp dưới tên mới (idempotent, `CREATE TABLE IF NOT EXISTS`) nên chạy lại vô hại | Không — `wrangler d1 migrations list nemo12-platform --remote` báo "No migrations to apply" |
| `0031_labs.sql` | đã đổi tên thành `0037_labs.sql` (đợt SRC-112 gộp cùng lúc với đợt đổi số ở trên) | Cùng nguyên nhân — đổi tên để gỡ trùng số, remote giữ tên cũ | Không — cùng lý do trên |

**Kết luận đối soát 2026-08-15 (lần 1):** 2/2 lệch đã biết, đã giải thích — AS-04.6.1 = PASS.

## Dọn nợ AS-04.6.2 — đã thực hiện 2026-08-15

```bash
npx wrangler d1 execute nemo12-platform --remote --command \
  "DELETE FROM d1_migrations WHERE name IN ('0022_whale_country_detail.sql', '0031_labs.sql')"
# → changes: 2, changed_db: true
```

Xác minh lại ngay sau khi xoá (cùng lệnh đối soát ở trên): `d1_migrations` remote **39 dòng**, đĩa **39 file**, **khớp tuyệt đối, comm -23 rỗng**. `wrangler d1 migrations list nemo12-platform --remote` → "No migrations to apply". AS-04.6.2 = PASS kể từ đây.

### Vì sao lỗi này tái diễn — bài học đã đưa vào Audit Standard v0.2

Cả hai lần đổi số migration đều xảy ra khi có nhiều phiên AI cùng chỉnh `migrations/` song song mà không ai đối soát lại `d1_migrations` remote sau khi đổi tên file. Audit Standard v0.2 §1.4 đã đổi nguồn sự thật của toàn bộ AS-04.6 sang `d1_migrations` remote (không dùng `git log --all`, vì chính `0031_labs.sql` từng được áp thẳng vào production mà **chưa bao giờ commit** — `git log --all -- migrations/0031_labs.sql` và `git fsck --unreachable` đều rỗng). AS-03.5.3 (Pilot Gate) cấm áp migration production ngoài CI để chặn đúng nguyên nhân gốc này.

## Schema-diff thật — AS-04.6.3 (2026-08-15)

Audit #002 chấm `AS-04.6.3` KHÔNG PASS (mã HH) bằng so sánh DDL text thô giữa production và `migrate:dryrun` cục bộ, phát hiện `whale_scholarships` "lệch". Đối soát lại bằng đúng cách kiểm mà chuẩn yêu cầu — **chuẩn hoá trước khi so, không so text thô** — cho kết quả khác:

```bash
# Production
wrangler d1 execute nemo12-platform --remote --json --command "PRAGMA table_info(whale_scholarships)"
# Local dryrun (chỉ chạy migration 0031, KHÔNG chạm production)
wrangler d1 execute nemo12-platform --local --persist-to /tmp/whale-schema-check \
  --file=migrations/0031_whale_country_detail.sql
wrangler d1 execute nemo12-platform --local --persist-to /tmp/whale-schema-check --json \
  --command "PRAGMA table_info(whale_scholarships)"
```

Kết quả: **15/15 cột khớp tuyệt đối** — tên, kiểu, `notnull`, `default` giống nhau từng cột, đúng thứ tự. Lý do DDL text thô khác nhau: production dựng bảng này qua `CREATE TABLE` gốc (chạy dưới tên cũ `0022_...`) rồi `ALTER TABLE ADD COLUMN` 5 lần từ migration `0026` (nay no-op); `migrate:dryrun` cục bộ dựng lại từ đầu bằng `0031` — một `CREATE TABLE` gộp cả 15 cột trong một câu lệnh (đã ghi rõ trong comment đầu file `0031_whale_country_detail.sql`, xác nhận đối chiếu ngày 2026-08-15). SQLite in lại DDL text khác nhau cho hai cách dựng schema **cấu trúc giống hệt nhau** — đây là hạn chế của so sánh text thô, không phải schema drift thật.

**Kết luận:** AS-04.6.3 = PASS khi so đúng cách (`PRAGMA table_info`, không phải diff văn bản `sqlite_master.sql`). Không cần sửa migration nào. Bài học cho lần chấm sau: cách kiểm "chuẩn hoá khoảng trắng" trong audit-standard/index.md chưa đủ — cần chuẩn hoá theo **cấu trúc** (PRAGMA table_info/index_list), không chỉ khoảng trắng, vì CREATE-gộp và CREATE+ALTER-lịch-sử luôn in ra text khác nhau dù schema giống hệt.

## Cấm chạy migration production ngoài CI (AS-03.5.3)

**Không ai được chạy `wrangler d1 migrations apply --remote` (hoặc `d1 execute --remote` với DDL) từ máy cá nhân hay phiên AI cục bộ.** Đường duy nhất để migration chạm production là qua GitHub Actions: `ci.yml` tự áp khi push vào `main` đụng `migrations/`, hoặc bấm tay `deploy-app.yml` với app `api` (SRC-948 — trước 22.09.2026 là `deploy-api.yml`). Cả hai đều chạy bước "Apply D1 migrations (trước code — QG-004)" TRƯỚC khi deploy worker. Đây chính là nguyên nhân gốc của cả hai lần lệch ghi ở trên: `0031_labs.sql` từng được áp thẳng vào production mà **chưa bao giờ được commit** (`git log --all -- migrations/0031_labs.sql` và `git fsck --unreachable --no-reflog` đều rỗng — xác nhận 2026-08-15). Sửa/xoá dữ liệu migration bookkeeping (như thao tác DELETE ở trên) là ngoại lệ hiếm, phải ghi lại ở file này, không lặp lại như một thói quen.

## Lịch đối soát

Chạy lại đối soát này mỗi khi có đợt đổi số migration, và tối thiểu mỗi 7 ngày trong giai đoạn nhiều phiên AI cùng chỉnh `migrations/` (AS-04.6.1 đòi ngày đối soát ≤ 7 ngày để PASS).
