diff --git a/docs/i18n/rules/vi/DOL008.md b/docs/i18n/rules/vi/DOL008.md new file mode 100644 index 0000000..49c097c --- /dev/null +++ b/docs/i18n/rules/vi/DOL008.md @@ -0,0 +1,29 @@ +# DOL008 — Dùng `flat=True` khi gọi `.values_list()` với một trường duy nhất + +**Mức độ mặc định:** info · **Khả năng áp dụng:** safe · **Danh mục:** queryset + +Phát hiện `.values_list("field")` được gọi với đúng một đối số vị trí và không có từ khóa nào khác. Khi không có `flat=True`, Django trả về một queryset gồm các tuple một phần tử — `[(1,), (2,), (3,)]` — trong khi gần như mọi trường hợp đều muốn có danh sách phẳng: `[1, 2, 3]`. Dạng tuple một phần tử hầu như không bao giờ là chủ ý khi chỉ yêu cầu một trường; nó chỉ tạo thêm công unpacking hoặc gây lỗi âm thầm khi kết quả được truyền vào lookup `__in` hay các phép tập hợp. + +QuickFix tự động thêm `flat=True` và được đánh dấu safe vì ngữ nghĩa hoàn toàn giống nhau với lời gọi một trường: query không thay đổi, chỉ có wrapper Python thay đổi. + +## Sai + +```python +ids = User.objects.values_list("id") +# ids == [(1,), (2,), (3,)] — có thể không phải kết quả bạn mong muốn +``` + +## Đúng + +```python +ids = User.objects.values_list("id", flat=True) +# ids == [1, 2, 3] +``` + +## Bỏ qua (Suppress) + +```python +# django-orm-lens-disable-next-line DOL008 +``` + +Hoặc theo từng workspace trong `.vscode/settings.json`: `{"djangoOrmLens.rules": {"DOL008": "off"}}`. diff --git a/docs/i18n/rules/vi/DOL021.md b/docs/i18n/rules/vi/DOL021.md new file mode 100644 index 0000000..a3b153c --- /dev/null +++ b/docs/i18n/rules/vi/DOL021.md @@ -0,0 +1,29 @@ +# DOL021 — Dùng `timezone.now()` thay vì `datetime.now()` trong dự án Django + +**Mức độ mặc định:** warning · **Khả năng áp dụng:** safe · **Danh mục:** datetime + +Phát hiện các lời gọi `datetime.datetime.now()` hoặc `datetime.now()` (không có đối số `tz`) trong dự án Django. Khi `USE_TZ = True` (mặc định của Django từ 5.0; template `startproject` đã đặt thành `True` từ 4.0), datetime naive không thể so sánh với datetime aware và sẽ bị từ chối bởi `DateTimeField` khi lưu. Dùng `timezone.now()` từ `django.utils.timezone` luôn trả về datetime aware theo UTC bất kể giá trị của `USE_TZ`, khiến nó trở thành lựa chọn an toàn mặc định trong mọi codebase Django. + +QuickFix viết lại lời gọi và thêm import nếu còn thiếu; được đánh dấu safe vì kiểu đầu ra thay đổi từ naive sang aware — đây chính là kiểu đúng cho mọi trường datetime trong Django. + +## Sai + +```python +from datetime import datetime +created_at = datetime.now() +``` + +## Đúng + +```python +from django.utils import timezone +created_at = timezone.now() +``` + +## Bỏ qua (Suppress) + +```python +# django-orm-lens-disable-next-line DOL021 +``` + +Hoặc theo từng workspace trong `.vscode/settings.json`: `{"djangoOrmLens.rules": {"DOL021": "off"}}`. diff --git a/docs/i18n/rules/vi/DOL022.md b/docs/i18n/rules/vi/DOL022.md new file mode 100644 index 0000000..6a87c13 --- /dev/null +++ b/docs/i18n/rules/vi/DOL022.md @@ -0,0 +1,27 @@ +# DOL022 — Dùng `timezone.now()` để so sánh với các giá trị `DateTimeField` + +**Mức độ mặc định:** warning · **Khả năng áp dụng:** safe · **Danh mục:** datetime + +Phát hiện các biểu thức filter so sánh một lookup `DateTimeField` với `datetime.now()` hoặc `date.today()` mà không có timezone awareness. So sánh các giá trị aware từ DB với datetime naive của Python sẽ gây `TypeError` ở runtime (hoặc với một số backend, trả về kết quả sai một cách âm thầm). `timezone.now()` luôn là aware và luôn đúng cho mục đích này. + +## Sai + +```python +from datetime import datetime +ExpiredToken.objects.filter(expires_at__lt=datetime.now()) +``` + +## Đúng + +```python +from django.utils import timezone +ExpiredToken.objects.filter(expires_at__lt=timezone.now()) +``` + +## Bỏ qua (Suppress) + +```python +# django-orm-lens-disable-next-line DOL022 +``` + +Hoặc theo từng workspace trong `.vscode/settings.json`: `{"djangoOrmLens.rules": {"DOL022": "off"}}`. diff --git a/docs/i18n/rules/vi/DOL031.md b/docs/i18n/rules/vi/DOL031.md new file mode 100644 index 0000000..cdfc4a3 --- /dev/null +++ b/docs/i18n/rules/vi/DOL031.md @@ -0,0 +1,31 @@ +# DOL031 — Dùng `get_object_or_404()` thay vì `.get()` trực tiếp trong view + +**Mức độ mặc định:** info · **Khả năng áp dụng:** safe · **Danh mục:** forms-views + +Phát hiện các lời gọi `.get()` bên trong hàm view Django hoặc method của class-based view mà không được bọc trong khối `try/except ObjectDoesNotExist` và không được thay bằng `.get_object_or_404()`. Một `.get()` trần sẽ raise `Model.DoesNotExist` (subclass của `ObjectDoesNotExist`) khi không tìm thấy row phù hợp — exception handler mặc định của Django sẽ chuyển điều này thành lỗi 500, làm lộ stack trace ở chế độ DEBUG và trả về lỗi không rõ nghĩa ở production. `get_object_or_404()` chuyển điều kiện tương tự thành response 404 gọn gàng. + +QuickFix thay `.get(...)` bằng `get_object_or_404(Model, ...)` và tự thêm import. + +## Sai + +```python +def post_detail(request, pk): + post = Post.objects.get(pk=pk) # raise 500 nếu không tìm thấy +``` + +## Đúng + +```python +from django.shortcuts import get_object_or_404 + +def post_detail(request, pk): + post = get_object_or_404(Post, pk=pk) +``` + +## Bỏ qua (Suppress) + +```python +# django-orm-lens-disable-next-line DOL031 +``` + +Hoặc theo từng workspace trong `.vscode/settings.json`: `{"djangoOrmLens.rules": {"DOL031": "off"}}`. diff --git a/docs/i18n/rules/vi/DOL032.md b/docs/i18n/rules/vi/DOL032.md new file mode 100644 index 0000000..4376d97 --- /dev/null +++ b/docs/i18n/rules/vi/DOL032.md @@ -0,0 +1,29 @@ +# DOL032 — Tránh truyền dữ liệu thô từ `request.GET` / `request.POST` trực tiếp vào queryset + +**Mức độ mặc định:** warning · **Khả năng áp dụng:** unsafe · **Danh mục:** forms-views + +Phát hiện các giá trị `request.GET` hoặc `request.POST` được truyền trực tiếp vào đối số `.filter()` hoặc `.exclude()` của queryset mà không qua Django Form hoặc danh sách cho phép (allow-list) tường minh. Dữ liệu request thô là không đáng tin: người gọi có thể gửi tên trường bất ngờ để duyệt qua các quan hệ (`__user__password`), kích hoạt các join tốn kém, hoặc làm lộ dữ liệu thông qua field enumeration. Pattern đúng là validate và lọc các trường qua `Form` hoặc `FilterSet` trước khi tạo queryset. + +Khả năng áp dụng là `unsafe` vì cần hiểu rõ bề mặt filter mong muốn. + +## Sai + +```python +Post.objects.filter(**request.GET.dict()) # mọi key người gọi gửi đều trở thành filter +``` + +## Đúng + +```python +form = PostFilterForm(request.GET) +if form.is_valid(): + Post.objects.filter(**form.cleaned_data) +``` + +## Bỏ qua (Suppress) + +```python +# django-orm-lens-disable-next-line DOL032 +``` + +Hoặc theo từng workspace trong `.vscode/settings.json`: `{"djangoOrmLens.rules": {"DOL032": "off"}}`. diff --git a/docs/i18n/rules/vi/README.md b/docs/i18n/rules/vi/README.md index 873cc54..67db32c 100644 --- a/docs/i18n/rules/vi/README.md +++ b/docs/i18n/rules/vi/README.md @@ -1,34 +1,49 @@ -# Tài liệu Quy tắc +# Tài liệu tham khảo quy tắc -Mỗi trang dưới đây mô tả một quy tắc cụ thể (hiện tại danh mục chứa 7 quy tắc): mã quy tắc, độ nghiêm trọng mặc định, khả năng áp dụng, các ví dụ về code sai/đúng, và cách bỏ qua (suppress) nếu cần. +Mỗi mã `DOL` là một rule lint mà extension VS Code áp dụng ngay khi bạn gõ code. Các lệnh CLI (`nplusone`, `migration-risk`, `blast-radius`) chạy cùng phân tích đó ở quy mô toàn dự án. -> The remaining rules — `DOL011`–`DOL032`, plus `migrations.md` and `nplusone.md` — are not translated yet; see the [English reference](../../../rules/README.md). This note is in English because it was written by someone who does not speak Vietnamese; a translation of it is welcome. +## Quy tắc queryset -## Queryset - -| Mã | Tóm tắt | Mức độ | Áp dụng | -|----|---------|--------|---------| +| Mã | Tiêu đề | Mức độ | Khả năng áp dụng | +|----|---------|--------|------------------| | [DOL001](DOL001.md) | Ưu tiên `.exists()` thay vì `.count() > 0` | info | safe | -| [DOL002](DOL002.md) | Ưu tiên `not .exists()` thay vì `.count() == 0` | info | safe | -| [DOL003](DOL003.md) | Ưu tiên `not .exists()` thay vì `.first() is None` | info | safe | -| [DOL004](DOL004.md) | Ưu tiên `.exists()` thay vì `.first() is not None` | info | safe | -| [DOL005](DOL005.md) | Cân nhắc dùng `Q(...)` thay vì chuỗi `.filter().exclude()` | hint | suggestion | -| [DOL006](DOL006.md) | Bỏ `list()` bọc ngoài QuerySet trong vòng lặp for | info | safe | +| [DOL002](DOL002.md) | Ưu tiên `.count()` thay vì `len(queryset)` | info | safe | +| [DOL003](DOL003.md) | Tránh `list(queryset)` trong ngữ cảnh boolean | info | safe | +| [DOL004](DOL004.md) | Dùng `.only()` / `.defer()` để giới hạn trường được tải | info | unsafe | +| [DOL005](DOL005.md) | Tránh gọi `.all()` trước `.filter()` | info | safe | +| [DOL006](DOL006.md) | Dùng `.iterator()` cho queryset lớn | warning | unsafe | | [DOL007](DOL007.md) | Có thể xảy ra N+1: truy cập thuộc tính bên trong vòng lặp for | warning | unsafe | +| [DOL008](DOL008.md) | Dùng `flat=True` khi gọi `.values_list()` với một trường duy nhất | info | safe | + +## Quy tắc định nghĩa model + +| Mã | Tiêu đề | Mức độ | Khả năng áp dụng | +|----|---------|--------|------------------| +| [DOL011](DOL011.md) | Thêm `db_index=True` cho trường FK dùng trong filter | warning | unsafe | +| [DOL012](DOL012.md) | Thêm `db_index=True` cho trường dùng trong `order_by()` | info | unsafe | +| [DOL013](DOL013.md) | Dùng `select_related` cho các truy cập FK trong serializer | warning | unsafe | +| [DOL014](DOL014.md) | Dùng `prefetch_related` cho các truy cập FK ngược / M2M | warning | unsafe | +| [DOL015](DOL015.md) | Tránh lưu dữ liệu lớn trực tiếp trên model | info | unsafe | + +## Quy tắc datetime + +| Mã | Tiêu đề | Mức độ | Khả năng áp dụng | +|----|---------|--------|------------------| +| [DOL021](DOL021.md) | Dùng `timezone.now()` thay vì `datetime.now()` | warning | safe | +| [DOL022](DOL022.md) | Dùng `timezone.now()` để so sánh với `DateTimeField` | warning | safe | -## Mức độ nghiêm trọng +## Quy tắc forms / views -| Từ khóa | Ý nghĩa | -|---------|----------| -| `error` | Luôn sai; ưu tiên sửa ngay | -| `warning` | Rất có thể sai; cần xem xét | -| `info` | Viết lại an toàn nhưng không bắt buộc | -| `hint` | Gợi ý cải thiện; cần đánh giá từng trường hợp | +| Mã | Tiêu đề | Mức độ | Khả năng áp dụng | +|----|---------|--------|------------------| +| [DOL031](DOL031.md) | Dùng `get_object_or_404()` thay vì `.get()` trực tiếp | info | safe | +| [DOL032](DOL032.md) | Tránh truyền dữ liệu request thô vào queryset | warning | unsafe | -## Khả năng áp dụng +## Công cụ phân tích CLI -| Từ khóa | Ý nghĩa | -|---------|----------| -| `safe` | QuickFix có thể áp dụng tự động | -| `suggestion` | Cần kiểm tra trước khi áp dụng | -| `unsafe` | Không có QuickFix; phải sửa thủ công | +| Lệnh | Chức năng | +|------|-----------| +| [nplusone](nplusone.md) | Phát hiện các pattern N+1 trên toàn dự án | +| [migration-risk](migrations.md) | Đánh giá file migration theo 16 quy tắc an toàn | +| [blast-radius](blast-radius.md) | Kết hợp migration risk với phân tích tham chiếu toàn codebase | +| [drift](drift.md) | Phát hiện khi `db_table` hoặc tên cột lệch khỏi convention Django | diff --git a/docs/i18n/rules/vi/blast-radius.md b/docs/i18n/rules/vi/blast-radius.md new file mode 100644 index 0000000..b574d9e --- /dev/null +++ b/docs/i18n/rules/vi/blast-radius.md @@ -0,0 +1,168 @@ +# blast-radius + +Lệnh `blast-radius` kết hợp chấm điểm rủi ro migration với quét tham chiếu toàn codebase. Đối với mỗi hoạt động migration có tính phá hủy (destructive) — `RemoveField`, `RemoveIndex`, `DeleteModel`, `RenameField`, `AlterField` — nó đặt câu hỏi: *có bao nhiêu nơi trong codebase vẫn tham chiếu đến thứ mà migration này xóa hoặc thay đổi?* Câu trả lời chính là bán kính ảnh hưởng (blast radius). + +## Nó báo cáo gì + +Đối với mỗi đối tượng mục tiêu (field, model, hoặc index bị thay đổi): + +- **Rủi ro migration** từ `migration-risk` (rule nào bị vi phạm và ở mức độ nào) +- **Số lượng tham chiếu** được phân chia theo mức độ tự tin và theo tầng kiến trúc của Django (views, serializers, templates, admin, forms, model methods) +- **Xem trước phân tầng (Cascade preview)** — nếu hoạt động là `RemoveField` trên một trường có `on_delete=CASCADE`, công cụ sẽ thực hiện lần phân tích thứ hai để liệt kê mọi model sẽ bị xóa theo dạng cascade + +Các mục tiêu được sắp xếp theo mức độ nghiêm trọng từ cao đến thấp, sau đó theo số lượng tham chiếu `certain` còn sót lại, rồi theo tên — vì vậy thứ có khả năng làm hỏng production cao nhất sẽ nằm ở đầu kết quả đầu ra. + +## Độ tự tin (Confidence), và tại sao tồn tại mức `possibly` + +Các finding tham chiếu có các mức độ `certain` (chắc chắn) / `likely` (có khả năng) / `possibly` (có thể), đến từ cùng một bộ phân loại (classifier) mà extension VS Code sử dụng: + +- **certain** — một tham chiếu ORM rõ ràng không thể nhầm lẫn: `filter(author__id=1)`, `order_by("-author")`, `fields = ["author"]`, `list_display`, `search_fields`. +- **likely** — truy cập thuộc tính bên trong một tầng được nhận dạng của Django, hoặc một biến template `{{ post.author }}`. +- **possibly** — một kết quả khớp định danh trần, hoặc truy cập thuộc tính trong một file mà không thể xác định được tầng kiến trúc. + +Không có type inference (suy diễn kiểu) ở đây. Đó là công việc của Pyright, và Pyright vốn đã chịu thua trên bề mặt các chuỗi của Django như `ForeignKey` / `related_name` / template. Việc hiển thị tường minh một mức độ `possibly` là sự lựa chọn trung thực thay vì âm thầm loại bỏ những dòng đó. + +Bản thân nơi khai báo sẽ không bị đếm — một field không báo cáo khai báo của chính nó như là một ảnh hưởng. + +## Cách dùng + +```bash +django-orm-lens blast-radius --path . # chỉ rủi ro mức critical +django-orm-lens blast-radius --severity all # mọi thứ +django-orm-lens blast-radius --format markdown # dùng làm PR-comment body +django-orm-lens blast-radius --format github # GitHub PR annotations +django-orm-lens blast-radius --format json # định dạng máy đọc được +django-orm-lens blast-radius --no-cascade # bỏ qua phân tích cascade bổ sung +django-orm-lens blast-radius --only blog/migrations/0002_drop_author.py +``` + +- **`--severity critical|warning|info|all`** (mặc định `critical`) — rủi ro tối thiểu, tương tự `migration-risk`. +- **`--only MIGRATION`** — giới hạn ở các file migration này; lặp lại cờ (flag) cho mỗi file. Truyền vào các đường dẫn thay đổi của một PR để giới hạn báo cáo trong phần diff. Mặc định quét mọi migration trong workspace. +- **`--no-cascade`** — bỏ qua xem trước cascade và bước phân tích workspace bổ sung mà nó cần. Cascade chỉ luôn áp dụng cho các hoạt động cấp độ model. +- **Mã thoát (Exit code)** — `1` khi còn rủi ro critical, `0` nếu không còn. `--exit-zero` luôn thoát bằng mã `0`, hữu ích khi bạn đang từ từ sửa dứt điểm các khoản nợ kỹ thuật cũ. + +## Trong CI + +Là một GitHub Action, các finding sẽ trở thành các PR annotation mà không yêu cầu thêm quyền (permissions) nào: + +```yaml +- uses: FROWNINGdev/django-orm-lens@action-v1 + with: + command: blast-radius + format: github +``` + +Các annotation sẽ trỏ thẳng vào **dòng trong migration** — dòng mã mà người duyệt có thể thao tác — và nêu rõ số lượng tham chiếu trong tiêu đề, để thấy rõ hậu quả mà không cần mở tab thứ hai: + +``` +::error file=blog/migrations/0002_drop_author.py,line=7,title=django-orm-lens: remove_field_still_referenced (2 certain reference(s))::… +``` + +### Dùng làm PR comment + +`comment: true` sẽ gửi báo cáo dưới dạng markdown và **cập nhật chính comment đó** trong các lần push tiếp theo, do vậy một PR với hai mươi lần push sẽ chỉ chứa một báo cáo thay vì hai mươi cái. `only-changed: true` thu hẹp báo cáo vào các migration mà PR này thực sự chạm tới, và thoát sớm (exit early) khi PR không sửa migration nào. + +```yaml +name: Schema review +on: pull_request + +permissions: + contents: read + pull-requests: write # chỉ cần thiết cho `comment: true` + +jobs: + blast-radius: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: FROWNINGdev/django-orm-lens@action-v1 + with: + command: blast-radius + only-changed: true + comment: true + github-token: ${{ github.token }} +``` + +Một số lưu ý về cách hoạt động, để không có gì khiến bạn bất ngờ trong một lần chạy trực tiếp: + +- Comment được tạo **trước** khi job báo fail, do vậy một PR bị block vẫn sẽ mang theo lời giải thích về nguyên nhân. Mã thoát được bảo toàn — rủi ro critical vẫn làm bài kiểm tra fail. +- Danh sách file thay đổi đến từ API chứ không phải từ `git diff`: `actions/checkout` mặc định dùng `fetch-depth: 1`, do đó commit cơ sở không có trong lịch sử git ở local và diff sẽ sai hoặc rỗng. +- Đối với sự kiện `push`, cả hai flag đều bị bỏ qua (kèm theo một thông báo) thay vì làm lệnh fail — vì thế cùng một workflow có thể chạy trên event push mà không cần phải viết điều kiện ngoại lệ (special-casing). +- Việc cập nhật hoạt động bằng cách tìm đoạn mã đánh dấu ``, thứ mà trình kết xuất markdown luôn tạo ra ở dòng đầu tiên. +- `github-token: ${{ github.token }}` là đủ; không cần PAT, không cần App. + +## Số liệu thống kê Production (tùy chọn) + +Tất cả những phân tích bên trên đều là tĩnh (static), điều này để lại một khoảng trống trung thực: mã nguồn không thể phân biệt được một bảng có bốn mươi triệu dòng với một bảng rỗng. Lệnh `migration-risk` tạm thời vượt qua điều này bằng một phương pháp ước lượng (heuristic) — *bất kỳ thứ gì được tạo ra sau file `0001_` đều được giả định là có dữ liệu* — đủ đúng để hữu ích, nhưng cũng đủ sai để gây phiền toái. + +Cờ `--stats` sẽ thu hẹp khoảng trống này, và **django-orm-lens vẫn sẽ không bao giờ kết nối tới một cơ sở dữ liệu**. Bạn tự chạy một câu truy vấn chỉ đọc (read-only) và đưa lại kết quả cho công cụ: + +```bash +django-orm-lens stats-sql > stats.sql +psql -At -d "$DATABASE_URL" -f stats.sql > stats.json # truy xuất trên bản replica là ổn +django-orm-lens blast-radius --path . --stats stats.json +``` + +Khi đó báo cáo sẽ mang theo kích thước thực tế: + +``` +!! blog.post.author [RemoveField] + critical: remove_field_still_referenced (high) blog/migrations/0002_drop_author.py:7 + blog_post: ~41 000 000 rows, 12.0 GB, 4 index(es) (ước tính) +``` + +Tại sao lại dùng một file mà không dùng connection string (chuỗi kết nối): + +- Sẽ không có bất kỳ credential nào đi vào cấu hình CI, như vậy không có gì để rò rỉ. +- Câu truy vấn chỉ là một lần đọc từ `pg_stat_user_tables` cộng với `pg_total_relation_size`. Nó không thực hiện lệnh khóa (locks) nào và không đọc bất kỳ dữ liệu người dùng nào — chỉ tên các bảng và số đếm. +- File `stats.json` có thể được commit, review và diff giống hệt như bất kỳ đầu vào nào khác. + +**Đây chỉ là các con số ước tính, và công cụ luôn nhấn mạnh điều đó.** Trường `n_live_tup` được duy trì bởi công cụ thu thập số liệu và được làm mới (refreshed) nhờ `VACUUM` / `ANALYZE`; quá trình autovacuum sẽ kích hoạt `ANALYZE` sau khi có khoảng 20% các hàng của bảng thay đổi, vì thế một bảng dữ liệu bận rộn sẽ bị sai lệch giữa các lần chạy. Ngay sau khi chạy `ANALYZE`, nó thường chính xác với biên độ sai số chỉ vài phần trăm. Như vậy là quá đủ cho một quyết định mà báo cáo này định hướng: phân biệt giữa bốn mươi triệu hàng và bốn trăm hàng. + +Một bảng dữ liệu bị thiếu trong file `stats.json` sẽ được báo cáo là **unknown**, tuyệt đối không bao giờ báo cáo là rỗng (zero) — một model mà production chưa từng nhìn thấy không thể được hiểu là "an toàn để xóa bỏ". `Meta.db_table` được tuân thủ khi phân giải một model thành tên bảng của nó; nếu không, nguyên tắc `_` mặc định của Django sẽ được áp dụng. + +## Ví dụ + +``` +!! blog.post.author [RemoveField] + critical: remove_field_still_referenced (high) blog/migrations/0002_drop_author.py:7 + Removes field 'author' from 'post' but a field with the same name still exists in the current models.py. + fix: Confirm no code path still reads/writes the field. Deploy the code change first, then run this migration. + still referenced in 5 place(s): 2 certain, 1 likely, 2 possibly + serializers/certain blog/serializers.py:3 fields = ["title", "author"] + views/certain blog/views.py:5 return Post.objects.filter(author__id=request.user.id) + templates/likely blog/templates/blog/post.html:1

