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
172 lines
4.5 KiB
Go
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
|
|
}
|