This is a fundamental architectural shift from binary SQLite to JSONL as the source of truth for git workflows. ## New Features - `bd export --format=jsonl` - Export issues to JSON Lines format - `bd import` - Import issues from JSONL (create new, update existing) - `--skip-existing` flag for import to only create new issues ## Architecture Change **Before:** Binary SQLite database committed to git **After:** JSONL text files as source of truth, SQLite as ephemeral cache Benefits: - Git-friendly text format with clean diffs - AI-resolvable merge conflicts (append-only is 95% conflict-free) - Human-readable issue tracking in git - No binary merge conflicts ## Documentation - Updated README with JSONL-first workflow and git hooks - Added TEXT_FORMATS.md analyzing JSONL vs CSV vs binary - Updated GIT_WORKFLOW.md with historical context - .gitignore now excludes *.db, includes .beads/*.jsonl ## Implementation Details - Export sorts issues by ID for consistent diffs - Import handles both creates and updates atomically - Proper handling of pointer fields (EstimatedMinutes) - All tests passing ## Breaking Changes - Database files (*.db) should now be gitignored - Use export/import workflow for git collaboration - Git hooks recommended for automation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
100 lines
3.2 KiB
Go
100 lines
3.2 KiB
Go
package sqlite
|
|
|
|
const schema = `
|
|
-- Issues table
|
|
CREATE TABLE IF NOT EXISTS issues (
|
|
id TEXT PRIMARY KEY,
|
|
title TEXT NOT NULL CHECK(length(title) <= 500),
|
|
description TEXT NOT NULL DEFAULT '',
|
|
design TEXT NOT NULL DEFAULT '',
|
|
acceptance_criteria TEXT NOT NULL DEFAULT '',
|
|
notes TEXT NOT NULL DEFAULT '',
|
|
status TEXT NOT NULL DEFAULT 'open',
|
|
priority INTEGER NOT NULL DEFAULT 2 CHECK(priority >= 0 AND priority <= 4),
|
|
issue_type TEXT NOT NULL DEFAULT 'task',
|
|
assignee TEXT,
|
|
estimated_minutes INTEGER,
|
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
closed_at DATETIME
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_issues_status ON issues(status);
|
|
CREATE INDEX IF NOT EXISTS idx_issues_priority ON issues(priority);
|
|
CREATE INDEX IF NOT EXISTS idx_issues_assignee ON issues(assignee);
|
|
CREATE INDEX IF NOT EXISTS idx_issues_created_at ON issues(created_at);
|
|
|
|
-- Dependencies table
|
|
CREATE TABLE IF NOT EXISTS dependencies (
|
|
issue_id TEXT NOT NULL,
|
|
depends_on_id TEXT NOT NULL,
|
|
type TEXT NOT NULL DEFAULT 'blocks',
|
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
created_by TEXT NOT NULL,
|
|
PRIMARY KEY (issue_id, depends_on_id),
|
|
FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (depends_on_id) REFERENCES issues(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_dependencies_issue ON dependencies(issue_id);
|
|
CREATE INDEX IF NOT EXISTS idx_dependencies_depends_on ON dependencies(depends_on_id);
|
|
|
|
-- Labels table
|
|
CREATE TABLE IF NOT EXISTS labels (
|
|
issue_id TEXT NOT NULL,
|
|
label TEXT NOT NULL,
|
|
PRIMARY KEY (issue_id, label),
|
|
FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_labels_label ON labels(label);
|
|
|
|
-- Events table (audit trail)
|
|
CREATE TABLE IF NOT EXISTS events (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
issue_id TEXT NOT NULL,
|
|
event_type TEXT NOT NULL,
|
|
actor TEXT NOT NULL,
|
|
old_value TEXT,
|
|
new_value TEXT,
|
|
comment TEXT,
|
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_events_issue ON events(issue_id);
|
|
CREATE INDEX IF NOT EXISTS idx_events_created_at ON events(created_at);
|
|
|
|
-- Config table (for storing settings like issue prefix)
|
|
CREATE TABLE IF NOT EXISTS config (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT NOT NULL
|
|
);
|
|
|
|
-- Ready work view
|
|
CREATE VIEW IF NOT EXISTS ready_issues AS
|
|
SELECT i.*
|
|
FROM issues i
|
|
WHERE i.status = 'open'
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM dependencies d
|
|
JOIN issues blocked ON d.depends_on_id = blocked.id
|
|
WHERE d.issue_id = i.id
|
|
AND d.type = 'blocks'
|
|
AND blocked.status IN ('open', 'in_progress', 'blocked')
|
|
);
|
|
|
|
-- Blocked issues view
|
|
CREATE VIEW IF NOT EXISTS blocked_issues AS
|
|
SELECT
|
|
i.*,
|
|
COUNT(d.depends_on_id) as blocked_by_count
|
|
FROM issues i
|
|
JOIN dependencies d ON i.id = d.issue_id
|
|
JOIN issues blocker ON d.depends_on_id = blocker.id
|
|
WHERE i.status IN ('open', 'in_progress', 'blocked')
|
|
AND d.type = 'blocks'
|
|
AND blocker.status IN ('open', 'in_progress', 'blocked')
|
|
GROUP BY i.id;
|
|
`
|