cobolx 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,406 @@
1
+ use rusqlite::{Connection, OpenFlags};
2
+ use std::error::Error;
3
+ use std::path::{Path, PathBuf};
4
+
5
+ type StoreResult<T> = Result<T, Box<dyn Error + Send + Sync>>;
6
+
7
+ #[allow(dead_code)]
8
+ pub struct MemoryPaths {
9
+ pub root: PathBuf,
10
+ pub base_dir: PathBuf,
11
+ pub db_path: PathBuf,
12
+ pub blobs_dir: PathBuf,
13
+ pub docs_dir: PathBuf,
14
+ pub runs_dir: PathBuf,
15
+ pub skills_dir: PathBuf,
16
+ }
17
+
18
+ #[allow(dead_code)]
19
+ pub struct MemoryStore {
20
+ conn: Connection,
21
+ paths: MemoryPaths,
22
+ }
23
+
24
+ impl MemoryPaths {
25
+ pub fn for_root(root: impl Into<PathBuf>) -> Self {
26
+ let root = root.into();
27
+ let base_dir = root.join(".cobolx");
28
+ let docs_dir = root.join("docs");
29
+ let memory_dir = base_dir.join("memory");
30
+
31
+ Self {
32
+ root,
33
+ base_dir: base_dir.clone(),
34
+ db_path: memory_dir.join("project.db"),
35
+ blobs_dir: base_dir.join("blobs"),
36
+ docs_dir,
37
+ runs_dir: base_dir.join("runs"),
38
+ skills_dir: base_dir.join("skills"),
39
+ }
40
+ }
41
+ }
42
+
43
+ #[allow(dead_code)]
44
+ impl MemoryStore {
45
+ pub fn open_or_create(root: impl Into<PathBuf>) -> StoreResult<Self> {
46
+ let paths = MemoryPaths::for_root(root);
47
+ create_dirs(&paths)?;
48
+
49
+ let conn = Connection::open_with_flags(
50
+ &paths.db_path,
51
+ OpenFlags::SQLITE_OPEN_READ_WRITE
52
+ | OpenFlags::SQLITE_OPEN_CREATE
53
+ | OpenFlags::SQLITE_OPEN_NO_MUTEX,
54
+ )?;
55
+
56
+ configure_connection(&conn)?;
57
+ migrate_schema(&conn)?;
58
+
59
+ Ok(Self { conn, paths })
60
+ }
61
+
62
+ pub fn project_root(&self) -> &Path {
63
+ &self.paths.root
64
+ }
65
+
66
+ pub fn db_path(&self) -> &Path {
67
+ &self.paths.db_path
68
+ }
69
+
70
+ pub fn docs_dir(&self) -> &Path {
71
+ &self.paths.docs_dir
72
+ }
73
+
74
+ pub fn skills_dir(&self) -> &Path {
75
+ &self.paths.skills_dir
76
+ }
77
+
78
+ pub fn connection(&self) -> &Connection {
79
+ &self.conn
80
+ }
81
+
82
+ pub fn connection_mut(&mut self) -> &mut Connection {
83
+ &mut self.conn
84
+ }
85
+
86
+ pub fn query_readonly(&self, sql: &str) -> StoreResult<serde_json::Value> {
87
+ let trimmed = sql.trim();
88
+ if !trimmed.to_ascii_uppercase().starts_with("SELECT") {
89
+ return Err("Only SELECT queries are allowed for security reasons".into());
90
+ }
91
+
92
+ let mut stmt = self.conn.prepare(trimmed)?;
93
+ let col_count = stmt.column_count();
94
+ let col_names: Vec<String> = stmt
95
+ .column_names()
96
+ .into_iter()
97
+ .map(|s| s.to_string())
98
+ .collect();
99
+
100
+ let mut rows = stmt.query([])?;
101
+ let mut result_rows = Vec::new();
102
+
103
+ while let Some(row) = rows.next()? {
104
+ let mut map = serde_json::Map::new();
105
+ for i in 0..col_count {
106
+ let col_name = &col_names[i];
107
+ let value_ref = row.get_ref(i)?;
108
+ let value = match value_ref {
109
+ rusqlite::types::ValueRef::Null => serde_json::Value::Null,
110
+ rusqlite::types::ValueRef::Integer(v) => {
111
+ serde_json::Value::Number(serde_json::Number::from(v))
112
+ }
113
+ rusqlite::types::ValueRef::Real(v) => {
114
+ if let Some(num) = serde_json::Number::from_f64(v) {
115
+ serde_json::Value::Number(num)
116
+ } else {
117
+ serde_json::Value::Null
118
+ }
119
+ }
120
+ rusqlite::types::ValueRef::Text(v) => {
121
+ let s = std::str::from_utf8(v).unwrap_or("");
122
+ serde_json::Value::String(s.to_string())
123
+ }
124
+ rusqlite::types::ValueRef::Blob(v) => {
125
+ let hex_str = v.iter().map(|b| format!("{:02x}", b)).collect::<String>();
126
+ serde_json::Value::String(hex_str)
127
+ }
128
+ };
129
+ map.insert(col_name.clone(), value);
130
+ }
131
+ result_rows.push(serde_json::Value::Object(map));
132
+ }
133
+
134
+ Ok(serde_json::Value::Array(result_rows))
135
+ }
136
+ }
137
+
138
+ fn create_dirs(paths: &MemoryPaths) -> StoreResult<()> {
139
+ std::fs::create_dir_all(paths.db_path.parent().unwrap())?;
140
+ std::fs::create_dir_all(&paths.blobs_dir)?;
141
+ std::fs::create_dir_all(&paths.docs_dir)?;
142
+ std::fs::create_dir_all(&paths.runs_dir)?;
143
+ std::fs::create_dir_all(&paths.skills_dir)?;
144
+ Ok(())
145
+ }
146
+
147
+ fn configure_connection(conn: &Connection) -> rusqlite::Result<()> {
148
+ conn.pragma_update(None, "journal_mode", "WAL")?;
149
+ conn.pragma_update(None, "synchronous", "NORMAL")?;
150
+ conn.pragma_update(None, "foreign_keys", "ON")?;
151
+ conn.pragma_update(None, "busy_timeout", 5000_i64)?;
152
+ Ok(())
153
+ }
154
+
155
+ fn migrate_schema(conn: &Connection) -> rusqlite::Result<()> {
156
+ conn.execute_batch(
157
+ r#"
158
+ CREATE TABLE IF NOT EXISTS schema_migrations (
159
+ version INTEGER PRIMARY KEY,
160
+ applied_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
161
+ );
162
+
163
+ CREATE TABLE IF NOT EXISTS files (
164
+ id INTEGER PRIMARY KEY,
165
+ path TEXT NOT NULL UNIQUE,
166
+ kind TEXT NOT NULL,
167
+ size_bytes INTEGER NOT NULL,
168
+ mtime_unix INTEGER NOT NULL,
169
+ sha256 BLOB,
170
+ indexed_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
171
+ );
172
+
173
+ CREATE TABLE IF NOT EXISTS programs (
174
+ id INTEGER PRIMARY KEY,
175
+ name TEXT NOT NULL,
176
+ file_id INTEGER NOT NULL,
177
+ start_offset INTEGER NOT NULL,
178
+ byte_len INTEGER NOT NULL,
179
+ FOREIGN KEY(file_id) REFERENCES files(id) ON DELETE CASCADE
180
+ );
181
+
182
+ CREATE INDEX IF NOT EXISTS idx_programs_name ON programs(name);
183
+ CREATE INDEX IF NOT EXISTS idx_programs_file ON programs(file_id);
184
+
185
+ CREATE TABLE IF NOT EXISTS copybook_uses (
186
+ id INTEGER PRIMARY KEY,
187
+ from_file_id INTEGER NOT NULL,
188
+ copybook_name TEXT NOT NULL,
189
+ start_offset INTEGER NOT NULL,
190
+ byte_len INTEGER NOT NULL,
191
+ resolved_file_id INTEGER,
192
+ resolve_status TEXT NOT NULL DEFAULT 'unknown',
193
+ replacing_text TEXT,
194
+ FOREIGN KEY(from_file_id) REFERENCES files(id) ON DELETE CASCADE,
195
+ FOREIGN KEY(resolved_file_id) REFERENCES files(id) ON DELETE SET NULL
196
+ );
197
+
198
+ CREATE INDEX IF NOT EXISTS idx_copybook_uses_from_file ON copybook_uses(from_file_id);
199
+ CREATE INDEX IF NOT EXISTS idx_copybook_uses_name ON copybook_uses(copybook_name);
200
+
201
+ CREATE TABLE IF NOT EXISTS call_edges (
202
+ id INTEGER PRIMARY KEY,
203
+ caller_program_id INTEGER NOT NULL,
204
+ callee_name TEXT NOT NULL,
205
+ start_offset INTEGER NOT NULL,
206
+ byte_len INTEGER NOT NULL,
207
+ kind TEXT NOT NULL DEFAULT 'static',
208
+ using_count INTEGER NOT NULL DEFAULT 0,
209
+ FOREIGN KEY(caller_program_id) REFERENCES programs(id) ON DELETE CASCADE
210
+ );
211
+
212
+ CREATE INDEX IF NOT EXISTS idx_call_edges_caller ON call_edges(caller_program_id);
213
+ CREATE INDEX IF NOT EXISTS idx_call_edges_callee ON call_edges(callee_name);
214
+
215
+ CREATE TABLE IF NOT EXISTS data_items (
216
+ id INTEGER PRIMARY KEY,
217
+ program_id INTEGER NOT NULL,
218
+ source_file_id INTEGER,
219
+ name TEXT NOT NULL,
220
+ level INTEGER NOT NULL,
221
+ parent_name TEXT,
222
+ pic TEXT,
223
+ usage_clause TEXT,
224
+ occurs INTEGER,
225
+ redefines TEXT,
226
+ section TEXT,
227
+ byte_offset INTEGER,
228
+ byte_size INTEGER,
229
+ storage_kind TEXT,
230
+ layout_status TEXT,
231
+ start_offset INTEGER NOT NULL,
232
+ byte_len INTEGER NOT NULL,
233
+ FOREIGN KEY(program_id) REFERENCES programs(id) ON DELETE CASCADE,
234
+ FOREIGN KEY(source_file_id) REFERENCES files(id) ON DELETE SET NULL
235
+ );
236
+
237
+ CREATE INDEX IF NOT EXISTS idx_data_items_program ON data_items(program_id);
238
+ CREATE INDEX IF NOT EXISTS idx_data_items_source_file ON data_items(source_file_id);
239
+ CREATE INDEX IF NOT EXISTS idx_data_items_name ON data_items(name);
240
+
241
+ CREATE TABLE IF NOT EXISTS runs (
242
+ id TEXT PRIMARY KEY,
243
+ started_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
244
+ status TEXT NOT NULL
245
+ );
246
+
247
+ CREATE TABLE IF NOT EXISTS run_events (
248
+ id INTEGER PRIMARY KEY,
249
+ run_id TEXT NOT NULL,
250
+ seq INTEGER NOT NULL,
251
+ kind TEXT NOT NULL,
252
+ payload_json TEXT NOT NULL,
253
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
254
+ FOREIGN KEY(run_id) REFERENCES runs(id) ON DELETE CASCADE,
255
+ UNIQUE(run_id, seq)
256
+ );
257
+
258
+ CREATE TABLE IF NOT EXISTS skills (
259
+ id INTEGER PRIMARY KEY,
260
+ name TEXT NOT NULL,
261
+ version TEXT NOT NULL,
262
+ path TEXT NOT NULL UNIQUE,
263
+ sha256 BLOB NOT NULL,
264
+ tags_json TEXT NOT NULL
265
+ );
266
+
267
+ INSERT OR IGNORE INTO schema_migrations(version) VALUES (1);
268
+ "#,
269
+ )?;
270
+
271
+ ensure_column(
272
+ conn,
273
+ "copybook_uses",
274
+ "resolve_status",
275
+ "ALTER TABLE copybook_uses ADD COLUMN resolve_status TEXT NOT NULL DEFAULT 'unknown'",
276
+ )?;
277
+ ensure_column(
278
+ conn,
279
+ "copybook_uses",
280
+ "replacing_text",
281
+ "ALTER TABLE copybook_uses ADD COLUMN replacing_text TEXT",
282
+ )?;
283
+ ensure_column(
284
+ conn,
285
+ "call_edges",
286
+ "kind",
287
+ "ALTER TABLE call_edges ADD COLUMN kind TEXT NOT NULL DEFAULT 'static'",
288
+ )?;
289
+ ensure_column(
290
+ conn,
291
+ "call_edges",
292
+ "using_count",
293
+ "ALTER TABLE call_edges ADD COLUMN using_count INTEGER NOT NULL DEFAULT 0",
294
+ )?;
295
+ ensure_column(
296
+ conn,
297
+ "data_items",
298
+ "source_file_id",
299
+ "ALTER TABLE data_items ADD COLUMN source_file_id INTEGER",
300
+ )?;
301
+ ensure_column(
302
+ conn,
303
+ "data_items",
304
+ "parent_name",
305
+ "ALTER TABLE data_items ADD COLUMN parent_name TEXT",
306
+ )?;
307
+ ensure_column(
308
+ conn,
309
+ "data_items",
310
+ "occurs",
311
+ "ALTER TABLE data_items ADD COLUMN occurs INTEGER",
312
+ )?;
313
+ ensure_column(
314
+ conn,
315
+ "data_items",
316
+ "redefines",
317
+ "ALTER TABLE data_items ADD COLUMN redefines TEXT",
318
+ )?;
319
+ ensure_column(
320
+ conn,
321
+ "data_items",
322
+ "section",
323
+ "ALTER TABLE data_items ADD COLUMN section TEXT",
324
+ )?;
325
+ ensure_column(
326
+ conn,
327
+ "data_items",
328
+ "byte_offset",
329
+ "ALTER TABLE data_items ADD COLUMN byte_offset INTEGER",
330
+ )?;
331
+ ensure_column(
332
+ conn,
333
+ "data_items",
334
+ "byte_size",
335
+ "ALTER TABLE data_items ADD COLUMN byte_size INTEGER",
336
+ )?;
337
+ ensure_column(
338
+ conn,
339
+ "data_items",
340
+ "storage_kind",
341
+ "ALTER TABLE data_items ADD COLUMN storage_kind TEXT",
342
+ )?;
343
+ ensure_column(
344
+ conn,
345
+ "data_items",
346
+ "layout_status",
347
+ "ALTER TABLE data_items ADD COLUMN layout_status TEXT",
348
+ )?;
349
+
350
+ Ok(())
351
+ }
352
+
353
+ fn ensure_column(
354
+ conn: &Connection,
355
+ table: &str,
356
+ column: &str,
357
+ alter_sql: &str,
358
+ ) -> rusqlite::Result<()> {
359
+ let mut stmt = conn.prepare(&format!("PRAGMA table_info({})", table))?;
360
+ let mut rows = stmt.query([])?;
361
+
362
+ while let Some(row) = rows.next()? {
363
+ let name: String = row.get(1)?;
364
+ if name == column {
365
+ return Ok(());
366
+ }
367
+ }
368
+
369
+ conn.execute_batch(alter_sql)
370
+ }
371
+
372
+ #[cfg(test)]
373
+ mod tests {
374
+ use super::*;
375
+
376
+ #[test]
377
+ fn test_query_readonly_safety() {
378
+ let temp_dir = tempfile::tempdir().unwrap();
379
+ let store = MemoryStore::open_or_create(temp_dir.path()).unwrap();
380
+
381
+ // 1. Valid read-only query
382
+ let res = store.query_readonly("SELECT * FROM files");
383
+ assert!(res.is_ok());
384
+ let val = res.unwrap();
385
+ assert!(val.is_array());
386
+
387
+ // 2. Reject modifying queries
388
+ let res_insert = store.query_readonly("INSERT INTO files (path, kind, size_bytes, mtime_unix) VALUES ('test.cob', 'source', 10, 0)");
389
+ assert!(res_insert.is_err());
390
+ assert!(
391
+ res_insert
392
+ .unwrap_err()
393
+ .to_string()
394
+ .contains("Only SELECT queries are allowed")
395
+ );
396
+
397
+ let res_drop = store.query_readonly("DROP TABLE files");
398
+ assert!(res_drop.is_err());
399
+ assert!(
400
+ res_drop
401
+ .unwrap_err()
402
+ .to_string()
403
+ .contains("Only SELECT queries are allowed")
404
+ );
405
+ }
406
+ }
package/src/memory.rs ADDED
@@ -0,0 +1,5 @@
1
+ pub mod files;
2
+ pub mod store;
3
+
4
+ #[allow(unused_imports)]
5
+ pub use store::{MemoryPaths, MemoryStore};