db

package
v0.0.0-...-b165b9f Latest Latest
Warning

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

Go to latest
Published: Sep 23, 2026 License: Apache-2.0 Imports: 27 Imported by: 0

Documentation

Overview

Read/write pool separation for the HelixKnowledge skill graph system.

ReadWritePool wraps two *sql.DB pools — a primary for writes and an optional replica for reads. When no replica is configured, all operations are routed to the primary, ensuring the system works identically in single-node deployments.

Usage:

pool, err := db.NewReadWritePool(cfg.Database)
if err != nil {
    log.Fatal(err)
}
defer pool.Close()

// Writes go to primary.
_, err = pool.Write().ExecContext(ctx, "INSERT INTO skills ...")

// Reads go to replica (or primary if no replica).
row := pool.Read().QueryRowContext(ctx, "SELECT ...")

Package db provides PostgreSQL connectivity, migration management, vector search helpers, audit logging, and embedding providers for the HelixKnowledge skill graph system.

All functions accept context.Context as the first parameter and use zap for structured logging. Errors are wrapped with descriptive messages.

Index

Constants

View Source
const (
	AuditEventSkillCreated        = "skill.created"
	AuditEventSkillUpdated        = "skill.updated"
	AuditEventSkillDeleted        = "skill.deleted"
	AuditEventSkillValidated      = "skill.validated"
	AuditEventSkillActivated      = "skill.activated"
	AuditEventSkillDeprecated     = "skill.deprecated"
	AuditEventDepAdded            = "dependency.added"
	AuditEventDepRemoved          = "dependency.removed"
	AuditEventEvidenceAdded       = "evidence.added"
	AuditEventEvidenceValidated   = "evidence.validated"
	AuditEventResourceAdded       = "resource.added"
	AuditEventResourceFetched     = "resource.fetched"
	AuditEventExpansionStarted    = "expansion.started"
	AuditEventExpansionCompleted  = "expansion.completed"
	AuditEventExpansionFailed     = "expansion.failed"
	AuditEventLearningStarted     = "learning.started"
	AuditEventLearningCompleted   = "learning.completed"
	AuditEventMigrationApplied    = "migration.applied"
	AuditEventMigrationRolledBack = "migration.rolled_back"
)

Common audit event names used throughout the skill graph system.

Variables

This section is empty.

Functions

func AssertEmbeddingDimension

func AssertEmbeddingDimension(ctx context.Context, pool *Pool, table, column string, wantDim int) error

AssertEmbeddingDimension is the G10 boot-time safety guard (§11.4.201): it reads the REAL pgvector dimension declared on table.column from the live database (via QueryColumnVectorDimension — the authoritative source) and asserts it equals wantDim, the configured embedder's OWN declared/output dimension (Embedder.Dimensions()). A mismatch returns a FAIL-CLOSED error naming BOTH dimensions and the source of each, so a misconfigured embedder can never silently corrupt every StoreSkillEmbedding/StoreEvidenceEmbedding insert and every KNN search (VectorSearch/HybridSearch) against a column sized for a different provider. Callers (cmd/server, cmd/worker) MUST treat a non-nil return as fatal — this is a hard stop, never a warn-and-continue (research/g10_embedding_provider_design.md §2.2 point 3).

func BatchEmbedAllSkills

func BatchEmbedAllSkills(
	ctx context.Context,
	pool *Pool,
	provider EmbeddingProvider,
	cfg BatchEmbedConfig,
) error

BatchEmbedAllSkills finds all skills without embeddings and generates them using the provided provider. This is useful for initial population or after a provider/model change.

func BatchEmbedSkills

func BatchEmbedSkills(
	ctx context.Context,
	pool *Pool,
	skillIDs []uuid.UUID,
	provider EmbeddingProvider,
	cfg BatchEmbedConfig,
) error

BatchEmbedSkills generates and stores embeddings for the given skill IDs. Skills are processed in configurable batches with rate limiting to avoid overwhelming the embedding provider.

Partial failures are tolerated: individual skill embedding errors are logged and the function continues with remaining skills. The returned error is non-nil only if the entire operation is aborted (e.g. context cancellation).

The provider is called with each skill's content (name + description + content concatenated) to generate the embedding vector.

