Skip to content

Commit 880c7d9

Browse files
committed
fix: use JsonValue wrapper for bincode + serde_json::Value compatibility
bincode's deserialize_any is not supported by serde_json::Value, causing deserialization errors when thoughts have metadata attributes. This fix: - Adds JsonValue wrapper type that serializes JSON values as strings - Updates Thought.attrs to use HashMap<String, JsonValue> - Adds Thought.get_attr() helper method - Fixes all usages in database.rs and main.rs - Bumps version to 0.1.9 Fixes test_search_with_embedder and other tests that use embedder attrs.
1 parent a32d190 commit 880c7d9

6 files changed

Lines changed: 70 additions & 13 deletions

File tree

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "indra_db"
3-
version = "0.1.8"
3+
version = "0.1.9"
44
edition = "2021"
55
description = "A content-addressed graph database for versioned thoughts"
66
license = "MIT"

src/database.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
55
use crate::embedding::Embedder;
66
use crate::graph::GraphView;
7-
use crate::model::{Commit, Edge, EdgeType, Hash, Thought, ThoughtId};
7+
use crate::model::{Commit, Edge, EdgeType, Hash, JsonValue, Thought, ThoughtId};
88
use crate::ops::{diff_trees, BranchManager, Diff};
99
use crate::search::{SearchResult, VectorSearch};
1010
use crate::store::ObjectStore;
@@ -114,7 +114,7 @@ impl Database {
114114
thought.embedding = Some(embedder.embed(&thought.content)?);
115115
thought.attrs.insert(
116116
"embedder_model".to_string(),
117-
serde_json::Value::String(embedder.model_name().to_string()),
117+
JsonValue::new(serde_json::Value::String(embedder.model_name().to_string())),
118118
);
119119
}
120120

@@ -139,7 +139,7 @@ impl Database {
139139
thought.embedding = Some(embedder.embed(&thought.content)?);
140140
thought.attrs.insert(
141141
"embedder_model".to_string(),
142-
serde_json::Value::String(embedder.model_name().to_string()),
142+
JsonValue::new(serde_json::Value::String(embedder.model_name().to_string())),
143143
);
144144
}
145145

@@ -185,7 +185,7 @@ impl Database {
185185
thought.embedding = Some(embedder.embed(&thought.content)?);
186186
thought.attrs.insert(
187187
"embedder_model".to_string(),
188-
serde_json::Value::String(embedder.model_name().to_string()),
188+
JsonValue::new(serde_json::Value::String(embedder.model_name().to_string())),
189189
);
190190
}
191191

src/main.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,9 @@ fn main() -> anyhow::Result<()> {
336336
let thought_id = indra_db::ThoughtId::new(&id);
337337
match db.get_thought(&thought_id)? {
338338
Some(thought) => {
339+
// Convert JsonValue attrs to serde_json::Value for output
340+
let attrs: std::collections::HashMap<String, serde_json::Value> =
341+
thought.attrs.into_iter().map(|(k, v)| (k, v.0)).collect();
339342
output(
340343
&cli.format,
341344
&serde_json::json!({
@@ -344,7 +347,7 @@ fn main() -> anyhow::Result<()> {
344347
"type": thought.thought_type,
345348
"created_at": thought.created_at,
346349
"modified_at": thought.modified_at,
347-
"attrs": thought.attrs,
350+
"attrs": attrs,
348351
"has_embedding": thought.embedding.is_some()
349352
}),
350353
);

src/model/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@ mod thought;
88
pub use commit::Commit;
99
pub use edge::{Edge, EdgeType};
1010
pub use hash::Hash;
11-
pub use thought::{Thought, ThoughtId};
11+
pub use thought::{JsonValue, Thought, ThoughtId};

src/model/thought.rs

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,59 @@
11
//! Thought (node) type - the fundamental unit of knowledge
22
33
use super::Hash;
4-
use serde::{Deserialize, Serialize};
4+
use serde::{Deserialize, Deserializer, Serialize, Serializer};
55
use std::collections::HashMap;
66
use std::time::{SystemTime, UNIX_EPOCH};
77

8+
/// Wrapper type for storing JSON values that is compatible with bincode.
9+
///
10+
/// bincode doesn't support `serde_json::Value` because it uses `deserialize_any`,
11+
/// so we serialize JSON values to strings for storage.
12+
#[derive(Clone, Debug, PartialEq)]
13+
pub struct JsonValue(pub serde_json::Value);
14+
15+
impl JsonValue {
16+
pub fn new(value: serde_json::Value) -> Self {
17+
JsonValue(value)
18+
}
19+
}
20+
21+
impl From<serde_json::Value> for JsonValue {
22+
fn from(v: serde_json::Value) -> Self {
23+
JsonValue(v)
24+
}
25+
}
26+
27+
impl From<JsonValue> for serde_json::Value {
28+
fn from(v: JsonValue) -> Self {
29+
v.0
30+
}
31+
}
32+
33+
impl Serialize for JsonValue {
34+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
35+
where
36+
S: Serializer,
37+
{
38+
// Serialize the JSON value as a string
39+
let json_string = serde_json::to_string(&self.0).map_err(serde::ser::Error::custom)?;
40+
serializer.serialize_str(&json_string)
41+
}
42+
}
43+
44+
impl<'de> Deserialize<'de> for JsonValue {
45+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
46+
where
47+
D: Deserializer<'de>,
48+
{
49+
// Deserialize as string, then parse as JSON
50+
let json_string = String::deserialize(deserializer)?;
51+
let value: serde_json::Value =
52+
serde_json::from_str(&json_string).map_err(serde::de::Error::custom)?;
53+
Ok(JsonValue(value))
54+
}
55+
}
56+
857
/// Unique identifier for a thought (semantic ID, not content hash)
958
/// This allows thoughts to evolve while maintaining identity
1059
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
@@ -82,8 +131,8 @@ pub struct Thought {
82131
/// Dimension is configurable at database level
83132
pub embedding: Option<Vec<f32>>,
84133

85-
/// Arbitrary metadata
86-
pub attrs: HashMap<String, serde_json::Value>,
134+
/// Arbitrary metadata (stored as JSON strings for bincode compatibility)
135+
pub attrs: HashMap<String, JsonValue>,
87136

88137
/// Creation timestamp (unix millis)
89138
pub created_at: u64,
@@ -147,10 +196,15 @@ impl Thought {
147196
key: impl Into<String>,
148197
value: impl Into<serde_json::Value>,
149198
) -> Self {
150-
self.attrs.insert(key.into(), value.into());
199+
self.attrs.insert(key.into(), JsonValue::new(value.into()));
151200
self
152201
}
153202

203+
/// Get a metadata attribute
204+
pub fn get_attr(&self, key: &str) -> Option<&serde_json::Value> {
205+
self.attrs.get(key).map(|v| &v.0)
206+
}
207+
154208
/// Compute the content hash of this thought
155209
/// This determines the blob's address in content-addressed storage
156210
pub fn content_hash(&self) -> Hash {
@@ -209,7 +263,7 @@ mod tests {
209263

210264
assert_eq!(thought.thought_type, Some("hypothesis".to_string()));
211265
assert_eq!(
212-
thought.attrs.get("confidence"),
266+
thought.get_attr("confidence"),
213267
Some(&serde_json::json!(0.8))
214268
);
215269
}

0 commit comments

Comments
 (0)