Files
upstream-mirror 0f4df23290
Some checks failed
kotlin / test (push) Has been skipped
python / test (push) Has been skipped
typescript / test (push) Has been skipped
go / build darwin/amd64 (push) Has been cancelled
go / build linux/amd64 (push) Has been cancelled
go / build windows/amd64 (push) Has been cancelled
go / build darwin/arm64 (push) Has been cancelled
go / build linux/arm64 (push) Has been cancelled
go / build windows/arm64 (push) Has been cancelled
go / test (push) Has been cancelled
go / vuln_check (push) Has been cancelled
snapshot of https://github.com/sqlc-dev/sqlc.git (main, 2026-09-16; blobs > 131072B omitted)
2026-09-16 02:17:34 +05:00

172 lines
4.5 KiB
Go

package core
import (
"context"
"database/sql"
"sync"
)
// stmtCache is a catalogdb.DBTX that keeps one prepared statement per distinct
// query string.
//
// The catalog runs the same dozen or so statements over and over — seeding a
// dialect is hundreds of inserts through a handful of queries, and analysis
// issues one lookup per table, column, type, operator and function a statement
// touches. Handing the raw *sql.DB to catalogdb means SQLite re-parses and
// re-plans that SQL text on every call, which dominated both paths. Preparing
// once and rebinding is the whole win.
//
// Statements live for the lifetime of the catalog and are released by Close.
type stmtCache struct {
db *sql.DB
mu sync.RWMutex
stmts map[string]*sql.Stmt
// While a transaction is open every statement must be prepared on the
// transaction's own connection: the catalog pins the pool to a single
// connection, so reaching for the *sql.DB mid-transaction would block
// forever waiting for a connection the transaction already holds. These
// statements are owned by the transaction and die with it.
tx *sql.Tx
txStmts map[string]*sql.Stmt
}
func newStmtCache(db *sql.DB) *stmtCache {
return &stmtCache{db: db, stmts: make(map[string]*sql.Stmt)}
}
func (c *stmtCache) prepared(ctx context.Context, query string) (*sql.Stmt, error) {
c.mu.RLock()
tx, cache := c.tx, c.stmts
if tx != nil {
cache = c.txStmts
}
stmt, ok := cache[query]
c.mu.RUnlock()
if ok {
return stmt, nil
}
if tx != nil {
stmt, err := tx.PrepareContext(ctx, query)
if err != nil {
return nil, err
}
c.mu.Lock()
defer c.mu.Unlock()
// Bail out if the transaction ended while we were preparing; the
// statement is dead either way.
if c.tx != tx {
return stmt, nil
}
if existing, ok := c.txStmts[query]; ok {
stmt.Close()
return existing, nil
}
c.txStmts[query] = stmt
return stmt, nil
}
stmt, err := c.db.PrepareContext(ctx, query)
if err != nil {
return nil, err
}
c.mu.Lock()
defer c.mu.Unlock()
// Another goroutine may have prepared the same query while we were not
// holding the lock; keep the winner so each query has a single statement.
if existing, ok := c.stmts[query]; ok {
stmt.Close()
return existing, nil
}
c.stmts[query] = stmt
return stmt, nil
}
func (c *stmtCache) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) {
stmt, err := c.prepared(ctx, query)
if err != nil {
return nil, err
}
return stmt.ExecContext(ctx, args...)
}
func (c *stmtCache) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) {
stmt, err := c.prepared(ctx, query)
if err != nil {
return nil, err
}
return stmt.QueryContext(ctx, args...)
}
func (c *stmtCache) QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row {
stmt, err := c.prepared(ctx, query)
if err != nil {
// *sql.Row carries an error but has no exported constructor, so fall
// back to the unprepared path and let it report the failure on Scan.
return c.db.QueryRowContext(ctx, query, args...)
}
return stmt.QueryRowContext(ctx, args...)
}
// PrepareContext returns a statement owned by the caller; it is deliberately
// not cached, since the caller is responsible for closing it.
func (c *stmtCache) PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) {
return c.db.PrepareContext(ctx, query)
}
// begin opens a transaction that every subsequent statement runs inside, until
// commit or rollback. Batching writes this way saves SQLite a commit per
// statement, which is most of the cost of installing a dialect seed.
func (c *stmtCache) begin(ctx context.Context) error {
tx, err := c.db.BeginTx(ctx, nil)
if err != nil {
return err
}
c.mu.Lock()
c.tx = tx
c.txStmts = make(map[string]*sql.Stmt)
c.mu.Unlock()
return nil
}
// endTx detaches the current transaction and returns it, or nil if none is
// open. The transaction's statements are released along with it.
func (c *stmtCache) endTx() *sql.Tx {
c.mu.Lock()
defer c.mu.Unlock()
tx := c.tx
c.tx, c.txStmts = nil, nil
return tx
}
func (c *stmtCache) commit() error {
if tx := c.endTx(); tx != nil {
return tx.Commit()
}
return nil
}
func (c *stmtCache) rollback() {
if tx := c.endTx(); tx != nil {
tx.Rollback()
}
}
func (c *stmtCache) Close() error {
c.rollback()
c.mu.Lock()
defer c.mu.Unlock()
var firstErr error
for query, stmt := range c.stmts {
if err := stmt.Close(); err != nil && firstErr == nil {
firstErr = err
}
delete(c.stmts, query)
}
return firstErr
}