sqlite

package module
v0.0.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 4, 2026 License: MIT Imports: 16 Imported by: 0

README

SQLite with Buffered Batch Writes

A high-performance SQLite driver wrapper providing transparent read/write separation and buffered batch writes.

Features

  • Read/Write Separation: Dedicated reader and writer connections for optimal concurrency
  • Buffered Batch Writes: Aggregates multiple writes and flushes based on size threshold or time interval
  • Automatic Memory Tuning: Dynamically configures SQLite cache sizes based on available system memory
  • WAL Mode: Enabled by default for file databases, enabling concurrent reads during writes
  • Savepoint Support: Nested transaction support via SQLite savepoints
  • Thread-Safe Writer: Serialized writes through a single goroutine via channel-based coordination

Installation

go get github.com/cnlangzi/sqlite

Quick Start

import "github.com/cnlangzi/sqlite"

db, err := sqlite.Open(context.Background(), "mydb.db")
if err != nil {
    log.Fatal(err)
}
defer db.Close()

// Write operations (buffered)
_, err = db.Exec("INSERT INTO users (name) VALUES (?)", "alice")
if err != nil {
    log.Fatal(err)
}

// Read operations (concurrent)
row := db.QueryRow("SELECT name FROM users WHERE id = ?", 1)
var name string
row.Scan(&name)

Configuration

Buffer Configuration
db, err := sqlite.Open(ctx, "mydb.db")
if err != nil {
    log.Fatal(err)
}

// Customize buffer behavior
db.Writer.(*sqlite.Writer).Configure(sqlite.Buffer{
    BufferSize:    200,              // Number of statements before flush (default: 100)
    FlushInterval: 200 * time.Millisecond, // Maximum wait before flush (default: 100ms)
})
In-Memory Database
// Use ":memory:" for an in-memory database
db, err := sqlite.Open(ctx, ":memory:")

Architecture

Read/Write Separation

The DB wrapper maintains two separate *sql.DB connections:

Connection Purpose Configuration
Writer All write operations Single connection, serialized writes
Reader All read operations Connection pool scaled to 2 * runtime.NumCPU()
Buffered Batch Writes

The Writer transparently batches writes through a dedicated goroutine:

  1. Writes are sent via a channel to a single flush goroutine
  2. The goroutine collects writes into an in-memory transaction
  3. Flush occurs when:
    • Buffer reaches BufferSize threshold
    • FlushInterval elapses since last commit
    • Writer.Commit() is called
    • Writer.Close() is called
Automatic Memory Tuning

For file databases, cache sizes are automatically configured based on physical memory:

Parameter Target Min Max Fallback
Writer cache_size 5% RAM 32 MB 512 MB 100 MB
Reader cache_size 12.5% RAM 64 MB 2 GB 256 MB
Reader mmap_size 50% RAM 256 MB 128 GB 1 GB

API Reference

Open
func Open(ctx context.Context, dsn string) (*DB, error)

Opens a SQLite database. DSN can be a file path or :memory: for in-memory mode.

DB
type DB struct {
    Writer *Writer  // Buffered write operations
    Reader *sql.DB  // Direct read operations (connection pool)
}
Writer
// Execute a write statement
func (w *Writer) Exec(query string, args ...any) (sql.Result, error)

// Execute with context
func (w *Writer) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)

// Flush pending writes immediately
func (w *Writer) Commit() error

// Begin a buffered transaction
func (w *Writer) BeginTx(ctx context.Context, opts *sql.TxOptions) (*Tx, error)

// Close the writer (flushes and waits for completion)
func (w *Writer) Close() error
Tx (Buffered Transaction)
// Buffer a statement (does not execute immediately)
func (btx *Tx) Exec(query string, args ...any) (sql.Result, error)

// Execute all buffered statements atomically
func (btx *Tx) Commit() error

// Discard all buffered statements
func (btx *Tx) Rollback() error
Savepoint

For fine-grained transaction control within a batch:

sp, err := sqlite.NewSavepoint(tx)
if err != nil {
    log.Fatal(err)
}
// ... do work ...
sp.Release() // or sp.Rollback() to undo

Structured Logging

The package uses log/slog for structured logging at key points in the writer/buffer pipeline. Configure the slog level and handler as needed.

