feat(db): add SQLite schema, FTS5, migration runner

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-17 15:04:59 +02:00
parent 2f3248c9a3
commit e90a37ff5e
4 changed files with 274 additions and 0 deletions

View File

@@ -0,0 +1,83 @@
import { describe, it, expect } from 'vitest';
import { openInMemoryForTest } from '../../src/lib/server/db';
import { runMigrations } from '../../src/lib/server/db/migrate';
describe('db migrations', () => {
it('creates all expected tables', () => {
const db = openInMemoryForTest();
const tables = (
db.prepare("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name").all() as {
name: string;
}[]
).map((r) => r.name);
for (const t of [
'profile',
'recipe',
'ingredient',
'step',
'tag',
'recipe_tag',
'rating',
'comment',
'favorite',
'cooking_log',
'allowed_domain',
'schema_migration'
]) {
expect(tables).toContain(t);
}
});
it('is idempotent', () => {
const db = openInMemoryForTest();
runMigrations(db);
const migs = db.prepare('SELECT COUNT(*) AS c FROM schema_migration').get() as { c: number };
expect(migs.c).toBe(1);
});
it('cascades recipe delete to ingredients and steps', () => {
const db = openInMemoryForTest();
const row = db
.prepare('INSERT INTO recipe(title) VALUES (?) RETURNING id')
.get('Test') as { id: number };
db.prepare('INSERT INTO ingredient(recipe_id, position, name) VALUES (?, ?, ?)').run(
row.id,
1,
'Salz'
);
db.prepare('INSERT INTO step(recipe_id, position, text) VALUES (?, ?, ?)').run(
row.id,
1,
'Kochen'
);
db.prepare('DELETE FROM recipe WHERE id = ?').run(row.id);
const ings = db.prepare('SELECT COUNT(*) AS c FROM ingredient').get() as { c: number };
const steps = db.prepare('SELECT COUNT(*) AS c FROM step').get() as { c: number };
expect(ings.c).toBe(0);
expect(steps.c).toBe(0);
});
it('FTS5 index finds recipes by title', () => {
const db = openInMemoryForTest();
db.prepare('INSERT INTO recipe(title, description) VALUES (?, ?)').run(
'Spaghetti Carbonara',
'Italienisches Pasta-Klassiker'
);
const hits = db
.prepare("SELECT rowid FROM recipe_fts WHERE recipe_fts MATCH 'carbonara'")
.all();
expect(hits.length).toBe(1);
});
it('enforces rating stars 1..5', () => {
const db = openInMemoryForTest();
db.prepare('INSERT INTO profile(name) VALUES (?)').run('Hendrik');
const r = db
.prepare('INSERT INTO recipe(title) VALUES (?) RETURNING id')
.get('Test') as { id: number };
db.prepare('INSERT INTO rating(recipe_id, profile_id, stars) VALUES (?, 1, 5)').run(r.id);
expect(() =>
db.prepare('INSERT INTO rating(recipe_id, profile_id, stars) VALUES (?, 1, 6)').run(r.id)
).toThrow();
});
});