detached 4d548f3

chore(deps): integrate approved upstream foundations

commit
4d548f3f69cfbdc7fc18360aeb70966c0b334bd3
author
brice · · 2026-07-14 14:11 UTC
parent
41933ea
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
13 files · +482 -125
M go.mod +3 -3
@@ -6,10 +6,10 @@ require (
66 github.com/a-h/templ v0.3.1020
77 github.com/andybalholm/brotli v1.2.2
88 github.com/bnema/purego v0.11.0-bnema.4
9- github.com/bnema/purego-cef v0.13.3
9+ github.com/bnema/purego-cef v0.13.4-0.20260714140305-f0b7414af67e
1010 github.com/bnema/purego-cef2gtk v0.8.4
11- github.com/bnema/purego-pipewire v0.1.4
12- github.com/bnema/purego-sqlite v0.1.3
11+ github.com/bnema/purego-pipewire v0.1.5-0.20260714124450-6504b9cdf02a
12+ github.com/bnema/purego-sqlite v0.1.4-0.20260714131321-3614fe683b9c
1313 github.com/bnema/puregotk v0.7.0
1414 github.com/charmbracelet/bubbles v1.0.0
1515 github.com/charmbracelet/bubbletea v1.3.10
@@ -6,10 +6,10 @@ require (
6 github.com/a-h/templ v0.3.10206 github.com/a-h/templ v0.3.1020
7 github.com/andybalholm/brotli v1.2.27 github.com/andybalholm/brotli v1.2.2
8 github.com/bnema/purego v0.11.0-bnema.48 github.com/bnema/purego v0.11.0-bnema.4
9- github.com/bnema/purego-cef v0.13.39+ github.com/bnema/purego-cef v0.13.4-0.20260714140305-f0b7414af67e
10 github.com/bnema/purego-cef2gtk v0.8.410 github.com/bnema/purego-cef2gtk v0.8.4
11- github.com/bnema/purego-pipewire v0.1.411+ github.com/bnema/purego-pipewire v0.1.5-0.20260714124450-6504b9cdf02a
12- github.com/bnema/purego-sqlite v0.1.312+ github.com/bnema/purego-sqlite v0.1.4-0.20260714131321-3614fe683b9c
13 github.com/bnema/puregotk v0.7.013 github.com/bnema/puregotk v0.7.0
14 github.com/charmbracelet/bubbles v1.0.014 github.com/charmbracelet/bubbles v1.0.0
15 github.com/charmbracelet/bubbletea v1.3.1015 github.com/charmbracelet/bubbletea v1.3.10
M go.sum +6 -0 generated file: collapsed
A internal/infrastructure/persistence/sqlite/history_repo_benchmark_test.go +66 -0
@@ -0,0 +1,66 @@
1+package sqlite_test
2+
3+import (
4+ "fmt"
5+ "path/filepath"
6+ "testing"
7+
8+ "github.com/bnema/dumber/internal/domain/entity"
9+ "github.com/bnema/dumber/internal/domain/repository"
10+ "github.com/bnema/dumber/internal/infrastructure/persistence/sqlite"
11+)
12+
13+// newHistoryBenchmarkRepository creates a representative local history corpus
14+// outside the measured operation. Sidebar reads use GetRecent and omnibox
15+// history suggestions use Search.
16+func newHistoryBenchmarkRepository(b *testing.B) (repository.HistoryRepository, func()) {
17+ b.Helper()
18+
19+ ctx := historyTestCtx()
20+ db, err := sqlite.NewConnection(ctx, filepath.Join(b.TempDir(), "dumber.db"))
21+ if err != nil {
22+ b.Fatal(err)
23+ }
24+
25+ repo := sqlite.NewHistoryRepository(db)
26+ for i := range 200 {
27+ entry := &entity.HistoryEntry{
28+ URL: fmt.Sprintf("https://example%d.test/dumber/history/%d", i%20, i),
29+ Title: fmt.Sprintf("Dumber history entry %d", i),
30+ }
31+ if err := repo.Save(ctx, entry); err != nil {
32+ _ = db.Close()
33+ b.Fatal(err)
34+ }
35+ }
36+
37+ return repo, func() { _ = db.Close() }
38+}
39+
40+func BenchmarkHistorySQLiteSidebarRecent(b *testing.B) {
41+ repo, closeDB := newHistoryBenchmarkRepository(b)
42+ defer closeDB()
43+ ctx := historyTestCtx()
44+
45+ b.ReportAllocs()
46+ b.ResetTimer()
47+ for range b.N {
48+ if _, err := repo.GetRecent(ctx, 50, 0); err != nil {
49+ b.Fatal(err)
50+ }
51+ }
52+}
53+
54+func BenchmarkHistorySQLiteOmniboxSearch(b *testing.B) {
55+ repo, closeDB := newHistoryBenchmarkRepository(b)
56+ defer closeDB()
57+ ctx := historyTestCtx()
58+
59+ b.ReportAllocs()
60+ b.ResetTimer()
61+ for range b.N {
62+ if _, err := repo.Search(ctx, "dumber history", 10); err != nil {
63+ b.Fatal(err)
64+ }
65+ }
66+}
@@ -0,0 +1,66 @@
1+package sqlite_test
2+
3+import (
4+ "fmt"
5+ "path/filepath"
6+ "testing"
7+
8+ "github.com/bnema/dumber/internal/domain/entity"
9+ "github.com/bnema/dumber/internal/domain/repository"
10+ "github.com/bnema/dumber/internal/infrastructure/persistence/sqlite"
11+)
12+
13+// newHistoryBenchmarkRepository creates a representative local history corpus
14+// outside the measured operation. Sidebar reads use GetRecent and omnibox
15+// history suggestions use Search.
16+func newHistoryBenchmarkRepository(b *testing.B) (repository.HistoryRepository, func()) {
17+ b.Helper()
18+
19+ ctx := historyTestCtx()
20+ db, err := sqlite.NewConnection(ctx, filepath.Join(b.TempDir(), "dumber.db"))
21+ if err != nil {
22+ b.Fatal(err)
23+ }
24+
25+ repo := sqlite.NewHistoryRepository(db)
26+ for i := range 200 {
27+ entry := &entity.HistoryEntry{
28+ URL: fmt.Sprintf("https://example%d.test/dumber/history/%d", i%20, i),
29+ Title: fmt.Sprintf("Dumber history entry %d", i),
30+ }
31+ if err := repo.Save(ctx, entry); err != nil {
32+ _ = db.Close()
33+ b.Fatal(err)
34+ }
35+ }
36+
37+ return repo, func() { _ = db.Close() }
38+}
39+
40+func BenchmarkHistorySQLiteSidebarRecent(b *testing.B) {
41+ repo, closeDB := newHistoryBenchmarkRepository(b)
42+ defer closeDB()
43+ ctx := historyTestCtx()
44+
45+ b.ReportAllocs()
46+ b.ResetTimer()
47+ for range b.N {
48+ if _, err := repo.GetRecent(ctx, 50, 0); err != nil {
49+ b.Fatal(err)
50+ }
51+ }
52+}
53+
54+func BenchmarkHistorySQLiteOmniboxSearch(b *testing.B) {
55+ repo, closeDB := newHistoryBenchmarkRepository(b)
56+ defer closeDB()
57+ ctx := historyTestCtx()
58+
59+ b.ReportAllocs()
60+ b.ResetTimer()
61+ for range b.N {
62+ if _, err := repo.Search(ctx, "dumber history", 10); err != nil {
63+ b.Fatal(err)
64+ }
65+ }
66+}
M vendor/github.com/bnema/purego-cef/cef/bridge.go +8 -13 generated file: collapsed
M vendor/github.com/bnema/purego-pipewire/internal/core/player.go +5 -0 generated file: collapsed
M vendor/github.com/bnema/purego-pipewire/internal/core/player_rt.go +10 -6 generated file: collapsed
M vendor/github.com/bnema/purego-pipewire/internal/core/pwbuffer.go +66 -32 generated file: collapsed
M vendor/github.com/bnema/purego-sqlite/driver/driver.go +268 -63
@@ -1,8 +1,13 @@
11 package driver
22
33 import (
4+ "context"
5+ "database/sql"
46 "database/sql/driver"
7+ "errors"
58 "io"
9+ "strings"
10+ "sync"
611 "time"
712
813 "github.com/bnema/purego-sqlite/sqlite"
@@ -38,9 +43,69 @@ func (d *Driver) Open(dsn string) (driver.Conn, error) {
3843 return &conn{db: db}, nil
3944 }
4045
41-// conn implements database/sql/driver.Conn.
46+// conn implements database/sql/driver.Conn and its context-aware extensions.
4247 type conn struct {
4348 db sqlite.DB
49+
50+ // opMu serializes SQLite calls on this connection. In particular, a query
51+ // retains it until its Rows are closed, so a late cancellation cannot
52+ // interrupt a later operation after this connection is returned to a pool.
53+ opMu sync.Mutex
54+ activeMu sync.Mutex
55+ active *operation
56+}
57+
58+type operation struct {
59+ done chan struct{}
60+ once sync.Once
61+}
62+
63+func (c *conn) beginOperation(ctx context.Context) (*operation, error) {
64+ if err := ctx.Err(); err != nil {
65+ return nil, err
66+ }
67+ c.opMu.Lock()
68+ if err := ctx.Err(); err != nil {
69+ c.opMu.Unlock()
70+ return nil, err
71+ }
72+ // A context without a Done channel cannot cancel. It still holds opMu to
73+ // serialize the SQLite connection, but avoids allocating a token/watcher.
74+ if ctx.Done() == nil {
75+ return nil, nil
76+ }
77+ op := &operation{done: make(chan struct{})}
78+ c.activeMu.Lock()
79+ c.active = op
80+ c.activeMu.Unlock()
81+ go func() {
82+ select {
83+ case <-ctx.Done():
84+ c.activeMu.Lock()
85+ if c.active == op {
86+ c.db.Interrupt()
87+ }
88+ c.activeMu.Unlock()
89+ case <-op.done:
90+ }
91+ }()
92+ return op, nil
93+}
94+
95+func (c *conn) finishOperation(op *operation) {
96+ if op == nil {
97+ c.opMu.Unlock()
98+ return
99+ }
100+ op.once.Do(func() {
101+ c.activeMu.Lock()
102+ if c.active == op {
103+ c.active = nil
104+ }
105+ close(op.done)
106+ c.activeMu.Unlock()
107+ c.opMu.Unlock()
108+ })
44109 }
45110
46111 // Prepare returns a prepared statement.
@@ -49,117 +114,242 @@ func (c *conn) Prepare(query string) (driver.Stmt, error) {
49114 if err != nil {
50115 return nil, err
51116 }
52- return &stmt{s: s}, nil
117+ return &stmt{s: s, conn: c}, nil
118+}
119+
120+// PrepareContext prepares a statement without database/sql's legacy fallback.
121+func (c *conn) PrepareContext(ctx context.Context, query string) (driver.Stmt, error) {
122+ op, err := c.beginOperation(ctx)
123+ if err != nil {
124+ return nil, err
125+ }
126+ defer c.finishOperation(op)
127+ s, err := c.db.Prepare(query)
128+ if err != nil {
129+ if ctxErr := ctx.Err(); ctxErr != nil {
130+ return nil, ctxErr
131+ }
132+ return nil, err
133+ }
134+ return &stmt{s: s, conn: c}, nil
53135 }
54136
55137 // Close closes the connection.
56138 func (c *conn) Close() error {
139+ c.opMu.Lock()
140+ defer c.opMu.Unlock()
57141 return c.db.Close()
58142 }
59143
60144 // Begin starts a transaction.
61-func (c *conn) Begin() (driver.Tx, error) {
145+func (c *conn) Begin() (driver.Tx, error) { return c.begin() }
146+
147+func (c *conn) begin() (driver.Tx, error) {
62148 if _, err := c.db.Exec("BEGIN"); err != nil {
63149 return nil, err
64150 }
65151 return &tx{db: c.db}, nil
66152 }
67153
68-// tx implements database/sql/driver.Tx.
69-type tx struct {
70- db sqlite.DB
154+// BeginTx starts a transaction directly while honoring context cancellation.
155+func (c *conn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) {
156+ if opts.ReadOnly {
157+ return nil, errors.New("sqlite3: read-only transactions are not supported")
158+ }
159+ if opts.Isolation != driver.IsolationLevel(sql.LevelDefault) && opts.Isolation != driver.IsolationLevel(sql.LevelSerializable) {
160+ return nil, errors.New("sqlite3: requested isolation level is not supported")
161+ }
162+ op, err := c.beginOperation(ctx)
163+ if err != nil {
164+ return nil, err
165+ }
166+ defer c.finishOperation(op)
167+ if _, err := c.db.Exec("BEGIN"); err != nil {
168+ if ctxErr := ctx.Err(); ctxErr != nil {
169+ return nil, ctxErr
170+ }
171+ return nil, err
172+ }
173+ return &tx{db: c.db}, nil
71174 }
72175
73-// Commit commits the transaction.
74-func (t *tx) Commit() error {
75- _, err := t.db.Exec("COMMIT")
76- return err
176+// ExecContext executes directly without database/sql's prepare/close fallback.
177+func (c *conn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) {
178+ op, err := c.beginOperation(ctx)
179+ if err != nil {
180+ return nil, err
181+ }
182+ defer c.finishOperation(op)
183+ result, err := c.db.Exec(query, namedValuesToAny(args)...)
184+ if err != nil && ctx.Err() != nil {
185+ return nil, ctx.Err()
186+ }
187+ return result, err
77188 }
78189
79-// Rollback rolls back the transaction.
80-func (t *tx) Rollback() error {
81- _, err := t.db.Exec("ROLLBACK")
82- return err
190+// QueryContext executes directly without database/sql's prepare/close fallback.
191+// Its operation remains owned by the returned Rows until Close or exhaustion.
192+func (c *conn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) {
193+ op, err := c.beginOperation(ctx)
194+ if err != nil {
195+ return nil, err
196+ }
197+ rows, err := c.db.Query(query, namedValuesToAny(args)...)
198+ if err != nil {
199+ c.finishOperation(op)
200+ if ctx.Err() != nil {
201+ return nil, ctx.Err()
202+ }
203+ return nil, err
204+ }
205+ declTypes, err := rows.ColumnDeclTypes()
206+ if err != nil {
207+ closeErr := rows.Close()
208+ c.finishOperation(op)
209+ return nil, errors.Join(err, closeErr)
210+ }
211+ return &driverRows{rows: rows, declTypes: declTypes, ctx: ctx, release: func() { c.finishOperation(op) }}, nil
83212 }
84213
85-// stmt implements database/sql/driver.Stmt.
214+// tx implements database/sql/driver.Tx.
215+type tx struct{ db sqlite.DB }
216+
217+func (t *tx) Commit() error { _, err := t.db.Exec("COMMIT"); return err }
218+func (t *tx) Rollback() error { _, err := t.db.Exec("ROLLBACK"); return err }
219+
220+// stmt implements database/sql/driver.Stmt and its context-aware extensions.
86221 type stmt struct {
87- s sqlite.Stmt
222+ s sqlite.Stmt
223+ conn *conn
88224 }
89225
90-// Close closes the statement.
91-func (s *stmt) Close() error {
92- return s.s.Close()
226+func (s *stmt) Close() error { return s.s.Close() }
227+func (s *stmt) NumInput() int { return s.s.NumInput() }
228+func (s *stmt) Exec(args []driver.Value) (driver.Result, error) {
229+ return s.s.Exec(driverValuesToAny(args)...)
230+}
231+func (s *stmt) Query(args []driver.Value) (driver.Rows, error) {
232+ return s.query(driverValuesToAny(args), context.Background(), nil)
93233 }
94234
95-// NumInput returns the number of placeholder parameters.
96-func (s *stmt) NumInput() int {
97- return s.s.NumInput()
235+func (s *stmt) ExecContext(ctx context.Context, args []driver.NamedValue) (driver.Result, error) {
236+ if s.conn == nil {
237+ if err := ctx.Err(); err != nil {
238+ return nil, err
239+ }
240+ return s.s.Exec(namedValuesToAny(args)...)
241+ }
242+ op, err := s.conn.beginOperation(ctx)
243+ if err != nil {
244+ return nil, err
245+ }
246+ defer s.conn.finishOperation(op)
247+ result, err := s.s.Exec(namedValuesToAny(args)...)
248+ if err != nil && ctx.Err() != nil {
249+ return nil, ctx.Err()
250+ }
251+ return result, err
98252 }
99253
100-// Exec executes the statement with the given args.
101-func (s *stmt) Exec(args []driver.Value) (driver.Result, error) {
102- anyArgs := driverValuesToAny(args)
103- return s.s.Exec(anyArgs...)
254+func (s *stmt) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) {
255+ if s.conn == nil {
256+ if err := ctx.Err(); err != nil {
257+ return nil, err
258+ }
259+ return s.query(namedValuesToAny(args), ctx, nil)
260+ }
261+ op, err := s.conn.beginOperation(ctx)
262+ if err != nil {
263+ return nil, err
264+ }
265+ return s.query(namedValuesToAny(args), ctx, func() { s.conn.finishOperation(op) })
104266 }
105267
106-// Query executes the statement and returns rows.
107-func (s *stmt) Query(args []driver.Value) (driver.Rows, error) {
108- anyArgs := driverValuesToAny(args)
109- rows, err := s.s.Query(anyArgs...)
268+func (s *stmt) query(args []any, ctx context.Context, release func()) (driver.Rows, error) {
269+ rows, err := s.s.Query(args...)
110270 if err != nil {
271+ if release != nil {
272+ release()
273+ }
274+ if ctx.Err() != nil {
275+ return nil, ctx.Err()
276+ }
111277 return nil, err
112278 }
113- return &driverRows{rows: rows}, nil
279+ declTypes, err := rows.ColumnDeclTypes()
280+ if err != nil {
281+ closeErr := rows.Close()
282+ if release != nil {
283+ release()
284+ }
285+ return nil, errors.Join(err, closeErr)
286+ }
287+ return &driverRows{rows: rows, declTypes: declTypes, ctx: ctx, release: release}, nil
114288 }
115289
116290 // driverRows implements database/sql/driver.Rows.
117291 type driverRows struct {
118- rows sqlite.Rows
292+ rows sqlite.Rows
293+ declTypes []string
294+ values []any
295+ ptrs []any
296+ ctx context.Context
297+ release func()
298+ closeOnce sync.Once
299+ closeErr error
119300 }
120301
121302 // Columns returns the column names.
122-func (r *driverRows) Columns() []string {
123- cols, _ := r.rows.Columns()
124- return cols
125-}
303+func (r *driverRows) Columns() []string { cols, _ := r.rows.Columns(); return cols }
126304
127-// Close closes the rows.
305+// Close closes the rows and releases its cancellation ownership.
128306 func (r *driverRows) Close() error {
129- return r.rows.Close()
307+ r.closeOnce.Do(func() {
308+ r.closeErr = r.rows.Close()
309+ if r.release != nil {
310+ r.release()
311+ }
312+ })
313+ return r.closeErr
130314 }
131315
132-// Next fills dest with the next row's values.
133-// Returns io.EOF when there are no more rows.
316+// Next fills dest with the next row's values and releases ownership at EOF.
134317 func (r *driverRows) Next(dest []driver.Value) error {
135318 if !r.rows.Next() {
136- if err := r.rows.Err(); err != nil {
137- return err
319+ err := r.rows.Err()
320+ closeErr := r.Close()
321+ if r.ctx != nil && r.ctx.Err() != nil {
322+ return errors.Join(r.ctx.Err(), err, closeErr)
323+ }
324+ if err != nil || closeErr != nil {
325+ return errors.Join(err, closeErr)
138326 }
139327 return io.EOF
140328 }
141- // We cannot pass *driver.Value to core.Scan because driver.Value is a
142- // distinct named type (not a type alias) in Go 1.22+, so *driver.Value
143- // does not match *any in a type switch. Use intermediate *any variables.
144- vals := make([]any, len(dest))
145- ptrs := make([]any, len(dest))
146- for i := range dest {
147- ptrs[i] = &vals[i]
329+ // driver.Value is a distinct named type, so Scan needs *any scratch slots.
330+ if len(r.values) != len(dest) {
331+ r.values = make([]any, len(dest))
332+ r.ptrs = make([]any, len(dest))
333+ for i := range dest {
334+ r.ptrs[i] = &r.values[i]
335+ }
148336 }
149- if err := r.rows.Scan(ptrs...); err != nil {
150- return err
337+ if err := r.rows.Scan(r.ptrs...); err != nil {
338+ return errors.Join(err, r.Close())
151339 }
152340 for i := range dest {
153- dest[i] = maybeParseTime(vals[i])
341+ dest[i] = maybeParseDeclaredTime(r.declTypes, i, r.values[i])
154342 }
155343 return nil
156344 }
157345
158-// maybeParseTime converts a string value to time.Time if it matches a known
159-// SQLite datetime format. Non-string values and non-matching strings pass through.
160-func maybeParseTime(v any) any {
346+// maybeParseDeclaredTime converts values for SQLite DATE, DATETIME, and TIMESTAMP columns.
347+func maybeParseDeclaredTime(declTypes []string, index int, v any) any {
348+ if index >= len(declTypes) || !isDateTimeDeclType(declTypes[index]) {
349+ return v
350+ }
161351 s, ok := v.(string)
162- if !ok || len(s) < 10 { // "2006-01-02" is the shortest format
352+ if !ok || len(s) < 10 {
163353 return v
164354 }
165355 for _, format := range sqliteTimeFormats {
@@ -169,17 +359,32 @@ func maybeParseTime(v any) any {
169359 }
170360 return v
171361 }
362+func isDateTimeDeclType(declType string) bool {
363+ switch strings.ToUpper(strings.TrimSpace(declType)) {
364+ case "DATE", "DATETIME", "TIMESTAMP":
365+ return true
366+ }
367+ return false
368+}
172369
173-// driverValuesToAny converts a []driver.Value to []any, formatting time.Time
174-// values as SQLite-compatible datetime strings.
370+// driverValuesToAny formats time.Time values as SQLite-compatible timestamps.
175371 func driverValuesToAny(args []driver.Value) []any {
176372 out := make([]any, len(args))
177373 for i, v := range args {
178- if t, ok := v.(time.Time); ok {
179- out[i] = t.UTC().Format("2006-01-02 15:04:05.999999999")
180- } else {
181- out[i] = v
182- }
374+ out[i] = normalizeValue(v)
183375 }
184376 return out
185377 }
378+func namedValuesToAny(args []driver.NamedValue) []any {
379+ out := make([]any, len(args))
380+ for i := range args {
381+ out[i] = normalizeValue(args[i].Value)
382+ }
383+ return out
384+}
385+func normalizeValue(v any) any {
386+ if t, ok := v.(time.Time); ok {
387+ return t.UTC().Format("2006-01-02 15:04:05.999999999")
388+ }
389+ return v
390+}
@@ -1,8 +1,13 @@
1 package driver1 package driver
2 2
3 import (3 import (
4+ "context"
5+ "database/sql"
4 "database/sql/driver"6 "database/sql/driver"
7+ "errors"
5 "io"8 "io"
9+ "strings"
10+ "sync"
6 "time"11 "time"
7 12
8 "github.com/bnema/purego-sqlite/sqlite"13 "github.com/bnema/purego-sqlite/sqlite"
@@ -38,9 +43,69 @@ func (d *Driver) Open(dsn string) (driver.Conn, error) {
38 return &conn{db: db}, nil43 return &conn{db: db}, nil
39 }44 }
40 45
41-// conn implements database/sql/driver.Conn.46+// conn implements database/sql/driver.Conn and its context-aware extensions.
42 type conn struct {47 type conn struct {
43 db sqlite.DB48 db sqlite.DB
49+
50+ // opMu serializes SQLite calls on this connection. In particular, a query
51+ // retains it until its Rows are closed, so a late cancellation cannot
52+ // interrupt a later operation after this connection is returned to a pool.
53+ opMu sync.Mutex
54+ activeMu sync.Mutex
55+ active *operation
56+}
57+
58+type operation struct {
59+ done chan struct{}
60+ once sync.Once
61+}
62+
63+func (c *conn) beginOperation(ctx context.Context) (*operation, error) {
64+ if err := ctx.Err(); err != nil {
65+ return nil, err
66+ }
67+ c.opMu.Lock()
68+ if err := ctx.Err(); err != nil {
69+ c.opMu.Unlock()
70+ return nil, err
71+ }
72+ // A context without a Done channel cannot cancel. It still holds opMu to
73+ // serialize the SQLite connection, but avoids allocating a token/watcher.
74+ if ctx.Done() == nil {
75+ return nil, nil
76+ }
77+ op := &operation{done: make(chan struct{})}
78+ c.activeMu.Lock()
79+ c.active = op
80+ c.activeMu.Unlock()
81+ go func() {
82+ select {
83+ case <-ctx.Done():
84+ c.activeMu.Lock()
85+ if c.active == op {
86+ c.db.Interrupt()
87+ }
88+ c.activeMu.Unlock()
89+ case <-op.done:
90+ }
91+ }()
92+ return op, nil
93+}
94+
95+func (c *conn) finishOperation(op *operation) {
96+ if op == nil {
97+ c.opMu.Unlock()
98+ return
99+ }
100+ op.once.Do(func() {
101+ c.activeMu.Lock()
102+ if c.active == op {
103+ c.active = nil
104+ }
105+ close(op.done)
106+ c.activeMu.Unlock()
107+ c.opMu.Unlock()
108+ })
44 }109 }
45 110
46 // Prepare returns a prepared statement.111 // Prepare returns a prepared statement.
@@ -49,117 +114,242 @@ func (c *conn) Prepare(query string) (driver.Stmt, error) {
49 if err != nil {114 if err != nil {
50 return nil, err115 return nil, err
51 }116 }
52- return &stmt{s: s}, nil117+ return &stmt{s: s, conn: c}, nil
118+}
119+
120+// PrepareContext prepares a statement without database/sql's legacy fallback.
121+func (c *conn) PrepareContext(ctx context.Context, query string) (driver.Stmt, error) {
122+ op, err := c.beginOperation(ctx)
123+ if err != nil {
124+ return nil, err
125+ }
126+ defer c.finishOperation(op)
127+ s, err := c.db.Prepare(query)
128+ if err != nil {
129+ if ctxErr := ctx.Err(); ctxErr != nil {
130+ return nil, ctxErr
131+ }
132+ return nil, err
133+ }
134+ return &stmt{s: s, conn: c}, nil
53 }135 }
54 136
55 // Close closes the connection.137 // Close closes the connection.
56 func (c *conn) Close() error {138 func (c *conn) Close() error {
139+ c.opMu.Lock()
140+ defer c.opMu.Unlock()
57 return c.db.Close()141 return c.db.Close()
58 }142 }
59 143
60 // Begin starts a transaction.144 // Begin starts a transaction.
61-func (c *conn) Begin() (driver.Tx, error) {145+func (c *conn) Begin() (driver.Tx, error) { return c.begin() }
146+
147+func (c *conn) begin() (driver.Tx, error) {
62 if _, err := c.db.Exec("BEGIN"); err != nil {148 if _, err := c.db.Exec("BEGIN"); err != nil {
63 return nil, err149 return nil, err
64 }150 }
65 return &tx{db: c.db}, nil151 return &tx{db: c.db}, nil
66 }152 }
67 153
68-// tx implements database/sql/driver.Tx.154+// BeginTx starts a transaction directly while honoring context cancellation.
69-type tx struct {155+func (c *conn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) {
70- db sqlite.DB156+ if opts.ReadOnly {
157+ return nil, errors.New("sqlite3: read-only transactions are not supported")
158+ }
159+ if opts.Isolation != driver.IsolationLevel(sql.LevelDefault) && opts.Isolation != driver.IsolationLevel(sql.LevelSerializable) {
160+ return nil, errors.New("sqlite3: requested isolation level is not supported")
161+ }
162+ op, err := c.beginOperation(ctx)
163+ if err != nil {
164+ return nil, err
165+ }
166+ defer c.finishOperation(op)
167+ if _, err := c.db.Exec("BEGIN"); err != nil {
168+ if ctxErr := ctx.Err(); ctxErr != nil {
169+ return nil, ctxErr
170+ }
171+ return nil, err
172+ }
173+ return &tx{db: c.db}, nil
71 }174 }
72 175
73-// Commit commits the transaction.176+// ExecContext executes directly without database/sql's prepare/close fallback.
74-func (t *tx) Commit() error {177+func (c *conn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) {
75- _, err := t.db.Exec("COMMIT")178+ op, err := c.beginOperation(ctx)
76- return err179+ if err != nil {
180+ return nil, err
181+ }
182+ defer c.finishOperation(op)
183+ result, err := c.db.Exec(query, namedValuesToAny(args)...)
184+ if err != nil && ctx.Err() != nil {
185+ return nil, ctx.Err()
186+ }
187+ return result, err
77 }188 }
78 189
79-// Rollback rolls back the transaction.190+// QueryContext executes directly without database/sql's prepare/close fallback.
80-func (t *tx) Rollback() error {191+// Its operation remains owned by the returned Rows until Close or exhaustion.
81- _, err := t.db.Exec("ROLLBACK")192+func (c *conn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) {
82- return err193+ op, err := c.beginOperation(ctx)
194+ if err != nil {
195+ return nil, err
196+ }
197+ rows, err := c.db.Query(query, namedValuesToAny(args)...)
198+ if err != nil {
199+ c.finishOperation(op)
200+ if ctx.Err() != nil {
201+ return nil, ctx.Err()
202+ }
203+ return nil, err
204+ }
205+ declTypes, err := rows.ColumnDeclTypes()
206+ if err != nil {
207+ closeErr := rows.Close()
208+ c.finishOperation(op)
209+ return nil, errors.Join(err, closeErr)
210+ }
211+ return &driverRows{rows: rows, declTypes: declTypes, ctx: ctx, release: func() { c.finishOperation(op) }}, nil
83 }212 }
84 213
85-// stmt implements database/sql/driver.Stmt.214+// tx implements database/sql/driver.Tx.
215+type tx struct{ db sqlite.DB }
216+
217+func (t *tx) Commit() error { _, err := t.db.Exec("COMMIT"); return err }
218+func (t *tx) Rollback() error { _, err := t.db.Exec("ROLLBACK"); return err }
219+
220+// stmt implements database/sql/driver.Stmt and its context-aware extensions.
86 type stmt struct {221 type stmt struct {
87- s sqlite.Stmt222+ s sqlite.Stmt
223+ conn *conn
88 }224 }
89 225
90-// Close closes the statement.226+func (s *stmt) Close() error { return s.s.Close() }
91-func (s *stmt) Close() error {227+func (s *stmt) NumInput() int { return s.s.NumInput() }
92- return s.s.Close()228+func (s *stmt) Exec(args []driver.Value) (driver.Result, error) {
229+ return s.s.Exec(driverValuesToAny(args)...)
230+}
231+func (s *stmt) Query(args []driver.Value) (driver.Rows, error) {
232+ return s.query(driverValuesToAny(args), context.Background(), nil)
93 }233 }
94 234
95-// NumInput returns the number of placeholder parameters.235+func (s *stmt) ExecContext(ctx context.Context, args []driver.NamedValue) (driver.Result, error) {
96-func (s *stmt) NumInput() int {236+ if s.conn == nil {
97- return s.s.NumInput()237+ if err := ctx.Err(); err != nil {
238+ return nil, err
239+ }
240+ return s.s.Exec(namedValuesToAny(args)...)
241+ }
242+ op, err := s.conn.beginOperation(ctx)
243+ if err != nil {
244+ return nil, err
245+ }
246+ defer s.conn.finishOperation(op)
247+ result, err := s.s.Exec(namedValuesToAny(args)...)
248+ if err != nil && ctx.Err() != nil {
249+ return nil, ctx.Err()
250+ }
251+ return result, err
98 }252 }
99 253
100-// Exec executes the statement with the given args.254+func (s *stmt) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) {
101-func (s *stmt) Exec(args []driver.Value) (driver.Result, error) {255+ if s.conn == nil {
102- anyArgs := driverValuesToAny(args)256+ if err := ctx.Err(); err != nil {
103- return s.s.Exec(anyArgs...)257+ return nil, err
258+ }
259+ return s.query(namedValuesToAny(args), ctx, nil)
260+ }
261+ op, err := s.conn.beginOperation(ctx)
262+ if err != nil {
263+ return nil, err
264+ }
265+ return s.query(namedValuesToAny(args), ctx, func() { s.conn.finishOperation(op) })
104 }266 }
105 267
106-// Query executes the statement and returns rows.268+func (s *stmt) query(args []any, ctx context.Context, release func()) (driver.Rows, error) {
107-func (s *stmt) Query(args []driver.Value) (driver.Rows, error) {269+ rows, err := s.s.Query(args...)
108- anyArgs := driverValuesToAny(args)
109- rows, err := s.s.Query(anyArgs...)
110 if err != nil {270 if err != nil {
271+ if release != nil {
272+ release()
273+ }
274+ if ctx.Err() != nil {
275+ return nil, ctx.Err()
276+ }
111 return nil, err277 return nil, err
112 }278 }
113- return &driverRows{rows: rows}, nil279+ declTypes, err := rows.ColumnDeclTypes()
280+ if err != nil {
281+ closeErr := rows.Close()
282+ if release != nil {
283+ release()
284+ }
285+ return nil, errors.Join(err, closeErr)
286+ }
287+ return &driverRows{rows: rows, declTypes: declTypes, ctx: ctx, release: release}, nil
114 }288 }
115 289
116 // driverRows implements database/sql/driver.Rows.290 // driverRows implements database/sql/driver.Rows.
117 type driverRows struct {291 type driverRows struct {
118- rows sqlite.Rows292+ rows sqlite.Rows
293+ declTypes []string
294+ values []any
295+ ptrs []any
296+ ctx context.Context
297+ release func()
298+ closeOnce sync.Once
299+ closeErr error
119 }300 }
120 301
121 // Columns returns the column names.302 // Columns returns the column names.
122-func (r *driverRows) Columns() []string {303+func (r *driverRows) Columns() []string { cols, _ := r.rows.Columns(); return cols }
123- cols, _ := r.rows.Columns()
124- return cols
125-}
126 304
127-// Close closes the rows.305+// Close closes the rows and releases its cancellation ownership.
128 func (r *driverRows) Close() error {306 func (r *driverRows) Close() error {
129- return r.rows.Close()307+ r.closeOnce.Do(func() {
308+ r.closeErr = r.rows.Close()
309+ if r.release != nil {
310+ r.release()
311+ }
312+ })
313+ return r.closeErr
130 }314 }
131 315
132-// Next fills dest with the next row's values.316+// Next fills dest with the next row's values and releases ownership at EOF.
133-// Returns io.EOF when there are no more rows.
134 func (r *driverRows) Next(dest []driver.Value) error {317 func (r *driverRows) Next(dest []driver.Value) error {
135 if !r.rows.Next() {318 if !r.rows.Next() {
136- if err := r.rows.Err(); err != nil {319+ err := r.rows.Err()
137- return err320+ closeErr := r.Close()
321+ if r.ctx != nil && r.ctx.Err() != nil {
322+ return errors.Join(r.ctx.Err(), err, closeErr)
323+ }
324+ if err != nil || closeErr != nil {
325+ return errors.Join(err, closeErr)
138 }326 }
139 return io.EOF327 return io.EOF
140 }328 }
141- // We cannot pass *driver.Value to core.Scan because driver.Value is a329+ // driver.Value is a distinct named type, so Scan needs *any scratch slots.
142- // distinct named type (not a type alias) in Go 1.22+, so *driver.Value330+ if len(r.values) != len(dest) {
143- // does not match *any in a type switch. Use intermediate *any variables.331+ r.values = make([]any, len(dest))
144- vals := make([]any, len(dest))332+ r.ptrs = make([]any, len(dest))
145- ptrs := make([]any, len(dest))333+ for i := range dest {
146- for i := range dest {334+ r.ptrs[i] = &r.values[i]
147- ptrs[i] = &vals[i]335+ }
148 }336 }
149- if err := r.rows.Scan(ptrs...); err != nil {337+ if err := r.rows.Scan(r.ptrs...); err != nil {
150- return err338+ return errors.Join(err, r.Close())
151 }339 }
152 for i := range dest {340 for i := range dest {
153- dest[i] = maybeParseTime(vals[i])341+ dest[i] = maybeParseDeclaredTime(r.declTypes, i, r.values[i])
154 }342 }
155 return nil343 return nil
156 }344 }
157 345
158-// maybeParseTime converts a string value to time.Time if it matches a known346+// maybeParseDeclaredTime converts values for SQLite DATE, DATETIME, and TIMESTAMP columns.
159-// SQLite datetime format. Non-string values and non-matching strings pass through.347+func maybeParseDeclaredTime(declTypes []string, index int, v any) any {
160-func maybeParseTime(v any) any {348+ if index >= len(declTypes) || !isDateTimeDeclType(declTypes[index]) {
349+ return v
350+ }
161 s, ok := v.(string)351 s, ok := v.(string)
162- if !ok || len(s) < 10 { // "2006-01-02" is the shortest format352+ if !ok || len(s) < 10 {
163 return v353 return v
164 }354 }
165 for _, format := range sqliteTimeFormats {355 for _, format := range sqliteTimeFormats {
@@ -169,17 +359,32 @@ func maybeParseTime(v any) any {
169 }359 }
170 return v360 return v
171 }361 }
362+func isDateTimeDeclType(declType string) bool {
363+ switch strings.ToUpper(strings.TrimSpace(declType)) {
364+ case "DATE", "DATETIME", "TIMESTAMP":
365+ return true
366+ }
367+ return false
368+}
172 369
173-// driverValuesToAny converts a []driver.Value to []any, formatting time.Time370+// driverValuesToAny formats time.Time values as SQLite-compatible timestamps.
174-// values as SQLite-compatible datetime strings.
175 func driverValuesToAny(args []driver.Value) []any {371 func driverValuesToAny(args []driver.Value) []any {
176 out := make([]any, len(args))372 out := make([]any, len(args))
177 for i, v := range args {373 for i, v := range args {
178- if t, ok := v.(time.Time); ok {374+ out[i] = normalizeValue(v)
179- out[i] = t.UTC().Format("2006-01-02 15:04:05.999999999")
180- } else {
181- out[i] = v
182- }
183 }375 }
184 return out376 return out
185 }377 }
378+func namedValuesToAny(args []driver.NamedValue) []any {
379+ out := make([]any, len(args))
380+ for i := range args {
381+ out[i] = normalizeValue(args[i].Value)
382+ }
383+ return out
384+}
385+func normalizeValue(v any) any {
386+ if t, ok := v.(time.Time); ok {
387+ return t.UTC().Format("2006-01-02 15:04:05.999999999")
388+ }
389+ return v
390+}
M vendor/github.com/bnema/purego-sqlite/internal/core/db.go +8 -0 generated file: collapsed
M vendor/github.com/bnema/purego-sqlite/internal/core/rows.go +13 -0 generated file: collapsed
M vendor/github.com/bnema/purego-sqlite/internal/loader/loader.go +23 -5 generated file: collapsed
M vendor/github.com/bnema/purego-sqlite/internal/ports/in/ports_gen.go +3 -0 generated file: collapsed
M vendor/modules.txt +3 -3 generated file: collapsed

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close