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>
268 lines
9.1 KiB
Go
268 lines
9.1 KiB
Go
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;
|
|
`
|