1use crate::db::GraphDb;
2use core_rules::{default_max_edges, Predicate, RuleDef};
3use core_storage::fs::Fs;
4use core_storage::{GraphError, Result, Value};
5use serde::Serialize;
6use std::collections::{BTreeMap, BTreeSet};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct IngestOptions {
11 pub key_field: String,
13 pub auto_fk: AutoFk,
14}
15
16impl Default for IngestOptions {
17 fn default() -> Self {
18 Self {
19 key_field: "id".into(),
20 auto_fk: AutoFk::default(),
21 }
22 }
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum AutoFk {
33 Auto { suffix: String },
34 Off,
35}
36
37impl Default for AutoFk {
38 fn default() -> Self {
39 AutoFk::Auto {
40 suffix: "_id".into(),
41 }
42 }
43}
44
45#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
47pub struct FkSkip {
48 pub field: String,
49 pub reason: String,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
55pub struct IngestReport {
56 pub inserted: usize,
57 pub row_errors: Vec<(usize, String)>,
58 pub rules_created: Vec<String>,
59 pub skipped_fk_fields: Vec<FkSkip>,
60 pub edges_inserted: usize,
63}
64
65pub fn json_to_value(v: serde_json::Value) -> Option<Value> {
79 match v {
80 serde_json::Value::Null => None,
81 serde_json::Value::Bool(b) => Some(Value::Bool(b)),
82 serde_json::Value::Number(n) => number_to_value(&n),
83 serde_json::Value::String(s) => Some(Value::Str(s)),
84 serde_json::Value::Array(items) => {
85 let mut out = Vec::with_capacity(items.len());
86 for item in items {
87 out.push(json_to_value(item)?);
88 }
89 Some(Value::List(out))
90 }
91 serde_json::Value::Object(obj) => {
92 let mut map = std::collections::BTreeMap::new();
93 for (k, v) in obj {
94 if let Some(val) = json_to_value(v) {
95 map.insert(k, val);
96 }
97 }
98 Some(Value::Map(map))
99 }
100 }
101}
102
103fn number_to_value(n: &serde_json::Number) -> Option<Value> {
104 if let Some(i) = n.as_i64() {
105 return Some(Value::Int(i));
106 }
107 let f = n.as_f64()?;
108 if f.is_finite() && f.fract() == 0.0 && f >= i64::MIN as f64 && f <= i64::MAX as f64 {
109 Some(Value::Int(f as i64))
110 } else {
111 Some(Value::Float(f))
112 }
113}
114
115fn field_shape_error(_field: &str, _v: &serde_json::Value) -> Option<String> {
116 None
120}
121
122fn object_to_row(
123 obj: &serde_json::Map<String, serde_json::Value>,
124) -> std::result::Result<BTreeMap<String, Value>, String> {
125 let mut row = BTreeMap::new();
126 for (k, v) in obj {
127 if let Some(err) = field_shape_error(k, v) {
128 return Err(err);
129 }
130 if let Some(val) = json_to_value(v.clone()) {
131 row.insert(k.clone(), val);
132 }
133 }
134 Ok(row)
135}
136
137pub struct JsonRows {
140 pub rows: Vec<BTreeMap<String, Value>>,
142 kept_indices: Vec<usize>,
143 shape_errors: Vec<(usize, String)>,
144}
145
146impl JsonRows {
147 pub fn into_report(self, mut report: IngestReport) -> IngestReport {
150 for (idx, _) in &mut report.row_errors {
151 *idx = self.kept_indices[*idx];
152 }
153 report.row_errors.extend(self.shape_errors);
154 report.row_errors.sort_by_key(|(i, _)| *i);
155 report
156 }
157}
158
159pub fn json_to_rows(value: &serde_json::Value) -> Result<JsonRows> {
165 let arr = value.as_array().ok_or_else(|| GraphError::IngestError {
166 detail: "top-level JSON must be an array of objects".into(),
167 })?;
168 if !arr.iter().all(|v| v.is_object()) {
169 return Err(GraphError::IngestError {
170 detail: "top-level JSON must be an array of objects".into(),
171 });
172 }
173
174 let mut rows = Vec::new();
175 let mut shape_errors = Vec::new();
176 let mut kept_indices = Vec::new();
177 for (i, item) in arr.iter().enumerate() {
178 let obj = item
179 .as_object()
180 .expect("top-level checked as array of objects");
181 match object_to_row(obj) {
182 Ok(row) => {
183 kept_indices.push(i);
184 rows.push(row);
185 }
186 Err(msg) => shape_errors.push((i, msg)),
187 }
188 }
189 Ok(JsonRows {
190 rows,
191 kept_indices,
192 shape_errors,
193 })
194}
195
196pub(crate) fn run_json<F: Fs>(
198 db: &mut GraphDb<F>,
199 label: &str,
200 json: &str,
201 opts: &IngestOptions,
202) -> Result<IngestReport> {
203 let parsed: serde_json::Value =
204 serde_json::from_str(json).map_err(|e| GraphError::IngestError {
205 detail: e.to_string(),
206 })?;
207 let mut converted = json_to_rows(&parsed)?;
208 let rows = std::mem::take(&mut converted.rows);
209 let report = run(db, label, rows, opts, &[])?;
210 Ok(converted.into_report(report))
211}
212
213type PropMap = BTreeMap<String, Value>;
214
215struct Classified {
216 accepted: Vec<(String, PropMap)>,
217 row_errors: Vec<(usize, String)>,
218}
219
220pub(crate) fn run<F: Fs>(
223 db: &mut GraphDb<F>,
224 label: &str,
225 rows: Vec<BTreeMap<String, Value>>,
226 opts: &IngestOptions,
227 edges: &[(String, String, String)],
228) -> Result<IngestReport> {
229 let Classified {
230 accepted,
231 row_errors,
232 } = classify_rows(db, rows, &opts.key_field);
233
234 let (new_rules, skipped_fk_fields) = match &opts.auto_fk {
235 AutoFk::Off => (Vec::new(), Vec::new()),
236 AutoFk::Auto { suffix } => infer_auto_fk(db, label, suffix, &opts.key_field, &accepted),
237 };
238
239 let rules_created: Vec<String> = new_rules.iter().map(|r| r.name.clone()).collect();
240
241 let mut batch = db.batch();
243 for def in new_rules {
244 batch.create_rule(def);
245 }
246 for (key, props) in &accepted {
247 let prop_vec: Vec<(String, Value)> =
248 props.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
249 batch.insert_node(label, key, prop_vec);
250 }
251 for (etype, src, dst) in edges {
252 batch.insert_edge(etype, src, dst);
253 }
254 let (_, edges_inserted) = batch.commit_ingest(label, accepted.len())?;
255
256 Ok(IngestReport {
257 inserted: accepted.len(),
258 row_errors,
259 rules_created,
260 skipped_fk_fields,
261 edges_inserted,
262 })
263}
264
265fn classify_rows<F: Fs>(db: &GraphDb<F>, rows: Vec<PropMap>, key_field: &str) -> Classified {
266 let mut accepted = Vec::new();
267 let mut row_errors = Vec::new();
268 let mut seen: BTreeSet<String> = BTreeSet::new();
269
270 for (i, row) in rows.into_iter().enumerate() {
271 match row.get(key_field) {
272 None => row_errors.push((i, format!("missing key field {key_field}"))),
273 Some(Value::Str(key)) => {
274 if db.has_node(key) || seen.contains(key) {
275 row_errors.push((i, format!("duplicate key {key}")));
276 } else {
277 seen.insert(key.clone());
278 accepted.push((key.clone(), row));
279 }
280 }
281 Some(_) => row_errors.push((i, format!("key field {key_field} is not a string"))),
282 }
283 }
284 Classified {
285 accepted,
286 row_errors,
287 }
288}
289
290fn infer_auto_fk<F: Fs>(
291 db: &GraphDb<F>,
292 src_label: &str,
293 suffix: &str,
294 key_field: &str,
295 accepted: &[(String, PropMap)],
296) -> (Vec<RuleDef>, Vec<FkSkip>) {
297 let existing_rule_names: BTreeSet<String> = db.rules().into_iter().map(|r| r.name).collect();
298 let accepted_keys: BTreeSet<&str> = accepted.iter().map(|(k, _)| k.as_str()).collect();
299
300 let mut fields: BTreeSet<String> = BTreeSet::new();
301 for (_, row) in accepted {
302 for field in row.keys() {
303 if field != key_field && field.ends_with(suffix) && field.len() > suffix.len() {
304 fields.insert(field.clone());
305 }
306 }
307 }
308
309 let mut new_rules = Vec::new();
310 let mut skipped = Vec::new();
311
312 for field in fields {
313 let mut values: BTreeSet<&str> = BTreeSet::new();
314 for (_, row) in accepted {
315 if let Some(Value::Str(s)) = row.get(&field) {
316 values.insert(s.as_str());
317 }
318 }
319
320 let mut labels: BTreeSet<String> = BTreeSet::new();
321 for value in values {
322 if let Some(n) = db.node_ref(value) {
323 labels.insert(n.label().to_string());
324 }
325 if accepted_keys.contains(value) {
326 labels.insert(src_label.to_string());
327 }
328 }
329
330 match labels.len() {
331 0 => skipped.push(FkSkip {
332 field,
333 reason: "no matching target keys".into(),
334 }),
335 1 => {
336 let dst_label = labels.into_iter().next().expect("len == 1");
337 let name = format!("auto_fk_{}_{field}", src_label.to_lowercase());
340 if existing_rule_names.contains(&name) {
341 continue;
342 }
343 let remainder = &field[..field.len() - suffix.len()];
344 let predicate = Predicate::KeyMatch {
345 field: field.clone(),
346 };
347 let max_edges = Some(default_max_edges(&predicate));
348 new_rules.push(RuleDef {
349 name,
350 src_label: src_label.to_string(),
351 dst_label,
352 predicate,
353 edge_type: remainder.to_uppercase(),
354 weight_prop: None,
355 max_edges,
356 approximate: false,
357 via_label: None,
358 via_edge: None,
359 via_dir: None,
360 });
361 }
362 _ => {
363 let listed = labels.into_iter().collect::<Vec<_>>().join(", ");
364 skipped.push(FkSkip {
365 field,
366 reason: format!("ambiguous target labels: {listed}"),
367 });
368 }
369 }
370 }
371
372 (new_rules, skipped)
373}