mirror of
https://github.com/chaisql/chai.git
synced 2025-09-27 03:55:59 +08:00
260 lines
5.5 KiB
Go
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()
|
|
}
|