Log Schema
Level Message Fields Description
Debug task received buffer Task received by flush goroutine
Debug buffered buffer, size_limit Statement added to buffer
Debug tx started buffer New transaction begun
Debug commit triggered trigger, buffer Commit initiated (trigger: size/interval/manual/close)
Info committed trigger, count, elapsed Successful commit with duration
Error commit failed trigger, buffer, err Commit failure
Debug flush requested buffer Manual Flush() called
Warn task blocked buffer Channel full, task rejected
Info close signal received - Close signal received
Info drained on close remaining_buffer Final commit on close
Debug tx committed count Buffered Tx committed successfully
Debug tx rolled back count Buffered Tx rolled back
Example Log Output
{"time":"2024-01-15T10:30:00Z","level":"DEBUG","msg":"task received","buffer":5}
{"time":"2024-01-15T10:30:00Z","level":"DEBUG","msg":"buffered","buffer":6,"size_limit":100}
{"time":"2024-01-15T10:30:01Z","level":"DEBUG","msg":"commit triggered","trigger":"size","buffer":100}
{"time":"2024-01-15T10:30:01Z","level":"INFO","msg":"committed","trigger":"size","count":100,"elapsed":"1.234ms"}
{"time":"2024-01-15T10:30:05Z","level":"INFO","msg":"close signal received"}
{"time":"2024-01-15T10:30:05Z","level":"INFO","msg":"drained on close","remaining_buffer":0}
Log Levels
  • Debug: Routine flow (task received, buffered, tx started, commit triggered, flush requested, tx committed/rolled back)
  • Info: Meaningful business events (committed, close signal received, drained on close)
  • Warn: Backpressure (task blocked)
  • Error: Failures (commit failed)

Performance Characteristics

  • Writes: Serialized through a single goroutine; batching reduces transaction overhead
  • Reads: Concurrent via connection pool; WAL mode allows reads during writes
  • Memory: In-memory mode shares a single connection; file mode uses separate reader/writer connections
  • Durability: Writes are committed to WAL on flush interval; call Commit() for immediate persistence

Development

Makefile Targets

This project uses a Makefile for common development tasks:

# Run all tests with race detection
make test

# Run the linter
make lint

# Install the pre-commit hook (runs test + lint before each commit)
make hooks-install
Pre-commit Hook

After cloning the repository, install the pre-commit hook to automatically run tests and linting before each commit:

make hooks-install

The hook blocks commits if make test or make lint fail, ensuring the codebase stays clean and tests pass.

CI

Pull requests to main trigger a GitHub Actions workflow that runs:

  1. go vet — static analysis
  2. go build — verify compilation
  3. make test — run tests with race detection
  4. make lint — run golangci-lint

The workflow must pass before a PR can be merged.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrClosed = errors.New("sqlite: writer is closed")
View Source
var ErrTxNotready = errors.New("sqlite: transaction not ready")

Functions

This section is empty.

Types

type Action added in v0.0.4

type Action int
const (
	ActionQuery Action = iota
	ActionExec
	ActionBegin
	ActionCommit
	ActionRollback
)

type BufferConfig

type BufferConfig struct {
	// Size is the number of statements to buffer before triggering an
	// automatic flush. A transaction is committed when this threshold
	// is reached. Default: 100.
	Size int

	// FlushInterval is the maximum duration between commits. The writer
	// flushes pending statements if this interval elapses since the last
	// commit, even if Size has not been reached. Default: 100ms.
	FlushInterval time.Duration
}

BufferConfig holds batch writer configuration. The writer buffers incoming statements and flushes them in batches to reduce transaction overhead and improve throughput.

func DefaultBufferConfig added in v0.0.3

func DefaultBufferConfig() BufferConfig

DefaultBufferConfig returns a BufferConfig with defaults applied, reading from SQLITE_BUFFER_SIZE and SQLITE_BUFFER_FLUSH_INTERVAL env vars when set. Call this at Open time to respect environment configuration.

func (*BufferConfig) Validate

func (c *BufferConfig) Validate()

Validate sets default values for any zero fields and ensures the configuration is usable. It is called automatically by NewWriter; you only need to invoke it if you wish to check a config before use.

type BufferWriter added in v0.0.2

type BufferWriter struct {
	*sql.DB
	// contains filtered or unexported fields
}

BufferWriter provides transparent batch writing with channel-based coordination. All writes are serialized through a single goroutine via cmdCh.

func NewWriter

func NewWriter(db *sql.DB, cfg BufferConfig) *BufferWriter

NewWriter creates a Writer wrapping the provided *sql.DB. The returned Writer starts a background flush goroutine immediately. Close must be called to shut it down cleanly.

