Files
chaisql/internal/database/database.go
Asdine El Hrychy 6bc4992d70 db: add Connection
2024-02-20 09:38:56 +04:00

260 lines
5.5 KiB
Go

// Package database provides database primitives such as tables, transactions and indexes.
package database
import (
"context"
"sync"
"sync/atomic"
"time"
"github.com/chaisql/chai/internal/engine"
"github.com/chaisql/chai/internal/kv"
"github.com/cockroachdb/errors"
)
const (
InternalPrefix = "__chai_"
)
type Database struct {
catalogMu sync.RWMutex
catalog *Catalog
// context used to notify all connections that the database is closing.
closeContext context.Context
closeCancel context.CancelFunc
// waitgroup to wait for all connections to be closed.
connectionWg sync.WaitGroup
// This is used to prevent creating a new transaction
// during certain operations (commit, close, etc.)
txmu sync.RWMutex
// This limits the number of write transactions to 1.
writetxmu sync.Mutex
// transactionIDs is used to assign transaction an ID at runtime.
// Since transaction IDs are not persisted and not used for concurrent
// access, we can use 8 bytes ids that will be reset every time
// the database restarts.
transactionIDs atomic.Uint64
closeOnce sync.Once
// Underlying kv store.
Engine engine.Engine
}
// Options are passed to Open to control
// how the database is loaded.
type Options struct {
CatalogLoader func(tx *Transaction) error
}
// CatalogLoader loads the catalog from the disk.
// It may parse a SQL representation of the catalog
// and return a Catalog that represents all entities stored on disk.
type CatalogLoader interface {
LoadCatalog(engine.Session) (*Catalog, error)
}
// TxOptions are passed to Begin to configure transactions.
type TxOptions struct {
// Open a read-only transaction.
ReadOnly bool
}
func Open(path string, opts *Options) (*Database, error) {
store, err := kv.NewEngine(path, kv.Options{
RollbackSegmentNamespace: int64(RollbackSegmentNamespace),
MinTransientNamespace: uint64(MinTransientNamespace),
MaxTransientNamespace: uint64(MaxTransientNamespace),
})
if err != nil {
return nil, err
}
db := Database{
Engine: store,
}
// create a context that will be cancelled when the database is closed.
db.closeContext, db.closeCancel = context.WithCancel(context.Background())
// ensure the rollback segment doesn't contain any data that needs to be rolled back
// due to a previous crash.
err = db.Engine.Recover()
if err != nil {
return nil, err
}
// clean up the transient namespaces
err = db.Engine.CleanupTransientNamespaces()
if err != nil {
return nil, err
}
tx, err := db.Begin(true)
if err != nil {
return nil, err
}
defer tx.Rollback()
db.catalog = NewCatalog()
tx.Catalog = db.catalog
if opts.CatalogLoader != nil {
err = opts.CatalogLoader(tx)
if err != nil {
return nil, errors.Wrap(err, "failed to load catalog")
}
} else {
err = tx.CatalogWriter().Init(tx)
if err != nil {
return nil, err
}
}
err = tx.Commit()
if err != nil {
return nil, err
}
return &db, nil
}
// Close the database.
func (db *Database) Close() error {
var err error
db.closeOnce.Do(func() {
db.closeCancel()
db.connectionWg.Wait()
err = db.closeDatabase()
})
return err
}
func (db *Database) closeDatabase() error {
// release all sequences
tx, err := db.beginTxUnlocked(nil)
if err != nil {
return err
}
defer tx.Session.Close()
for _, seqName := range tx.Catalog.ListSequences() {
seq, err := tx.Catalog.GetSequence(seqName)
if err != nil {
return err
}
err = seq.Release(tx)
if err != nil {
return err
}
}
err = tx.Session.Commit()
if err != nil {
return err
}
return db.Engine.Close()
}
// Connect returns a new connection to the database.
// The returned connection is not thread safe.
// It is the caller's responsibility to close the connection.
func (db *Database) Connect() (*Connection, error) {
if db.closeContext.Err() != nil {
return nil, errors.New("database is closed")
}
db.connectionWg.Add(1)
return &Connection{
db: db,
ctx: db.closeContext,
}, nil
}
// Begin starts a new transaction with default options.
// The returned transaction must be closed either by calling Rollback or Commit.
func (db *Database) Begin(writable bool) (*Transaction, error) {
if db.closeContext.Err() != nil {
return nil, errors.New("database is closed")
}
return db.beginTx(&TxOptions{
ReadOnly: !writable,
})
}
// BeginTx starts a new transaction with the given options.
// If opts is empty, it will use the default options.
// The returned transaction must be closed either by calling Rollback or Commit.
func (db *Database) beginTx(opts *TxOptions) (*Transaction, error) {
if db.closeContext.Err() != nil {
return nil, errors.New("database is closed")
}
db.txmu.RLock()
defer db.txmu.RUnlock()
if opts == nil {
opts = new(TxOptions)
}
if !opts.ReadOnly {
db.writetxmu.Lock()
}
return db.beginTxUnlocked(opts)
}
// beginTxUnlocked creates a transaction without locks.
func (db *Database) beginTxUnlocked(opts *TxOptions) (*Transaction, error) {
if opts == nil {
opts = &TxOptions{}
}
var sess engine.Session
if opts.ReadOnly {
sess = db.Engine.NewSnapshotSession()
} else {
sess = db.Engine.NewBatchSession()
}
tx := Transaction{
db: db,
Engine: db.Engine,
Session: sess,
Writable: !opts.ReadOnly,
ID: db.transactionIDs.Add(1),
Catalog: db.Catalog(),
TxStart: time.Now(),
}
if !opts.ReadOnly {
tx.WriteTxMu = &db.writetxmu
}
return &tx, nil
}
func (db *Database) Catalog() *Catalog {
db.catalogMu.RLock()
c := db.catalog
db.catalogMu.RUnlock()
return c
}
func (db *Database) SetCatalog(c *Catalog) {
db.catalogMu.Lock()
db.catalog = c
db.catalogMu.Unlock()
}