@@ -9,16 +9,19 @@ import (
99 "database/sql/driver"
1010 "encoding/hex"
1111 "fmt"
12+ "log/slog"
1213 "os"
1314 "path/filepath"
14- "sync"
1515 "time"
1616
1717 lru "github.com/hashicorp/golang-lru/v2"
18+ "golang.org/x/sync/singleflight"
1819 "modernc.org/sqlite"
1920 _ "modernc.org/sqlite"
2021)
2122
23+ var sf singleflight.Group
24+
2225func init () {
2326 // MySQL-compatible functions
2427 sqlite .MustRegisterFunction ("YEAR" , & sqlite.FunctionImpl {
@@ -117,8 +120,7 @@ type SQLRunner struct {
117120 cache * lru.Cache [string , * QueryResult ]
118121
119122 // cache
120- db * sql.DB
121- mu sync.Mutex
123+ filename string
122124}
123125
124126func NewSQLRunner (schema string ) (* SQLRunner , error ) {
@@ -144,21 +146,6 @@ func NewSQLRunner(schema string) (*SQLRunner, error) {
144146 return runner , nil
145147}
146148
147- // Close closes the SQLite instance.
148- func (r * SQLRunner ) Close () error {
149- r .mu .Lock ()
150- defer r .mu .Unlock ()
151-
152- if r .db == nil {
153- return nil
154- }
155-
156- err := r .db .Close ()
157- r .db = nil
158-
159- return err
160- }
161-
162149// Query executes a query and returns the result.
163150func (r * SQLRunner ) Query (ctx context.Context , query string ) (* QueryResult , error ) {
164151 // Check the cache first
@@ -170,13 +157,20 @@ func (r *SQLRunner) Query(ctx context.Context, query string) (*QueryResult, erro
170157 if err != nil {
171158 return nil , fmt .Errorf ("get schema: %w" , err )
172159 }
160+ defer func () {
161+ if err := db .Close (); err != nil {
162+ slog .WarnContext (ctx , "close schema database" , slog .Any ("error" , err ))
163+ }
164+ }()
173165
174166 result , err := db .QueryContext (ctx , query )
175167 if err != nil {
176168 return nil , NewQueryError (err )
177169 }
178170 defer func () {
179- _ = result .Close ()
171+ if err := result .Close (); err != nil {
172+ slog .WarnContext (ctx , "close result" , slog .Any ("error" , err ))
173+ }
180174 }()
181175
182176 cols , err := result .Columns ()
@@ -216,28 +210,36 @@ func (r *SQLRunner) Query(ctx context.Context, query string) (*QueryResult, erro
216210
217211// getSqliteInstance gets the initialized SQLite instance.
218212//
219- // You don't need to close it after using this instance .
213+ // You should close the database after using it .
220214func (r * SQLRunner ) getSqliteInstance () (* sql.DB , error ) {
221- r .mu .Lock ()
222- defer r .mu .Unlock ()
215+ if r .filename != "" {
216+ filename , err := initializeThreadSafe (r .schema )
217+ if err != nil {
218+ return nil , NewSchemaError (err )
219+ }
223220
224- if r .db != nil {
225- return r .db , nil
221+ r .filename = filename
226222 }
227223
228- filename , err := initialize ( r . schema )
224+ db , err := sql . Open ( "sqlite" , fmt . Sprintf ( "file:%s?mode=ro" , r . filename ) )
229225 if err != nil {
230- return nil , NewSchemaError ( err )
226+ return nil , fmt . Errorf ( "open schema database (r/o): %w" , err )
231227 }
232228
233- db , err := sql .Open ("sqlite" , fmt .Sprintf ("file:%s?mode=ro" , filename ))
229+ return db , nil
230+ }
231+
232+ // initializeThreadSafe creates a new SQLite database and sets up the schema.
233+ // It is thread safe which ensures that the schema is only initialized once.
234+ func initializeThreadSafe (schema string ) (filename string , err error ) {
235+ filenameAny , err , _ := sf .Do (schema , func () (interface {}, error ) {
236+ return initialize (schema )
237+ })
234238 if err != nil {
235- return nil , fmt . Errorf ( "open schema database (r/o): %w " , err )
239+ return " " , err
236240 }
237241
238- r .db = db
239-
240- return db , nil
242+ return filenameAny .(string ), nil
241243}
242244
243245// initialize creates a new SQLite database and sets up the schema.
0 commit comments