@@ -5,37 +5,68 @@ import (
55 "fmt"
66 "os"
77 "path/filepath"
8-
9- _ "modernc.org/sqlite"
8+ "sync"
109)
1110
1211// Tracker manages token savings tracking in SQLite.
1312type Tracker struct {
14- db * sql.DB
13+ db * sql.DB
14+ dbPath string
15+ once sync.Once
16+ initErr error
1517}
1618
17- // NewTracker opens or creates a SQLite database for tracking.
19+ // NewTracker opens or creates a SQLite database for tracking (immediate open) .
1820func NewTracker (dbPath string ) (* Tracker , error ) {
19- dir := filepath . Dir ( dbPath )
20- if err := os . MkdirAll ( dir , 0755 ); err != nil {
21- return nil , fmt . Errorf ( "create db dir: %w" , err )
21+ t := & Tracker { dbPath : dbPath }
22+ if err := t . ensureOpen ( ); err != nil {
23+ return nil , err
2224 }
25+ return t , nil
26+ }
2327
24- db , err := sql . Open ( "sqlite" , dbPath )
25- if err != nil {
26- return nil , fmt . Errorf ( "open db: %w" , err )
27- }
28+ // NewLazyTracker creates a tracker that defers DB opening until first use.
29+ func NewLazyTracker ( dbPath string ) * Tracker {
30+ return & Tracker { dbPath : dbPath }
31+ }
2832
29- if _ , err := db .Exec (createTableSQL ); err != nil {
30- _ = db .Close ()
31- return nil , fmt .Errorf ("create table: %w" , err )
32- }
33+ // WarmUp starts opening the DB in the background.
34+ // Call this before command execution so SQLite init overlaps with the command.
35+ func (t * Tracker ) WarmUp () {
36+ go func () { _ = t .ensureOpen () }()
37+ }
3338
34- return & Tracker {db : db }, nil
39+ func (t * Tracker ) ensureOpen () error {
40+ t .once .Do (func () {
41+ dir := filepath .Dir (t .dbPath )
42+ if err := os .MkdirAll (dir , 0755 ); err != nil {
43+ t .initErr = fmt .Errorf ("create db dir: %w" , err )
44+ return
45+ }
46+
47+ db , err := sql .Open ("sqlite" , t .dbPath )
48+ if err != nil {
49+ t .initErr = fmt .Errorf ("open db: %w" , err )
50+ return
51+ }
52+
53+ if _ , err := db .Exec (createTableSQL ); err != nil {
54+ _ = db .Close ()
55+ t .initErr = fmt .Errorf ("create table: %w" , err )
56+ return
57+ }
58+
59+ t .db = db
60+ })
61+ return t .initErr
3562}
3663
3764// Track records a filtered command execution.
3865func (t * Tracker ) Track (originalCmd , snipCmd string , inputTokens , outputTokens int , execTimeMs int64 ) error {
66+ if err := t .ensureOpen (); err != nil {
67+ return fmt .Errorf ("track: %w" , err )
68+ }
69+
3970 saved := inputTokens - outputTokens
4071 pct := 0.0
4172 if inputTokens > 0 {
@@ -59,6 +90,9 @@ func (t *Tracker) TrackPassthrough(cmd string, tokens int, execTimeMs int64) err
5990
6091// GetSummary returns aggregate tracking stats.
6192func (t * Tracker ) GetSummary () (* Summary , error ) {
93+ if err := t .ensureOpen (); err != nil {
94+ return nil , fmt .Errorf ("summary: %w" , err )
95+ }
6296 var s Summary
6397 err := t .db .QueryRow (summarySQL ).Scan (& s .TotalCommands , & s .TotalSaved , & s .AvgSavings , & s .TotalTimeMs )
6498 if err != nil {
@@ -69,6 +103,9 @@ func (t *Tracker) GetSummary() (*Summary, error) {
69103
70104// GetDaily returns daily stats for the last N days.
71105func (t * Tracker ) GetDaily (days int ) ([]DayStats , error ) {
106+ if err := t .ensureOpen (); err != nil {
107+ return nil , fmt .Errorf ("daily: %w" , err )
108+ }
72109 if days <= 0 {
73110 days = 7
74111 }
@@ -91,6 +128,9 @@ func (t *Tracker) GetDaily(days int) ([]DayStats, error) {
91128
92129// GetRecent returns the last N tracked commands.
93130func (t * Tracker ) GetRecent (n int ) ([]CommandRecord , error ) {
131+ if err := t .ensureOpen (); err != nil {
132+ return nil , fmt .Errorf ("recent: %w" , err )
133+ }
94134 rows , err := t .db .Query (recentSQL , n )
95135 if err != nil {
96136 return nil , fmt .Errorf ("recent: %w" , err )
@@ -110,6 +150,9 @@ func (t *Tracker) GetRecent(n int) ([]CommandRecord, error) {
110150
111151// GetByCommand returns top N commands by tokens saved.
112152func (t * Tracker ) GetByCommand (limit int ) ([]CommandStats , error ) {
153+ if err := t .ensureOpen (); err != nil {
154+ return nil , fmt .Errorf ("by command: %w" , err )
155+ }
113156 if limit <= 0 {
114157 limit = 10
115158 }
@@ -132,6 +175,9 @@ func (t *Tracker) GetByCommand(limit int) ([]CommandStats, error) {
132175
133176// GetWeekly returns weekly stats for the last N weeks.
134177func (t * Tracker ) GetWeekly (weeks int ) ([]PeriodStats , error ) {
178+ if err := t .ensureOpen (); err != nil {
179+ return nil , fmt .Errorf ("weekly: %w" , err )
180+ }
135181 if weeks <= 0 {
136182 weeks = 4
137183 }
@@ -155,6 +201,9 @@ func (t *Tracker) GetWeekly(weeks int) ([]PeriodStats, error) {
155201
156202// GetMonthly returns monthly stats for the last N months.
157203func (t * Tracker ) GetMonthly (months int ) ([]PeriodStats , error ) {
204+ if err := t .ensureOpen (); err != nil {
205+ return nil , fmt .Errorf ("monthly: %w" , err )
206+ }
158207 if months <= 0 {
159208 months = 6
160209 }
@@ -178,7 +227,10 @@ func (t *Tracker) GetMonthly(months int) ([]PeriodStats, error) {
178227
179228// Close closes the database connection.
180229func (t * Tracker ) Close () error {
181- return t .db .Close ()
230+ if t .db != nil {
231+ return t .db .Close ()
232+ }
233+ return nil
182234}
183235
184236// DBPath resolves the tracking database path.
0 commit comments