func ClearSkillEmbedding

func ClearSkillEmbedding(
	ctx context.Context,
	pool *Pool,
	skillID uuid.UUID,
) error

ClearSkillEmbedding sets a skill's embedding column to NULL. This is the honest-degrade counterpart to StoreSkillEmbedding, used by internal/skill.Store's embedWriteThrough (§G59 F3, code-review remediation) on ALL FOUR of its embed failure/skip paths -- empty embed text, an Embed() error, Embed() returning an empty/unusable vector, and (F3 round-2 remediation) Embed() succeeding but the subsequent StoreSkillEmbedding call itself failing (e.g. a dimension mismatch between the returned vector and this table's `embedding vector(768)` column) -- so a re-embed attempted during an ON CONFLICT (name) DO UPDATE never leaves a STALE vector -- one computed from the skill's PREVIOUS content -- silently serving vector-KNN matches against content that no longer exists. "A failed store means the clear would fail too" does NOT generally hold: this UPDATE writes a literal NULL, which has no dimension (or other content-shaped constraint) to violate, so it succeeds independently of why the preceding store failed. Called unconditionally on every one of those four paths regardless of whether the skill was freshly inserted or updated: on a fresh insert the column is already NULL by column default, so this is a harmless no-op there.

func CurrentMigrationVersion

func CurrentMigrationVersion(ctx context.Context, pool *Pool) (int64, error)

CurrentMigrationVersion returns the highest applied migration version, or -1 if no migrations have been applied.

func EmbedAsync

func EmbedAsync(
	ctx context.Context,
	embedder Embedder,
	texts []string,
	maxConcurrency int,
) <-chan AsyncEmbedResult

EmbedAsync embeds each text in a separate goroutine (up to maxConcurrency at a time) and returns the results via a channel. The channel is closed when all work is complete.

Callers should range over the returned channel until closed.

func EmbedBatch

func EmbedBatch(
	ctx context.Context,
	embedder Embedder,
	texts []string,
	batchSize int,
) ([][]float32, error)

EmbedBatch breaks a large text slice into smaller batches and embeds them sequentially. If a single batch fails the entire operation fails.

func LogDependencyChange

func LogDependencyChange(
	ctx context.Context,
	pool *Pool,
	event string,
	skillID uuid.UUID,
	dependsOn uuid.UUID,
	relationType string,
) error

LogDependencyChange records a dependency addition or removal.

func LogEvent

func LogEvent(
	ctx context.Context,
	pool *Pool,
	event string,
	skillID *uuid.UUID,
	details json.RawMessage,
) error

LogEvent inserts a single event into the audit_log table.

event should be one of the AuditEvent* constants. skillID may be nil for system-level events. details may be nil if no extra JSON data is associated with the event.

func LogEventWithDetails

func LogEventWithDetails(
	ctx context.Context,
	pool *Pool,
	event string,
	skillID *uuid.UUID,
	details map[string]interface{},
) error

LogEventWithDetails is a convenience helper that marshals a details map into JSON and then calls LogEvent.

func LogEvidenceChange

func LogEvidenceChange(
	ctx context.Context,
	pool *Pool,
	event string,
	skillID uuid.UUID,
	evidenceID uuid.UUID,
	extra map[string]interface{},
) error

LogEvidenceChange records evidence addition or validation.

func LogSkillChange

func LogSkillChange(
	ctx context.Context,
	pool *Pool,
	event string,
	skillID uuid.UUID,
	changes map[string]interface{},
) error

LogSkillChange records a skill create/update/delete event.

func LogSystemEvent

func LogSystemEvent(
	ctx context.Context,
	pool *Pool,
	event string,
	details map[string]interface{},
) error

LogSystemEvent records a system-level event with no associated skill.

func Migrate

func Migrate(ctx context.Context, pool *Pool, migrationsDir string) error

Migrate runs all pending "up" migrations found in migrationsDir. It tracks applied versions in the schema_migrations table (created automatically).

Migration files must be named as NNN_description.up.sql and NNN_description.down.sql where NNN is a zero-padded version number. Migrations are executed in version order inside transactions.

Migrate is a thin wrapper over MigrateFS that reads the migrations from an on-disk directory (os.DirFS). Production startup (cmd/server) uses MigrateFS with the embedded migrations FS so migrations apply independently of the process working directory (§G23); this string-path form is retained for the test suites that drive a controlled on-disk migration subset.

func MigrateDown

func MigrateDown(ctx context.Context, pool *Pool, migrationsDir string, n int) error

MigrateDown rolls back the last n applied migrations. Use with caution; in production prefer targeted down migrations during maintenance windows.

Like Migrate, this is a thin wrapper over MigrateDownFS reading from an on-disk directory (os.DirFS); the string-path form is retained for the test suites.

func MigrateDownFS

func MigrateDownFS(ctx context.Context, pool *Pool, fsys fs.FS, n int) error

MigrateDownFS rolls back the last n applied migrations, reading the down migrations from fsys (an io/fs.FS rooted at the migrations directory). See MigrateFS for the FS-shape contract.

func MigrateFS

func MigrateFS(ctx context.Context, pool *Pool, fsys fs.FS) error

MigrateFS runs all pending "up" migrations discovered in fsys (an io/fs.FS). Both an embed.FS (the compiled-in migrations, cwd-independent) and os.DirFS(dir) (an on-disk directory) satisfy fs.FS, so the same discovery and apply logic serves the production embedded path AND the on-disk test path.

fsys MUST be rooted at the migrations directory itself, so its entries are the bare NNN_description.{up,down}.sql filenames (skillsystem.MigrationsFS and os.DirFS(dir) both satisfy this).

It tracks applied versions in the schema_migrations table (created automatically) and applies each pending migration in version order inside a transaction. Any discovery, read, or apply failure is returned to the caller UNMODIFIED-in-severity: it is a hard error, never swallowed — cmd/server treats it as fatal and refuses to bind the port (§G23 fail-closed).

func PruneAuditLog

func PruneAuditLog(ctx context.Context, pool *Pool, olderThan time.Duration) (int64, error)

PruneAuditLog deletes audit log entries older than the given duration.

func QueryColumnVectorDimension

func QueryColumnVectorDimension(ctx context.Context, pool *Pool, table, column string) (int, error)

QueryColumnVectorDimension reads the ACTUAL pgvector dimension declared on table.column from the live database's system catalog (pg_attribute joined to pg_class, rendered via format_type) — the single authoritative source of truth for "what dimension is this column really sized for" (§11.4.201). It is NEVER derived from a hardcoded Go constant or from the embedding provider's own configuration.

Returns an error if the column does not exist (or has been dropped), or if it exists but is not a dimensioned pgvector column: a bare "vector" with no typmod accepts a vector of ANY length, which would defeat the entire purpose of a boot-time dimension assertion, so that case is rejected rather than silently treated as "no constraint" (§11.4.201: an unresolvable/ambiguous signal refuses, it never assumes OK).

func StoreEvidenceEmbedding

func StoreEvidenceEmbedding(
	ctx context.Context,
	pool *Pool,
	evidenceID uuid.UUID,
	embedding pgvector.Vector,
) error

StoreEvidenceEmbedding updates the embedding vector for a specific evidence record.

func StoreSkillEmbedding

func StoreSkillEmbedding(
	ctx context.Context,
	pool *Pool,
	skillID uuid.UUID,
	embedding pgvector.Vector,
) error

StoreEmbedding updates the embedding vector for a specific skill.

func VectorIndexStats

func VectorIndexStats(
	ctx context.Context,
	pool *Pool,
	table string,
) (indexedCount int64, indexSizeBytes int64, err error)

VectorIndexStats returns the number of indexed vectors and index size for the given table's embedding column.

func WaitForVectorIndexReady

func WaitForVectorIndexReady(
	ctx context.Context,
	pool *Pool,
	table string,
	timeout time.Duration,
) error

WaitForVectorIndexReady polls until the pgvector HNSW index for the given table is ready (indisvalid = true), or until the timeout.

Error handling distinguishes the two cases the previous swallow-everything `continue` conflated (§11.4.201 — a guard must assert the real condition, not a proxy that any failure satisfies): pgx.ErrNoRows means the index does not exist yet and polling should continue, whereas any OTHER query error means the query itself (or the connection) is broken and is surfaced immediately rather than silently spun on until the deadline. A per-iteration query cancelled by this function's own deadline is not a broken query — it is deferred to the ctx.Done() branch so the timeout is reported once, with its clearer message.

Types

type AsyncEmbedResult

type AsyncEmbedResult struct {
	Index  int
	Vector []float32
	Error  error
}

AsyncEmbedResult holds the result of a single async embedding job.

type AuditLogEntry

type AuditLogEntry struct {
	Timestamp time.Time       `db:"ts"`
	Event     string          `db:"event"`
	SkillID   *uuid.UUID      `db:"skill_id"`
	Details   json.RawMessage `db:"details"`
}

AuditLogEntry mirrors models.AuditLogEntry for database scanning.

func AuditLogForEvent

func AuditLogForEvent(
	ctx context.Context,
	pool *Pool,
	event string,
	limit int,
) ([]AuditLogEntry, error)

AuditLogForEvent returns audit entries filtered to a specific event type.

func AuditLogForSkill

func AuditLogForSkill(
	ctx context.Context,
	pool *Pool,
	skillID uuid.UUID,
	limit int,
) ([]AuditLogEntry, error)

AuditLogForSkill returns audit entries filtered to a specific skill.

func RecentAuditLog

func RecentAuditLog(ctx context.Context, pool *Pool, limit int) ([]AuditLogEntry, error)

RecentAuditLog returns the most recent audit log entries, up to limit.

type BatchEmbedConfig

type BatchEmbedConfig struct {
	// BatchSize is the number of skills to embed per provider call.
	// A value <= 0 defaults to 100.
	BatchSize int

	// RequestsPerSecond caps the rate of embedding-provider API calls.
	// A value <= 0 defaults to 10 (conservative).
	RequestsPerSecond float64

	// OnProgress is an optional callback invoked after each batch.
	OnProgress EmbeddingProgress
}

BatchEmbedConfig controls batch-embedding behaviour.

type Embedder

type Embedder interface {
	// Embed returns a vector for each input text. The returned slice has
	// the same length as texts. Each inner slice has Dimensions() elements.
	Embed(ctx context.Context, texts []string) ([][]float32, error)

	// Dimensions returns the embedding vector size (e.g. 768, 1536).
	Dimensions() int
}

Embedder converts text into dense vector embeddings suitable for storage in pgvector columns and similarity search.

func NewEmbedderFromConfig

func NewEmbedderFromConfig(cfg config.EmbeddingConfig) (Embedder, error)

NewEmbedderFromConfig creates the appropriate Embedder implementation based on the embedding provider configured in the application config.

Supported providers:

  • "openai" -> OpenAIEmbedder
  • "local" -> LocalEmbedder

NewEmbedderFromConfig is fail-closed on an incompletely-specified provider: it errors rather than construct an Embedder that is guaranteed to fail its first real request. This is the SINGLE source of truth for "is this provider fully configured" -- callers (e.g. internal/mcp.NewMCPServer's §G29 hybrid-search wiring) determine whether to attach an embedder solely from this function's error return, never by re-deriving the same per-provider policy a second time (Fable code-review remediation, finding 6a: a duplicated "is it configured" check is a second source of truth that can silently drift from this one).

A positive cfg.Dimensions is a CONSTRUCTION INVARIANT enforced here, once, for every provider (Fable round-2 finding F4): both OpenAIEmbedder.Embed and LocalEmbedder.Embed unconditionally reject any response whose embedding length does not equal e.dimensions -- a zero (or negative) e.dimensions would make every single Embed call fail that check, since a real provider response is never zero-length. config.Load()'s own validate() already rejects embedding.dimensions <= 0 before a *Config ever reaches this factory, but this factory is the SOLE production constructor (see above) and MUST NOT rely solely on that upstream, call-site-external check to hold an invariant its own returned Embedder depends on to ever succeed -- the check belongs at the point of construction.

type EmbeddingProgress

type EmbeddingProgress func(succeeded, failed, total int)

EmbeddingProgress is called after each batch completes to report progress. succeeded is the cumulative number of skills embedded so far; failed is the cumulative number of failures; total is the total number of skills requested.

type EmbeddingProvider

type EmbeddingProvider = Embedder

EmbeddingProvider is an alias for the Embedder interface in this package, used to clarify the batch-embedding contract.

type LocalEmbedder

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

LocalEmbedder calls a local embedding model server via HTTP. The expected endpoint accepts a JSON POST body { "texts": [...] } and returns { "embeddings": [[...], ...] }.

func NewLocalEmbedder

func NewLocalEmbedder(cfg config.EmbeddingConfig) *LocalEmbedder

NewLocalEmbedder creates a local embedder from configuration.

func (*LocalEmbedder) Dimensions

func (e *LocalEmbedder) Dimensions() int

Dimensions returns the configured embedding dimensionality.

func (*LocalEmbedder) Embed

func (e *LocalEmbedder) Embed(ctx context.Context, texts []string) ([][]float32, error)

Embed calls the local embedding server.

func (*LocalEmbedder) SetHTTPClient

func (e *LocalEmbedder) SetHTTPClient(client *http.Client)

SetHTTPClient replaces the default HTTP client.

type OpenAIEmbedder

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

OpenAIEmbedder calls the OpenAI Embeddings API.

func NewOpenAIEmbedder

func NewOpenAIEmbedder(cfg config.EmbeddingConfig) *OpenAIEmbedder

NewOpenAIEmbedder creates an OpenAI embedder from configuration.

func (*OpenAIEmbedder) Dimensions

func (e *OpenAIEmbedder) Dimensions() int

Dimensions returns the configured embedding dimensionality.

func (*OpenAIEmbedder) Embed

func (e *OpenAIEmbedder) Embed(ctx context.Context, texts []string) ([][]float32, error)

Embed calls the OpenAI embeddings endpoint.

func (*OpenAIEmbedder) SetHTTPClient

func (e *OpenAIEmbedder) SetHTTPClient(client *http.Client)

SetHTTPClient replaces the default HTTP client (useful for tests or custom transport).

type Pool

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

Pool wraps *pgxpool.Pool with pgvector type support and convenience methods for the skill graph application.

func New

func New(cfg config.DatabaseConfig) (*Pool, error)

New creates a new PostgreSQL connection pool from the provided configuration. It registers the pgvector type with pgx and verifies connectivity with a ping before returning.

The caller is responsible for calling Close() when the pool is no longer needed.

func (*Pool) Close

func (p *Pool) Close()

Close gracefully shuts down the connection pool.

func (*Pool) Exec

func (p *Pool) Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error)

Exec is a thin wrapper around pgxpool.Pool.Exec. It returns the command tag (which carries the number of rows affected) alongside any error, mirroring the pgx-native signature so callers can inspect RowsAffected.

func (*Pool) Health

func (p *Pool) Health(ctx context.Context) error

Health performs a connectivity check on the pool. Returns nil if the database is reachable, otherwise an error.

func (*Pool) Inner

func (p *Pool) Inner() *pgxpool.Pool

Inner returns the underlying *pgxpool.Pool for advanced use cases.

func (*Pool) Query

func (p *Pool) Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error)

