Skip to content

Commit 7b62eff

Browse files
committed
feat: Implement logging for database operations
1 parent e74c805 commit 7b62eff

7 files changed

Lines changed: 225 additions & 10 deletions

File tree

Cargo.lock

Lines changed: 57 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,7 @@ sqlparser = "0.60.0"
99
serde = { version = "1.0", features = ["derive"] }
1010
serde_json = "1.0"
1111
uuid = { version = "1.8.0", features = ["v7", "serde"] }
12+
chrono = { version = "0.4.38", features = ["serde"] }
13+
14+
[dev-dependencies]
15+
tempfile = "3.10.1"

ROADMAP.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ This stage will implement a simple in-memory version of ArgusDB that will serve
1818

1919
This stage will implement logging of all operations in ArgusDB to allow for fault tolerance.
2020

21-
- [] Define a log file format in specs/logging.md
22-
- [] Add log entries whenever new documents are inserted
21+
- [x] Define a log file format in specs/logging.md
22+
- [x] Add log entries whenever new documents are inserted
2323
- [] Implement log recovery to read the log on restart and recover from a crash
2424
- [] Implement log rotation and deletion of old log files
2525

specs/logging.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,48 @@
11
# Log file format
22

3+
The log file is a sequence of JSON objects, one per line. Each object represents a single operation that modifies the database state.
4+
5+
## Log entry format
6+
7+
Each log entry is a JSON object with the following fields:
8+
9+
- `ts`: An ISO 8601 timestamp of when the operation occurred.
10+
- `op`: The type of operation. Can be one of "insert", "update", or "delete".
11+
- `doc`: The document for "insert" and "update" operations.
12+
- `id`: The document ID for "update" and "delete" operations.
13+
14+
### Insert operation
15+
16+
```json
17+
{
18+
"ts": "2026-01-10T12:00:00.000Z",
19+
"op": "insert",
20+
"doc": { "a": 1 }
21+
}
22+
```
23+
24+
### Update operation
25+
26+
```json
27+
{
28+
"ts": "2026-01-10T12:00:01.000Z",
29+
"op": "update",
30+
"id": "01H4J3J4J3J4J3J4J3J4J3J4J3",
31+
"doc": { "b": "hello" }
32+
}
33+
```
34+
35+
### Delete operation
36+
37+
```json
38+
{
39+
"ts": "2026-01-10T12:00:02.000Z",
40+
"op": "delete",
41+
"id": "01H4J3J4J3J4J3J4J3J4J3J4J3"
42+
}
43+
```
44+
345
# Log rotation
46+
47+
When the log file reaches a certain size, a new log file is created. The old log file can be archived or deleted.
48+
The exact mechanism for rotation and archiving is to be determined.

src/log.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
use chrono::{DateTime, Utc};
2+
use serde::{Deserialize, Serialize};
3+
use serde_json::Value;
4+
use std::fs::OpenOptions;
5+
use std::io::Write;
6+
use std::path::Path;
7+
8+
#[derive(Serialize, Deserialize, Debug)]
9+
pub enum Operation {
10+
Insert {
11+
doc: Value,
12+
},
13+
Update {
14+
id: String,
15+
doc: Value,
16+
},
17+
Delete {
18+
id: String,
19+
},
20+
}
21+
22+
#[derive(Serialize, Deserialize, Debug)]
23+
pub struct LogEntry {
24+
pub ts: DateTime<Utc>,
25+
pub op: Operation,
26+
}
27+
28+
pub struct Logger {
29+
file: std::fs::File,
30+
}
31+
32+
impl Logger {
33+
pub fn new<P: AsRef<Path>>(path: P) -> std::io::Result<Self> {
34+
let file = OpenOptions::new()
35+
.create(true)
36+
.write(true)
37+
.append(true)
38+
.open(path)?;
39+
Ok(Logger { file })
40+
}
41+
42+
pub fn log(&mut self, op: Operation) -> std::io::Result<()> {
43+
let entry = LogEntry {
44+
ts: Utc::now(),
45+
op,
46+
};
47+
let json = serde_json::to_string(&entry)?;
48+
writeln!(self.file, "{}", json)
49+
}
50+
}

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
pub mod schema;
22
pub mod storage;
3+
pub mod log;
34

