Skip to content
File

Blob: kv/sqlite3/schema.go

go165 lines
1package sqlite3
2 
3import (
4 "database/sql"
5 "fmt"
6)
7 
8// Frozen schema v1: exact DDL captured from GORM AutoMigrate output.
9// These must match the tables created by the previous GORM-based implementation.
10const schemaVersion = 1
11 
12func getUserVersion(db *sql.DB) (int, error) {
13 var v int
14 err := db.QueryRow("PRAGMA user_version").Scan(&v)
15 return v, err
16}
17 
18func setUserVersion(db *sql.DB, v int) error {
19 _, err := db.Exec(fmt.Sprintf("PRAGMA user_version = %d", v))
20 return err
21}
22 
23func setTxUserVersion(tx *sql.Tx, v int) error {
24 _, err := tx.Exec(fmt.Sprintf("PRAGMA user_version = %d", v))
25 return err
26}
27 
28func tableExists(db *sql.DB, name string) (bool, error) {
29 var count int
30 err := db.QueryRow(
31 "SELECT COUNT(*) FROM sqlite_schema WHERE type='table' AND name=?", name,
32 ).Scan(&count)
33 return count > 0, err
34}
35 
36func indexExists(db *sql.DB, name string) (bool, error) {
37 var count int
38 err := db.QueryRow(
39 "SELECT COUNT(*) FROM sqlite_schema WHERE type='index' AND name=?", name,
40 ).Scan(&count)
41 return count > 0, err
42}
43 
44func schemaHasAnyV1Objects(db *sql.DB) (bool, error) {
45 for _, tbl := range []string{"key_trackers", "simple_entries", "prefix_entries", "lease_entries"} {
46 ok, err := tableExists(db, tbl)
47 if err != nil {
48 return false, err
49 }
50 if ok {
51 return true, nil
52 }
53 }
54 ok, err := indexExists(db, "idx_hash")
55 if err != nil {
56 return false, err
57 }
58 return ok, nil
59}
60 
61// schemaLooksLikeV1 checks whether the expected v1 tables exist.
62func schemaLooksLikeV1(db *sql.DB) (bool, error) {
63 for _, tbl := range []string{"key_trackers", "simple_entries", "prefix_entries", "lease_entries"} {
64 ok, err := tableExists(db, tbl)
65 if err != nil {
66 return false, err
67 }
68 if !ok {
69 return false, nil
70 }
71 }
72 ok, err := indexExists(db, "idx_hash")
73 if err != nil {
74 return false, err
75 }
76 if !ok {
77 return false, nil
78 }
79 return true, nil
80}
81 
82func applyMigration(db *sql.DB, migration migration) (err error) {
83 tx, err := db.Begin()
84 if err != nil {
85 return err
86 }
87 defer func() {
88 if err != nil {
89 _ = tx.Rollback()
90 }
91 }()
92 
93 if _, err = tx.Exec(migration.sql); err != nil {
94 return err
95 }
96 if err = setTxUserVersion(tx, migration.version); err != nil {
97 return err
98 }
99 err = tx.Commit()
100 return err
101}
102 
103// migrate brings the database schema up to the current version.
104// It handles three cases:
105// - Fresh DB (no tables, user_version=0): create v1 schema and set user_version=1.
106// - Legacy GORM DB (v1 tables exist, user_version=0): accept as v1 baseline, set user_version=1.
107// - Already migrated DB (user_version=1): no-op.
108func migrate(db *sql.DB) error {
109 migrations, err := loadMigrations()
110 if err != nil {
111 return fmt.Errorf("loading migrations: %w", err)
112 }
113 latestVersion := 0
114 if len(migrations) > 0 {
115 latestVersion = migrations[len(migrations)-1].version
116 }
117 
118 uv, err := getUserVersion(db)
119 if err != nil {
120 return fmt.Errorf("reading user_version: %w", err)
121 }
122 
123 if uv == latestVersion {
124 return nil
125 }
126 
127 if uv > latestVersion {
128 return fmt.Errorf("database user_version %d is newer than supported version %d", uv, latestVersion)
129 }
130 
131 // uv == 0: either fresh or legacy GORM DB
132 if uv == 0 {
133 isLegacy, err := schemaLooksLikeV1(db)
134 if err != nil {
135 return fmt.Errorf("inspecting schema: %w", err)
136 }
137 
138 if isLegacy {
139 if err := setUserVersion(db, schemaVersion); err != nil {
140 return err
141 }
142 uv = schemaVersion
143 } else {
144 hasObjects, err := schemaHasAnyV1Objects(db)
145 if err != nil {
146 return fmt.Errorf("inspecting partial schema: %w", err)
147 }
148 if hasObjects {
149 return fmt.Errorf("database user_version %d has unexpected partial sqlite schema", uv)
150 }
151 }
152 }
153 
154 for _, migration := range migrations {
155 if migration.version <= uv {
156 continue
157 }
158 if err := applyMigration(db, migration); err != nil {
159 return fmt.Errorf("applying migration %s: %w", migration.name, err)
160 }
161 }
162 
163 return nil
164}