Query is a thin wrapper around pgxpool.Pool.Query.

func (*Pool) QueryRow

func (p *Pool) QueryRow(ctx context.Context, sql string, args ...any) pgx.Row

QueryRow is a thin wrapper around pgxpool.Pool.QueryRow.

func (*Pool) Stats

func (p *Pool) Stats() pgxpool.Stat

Stats returns current pool statistics for observability.

func (*Pool) StdlibDB

func (p *Pool) StdlibDB() *sql.DB

StdlibDB returns a *sql.DB backed by the same pgx pool. This is useful when a library requires the standard database/sql interface (e.g. golang-migrate).

The returned *sql.DB shares the same underlying pool. Callers should NOT close it independently; Close the Pool instead.

func (*Pool) WithTx

func (p *Pool) WithTx(ctx context.Context, fn TxFn) error

WithTx acquires a connection, begins a transaction, executes fn, and commits on success or rolls back on error.

type PoolStat

type PoolStat struct {
	MaxOpenConnections int           `json:"max_open_connections"`
	OpenConnections    int           `json:"open_connections"`
	InUse              int           `json:"in_use"`
	Idle               int           `json:"idle"`
	WaitCount          int64         `json:"wait_count"`
	WaitDuration       time.Duration `json:"wait_duration"`
	MaxIdleClosed      int64         `json:"max_idle_closed"`
	MaxLifetimeClosed  int64         `json:"max_lifetime_closed"`
}

