add-domain-field

GitHub

指导如何在Tauri Rust应用中为领域模型添加新字段,涵盖类型定义、数据库仓库五处手动SQL修改、迁移脚本编写及前后端同步。

.claude/skills/add-domain-field/SKILL.md openfootmanager/openfootmanager

触发场景

需要向现有数据模型添加新字段 处理SQLite数据库表结构变更与向后兼容

安装

npx skills add openfootmanager/openfootmanager --skill add-domain-field -g -y
更多选项

非标准路径

npx skills add https://github.com/openfootmanager/openfootmanager/tree/develop/.claude/skills/add-domain-field -g -y

不安装直接使用

npx skills use openfootmanager/openfootmanager@add-domain-field

指定 Agent (Claude Code)

npx skills add openfootmanager/openfootmanager --skill add-domain-field -a claude-code -g -y

安装 repo 全部 skill

npx skills add openfootmanager/openfootmanager --all -g -y

预览 repo 内 skill

npx skills add openfootmanager/openfootmanager --list

SKILL.md

Frontmatter
{
    "name": "add-domain-field",
    "description": "Add a field to a domain type so it survives save\/load. Covers serde(default) for backward compatibility, the five positional edit sites in the db repositories, the SQL migration and MIGRATION_COUNT bump, the round-trip test, and the frontend type plus translated label.",
    "when_to_use": "Adding a field to Team, Player, Staff, Manager, League, or any other type in the domain crate; or when a new field works in memory but is empty after saving and reloading.",
    "allowed-tools": "Read, Edit, Write, Grep, Glob, Bash(cargo test*), Bash(cargo clippy*), Bash(cargo fmt*)",
    "argument-hint": "[type and field, e.g. Team.youth_budget]"
}

Adding a domain field

The trap: #[serde(default)] makes old saves load, but it does nothing for SQLite. The repositories in src-tauri/crates/db/src/repositories/ use hand-written positional SQL, so a field that isn't in the column lists is silently dropped on every save/load round trip — with no error, no warning, and a passing test suite.

Do all six steps.


1. The domain type

src-tauri/crates/domain/src/team.rs, player.rs, staff.rs, manager.rs, league.rs, …

pub struct Team {
    // …
    #[serde(default)]
    pub youth_budget: i64,
}

Use #[serde(default = "default_youth_budget")] with a function when zero, empty, or None is the wrong value for an old save. Think about what a save written before this feature existed should look like once loaded — that is what the default has to produce.

Domain types hold data, not logic. Behaviour goes in ofm_core.

2. The repository — five edit sites

Take Team as the worked example; team_repo.rs is the reference for the rest. Open src-tauri/crates/db/src/repositories/team_repo.rs and edit all five:

  1. save_team's INSERT OR REPLACE INTO teams (…) column list — add the column.

  2. Its VALUES (?1, ?2, …, ?N) run — add one more placeholder. The count must match the column list exactly or the insert fails at runtime.

  3. The params![…] list — add the value, in the same position as the column.

  4. row_to_team's positional mappingrow.get(N)?. Appending at the end is safest; inserting in the middle shifts every index after it, and nothing will tell you if you miss one.

  5. Both SELECT column listsload_all_teams and load_team. Two separate strings that must stay in sync. For a column added by a migration, wrap it as COALESCE(youth_budget, <default>) so rows written before the migration read back as the default — the existing JSON columns (media_json, player_roles_json, …) show the pattern.

    <default> must be the same value your serde default produces. These are two independent fallbacks for the same field: serde covers a save loaded from JSON, COALESCE covers a row written before the migration. If step 1 used #[serde(default = "default_youth_budget")] returning 500_000, then COALESCE(youth_budget, 0) makes the same save load differently depending on which path it came through. Either match the two, or give the column a non-null SQL DEFAULT in the migration and drop the COALESCE.

Other repositories in that directory (player_repo.rs, staff_repo.rs, competition_repo.rs, …) follow the same five-site shape.

3. The migration

  • Add src-tauri/crates/db/src/sql/vNNN_<short_description>.sql, numbered after the current highest file.
  • Register it in src-tauri/crates/db/src/migrations.rs (all_migrations()).
  • Bump MIGRATION_COUNT at the top of that file — the migration tests assert on it.

Migrations are append-only and must be idempotent; migrations.rs has a test that re-applies them (test_migrations_are_idempotent). Use ALTER TABLE … ADD COLUMN … DEFAULT …; never rewrite or renumber an existing migration, because shipped saves have already run it.

4. Prove it round-trips

This is the step that catches the dropped-column bug, so write it before the repository edits.

In src-tauri/crates/db/, extend the nearest existing round-trip test: build a value with the new field set to something non-default, save it, load it back, assert the field survived. A test that uses the default value passes even when the column is missing entirely.

cargo test --manifest-path src-tauri/Cargo.toml -p db
cargo test --manifest-path src-tauri/Cargo.toml --workspace

5. The rest of the backend

  • Game logic for the field goes in ofm_core, not domain.
  • If the field should reach the match engine, do not import domain into engine. Add the field to the engine's own mirror type and extend the conversion in ofm_core/turn/.
  • If an AI agent should be able to read or set it, see /add-mcp-tool.

6. The frontend

  • Add the field to the matching TypeScript type (src/store/types.ts or the relevant service types). Optional (?) if old saves may not have it.
  • Any label, unit, or tooltip for it is a user-facing string → /add-ui-string, all 11 locales.

Checklist

  • Field added to the domain type with #[serde(default)] or an explicit default fn
  • Default value is correct for a save written before the field existed
  • Repository INSERT column list updated
  • VALUES (?1…?N) placeholder count updated to match
  • params![…] updated, same order
  • row_to_* positional row.get(N)? updated
  • Both SELECT lists updated, with COALESCE for the migrated column
  • sql/vNNN_*.sql added and registered in migrations.rs
  • MIGRATION_COUNT bumped
  • Round-trip test with a non-default value, written first
  • cargo test --workspace and cargo clippy --workspace --all-targets green
  • Frontend type updated; any new label translated into all 11 locales

版本历史

  • 9d401d6 当前 2026-07-30 22:45

同 Skill 集合

.claude/skills/add-mcp-tool/SKILL.md
.claude/skills/add-tauri-command/SKILL.md
.claude/skills/add-ui-string/SKILL.md
.claude/skills/new-ui-surface/SKILL.md
.claude/skills/preflight/SKILL.md

元信息

文件数
0
版本
3ff2385
Hash
f4a25bba
收录时间
2026-07-30 22:45

首页 - Wiki
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-08-03 23:18
浙ICP备14020137号-1 $访客地图$