Fix schema drift: fresh installs were broken

- Create heat_cycles table in init.js (breeding.js used it but nothing created it)
- Add litters columns the routes actually use (breeding_date, whelping_date, puppy_count)
- Add health_records columns used by puppy logs (record_date, description)
- Puppy log insert now also sets NOT NULL test_date
- Migration 002 no-ops on a fresh DB instead of crashing on missing dogs table
- init.js honors DB_PATH and creates the data dir, so migrations and the app
  always target the same file

Verified: full heat-cycle/litter/puppy-log CRUD passes against a virgin database.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jason Stedwell
2026-07-06 14:45:22 -05:00
parent c9d2aa734e
commit ce287f63d1
3 changed files with 54 additions and 6 deletions
+37 -2
View File
@@ -2,8 +2,11 @@ const Database = require('better-sqlite3');
const path = require('path');
const fs = require('fs');
const dbPath = path.join(__dirname, '../../data');
const db = new Database(path.join(dbPath, 'breedr.db'));
// Honor DB_PATH (used by Docker and the migration runner in index.js) so
// migrations and the app always operate on the same file.
const dbFile = process.env.DB_PATH || path.join(__dirname, '../../data/breedr.db');
fs.mkdirSync(path.dirname(dbFile), { recursive: true });
const db = new Database(dbFile);
function getDatabase() {
return db;
@@ -91,6 +94,9 @@ function initDatabase() {
male_count INTEGER DEFAULT 0,
female_count INTEGER DEFAULT 0,
stillborn_count INTEGER DEFAULT 0,
breeding_date TEXT,
whelping_date TEXT,
puppy_count INTEGER DEFAULT 0,
notes TEXT,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now')),
@@ -100,6 +106,33 @@ function initDatabase() {
)
`);
// migrate: columns the litters routes use (routes/litters.js) that older
// schemas lack
const litterMigrations = [
['breeding_date', 'TEXT'],
['whelping_date', 'TEXT'],
['puppy_count', 'INTEGER DEFAULT 0'],
];
for (const [col, def] of litterMigrations) {
try { db.exec(`ALTER TABLE litters ADD COLUMN ${col} ${def}`); } catch (_) { /* already exists */ }
}
// ── Heat Cycles ───────────────────────────────────────────────────────────
db.exec(`
CREATE TABLE IF NOT EXISTS heat_cycles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
dog_id INTEGER NOT NULL,
start_date TEXT NOT NULL,
end_date TEXT,
breeding_date TEXT,
breeding_successful INTEGER DEFAULT 0,
notes TEXT,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (dog_id) REFERENCES dogs(id)
)
`);
// ── Health Records (OFA-extended) ─────────────────────────────────────────
db.exec(`
CREATE TABLE IF NOT EXISTS health_records (
@@ -135,6 +168,8 @@ function initDatabase() {
['result', 'TEXT'],
['vet_name', 'TEXT'],
['next_due', 'TEXT'],
['record_date', 'TEXT'],
['description', 'TEXT'],
];
for (const [col, def] of healthMigrations) {
try { db.exec(`ALTER TABLE health_records ADD COLUMN ${col} ${def}`); } catch (_) { /* already exists */ }
+12
View File
@@ -52,6 +52,13 @@ class MigrationRunner {
return columns.some(col => col.name === 'sire' || col.name === 'dam');
}
// Check if a table exists (fresh DBs have none — migrations must no-op)
tableExists(name) {
return !!this.db.prepare(
"SELECT name FROM sqlite_master WHERE type='table' AND name = ?"
).get(name);
}
// Check if litter_id column exists
hasLitterIdColumn() {
const columns = this.db.prepare("PRAGMA table_info(dogs)").all();
@@ -188,6 +195,11 @@ class MigrationRunner {
migration002_addLitterIdColumn() {
console.log('[Migration 002] Checking for litter_id column...');
if (!this.tableExists('dogs')) {
console.log('[Migration 002] Fresh database (no dogs table yet), skipping — init will create it');
return;
}
if (this.hasLitterIdColumn()) {
console.log('[Migration 002] litter_id column already exists, skipping');
return;
+4 -3
View File
@@ -214,10 +214,11 @@ router.post('/:litterId/puppies/:puppyId/logs', (req, res) => {
notes: notes || ''
});
// test_date mirrors record_date: the column is NOT NULL on fresh schemas
const result = db.prepare(`
INSERT INTO health_records (dog_id, record_type, record_date, description, vet_name)
VALUES (?, ?, ?, ?, ?)
`).run(puppyId, record_type || 'weight_log', record_date, description, null);
INSERT INTO health_records (dog_id, record_type, record_date, test_date, description, vet_name)
VALUES (?, ?, ?, ?, ?, ?)
`).run(puppyId, record_type || 'weight_log', record_date, record_date, description, null);
const log = db.prepare('SELECT * FROM health_records WHERE id = ?').get(result.lastInsertRowid);
res.status(201).json(log);