PoolStat holds the statistics for a single database pool.

type PoolStats

type PoolStats struct {
	Primary    PoolStat `json:"primary"`
	Replica    PoolStat `json:"replica"`
	HasReplica bool     `json:"has_replica"`
}

PoolStats reports the state of both the primary and replica pools.

type ReadWritePool

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

ReadWritePool separates read and write traffic across two database connection pools. The primary pool handles all write operations and serves as fallback when no replica is configured or when the replica is unhealthy.

func NewReadWritePool

func NewReadWritePool(cfg config.DatabaseConfig) (*ReadWritePool, error)

NewReadWritePool creates a ReadWritePool from the provided database configuration. The primary pool is always created. If cfg.Replica.DSN is non-empty, a second pool is opened for the replica.

Both pools are pinged before returning. If the replica ping fails, the system logs a warning and falls back to primary-only mode — it never fails closed on a missing replica.

func (*ReadWritePool) Close

func (p *ReadWritePool) Close() error

Close shuts down both connection pools.

func (*ReadWritePool) Health

func (p *ReadWritePool) Health(ctx context.Context) error

Health checks the primary pool. If a replica is configured, it also checks replica health and replication lag. When the replica is unhealthy, reads are routed to the primary until the next successful health check.

func (*ReadWritePool) Read