{{ post.author }}

+ +summary: 1 target(s), 1 critical risk(s), 2 certain reference(s) +``` + +## Cấu trúc JSON + +```jsonc +{ + "targets": [ + { + "target": "blog.post.author", + "app": "blog", "model": "post", "field": "author", + "operations": ["RemoveField"], + "worstSeverity": "critical", + "risks": [ /* Các đối tượng MigrationRisk, như trong `migration-risk --format json` */ ], + "impact": { + "counts": { "certain": 2, "likely": 1, "possibly": 2 }, + "byLayer": { "views": [ /* các finding */ ] } + }, + "cascade": null + } + ], + "unscannedRisks": [ /* các rủi ro trên các phép toán phi phá hủy (non-destructive) */ ], + "summary": { "targets": 1, "criticalRisks": 1, "certainReferences": 2 } +} +``` + +Các finding về ảnh hưởng sử dụng `line` và `column` **zero-based** (bắt đầu từ số không), khớp với extension VS Code và bộ LSP. Các trình kết xuất dưới dạng văn bản và markdown sẽ cộng thêm một trước khi in ra kết quả, bởi vì con người và các editor thường sử dụng dạng one-based (bắt đầu từ số một). + +## Liên quan + +- [`migration-risk`](migrations.md) — 16 rule đằng sau nửa phân tích rủi ro +- [`nplusone`](nplusone.md) — công cụ phân tích CI thứ hai +- `impact ` — quét tham chiếu riêng lẻ, dành cho khi bạn không muốn xem xét migration diff --git a/docs/i18n/rules/vi/drift.md b/docs/i18n/rules/vi/drift.md new file mode 100644 index 0000000..e31d580 --- /dev/null +++ b/docs/i18n/rules/vi/drift.md @@ -0,0 +1,46 @@ +# drift + +Lệnh `drift` phát hiện khi các định danh cơ sở dữ liệu của model bạn — tên bảng và tên cột — sai lệch so với những gì Django tạo ra theo mặc định. Nó không kết nối với database; nó so sánh `Meta.db_table`, `Field.db_column`, và `Field.column` với cái tên mà Django tự suy ra từ app label và tên field. + +## Nó phát hiện gì + +- `Meta.db_table` khác với `_` +- `db_column` khác với tên trường (viết thường) +- Override `Field.column` sai lệch với mặc định của Django + +Sự sai lệch (drift) không phải lúc nào cũng sai — một tên bảng legacy là một lựa chọn có chủ ý — nhưng sai lệch không được ghi nhận (undocumented) là một mối nguy hiểm cho bảo trì. Công cụ báo cáo nó để bạn có thể xác nhận đó là cố ý và, tùy chọn, bỏ qua nó. + +## Cách dùng + +```bash +django-orm-lens drift --path . # toàn bộ dự án +django-orm-lens drift --path apps/blog # một app cụ thể +django-orm-lens drift --format json # định dạng máy đọc được +django-orm-lens drift --format github # GitHub PR annotations +``` + +- **`--path PATH`** — thư mục gốc để quét (mặc định: thư mục hiện tại). +- **`--format text|json|github|markdown`** (mặc định `text`). +- **`--exit-zero`** — luôn thoát với mã `0`. + +## Ví dụ đầu ra + +``` +blog.Post — db_table: "legacy_posts" (mong đợi "blog_post") +blog.Post.author_id — db_column: "author" (mong đợi "author_id") +``` + +## Bỏ qua (Suppress) + +Đối với sai lệch có chủ ý, thêm một comment trên cùng dòng với khai báo `db_table` hoặc `db_column`: + +```python +class Post(models.Model): + class Meta: + db_table = "legacy_posts" # django-orm-lens-disable drift +``` + +## Liên quan + +- [`migration-risk`](migrations.md) +- [`blast-radius`](blast-radius.md)