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

188 lines
3.7 KiB
Go

package query
import (
"context"
"github.com/chaisql/chai/internal/database"
"github.com/chaisql/chai/internal/environment"
"github.com/chaisql/chai/internal/query/statement"
)
// A Query can execute statements against the database. It can read or write data
// from any table, or even alter the structure of the database.
// Results are returned as streams.
type Query struct {
Statements []statement.Statement
tx *database.Transaction
autoCommit bool
}
// New creates a new query with the given statements.
func New(statements ...statement.Statement) Query {
return Query{Statements: statements}
}
type Context struct {
Ctx context.Context
DB *database.Database
Conn *database.Connection
Params []environment.Param
}
func (c *Context) GetTx() *database.Transaction {
return c.Conn.GetTx()
}
// Prepare the statements by calling their Prepare methods.
// It stops at the first statement that doesn't implement the statement.Preparer interface.
func (q *Query) Prepare(context *Context) error {
var err error
var tx *database.Transaction
ctx := context.Ctx
for i, stmt := range q.Statements {
if ctx != nil {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
}
p, ok := stmt.(statement.Preparer)
if !ok {
return nil
}
if tx == nil {
tx = context.GetTx()
if tx == nil {
tx, err = context.Conn.BeginTx(&database.TxOptions{
ReadOnly: true,
})
if err != nil {
return err
}
defer tx.Rollback()
}
}
stmt, err := p.Prepare(&statement.Context{
DB: context.DB,
Conn: context.Conn,
Tx: tx,
})
if err != nil {
return err
}
q.Statements[i] = stmt
}
return nil
}
// Run executes all the statements in their own transaction and returns the last result.
func (q Query) Run(context *Context) (*statement.Result, error) {
var res statement.Result
var err error
q.tx = context.GetTx()
if q.tx == nil {
q.autoCommit = true
}
ctx := context.Ctx
for i, stmt := range q.Statements {
if ctx != nil {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
}
// reinitialize the result
res = statement.Result{}
if qa, ok := stmt.(queryAlterer); ok {
err = qa.alterQuery(context.Conn, &q)
if err != nil {
if tx := context.GetTx(); tx != nil {
_ = tx.Rollback()
}
return nil, err
}
continue
}
if q.tx == nil {
q.tx, err = context.Conn.BeginTx(&database.TxOptions{
ReadOnly: stmt.IsReadOnly(),
})
if err != nil {
return nil, err
}
}
res, err = stmt.Run(&statement.Context{
DB: context.DB,
Conn: context.Conn,
Tx: q.tx,
Params: context.Params,
})
if err != nil {
if q.autoCommit {
q.tx.Rollback()
}
return nil, err
}
// if there are still statements to be executed,
// and the current statement is not read-only,
// iterate over the result.
if !stmt.IsReadOnly() && i+1 < len(q.Statements) {
err = res.Iterate(func(database.Row) error { return nil })
if err != nil {
if q.autoCommit {
q.tx.Rollback()
}
return nil, err
}
}
// it there is an opened transaction but there are still statements
// to be executed, close the current transaction.
if q.tx != nil && q.autoCommit && i+1 < len(q.Statements) {
if q.tx.Writable {
err := q.tx.Commit()
if err != nil {
return nil, err
}
} else {
err := q.tx.Rollback()
if err != nil {
return nil, err
}
}
q.tx = nil
}
}
if q.autoCommit {
// the returned result will now own the transaction.
// its Close method is expected to be called.
res.Tx = q.tx
}
return &res, nil
}
type queryAlterer interface {
alterQuery(conn *database.Connection, q *Query) error
}