task_graph_mcp/export/
mod.rs1pub mod diff;
10
11use serde::{Deserialize, Serialize};
12use serde_json::Value;
13use std::collections::BTreeMap;
14
15pub const CURRENT_SCHEMA_VERSION: i32 = 3;
18
19pub const EXPORT_VERSION: &str = "1.0.0";
21
22pub const EXPORTED_TABLES: &[&str] = &[
24 "tasks",
25 "dependencies",
26 "attachments",
27 "task_tags",
28 "task_needed_tags",
29 "task_wanted_tags",
30 "task_sequence",
31];
32
33pub const EXCLUDED_TABLES: &[&str] = &[
35 "workers",
36 "file_locks",
37 "claim_sequence",
38 ];
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct Snapshot {
49 pub schema_version: i32,
52
53 pub export_version: String,
55
56 pub exported_at: String,
58
59 pub exported_by: String,
61
62 pub tables: BTreeMap<String, Vec<Value>>,
65}
66
67impl Snapshot {
68 pub fn new() -> Self {
70 Self {
71 schema_version: CURRENT_SCHEMA_VERSION,
72 export_version: EXPORT_VERSION.to_string(),
73 exported_at: chrono::Utc::now().to_rfc3339(),
74 exported_by: format!("task-graph-mcp v{}", env!("CARGO_PKG_VERSION")),
75 tables: BTreeMap::new(),
76 }
77 }
78
79 pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
81 serde_json::from_str(json)
82 }
83
84 pub fn from_file(path: &std::path::Path) -> anyhow::Result<Self> {
86 use std::fs::File;
87 use std::io::{BufReader, Read};
88
89 let file = File::open(path)?;
90 let mut reader = BufReader::new(file);
91
92 let mut magic = [0u8; 2];
94 reader.read_exact(&mut magic)?;
95
96 drop(reader);
98 let file = File::open(path)?;
99 let reader = BufReader::new(file);
100
101 if magic == [0x1f, 0x8b] {
102 let decoder = flate2::read::GzDecoder::new(reader);
104 let snapshot: Snapshot = serde_json::from_reader(decoder)?;
105 Ok(snapshot)
106 } else {
107 let snapshot: Snapshot = serde_json::from_reader(reader)?;
109 Ok(snapshot)
110 }
111 }
112
113 pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
115 serde_json::to_string_pretty(self)
116 }
117
118 pub fn get_table(&self, name: &str) -> Option<&Vec<Value>> {
120 self.tables.get(name)
121 }
122
123 pub fn is_schema_compatible(&self) -> bool {
125 self.schema_version == CURRENT_SCHEMA_VERSION
126 }
127
128 pub fn table_names(&self) -> Vec<&str> {
130 self.tables.keys().map(|s| s.as_str()).collect()
131 }
132}
133
134impl Default for Snapshot {
135 fn default() -> Self {
136 Self::new()
137 }
138}
139
140pub fn get_table_ordering(table: &str) -> &'static str {
143 match table {
144 "tasks" => "ORDER BY id",
145 "dependencies" => "ORDER BY from_task_id, to_task_id, dep_type",
146 "attachments" => "ORDER BY task_id, attachment_type, sequence",
147 "task_tags" => "ORDER BY task_id, tag",
148 "task_needed_tags" => "ORDER BY task_id, tag",
149 "task_wanted_tags" => "ORDER BY task_id, tag",
150 "task_sequence" => "ORDER BY task_id, id",
151 _ => "ORDER BY rowid",
152 }
153}
154
155pub fn get_table_primary_key(table: &str) -> &'static [&'static str] {
158 match table {
159 "tasks" => &["id"],
160 "dependencies" => &["from_task_id", "to_task_id", "dep_type"],
161 "attachments" => &["task_id", "attachment_type", "sequence"],
162 "task_tags" => &["task_id", "tag"],
163 "task_needed_tags" => &["task_id", "tag"],
164 "task_wanted_tags" => &["task_id", "tag"],
165 "task_sequence" => &["id"],
166 _ => &["rowid"],
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173
174 #[test]
175 fn test_snapshot_new() {
176 let snapshot = Snapshot::new();
177 assert_eq!(snapshot.schema_version, CURRENT_SCHEMA_VERSION);
178 assert_eq!(snapshot.export_version, EXPORT_VERSION);
179 assert!(snapshot.tables.is_empty());
180 }
181
182 #[test]
183 fn test_snapshot_json_roundtrip() {
184 let mut snapshot = Snapshot::new();
185 snapshot.tables.insert(
186 "tasks".to_string(),
187 vec![serde_json::json!({
188 "id": "test-1",
189 "title": "Test Task"
190 })],
191 );
192
193 let json = snapshot.to_json_pretty().unwrap();
194 let loaded = Snapshot::from_json(&json).unwrap();
195
196 assert_eq!(loaded.schema_version, snapshot.schema_version);
197 assert_eq!(loaded.tables.len(), 1);
198 }
199
200 #[test]
201 fn test_table_ordering() {
202 assert_eq!(get_table_ordering("tasks"), "ORDER BY id");
203 assert_eq!(
204 get_table_ordering("dependencies"),
205 "ORDER BY from_task_id, to_task_id, dep_type"
206 );
207 }
208
209 #[test]
210 fn test_table_primary_key() {
211 assert_eq!(get_table_primary_key("tasks"), &["id"]);
212 assert_eq!(
213 get_table_primary_key("dependencies"),
214 &["from_task_id", "to_task_id", "dep_type"]
215 );
216 assert_eq!(
217 get_table_primary_key("attachments"),
218 &["task_id", "attachment_type", "sequence"]
219 );
220 }
221}