feat(schema): 002 — pages, page_revisions, refs with FTS + vector indexes
This commit is contained in:
61
lib/db/migrations/002_knowledge.sql
Normal file
61
lib/db/migrations/002_knowledge.sql
Normal file
@@ -0,0 +1,61 @@
|
||||
CREATE TABLE pages (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
space_id uuid REFERENCES spaces(id) ON DELETE SET NULL,
|
||||
slug text NOT NULL,
|
||||
title text NOT NULL,
|
||||
body_md text NOT NULL DEFAULT '',
|
||||
body_html text,
|
||||
parent_id uuid REFERENCES pages(id) ON DELETE SET NULL,
|
||||
embedding vector(1024),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE (space_id, slug)
|
||||
);
|
||||
|
||||
CREATE TABLE page_revisions (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
page_id uuid NOT NULL REFERENCES pages(id) ON DELETE CASCADE,
|
||||
body_md text NOT NULL,
|
||||
edited_by text,
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE refs (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
space_id uuid REFERENCES spaces(id) ON DELETE SET NULL,
|
||||
kind text NOT NULL
|
||||
CHECK (kind IN ('url','video','pdf','image','file')),
|
||||
source_url text,
|
||||
title text,
|
||||
description text,
|
||||
summary text,
|
||||
body_text text,
|
||||
blob_path text,
|
||||
thumbnail text,
|
||||
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
embedding vector(1024),
|
||||
status text NOT NULL DEFAULT 'ingested'
|
||||
CHECK (status IN ('ingested','indexed','enriched')),
|
||||
source_kind text,
|
||||
external_id text,
|
||||
captured_at timestamptz NOT NULL DEFAULT now(),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_pages_space ON pages(space_id);
|
||||
CREATE INDEX idx_pages_parent ON pages(parent_id);
|
||||
CREATE INDEX idx_pages_fts ON pages
|
||||
USING GIN (to_tsvector('english', title || ' ' || body_md));
|
||||
CREATE INDEX idx_pages_embed ON pages
|
||||
USING hnsw (embedding vector_cosine_ops);
|
||||
|
||||
CREATE INDEX idx_refs_space ON refs(space_id);
|
||||
CREATE INDEX idx_refs_kind ON refs(kind);
|
||||
CREATE INDEX idx_refs_external ON refs(source_kind, external_id)
|
||||
WHERE source_kind IS NOT NULL;
|
||||
CREATE INDEX idx_refs_fts ON refs USING GIN (
|
||||
to_tsvector('english',
|
||||
coalesce(title,'') || ' ' || coalesce(summary,'') || ' ' || coalesce(body_text,''))
|
||||
);
|
||||
CREATE INDEX idx_refs_embed ON refs USING hnsw (embedding vector_cosine_ops);
|
||||
29
tests/db/migration_002.test.js
Normal file
29
tests/db/migration_002.test.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { resetDb, withClient } from '../helpers/db.js';
|
||||
import { migrateUp } from '../../lib/db/migrate.js';
|
||||
|
||||
describe('migration 002 — knowledge', () => {
|
||||
beforeEach(async () => { await resetDb(); await migrateUp(); });
|
||||
|
||||
it('creates pages, page_revisions, refs', async () => {
|
||||
await withClient(async (c) => {
|
||||
for (const t of ['pages','page_revisions','refs']) {
|
||||
const { rows } = await c.query(
|
||||
`SELECT to_regclass('public.' || $1) AS t;`, [t]
|
||||
);
|
||||
expect(rows[0].t).toBe(t);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('refs.kind check enforces enum', async () => {
|
||||
await withClient(async (c) => {
|
||||
const { rows: [s] } = await c.query(
|
||||
`INSERT INTO spaces(slug,name) VALUES('h','H') RETURNING id;`
|
||||
);
|
||||
await expect(c.query(
|
||||
`INSERT INTO refs(space_id, kind) VALUES($1, 'invalid');`, [s.id]
|
||||
)).rejects.toThrow(/check/i);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user