func (*BufferWriter) Begin added in v0.0.4

func (bw *BufferWriter) Begin() (*Tx, error)

func (*BufferWriter) BeginTx added in v0.0.2

func (bw *BufferWriter) BeginTx(ctx context.Context, opts *sql.TxOptions) (*Tx, error)

BeginTx acquires an exclusive write lock, flushes any pending buffered writes, and returns a Tx that executes statements immediately through the Writer's background goroutine. No other writes via Exec/ExecContext can proceed until Tx.Commit or Tx.Rollback is called.

func (*BufferWriter) Close added in v0.0.2

func (bw *BufferWriter) Close() error

Close signals the flush goroutine to stop, waits for it to drain and commit any remaining buffered work, then marks the writer as closed. It is safe to call Close multiple times concurrently; subsequent calls return nil immediately.

func (*BufferWriter) Exec added in v0.0.2

func (bw *BufferWriter) Exec(query string, args ...any) (sql.Result, error)

Exec executes a write statement (INSERT, UPDATE, DELETE, etc.) asynchronously. The statement is buffered and flushed according to the BufferConfig settings. This is the non-context variant; use ExecContext if you need cancellation or deadline control.

func (*BufferWriter) ExecContext added in v0.0.2

func (bw *BufferWriter) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)

ExecContext executes a write statement (INSERT, UPDATE, DELETE, etc.) asynchronously. The statement is buffered and flushed according to the BufferConfig settings. If the context is cancelled before the statement is processed, ctx.Err() is returned.

func (*BufferWriter) Flush added in v0.0.2

func (bw *BufferWriter) Flush() error

type DB

type DB struct {
	Writer *BufferWriter
	Reader *sql.DB
	// contains filtered or unexported fields
}

DB wraps a SQLite database with logical read/write separation. All writes go through the buffered Writer; all reads go directly to the Reader. This separation allows concurrent reads while writes are being buffered and committed by the background flush goroutine.

func New

func New(ctx context.Context, dsn string) (*DB, error)

New opens a SQLite database from the given DSN. If DSN is empty, it defaults to ":memory:" (an in-memory database). For file paths, the file is created if it does not exist. This is an alias for Open; prefer Open for clarity in application code.

func Open

func Open(ctx context.Context, dsn string) (*DB, error)

Open opens a SQLite database from the given DSN. If DSN is empty, it defaults to ":memory:" (an in-memory database). For file paths, the file is created if it does not exist. This is the preferred constructor.

func (*DB) BeginTx added in v0.0.4

func (db *DB) BeginTx(ctx context.Context, opts *sql.TxOptions) (*Tx, error)

func (*DB) Close

func (db *DB) Close() error

Close shuts down the writer flush goroutine (committing any pending writes) and closes both the Writer and Reader database connections. Always call Close when you are done with the DB to avoid leaking goroutines or connections.

func (*DB) Exec

func (db *DB) Exec(query string, args ...interface{}) (sql.Result, error)

Exec delegates to Writer.Exec, executing a write statement (INSERT, UPDATE, DELETE, CREATE TABLE, etc.) asynchronously with buffering. See Writer.Exec for details on buffering behavior.

func (*DB) ExecContext

func (db *DB) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error)

ExecContext delegates to Writer.ExecContext, executing a write statement with context cancellation support. See Writer.ExecContext for details.

func (*DB) Flush added in v0.0.2

func (db *DB) Flush() error

func (*DB) Query

func (db *DB) Query(query string, args ...interface{}) (*sql.Rows, error)

Query delegates to Reader.Query, executing a read-only query on the dedicated reader connection pool. Returns *sql.Rows that must be closed. Reads are served concurrently with writes and do not block the Writer.

func (*DB) QueryContext

func (db *DB) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error)

QueryContext delegates to Reader.QueryContext, executing a read-only query with context cancellation support. Returns *sql.Rows that must be closed.

func (*DB) QueryRow

func (db *DB) QueryRow(query string, args ...interface{}) *sql.Row

QueryRow delegates to Reader.QueryRow, executing a read-only query that returns at most one row. This is more efficient than Query when you only need a single row. Returns *sql.Row (not an error) even if no row is found.

func (*DB) QueryRowContext

func (db *DB) QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row

QueryRowContext delegates to Reader.QueryRowContext, executing a read-only query with context cancellation support and returning at most one row.

type Savepoint

type Savepoint struct {
	// contains filtered or unexported fields
}

