diff --git a/backend/turso/README.md b/backend/turso/README.md new file mode 100644 index 00000000..d7e67897 --- /dev/null +++ b/backend/turso/README.md @@ -0,0 +1,8 @@ +# Sqlite backend + +## Adding a migration + +1. Install [golang-migrate/migrate](https://www.github.com/golang-migrate/migrate) +1. ```bash + migrate create -ext sql -dir ./db/migrations -seq + ``` \ No newline at end of file diff --git a/backend/turso/activities.go b/backend/turso/activities.go new file mode 100644 index 00000000..0a81a740 --- /dev/null +++ b/backend/turso/activities.go @@ -0,0 +1,31 @@ +package turso + +import ( + "context" + "database/sql" + "fmt" + + "github.com/cschleiden/go-workflows/backend/history" + "github.com/cschleiden/go-workflows/core" +) + +func scheduleActivity(ctx context.Context, tx *sql.Tx, instance *core.WorkflowInstance, event *history.Event) error { + // Attributes are already persisted via the history, we do not need to add them again. + + if _, err := tx.ExecContext( + ctx, + `INSERT INTO activities + (id, instance_id, execution_id, event_type, timestamp, schedule_event_id, visible_at) VALUES (?, ?, ?, ?, ?, ?, ?)`, + event.ID, + instance.InstanceID, + instance.ExecutionID, + event.Type, + event.Timestamp, + event.ScheduleEventID, + event.VisibleAt, + ); err != nil { + return fmt.Errorf("inserting events: %w", err) + } + + return nil +} diff --git a/backend/turso/db/migrations/000001_initial.down.sql b/backend/turso/db/migrations/000001_initial.down.sql new file mode 100644 index 00000000..928470b9 --- /dev/null +++ b/backend/turso/db/migrations/000001_initial.down.sql @@ -0,0 +1,4 @@ +DROP TABLE IF EXISTS `instances`; +DROP TABLE IF EXISTS `pending_events`; +DROP TABLE IF EXISTS `history`; +DROP TABLE IF EXISTS `activities`; \ No newline at end of file diff --git a/backend/turso/db/migrations/000001_initial.up.sql b/backend/turso/db/migrations/000001_initial.up.sql new file mode 100644 index 00000000..c8ff4256 --- /dev/null +++ b/backend/turso/db/migrations/000001_initial.up.sql @@ -0,0 +1,67 @@ +CREATE TABLE IF NOT EXISTS `instances` ( + `id` TEXT NOT NULL, + `execution_id` TEXT NOT NULL, + `parent_instance_id` TEXT NULL, + `parent_execution_id` TEXT NULL, + `parent_schedule_event_id` INTEGER NULL, + `metadata` TEXT NULL, + `state` INTEGER NOT NULL, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `completed_at` DATETIME NULL, + `locked_until` DATETIME NULL, + `sticky_until` DATETIME NULL, + `worker` TEXT NULL, + PRIMARY KEY(`id`, `execution_id`) +); + +CREATE INDEX IF NOT EXISTS `idx_instances_id_execution_id` ON `instances` (`id`, `execution_id`); +CREATE INDEX IF NOT EXISTS `idx_instances_locked_until_completed_at` ON `instances` (`locked_until`, `sticky_until`, `completed_at`, `worker`); +CREATE INDEX IF NOT EXISTS `idx_instances_parent_instance_id_parent_execution_id` ON `instances` (`parent_instance_id`, `parent_execution_id`); + +CREATE TABLE IF NOT EXISTS `pending_events` ( + `id` TEXT, + `sequence_id` INTEGER NOT NULL, -- not used but keep for now for query compat + `instance_id` TEXT NOT NULL, + `execution_id` TEXT NOT NULL, + `event_type` INTEGER NOT NULL, + `timestamp` DATETIME NOT NULL, + `schedule_event_id` INT NOT NULL, + `attributes` BLOB NOT NULL, + `visible_at` DATETIME NULL, + PRIMARY KEY(`id`, `instance_id`) +); + +CREATE INDEX IF NOT EXISTS `idx_pending_events_instance_id_execution_id_visible_at_schedule_event_id` ON `pending_events` (`instance_id`, `execution_id`, `visible_at`, `schedule_event_id`); + +CREATE TABLE IF NOT EXISTS `history` ( + `id` TEXT, + `sequence_id` INTEGER NOT NULL, + `instance_id` TEXT NOT NULL, + `execution_id` TEXT NOT NULL, + `event_type` INTEGER NOT NULL, + `timestamp` DATETIME NOT NULL, + `schedule_event_id` INT NOT NULL, + `attributes` BLOB NOT NULL, + `visible_at` DATETIME NULL, + PRIMARY KEY(`id`, `instance_id`) +); + +CREATE INDEX IF NOT EXISTS `idx_history_instance_sequence_id` ON `history` (`instance_id`, `execution_id`, `sequence_id`); + +CREATE TABLE IF NOT EXISTS `activities` ( + `id` TEXT PRIMARY KEY, + `instance_id` TEXT NOT NULL, + `execution_id` TEXT NOT NULL, + `event_type` INTEGER NOT NULL, + `timestamp` DATETIME NOT NULL, + `schedule_event_id` INT NOT NULL, + `attributes` BLOB NOT NULL, + `visible_at` DATETIME NULL, + `locked_until` DATETIME NULL, + `worker` TEXT NULL +); + + +CREATE INDEX IF NOT EXISTS `idx_activities_id_worker` ON `activities` (`id`, `worker`); +CREATE INDEX IF NOT EXISTS `idx_activities_locked_until` ON `activities` (`locked_until`); +CREATE INDEX IF NOT EXISTS `idx_activities_instance_id_execution_id_worker` ON `activities` (`instance_id`, `execution_id`, `worker`); diff --git a/backend/turso/db/migrations/000002_add_attributes_table.down.sql b/backend/turso/db/migrations/000002_add_attributes_table.down.sql new file mode 100644 index 00000000..f4d29de8 --- /dev/null +++ b/backend/turso/db/migrations/000002_add_attributes_table.down.sql @@ -0,0 +1,11 @@ +ALTER TABLE `activities` ADD COLUMN `attributes` BLOB NULL; +UPDATE `activities` SET `attributes` = `attributes`.`data` FROM `attributes` WHERE `activities`.`id` = `attributes`.`id` AND `activities`.`instance_id` = `attributes`.`instance_id` AND `activities`.`execution_id` = `attributes`.`execution_id`; + +ALTER TABLE `history` ADD COLUMN `attributes` BLOB NULL; +UPDATE `history` SET `attributes` = `attributes`.`data` FROM `attributes` WHERE `history`.`id` = `attributes`.`id` AND `history`.`instance_id` = `attributes`.`instance_id` AND `history`.`execution_id` = `attributes`.`execution_id`; + +ALTER TABLE `pending_events` ADD COLUMN `attributes` BLOB NULL; +UPDATE `pending_events` SET `attributes` = `attributes`.`data` FROM `attributes` WHERE `pending_events`.`id` = `attributes`.`id` AND `pending_events`.`instance_id` = `attributes`.`instance_id` AND `pending_events`.`execution_id` = `attributes`.`execution_id`; + +-- Drop attributes table +DROP TABLE `attributes`; \ No newline at end of file diff --git a/backend/turso/db/migrations/000002_add_attributes_table.up.sql b/backend/turso/db/migrations/000002_add_attributes_table.up.sql new file mode 100644 index 00000000..bb390cad --- /dev/null +++ b/backend/turso/db/migrations/000002_add_attributes_table.up.sql @@ -0,0 +1,19 @@ +CREATE TABLE IF NOT EXISTS `attributes` ( + `id` TEXT NOT NULL, + `instance_id` TEXT NOT NULL, + `execution_id` TEXT NOT NULL, + `data` BLOB NOT NULL, + PRIMARY KEY(`id`, `instance_id`, `execution_id`) +); + +-- Move activity attributes to attributes table +INSERT OR IGNORE INTO `attributes` (`id`, `instance_id`, `execution_id`, `data`) SELECT `id`, `instance_id`, `execution_id`, `attributes` FROM `activities`; +ALTER TABLE `activities` DROP COLUMN `attributes`; + +-- Move history attributes to attributes table +INSERT OR IGNORE INTO `attributes` (`id`, `instance_id`, `execution_id`, `data`) SELECT `id`, `instance_id`, `execution_id`, `attributes` FROM `history`; +ALTER TABLE `history` DROP COLUMN `attributes`; + +-- Move pending_events attributes to attributes table +INSERT OR IGNORE INTO `attributes` (`id`, `instance_id`, `execution_id`, `data`) SELECT `id`, `instance_id`, `execution_id`, `attributes` FROM `pending_events`; +ALTER TABLE `pending_events` DROP COLUMN `attributes`; \ No newline at end of file diff --git a/backend/turso/diagnostics.go b/backend/turso/diagnostics.go new file mode 100644 index 00000000..af85891e --- /dev/null +++ b/backend/turso/diagnostics.go @@ -0,0 +1,119 @@ +package turso + +import ( + "context" + "database/sql" + "time" + + "github.com/cschleiden/go-workflows/core" + "github.com/cschleiden/go-workflows/diag" +) + +var _ diag.Backend = (*tursoBackend)(nil) + +func (sb *tursoBackend) GetWorkflowInstances(ctx context.Context, afterInstanceID, afterExecutionID string, count int) ([]*diag.WorkflowInstanceRef, error) { + var err error + tx, err := sb.db.BeginTx(ctx, nil) + if err != nil { + return nil, err + } + defer tx.Rollback() + + var rows *sql.Rows + if afterInstanceID != "" { + rows, err = tx.QueryContext( + ctx, + `SELECT i.id, i.execution_id, i.created_at, i.completed_at + FROM instances i + INNER JOIN (SELECT id, created_at FROM instances WHERE id = ? AND execution_id = ?) ii + ON i.created_at < ii.created_at OR (i.created_at = ii.created_at AND i.id < ii.id) + ORDER BY i.created_at DESC, i.id DESC + LIMIT ?`, + afterInstanceID, + afterExecutionID, + count, + ) + } else { + rows, err = tx.QueryContext( + ctx, + `SELECT i.id, i.execution_id, i.created_at, i.completed_at + FROM instances i + ORDER BY i.created_at DESC, i.id DESC + LIMIT ?`, + count, + ) + } + if err != nil { + return nil, err + } + + defer rows.Close() + + var instances []*diag.WorkflowInstanceRef + + for rows.Next() { + var id, executionID string + var createdAt time.Time + var completedAt *time.Time + err = rows.Scan(&id, &executionID, &createdAt, &completedAt) + if err != nil { + return nil, err + } + + var state core.WorkflowInstanceState + if completedAt != nil { + state = core.WorkflowInstanceStateFinished + } + + instances = append(instances, &diag.WorkflowInstanceRef{ + Instance: core.NewWorkflowInstance(id, executionID), + CreatedAt: createdAt, + CompletedAt: completedAt, + State: state, + }) + } + + tx.Commit() + + return instances, nil +} + +func (sb *tursoBackend) GetWorkflowInstance(ctx context.Context, instance *core.WorkflowInstance) (*diag.WorkflowInstanceRef, error) { + tx, err := sb.db.BeginTx(ctx, nil) + if err != nil { + return nil, err + } + defer tx.Rollback() + + res := tx.QueryRowContext(ctx, "SELECT id, execution_id, created_at, completed_at FROM instances WHERE id = ? AND execution_id = ?", instance.InstanceID, instance.ExecutionID) + + var id, executionID string + var createdAt time.Time + var completedAt *time.Time + + err = res.Scan(&id, &executionID, &createdAt, &completedAt) + if err != nil { + if err == sql.ErrNoRows { + return nil, nil + } + + return nil, err + } + + var state core.WorkflowInstanceState + if completedAt != nil { + state = core.WorkflowInstanceStateFinished + } + + return &diag.WorkflowInstanceRef{ + Instance: core.NewWorkflowInstance(id, executionID), + CreatedAt: createdAt, + CompletedAt: completedAt, + State: state, + }, nil +} + +func (sb *tursoBackend) GetWorkflowTree(ctx context.Context, instance *core.WorkflowInstance) (*diag.WorkflowInstanceTree, error) { + itb := diag.NewInstanceTreeBuilder(sb) + return itb.BuildWorkflowInstanceTree(ctx, instance) +} diff --git a/backend/turso/events.go b/backend/turso/events.go new file mode 100644 index 00000000..e7cafdd1 --- /dev/null +++ b/backend/turso/events.go @@ -0,0 +1,254 @@ +package turso + +import ( + "context" + "database/sql" + "fmt" + "strings" + "time" + + "github.com/cschleiden/go-workflows/backend/history" + "github.com/cschleiden/go-workflows/backend/test" + "github.com/cschleiden/go-workflows/core" +) + +var _ test.TestBackend = (*tursoBackend)(nil) + +func (sb *tursoBackend) GetFutureEvents(ctx context.Context) ([]*history.Event, error) { + tx, err := sb.db.BeginTx(ctx, nil) + if err != nil { + return nil, err + } + defer tx.Rollback() + + // There is no index on `visible_at`, but this is okay for test only usage. + futureEvents, err := tx.QueryContext( + ctx, + "SELECT pe.id, pe.sequence_id, pe.instance_id, pe.execution_id, pe.event_type, pe.timestamp, pe.schedule_event_id, pe.visible_at, a.data FROM `pending_events` pe JOIN `attributes` a ON a.id = pe.id AND a.instance_id = pe.instance_id AND a.execution_id = pe.execution_id WHERE pe.visible_at IS NOT NULL", + ) + if err != nil { + return nil, fmt.Errorf("getting history: %w", err) + } + + defer futureEvents.Close() + + f := make([]*history.Event, 0) + + for futureEvents.Next() { + var instanceID, executionID string + var attributes []byte + + fe := &history.Event{} + + if err := futureEvents.Scan( + &fe.ID, + &fe.SequenceID, + &instanceID, + &executionID, + &fe.Type, + &fe.Timestamp, + &fe.ScheduleEventID, + &fe.VisibleAt, + &attributes, + ); err != nil { + return nil, fmt.Errorf("scanning event: %w", err) + } + + a, err := history.DeserializeAttributes(fe.Type, attributes) + if err != nil { + return nil, fmt.Errorf("deserializing attributes: %w", err) + } + + fe.Attributes = a + + f = append(f, fe) + } + + return f, nil +} + +func getPendingEvents(ctx context.Context, tx *sql.Tx, instance *core.WorkflowInstance) ([]*history.Event, error) { + now := time.Now() + events, err := tx.QueryContext( + ctx, + "SELECT pe.*, a.data FROM `pending_events` pe INNER JOIN `attributes` a ON a.id = pe.id AND a.instance_id = pe.instance_id AND a.execution_id = pe.execution_id WHERE pe.instance_id = ? AND pe.execution_id = ? AND (pe.`visible_at` IS NULL OR pe.`visible_at` <= ?)", + instance.InstanceID, + instance.ExecutionID, + now, + ) + if err != nil { + return nil, fmt.Errorf("getting new events: %w", err) + } + + defer events.Close() + + pendingEvents := make([]*history.Event, 0) + + for events.Next() { + pendingEvent, err := scanEvent(events) + if err != nil { + return nil, fmt.Errorf("reading event: %w", err) + } + + pendingEvents = append(pendingEvents, pendingEvent) + } + + return pendingEvents, nil +} + +func getHistory(ctx context.Context, tx *sql.Tx, instance *core.WorkflowInstance, lastSequenceID *int64) ([]*history.Event, error) { + var historyEvents *sql.Rows + var err error + if lastSequenceID != nil { + historyEvents, err = tx.QueryContext( + ctx, "SELECT h.*, a.data FROM `history` h INNER JOIN `attributes` a ON a.id = h.id AND a.instance_id = h.instance_id AND a.execution_id = h.execution_id WHERE h.instance_id = ? AND h.execution_id = ? AND h.sequence_id > ?", instance.InstanceID, instance.ExecutionID, *lastSequenceID) + } else { + historyEvents, err = tx.QueryContext( + ctx, "SELECT h.*, a.data FROM `history` h INNER JOIN `attributes` a ON a.id = h.id AND a.instance_id = h.instance_id AND a.execution_id = h.execution_id WHERE h.instance_id = ? AND h.execution_id = ?", instance.InstanceID, instance.ExecutionID) + } + if err != nil { + return nil, fmt.Errorf("getting history: %w", err) + } + + defer historyEvents.Close() + + events := make([]*history.Event, 0) + + for historyEvents.Next() { + historyEvent, err := scanEvent(historyEvents) + if err != nil { + return nil, fmt.Errorf("reading event: %w", err) + } + + events = append(events, historyEvent) + } + + return events, nil +} + +type Scanner interface { + Scan(dest ...interface{}) error +} + +func scanEvent(row Scanner) (*history.Event, error) { + var instanceID, executionID string + var attributes []byte + + historyEvent := &history.Event{} + + if err := row.Scan( + &historyEvent.ID, + &historyEvent.SequenceID, + &instanceID, + &executionID, + &historyEvent.Type, + &historyEvent.Timestamp, + &historyEvent.ScheduleEventID, + &historyEvent.VisibleAt, + &attributes, + ); err != nil { + return historyEvent, fmt.Errorf("scanning event: %w", err) + } + + a, err := history.DeserializeAttributes(historyEvent.Type, attributes) + if err != nil { + return historyEvent, fmt.Errorf("deserializing attributes: %w", err) + } + + historyEvent.Attributes = a + + return historyEvent, nil +} + +func insertPendingEvents(ctx context.Context, tx *sql.Tx, instance *core.WorkflowInstance, newEvents []*history.Event) error { + return insertEvents(ctx, tx, "pending_events", instance, newEvents) +} + +func insertEvents(ctx context.Context, tx *sql.Tx, tableName string, instance *core.WorkflowInstance, events []*history.Event) error { + const batchSize = 20 + for batchStart := 0; batchStart < len(events); batchStart += batchSize { + batchEnd := batchStart + batchSize + if batchEnd > len(events) { + batchEnd = len(events) + } + batchEvents := events[batchStart:batchEnd] + + // INSERT OR IGNORE since the attributes might already exist due to an event being moved from pending to history. + aquery := "INSERT OR IGNORE INTO `attributes` (id, instance_id, execution_id, data) VALUES (?, ?, ?, ?)" + strings.Repeat(", (?, ?, ?, ?)", len(batchEvents)-1) + aargs := make([]interface{}, 0, len(batchEvents)*4) + + query := "INSERT INTO `" + tableName + "` (id, sequence_id, instance_id, execution_id, event_type, timestamp, schedule_event_id, visible_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)" + + strings.Repeat(", (?, ?, ?, ?, ?, ?, ?, ?)", len(batchEvents)-1) + + args := make([]interface{}, 0, len(batchEvents)*8) + + for _, newEvent := range batchEvents { + a, err := history.SerializeAttributes(newEvent.Attributes) + if err != nil { + return err + } + + aargs = append(aargs, newEvent.ID, instance.InstanceID, instance.ExecutionID, a) + + args = append( + args, newEvent.ID, newEvent.SequenceID, instance.InstanceID, instance.ExecutionID, newEvent.Type, newEvent.Timestamp, newEvent.ScheduleEventID, newEvent.VisibleAt) + } + + if _, err := tx.ExecContext( + ctx, + aquery, + aargs..., + ); err != nil { + return fmt.Errorf("inserting attributes: %w", err) + } + + if _, err := tx.ExecContext( + ctx, + query, + args..., + ); err != nil { + return fmt.Errorf("inserting events: %w", err) + } + } + return nil +} + +func removeFutureEvent(ctx context.Context, tx *sql.Tx, instance *core.WorkflowInstance, scheduleEventID int64) error { + row, err := tx.QueryContext( + ctx, + "DELETE FROM `pending_events` WHERE instance_id = ? AND execution_id = ? AND schedule_event_id = ? AND visible_at IS NOT NULL RETURNING id", + instance.InstanceID, + instance.ExecutionID, + scheduleEventID, + ) + if err != nil { + return fmt.Errorf("removing future event: %w", err) + } + + ids := make([]interface{}, 0) + + defer row.Close() + for row.Next() { + var id string + if err := row.Scan(&id); err != nil { + return fmt.Errorf("scanning id: %w", err) + } + + ids = append(ids, id) + } + + // Delete attributes + if len(ids) > 0 { + query := "DELETE FROM `attributes` WHERE id IN (?)" + strings.Repeat(", (?)", len(ids)-1) + + if _, err := tx.ExecContext( + ctx, + query, + ids..., + ); err != nil { + return fmt.Errorf("removing attributes: %w", err) + } + } + + return err +} diff --git a/backend/turso/options.go b/backend/turso/options.go new file mode 100644 index 00000000..83f53f0c --- /dev/null +++ b/backend/turso/options.go @@ -0,0 +1,30 @@ +package turso + +import ( + "github.com/cschleiden/go-workflows/backend" +) + +type options struct { + backend.Options + + // ApplyMigrations automatically applies database migrations on startup. + ApplyMigrations bool +} + +type option func(*options) + +// WithApplyMigrations automatically applies database migrations on startup. +func WithApplyMigrations(applyMigrations bool) option { + return func(o *options) { + o.ApplyMigrations = applyMigrations + } +} + +// WithBackendOptions allows to pass generic backend options. +func WithBackendOptions(opts ...backend.BackendOption) option { + return func(o *options) { + for _, opt := range opts { + opt(&o.Options) + } + } +} diff --git a/backend/turso/stats.go b/backend/turso/stats.go new file mode 100644 index 00000000..e791d6a5 --- /dev/null +++ b/backend/turso/stats.go @@ -0,0 +1,81 @@ +package turso + +import ( + "context" + "fmt" + "time" + + "github.com/cschleiden/go-workflows/backend" + "github.com/cschleiden/go-workflows/core" +) + +func (b *tursoBackend) GetStats(ctx context.Context) (*backend.Stats, error) { + s := &backend.Stats{} + + tx, err := b.db.BeginTx(ctx, nil) + if err != nil { + return nil, fmt.Errorf("failed to start transaction: %w", err) + } + defer tx.Rollback() + + row := tx.QueryRowContext( + ctx, + "SELECT COUNT(*) FROM instances i WHERE i.completed_at IS NULL", + ) + if err := row.Err(); err != nil { + return nil, fmt.Errorf("failed to query active instances: %w", err) + } + + var activeInstances int64 + if err := row.Scan(&activeInstances); err != nil { + return nil, fmt.Errorf("failed to scan active instances: %w", err) + } + + s.ActiveWorkflowInstances = activeInstances + + // Get workflow instances ready to be picked up + now := time.Now() + row = tx.QueryRowContext( + ctx, + `SELECT COUNT(*) FROM instances i + WHERE + (locked_until IS NULL OR locked_until < ?) + AND state = ? AND i.completed_at IS NULL + AND EXISTS ( + SELECT 1 + FROM pending_events + WHERE instance_id = i.id AND execution_id = i.execution_id AND (visible_at IS NULL OR visible_at <= ?) + ) + LIMIT 1`, + now, // locked_until + core.WorkflowInstanceStateActive, // state + now, // pending_event.visible_at + ) + if err := row.Err(); err != nil { + return nil, fmt.Errorf("failed to query active instances: %w", err) + } + + var pendingInstances int64 + if err := row.Scan(&pendingInstances); err != nil { + return nil, fmt.Errorf("failed to scan active instances: %w", err) + } + + s.PendingWorkflowTasks = pendingInstances + + // Get pending activities + row = tx.QueryRowContext( + ctx, + "SELECT COUNT(*) FROM activities") + if err := row.Err(); err != nil { + return nil, fmt.Errorf("failed to query active activities: %w", err) + } + + var pendingActivities int64 + if err := row.Scan(&pendingActivities); err != nil { + return nil, fmt.Errorf("failed to scan active activities: %w", err) + } + + s.PendingActivities = pendingActivities + + return s, nil +} diff --git a/backend/turso/turso.go b/backend/turso/turso.go new file mode 100644 index 00000000..00c948a3 --- /dev/null +++ b/backend/turso/turso.go @@ -0,0 +1,748 @@ +package turso + +import ( + "context" + "database/sql" + "embed" + _ "embed" + "encoding/json" + "errors" + "fmt" + "log/slog" + "strings" + "time" + + "github.com/cschleiden/go-workflows/backend" + "github.com/cschleiden/go-workflows/backend/converter" + "github.com/cschleiden/go-workflows/backend/history" + "github.com/cschleiden/go-workflows/backend/metadata" + "github.com/cschleiden/go-workflows/backend/metrics" + "github.com/cschleiden/go-workflows/core" + "github.com/cschleiden/go-workflows/internal/metrickeys" + "github.com/cschleiden/go-workflows/internal/workflowerrors" + "github.com/cschleiden/go-workflows/workflow" + "github.com/google/uuid" + "go.opentelemetry.io/otel/trace" + + _ "github.com/tursodatabase/libsql-client-go/libsql" + _ "modernc.org/sqlite" + + "github.com/golang-migrate/migrate/v4" + "github.com/golang-migrate/migrate/v4/database/sqlite" + "github.com/golang-migrate/migrate/v4/source/iofs" +) + +//go:embed db/migrations/*.sql +var migrationsFS embed.FS + +func NewInMemoryBackend(opts ...option) *tursoBackend { + b := newTursoBackend("file::memory:?mode=memory&cache=shared", opts...) + + b.db.SetConnMaxIdleTime(0) + b.db.SetMaxIdleConns(1) + + // WORKAROUND: Keep a connection open at all times to prevent hte in-memory db from being dropped + b.db.SetMaxOpenConns(2) + + var err error + b.memConn, err = b.db.Conn(context.Background()) + if err != nil { + panic(err) + } + + return b +} + +func NewTursoBackend(dsn string, opts ...option) *tursoBackend { + return newTursoBackend(dsn, opts...) +} + +func newTursoBackend(dsn string, opts ...option) *tursoBackend { + options := &options{ + Options: backend.ApplyOptions(), + ApplyMigrations: true, + } + + for _, opt := range opts { + opt(options) + } + + db, err := sql.Open("libsql", dsn) + if err != nil { + panic(err) + } + + // SQLite does not support multiple writers on the database, see https://www.sqlite.org/faq.html#q5 + // A frequently used workaround is to have a single connection, effectively acting as a mutex + // See https://github.com/mattn/go-sqlite3/issues/274 for more context + db.SetMaxOpenConns(1) + + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + err = db.PingContext(ctx) + if err != nil { + panic(err) + } + + b := &tursoBackend{ + db: db, + workerName: fmt.Sprintf("worker-%v", uuid.NewString()), + options: options, + } + + // Apply migrations + if options.ApplyMigrations { + if err := b.Migrate(); err != nil { + panic(err) + } + } + + return b +} + +type tursoBackend struct { + db *sql.DB + workerName string + options *options + + memConn *sql.Conn +} + +var _ backend.Backend = (*tursoBackend)(nil) + +func (sb *tursoBackend) Close() error { + if sb.memConn != nil { + if err := sb.memConn.Close(); err != nil { + return err + } + } + + return sb.db.Close() +} + +// Migrate applies any pending database migrations. +func (sb *tursoBackend) Migrate() error { + sb.options.Logger.Info("Applying migrations...") + + dbi, err := sqlite.WithInstance(sb.db, &sqlite.Config{}) + if err != nil { + return fmt.Errorf("creating migration instance: %w", err) + } + + migrations, err := iofs.New(migrationsFS, "db/migrations") + if err != nil { + return fmt.Errorf("creating migration source: %w", err) + } + + m, err := migrate.NewWithInstance("iofs", migrations, "sqlite", dbi) + if err != nil { + return fmt.Errorf("creating migration: %w", err) + } + + if err := m.Up(); err != nil { + if !errors.Is(err, migrate.ErrNoChange) { + return fmt.Errorf("running migrations: %w", err) + } + + sb.options.Logger.Info("No migrations to apply") + } + + return nil +} + +func (sb *tursoBackend) Logger() *slog.Logger { + return sb.options.Logger +} + +func (sb *tursoBackend) Metrics() metrics.Client { + return sb.options.Metrics.WithTags(metrics.Tags{metrickeys.Backend: "sqlite"}) +} + +func (sb *tursoBackend) Tracer() trace.Tracer { + return sb.options.TracerProvider.Tracer(backend.TracerName) +} + +func (sb *tursoBackend) Converter() converter.Converter { + return sb.options.Converter +} + +func (sb *tursoBackend) ContextPropagators() []workflow.ContextPropagator { + return sb.options.ContextPropagators +} + +func (sb *tursoBackend) CreateWorkflowInstance(ctx context.Context, instance *workflow.Instance, event *history.Event) error { + tx, err := sb.db.BeginTx(ctx, nil) + if err != nil { + return fmt.Errorf("starting transaction: %w", err) + } + defer tx.Rollback() + + // Create workflow instance + if err := createInstance(ctx, tx, instance, event.Attributes.(*history.ExecutionStartedAttributes).Metadata); err != nil { + return err + } + + if err := insertPendingEvents(ctx, tx, instance, []*history.Event{event}); err != nil { + return fmt.Errorf("inserting new event: %w", err) + } + + if err := tx.Commit(); err != nil { + return fmt.Errorf("creating workflow instance: %w", err) + } + + return nil +} + +func createInstance(ctx context.Context, tx *sql.Tx, wfi *workflow.Instance, metadata *workflow.Metadata) error { + // Check for existing instance + if err := tx.QueryRowContext(ctx, "SELECT 1 FROM `instances` WHERE id = ? AND state = ? LIMIT 1", wfi.InstanceID, core.WorkflowInstanceStateActive). + Scan(new(int)); err != sql.ErrNoRows { + return backend.ErrInstanceAlreadyExists + } + + var parentInstanceID, parentExecutionID *string + var parentEventID *int64 + if wfi.SubWorkflow() { + parentInstanceID = &wfi.Parent.InstanceID + parentExecutionID = &wfi.Parent.ExecutionID + parentEventID = &wfi.ParentEventID + } + + metadataJson, err := json.Marshal(metadata) + if err != nil { + return fmt.Errorf("marshaling metadata: %w", err) + } + + _, err = tx.ExecContext( + ctx, + "INSERT INTO `instances` (id, execution_id, parent_instance_id, parent_execution_id, parent_schedule_event_id, metadata, state) VALUES (?, ?, ?, ?, ?, ?, ?)", + wfi.InstanceID, + wfi.ExecutionID, + parentInstanceID, + parentExecutionID, + parentEventID, + string(metadataJson), + core.WorkflowInstanceStateActive, + ) + if err != nil { + return fmt.Errorf("inserting workflow instance: %w", err) + } + + return nil +} + +func (sb *tursoBackend) RemoveWorkflowInstance(ctx context.Context, instance *core.WorkflowInstance) error { + tx, err := sb.db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer tx.Rollback() + + instanceID := instance.InstanceID + executionID := instance.ExecutionID + + // Check status of the instance + row := tx.QueryRowContext(ctx, "SELECT state FROM `instances` WHERE id = ? AND execution_id = ? LIMIT 1", instanceID, executionID) + var state core.WorkflowInstanceState + if err := row.Scan(&state); err != nil { + if err == sql.ErrNoRows { + return backend.ErrInstanceNotFound + } + } + + if state == core.WorkflowInstanceStateActive { + return backend.ErrInstanceNotFinished + } + + // Delete from instances and history tables + if _, err := tx.ExecContext(ctx, "DELETE FROM `instances` WHERE id = ? AND execution_id = ?", instanceID, executionID); err != nil { + return err + } + + if _, err := tx.ExecContext(ctx, "DELETE FROM `history` WHERE instance_id = ? AND execution_id = ?", instanceID, executionID); err != nil { + return err + } + + if _, err := tx.ExecContext(ctx, "DELETE FROM `attributes` WHERE instance_id = ? AND execution_id = ?", instanceID, executionID); err != nil { + return err + } + + return tx.Commit() +} + +func (sb *tursoBackend) CancelWorkflowInstance(ctx context.Context, instance *workflow.Instance, event *history.Event) error { + tx, err := sb.db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer tx.Rollback() + + instanceID := instance.InstanceID + executionID := instance.ExecutionID + + // TODO: Combine with event insertion + res := tx.QueryRowContext(ctx, "SELECT 1 FROM `instances` WHERE id = ? AND execution_id = ? LIMIT 1", instanceID, executionID) + if err := res.Scan(new(int)); err != nil { + if err == sql.ErrNoRows { + return backend.ErrInstanceNotFound + } + + return err + } + + if err := insertPendingEvents(ctx, tx, instance, []*history.Event{event}); err != nil { + return fmt.Errorf("inserting cancellation event: %w", err) + } + + return tx.Commit() +} + +func (sb *tursoBackend) GetWorkflowInstanceHistory(ctx context.Context, instance *workflow.Instance, lastSequenceID *int64) ([]*history.Event, error) { + tx, err := sb.db.BeginTx(ctx, nil) + if err != nil { + return nil, err + } + defer tx.Rollback() + + h, err := getHistory(ctx, tx, instance, lastSequenceID) + if err != nil { + return nil, fmt.Errorf("getting workflow history: %w", err) + } + + return h, nil +} + +func (sb *tursoBackend) GetWorkflowInstanceState(ctx context.Context, instance *workflow.Instance) (core.WorkflowInstanceState, error) { + tx, err := sb.db.BeginTx(ctx, nil) + if err != nil { + return core.WorkflowInstanceStateActive, err + } + defer tx.Rollback() + + row := tx.QueryRowContext( + ctx, + "SELECT state FROM instances WHERE id = ? AND execution_id = ?", + instance.InstanceID, + instance.ExecutionID, + ) + + var state core.WorkflowInstanceState + if err := row.Scan(&state); err != nil { + if err == sql.ErrNoRows { + return core.WorkflowInstanceStateActive, backend.ErrInstanceNotFound + } + } + + return state, nil +} + +func (sb *tursoBackend) SignalWorkflow(ctx context.Context, instanceID string, event *history.Event) error { + tx, err := sb.db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer tx.Rollback() + + // TODO: Combine this with the event insertion + var executionID string + res := tx.QueryRowContext(ctx, "SELECT execution_id FROM `instances` WHERE id = ? AND state = ? LIMIT 1", instanceID, core.WorkflowInstanceStateActive) + if err := res.Scan(&executionID); err == sql.ErrNoRows { + return backend.ErrInstanceNotFound + } + + if err := insertPendingEvents(ctx, tx, core.NewWorkflowInstance(instanceID, executionID), []*history.Event{event}); err != nil { + return fmt.Errorf("inserting signal event: %w", err) + } + + return tx.Commit() +} + +func (sb *tursoBackend) GetWorkflowTask(ctx context.Context) (*backend.WorkflowTask, error) { + tx, err := sb.db.BeginTx(ctx, nil) + if err != nil { + return nil, err + } + defer tx.Rollback() + + // Lock next workflow task by finding an unlocked instance with new events to process + // (work around missing LIMIT support in sqlite driver for UPDATE statements by using sub-query) + now := time.Now() + row := tx.QueryRowContext( + ctx, + `UPDATE instances + SET locked_until = ?, worker = ? + WHERE rowid = ( + SELECT rowid FROM instances i + WHERE + (locked_until IS NULL OR locked_until < ?) + AND (sticky_until IS NULL OR sticky_until < ? OR worker = ?) + AND state = ? AND i.completed_at IS NULL + AND EXISTS ( + SELECT 1 + FROM pending_events + WHERE instance_id = i.id AND execution_id = i.execution_id AND (visible_at IS NULL OR visible_at <= ?) + ) + LIMIT 1 + ) RETURNING id, execution_id, parent_instance_id, parent_execution_id, parent_schedule_event_id, metadata, sticky_until`, + now.Add(sb.options.WorkflowLockTimeout), // new locked_until + sb.workerName, + now, // locked_until + now, // sticky_until + sb.workerName, // worker + core.WorkflowInstanceStateActive, // state + now, // pending_event.visible_at + ) + + var instanceID, executionID string + var parentInstanceID, parentExecutionID *string + var parentEventID *int64 + var metadataJson sql.NullString + var stickyUntil *time.Time + if err := row.Scan(&instanceID, &executionID, &parentInstanceID, &parentExecutionID, &parentEventID, &metadataJson, &stickyUntil); err != nil { + if err == sql.ErrNoRows { + return nil, nil + } + + return nil, fmt.Errorf("locking workflow task: %w", err) + } + + var wfi *workflow.Instance + if parentInstanceID != nil { + wfi = core.NewSubWorkflowInstance(instanceID, executionID, core.NewWorkflowInstance(*parentInstanceID, *parentExecutionID), *parentEventID) + } else { + wfi = core.NewWorkflowInstance(instanceID, executionID) + } + + var metadata *metadata.WorkflowMetadata + if metadataJson.Valid { + if err := json.Unmarshal([]byte(metadataJson.String), &metadata); err != nil { + return nil, fmt.Errorf("parsing workflow metadata: %w", err) + } + } + + t := &backend.WorkflowTask{ + ID: wfi.InstanceID, + WorkflowInstance: wfi, + WorkflowInstanceState: core.WorkflowInstanceStateActive, + Metadata: metadata, + NewEvents: []*history.Event{}, + } + + // Get new events + pendingEvents, err := getPendingEvents(ctx, tx, wfi) + if err != nil { + return nil, fmt.Errorf("getting pending events: %w", err) + } + + // Return if there aren't any new events + if len(pendingEvents) == 0 { + return nil, nil + } + + t.NewEvents = pendingEvents + + // Get only most recent sequence ID + // TODO: Denormalize to instances table + row = tx.QueryRowContext(ctx, "SELECT sequence_id FROM `history` WHERE instance_id = ? AND execution_id = ? ORDER BY rowid DESC LIMIT 1", instanceID, executionID) + if err := row.Scan(&t.LastSequenceID); err != nil { + if err != sql.ErrNoRows { + return nil, fmt.Errorf("getting most recent sequence id: %w", err) + } + } + + if err := tx.Commit(); err != nil { + return nil, err + } + + return t, nil +} + +func (sb *tursoBackend) CompleteWorkflowTask( + ctx context.Context, + task *backend.WorkflowTask, + instance *workflow.Instance, + state core.WorkflowInstanceState, + executedEvents, activityEvents, timerEvents []*history.Event, + workflowEvents []history.WorkflowEvent, +) error { + tx, err := sb.db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer tx.Rollback() + + var completedAt *time.Time + if state == core.WorkflowInstanceStateContinuedAsNew || state == core.WorkflowInstanceStateFinished { + t := time.Now() + completedAt = &t + } + + // Unlock instance, but keep it sticky to the current worker + if res, err := tx.ExecContext( + ctx, + `UPDATE instances SET locked_until = NULL, sticky_until = ?, completed_at = ?, state = ? WHERE id = ? AND execution_id = ? AND worker = ?`, + time.Now().Add(sb.options.StickyTimeout), + completedAt, + state, + instance.InstanceID, + instance.ExecutionID, + sb.workerName, + ); err != nil { + return fmt.Errorf("unlocking workflow instance: %w", err) + } else if n, err := res.RowsAffected(); err != nil { + return fmt.Errorf("checking for unlocked workflow instances: %w", err) + } else if n != 1 { + return errors.New("could not find workflow instance to unlock") + } + + // Remove handled events from task + if len(executedEvents) > 0 { + args := make([]interface{}, 0, len(executedEvents)+1) + args = append(args, instance.InstanceID, instance.ExecutionID) + for _, e := range executedEvents { + args = append(args, e.ID) + } + + // Remove from pending + if _, err := tx.ExecContext( + ctx, + fmt.Sprintf(`DELETE FROM pending_events WHERE instance_id = ? AND execution_id = ? AND id IN (?%v)`, strings.Repeat(",?", len(executedEvents)-1)), + args..., + ); err != nil { + return fmt.Errorf("deleting handled new events: %w", err) + } + } + + if err := insertEvents(ctx, tx, "history", instance, executedEvents); err != nil { + return fmt.Errorf("inserting history events: %w", err) + } + + // Schedule activities + for _, event := range activityEvents { + if err := scheduleActivity(ctx, tx, instance, event); err != nil { + return fmt.Errorf("scheduling activity: %w", err) + } + } + + // Timer events + if err := insertPendingEvents(ctx, tx, instance, timerEvents); err != nil { + return fmt.Errorf("scheduling timers: %w", err) + } + + for _, event := range executedEvents { + switch event.Type { + case history.EventType_TimerCanceled: + if err := removeFutureEvent(ctx, tx, instance, event.ScheduleEventID); err != nil { + return fmt.Errorf("removing future event: %w", err) + } + } + } + + // Insert new workflow events + groupedEvents := history.EventsByWorkflowInstance(workflowEvents) + + for targetInstance, events := range groupedEvents { + // Are we creating a new sub-workflow instance? + m := events[0] + if m.HistoryEvent.Type == history.EventType_WorkflowExecutionStarted { + a := m.HistoryEvent.Attributes.(*history.ExecutionStartedAttributes) + // Create new instance + if err := createInstance(ctx, tx, m.WorkflowInstance, a.Metadata); err != nil { + if err == backend.ErrInstanceAlreadyExists { + if err := insertPendingEvents(ctx, tx, instance, []*history.Event{ + history.NewPendingEvent(time.Now(), history.EventType_SubWorkflowFailed, &history.SubWorkflowFailedAttributes{ + Error: workflowerrors.FromError(backend.ErrInstanceAlreadyExists), + }, history.ScheduleEventID(m.WorkflowInstance.ParentEventID)), + }); err != nil { + return fmt.Errorf("inserting sub-workflow failed event: %w", err) + } + + continue + } + + return fmt.Errorf("creating sub-workflow instance: %w", err) + } + } + + // Insert pending events for target instance + historyEvents := []*history.Event{} + for _, m := range events { + historyEvents = append(historyEvents, m.HistoryEvent) + } + if err := insertPendingEvents(ctx, tx, &targetInstance, historyEvents); err != nil { + return fmt.Errorf("inserting messages: %w", err) + } + } + + return tx.Commit() +} + +func (sb *tursoBackend) ExtendWorkflowTask(ctx context.Context, taskID string, instance *workflow.Instance) error { + tx, err := sb.db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer tx.Rollback() + + until := time.Now().Add(sb.options.WorkflowLockTimeout) + res, err := tx.ExecContext( + ctx, + `UPDATE instances SET locked_until = ? WHERE id = ? AND execution_id = ? AND worker = ?`, + until, + instance.InstanceID, + instance.ExecutionID, + sb.workerName, + ) + if err != nil { + return fmt.Errorf("extending workflow task lock: %w", err) + } + + if rowsAffected, err := res.RowsAffected(); err != nil { + return fmt.Errorf("determining if workflow task was extended: %w", err) + } else if rowsAffected == 0 { + return errors.New("could not extend workflow task") + } + + return tx.Commit() +} + +func (sb *tursoBackend) GetActivityTask(ctx context.Context) (*backend.ActivityTask, error) { + tx, err := sb.db.BeginTx(ctx, nil) + if err != nil { + return nil, err + } + defer tx.Rollback() + + // Lock next activity + // (work around missing LIMIT support in sqlite driver for UPDATE statements by using sub-query) + now := time.Now() + row := tx.QueryRowContext( + ctx, + `UPDATE activities + SET locked_until = ?, worker = ? + WHERE rowid = ( + SELECT rowid FROM activities WHERE locked_until IS NULL OR locked_until < ? LIMIT 1 + ) RETURNING id, instance_id, execution_id, event_type, timestamp, schedule_event_id, visible_at`, + now.Add(sb.options.ActivityLockTimeout), + sb.workerName, + now, + ) + if err != nil { + return nil, err + } + + var instanceID, executionID string + event := &history.Event{} + + if err := row.Scan( + &event.ID, + &instanceID, + &executionID, + &event.Type, + &event.Timestamp, + &event.ScheduleEventID, + &event.VisibleAt, + ); err != nil { + if err == sql.ErrNoRows { + // No rows locked, just return + return nil, nil + } + + return nil, fmt.Errorf("scanning event: %w", err) + } + + var attributes []byte + if err := tx.QueryRowContext( + ctx, "SELECT data FROM attributes WHERE instance_id = ? AND execution_id = ? AND id = ?", instanceID, executionID, event.ID, + ).Scan(&attributes); err != nil { + return nil, fmt.Errorf("scanning attributes: %w", err) + } + + a, err := history.DeserializeAttributes(event.Type, attributes) + if err != nil { + return nil, fmt.Errorf("deserializing attributes: %w", err) + } + + event.Attributes = a + + var metadataJson sql.NullString + if err := tx.QueryRowContext(ctx, "SELECT metadata FROM instances WHERE id = ?", instanceID).Scan(&metadataJson); err != nil { + return nil, fmt.Errorf("scanning metadata: %w", err) + } + + var metadata *workflow.Metadata + if err := json.Unmarshal([]byte(metadataJson.String), &metadata); err != nil { + return nil, fmt.Errorf("unmarshaling metadata: %w", err) + } + + t := &backend.ActivityTask{ + ID: event.ID, + WorkflowInstance: core.NewWorkflowInstance(instanceID, executionID), + Event: event, + } + + if err := tx.Commit(); err != nil { + return nil, err + } + + return t, nil +} + +func (sb *tursoBackend) CompleteActivityTask(ctx context.Context, instance *workflow.Instance, id string, event *history.Event) error { + tx, err := sb.db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer tx.Rollback() + + // Remove activity but keep the attributes, they are still needed for the history + if res, err := tx.ExecContext( + ctx, + `DELETE FROM activities WHERE instance_id = ? AND execution_id = ? AND id = ? AND worker = ?`, + instance.InstanceID, + instance.ExecutionID, + id, + sb.workerName, + ); err != nil { + return fmt.Errorf("unlocking instance: %w", err) + } else if n, err := res.RowsAffected(); err != nil { + return fmt.Errorf("checking for deleted activities: %w", err) + } else if n != 1 { + return errors.New("could not find activity to delete") + } + + // Insert new event generated during this workflow execution + if err := insertPendingEvents(ctx, tx, instance, []*history.Event{event}); err != nil { + return fmt.Errorf("inserting new events for completed activity: %w", err) + } + + return tx.Commit() +} + +func (sb *tursoBackend) ExtendActivityTask(ctx context.Context, activityID string) error { + tx, err := sb.db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer tx.Rollback() + + until := time.Now().Add(sb.options.ActivityLockTimeout) + res, err := tx.ExecContext( + ctx, + `UPDATE activities SET locked_until = ? WHERE id = ? AND worker = ?`, + until, + activityID, + sb.workerName, + ) + if err != nil { + return fmt.Errorf("extending activity lock: %w", err) + } + + if rowsAffected, err := res.RowsAffected(); err != nil { + return fmt.Errorf("determining if activity was extended: %w", err) + } else if rowsAffected == 0 { + return errors.New("could not extend activity") + } + + return tx.Commit() +} diff --git a/backend/turso/turso_test.go b/backend/turso/turso_test.go new file mode 100644 index 00000000..1d486bfa --- /dev/null +++ b/backend/turso/turso_test.go @@ -0,0 +1,37 @@ +package turso + +import ( + "testing" + + "github.com/cschleiden/go-workflows/backend" + "github.com/cschleiden/go-workflows/backend/test" + "github.com/stretchr/testify/require" +) + +func Test_SqliteBackend(t *testing.T) { + if testing.Short() { + t.Skip() + } + + test.BackendTest(t, func(options ...backend.BackendOption) test.TestBackend { + // Disable sticky workflow behavior for the test execution + return NewInMemoryBackend(WithBackendOptions(append(options, backend.WithStickyTimeout(0))...)) + }, func(b test.TestBackend) { + // Ensure we close the database so the next test will get a clean in-memory db + require.NoError(t, b.(*tursoBackend).Close()) + }) +} + +func Test_EndToEndSqliteBackend(t *testing.T) { + if testing.Short() { + t.Skip() + } + + test.EndToEndBackendTest(t, func(options ...backend.BackendOption) test.TestBackend { + // Disable sticky workflow behavior for the test execution + return NewInMemoryBackend(WithBackendOptions(append(options, backend.WithStickyTimeout(0))...)) + }, func(b test.TestBackend) { + // Ensure we close the database so the next test will get a clean in-memory db + require.NoError(t, b.Close()) + }) +} diff --git a/go.mod b/go.mod index f5fec416..baafabb5 100644 --- a/go.mod +++ b/go.mod @@ -12,13 +12,14 @@ require ( github.com/jstemmer/go-junit-report/v2 v2.0.0-beta1 github.com/redis/go-redis/v9 v9.0.2 github.com/stretchr/testify v1.8.4 + github.com/tursodatabase/libsql-client-go v0.0.0-20231216154754-8383a53d618f go.opentelemetry.io/otel v1.16.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.16.0 go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.16.0 go.opentelemetry.io/otel/trace v1.16.0 golang.org/x/tools v0.12.0 - modernc.org/sqlite v1.27.0 + modernc.org/sqlite v1.28.0 ) require ( @@ -29,6 +30,7 @@ require ( github.com/OpenPeeDeeP/depguard/v2 v2.1.0 // indirect github.com/alexkohler/nakedret/v2 v2.0.2 // indirect github.com/alingse/asasalint v0.0.11 // indirect + github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9 // indirect github.com/breml/bidichk v0.2.4 // indirect github.com/butuzov/mirror v1.1.0 // indirect github.com/ccojocar/zxcvbn-go v1.0.1 // indirect @@ -38,6 +40,8 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kkHAIKE/contextcheck v1.1.4 // indirect + github.com/klauspost/compress v1.15.15 // indirect + github.com/libsql/sqlite-antlr4-parser v0.0.0-20230802215326-5cb5bb604475 // indirect github.com/lufeee/execinquery v1.2.1 // indirect github.com/maratori/testableexamples v1.0.0 // indirect github.com/nunnatsa/ginkgolinter v0.13.5 // indirect @@ -72,6 +76,7 @@ require ( modernc.org/opt v0.1.3 // indirect modernc.org/strutil v1.1.3 // indirect modernc.org/token v1.0.1 // indirect + nhooyr.io/websocket v1.8.7 // indirect ) require ( @@ -154,7 +159,7 @@ require ( github.com/maratori/testpackage v1.1.1 // indirect github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.17 // indirect + github.com/mattn/go-isatty v0.0.18 // indirect github.com/mattn/go-runewidth v0.0.9 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/mbilski/exhaustivestruct v1.2.0 // indirect diff --git a/go.sum b/go.sum index 51db2e1a..81e355af 100644 --- a/go.sum +++ b/go.sum @@ -77,6 +77,8 @@ github.com/alexkohler/prealloc v1.0.0/go.mod h1:VetnK3dIgFBBKmg0YnD9F9x6Icjd+9cv github.com/alingse/asasalint v0.0.11 h1:SFwnQXJ49Kx/1GghOFz1XGqHYKp21Kq1nHad/0WQRnw= github.com/alingse/asasalint v0.0.11/go.mod h1:nCaoMhw7a9kSJObvQyVzNTPBDbNpdocqrSP7t/cW5+I= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9 h1:goHVqTbFX3AIo0tzGr14pgfAW2ZfPChKO21Z9MGf/gk= +github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM= github.com/ashanbrown/forbidigo v1.6.0 h1:D3aewfM37Yb3pxHujIPSpTf6oQk9sc9WZi8gerOIVIY= github.com/ashanbrown/forbidigo v1.6.0/go.mod h1:Y8j9jy9ZYAEHXdu723cUlraTqbzjKF1MUyfOKL+AjcU= github.com/ashanbrown/makezero v1.1.1 h1:iCQ87C0V0vSyO+M9E/FZYbu65auqH0lnsOkf5FcB28s= @@ -180,6 +182,10 @@ github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmV github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo= github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/go-critic/go-critic v0.9.0 h1:Pmys9qvU3pSML/3GEQ2Xd9RZ/ip+aXHKILuxczKGV/U= github.com/go-critic/go-critic v0.9.0/go.mod h1:5P8tdXL7m/6qnyG6oRAlYLORvoXH0WDypYgAEmagT40= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= @@ -198,6 +204,13 @@ github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -225,6 +238,12 @@ github.com/go-xmlfmt/xmlfmt v1.1.2 h1:Nea7b4icn8s57fTx1M5AI4qQT5HEM3rVUO8MuE6g80 github.com/go-xmlfmt/xmlfmt v1.1.2/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= @@ -326,6 +345,8 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gordonklaus/ineffassign v0.0.0-20230610083614-0e73809eb601 h1:mrEEilTAUmaAORhssPPkxj84TsHrPMLBGW2Z4SoTxm8= github.com/gordonklaus/ineffassign v0.0.0-20230610083614-0e73809eb601/go.mod h1:Qcp2HIAYhR7mNUVSIxZww3Guk4it82ghYcEXIAk+QT0= +github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk= github.com/gostaticanalysis/analysisutil v0.7.1/go.mod h1:v21E3hY37WKMGSnbsw2S/ojApNWb6C1//mXO48CXbVc= github.com/gostaticanalysis/comment v1.4.1/go.mod h1:ih6ZxzTHLdadaiSnF5WY3dxUoXfXAlTaRzuaNDlSado= @@ -369,8 +390,10 @@ github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af h1:KA9B github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af/go.mod h1:HEWGJkRDzjJY2sqdDwxccsGicWEf9BQOZsq2tV+xzM0= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= @@ -388,6 +411,9 @@ github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kkHAIKE/contextcheck v1.1.4 h1:B6zAaLhOEEcjvUgIYEqystmnFk1Oemn8bvJhbt0GMb8= github.com/kkHAIKE/contextcheck v1.1.4/go.mod h1:1+i/gWqokIa+dm31mqGLZhZJ7Uh44DJGZVmr6QRBNJg= +github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw= +github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= @@ -410,10 +436,14 @@ github.com/ldez/gomoddirectives v0.2.3 h1:y7MBaisZVDYmKvt9/l1mjNCiSA1BVn34U0ObUc github.com/ldez/gomoddirectives v0.2.3/go.mod h1:cpgBogWITnCfRq2qGoDkKMEVSaarhdBr6g8G04uz6d0= github.com/ldez/tagliatelle v0.5.0 h1:epgfuYt9v0CG3fms0pEgIMNPuFf/LpPIfjk4kyqSioo= github.com/ldez/tagliatelle v0.5.0/go.mod h1:rj1HmWiL1MiKQuOONhd09iySTEkUuE/8+5jtPYz9xa4= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leonklingele/grouper v1.1.1 h1:suWXRU57D4/Enn6pXR0QVqqWWrnJ9Osrz+5rjt8ivzU= github.com/leonklingele/grouper v1.1.1/go.mod h1:uk3I3uDfi9B6PeUjsCKi6ndcf63Uy7snXgR4yDYQVDY= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/libsql/sqlite-antlr4-parser v0.0.0-20230802215326-5cb5bb604475 h1:6PfEMwfInASh9hkN83aR0j4W/eKaAZt/AURtXAXlas0= +github.com/libsql/sqlite-antlr4-parser v0.0.0-20230802215326-5cb5bb604475/go.mod h1:20nXSmcf0nAscrzqsXeC2/tA3KkV2eCiJqYuyAgl+ss= github.com/lufeee/execinquery v1.2.1 h1:hf0Ems4SHcUGBxpGN7Jz78z1ppVkP/837ZlETPCEtOM= github.com/lufeee/execinquery v1.2.1/go.mod h1:EC7DrEKView09ocscGHC+apXMIaorh4xqSxS/dy8SbM= github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= @@ -428,9 +458,10 @@ github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= -github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= +github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= @@ -448,9 +479,11 @@ github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/moricho/tparallel v0.3.1 h1:fQKD4U1wRMAYNngDonW5XupoB/ZGJHdpzrWqgyg9krA= github.com/moricho/tparallel v0.3.1/go.mod h1:leENX2cUv7Sv2qDgdi0D0fCftN8fRC67Bcn8pqzeYNI= @@ -618,6 +651,12 @@ github.com/tomarrell/wrapcheck/v2 v2.8.1 h1:HxSqDSN0sAt0yJYsrcYVoEeyM4aI9yAm3KQp github.com/tomarrell/wrapcheck/v2 v2.8.1/go.mod h1:/n2Q3NZ4XFT50ho6Hbxg+RV1uyo2Uow/Vdm9NQcl5SE= github.com/tommy-muehle/go-mnd/v2 v2.5.1 h1:NowYhSdyE/1zwK9QCLeRb6USWdoif80Ie+v+yU8u1Zw= github.com/tommy-muehle/go-mnd/v2 v2.5.1/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw= +github.com/tursodatabase/libsql-client-go v0.0.0-20231216154754-8383a53d618f h1:teZ0Pj1Wp3Wk0JObKBiKZqgxhYwLeJhVAyj6DRgmQtY= +github.com/tursodatabase/libsql-client-go v0.0.0-20231216154754-8383a53d618f/go.mod h1:UMde0InJz9I0Le/1YIR4xsB0E2vb01MrDY6k/eNdfkg= +github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ultraware/funlen v0.1.0 h1:BuqclbkY6pO+cvxoq7OsktIXZpgBSkYTQtmwhAK81vI= github.com/ultraware/funlen v0.1.0/go.mod h1:XJqmOQja6DpxarLj6Jj1U7JuoS8PvL4nEqDaQhy22p4= github.com/ultraware/whitespace v0.0.5 h1:hh+/cpIcopyMYbZNVov9iSxvJU3OYQg78Sfaqzi/CzI= @@ -822,6 +861,7 @@ golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -863,6 +903,7 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -1079,6 +1120,7 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= @@ -1112,8 +1154,8 @@ modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E= modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E= modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= -modernc.org/sqlite v1.27.0 h1:MpKAHoyYB7xqcwnUwkuD+npwEa0fojF0B5QRbN+auJ8= -modernc.org/sqlite v1.27.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0= +modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ= +modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0= modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY= modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY= @@ -1130,6 +1172,8 @@ mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b h1:DxJ5nJdkhDlLok9K6qO+5290kphD mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4= mvdan.cc/unparam v0.0.0-20221223090309-7455f1af531d h1:3rvTIIM22r9pvXk+q3swxUQAQOxksVMGK7sml4nG57w= mvdan.cc/unparam v0.0.0-20221223090309-7455f1af531d/go.mod h1:IeHQjmn6TOD+e4Z3RFiZMMsLVL+A96Nvptar8Fj71is= +nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= +nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=