Files
wehub-resource-sync f99010fae1
Desktop Artifacts / Desktop Build (Linux) (push) Waiting to run
Desktop Artifacts / Desktop Build (Windows) (push) Waiting to run
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Waiting to run
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

293 lines
11 KiB
Go

package postgres
import (
"context"
"database/sql"
"fmt"
"log"
)
// vectorBaseDDL creates the backend-agnostic vector tables. Chunk tables
// are per-generation (fixed halfvec dimension) and created lazily by
// ensureVectorChunkTable. vector_documents mirrors the local vectors.db
// mirror table (internal/vector/index.go messageMirrorDDL); vector_push_state
// is the pusher-side delta state, written transactionally with doc/chunk
// upserts so a PG reset wipes state and data together (self-healing).
const vectorBaseDDL = `
CREATE TABLE IF NOT EXISTS vector_generations (
id BIGSERIAL PRIMARY KEY,
fingerprint TEXT NOT NULL UNIQUE,
model TEXT NOT NULL,
dimension INTEGER NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS vector_generation_machines (
generation_id BIGINT NOT NULL,
machine TEXT NOT NULL,
last_push_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (generation_id, machine)
);
CREATE TABLE IF NOT EXISTS vector_documents (
doc_key TEXT PRIMARY KEY,
session_id TEXT NOT NULL,
source_uuid TEXT NOT NULL DEFAULT '',
ordinal INTEGER NOT NULL,
ordinal_end INTEGER NOT NULL,
subordinate BOOLEAN NOT NULL DEFAULT FALSE,
offsets TEXT NOT NULL DEFAULT '[]',
content TEXT NOT NULL,
content_hash TEXT NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_vector_documents_session_ordinal
ON vector_documents (session_id, ordinal);
CREATE TABLE IF NOT EXISTS vector_push_state (
generation_id BIGINT NOT NULL,
session_id TEXT NOT NULL,
doc_agg_hash TEXT NOT NULL,
PRIMARY KEY (generation_id, session_id)
);
`
// VectorExtensionAvailable reports whether the pgvector extension is
// installed in this database (probe only; no DDL).
func VectorExtensionAvailable(ctx context.Context, pg *sql.DB) (bool, error) {
var one int
err := pg.QueryRowContext(ctx,
`SELECT 1 FROM pg_extension WHERE extname = 'vector'`).Scan(&one)
if err == sql.ErrNoRows {
return false, nil
}
if err != nil {
return false, fmt.Errorf("probing pgvector extension: %w", err)
}
return true, nil
}
// vectorHalfvecAvailable reports whether the installed pgvector provides the
// halfvec type (added in pgvector 0.7.0), plus the installed extension
// version for diagnostics. The type probe is scoped to the extension's own
// namespace so an unrelated type named halfvec elsewhere cannot satisfy it.
func vectorHalfvecAvailable(ctx context.Context, pg *sql.DB) (bool, string, error) {
var hasHalfvec bool
var version string
err := pg.QueryRowContext(ctx, `
SELECT EXISTS (
SELECT 1 FROM pg_type t
WHERE t.typname = 'halfvec' AND t.typnamespace = e.extnamespace
), e.extversion
FROM pg_extension e WHERE e.extname = 'vector'`).Scan(&hasHalfvec, &version)
if err == sql.ErrNoRows {
return false, "", nil
}
if err != nil {
return false, "", fmt.Errorf("probing pgvector halfvec support: %w", err)
}
return hasHalfvec, version, nil
}
// ensureVectorBaseSchemaPG best-effort-installs pgvector and creates the
// base vector tables. A non-empty reason (with nil error) means vectors are
// unavailable on this server: like createContentSearchIndexesPG it never
// fails schema setup — CockroachDB, a missing package, a role without CREATE
// privilege, or a pgvector too old for halfvec leave semantic search
// unavailable with a one-line notice.
func ensureVectorBaseSchemaPG(ctx context.Context, pg *sql.DB) (string, error) {
if _, err := pg.ExecContext(ctx,
`CREATE EXTENSION IF NOT EXISTS vector`); err != nil {
log.Printf("pg schema: pgvector unavailable, semantic search disabled: %v", err)
}
available, err := VectorExtensionAvailable(ctx, pg)
if err != nil {
return "", err
}
if !available {
return "pgvector extension unavailable", nil
}
hasHalfvec, version, err := vectorHalfvecAvailable(ctx, pg)
if err != nil {
return "", err
}
if !hasHalfvec {
// CREATE EXTENSION IF NOT EXISTS never upgrades an existing
// extension object, so a server whose pgvector package was upgraded
// in place can still run an old extension version. ALTER EXTENSION
// heals that case; a server with no newer version fails it and
// vectors degrade below.
if _, err := pg.ExecContext(ctx, `ALTER EXTENSION vector UPDATE`); err != nil {
log.Printf("pg schema: pgvector %s upgrade attempt failed: %v", version, err)
} else if hasHalfvec, version, err = vectorHalfvecAvailable(ctx, pg); err != nil {
return "", err
}
}
if !hasHalfvec {
reason := fmt.Sprintf(
"pgvector %s lacks the halfvec type; upgrade the server to pgvector 0.7.0+",
version)
log.Printf("pg schema: %s; semantic search disabled", reason)
return reason, nil
}
if _, err := pg.ExecContext(ctx, vectorBaseDDL); err != nil {
return "", fmt.Errorf("creating vector base schema: %w", err)
}
return "", nil
}
// vectorChunkTable names generation genID's chunk table. Per-generation
// tables are required: pgvector columns have a fixed typed dimension and
// HNSW requires it (mirrors sqlite-vec's per-generation vec0 tables).
func vectorChunkTable(genID int64) string {
return fmt.Sprintf("vector_chunks_g%d", genID)
}
// vectorExtensionSchema locates the namespace pgvector's types and operator
// classes were installed into. CREATE EXTENSION installs into whichever
// schema was first on search_path the first time it ran anywhere in the
// database, which is not necessarily the connection's current search_path
// (Open sets search_path to the target schema only, mirroring
// createContentSearchIndexesPG's pg_trgm lookup). halfvec and
// halfvec_cosine_ops must be schema-qualified with it, or every schema
// besides the one that happened to install the extension fails to resolve
// them.
func vectorExtensionSchema(ctx context.Context, pg *sql.DB) (string, error) {
var extSchema string
if err := pg.QueryRowContext(ctx,
`SELECT n.nspname FROM pg_extension e
JOIN pg_namespace n ON n.oid = e.extnamespace
WHERE e.extname = 'vector'`,
).Scan(&extSchema); err != nil {
return "", fmt.Errorf("locating pgvector schema: %w", err)
}
quoted, err := quoteIdentifier(extSchema)
if err != nil {
return "", fmt.Errorf("invalid pgvector schema %q: %w", extSchema, err)
}
return quoted, nil
}
// ensureVectorChunkTable creates genID's halfvec chunk table and its HNSW
// cosine index. halfvec stays under HNSW's dimension limits (covers
// 2560-dim models plain vector cannot index) and halves storage.
func ensureVectorChunkTable(
ctx context.Context, pg *sql.DB, genID int64, dimension int,
) error {
extSchema, err := vectorExtensionSchema(ctx, pg)
if err != nil {
return err
}
table := vectorChunkTable(genID)
if _, err := pg.ExecContext(ctx, fmt.Sprintf(`
CREATE TABLE IF NOT EXISTS %s (
doc_key TEXT NOT NULL,
chunk_index INTEGER NOT NULL,
embedding %s.halfvec(%d) NOT NULL,
PRIMARY KEY (doc_key, chunk_index)
)`, table, extSchema, dimension)); err != nil {
return fmt.Errorf("creating %s: %w", table, err)
}
if _, err := pg.ExecContext(ctx, fmt.Sprintf(
`CREATE INDEX IF NOT EXISTS idx_%s_hnsw ON %s
USING hnsw (embedding %s.halfvec_cosine_ops)`, table, table, extSchema)); err != nil {
return fmt.Errorf("creating %s hnsw index: %w", table, err)
}
return nil
}
// ensureVectorGeneration registers fingerprint's generation, returning its
// id. Machines with matching embedding configs share one generation.
func ensureVectorGeneration(
ctx context.Context, pg *sql.DB, fingerprint, model string, dimension int,
) (int64, error) {
if _, err := pg.ExecContext(ctx, `
INSERT INTO vector_generations (fingerprint, model, dimension)
VALUES ($1, $2, $3) ON CONFLICT (fingerprint) DO NOTHING`,
fingerprint, model, dimension); err != nil {
return 0, fmt.Errorf("registering vector generation: %w", err)
}
id, _, ok, err := LookupVectorGeneration(ctx, pg, fingerprint)
if err != nil {
return 0, err
}
if !ok {
return 0, fmt.Errorf("vector generation %q vanished after insert", fingerprint)
}
return id, nil
}
// LookupVectorGeneration resolves a config fingerprint to its PG
// generation, the read-side gate pg serve checks at startup. A missing
// vector_generations table (pgvector never installed, SQLSTATE 42P01) is
// treated as a clean not-found, not an error, so pg serve degrades to a
// plain "semantic search unavailable" notice instead of failing startup.
func LookupVectorGeneration(
ctx context.Context, pg *sql.DB, fingerprint string,
) (int64, int, bool, error) {
var id int64
var dim int
err := pg.QueryRowContext(ctx,
`SELECT id, dimension FROM vector_generations WHERE fingerprint = $1`,
fingerprint).Scan(&id, &dim)
if err == sql.ErrNoRows {
return 0, 0, false, nil
}
if isUndefinedTable(err) {
return 0, 0, false, nil
}
if err != nil {
return 0, 0, false, fmt.Errorf("looking up vector generation: %w", err)
}
return id, dim, true, nil
}
// VectorChunkTableExists reports whether genID's chunk table exists — the
// second half of the read-side startup gate. A generation row can exist
// without its chunk table: registering the generation and creating the table
// are separate statements on the push side, so a push interrupted between
// them (or a manual table drop) leaves the row behind. Wiring a searcher
// against such a generation would fail every query with a missing-relation
// error instead of degrading to db.ErrSemanticUnavailable. The unqualified
// name resolves through the connection's search_path, like every other
// read-side query.
func VectorChunkTableExists(
ctx context.Context, pg *sql.DB, genID int64,
) (bool, error) {
var present bool
if err := pg.QueryRowContext(ctx, `SELECT to_regclass($1) IS NOT NULL`,
vectorChunkTable(genID)).Scan(&present); err != nil {
return false, fmt.Errorf(
"probing chunk table for generation %d: %w", genID, err)
}
return present, nil
}
// ListVectorGenerationFingerprints returns every registered generation's
// fingerprint ordered by id (oldest first), for pg serve's startup notice
// when no generation matches the local config. A missing vector_generations
// table (SQLSTATE 42P01) yields an empty slice, not an error, matching
// LookupVectorGeneration's tolerance for a database without pgvector.
func ListVectorGenerationFingerprints(
ctx context.Context, pg *sql.DB,
) ([]string, error) {
rows, err := pg.QueryContext(ctx,
`SELECT fingerprint FROM vector_generations ORDER BY id`)
if isUndefinedTable(err) {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("listing vector generations: %w", err)
}
defer func() { _ = rows.Close() }()
var fingerprints []string
for rows.Next() {
var fp string
if err := rows.Scan(&fp); err != nil {
return nil, fmt.Errorf("scanning vector generation fingerprint: %w", err)
}
fingerprints = append(fingerprints, fp)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterating vector generations: %w", err)
}
return fingerprints, nil
}