From 14aa60b6985e6b1e40023e48d7bf27814ed6cf24 Mon Sep 17 00:00:00 2001 From: weichen Date: Mon, 5 Oct 2026 02:52:36 +0000 Subject: [PATCH] Bind booleans as bool for the postgres dialect in auth and conversations Why: user create/reset-password and conversation flag updates wrote 1/0 literals and boolInt ints into BOOLEAN columns; SQLite tolerates it, PostgreSQL rejects with 42804. The 10-04 user create on pg1 failed midway (empty password_hash, is_admin=f), and password reset, user list (bool scanned into int) and login (GetUserAuthState scan) were all broken on the postgres tier. Evidence: store tests green; against the live pg1 the fixed CLI completes user reset-password and user list (isAdmin=true, mustChangePassword=true, hasPassword=true). Impact: auth/account management and conversation list/flag paths work on postgres; SQLite unchanged (0/1 converts via driver.Bool). No schema changes. Refs: dev-repo PR for gitlink bump (feature/store-pg-bool-dialect) --- internal/store/auth.go | 23 ++++++++++------------- internal/store/projects.go | 24 ++++++++---------------- 2 files changed, 18 insertions(+), 29 deletions(-) diff --git a/internal/store/auth.go b/internal/store/auth.go index 84b1684..66cb233 100644 --- a/internal/store/auth.go +++ b/internal/store/auth.go @@ -226,15 +226,17 @@ func (s *Store) CreateAccount(owner, name, passwordHash string, isAdmin bool) er if err := s.EnsureUser(owner, name); err != nil { return err } - _, err := s.db.Exec(s.q(`UPDATE users SET name=?, password_hash=?, password_set_at=?, must_change_password=1, is_admin=? WHERE owner=?`), - name, passwordHash, nowISO(), boolInt(isAdmin), owner) + // 布尔列一律绑 Go bool 参数、不写 1/0 字面量:SQLite 两侧都容忍, + // PostgreSQL 对 boolean 列收到 integer 会 42804(2026-10-05 user create 半失败根因)。 + _, err := s.db.Exec(s.q(`UPDATE users SET name=?, password_hash=?, password_set_at=?, must_change_password=?, is_admin=? WHERE owner=?`), + name, passwordHash, nowISO(), true, isAdmin, owner) return err } // SetUserPassword 设置/重置密码:must_change=1(首登强制改密)。 func (s *Store) SetUserPassword(owner, passwordHash string) error { - res, err := s.db.Exec(s.q(`UPDATE users SET password_hash=?, password_set_at=?, must_change_password=1 WHERE owner=?`), - passwordHash, nowISO(), owner) + res, err := s.db.Exec(s.q(`UPDATE users SET password_hash=?, password_set_at=?, must_change_password=? WHERE owner=?`), + passwordHash, nowISO(), true, owner) if err != nil { return err } @@ -246,7 +248,7 @@ func (s *Store) SetUserPassword(owner, passwordHash string) error { // ClearMustChangePassword 改密完成后清标志(改密者会话保留,其余吊销由调用方处理)。 func (s *Store) ClearMustChangePassword(owner string) error { - _, err := s.db.Exec(s.q(`UPDATE users SET must_change_password=0 WHERE owner=?`), owner) + _, err := s.db.Exec(s.q(`UPDATE users SET must_change_password=? WHERE owner=?`), false, owner) return err } @@ -263,17 +265,15 @@ type UserAuthState struct { func (s *Store) GetUserAuthState(owner string) (UserAuthState, error) { var st UserAuthState var hash, ca string - var mustChange, isAdmin int + // 直接扫进 bool:PG 返回原生 boolean;SQLite 整数 0/1 经 driver.Bool 转换同样成立。 err := s.db.QueryRow(s.q(`SELECT owner,COALESCE(name,''),password_hash,must_change_password,is_admin,created_at FROM users WHERE owner=?`), owner). - Scan(&st.Owner, &st.Name, &hash, &mustChange, &isAdmin, &ca) + Scan(&st.Owner, &st.Name, &hash, &st.MustChangePassword, &st.IsAdmin, &ca) if errors.Is(err, sql.ErrNoRows) { return st, ErrNotFound } if err != nil { return st, err } - st.MustChangePassword = mustChange == 1 - st.IsAdmin = isAdmin == 1 st.HasPassword = hash != "" return st, nil } @@ -350,12 +350,9 @@ func (s *Store) ListUsersAdmin() ([]AdminUser, error) { for rows.Next() { var u AdminUser var hash, psa, ca string - var mustChange, isAdmin int - if err := rows.Scan(&u.Owner, &u.Name, &hash, &mustChange, &isAdmin, &psa, &ca); err != nil { + if err := rows.Scan(&u.Owner, &u.Name, &hash, &u.MustChangePassword, &u.IsAdmin, &psa, &ca); err != nil { return nil, err } - u.MustChangePassword = mustChange == 1 - u.IsAdmin = isAdmin == 1 u.HasPassword = hash != "" if psa != "" { t, _ := time.Parse(time.RFC3339Nano, psa) diff --git a/internal/store/projects.go b/internal/store/projects.go index 0715496..4ec6511 100644 --- a/internal/store/projects.go +++ b/internal/store/projects.go @@ -181,14 +181,10 @@ func (s *Store) ListTasks(owner string, includeArchived bool) ([]core.Conversati func scanConversation(rows interface{ Scan(dest ...any) error }, c *core.Conversation) error { var ca, ua string - var pinned, archived, allowCrossRead, crossPrivate int - if err := rows.Scan(&c.ID, &c.ProjectID, &c.Title, &c.ProfileID, &pinned, &archived, &allowCrossRead, &crossPrivate, &c.PermissionMode, &c.SessionNote, &ca, &ua); err != nil { + // 直接扫 bool:PG 原生 boolean;SQLite 整数 0/1 经 driver.Bool 转换同样成立。 + if err := rows.Scan(&c.ID, &c.ProjectID, &c.Title, &c.ProfileID, &c.Pinned, &c.Archived, &c.AllowCrossRead, &c.CrossPrivate, &c.PermissionMode, &c.SessionNote, &ca, &ua); err != nil { return err } - c.Pinned = pinned == 1 - c.Archived = archived == 1 - c.AllowCrossRead = allowCrossRead == 1 - c.CrossPrivate = crossPrivate == 1 c.CreatedAt, _ = time.Parse(time.RFC3339Nano, ca) c.UpdatedAt, _ = time.Parse(time.RFC3339Nano, ua) return nil @@ -197,9 +193,8 @@ func scanConversation(rows interface{ Scan(dest ...any) error }, c *core.Convers func (s *Store) GetConversation(owner, id string) (core.Conversation, error) { var c core.Conversation var ca, ua string - var pinned, archived, allowCrossRead, crossPrivate int err := s.db.QueryRow(s.q(`SELECT id,project_id,title,profile_id,pinned,archived,allow_cross_read,cross_private,permission_mode,session_note,created_at,updated_at FROM conversations WHERE id=? AND owner=?`), id, owner). - Scan(&c.ID, &c.ProjectID, &c.Title, &c.ProfileID, &pinned, &archived, &allowCrossRead, &crossPrivate, &c.PermissionMode, &c.SessionNote, &ca, &ua) + Scan(&c.ID, &c.ProjectID, &c.Title, &c.ProfileID, &c.Pinned, &c.Archived, &c.AllowCrossRead, &c.CrossPrivate, &c.PermissionMode, &c.SessionNote, &ca, &ua) if errors.Is(err, sql.ErrNoRows) { return c, ErrNotFound } @@ -207,10 +202,6 @@ func (s *Store) GetConversation(owner, id string) (core.Conversation, error) { return c, err } c.Owner = owner - c.Pinned = pinned == 1 - c.Archived = archived == 1 - c.AllowCrossRead = allowCrossRead == 1 - c.CrossPrivate = crossPrivate == 1 c.CreatedAt, _ = time.Parse(time.RFC3339Nano, ca) c.UpdatedAt, _ = time.Parse(time.RFC3339Nano, ua) return c, nil @@ -242,24 +233,25 @@ func (s *Store) UpdateConversation(owner, id, title, profileID string) error { } // SetConversationFlags 更新会话标记(nil = 该字段不动;方案 A R1 + 跨会话读取提案)。 +// conversations 的四个标记列在 PG 档是 BOOLEAN——绑 Go bool,不绑 boolInt 整数。 func (s *Store) SetConversationFlags(owner, id string, pinned, archived, allowCrossRead, crossPrivate *bool) error { var sets []string var args []any if pinned != nil { sets = append(sets, "pinned=?") - args = append(args, boolInt(*pinned)) + args = append(args, *pinned) } if archived != nil { sets = append(sets, "archived=?") - args = append(args, boolInt(*archived)) + args = append(args, *archived) } if allowCrossRead != nil { sets = append(sets, "allow_cross_read=?") - args = append(args, boolInt(*allowCrossRead)) + args = append(args, *allowCrossRead) } if crossPrivate != nil { sets = append(sets, "cross_private=?") - args = append(args, boolInt(*crossPrivate)) + args = append(args, *crossPrivate) } if len(sets) == 0 { return nil