Skip to content

Commit 7c4a006

Browse files
committed
feat: Implement schema inference and merging
1 parent c30e557 commit 7c4a006

5 files changed

Lines changed: 217 additions & 2 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
target/

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@ edition = "2024"
66
[dependencies]
77
pgwire = "0.37.0"
88
sqlparser = "0.60.0"
9+
serde = { version = "1.0", features = ["derive"] }
10+
serde_json = "1.0"

ROADMAP.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
This stage will implement a basic version of JSON Schema discovery to be used in ArgusDB.
44

5-
- [] Infer a schema for an individual document
6-
- [] Merge schemas to allow inference of schemas for multiple documents
5+
- [x] Infer a schema for an individual document
6+
- [x] Merge schemas to allow inference of schemas for multiple documents
77

88
# Stage 2 - In-memory storage
99

src/schema.rs

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
use serde::{Deserialize, Serialize};
2+
use serde_json::Value;
3+
use std::collections::BTreeMap;
4+
5+
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
6+
#[serde(rename_all = "lowercase")]
7+
pub enum SchemaType {
8+
String,
9+
Integer,
10+
Number,
11+
Boolean,
12+
Null,
13+
Object,
14+
Array,
15+
}
16+
17+
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
18+
pub struct Schema {
19+
#[serde(rename = "type")]
20+
pub types: Vec<SchemaType>,
21+
#[serde(skip_serializing_if = "Option::is_none")]
22+
pub properties: Option<BTreeMap<String, Schema>>,
23+
#[serde(skip_serializing_if = "Option::is_none")]
24+
pub items: Option<Box<Schema>>,
25+
}
26+
27+
impl Schema {
28+
fn new(schema_type: SchemaType) -> Self {
29+
Schema {
30+
types: vec![schema_type],
31+
properties: None,
32+
items: None,
33+
}
34+
}
35+
36+
pub fn merge(&mut self, other: Self) {
37+
for t in other.types {
38+
if !self.types.contains(&t) {
39+
self.types.push(t);
40+
}
41+
}
42+
43+
if let Some(other_props) = other.properties {
44+
let self_props = self.properties.get_or_insert_with(BTreeMap::new);
45+
for (key, other_schema) in other_props {
46+
if let Some(self_schema) = self_props.get_mut(&key) {
47+
self_schema.merge(other_schema);
48+
} else {
49+
self_props.insert(key, other_schema);
50+
}
51+
}
52+
}
53+
54+
if let Some(other_items) = other.items {
55+
if let Some(self_items) = self.items.as_mut() {
56+
self_items.merge(*other_items);
57+
} else {
58+
self.items = Some(other_items);
59+
}
60+
}
61+
}
62+
}
63+
64+
pub fn infer_schema(doc: &Value) -> Schema {
65+
match doc {
66+
Value::Null => Schema::new(SchemaType::Null),
67+
Value::Bool(_) => Schema::new(SchemaType::Boolean),
68+
Value::Number(n) => {
69+
if n.is_i64() {
70+
Schema::new(SchemaType::Integer)
71+
} else {
72+
Schema::new(SchemaType::Number)
73+
}
74+
}
75+
Value::String(_) => Schema::new(SchemaType::String),
76+
Value::Array(arr) => {
77+
let mut items_schema = if let Some(first) = arr.get(0) {
78+
infer_schema(first)
79+
} else {
80+
// Empty array, we can't infer the type.
81+
// We could represent this as a special "unknown" type,
82+
// but for now we'll just make it an empty schema.
83+
Schema {
84+
types: vec![],
85+
properties: None,
86+
items: None,
87+
}
88+
};
89+
90+
for item in arr.iter().skip(1) {
91+
items_schema.merge(infer_schema(item));
92+
}
93+
94+
Schema {
95+
types: vec![SchemaType::Array],
96+
properties: None,
97+
items: Some(Box::new(items_schema)),
98+
}
99+
}
100+
Value::Object(obj) => {
101+
let mut properties = BTreeMap::new();
102+
for (key, value) in obj {
103+
properties.insert(key.clone(), infer_schema(value));
104+
}
105+
Schema {
106+
types: vec![SchemaType::Object],
107+
properties: Some(properties),
108+
items: None,
109+
}
110+
}
111+
}
112+
}
113+
114+
#[cfg(test)]
115+
mod tests {
116+
use super::*;
117+
use serde_json::json;
118+
119+
#[test]
120+
fn test_infer_simple_object() {
121+
let doc = json!({
122+
"a": 1,
123+
"b": "hello"
124+
});
125+
let schema = infer_schema(&doc);
126+
assert_eq!(schema.types, vec![SchemaType::Object]);
127+
let props = schema.properties.unwrap();
128+
assert_eq!(props.get("a").unwrap().types, vec![SchemaType::Integer]);
129+
assert_eq!(props.get("b").unwrap().types, vec![SchemaType::String]);
130+
}
131+
132+
#[test]
133+
fn test_infer_nested_object() {
134+
let doc = json!({
135+
"a": {
136+
"b": true
137+
}
138+
});
139+
let schema = infer_schema(&doc);
140+
assert_eq!(schema.types, vec![SchemaType::Object]);
141+
let props = schema.properties.unwrap();
142+
let a_schema = props.get("a").unwrap();
143+
assert_eq!(a_schema.types, vec![SchemaType::Object]);
144+
let a_props = a_schema.properties.as_ref().unwrap();
145+
assert_eq!(a_props.get("b").unwrap().types, vec![SchemaType::Boolean]);
146+
}
147+
148+
#[test]
149+
fn test_infer_array() {
150+
let doc = json!([1, 2, 3]);
151+
let schema = infer_schema(&doc);
152+
assert_eq!(schema.types, vec![SchemaType::Array]);
153+
let items = schema.items.unwrap();
154+
assert_eq!(items.types, vec![SchemaType::Integer]);
155+
}
156+
157+
#[test]
158+
fn test_infer_array_mixed_types() {
159+
let doc = json!([1, "hello"]);
160+
let schema = infer_schema(&doc);
161+
assert_eq!(schema.types, vec![SchemaType::Array]);
162+
let items = schema.items.unwrap();
163+
assert_eq!(items.types.len(), 2);
164+
assert!(items.types.contains(&SchemaType::Integer));
165+
assert!(items.types.contains(&SchemaType::String));
166+
}
167+
168+
#[test]
169+
fn test_merge_schemas() {
170+
let mut schema1 = infer_schema(&json!({"a": 1, "b": "hello"}));
171+
let schema2 = infer_schema(&json!({"b": 2, "c": "world"}));
172+
schema1.merge(schema2);
173+
174+
assert_eq!(schema1.types, vec![SchemaType::Object]);
175+
let props = schema1.properties.unwrap();
176+
assert_eq!(props.get("a").unwrap().types, vec![SchemaType::Integer]);
177+
let b_types = &props.get("b").unwrap().types;
178+
assert_eq!(b_types.len(), 2);
179+
assert!(b_types.contains(&SchemaType::String));
180+
assert!(b_types.contains(&SchemaType::Integer));
181+
assert_eq!(props.get("c").unwrap().types, vec![SchemaType::String]);
182+
}
183+
184+
#[test]
185+
fn test_infer_array_of_objects() {
186+
let doc = json!([
187+
{"a": 1},
188+
{"b": "hello"}
189+
]);
190+
let schema = infer_schema(&doc);
191+
assert_eq!(schema.types, vec![SchemaType::Array]);
192+
let items = schema.items.unwrap();
193+
assert_eq!(items.types, vec![SchemaType::Object]);
194+
let props = items.properties.unwrap();
195+
assert_eq!(props.get("a").unwrap().types, vec![SchemaType::Integer]);
196+
assert_eq!(props.get("b").unwrap().types, vec![SchemaType::String]);
197+
}
198+
199+
#[test]
200+
fn test_schema_type_variants() {
201+
assert_eq!(serde_json::to_string(&SchemaType::String).unwrap(), r#""string""#);
202+
assert_eq!(serde_json::to_string(&SchemaType::Integer).unwrap(), r#""integer""#);
203+
assert_eq!(serde_json::to_string(&SchemaType::Number).unwrap(), r#""number""#);
204+
assert_eq!(serde_json::to_string(&SchemaType::Boolean).unwrap(), r#""boolean""#);
205+
assert_eq!(serde_json::to_string(&SchemaType::Null).unwrap(), r#""null""#);
206+
assert_eq!(serde_json::to_string(&SchemaType::Object).unwrap(), r#""object""#);
207+
assert_eq!(serde_json::to_string(&SchemaType::Array).unwrap(), r#""array""#);
208+
}
209+
}

0 commit comments

Comments
 (0)