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

162 lines
5.0 KiB
Go

package core
import (
"context"
"database/sql"
"errors"
"fmt"
"log/slog"
"github.com/ncruces/go-sqlite3"
"github.com/ncruces/go-sqlite3/ext/serdes"
"github.com/sqlc-dev/sqlc/internal/cache"
"github.com/sqlc-dev/sqlc/internal/core/catalogdb"
)
// The name of the sole output blob a CoreCatalog action produces.
const catalogOutput = "catalog.db"
// rawConn is the driver's escape hatch to the SQLite connection underneath a
// database/sql connection, which the serialization API needs.
type rawConn interface {
Raw() *sqlite3.Conn
}
// NewCached returns the catalog a dialect and a schema produce, restored from
// the cache when a run has produced it before.
//
// Producing one means seeding the dialect — thousands of rows, since
// PostgreSQL's functions and system catalogs alone run to five figures — and
// then parsing the schema and applying its DDL. The result is the same every
// time, so the finished database is stored in the cache and opened directly on
// the next run, turning all of that work into opening a file.
// It is a single action: the dialect and the schema together are what the
// catalog is, so they are one key, and a hit means apply is never called.
//
// The dialect data is embedded in the sqlc binary, whose digest is an input to
// every action, so the dialect only has to be named. The schema is hashed in
// the order it is applied, since DDL order decides what the catalog holds.
//
// A catalog whose apply fails is returned but not cached, so that a caller can
// report the failure against the catalog it got. A nil catalog means the
// catalog could not be created at all. A cache that cannot be opened, read or
// written is not an error: the catalog is simply built the long way.
func NewCached(dialect string, schema []string, apply func(*Catalog) error, opts ...Option) (*Catalog, error) {
store, action := openAction(dialect, schema)
if store != nil {
defer store.Close()
if cat, err := restore(store, action); err == nil {
return cat, nil
} else if !errors.Is(err, cache.ErrNotFound) {
slog.Debug("restoring the catalog from the cache failed", "err", err)
}
}
cat, err := New(opts...)
if err != nil {
return nil, err
}
if err := apply(cat); err != nil {
return cat, err
}
if store != nil {
if err := save(store, action, cat); err != nil {
slog.Debug("saving the catalog to the cache failed", "err", err)
}
}
return cat, nil
}
// openAction opens the cache and digests the action that produces a catalog. A
// nil store means the cache is unavailable and the catalog has to be built.
func openAction(dialect string, schema []string) (*cache.Cache, cache.Digest) {
store, err := cache.Open()
if err != nil {
slog.Debug("opening the cache failed", "err", err)
return nil, cache.Digest{}
}
action := store.NewAction("CoreCatalog").AddInput("dialect", []byte(dialect))
for _, s := range schema {
action.AddInput("schema", []byte(s))
}
return store, action.Digest()
}
// restore opens the catalog a previous run cached. The stored blob is opened
// where it lies rather than copied: analysis only ever reads the catalog, so
// every run that wants this one reads the same file.
func restore(store *cache.Cache, action cache.Digest) (*Catalog, error) {
result, err := store.Actions.Get(action)
if err != nil {
return nil, err
}
path, ok := store.CAS.Filename(result.Outputs[catalogOutput])
if !ok {
return nil, cache.ErrNotFound
}
db, err := openFile(path)
if err != nil {
return nil, err
}
stmts := newStmtCache(db)
cat := &Catalog{db: db, stmts: stmts, q: catalogdb.New(stmts)}
// The seeds recorded the dialect on the catalog as they ran. A restored
// catalog reads it back from the database instead.
row, err := cat.q.SeededDialect(context.Background())
if err != nil {
cat.Close()
return nil, fmt.Errorf("core: restored catalog has no dialect: %w", err)
}
cat.dialectOID = row.Oid
return cat, nil
}
// save stores a finished catalog for the next run.
func save(store *cache.Cache, action cache.Digest, cat *Catalog) error {
blob, err := serialize(cat.db)
if err != nil {
return err
}
digest, err := store.CAS.Put(blob)
if err != nil {
return err
}
return store.Actions.Put(action, &cache.ActionResult{
Outputs: map[string]cache.Digest{catalogOutput: digest},
})
}
// serialize returns the bytes a database being built in memory would be
// written to disk as, which is what gets stored.
func serialize(db *sql.DB) ([]byte, error) {
var blob []byte
err := withRawConn(db, func(c *sqlite3.Conn) error {
var err error
blob, err = serdes.Serialize(c, "main")
return err
})
if err != nil {
return nil, fmt.Errorf("core: serialize catalog: %w", err)
}
return blob, nil
}
func withRawConn(db *sql.DB, fn func(*sqlite3.Conn) error) error {
conn, err := db.Conn(context.Background())
if err != nil {
return err
}
defer conn.Close()
return conn.Raw(func(driverConn any) error {
r, ok := driverConn.(rawConn)
if !ok {
return fmt.Errorf("driver does not expose its SQLite connection")
}
return fn(r.Raw())
})
}