# Goose 迁移操作手册 Lyra 数据库迁移的**唯一完整操作手册**。`migrations/` 目录是数据库结构的 唯一正式执行来源;[demo.sql](demo.sql) 仅保留作参考,不要再手工执行它。 迁移目录本身的说明见 [migrations/README.md](../migrations/README.md)。 ## 目录与前提 - 所有 goose 命令都在 Lyra module 根目录(`migrations/` 的上一级)执行: ```bash cd go/projects/lyra # 从仓库根出发 ``` - 目标数据库**已创建**:迁移不负责建库,`lab` 库需先存在。 - 首次迁移要求目标对象**尚不存在**:如果之前手工执行过 demo.sql 建过 同名对象,`up` 会报 `already exists`,先按故障表评估处理。 ## 安装 goose CLI(固定 v3.29.0) goose 只作为独立命令行工具使用(Go 应用尚未引入 goose 依赖): ```bash go install -tags='no_clickhouse no_libsql no_mssql no_mysql no_sqlite3 no_vertica no_ydb' github.com/pressly/goose/v3/cmd/goose@v3.29.0 goose -version # 应输出 v3.29.0 ``` - 固定 `@v3.29.0`,保证各机器行为一致。 - `-tags=...` 是可选的 PostgreSQL 专用精简构建(`no_*` 标签已在 goose v3.29.0 源码中核实);不追求精简可省略。 - 二进制装到 `$(go env GOPATH)/bin`(设了 `GOBIN` 则装到 `GOBIN`)。 `goose -version` 提示找不到命令时,把该目录加入 `PATH`: ```bash export PATH="$PATH:$(go env GOPATH)/bin" # 可写入 ~/.bashrc ``` ## 配置连接:.env.local goose **不读取**应用的 `etc/lyra.toml`,也不会只凭 `PG_PASSWORD` 之类的 应用环境变量自动拼出连接串——它只认自己环境变量里的 `GOOSE_DBSTRING`。 在 module 根创建 `.env.local` 并追加以下内容(**全部是占位值**,换成实际 信息;`lab` 库需已创建): ```bash GOOSE_DRIVER=postgres GOOSE_MIGRATION_DIR=./migrations GOOSE_DBSTRING="postgres://example_user:URL_ENCODED_PASSWORD@127.0.0.1:5432/lab?sslmode=disable" ``` 要点: - `GOOSE_DRIVER=postgres` 是 **goose 的驱动名**;Go 应用内用的是 pgx (驱动名 `pgx`),两边名字不同但都连 PostgreSQL,不要混填。 - 本仓库 `.gitignore` 已忽略 `.env*`,`.env.local` 不会提交;建议再执行 `chmod 600 .env.local` 收紧文件权限。 - `127.0.0.1` 与端口必须换成**实际可达**的地址和端口(远程/容器按实际 映射填写)。 - `sslmode=disable` 仅适用于本地确认无 TLS 要求的场景;远程环境按部署的 TLS 要求调整该参数。 - 本手册所有 goose 命令统一用 `-env=.env.local` 显式加载该文件:goose 默认读的是 `.env`,`-env=.env.local` 既明确来源、又避免误用其他本地 配置。连接信息配置一次即可,无需每次交互输入密码。 - 系统环境里已有的同名变量(如曾 `export` 过 `GOOSE_DBSTRING`)可能优先 于 `.env.local` 生效:改了 `.env.local` 仍行为异常时,先查是否存在同名 环境变量——只列变量名、不打印值,避免泄露连接串: ```bash env | cut -d= -f1 | grep -i '^GOOSE' ``` ## 连接串特殊字符:URL 编码 `GOOSE_DBSTRING` 是 URL 格式:**用户名、密码**中的特殊字符必须逐个做 百分号编码,否则主机名/路径解析会错位。 - 对用户名和密码分别编码,**不要把整串 URL 一起编码**,也不要为迁就 DSN 去修改数据库里的实际密码。避免对已经编码的内容重复编码。 - 不要把真实密码交给在线网站转换;按下表手工替换即可。 | 字符 | 编码 | 字符 | 编码 | |---|---|---|---| | `@` | `%40` | `:` | `%3A` | | `#` | `%23` | `/` | `%2F` | | `%` | `%25` | `?` | `%3F` | 示例:密码 `demo#pass`(演示值)在 DSN 中写成 `demo%23pass`。 ## 常用命令 ```bash # 离线检查迁移文件能否被 goose 解析(注解与语句拆分); # 不连接数据库,也不验证 SQL 在 PostgreSQL 上的语义 goose -env=.env.local validate # goose 工具自身版本(与数据库里的迁移版本是两回事) goose -version # 连接数据库:查看迁移状态 / 当前版本 goose -env=.env.local status goose -env=.env.local version # 执行迁移(连接数据库) goose -env=.env.local up ``` 注意:v3.29.0 的 `validate` **不接受位置路径参数**,迁移目录只由 `GOOSE_MIGRATION_DIR`(`.env.local` 已设 `./migrations`)或 `-dir` 标志 决定。`validate` 通过只证明文件能解析,不证明连接正常或 SQL 语义正确。 ## 首次初始化 前提:`lab` 库已创建,且 00001 中的对象(函数、三张表、索引、触发器) 都不存在——**不要**先手工执行 demo.sql 再 `up`。 ```bash goose -env=.env.local validate # 离线确认文件可解析 goose -env=.env.local status # 显示 00001 Pending:连接成功,该迁移尚未应用 goose -env=.env.local up # 执行 00001,并在 goose_db_version 中记录版本: # set_updated_at() 函数、users/categories/links 三张表、 # 约束、注释、索引与触发器 goose -env=.env.local status # 00001 显示应用时间 goose -env=.env.local version # 显示 version 1 ``` - `up` 可重复执行:已应用版本按 `goose_db_version` 自动跳过。 - **已执行过的迁移文件不再修改**;要变更结构就新增迁移文件(见下节)。 ## 日常新增迁移 ```bash goose -env=.env.local -s create add_example_column sql ``` `-s` 表示顺序编号:在现有 00001 基础上生成 `migrations/00002_add_example_column.sql`(实际编号按目录中现有最大版本 延续)。例如给 users 表加一列,编辑生成的文件: ```sql -- +goose Up ALTER TABLE public.users ADD COLUMN avatar_url text NOT NULL DEFAULT ''; -- +goose Down ALTER TABLE public.users DROP COLUMN avatar_url; ``` 然后按顺序执行: ```bash goose -env=.env.local validate goose -env=.env.local status goose -env=.env.local up goose -env=.env.local status ``` Up/Down 成对编写,保证迁移可回滚。 ## 回滚警告 ```bash goose -env=.env.local down # ⚠ 危险操作,见下 ``` - `down` 只回滚**最近一个**已应用迁移的 Down 部分,**不是**清库或重建的 通用命令。 - **回滚 00001 会删除 `links`/`categories`/`users` 三张表及表中全部数据。** - 执行任何 `down` 之前:阅读对应迁移 SQL、评估数据损失、备份数据库。 不建议随手“试一下” `down`,也不要把它列入日常维护流程。 ## 故障速查 | 报错特征 | 常见原因与处理 | |---|---| | `hostname resolving error`(主机名里混入非法字符) | DBSTRING 格式或 URL 编码不对:检查用户名/密码特殊字符是否已按上表编码、`://` 与 `@` `:` 分隔符是否完整 | | `password authentication failed` | 用户名或密码不对:核对 `.env.local`(编码后)的值;再排查系统环境同名变量是否优先生效(只查变量名,不打印值) | | `connection refused` / `i/o timeout` | 主机端口不可达:确认 PostgreSQL 实际监听地址、端口、容器端口映射与防火墙 | | `already exists`(对象已存在) | 目标对象曾被手工创建(如执行过 demo.sql):先评估现有对象与迁移的差异再决定处理方式;**不要直接删数据**,也不要手工往 `goose_db_version` 插版本记录伪造状态 |