func (p *ReadWritePool) Read() *sql.DB

Read returns the database connection for read operations. If a replica is configured and healthy, reads go to the replica. Otherwise they go to the primary.

func (*ReadWritePool) Stats

func (p *ReadWritePool) Stats() PoolStats

Stats returns connection-pool statistics for both pools. If a pool is nil (e.g. in tests), the corresponding stats are zero-valued.

func (*ReadWritePool) Write

func (p *ReadWritePool) Write() *sql.DB

Write returns the primary database connection for write operations. All writes always go to the primary.

type TxFn

type TxFn func(tx pgx.Tx) error

TxFn is a function executed inside a database transaction.

type VectorSearchResult

type VectorSearchResult struct {
	ID    uuid.UUID
	Score float64 // cosine similarity (higher = more similar)
}

VectorSearchResult holds a single result from vector similarity search.

func FindSimilarEvidences

func FindSimilarEvidences(
	ctx context.Context,
	pool *Pool,
	embedding pgvector.Vector,
	limit int,
) ([]VectorSearchResult, error)

FindSimilarEvidences performs vector search against the evidences table.

func FindSimilarSkills

func FindSimilarSkills(
	ctx context.Context,
	pool *Pool,
	embedding pgvector.Vector,
	limit int,
) ([]VectorSearchResult, error)

FindSimilarSkills performs vector search against the skills table and returns the matching skill IDs with scores.

func HybridSearch

func HybridSearch(
	ctx context.Context,
	pool *Pool,
	table string,
	embedding pgvector.Vector,
	keywords string,
	limit int,
) ([]VectorSearchResult, error)

HybridSearch combines vector similarity and full-text keyword search using Reciprocal Rank Fusion (RRF). The final results are ordered by the RRF score descending.

The keywords string is split on whitespace and used in a tsvector match against the name, title, and description columns.

func VectorSearch

func VectorSearch(
	ctx context.Context,
	pool *Pool,
	table string,
	embedding pgvector.Vector,
	limit int,
) ([]VectorSearchResult, error)

VectorSearch performs cosine-similarity search against the embedding column of the given table. Returns up to limit results ordered by similarity descending.

The table must have an embedding column of type vector(N) and an HNSW index for acceptable performance at scale.

func VectorSearchFiltered

func VectorSearchFiltered(
	ctx context.Context,
	pool *Pool,
	table string,
	embedding pgvector.Vector,
	status string,
	limit int,
) ([]VectorSearchResult, error)

VectorSearchFiltered performs vector search with an additional status filter.

Jump to

Keyboard shortcuts

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