5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-14 16:09:31 +08:00
wails/v3/pkg/services/sqlite/sqlite.go
2025-02-13 03:11:22 +01:00

501 lines
13 KiB
Go

//wails:include stmt.js
package sqlite
import (
"bytes"
"context"
"database/sql"
"fmt"
"sync"
"sync/atomic"
"github.com/pkg/errors"
"github.com/wailsapp/wails/v3/pkg/application"
_ "modernc.org/sqlite"
)
type Config struct {
// DBSource is the database URI to use.
// The string ":memory:" can be used to create an in-memory database.
// The sqlite driver can be configured through query parameters.
// For more details see https://pkg.go.dev/modernc.org/sqlite#Driver.Open
DBSource string
}
//wails:inject export {
//wails:inject ExecContext as Execute,
//wails:inject QueryContext as Query
//wails:inject };
//wails:inject
//wails:inject import { Stmt } from "./stmt.js";
//wails:inject
//wails:inject **:/**
//wails:inject **: * Prepare creates a prepared statement for later queries or executions.
//wails:inject **: * Multiple queries or executions may be run concurrently from the returned statement.
//wails:inject **: *
//wails:inject **: * The caller must call the statement's Close method when it is no longer needed.
//wails:inject **: * Statements are closed automatically
//wails:inject **: * when the connection they are associated with is closed.
//wails:inject **: *
//wails:inject **: * Prepare supports early cancellation.
//wails:inject j*: *
//wails:inject j*: * @param {string} query
//wails:inject j*: * @returns {Promise<Stmt | null> & { cancel(): void }}
//wails:inject **: */
//wails:inject j*:export function Prepare(query) {
//wails:inject t*:export function Prepare(query: string): Promise<Stmt | null> & { cancel(): void } {
//wails:inject **: const promise = PrepareContext(query);
//wails:inject j*: const wrapper = /** @type {any} */(promise.then(function (id) {
//wails:inject t*: const wrapper: any = (promise.then(function (id) {
//wails:inject **: return id == null ? null : new Stmt(
//wails:inject **: ClosePrepared.bind(null, id),
//wails:inject **: ExecPrepared.bind(null, id),
//wails:inject **: QueryPrepared.bind(null, id));
//wails:inject **: }));
//wails:inject **: wrapper.cancel = promise.cancel;
//wails:inject **: return wrapper;
//wails:inject **:}
type Service struct {
lock sync.RWMutex
config *Config
conn *sql.DB
stmts map[uint64]struct{}
}
// New initialises a sqlite service with the default configuration.
func New() *Service {
return NewWithConfig(nil)
}
// NewWithConfig initialises a sqlite service with a custom configuration.
// If config is nil, it falls back to the default configuration, i.e. an in-memory database.
//
// The database connection is not opened right away.
// A call to [Service.Open] must succeed before using all other methods.
// If the service is registered with the application,
// [Service.Open] will be called automatically at startup.
func NewWithConfig(config *Config) *Service {
result := &Service{}
result.Configure(config)
return result
}
// ServiceName returns the name of the plugin.
// You should use the go module format e.g. github.com/myuser/myplugin
func (s *Service) ServiceName() string {
return "github.com/wailsapp/wails/v3/plugins/sqlite"
}
// ServiceStartup opens the database connection.
// It returns a non-nil error in case of failures.
func (s *Service) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
return s.Open()
}
// ServiceShutdown closes the database connection.
// It returns a non-nil error in case of failures.
func (s *Service) ServiceShutdown() error {
return s.Close()
}
// Configure changes the database service configuration.
// The connection state at call time is preserved.
// Consumers will need to call [Service.Open] manually after Configure
// in order to reconnect with the new configuration.
//
// See [NewWithConfig] for details on configuration.
//
//wails:ignore
func (s *Service) Configure(config *Config) {
if config == nil {
config = &Config{DBSource: ":memory:"}
} else {
// Clone to prevent changes from the outside.
clone := new(Config)
*clone = *config
config = clone
}
s.lock.Lock()
defer s.lock.Unlock()
s.config = config
}
// Open validates the current configuration,
// closes the current connection if one is present,
// then opens and validates a new connection.
//
// Even when a non-nil error is returned,
// the database service is left in a consistent state,
// ready for a new call to Open.
func (s *Service) Open() error {
s.lock.Lock()
defer s.lock.Unlock()
if s.config.DBSource == "" {
return errors.New(`no database source specified; please set DBSource in the config to a filename or specify ":memory:" to use an in-memory database`)
}
if err := s.closeImpl(); err != nil {
return err
}
conn, err := sql.Open("sqlite", s.config.DBSource)
if err != nil {
return errors.Wrap(err, "error opening database connection")
}
// Test connection
if err := conn.Ping(); err != nil {
_ = conn.Close()
return errors.Wrap(err, "error opening database connection")
}
s.conn = conn
s.stmts = make(map[uint64]struct{})
return nil
}
// Close closes the current database connection if one is open, otherwise has no effect.
// Additionally, Close closes all open prepared statements associated to the connection.
//
// Even when a non-nil error is returned,
// the database service is left in a consistent state,
// ready for a call to [Service.Open].
func (s *Service) Close() error {
s.lock.Lock()
defer s.lock.Unlock()
return s.closeImpl()
}
// closeImpl performs the close operation without acquiring the lock first.
// It is the caller's responsibility
// to ensure the lock is held exclusively (in write mode)
// for the entire duration of the call.
func (s *Service) closeImpl() error {
if s.conn == nil {
return nil
}
for id := range s.stmts {
if stmt, ok := stmts.Load(id); ok {
// WARN: do not delegate to [Stmt.Close], it would cause a deadlock.
// Ignore errors, closing the connection should free up all resources.
_ = stmt.(*Stmt).sqlStmt.Close()
}
}
err := s.conn.Close()
// Clear the connection even in case of errors:
// if [sql.DB.Close] returns an error,
// the connection becomes unusable.
s.conn = nil
s.stmts = nil
return err
}
// Execute executes a query without returning any rows.
//
//wails:ignore
func (s *Service) Execute(query string, args ...any) error {
return s.ExecContext(context.Background(), query, args...)
}
// ExecContext executes a query without returning any rows.
// It supports early cancellation.
//
//wails:internal
func (s *Service) ExecContext(ctx context.Context, query string, args ...any) error {
s.lock.RLock()
conn := s.conn
s.lock.RUnlock()
if conn == nil {
return errors.New("no open database connection")
}
_, err := conn.ExecContext(ctx, query, args...)
if err != nil && !errors.Is(err, context.Canceled) {
return err
}
return nil
}
// Query executes a query and returns a slice of key-value records,
// one per row, with column names as keys.
//
//wails:ignore
func (s *Service) Query(query string, args ...any) (Rows, error) {
return s.QueryContext(context.Background(), query, args...)
}
// QueryContext executes a query and returns a slice of key-value records,
// one per row, with column names as keys.
// It supports early cancellation, returning the slice of results fetched so far.
//
//wails:internal
func (s *Service) QueryContext(ctx context.Context, query string, args ...any) (Rows, error) {
s.lock.RLock()
conn := s.conn
s.lock.RUnlock()
if conn == nil {
return nil, errors.New("no open database connection")
}
rows, err := conn.QueryContext(ctx, query, args...)
if err != nil {
if errors.Is(err, context.Canceled) {
return Rows{}, nil
} else {
return nil, err
}
}
return parseRows(ctx, rows)
}
// Prepare creates a prepared statement for later queries or executions.
// Multiple queries or executions may be run concurrently from the returned statement.
//
// The caller should call the statement's Close method when it is no longer needed.
// Statements are closed automatically
// when the connection they are associated with is closed.
//
//wails:ignore
func (s *Service) Prepare(query string) (*Stmt, error) {
return s.PrepareContext(context.Background(), query)
}
// PrepareContext creates a prepared statement for later queries or executions.
// Multiple queries or executions may be run concurrently from the returned statement.
//
// The caller must call the statement's Close method when it is no longer needed.
// Statements are closed automatically
// when the connection they are associated with is closed.
//
// PrepareContext supports early cancellation.
//
//wails:internal
func (s *Service) PrepareContext(ctx context.Context, query string) (*Stmt, error) {
s.lock.RLock()
conn := s.conn
s.lock.RUnlock()
if conn == nil {
return nil, errors.New("no open database connection")
}
id := nextId.Load()
for id != 0 && !nextId.CompareAndSwap(id, id+1) {
}
if id == 0 {
return nil, errors.New("prepared statement ids exhausted")
}
stmt, err := conn.PrepareContext(ctx, query)
if err != nil {
if errors.Is(err, context.Canceled) {
return nil, nil
} else {
return nil, err
}
}
func() {
s.lock.Lock()
defer s.lock.Unlock()
s.stmts[id] = struct{}{}
}()
wrapper := &Stmt{
sqlStmt: stmt,
db: s,
id: id,
}
stmts.Store(id, wrapper)
return wrapper, nil
}
// ClosePrepared closes a prepared statement
// obtained with [Service.Prepare] or [Service.PrepareContext].
// ClosePrepared is idempotent:
// it has no effect on prepared statements that are already closed.
//
//wails:internal
func (s *Service) ClosePrepared(stmt *Stmt) error {
return stmt.Close()
}
// ExecPrepared executes a prepared statement
// obtained with [Service.Prepare] or [Service.PrepareContext]
// without returning any rows.
// It supports early cancellation.
//
//wails:internal
func (s *Service) ExecPrepared(ctx context.Context, stmt *Stmt, args ...any) error {
if stmt == nil {
return errors.New("no prepared statement provided")
} else if stmt.sqlStmt == nil {
return errors.New("prepared statement is not valid")
}
_, err := stmt.ExecContext(ctx, args...)
if err != nil && !errors.Is(err, context.Canceled) {
return err
}
return nil
}
// QueryPrepared executes a prepared statement
// obtained with [Service.Prepare] or [Service.PrepareContext]
// and returns a slice of key-value records, one per row, with column names as keys.
// It supports early cancellation, returning the slice of results fetched so far.
//
//wails:internal
func (s *Service) QueryPrepared(ctx context.Context, stmt *Stmt, args ...any) (Rows, error) {
if stmt == nil {
return nil, errors.New("no prepared statement provided")
} else if stmt.sqlStmt == nil {
return nil, errors.New("prepared statement is not valid")
}
rows, err := stmt.sqlStmt.QueryContext(ctx, args...)
if err != nil {
if errors.Is(err, context.Canceled) {
return Rows{}, nil
} else {
return nil, err
}
}
return parseRows(ctx, rows)
}
type (
// Row holds a single row in the result of a query.
// It is a key-value map where keys are column names.
Row = map[string]any
// Rows holds the result of a query
// as an array of key-value maps where keys are column names.
Rows = []Row
)
func parseRows(ctx context.Context, rows *sql.Rows) (Rows, error) {
defer rows.Close()
columns, _ := rows.Columns()
values := make([]any, len(columns))
pointers := make([]any, len(columns))
results := []map[string]any{}
for rows.Next() {
select {
default:
case <-ctx.Done():
return results, nil
}
for i := range values {
pointers[i] = &values[i]
}
if err := rows.Scan(pointers...); err != nil {
return nil, err
}
row := make(map[string]any, len(columns))
for i, column := range columns {
row[column] = values[i]
}
results = append(results, row)
}
return results, nil
}
var (
// stmts holds all currently active prepared statements,
// for all [Service] instances.
stmts sync.Map
// nextId holds the next available prepared statement id.
// We use a counter to make sure IDs are never reused.
nextId atomic.Uint64
)
func init() {
nextId.Store(1)
}
type (
sqlStmt = *sql.Stmt
// Stmt wraps a prepared sql statement pointer.
// It provides the same methods as the [sql.Stmt] type.
//
//wails:internal
Stmt struct {
sqlStmt
db *Service
id uint64
}
)
// Close closes the statement.
// It has no effect when the statement is already closed.
func (s *Stmt) Close() error {
if s == nil || s.sqlStmt == nil {
return nil
}
err := s.sqlStmt.Close()
stmts.Delete(s.id)
func() {
s.db.lock.Lock()
defer s.db.lock.Unlock()
delete(s.db.stmts, s.id)
}()
return errors.Wrap(err, "error closing prepared statement")
}
func (s *Stmt) MarshalText() ([]byte, error) {
var buf bytes.Buffer
buf.Grow(16)
if _, err := fmt.Fprintf(&buf, "%016x", s.id); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func (s *Stmt) UnmarshalText(text []byte) error {
if n, err := fmt.Fscanf(bytes.NewReader(text), "%x", &s.id); n < 1 || err != nil {
return errors.New("invalid prepared statement id")
}
if stmt, ok := stmts.Load(s.id); ok {
*s = *(stmt.(*Stmt))
} else {
s.sqlStmt = nil
s.db = nil
}
return nil
}