feat(lyra): 接入 PostgreSQL 与 Goose 迁移
- 使用 pgx/sqlx 建立连接,从环境变量读取数据库密码 - 添加本地 PostgreSQL Compose 配置与初始迁移 - 补充 Goose 操作手册和连接串排错说明
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
## 已确认技术栈
|
||||
|
||||
- 已引入:Gin v1.12.0(HTTP 路由)、Viper v1.21.0(配置读取,TOML)。`go.sum` 已由 Go 工具生成。
|
||||
- 目标选型(选型已确认,尚未作为代码依赖引入):PostgreSQL、Squirrel + sqlx(pgx stdlib 驱动)、Goose(数据库迁移)。
|
||||
- 目标选型(选型已确认,尚未作为代码依赖引入):PostgreSQL、Squirrel + sqlx(pgx stdlib 驱动)、Goose(数据库迁移;迁移 SQL 已就位,以独立 goose CLI v3.29.0 执行,Go 依赖未引入)。
|
||||
|
||||
## 当前进度
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
- 已注册 `GET /hello`:`internal/router/router.go` 的 `RegisterRoutes` 将其指向 `handler.Hello`,经 `httpx.OK` 输出 `api.Response`;路由注册配有 httptest 回归测试,不监听实际端口、不连接数据库。
|
||||
- 已定义公共响应契约:`api.Response`(success/data/error)、`api.ErrorInfo`(code/message/debug)与错误码类型 `api.ErrorCode` 及其默认文案 `Message()`。
|
||||
- 已实现响应输出辅助 `httpx.OK`、`httpx.Fail`。
|
||||
- 尚未实现:真实业务逻辑(`/hello` 只是 HTTP 通路示例)、数据库访问与迁移 SQL;配置校验、环境变量覆盖与默认值;`ErrorInfo.Debug` 的填充与按环境开关。日志方案未定。
|
||||
- 数据库迁移已就位:`migrations/00001_init.sql`(`set_updated_at()` 函数、users/categories/links 三张表、约束、注释、索引与触发器,建库内容与 [demo.sql](demo.sql) 一致),由手工 goose CLI(v3.29.0)执行,见 [../migrations/README.md](../migrations/README.md)。
|
||||
- 尚未实现:真实业务逻辑(`/hello` 只是 HTTP 通路示例)、Go 应用侧数据库访问与 Goose Go 依赖;配置校验、环境变量覆盖与默认值;`ErrorInfo.Debug` 的填充与按环境开关。日志方案未定。
|
||||
|
||||
## 目录结构
|
||||
|
||||
@@ -52,7 +53,7 @@ lyra/
|
||||
│ └── response.go # 公共响应输出辅助(OK/Fail)
|
||||
├── etc/
|
||||
│ └── lyra.toml # 运行配置(App/HTTP)
|
||||
├── migrations/ # Goose SQL 迁移文件(暂无迁移,仅 README)
|
||||
├── migrations/ # Goose SQL 迁移文件(00001_init.sql 初始迁移 + README)
|
||||
├── docs/
|
||||
│ ├── plans/
|
||||
│ └── architecture.md # 本文
|
||||
@@ -62,7 +63,7 @@ lyra/
|
||||
```
|
||||
|
||||
- 标注“待新增”的文件只是职责分布示例,随实现按需新增,不预建空文件或空目录;`internal/` 各目录均以 `AGENTS.md` 记录职责与依赖约束。
|
||||
- model 下的 `link*.go` 展示一个资源在各层的文件分布;分类、设置等资源按同样方式在 api/handler/service/model 新增对应文件。具体迁移文件名随首个迁移确定。
|
||||
- model 下的 `link*.go` 展示一个资源在各层的文件分布;分类、设置等资源按同样方式在 api/handler/service/model 新增对应文件。首个迁移已落地为 `migrations/00001_init.sql`。
|
||||
- `handler/polaris/`、`service/polaris/` 仅示意业务分包的形态:实际分包与否随业务边界明确时决定,路由、api、model 不照搬同样的分包。
|
||||
|
||||
## 组织原则
|
||||
@@ -92,6 +93,6 @@ lyra/
|
||||
- 配置校验、环境变量覆盖与默认值:尚未实现(当前无任何校验、覆盖或默认值)。
|
||||
- `ErrorInfo.Debug` 的填充与按环境(dev)开关:尚未实现,目前仅有字段。
|
||||
- 分页响应约定(如 data/meta 结构)与统一错误处理中间件:尚未引入,仅为历史讨论。
|
||||
- Goose 的部署执行入口与并发控制(串行执行或数据库锁)方案。
|
||||
- Goose 迁移的部署执行入口已确定为手工 CLI(见 [../migrations/README.md](../migrations/README.md)),并发控制以“单操作者串行执行”为约定,数据库锁未引入,服务启动不自动迁移。
|
||||
- PostgreSQL 按项目划分 schema 的做法(组织边界,不等于权限隔离)。
|
||||
- 日志方案(候选标准库 slog)与中间件集成方式。
|
||||
|
||||
@@ -71,16 +71,12 @@ CREATE TABLE public.links (
|
||||
pinned_sort integer,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT links_category_fk
|
||||
FOREIGN KEY (category_id) REFERENCES public.categories (id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT links_category_fk FOREIGN KEY (category_id) REFERENCES public.categories (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT links_title_not_empty CHECK (btrim(title) <> ''),
|
||||
CONSTRAINT links_url_not_empty CHECK (btrim(url) <> ''),
|
||||
CONSTRAINT links_pinned_sort_consistent CHECK (pinned = (pinned_sort IS NOT NULL)),
|
||||
CONSTRAINT links_keywords_one_dimension
|
||||
CHECK (cardinality(keywords) = 0 OR array_ndims(keywords) = 1),
|
||||
CONSTRAINT links_keywords_no_null_elements
|
||||
CHECK (array_position(keywords, NULL) IS NULL)
|
||||
CONSTRAINT links_keywords_one_dimension CHECK (cardinality(keywords) = 0 OR array_ndims(keywords) = 1),
|
||||
CONSTRAINT links_keywords_no_null_elements CHECK (array_position(keywords, NULL) IS NULL)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE public.links IS '链接表,归属分类';
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
# 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` 插版本记录伪造状态 |
|
||||
@@ -0,0 +1,71 @@
|
||||
# Goose 初始迁移
|
||||
|
||||
日期:2026-10-17
|
||||
|
||||
## 目标与范围
|
||||
|
||||
用户已同意创建首个 Goose 迁移并提供后续自行管理的说明。以当前工作区的
|
||||
`docs/demo.sql` 为来源,保留已有未提交改动,不修改其表结构设计。
|
||||
本次不连接或修改用户数据库,不把迁移嵌入服务启动,也不修改 Go 应用依赖。
|
||||
|
||||
## 实施步骤
|
||||
|
||||
1. 核对 Goose 官方 SQL 注解、事务行为及 CLI 命令。
|
||||
2. 新建 `migrations/00001_init.sql`,Up 保留示例中的函数、三张表、约束、
|
||||
注释、索引与触发器;去掉外层事务,以 StatementBegin/StatementEnd
|
||||
包裹 PL/pgSQL 函数。Down 按依赖逆序删除表,最后删除函数,不使用 CASCADE。
|
||||
3. 更新 `migrations/README.md`,说明 CLI 使用、连接信息的安全提供、首次
|
||||
初始化、状态检查、后续迁移及回滚风险。采用固定 CLI 版本 v3.29.0,命令在 Lyra
|
||||
module 根执行;数据库必须已创建,首个迁移要求目标对象尚不存在。
|
||||
4. 同步项目 README 与架构文档的迁移进度。`docs/demo.sql` 保留为参考,
|
||||
正式迁移以 migrations 为准,避免双重执行。
|
||||
|
||||
## 工作分工与验证
|
||||
|
||||
- librarian:只读核对官方规则与 CLI 能力。
|
||||
- fixer:仅负责首个 SQL 迁移及上述三个说明文档;不修改其他代码或配置,
|
||||
不运行任何连接数据库的命令。
|
||||
- 父会话:维护计划、检查最终 diff 与文案,确认无意外改动及敏感信息。
|
||||
- fixer 验证:静态对比 Up 与当前 demo 的 SQL 内容,检查 Down 依赖顺序;
|
||||
使用 v3.29.0 的 `goose -env=none -dir ./migrations validate` 检查文件解析,
|
||||
此命令不连接数据库。校验用工具仅构建到 `/tmp/opencode`,不修改应用依赖。
|
||||
- 不为仅 SQL/文档变更重复运行无关 Go HTTP 测试。不宣称数据库集成验证通过;
|
||||
未在 PostgreSQL 实际执行的限制在交付时明确说明。
|
||||
|
||||
## 已核对的规则
|
||||
|
||||
- 官方 v3.29.0:Up/Down 默认使用事务;函数体需 StatementBegin/StatementEnd。
|
||||
- 后续新建迁移使用 `goose -s create <名称> sql`,延续顺序编号。
|
||||
- CLI 默认读取 `.env`;说明中的命令统一加 `-env=none`,显式使用环境变量,
|
||||
避免误用本地配置。`-version` 为工具版本,`version` 为数据库版本。
|
||||
- 官方来源:https://pressly.github.io/goose/documentation/annotations/
|
||||
与 https://github.com/pressly/goose/releases/tag/v3.29.0。
|
||||
|
||||
## 实施与验收记录
|
||||
|
||||
- 已完成初始迁移与使用文档;父会话已检查 SQL 与相关文档差异。
|
||||
- 实测修正:v3.29.0 的 validate 不使用位置参数,必须通过 `-dir` 或
|
||||
`GOOSE_MIGRATION_DIR` 选择目录;已按实际行为编写说明。
|
||||
- fixer 已构建 v3.29.0 PostgreSQL 专用 CLI 至 `/tmp/opencode/lyra-goose-bin`,
|
||||
离线解析得到 Up=40、Down=4;与 demo 的 Up 内容逐行对比完全一致。
|
||||
- 后续编号 00002 已在临时目录验证;没有修改应用依赖或连接数据库。
|
||||
- 最终文案校正:目标库为空是执行前提,不当作已验证事实;普通 down 是否
|
||||
丢数据取决于迁移内容,00001 的 Down 则确定会删除三张表及其数据。
|
||||
- `git diff --check` 通过。PostgreSQL 上的实际执行尚未验证。
|
||||
|
||||
## 后续:整理日常操作手册
|
||||
|
||||
用户已要求将实际操作步骤写入 Lyra docs。以下为新增文档任务,不改已有迁移。
|
||||
|
||||
1. 新建 `docs/goose.md`,作为唯一完整操作手册:固定版本安装、项目根目录、
|
||||
`.env.local` 配置、密码 URL 编码、validate/status/up/version、后续新增迁移、
|
||||
回滚警告与常见故障。所有数据库操作命令使用 `-env=.env.local`。
|
||||
2. 将 `migrations/README.md` 收敛为迁移目录说明和手册链接,移除旧的交互输入
|
||||
密码流程,避免两份操作步骤不一致;项目 README 增加手册入口。
|
||||
3. 仅使用演示占位值,不读取 .env.local、真实连接配置或既有错误中的疑似凭据。
|
||||
不运行服务、不连接数据库、不更改 SQL、依赖和配置,也不暂存用户改动。
|
||||
4. fixer 负责上述三个文档和静态链接/命令一致性检查;父会话负责最终差异与
|
||||
文案审查。已有 CLI 验证证据复用,不为文档改动重复构建或执行数据库命令。
|
||||
|
||||
说明:用户已确认 status 成功并显示 00001 Pending;这是连接及迁移发现成功的
|
||||
证据,不足以宣称 Up 或 Down 已完成数据库集成验证。
|
||||
@@ -0,0 +1,32 @@
|
||||
# PostgreSQL 启动连接检查
|
||||
|
||||
日期:2026-10-17
|
||||
|
||||
## 已批准目标
|
||||
|
||||
用户要求在 main.go 中通过 sqlx 查询数据库信息,检查尚无业务表的数据库连接。
|
||||
查询只读取 current_database()、current_user、version(),不依赖业务表,不执行 DDL。
|
||||
|
||||
## 实施范围
|
||||
|
||||
1. 保留当前工作区所有用户改动,仅在 cmd/lyra/main.go 中注册项目已选定的
|
||||
pgx stdlib 驱动,使用带超时的连接及 GetContext 查询,输出数据库信息。
|
||||
2. 通过 go.mod/go.sum 加入 pgx/v5/stdlib 所需依赖;etc/lyra.toml 仅将
|
||||
driver 的 postgres 改为 pgx,与驱动注册名保持一致,不读出或改动敏感配置。
|
||||
3. 补充数据库连接关闭;不重构配置加载、不实现 dotenv、不涉及 Goose 或业务表。
|
||||
|
||||
## 分工与证据路径
|
||||
|
||||
- fixer:写入范围限定 main.go、go.mod、go.sum、TOML 的 driver 一行;
|
||||
首次修改检查缩进和换行,保留已有修改。负责 gofmt、go test ./... 与构建验证。
|
||||
- 父会话:检查新增查询、超时、驱动名与范围,检查无明文凭据及意外文件改动。
|
||||
- 不打印配置文件完整内容或完整 DSN;不读取 .env.local 或 compose 中的敏感值。
|
||||
- 不运行服务、不连接真实数据库;编译/测试只证明查询代码可编译和现有测试通过,
|
||||
实际连接是否成功由用户本地启动后观察查询输出确认。
|
||||
|
||||
## 验收
|
||||
|
||||
- 已加入 pgx/v5 v5.11.0 驱动、五秒超时连接与系统信息查询,保留 HTTP 通路。
|
||||
- fixer 执行 `go build ./...`、`go test ./...` 均通过,未连接真实数据库。
|
||||
- 最终安全校正:连接与查询失败时只记录固定的阶段提示,不直接输出驱动原始
|
||||
错误,避免连接串解析错误意外携带敏感配置。
|
||||
Reference in New Issue
Block a user