45
fn main() {
56
println!("Hello, world!");

src/storage.rs

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::log::{Logger, Operation};
12
use crate::schema::{infer_schema, Schema};
23
use serde_json::Value;
34
use std::collections::BTreeMap;
@@ -6,36 +7,51 @@ use uuid::Uuid;
67
pub struct MemTable {
78
documents: BTreeMap<String, Value>,
89
schema: Schema,
10+
logger: Logger,
911
}
1012

1113
impl MemTable {
12-
pub fn new() -> Self {
14+
pub fn new(log_path: &str) -> Self {
15+
let logger = Logger::new(log_path).unwrap();
1316
MemTable {
1417
documents: BTreeMap::new(),
1518
schema: Schema {
1619
types: vec![],
1720
properties: None,
1821
items: None,
1922
},
23+
logger,
2024
}
2125
}
2226

2327
pub fn insert(&mut self, doc: Value) -> String {
2428
let doc_schema = infer_schema(&doc);
25-
self.schema.merge(doc_schema);
29+
self.schema.merge(doc_schema.clone());
2630
let id = Uuid::now_v7().to_string();
27-
self.documents.insert(id.clone(), doc);
31+
self.documents.insert(id.clone(), doc.clone());
32+
self.logger
33+
.log(Operation::Insert { doc })
34+
.expect("Failed to log insert");
2835
id
2936
}
3037

3138
pub fn delete(&mut self, id: &str) {
3239
self.documents.remove(id);
40+
self.logger
41+
.log(Operation::Delete { id: id.to_string() })
42+
.expect("Failed to log delete");
3343
}
3444

3545
pub fn update(&mut self, id: &str, doc: Value) {
3646
let doc_schema = infer_schema(&doc);
3747
self.schema.merge(doc_schema);
38-
self.documents.insert(id.to_string(), doc);
48+
self.documents.insert(id.to_string(), doc.clone());
49+
self.logger
50+
.log(Operation::Update {
51+
id: id.to_string(),
52+
doc,
53+
})
54+
.expect("Failed to log update");
3955
}
4056
}
4157

@@ -44,10 +60,17 @@ mod tests {
4460
use super::*;
4561
use crate::schema::SchemaType;
4662
use serde_json::json;
63+
use tempfile::NamedTempFile;
64+
65+
fn create_test_memtable() -> (NamedTempFile, MemTable) {
66+
let log_file = NamedTempFile::new().unwrap();
67+
let memtable = MemTable::new(log_file.path().to_str().unwrap());
68+
(log_file, memtable)
69+
}
4770

4871
#[test]
4972
fn test_memtable_insert() {
50-
let mut memtable = MemTable::new();
73+
let (_log_file, mut memtable) = create_test_memtable();
5174
memtable.insert(json!({"a": 1}));
5275
memtable.insert(json!({"b": "hello"}));
5376

@@ -62,7 +85,7 @@ mod tests {
6285

6386
#[test]
6487
fn test_memtable_delete() {
65-
let mut memtable = MemTable::new();
88+
let (_log_file, mut memtable) = create_test_memtable();
6689
let id = memtable.insert(json!({"a": 1}));
6790
assert_eq!(memtable.documents.len(), 1);
6891
memtable.delete(&id);
@@ -71,7 +94,7 @@ mod tests {
7194

7295
#[test]
7396
fn test_memtable_update() {
74-
let mut memtable = MemTable::new();
97+
let (_log_file, mut memtable) = create_test_memtable();
7598
let id = memtable.insert(json!({"a": 1}));
7699
memtable.update(&id, json!({"b": "hello"}));
77100

@@ -85,4 +108,40 @@ mod tests {
85108
assert_eq!(props.get("a").unwrap().types, vec![SchemaType::Integer]);
86109
assert_eq!(props.get("b").unwrap().types, vec![SchemaType::String]);
87110
}
111+
112+
#[test]
113+
fn test_log_content() {
114+
let (log_file, mut memtable) = create_test_memtable();
115+
let doc1 = json!({"a": 1});
116+
let id1 = memtable.insert(doc1.clone());
117+
118+
let doc2 = json!({"b": "hello"});
119+
memtable.update(&id1, doc2.clone());
120+
121+
memtable.delete(&id1);
122+
123+
let log_content = std::fs::read_to_string(log_file.path()).unwrap();
124+
let mut lines = log_content.lines();
125+
126+
let entry1: crate::log::LogEntry = serde_json::from_str(lines.next().unwrap()).unwrap();
127+
match entry1.op {
128+
Operation::Insert { doc } => assert_eq!(doc, doc1),
129+
_ => panic!("Expected insert operation"),
130+
}
131+
132+
let entry2: crate::log::LogEntry = serde_json::from_str(lines.next().unwrap()).unwrap();
133+
match entry2.op {
134+
Operation::Update { id, doc } => {
135+
assert_eq!(id, id1);
136+
assert_eq!(doc, doc2);
137+
}
138+
_ => panic!("Expected update operation"),
139+
}
140+
141+
let entry3: crate::log::LogEntry = serde_json::from_str(lines.next().unwrap()).unwrap();
142+
match entry3.op {
143+
Operation::Delete { id } => assert_eq!(id, id1),
144+
_ => panic!("Expected delete operation"),
145+
}
146+
}
88147
}

0 commit comments

Comments
 (0)