Skip to main content

elf-postgresql

Elf Express PostgreSQL 18 資料庫規範。當任務涉及資料表 / 欄位 / 索引命名(snake_case)、C# 型別對應 PostgreSQL 型別、 timestamptz 與 UTC 時間處理、jsonb、連線字串格式(Npgsql)、本機 docker compose postgres:18、 CodeFirst 結構變更與手寫遷移 SQL、schema 匯出審查、索引設計、備份還原(pg_dump / pg_restore)時觸發。

معلومات المصدر

المستودع
elf-express/skillshare-source
آخر نشاط في المصدر
٢٧ سبتمبر ٢٠٢٦ في ٠١:٢٦
لغة SKILL.md المكتشفة
الصينية
النجوم
٠
التفرعات
٠

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
name
elf-postgresql
description
Elf Express PostgreSQL 18 資料庫規範。當任務涉及資料表 / 欄位 / 索引命名(snake_case)、C# 型別對應 PostgreSQL 型別、 timestamptz 與 UTC 時間處理、jsonb、連線字串格式(Npgsql)、本機 docker compose postgres:18、 CodeFirst 結構變更與手寫遷移 SQL、schema 匯出審查、索引設計、備份還原(pg_dump / pg_restore)時觸發。
metadata
{"version":"1.0.0","owner":"Elf Express"}
# Elf Express PostgreSQL 18 規範(elf-postgresql) 背景:參考專案 `TypingTrainer` 使用 SQLite;團隊標準資料庫為 **PostgreSQL 18**。本 skill 定義資料庫端規則; C# 端寫法見 **elf-sqlsugar**,實體設計見 **elf-domain-modeling**,版本見 **elf-stack**。 本 skill 只寫得出理由的規則;無法驗證的列在「待確認」。 ## 1. 何時使用 - 新增 / 修改資料表、欄位、索引。 - 設定連線字串、本機資料庫、CI 測試資料庫。 - 處理時間、JSON、金額欄位。 - 需要改名、刪欄位、改型別等 CodeFirst 做不到的結構變更。 - 備份、還原、匯出 schema。 ## 2. 固定規則 ### 2.1 命名 1. **MUST** 資料表、欄位、索引名稱全部**小寫 snake_case**。 理由:PostgreSQL 會把未加引號的識別字轉小寫;大小寫混用的名稱之後每次手寫 SQL 都得加雙引號。 2. **MUST** 表名為複數:`orders`、`order_lines`。由 C# `[SugarTable("OrderLines")]` 經 SqlSugar `ToUnderLine` 自動產生(elf-sqlsugar §2.1)。 3. **MUST** 主鍵欄位 `id`;外鍵欄位 `<單數表名>_id`(`order_id`、`company_id`)。 4. **MUST** 索引命名:唯一 `ux_<table>_<cols>`、一般 `ix_<table>_<cols>`、partial / 特殊用途同規則再加描述(`ux_customers_company_code_active`)。 5. **MUST NOT** 使用 PostgreSQL 保留字當名稱(`user`、`order`、`group`、`table`);表名因為是複數(`users`、`orders`)通常可避開,欄位遇到時改名(`order` → `sort_order`)。 6. **MUST** 使用預設 schema `public`(多 schema 規劃「待確認」)。 ### 2.2 型別對應 | C# | SqlSugar 標註 | PostgreSQL | |----|---------------|------------| | `int` | — | `integer` | | `int` 主鍵 | `IsPrimaryKey = true, IsIdentity = true` | `integer generated ... as identity`(`PostgresIdentityStrategy.Identity`) | | `long` | — | `bigint` | | `bool` | — | `boolean` | | `string`(有上限) | `Length = n` | `varchar(n)` | | `string`(無上限) | `ColumnDataType = "text"` | `text` | | `decimal`(金額) | `Length = 18, DecimalDigits = 2` | `numeric(18,2)` | | `decimal`(單價、可小數的數量) | `Length = 18, DecimalDigits = 4` | `numeric(18,4)` | | `decimal`(匯率、比率) | `Length = 18, DecimalDigits = 6` | `numeric(18,6)` | | `DateTime`(時間點) | `ColumnDataType = "timestamptz"` | `timestamptz` | | enum | — | `integer`(成員明寫數值,見 elf-domain-modeling) | | 物件 / 集合 | `IsJson = true, ColumnDataType = "jsonb"` | `jsonb` | | `Guid` | — | `uuid` | 7. **MUST** 金額用 `numeric`,**MUST NOT** 用 `real` / `double precision`(浮點誤差)。 8. **MUST** JSON 用 `jsonb`,**MUST NOT** 用 `json` 或把 JSON 存成 `text`(`jsonb` 可建 GIN 索引、可用 `@>` 查詢)。 9. **MUST NOT** 用 `varchar` 無長度;要無上限就明確用 `text`。 ### 2.3 時間 10. **MUST** 表示「某個時刻」的欄位一律 `timestamptz`,應用程式寫入 **UTC**(`DateTime.UtcNow`)。 11. **MUST** 連線字串帶 `Timezone=UTC`,讓 session 時區為 UTC。 理由:SqlSugar 傳 `DateTime` 參數時,PostgreSQL 若把它當 `timestamp` 轉成 `timestamptz`,會依 session 時區解讀;session 為 UTC 才不會位移。 12. **MUST** 從資料庫讀出的 `DateTime` 在轉成 DTO 前經過 `UtcTime.Normalize(...)`(§3.4)。 理由:目前 SqlSugarCore 帶入的 **Npgsql 5.0.18** 讀 `timestamptz` 會依 **.NET 行程所在時區**回傳本機時間(`Kind = Local`); Windows 開發機(UTC+8)與 UTC 容器會得到不同值。`Normalize` 在兩種情況下都回傳正確的 UTC。 13. **MUST** API 容器設定 `TZ=UTC`(elf-cicd-docker 的 `api.Dockerfile` 與 compose 已設定);注意 Windows 上 .NET **不讀** `TZ` 環境變數,所以規則 12 不可省略。 14. 只有「日期」沒有時刻的欄位(生日、會計日)用 `date`(C# `DateOnly` 支援度「待確認」,先以 `DateTime` + `ColumnDataType = "date"`)。 ### 2.4 索引與約束 15. **MUST** 每個外鍵欄位建立索引。理由:PostgreSQL **不會**自動為外鍵欄位建索引,join 與父列刪除都會掃全表。 16. **MUST** 租戶資料表的索引以 `company_id` 為第一欄;租戶內唯一鍵包含 `company_id`。 17. **MUST** 軟刪除表若需「刪除後可重建同代碼」,唯一索引用 partial index:`... WHERE is_deleted = false`(手寫 SQL,§2.5)。 18. **MUST** `jsonb` 的 GIN 索引依下列規則決定,**MUST NOT** 預先為所有 `jsonb` 建索引: - 程式碼中**有**以 `@>`(包含)查詢該欄位 → 建 `USING gin (<col> jsonb_path_ops)`; - 有以 `?` / `?|` / `?&`(鍵存在)查詢 → 建 `USING gin (<col>)`(預設 `jsonb_ops`,`jsonb_path_ops` 不支援這些運算子); - 只以單一路徑等值查詢(`<col> ->> 'key' = @v`)→ 不建 GIN,改建運算式 B-tree 索引 `((<col> ->> 'key'))`; - 沒有上述任何查詢 → 不建。 GIN / 運算式索引 CodeFirst 建不出來,一律寫手寫遷移 SQL(§2.5、§3.5)。依 elf-domain-modeling 規則 29,出現這類查詢時應先考慮把該鍵改成一般欄位。 19. 注意:SqlSugar CodeFirst **不建立外鍵約束**(參考專案產生的 DDL 中沒有任何 FOREIGN KEY);是否以手寫 SQL 補外鍵約束「待確認」。 ### 2.5 結構變更(遷移) 20. **MUST** 新增表 / 新增欄位 / 一般索引:交給 CodeFirst(`db.CodeFirst.InitTables(Entities)`,見 elf-sqlsugar)。 CodeFirst 只在**非 Production** 啟動時自動執行;Production 由部署步驟以 owner 帳號 + `Database__RunCodeFirst=true` 一次性執行(elf-sqlsugar references §2.1),應用程式平常的帳號沒有 DDL 權限(規則 28)。 21. **MUST** 欄位改名:`[SugarColumn(OldColumnName = "old_snake_name")]`,上線並確認後再移除該標註。 22. **MUST NOT** 期待 CodeFirst 刪欄位、改型別、縮短長度、建 partial / GIN 索引、建外鍵約束;這些寫成**手寫 SQL 腳本**: `server/db/migrations/<yyyyMMdd>_<nn>_<snake_description>.sql`,腳本必須可重複執行(`IF NOT EXISTS` / `IF EXISTS`)。 23. **MUST** 每次實體有變動,重新匯出 schema 並 commit:`server/db/schema.sql`(§3.3 指令)。PR review 以此檔 diff 審查結構變更(參考專案 `npm run db:schema` 的作法)。該檔**只能由指令產生,不可手改**。 24. **MUST NOT** 在正式環境讓應用程式執行 `CREATE DATABASE`;`DbMaintenance.CreateDatabase()` 只在 Development 執行(elf-sqlsugar references)。 ### 2.6 連線與帳號 25. **MUST** 連線字串格式(Npgsql): `Host=<host>;Port=5432;Database=<app>;Username=<app>;Password=<secret>;Timezone=UTC` 26. **MUST** 連線字串經 `ConnectionStrings__Default` 環境變數注入;只有本機 docker 開發帳密可以出現在 `appsettings.json`(elf-dotnet §2.4)。 27. **MUST** 資料庫名稱與帳號名稱 = 產品代號小寫(`acme`)。 28. **MUST NOT** 在非本機環境以 `postgres` 超級使用者連線。Production 分兩個帳號: - **owner 帳號**(擁有 schema,可 DDL):只給部署步驟使用(CodeFirst 一次性執行、手寫遷移 SQL); - **應用程式帳號**(`ConnectionStrings__Default` 平常用的):只有資料表的 `SELECT / INSERT / UPDATE / DELETE`,**沒有** `CREATE` 與任何 DDL 權限。 確切 GRANT 腳本與是否用多 schema 仍「待確認」。 ### 2.7 備份 29. **MUST** 邏輯備份使用自訂格式:`pg_dump -Fc`;還原用 `pg_restore`(§3.6)。 30. **MUST** 備份檔不得放在與資料庫同一個 volume。 31. 排程、保留期限、是否啟用 WAL 歸檔 / PITR:「待確認」。 ## 3. 標準範本 ### 3.1 本機開發 `docker/docker-compose.dev.yml` 只給本機開發用(只有資料庫,API 在 IDE 跑)。服務名與帳密來源和部署 compose 一致:服務 `postgres`、帳密取自 `docker/.env` 的 `POSTGRES_*`。 ```yaml # Local PostgreSQL for development only (the API runs from the IDE). # Run from the repo root; docker/.env is git-ignored (copy docker/.env.example): # docker compose --env-file docker/.env -f docker/docker-compose.dev.yml up -d services: postgres: image: postgres:18 restart: unless-stopped environment: POSTGRES_DB: ${POSTGRES_DB:?set POSTGRES_DB in docker/.env} POSTGRES_USER: ${POSTGRES_USER:?set POSTGRES_USER in docker/.env} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in docker/.env} TZ: UTC ports: - '5432:5432' volumes: # postgres:18 images keep data under /var/lib/postgresql/18/docker; # mount the parent so the major-version subdirectory is included. - pgdata:/var/lib/postgresql healthcheck: test: ['CMD-SHELL', 'pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}'] interval: 10s timeout: 3s retries: 5 volumes: pgdata: ``` 本機的 `docker/.env` 填本機開發值(非機密),與 `appsettings.json` 的本機連線字串一致: ```ini POSTGRES_DB=acme POSTGRES_USER=acme POSTGRES_PASSWORD=acme_dev ``` ```json "ConnectionStrings": { "Default": "Host=localhost;Port=5432;Database=acme;Username=acme;Password=acme_dev;Timezone=UTC" } ``` 本機值若不同,以環境變數 `ConnectionStrings__Default` 覆寫,不要改 commit 進去的 `appsettings.json`。 ### 3.2 部署(API + DB 一起跑) 部署用 compose **只有一份**:elf-cicd-docker 的 `docker/docker-compose.yml`(服務 `postgres` / `api` / `web`,帳密同樣來自 `docker/.env` 的 `POSTGRES_*`, api 連線字串 `Host=postgres;...;Timezone=UTC`、api 容器 `TZ=UTC`)。本 skill 不另提供部署 compose;資料庫端規則(volume 路徑、healthcheck、帳號)以本 skill 為準。 ### 3.3 匯出 schema(review 用) ```bash docker compose --env-file docker/.env -f docker/docker-compose.dev.yml exec -T postgres \ pg_dump -U acme -d acme --schema-only --no-owner --no-privileges \ > server/db/schema.sql ``` 先啟動 API 一次(讓 CodeFirst 建好表)再匯出。檢查重點:表 / 欄位 / 索引皆 snake_case、時間欄位為 `timestamp with time zone`、JSON 欄位為 `jsonb`。 ### 3.4 `UtcTime.Normalize`(放 `server/src/<App>.Api/Data/UtcTime.cs`) ```csharp namespace Acme.Api.Data; /// <summary> /// Npgsql 5 returns timestamptz values converted to the process's local zone (Kind = Local). /// Everything this app writes is UTC, so Unspecified is treated as UTC too. /// </summary> public static class UtcTime { public static DateTime Normalize(DateTime value) => value.Kind switch { DateTimeKind.Utc => value, DateTimeKind.Local => value.ToUniversalTime(), _ => DateTime.SpecifyKind(value, DateTimeKind.Utc), }; public static DateTime? Normalize(DateTime? value) => value is null ? null : Normalize(value.Value); } ``` 使用:`new OrderDto(o.Id, o.Name, o.Quantity, UtcTime.Normalize(o.CreatedAt))`。 ### 3.5 手寫遷移腳本範例 `server/db/migrations/20260927_01_customers_code_partial_unique.sql` ```sql -- Allow a soft-deleted customer's code to be reused within the same company. DROP INDEX IF EXISTS ux_customers_company_code; CREATE UNIQUE INDEX IF NOT EXISTS ux_customers_company_code_active ON customers (company_id, code) WHERE is_deleted = false; ``` > 若改用 partial index,須同時移除實體上的 `[SugarIndex("ux_customers_company_code", ...)]`,否則下次啟動 CodeFirst 會把舊索引建回來。 ### 3.6 備份與還原 ```bash # Backup (custom format, compressed) docker compose --env-file docker/.env -f docker/docker-compose.dev.yml exec -T postgres \ pg_dump -U acme -d acme -Fc > backup/acme_$(date +%Y%m%d_%H%M).dump # Restore into an empty database docker compose --env-file docker/.env -f docker/docker-compose.dev.yml exec -T postgres \ pg_restore -U acme -d acme --clean --if-exists --no-owner < backup/acme_20260927_0300.dump ``` ## 4. 檢查清單 - [ ] 表 / 欄位 / 索引名稱皆小寫 snake_case(以 `schema.sql` 確認) - [ ] 表名複數;外鍵 `<x>_id` 且有索引 - [ ] 時間欄位 `timestamptz`;連線字串含 `Timezone=UTC`;讀出轉 DTO 時經 `UtcTime.Normalize` - [ ] 金額 `numeric(18,2)`;JSON 為 `jsonb`;沒有無長度 `varchar` - [ ] 租戶表索引以 `company_id` 開頭 - [ ] CodeFirst 做不到的變更已寫成可重複執行的 `server/db/migrations/*.sql` - [ ] `server/db/schema.sql` 已重新匯出並 commit - [ ] 本機 / CI 使用 `postgres:18`,volume 掛在 `/var/lib/postgresql` - [ ] 沒有把正式密碼寫進 compose 或 appsettings(compose 用 `${POSTGRES_PASSWORD:?}` 取自 `docker/.env`;API 用 `ConnectionStrings__Default` 環境變數) - [ ] 本機 / 部署 compose 的資料庫服務名都是 `postgres` - [ ] `jsonb` GIN 索引只在有 `@>` / `?` 查詢時建立(規則 18) - [ ] Production 應用程式帳號沒有 DDL 權限;CodeFirst 只由部署步驟以 owner 帳號執行 - [ ] 整合測試跑在真的 PostgreSQL 18(不可用 SQLite 代替),涵蓋時間讀寫往返 ## 5. 常見錯誤 | 錯誤 | 正確 | |------|------| | 表名 `"OrderLines"`(帶引號、大小寫混用) | `order_lines` | | 欄位 `createdat`(只轉小寫、沒加底線) | 設定 ToUnderLine → `created_at` | | `DateTime.Now` 寫入 | `DateTime.UtcNow` | | 讀出的時間直接序列化給前端,開發機差 8 小時 | `UtcTime.Normalize` | | 連線字串沒有 `Timezone=UTC` | 加上 | | `volumes: - pgdata:/var/lib/postgresql/data`(沿用 17 以前的寫法) | `postgres:18` 掛 `/var/lib/postgresql` | | `image: postgres:latest` | `postgres:18` | | 刪掉實體屬性,以為欄位會被刪 | CodeFirst 不刪欄位;寫遷移 SQL | | 外鍵欄位沒有索引 | PostgreSQL 不會自動建,必須加 `[SugarIndex]` | | 金額用 `double` | `decimal` + `numeric(18,2)` | | 用 SQLite in-memory 測 PostgreSQL 行為 | 用 Testcontainers 啟動的 `postgres:18`(elf-integration) | | 本機 compose 服務叫 `db`、帳密寫死在 yml | 服務 `postgres`,帳密取自 `docker/.env` 的 `POSTGRES_*` | | 為每個 `jsonb` 欄位先建 GIN 索引 | 依規則 18:有 `@>` / `?` 查詢才建 | ## 6. 待確認 - 遷移腳本的執行方式(部署流程手動執行、啟動時由程式執行、或採用 migration 工具)與版本紀錄表。 - 是否以手寫 SQL 補外鍵約束(CodeFirst 不建立)。 - 正式環境帳號的確切 GRANT 腳本、是否使用多個 schema(owner / 應用程式帳號分離本身已決定,見規則 28)。 - 備份排程、保留期限、WAL 歸檔 / PITR、還原演練頻率;託管平台(自架或雲端)。 - `postgres:18` 是否釘 minor 或 OS 變體(見 elf-stack)。 - 是否升級 Npgsql(6+ 的時間行為不同,規則 11–13 需重新驗證;SqlSugar 在 Npgsql 6+ 會預設開啟 `Npgsql.EnableLegacyTimestampBehavior`)。 - `DateOnly` / `TimeOnly` 在 SqlSugarCore 5.1.4.x + Npgsql 5 的支援度。 - 連線池大小(`Maximum Pool Size`)等效能參數的團隊預設值。 - 本 skill 的 postgres:18 volume 路徑與 Npgsql 5 時間行為是依官方文件 / 版本說明撰寫,尚未在團隊環境實測;首次導入時以整合測試確認。
عرض على GitHub