Files
goclaw/internal/store/sqlitestore/schema.go
T
viettranx 21b6c454ca feat: merge pipeline, per-user credentials, unified picker, group contacts
- Enable merge UI for linking channel contacts to tenant_users
- Contact → tenant_user resolution with cached lookup (60s TTL)
- MCP per-user credentials via user-keyed connection pool
- Secure CLI per-user credentials with AES-256-GCM encryption
- Unified UserPickerCombobox searching contacts + tenant_users
- Group contact collection with chat title in all channels
- Group permission inheritance via wildcard user_id="*"
- Fix heartbeat using wrong userID in group chats
- Filter internal senders from contact collection
- Add contact_type column (user/group) to channel_contacts
- SQLite schema v2 migration for desktop edition
2026-03-29 22:33:17 +07:00

118 lines
3.6 KiB
Go

//go:build sqlite || sqliteonly
package sqlitestore
import (
"database/sql"
_ "embed"
"fmt"
"log/slog"
)
//go:embed schema.sql
var schemaSQL string
// SchemaVersion is the current SQLite schema version.
// Bump this when adding new migration steps below.
const SchemaVersion = 2
// migrations maps version → SQL to apply when upgrading FROM that version.
// schema.sql always represents the LATEST full schema (for fresh DBs).
// Existing DBs are patched incrementally via these steps.
//
// Example: to add a new column in the future:
//
// var migrations = map[int]string{
// 1: `ALTER TABLE agents ADD COLUMN new_col TEXT DEFAULT '';`,
// }
//
// Then bump SchemaVersion to 2.
var migrations = map[int]string{
// Version 1 → 2: add contact_type column to channel_contacts.
1: `ALTER TABLE channel_contacts ADD COLUMN contact_type VARCHAR(20) NOT NULL DEFAULT 'user';`,
}
// EnsureSchema creates tables if they don't exist and applies incremental migrations.
//
// Flow:
// 1. Fresh DB (no schema_version row) → apply full schema.sql + set version = SchemaVersion
// 2. Existing DB with version < SchemaVersion → apply patches sequentially
// 3. Existing DB with version == SchemaVersion → no-op
// 4. Always: seed master tenant (idempotent)
func EnsureSchema(db *sql.DB) error {
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS schema_version (
version INTEGER NOT NULL PRIMARY KEY
)`); err != nil {
return fmt.Errorf("create schema_version: %w", err)
}
var current int
err := db.QueryRow("SELECT version FROM schema_version LIMIT 1").Scan(&current)
if err == sql.ErrNoRows {
// Fresh database — apply full schema.
slog.Info("sqlite: applying initial schema", "version", SchemaVersion)
tx, txErr := db.Begin()
if txErr != nil {
return fmt.Errorf("begin schema tx: %w", txErr)
}
if _, err := tx.Exec(schemaSQL); err != nil {
tx.Rollback()
return fmt.Errorf("apply schema: %w", err)
}
if _, err := tx.Exec("INSERT INTO schema_version (version) VALUES (?)", SchemaVersion); err != nil {
tx.Rollback()
return fmt.Errorf("set schema version: %w", err)
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("commit schema tx: %w", err)
}
return seedMasterTenant(db)
}
if err != nil {
return fmt.Errorf("read schema version: %w", err)
}
// Apply incremental migrations for existing DBs.
if current < SchemaVersion {
slog.Info("sqlite: migrating schema", "from", current, "to", SchemaVersion)
for v := current; v < SchemaVersion; v++ {
patch, ok := migrations[v]
if !ok {
return fmt.Errorf("sqlite: missing migration for version %d → %d", v, v+1)
}
tx, txErr := db.Begin()
if txErr != nil {
return fmt.Errorf("begin migration tx v%d: %w", v, txErr)
}
if _, err := tx.Exec(patch); err != nil {
tx.Rollback()
return fmt.Errorf("apply migration v%d: %w", v, err)
}
if _, err := tx.Exec(
"UPDATE schema_version SET version = ? WHERE version = ?", v+1, v,
); err != nil {
tx.Rollback()
return fmt.Errorf("update schema version v%d: %w", v, err)
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("commit migration v%d: %w", v, err)
}
slog.Info("sqlite: applied migration", "version", v+1)
}
}
return seedMasterTenant(db)
}
// seedMasterTenant ensures the master tenant row exists (idempotent).
func seedMasterTenant(db *sql.DB) error {
_, err := db.Exec(
`INSERT OR IGNORE INTO tenants (id, name, slug, status) VALUES (?, 'Master', 'master', 'active')`,
"0193a5b0-7000-7000-8000-000000000001",
)
if err != nil {
slog.Warn("sqlite: seed master tenant failed", "error", err)
}
return nil
}