1use crate::error::Result;
2
3use super::StateStore;
4
5#[derive(Debug, serde::Serialize)]
7#[allow(dead_code)]
8pub struct FileRecord {
9 pub run_id: String,
10 pub export_name: String,
11 pub file_name: String,
12 pub row_count: i64,
13 pub bytes: i64,
14 pub format: String,
15 pub compression: Option<String>,
16 pub created_at: String,
17}
18
19impl StateStore {
29 #[allow(clippy::too_many_arguments)]
30 pub fn record_file(
31 &self,
32 run_id: &str,
33 export_name: &str,
34 file_name: &str,
35 row_count: i64,
36 bytes: i64,
37 format: &str,
38 compression: Option<&str>,
39 ) -> Result<()> {
40 self.execute(
41 "INSERT INTO file_log (run_id, export_name, file_name, row_count, bytes, format, compression, created_at) \
42 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
43 &[
44 run_id.into(),
45 export_name.into(),
46 file_name.into(),
47 row_count.into(),
48 bytes.into(),
49 format.into(),
50 compression.into(),
51 chrono::Utc::now().to_rfc3339().into(),
52 ],
53 )?;
54 Ok(())
55 }
56
57 pub fn get_files(&self, export_name: Option<&str>, limit: usize) -> Result<Vec<FileRecord>> {
58 let cols =
59 "run_id, export_name, file_name, row_count, bytes, format, compression, created_at";
60 match export_name {
61 Some(name) => self.query(
62 &format!(
63 "SELECT {cols} FROM file_log WHERE export_name = ?1 ORDER BY id DESC LIMIT ?2"
64 ),
65 &[name.into(), (limit as i64).into()],
66 file_record,
67 ),
68 None => self.query(
69 &format!("SELECT {cols} FROM file_log ORDER BY id DESC LIMIT ?1"),
70 &[(limit as i64).into()],
71 file_record,
72 ),
73 }
74 }
75
76 pub fn list_files_for_run(&self, run_id: &str) -> Result<Vec<FileRecord>> {
83 let cols =
84 "run_id, export_name, file_name, row_count, bytes, format, compression, created_at";
85 self.query(
86 &format!("SELECT {cols} FROM file_log WHERE run_id = ?1 ORDER BY id ASC"),
87 &[run_id.into()],
88 file_record,
89 )
90 }
91}
92
93fn file_record(r: &dyn super::row::StateRow) -> FileRecord {
95 FileRecord {
96 run_id: r.text(0),
97 export_name: r.text(1),
98 file_name: r.text(2),
99 row_count: r.i64(3),
100 bytes: r.i64(4),
101 format: r.text(5),
102 compression: r.opt_text(6),
103 created_at: r.text(7),
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 fn store() -> StateStore {
112 StateStore::open_in_memory().expect("in-memory store")
113 }
114
115 #[test]
116 fn record_and_query_files() {
117 let s = store();
118 s.record_file(
119 "run_001",
120 "orders",
121 "orders_20260329.parquet",
122 50000,
123 4096,
124 "parquet",
125 Some("zstd"),
126 )
127 .unwrap();
128 s.record_file(
129 "run_001",
130 "orders",
131 "orders_20260329_chunk1.parquet",
132 25000,
133 2048,
134 "parquet",
135 Some("zstd"),
136 )
137 .unwrap();
138 s.record_file(
139 "run_002",
140 "users",
141 "users_20260329.csv",
142 1000,
143 500,
144 "csv",
145 None,
146 )
147 .unwrap();
148
149 let files = s.get_files(Some("orders"), 10).unwrap();
150 assert_eq!(files.len(), 2);
151 assert_eq!(files[0].run_id, "run_001");
152 assert_eq!(files[0].row_count, 25000);
153
154 let all = s.get_files(None, 10).unwrap();
155 assert_eq!(all.len(), 3);
156 }
157
158 #[test]
159 fn files_limit_works() {
160 let s = store();
161 for i in 0..10 {
162 s.record_file(
163 &format!("r{}", i),
164 "t",
165 &format!("f{}.parquet", i),
166 i,
167 i * 100,
168 "parquet",
169 None,
170 )
171 .unwrap();
172 }
173 let files = s.get_files(Some("t"), 3).unwrap();
174 assert_eq!(files.len(), 3);
175 }
176}