Savepoint represents a named SQLite savepoint within a transaction. Savepoints allow partial rollback of a transaction without aborting it entirely. The savepoint is created with a cryptographically random name to avoid collisions when multiple savepoints are used concurrently.

func NewSavepoint

func NewSavepoint(tx *sql.Tx) (*Savepoint, error)

NewSavepoint creates and names a new SQLite savepoint within the given transaction. The savepoint is immediately active. Use Release() to commit the savepoint or Rollback() to undo all statements since it was created.

func (*Savepoint) Name

func (s *Savepoint) Name() string

Name returns the SQLite savepoint name. Useful for debugging and for executing raw ROLLBACK TO or RELEASE statements if needed.

func (*Savepoint) Release

func (s *Savepoint) Release()

Release commits the savepoint, making all changes since its creation permanent. After Release is called, ok is set to false and subsequent calls are no-ops.

func (*Savepoint) Rollback

func (s *Savepoint) Rollback()

Rollback undoes all database changes made after the savepoint was created. After Rollback is called, ok is set to false and subsequent calls are no-ops.

type Task

type Task struct {
	// contains filtered or unexported fields
}

Task is a concrete implementation of TaskFunc that wraps a query and its execution function. It is created by the package-level helper functions Query, QueryRow, Exec, and Commit.

func (*Task) Action added in v0.0.4

func (t *Task) Action() Action

func (*Task) Exec

func (t *Task) Exec(bw *BufferWriter) TaskResult

Exec implements TaskFunc by running the stored execution function.

func (*Task) Notify

func (t *Task) Notify() chan TaskResult

Notify implements TaskFunc by returning the result channel.

type TaskFunc

type TaskFunc interface {
	// Exec executes the task using the provided transaction. It returns
	// the result of the execution (e.g., sql.Result for writes) or an error.
	Exec(bw *BufferWriter) TaskResult

	// Notify returns a channel that receives the TaskResult once Exec completes.
	// The caller uses this to wait for the task to finish.
	Notify() chan TaskResult

	Action() Action
}

TaskFunc is the interface implemented by tasks submitted to the Writer. It represents a unit of work to be executed within a SQLite transaction.

func BeginTx added in v0.0.4

func BeginTx() TaskFunc

func Commit

func Commit() TaskFunc

Commit creates a TaskFunc that commits the current transaction. It also sets Flush() to true, ensuring any pending buffered writes are flushed before this task is processed.

func Exec

func Exec(query string, args ...any) TaskFunc

Exec creates a TaskFunc that executes a write statement (INSERT, UPDATE, DELETE, etc.) with the given arguments. The statement is not executed until the Writer's flush goroutine processes the task.

func Query

func Query(query string, args ...any) TaskFunc

Query creates a read-only TaskFunc that executes a query with the given arguments and returns a *sql.Row. The query is not executed until the Writer's flush goroutine processes the task.

func QueryRow

func QueryRow(query string, args ...any) TaskFunc

QueryRow creates a TaskFunc that executes a query returning at most one row. It is equivalent to Query but provided for symmetry with the database/sql API.

func Rollback added in v0.0.4

func Rollback() TaskFunc

type TaskResult

type TaskResult struct {
	Result   any   // the execution result, typically a sql.Result or *sql.Row
	Buffered bool  // true if the task was buffered and not executed immediately
	Error    error // any error that occurred during execution
}

TaskResult holds the outcome of a task execution.

type Tx

type Tx struct {
	// contains filtered or unexported fields
}

Tx is an exclusive transaction that holds the Writer's write lock for its entire lifetime. All Exec calls are executed immediately through the Writer's background goroutine within the shared transaction. No other writes via Writer.Exec or Writer.ExecContext can proceed while a Tx is active. The lock is released when Commit or Rollback is called.

func (*Tx) Commit

func (btx *Tx) Commit() error

Commit flushes the exclusive transaction and releases the write lock. After Commit returns, the Tx cannot be reused.

func (*Tx) Exec

func (btx *Tx) Exec(query string, args ...any) (sql.Result, error)

Exec immediately sends a write statement to the Writer's background goroutine for execution within the active exclusive transaction. Returns the real sql.Result (RowsAffected, LastInsertId) unlike the previous buffered implementation.

func (*Tx) Rollback

func (btx *Tx) Rollback() error

Rollback rolls back the exclusive transaction and releases the write lock. If the Tx has already been committed or rolled back, Rollback is a no-op.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL