Skip to content

Commit 3b7f103

Browse files
committed
Normalize the stamps in a table to reduce storage space
1 parent f72290c commit 3b7f103

1 file changed

Lines changed: 155 additions & 8 deletions

File tree

www/db.go

Lines changed: 155 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,12 @@ func NewDB(path string) (*DB, error) {
7373
return nil, err
7474
}
7575

76+
// Migrate from old schema if needed
77+
if err := migrateSchema(db); err != nil {
78+
db.Close()
79+
return nil, fmt.Errorf("failed to migrate schema: %w", err)
80+
}
81+
7682
return &DB{db: db}, nil
7783
}
7884

@@ -87,15 +93,24 @@ func createTables(db *sql.DB) error {
8793
UNIQUE(name, type)
8894
);
8995
90-
CREATE TABLE IF NOT EXISTS test_results (
96+
CREATE TABLE IF NOT EXISTS stamps (
9197
id INTEGER PRIMARY KEY AUTOINCREMENT,
9298
resolver_id INTEGER NOT NULL,
9399
stamp TEXT NOT NULL,
100+
UNIQUE(resolver_id, stamp),
101+
FOREIGN KEY (resolver_id) REFERENCES resolvers(id)
102+
);
103+
104+
CREATE TABLE IF NOT EXISTS test_results (
105+
id INTEGER PRIMARY KEY AUTOINCREMENT,
106+
resolver_id INTEGER NOT NULL,
107+
stamp_id INTEGER NOT NULL,
94108
success INTEGER NOT NULL,
95109
rtt_ms INTEGER,
96110
error TEXT,
97111
tested_at DATETIME DEFAULT CURRENT_TIMESTAMP,
98-
FOREIGN KEY (resolver_id) REFERENCES resolvers(id)
112+
FOREIGN KEY (resolver_id) REFERENCES resolvers(id),
113+
FOREIGN KEY (stamp_id) REFERENCES stamps(id)
99114
);
100115
101116
-- For GetTestCount: MAX(tested_at)
@@ -130,6 +145,103 @@ func createTables(db *sql.DB) error {
130145
return err
131146
}
132147

148+
// migrateSchema migrates from the old schema (stamp column in test_results)
149+
// to the new normalized schema (stamps table with stamp_id reference).
150+
func migrateSchema(db *sql.DB) error {
151+
// Check if migration is needed by looking for stamp column in test_results
152+
var hasStampColumn bool
153+
rows, err := db.Query("PRAGMA table_info(test_results)")
154+
if err != nil {
155+
return err
156+
}
157+
for rows.Next() {
158+
var cid int
159+
var name, typ string
160+
var notnull, pk int
161+
var dflt sql.NullString
162+
if err := rows.Scan(&cid, &name, &typ, &notnull, &dflt, &pk); err != nil {
163+
rows.Close()
164+
return err
165+
}
166+
if name == "stamp" {
167+
hasStampColumn = true
168+
break
169+
}
170+
}
171+
rows.Close()
172+
173+
if !hasStampColumn {
174+
return nil // Already migrated
175+
}
176+
177+
// Check if there's any data to migrate
178+
var count int
179+
if err := db.QueryRow("SELECT COUNT(*) FROM test_results").Scan(&count); err != nil {
180+
return err
181+
}
182+
if count == 0 {
183+
// No data, just recreate the table with new schema
184+
if _, err := db.Exec("DROP TABLE IF EXISTS test_results"); err != nil {
185+
return err
186+
}
187+
return nil
188+
}
189+
190+
// Migration needed - populate stamps table and update test_results
191+
if _, err := db.Exec(`
192+
INSERT OR IGNORE INTO stamps (resolver_id, stamp)
193+
SELECT DISTINCT resolver_id, stamp FROM test_results WHERE stamp IS NOT NULL
194+
`); err != nil {
195+
return fmt.Errorf("failed to populate stamps table: %w", err)
196+
}
197+
198+
// Create new test_results table with stamp_id
199+
if _, err := db.Exec(`
200+
CREATE TABLE test_results_new (
201+
id INTEGER PRIMARY KEY AUTOINCREMENT,
202+
resolver_id INTEGER NOT NULL,
203+
stamp_id INTEGER NOT NULL,
204+
success INTEGER NOT NULL,
205+
rtt_ms INTEGER,
206+
error TEXT,
207+
tested_at DATETIME DEFAULT CURRENT_TIMESTAMP,
208+
FOREIGN KEY (resolver_id) REFERENCES resolvers(id),
209+
FOREIGN KEY (stamp_id) REFERENCES stamps(id)
210+
)
211+
`); err != nil {
212+
return fmt.Errorf("failed to create new test_results table: %w", err)
213+
}
214+
215+
// Copy data from old table to new, joining with stamps to get stamp_id
216+
if _, err := db.Exec(`
217+
INSERT INTO test_results_new (resolver_id, stamp_id, success, rtt_ms, error, tested_at)
218+
SELECT t.resolver_id, s.id, t.success, t.rtt_ms, t.error, t.tested_at
219+
FROM test_results t
220+
JOIN stamps s ON t.resolver_id = s.resolver_id AND t.stamp = s.stamp
221+
`); err != nil {
222+
return fmt.Errorf("failed to migrate test_results data: %w", err)
223+
}
224+
225+
// Drop old table and rename new one
226+
if _, err := db.Exec("DROP TABLE test_results"); err != nil {
227+
return fmt.Errorf("failed to drop old test_results table: %w", err)
228+
}
229+
if _, err := db.Exec("ALTER TABLE test_results_new RENAME TO test_results"); err != nil {
230+
return fmt.Errorf("failed to rename test_results table: %w", err)
231+
}
232+
233+
// Recreate indexes
234+
if _, err := db.Exec(`
235+
CREATE INDEX IF NOT EXISTS idx_test_results_tested_at ON test_results(tested_at);
236+
CREATE INDEX IF NOT EXISTS idx_test_results_resolver_tested_at ON test_results(resolver_id, tested_at DESC);
237+
CREATE INDEX IF NOT EXISTS idx_test_results_resolver_success ON test_results(resolver_id, success, tested_at DESC);
238+
`); err != nil {
239+
return fmt.Errorf("failed to recreate indexes: %w", err)
240+
}
241+
242+
return nil
243+
}
244+
133245
func (d *DB) Close() error {
134246
return d.db.Close()
135247
}
@@ -155,11 +267,29 @@ func (d *DB) UpsertResolver(name, typ, description, sourceFile string) (int64, e
155267
return id, err
156268
}
157269

270+
func (d *DB) GetOrCreateStamp(resolverID int64, stamp string) (int64, error) {
271+
_, err := d.db.Exec(`
272+
INSERT OR IGNORE INTO stamps (resolver_id, stamp) VALUES (?, ?)
273+
`, resolverID, stamp)
274+
if err != nil {
275+
return 0, err
276+
}
277+
278+
var id int64
279+
err = d.db.QueryRow("SELECT id FROM stamps WHERE resolver_id = ? AND stamp = ?", resolverID, stamp).Scan(&id)
280+
return id, err
281+
}
282+
158283
func (d *DB) RecordTest(resolverID int64, stamp string, success bool, rttMs int64, errMsg string) error {
284+
stampID, err := d.GetOrCreateStamp(resolverID, stamp)
285+
if err != nil {
286+
return fmt.Errorf("failed to get stamp id: %w", err)
287+
}
288+
159289
result, err := d.db.Exec(`
160-
INSERT INTO test_results (resolver_id, stamp, success, rtt_ms, error)
290+
INSERT INTO test_results (resolver_id, stamp_id, success, rtt_ms, error)
161291
VALUES (?, ?, ?, ?, ?)
162-
`, resolverID, stamp, success, rttMs, errMsg)
292+
`, resolverID, stampID, success, rttMs, errMsg)
163293
if err != nil {
164294
return err
165295
}
@@ -335,7 +465,7 @@ func (d *DB) RebuildStats() error {
335465
)
336466
SELECT
337467
r.id,
338-
(SELECT stamp FROM test_results t3 WHERE t3.resolver_id = r.id ORDER BY t3.tested_at DESC LIMIT 1),
468+
(SELECT s.stamp FROM test_results t3 JOIN stamps s ON t3.stamp_id = s.id WHERE t3.resolver_id = r.id ORDER BY t3.tested_at DESC LIMIT 1),
339469
COUNT(t.id),
340470
SUM(CASE WHEN t.success = 1 THEN 1 ELSE 0 END),
341471
SUM(CASE WHEN t.success = 0 THEN 1 ELSE 0 END),
@@ -401,11 +531,14 @@ func (d *DB) RemoveStaleResolvers(noSuccessSince time.Duration) ([]string, error
401531
return nil, nil
402532
}
403533

404-
// Delete test results, stats, and resolvers
534+
// Delete test results, stamps, stats, and resolvers
405535
for _, id := range idsToDelete {
406536
if _, err := d.db.Exec("DELETE FROM test_results WHERE resolver_id = ?", id); err != nil {
407537
return names, err
408538
}
539+
if _, err := d.db.Exec("DELETE FROM stamps WHERE resolver_id = ?", id); err != nil {
540+
return names, err
541+
}
409542
if _, err := d.db.Exec("DELETE FROM resolver_stats WHERE resolver_id = ?", id); err != nil {
410543
return names, err
411544
}
@@ -438,6 +571,14 @@ func (d *DB) PruneOldTests(maxAge time.Duration) (int64, error) {
438571
return deleted, fmt.Errorf("failed to clean orphaned stats: %w", err)
439572
}
440573

574+
// Remove orphaned stamps (no remaining test results)
575+
if _, err := d.db.Exec(`
576+
DELETE FROM stamps
577+
WHERE id NOT IN (SELECT DISTINCT stamp_id FROM test_results)
578+
`); err != nil {
579+
return deleted, fmt.Errorf("failed to clean orphaned stamps: %w", err)
580+
}
581+
441582
// Remove orphaned resolvers (no remaining test results)
442583
if _, err := d.db.Exec(`
443584
DELETE FROM resolvers
@@ -486,7 +627,7 @@ func (d *DB) GetTestCount() (count int64, lastTest time.Time, err error) {
486627
return count, lastTest, nil
487628
}
488629

489-
// RemoveResolver removes a resolver by name along with all its test results and stats.
630+
// RemoveResolver removes a resolver by name along with all its test results, stamps, and stats.
490631
// Returns an error if the resolver is not found.
491632
func (d *DB) RemoveResolver(name string) error {
492633
var id int64
@@ -498,10 +639,13 @@ func (d *DB) RemoveResolver(name string) error {
498639
return err
499640
}
500641

501-
// Delete in order: test_results, resolver_stats, resolvers
642+
// Delete in order: test_results, stamps, resolver_stats, resolvers
502643
if _, err := d.db.Exec("DELETE FROM test_results WHERE resolver_id = ?", id); err != nil {
503644
return fmt.Errorf("failed to delete test results: %w", err)
504645
}
646+
if _, err := d.db.Exec("DELETE FROM stamps WHERE resolver_id = ?", id); err != nil {
647+
return fmt.Errorf("failed to delete stamps: %w", err)
648+
}
505649
if _, err := d.db.Exec("DELETE FROM resolver_stats WHERE resolver_id = ?", id); err != nil {
506650
return fmt.Errorf("failed to delete stats: %w", err)
507651
}
@@ -547,6 +691,9 @@ func (d *DB) RemoveUnreliableResolvers(minReliability float64) ([]string, error)
547691
if _, err := d.db.Exec("DELETE FROM test_results WHERE resolver_id = ?", id); err != nil {
548692
return names, err
549693
}
694+
if _, err := d.db.Exec("DELETE FROM stamps WHERE resolver_id = ?", id); err != nil {
695+
return names, err
696+
}
550697
if _, err := d.db.Exec("DELETE FROM resolver_stats WHERE resolver_id = ?", id); err != nil {
551698
return names, err
552699
}

0 commit comments

Comments
 (0)