Skip to main content

mangle_db/
simplerow.rs

1// Copyright 2025 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! SimpleRow format: a row-oriented fact file format using Mangle syntax.
16//!
17//! Each fact is written as a valid Mangle atom (e.g. `edge(1, 2).`).
18//! The file has the same header as SimpleColumn (predicate count, then
19//! predicate name/arity/count lines), followed by facts grouped by predicate.
20
21use anyhow::{Context, Result, anyhow};
22use mangle_ast as ast;
23use mangle_common::Value;
24use mangle_parse::Parser;
25use std::collections::HashMap;
26use std::io::{BufRead, BufReader, Read, Write};
27
28/// Parsed simplerow data: predicate name → list of fact tuples.
29pub struct SimpleRowData {
30    pub tables: HashMap<String, Vec<Vec<Value>>>,
31}
32
33/// Read a simplerow file from bytes.
34pub fn read_from_bytes(data: &[u8]) -> Result<SimpleRowData> {
35    let reader = BufReader::new(data);
36    read_simple_row(reader)
37}
38
39/// Read a simplerow file from a reader.
40pub fn read_from_reader<R: Read>(reader: R) -> Result<SimpleRowData> {
41    let reader = BufReader::new(reader);
42    read_simple_row(reader)
43}
44
45struct PredInfo {
46    name: String,
47    arity: usize,
48    num_facts: usize,
49}
50
51fn read_simple_row<R: BufRead>(mut reader: R) -> Result<SimpleRowData> {
52    let mut line = String::new();
53
54    // 1. Num Predicates
55    reader.read_line(&mut line)?;
56    let num_preds: usize = line.trim().parse().context("parsing num_preds")?;
57    line.clear();
58
59    // 2. Predicate Headers
60    let mut preds = Vec::with_capacity(num_preds);
61    for _ in 0..num_preds {
62        reader.read_line(&mut line)?;
63        let parts: Vec<&str> = line.split_whitespace().collect();
64        if parts.len() != 3 {
65            return Err(anyhow!("Invalid predicate header: {line}"));
66        }
67        let name = parts[0].to_string();
68        let arity: usize = parts[1].parse().context("parsing arity")?;
69        let num_facts: usize = parts[2].parse().context("parsing num_facts")?;
70        preds.push(PredInfo {
71            name,
72            arity,
73            num_facts,
74        });
75        line.clear();
76    }
77
78    let mut tables = HashMap::new();
79
80    // 3. Facts (one per line, as Mangle atoms)
81    let arena = ast::Arena::new_with_global_interner();
82    for pred in &preds {
83        let mut facts = Vec::with_capacity(pred.num_facts);
84
85        if pred.arity == 0 {
86            // Flag facts: just the predicate name followed by a dot
87            for _ in 0..pred.num_facts {
88                line.clear();
89                reader.read_line(&mut line)?;
90                // e.g. "flag_pred."
91                facts.push(vec![]);
92            }
93            tables.insert(pred.name.clone(), facts);
94            continue;
95        }
96
97        for _ in 0..pred.num_facts {
98            line.clear();
99            if reader.read_line(&mut line)? == 0 {
100                return Err(anyhow!("Unexpected EOF reading facts for {}", pred.name));
101            }
102            let text = line.trim();
103            if text.is_empty() {
104                continue;
105            }
106
107            // Parse the atom using mangle-parse
108            let mut parser = Parser::new(&arena, text.as_bytes(), "simplerow");
109            parser.next_token().map_err(|e| anyhow!(e))?;
110            let clause = parser.parse_clause()?;
111            let atom = &clause.head;
112
113            let mut tuple = Vec::with_capacity(pred.arity);
114            for arg in atom.args {
115                tuple.push(term_to_value(arg));
116            }
117            facts.push(tuple);
118        }
119        tables.insert(pred.name.clone(), facts);
120    }
121
122    Ok(SimpleRowData { tables })
123}
124
125fn term_to_value(term: &ast::BaseTerm) -> Value {
126    match term {
127        ast::BaseTerm::Const(ast::Const::Number(n)) => Value::Number(*n),
128        ast::BaseTerm::Const(ast::Const::String(s)) => Value::String(s.to_string()),
129        ast::BaseTerm::Const(ast::Const::Name(n)) => {
130            // Name constants become strings
131            Value::String(format!("{n:?}"))
132        }
133        _ => Value::String(format!("{term:?}")),
134    }
135}
136
137/// Write facts in simplerow format.
138pub fn write_simple_row<W: Write>(
139    writer: &mut W,
140    tables: &[(String, Vec<Vec<Value>>)],
141) -> Result<()> {
142    // Header: number of predicates
143    writeln!(writer, "{}", tables.len())?;
144
145    // Predicate info lines
146    for (name, facts) in tables {
147        let arity = facts.first().map_or(0, |f| f.len());
148        writeln!(writer, "{} {} {}", name, arity, facts.len())?;
149    }
150
151    // Facts as Mangle atoms
152    for (name, facts) in tables {
153        for tuple in facts {
154            write!(writer, "{name}(")?;
155            for (i, val) in tuple.iter().enumerate() {
156                if i > 0 {
157                    write!(writer, ", ")?;
158                }
159                write!(writer, "{val}")?;
160            }
161            writeln!(writer, ").")?;
162        }
163    }
164
165    Ok(())
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn test_round_trip() -> Result<()> {
174        let tables = vec![
175            (
176                "edge".to_string(),
177                vec![
178                    vec![Value::Number(1), Value::Number(2)],
179                    vec![Value::Number(2), Value::Number(3)],
180                ],
181            ),
182            (
183                "user".to_string(),
184                vec![vec![Value::String("Alice".to_string()), Value::Number(30)]],
185            ),
186        ];
187
188        let mut buf = Vec::new();
189        write_simple_row(&mut buf, &tables)?;
190
191        let data = read_from_bytes(&buf)?;
192
193        assert_eq!(data.tables["edge"].len(), 2);
194        assert_eq!(
195            data.tables["edge"][0],
196            vec![Value::Number(1), Value::Number(2)]
197        );
198        assert_eq!(
199            data.tables["edge"][1],
200            vec![Value::Number(2), Value::Number(3)]
201        );
202
203        assert_eq!(data.tables["user"].len(), 1);
204        assert_eq!(
205            data.tables["user"][0],
206            vec![Value::String("Alice".to_string()), Value::Number(30)]
207        );
208
209        Ok(())
210    }
211
212    #[test]
213    fn test_write_format() -> Result<()> {
214        let tables = vec![(
215            "p".to_string(),
216            vec![vec![Value::Number(42), Value::String("hello".to_string())]],
217        )];
218
219        let mut buf = Vec::new();
220        write_simple_row(&mut buf, &tables)?;
221        let output = String::from_utf8(buf)?;
222
223        assert!(output.contains("1\n")); // 1 predicate
224        assert!(output.contains("p 2 1\n")); // name arity count
225        assert!(output.contains("p(42, \"hello\").\n"));
226
227        Ok(())
228    }
229}