feat(storage): add Dolt backend for version-controlled issue storage
Implements a complete Dolt storage backend that mirrors the SQLite implementation with MySQL-compatible syntax and adds version control capabilities. Key features: - Full Storage interface implementation (~50 methods) - Version control operations: commit, push, pull, branch, merge, checkout - History queries via AS OF and dolt_history_* tables - Cell-level merge instead of line-level JSONL merge - SQL injection protection with input validation Bug fixes applied during implementation: - Added missing quality_score, work_type, source_system to scanIssue - Fixed Status() to properly parse boolean staged column - Added validation to CreateIssues (was missing in batch create) - Made RenameDependencyPrefix transactional - Expanded GetIssueHistory to return more complete data Test coverage: 17 tests covering CRUD, dependencies, labels, search, comments, events, statistics, and SQL injection protection. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
267
internal/storage/dolt/schema.go
Normal file
267
internal/storage/dolt/schema.go
Normal file
@@ -0,0 +1,267 @@
|
||||
package dolt
|
||||
|
||||
// schema defines the MySQL-compatible database schema for Dolt.
|
||||
// This mirrors the SQLite schema but uses MySQL syntax.
|
||||
const schema = `
|
||||
-- Issues table
|
||||
CREATE TABLE IF NOT EXISTS issues (
|
||||
id VARCHAR(255) PRIMARY KEY,
|
||||
content_hash VARCHAR(64),
|
||||
title VARCHAR(500) NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
design TEXT NOT NULL,
|
||||
acceptance_criteria TEXT NOT NULL,
|
||||
notes TEXT NOT NULL,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'open',
|
||||
priority INT NOT NULL DEFAULT 2,
|
||||
issue_type VARCHAR(32) NOT NULL DEFAULT 'task',
|
||||
assignee VARCHAR(255),
|
||||
estimated_minutes INT,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by VARCHAR(255) DEFAULT '',
|
||||
owner VARCHAR(255) DEFAULT '',
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
closed_at DATETIME,
|
||||
closed_by_session VARCHAR(255) DEFAULT '',
|
||||
external_ref VARCHAR(255),
|
||||
compaction_level INT DEFAULT 0,
|
||||
compacted_at DATETIME,
|
||||
compacted_at_commit VARCHAR(64),
|
||||
original_size INT,
|
||||
deleted_at DATETIME,
|
||||
deleted_by VARCHAR(255) DEFAULT '',
|
||||
delete_reason TEXT DEFAULT '',
|
||||
original_type VARCHAR(32) DEFAULT '',
|
||||
-- Messaging fields
|
||||
sender VARCHAR(255) DEFAULT '',
|
||||
ephemeral TINYINT(1) DEFAULT 0,
|
||||
-- Pinned field
|
||||
pinned TINYINT(1) DEFAULT 0,
|
||||
-- Template field
|
||||
is_template TINYINT(1) DEFAULT 0,
|
||||
-- Work economics field (HOP Decision 006)
|
||||
crystallizes TINYINT(1) DEFAULT 0,
|
||||
-- Molecule type field
|
||||
mol_type VARCHAR(32) DEFAULT '',
|
||||
-- Work type field (Decision 006: mutex vs open_competition)
|
||||
work_type VARCHAR(32) DEFAULT 'mutex',
|
||||
-- HOP quality score field (0.0-1.0)
|
||||
quality_score DOUBLE,
|
||||
-- Federation source system field
|
||||
source_system VARCHAR(255) DEFAULT '',
|
||||
-- Source repo for multi-repo
|
||||
source_repo VARCHAR(512) DEFAULT '',
|
||||
-- Close reason
|
||||
close_reason TEXT DEFAULT '',
|
||||
-- Event fields
|
||||
event_kind VARCHAR(32) DEFAULT '',
|
||||
actor VARCHAR(255) DEFAULT '',
|
||||
target VARCHAR(255) DEFAULT '',
|
||||
payload TEXT DEFAULT '',
|
||||
-- Gate fields
|
||||
await_type VARCHAR(32) DEFAULT '',
|
||||
await_id VARCHAR(255) DEFAULT '',
|
||||
timeout_ns BIGINT DEFAULT 0,
|
||||
waiters TEXT DEFAULT '',
|
||||
-- Agent fields
|
||||
hook_bead VARCHAR(255) DEFAULT '',
|
||||
role_bead VARCHAR(255) DEFAULT '',
|
||||
agent_state VARCHAR(32) DEFAULT '',
|
||||
last_activity DATETIME,
|
||||
role_type VARCHAR(32) DEFAULT '',
|
||||
rig VARCHAR(255) DEFAULT '',
|
||||
-- Time-based scheduling fields
|
||||
due_at DATETIME,
|
||||
defer_until DATETIME,
|
||||
INDEX idx_issues_status (status),
|
||||
INDEX idx_issues_priority (priority),
|
||||
INDEX idx_issues_assignee (assignee),
|
||||
INDEX idx_issues_created_at (created_at),
|
||||
INDEX idx_issues_external_ref (external_ref)
|
||||
);
|
||||
|
||||
-- Dependencies table (edge schema)
|
||||
CREATE TABLE IF NOT EXISTS dependencies (
|
||||
issue_id VARCHAR(255) NOT NULL,
|
||||
depends_on_id VARCHAR(255) NOT NULL,
|
||||
type VARCHAR(32) NOT NULL DEFAULT 'blocks',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by VARCHAR(255) NOT NULL,
|
||||
metadata JSON DEFAULT (JSON_OBJECT()),
|
||||
thread_id VARCHAR(255) DEFAULT '',
|
||||
PRIMARY KEY (issue_id, depends_on_id),
|
||||
INDEX idx_dependencies_issue (issue_id),
|
||||
INDEX idx_dependencies_depends_on (depends_on_id),
|
||||
INDEX idx_dependencies_depends_on_type (depends_on_id, type),
|
||||
INDEX idx_dependencies_thread (thread_id),
|
||||
CONSTRAINT fk_dep_issue FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_dep_depends_on FOREIGN KEY (depends_on_id) REFERENCES issues(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Labels table
|
||||
CREATE TABLE IF NOT EXISTS labels (
|
||||
issue_id VARCHAR(255) NOT NULL,
|
||||
label VARCHAR(255) NOT NULL,
|
||||
PRIMARY KEY (issue_id, label),
|
||||
INDEX idx_labels_label (label),
|
||||
CONSTRAINT fk_labels_issue FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Comments table
|
||||
CREATE TABLE IF NOT EXISTS comments (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
issue_id VARCHAR(255) NOT NULL,
|
||||
author VARCHAR(255) NOT NULL,
|
||||
text TEXT NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_comments_issue (issue_id),
|
||||
INDEX idx_comments_created_at (created_at),
|
||||
CONSTRAINT fk_comments_issue FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Events table (audit trail)
|
||||
CREATE TABLE IF NOT EXISTS events (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
issue_id VARCHAR(255) NOT NULL,
|
||||
event_type VARCHAR(32) NOT NULL,
|
||||
actor VARCHAR(255) NOT NULL,
|
||||
old_value TEXT,
|
||||
new_value TEXT,
|
||||
comment TEXT,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_events_issue (issue_id),
|
||||
INDEX idx_events_created_at (created_at),
|
||||
CONSTRAINT fk_events_issue FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Config table
|
||||
CREATE TABLE IF NOT EXISTS config (
|
||||
` + "`key`" + ` VARCHAR(255) PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- Metadata table
|
||||
CREATE TABLE IF NOT EXISTS metadata (
|
||||
` + "`key`" + ` VARCHAR(255) PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- Dirty issues table (for incremental export)
|
||||
CREATE TABLE IF NOT EXISTS dirty_issues (
|
||||
issue_id VARCHAR(255) PRIMARY KEY,
|
||||
marked_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_dirty_issues_marked_at (marked_at),
|
||||
CONSTRAINT fk_dirty_issue FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Export hashes table
|
||||
CREATE TABLE IF NOT EXISTS export_hashes (
|
||||
issue_id VARCHAR(255) PRIMARY KEY,
|
||||
content_hash VARCHAR(64) NOT NULL,
|
||||
exported_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_export_issue FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Child counters table
|
||||
CREATE TABLE IF NOT EXISTS child_counters (
|
||||
parent_id VARCHAR(255) PRIMARY KEY,
|
||||
last_child INT NOT NULL DEFAULT 0,
|
||||
CONSTRAINT fk_counter_parent FOREIGN KEY (parent_id) REFERENCES issues(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Issue snapshots table (for compaction)
|
||||
CREATE TABLE IF NOT EXISTS issue_snapshots (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
issue_id VARCHAR(255) NOT NULL,
|
||||
snapshot_time DATETIME NOT NULL,
|
||||
compaction_level INT NOT NULL,
|
||||
original_size INT NOT NULL,
|
||||
compressed_size INT NOT NULL,
|
||||
original_content TEXT NOT NULL,
|
||||
archived_events TEXT,
|
||||
INDEX idx_snapshots_issue (issue_id),
|
||||
INDEX idx_snapshots_level (compaction_level),
|
||||
CONSTRAINT fk_snapshots_issue FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Compaction snapshots table
|
||||
CREATE TABLE IF NOT EXISTS compaction_snapshots (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
issue_id VARCHAR(255) NOT NULL,
|
||||
compaction_level INT NOT NULL,
|
||||
snapshot_json BLOB NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_comp_snap_issue (issue_id, compaction_level, created_at DESC),
|
||||
CONSTRAINT fk_comp_snap_issue FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Repository mtimes table (for multi-repo)
|
||||
CREATE TABLE IF NOT EXISTS repo_mtimes (
|
||||
repo_path VARCHAR(512) PRIMARY KEY,
|
||||
jsonl_path VARCHAR(512) NOT NULL,
|
||||
mtime_ns BIGINT NOT NULL,
|
||||
last_checked DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_repo_mtimes_checked (last_checked)
|
||||
);
|
||||
`
|
||||
|
||||
// defaultConfig contains the default configuration values
|
||||
const defaultConfig = `
|
||||
INSERT IGNORE INTO config (` + "`key`" + `, value) VALUES
|
||||
('compaction_enabled', 'false'),
|
||||
('compact_tier1_days', '30'),
|
||||
('compact_tier1_dep_levels', '2'),
|
||||
('compact_tier2_days', '90'),
|
||||
('compact_tier2_dep_levels', '5'),
|
||||
('compact_tier2_commits', '100'),
|
||||
('compact_model', 'claude-3-5-haiku-20241022'),
|
||||
('compact_batch_size', '50'),
|
||||
('compact_parallel_workers', '5'),
|
||||
('auto_compact_enabled', 'false');
|
||||
`
|
||||
|
||||
// readyIssuesView is a MySQL-compatible view for ready work
|
||||
// Note: Dolt supports recursive CTEs like SQLite
|
||||
const readyIssuesView = `
|
||||
CREATE OR REPLACE VIEW ready_issues AS
|
||||
WITH RECURSIVE
|
||||
blocked_directly AS (
|
||||
SELECT DISTINCT d.issue_id
|
||||
FROM dependencies d
|
||||
JOIN issues blocker ON d.depends_on_id = blocker.id
|
||||
WHERE d.type = 'blocks'
|
||||
AND blocker.status IN ('open', 'in_progress', 'blocked', 'deferred', 'hooked')
|
||||
),
|
||||
blocked_transitively AS (
|
||||
SELECT issue_id, 0 as depth
|
||||
FROM blocked_directly
|
||||
UNION ALL
|
||||
SELECT d.issue_id, bt.depth + 1
|
||||
FROM blocked_transitively bt
|
||||
JOIN dependencies d ON d.depends_on_id = bt.issue_id
|
||||
WHERE d.type = 'parent-child'
|
||||
AND bt.depth < 50
|
||||
)
|
||||
SELECT i.*
|
||||
FROM issues i
|
||||
WHERE i.status = 'open'
|
||||
AND (i.ephemeral = 0 OR i.ephemeral IS NULL)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM blocked_transitively WHERE issue_id = i.id
|
||||
);
|
||||
`
|
||||
|
||||
// blockedIssuesView is a MySQL-compatible view for blocked issues
|
||||
const blockedIssuesView = `
|
||||
CREATE OR REPLACE VIEW 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', 'deferred', 'hooked')
|
||||
AND d.type = 'blocks'
|
||||
AND blocker.status IN ('open', 'in_progress', 'blocked', 'deferred', 'hooked')
|
||||
GROUP BY i.id;
|
||||
`
|
||||
Reference in New Issue
Block a user