From f6828dabeab58c73c3438529a137ebe7d5b2b56c Mon Sep 17 00:00:00 2001 From: Shuntian Liu Date: Mon, 22 Jun 2026 16:12:35 +0100 Subject: [PATCH 01/19] feat: update repl with more features --- packages/coln-store/README.md | 34 +++++-- packages/coln-store/src/repl/exe.rs | 79 +++++++++++++-- packages/coln-store/src/repl/mod.rs | 90 +++++++++--------- packages/coln-store/src/repl/parse.rs | 132 ++++++++++++++++++-------- 4 files changed, 234 insertions(+), 101 deletions(-) diff --git a/packages/coln-store/README.md b/packages/coln-store/README.md index 9e8cf50a..0c31e476 100644 --- a/packages/coln-store/README.md +++ b/packages/coln-store/README.md @@ -10,9 +10,9 @@ Coln Store is the storage engine for Coln. Roughly it ought to support: This repo lets you give the store a compiled Coln theory, load it, and then add data to its tables. -Example of loading a schema and inserting several related rows. Type `/help` -for the full command list (for example `load-schema`, `add`, `dump-table`, -`dump-store`, and the batch form below). +Example of loading a schema and inserting several related rows. Type `.help` +for the full command list (for example `.load`, `.schema`, `.rules`, +`.dump`, `.tables`, `.open`, `.save`, `add`, and the batch form below). ## Transaction @@ -27,12 +27,12 @@ The snippet below loads the `paths` theory, creates two graphs (`g0`, `g1`), records `g1` as the designated graph for the `Path.G0` and `Path.G1` indices, adds two vertices on `g0`, and adds one edge between them on `g0`. -After `commit`, inspect what landed with `dump-table ;`. For example, -`dump-table Path.Graphs;`, `dump-table Path.G.V;`, or `dump-table Path.G.E;`, -or print the entire store with `dump-store;`. +After `commit`, inspect what landed with `.dump
`. For example, +`.dump Path.Graphs`, `.dump Path.G.V`, or `.dump Path.G.E`, print every +table with `.tables`, or print all compiled rules with `.rules`. ```text -coln-store> load-schema tests/data/Path.json; +coln-store> .load tests/data/Path.json begin transact; g0 = add Path.Graphs values (); g1 = add Path.Graphs values (); @@ -47,8 +47,24 @@ begin transact; commit; -persist paths.bin; -load-store paths.bin; +.save paths.bin +.open paths.bin + +.load tests/data/ExprRealm.json +begin transact; + t0 = add ExprRealm.exprs.t values (); + t1 = add ExprRealm.exprs.t values (); + t2 = add ExprRealm.exprs.t values (); + va = add ExprRealm.v0 values (); + vb = add ExprRealm.v0 values (); + + ta = add ExprRealm.exprs.vars values (va t0); + tb = add ExprRealm.exprs.vars values (vb t1); + + apb = add ExprRealm.exprs.plus values (t0 t1 t2); + +commit; + ``` To get a violation of the law, say (`Path.Hom.V.total`), change the line diff --git a/packages/coln-store/src/repl/exe.rs b/packages/coln-store/src/repl/exe.rs index 1749996c..7ae27586 100644 --- a/packages/coln-store/src/repl/exe.rs +++ b/packages/coln-store/src/repl/exe.rs @@ -157,18 +157,52 @@ pub fn render_schema_summary(schema: Option<&SchemaSummary>) -> String { ]; for table in &schema.tables { - lines.push(format!( - "- {} | cols={} | pk={} | [{}]", - table.path, - table.column_count, - format_primary_key(&table.primary_key), - table.columns.join(", ") - )); + lines.push(String::new()); + lines.push(render_table_schema_summary(table)); } lines.join("\n") } +fn render_table_schema_summary(table: &TableSummary) -> String { + let mut lines = vec![ + format!("table: {}", table.path), + format!("columns: {}", table.column_count), + format!("primary key: {}", format_primary_key(&table.primary_key)), + ]; + lines.extend(table.columns.iter().map(|column| format!("- {column}"))); + + lines.join("\n") +} + +pub fn render_table_schema( + schema: Option<&SchemaSummary>, + table_name: &str, +) -> Result { + let schema = schema.ok_or(ReplError::NoSchemaLoaded)?; + let table = schema + .tables + .iter() + .find(|table| table.path == table_name) + .ok_or_else(|| ReplError::UnknownTable(table_name.to_string()))?; + + Ok(render_table_schema_summary(table)) +} + +pub fn render_rules(store: Option<&Store>) -> Result { + let store = store.ok_or(ReplError::NoSchemaLoaded)?; + if store.rules().is_empty() { + return Ok("no rules".to_string()); + } + + Ok(store + .rules() + .iter() + .map(ToString::to_string) + .collect::>() + .join("\n")) +} + pub fn add_rows( store: &mut Store, table_name: &str, @@ -297,6 +331,17 @@ mod tests { assert_eq!(render_schema_summary(None), "no schema loaded"); } + #[test] + fn renders_all_table_schemas() { + let loaded = load_schema(&Path::new("tests/data/").join(PATHS_IR)).expect("load schema"); + let rendered = render_schema_summary(Some(&loaded.schema)); + assert!(rendered.contains("source: tests/data/Path.json")); + assert!(rendered.contains("table: Path.G.V")); + assert!(rendered.contains("- a: entity(Path.Graphs)")); + assert!(rendered.contains("table: Path.G.E")); + assert!(rendered.contains("- b: entity(Path.G.V)")); + } + #[test] fn loads_schema_summary_from_fixture() { let loaded = load_schema(&Path::new("tests/data/").join(PATHS_IR)).expect("load schema"); @@ -305,4 +350,24 @@ mod tests { assert_eq!(loaded.schema.law_count, 27); assert_eq!(loaded.schema.tables[0].path, "Path.G0"); } + + #[test] + fn renders_single_table_schema() { + let loaded = load_schema(&Path::new("tests/data/").join(PATHS_IR)).expect("load schema"); + let rendered = + render_table_schema(Some(&loaded.schema), "Path.G.V").expect("render table schema"); + assert!(rendered.contains("table: Path.G.V")); + assert!(rendered.contains("primary key:")); + assert!(rendered.contains("- a: entity(Path.Graphs)")); + } + + #[test] + fn renders_rules_one_per_line() { + let loaded = load_schema(&Path::new("tests/data/").join(PATHS_IR)).expect("load schema"); + let rendered = render_rules(Some(&loaded.store)).expect("render rules"); + let lines = rendered.lines().collect::>(); + assert_eq!(lines.len(), loaded.store.rules().len()); + assert!(lines[0].contains(" := forall")); + assert!(lines[0].contains(" |- ")); + } } diff --git a/packages/coln-store/src/repl/mod.rs b/packages/coln-store/src/repl/mod.rs index 95c413b2..e048023e 100644 --- a/packages/coln-store/src/repl/mod.rs +++ b/packages/coln-store/src/repl/mod.rs @@ -15,7 +15,8 @@ use crate::commit::pst::encode_store; use crate::ir::Path; use crate::repl::error::ReplError; use crate::repl::exe::{ - LoadedState, add_rows, load_schema, load_store, render_schema_summary, run_transact, + LoadedState, add_rows, load_schema, load_store, render_rules, render_schema_summary, + render_table_schema, run_transact, }; use crate::repl::parse::Command; use crate::repl::parse::parse_command; @@ -25,17 +26,8 @@ pub mod exe; pub mod parse; const COMMANDS: &[&str] = &[ - "/help", - "/exit", - "/quit", - "load-schema", - "load-store", - "list-schema", - "dump-table", - "dump-store", - "persist", - "add", - "begin", + ".help", ".exit", ".quit", ".load", ".open", ".save", ".tables", ".rules", ".schema", ".dump", + "add", "begin", ]; #[derive(Default)] struct Session { @@ -51,7 +43,7 @@ enum Step { struct CommandHelper; fn is_statement_start(input: &str) -> bool { - !input.trim_start().starts_with('/') + !input.trim_start().starts_with('.') } pub fn run() -> Result<(), Box> { @@ -61,7 +53,7 @@ pub fn run() -> Result<(), Box> { let mut pending_statement: Option = None; println!("coln-store repl"); - println!("Type /help for commands."); + println!("Type .help for commands."); loop { let prompt = if pending_statement.is_some() { @@ -76,7 +68,7 @@ pub fn run() -> Result<(), Box> { pending_statement = None; println!("cancelled pending statement"); } else { - println!("Use `/exit` to quit."); + println!("Use `.exit` to quit."); } continue; } @@ -167,7 +159,7 @@ impl Completer for CommandHelper { fn execute(session: &mut Session, command: Command) -> Result { match command { Command::Help => Ok(Step::Continue(help_text())), - Command::LoadSchema { path } => { + Command::Load { path } => { let loaded = load_schema(std::path::Path::new(&path))?; info!( source = %loaded.schema.source.display(), @@ -184,7 +176,7 @@ fn execute(session: &mut Session, command: Command) -> Result { session.loaded = Some(loaded); Ok(Step::Continue(message)) } - Command::LoadStore { path } => { + Command::Open { path } => { let loaded = load_store(std::path::Path::new(&path))?; info!( source = %loaded.schema.source.display(), @@ -201,9 +193,18 @@ fn execute(session: &mut Session, command: Command) -> Result { session.loaded = Some(loaded); Ok(Step::Continue(message)) } - Command::ListSchema => Ok(Step::Continue(render_schema_summary( - session.loaded.as_ref().map(|loaded| &loaded.schema), - ))), + Command::Schema { table } => { + let schema = session.loaded.as_ref().map(|loaded| &loaded.schema); + let message = match table { + Some(table) => render_table_schema(schema, &table)?, + None => render_schema_summary(schema), + }; + Ok(Step::Continue(message)) + } + Command::Rules => { + let store = session.loaded.as_ref().map(|loaded| &loaded.store); + Ok(Step::Continue(render_rules(store)?)) + } Command::Add { table, rows } => { let loaded = session.loaded.as_mut().ok_or(ReplError::NoSchemaLoaded)?; let row_ids = add_rows(&mut loaded.store, &table, &rows)?; @@ -221,25 +222,25 @@ fn execute(session: &mut Session, command: Command) -> Result { let message = run_transact(&mut loaded.store, &assignments)?; Ok(Step::Continue(message)) } - Command::DumpTbl { name } => { + Command::Dump { table } => { let loaded = session.loaded.as_mut().ok_or(ReplError::NoSchemaLoaded)?; let tbl = loaded .store - .table_at(&Path::from(name.clone())) - .ok_or(ReplError::UnknownTable(name))?; + .table_at(&Path::from(table.clone())) + .ok_or(ReplError::UnknownTable(table))?; let message = tbl.dump(); Ok(Step::Continue(message)) } - Command::DumpStore => { + Command::Tables => { let loaded = session.loaded.as_mut().ok_or(ReplError::NoSchemaLoaded)?; let message = loaded.store.dump(); Ok(Step::Continue(message)) } - Command::Persist { path } => { + Command::Save { path } => { let loaded = session.loaded.as_mut().ok_or(ReplError::NoSchemaLoaded)?; let bytes = encode_store(&loaded.store)?; std::fs::write(&path, &bytes)?; - Ok(Step::Continue(format!("persisted store to {path}"))) + Ok(Step::Continue(format!("saved store to {path}"))) } Command::Exit => Ok(Step::Exit), } @@ -248,22 +249,25 @@ fn execute(session: &mut Session, command: Command) -> Result { fn help_text() -> String { [ "Commands:", - " /help", - " /exit", - " /quit", - " load-schema ;", - " load-store ;", - " list-schema;", + " .help", + " .exit", + " .quit", + " .load ", + " .open ", + " .save ", + " .tables", + " .rules", + " .schema [table]", + " .dump
", " add
values (...), (...);", - " dump-table
;", - " dump-store;", - " persist ;", " begin transact; name = add
values (...); ... commit;", "", "Examples:", - " /help", - " load-schema tests/data/paths.json;", - " list-schema;", + " .help", + " .load tests/data/paths.json", + " .schema", + " .rules", + " .dump T", " add T values (7 \"alice\"), (8 \"bob\");", " begin transact; g = add Graphs values (); e = add G0 values (g); commit;", ] @@ -426,15 +430,15 @@ mod tests { #[test] fn completes_command_prefix() { - let (start, matches) = complete_command("/q", 2); + let (start, matches) = complete_command(".q", 2); assert_eq!(start, 0); assert_eq!(matches.len(), 1); - assert_eq!(matches[0].replacement, "/quit"); + assert_eq!(matches[0].replacement, ".quit"); } #[test] fn does_not_complete_after_first_argument() { - let (_, matches) = complete_command("load-schema te", "load-schema te".len()); + let (_, matches) = complete_command(".load te", ".load te".len()); assert!(matches.is_empty()); } @@ -466,8 +470,8 @@ mod tests { } #[test] - fn slash_command_is_not_statement_start() { - assert!(!is_statement_start("/help")); + fn meta_command_is_not_statement_start() { + assert!(!is_statement_start(".help")); assert!(is_statement_start("add T 7")); } } diff --git a/packages/coln-store/src/repl/parse.rs b/packages/coln-store/src/repl/parse.rs index f62713b6..257a28bb 100644 --- a/packages/coln-store/src/repl/parse.rs +++ b/packages/coln-store/src/repl/parse.rs @@ -23,13 +23,17 @@ pub struct BatchAssignment { #[derive(Debug, PartialEq, Eq)] pub enum Command { Help, - LoadSchema { + Load { path: String, }, - LoadStore { + Open { path: String, }, - ListSchema, + Schema { + table: Option, + }, + Tables, + Rules, Add { table: String, rows: Vec>, @@ -38,11 +42,10 @@ pub enum Command { Batch { assignments: Vec, }, - DumpTbl { - name: String, + Dump { + table: String, }, - DumpStore, - Persist { + Save { path: String, }, Exit, @@ -71,7 +74,7 @@ pub fn parse_add_statement(input: &str) -> Result { pub fn parse_command(input: &str) -> Result { let input = input.trim(); - if input.starts_with('/') { + if input.starts_with('.') { parse_meta_command(input) } else { let Some(input) = input.strip_suffix(';') else { @@ -89,22 +92,61 @@ pub fn parse_meta_command(input: &str) -> Result { }; match command.as_str() { - "/help" => { + ".help" => { if parts.len() == 1 { Ok(Command::Help) } else { - Err("usage: /help".to_string()) + Err("usage: .help".to_string()) } } - "/exit" | "/quit" => { + ".exit" | ".quit" => { if parts.len() == 1 { Ok(Command::Exit) } else { Err(format!("usage: {command}")) } } + ".load" => match parts.as_slice() { + [_, path] => Ok(Command::Load { path: path.clone() }), + _ => Err("usage: .load ".to_string()), + }, + ".open" => match parts.as_slice() { + [_, path] => Ok(Command::Open { path: path.clone() }), + _ => Err("usage: .open ".to_string()), + }, + ".save" => match parts.as_slice() { + [_, path] => Ok(Command::Save { path: path.clone() }), + _ => Err("usage: .save ".to_string()), + }, + ".tables" => { + if parts.len() == 1 { + Ok(Command::Tables) + } else { + Err("usage: .tables".to_string()) + } + } + ".rules" => { + if parts.len() == 1 { + Ok(Command::Rules) + } else { + Err("usage: .rules".to_string()) + } + } + ".schema" => match parts.as_slice() { + [_] => Ok(Command::Schema { table: None }), + [_, table] => Ok(Command::Schema { + table: Some(table.clone()), + }), + _ => Err("usage: .schema [table]".to_string()), + }, + ".dump" => match parts.as_slice() { + [_, table] => Ok(Command::Dump { + table: table.clone(), + }), + _ => Err("usage: .dump
".to_string()), + }, _ => Err(format!( - "unknown meta command: {command}. Type `/help` for commands." + "unknown meta command: {command}. Type `.help` for commands." )), } } @@ -121,33 +163,9 @@ pub fn parse_statement(input: &str) -> Result { }; match command.as_str() { - "load-schema" => match parts.as_slice() { - [_, path] => Ok(Command::LoadSchema { path: path.clone() }), - _ => Err("usage: load-schema ;".to_string()), - }, - "load-store" => match parts.as_slice() { - [_, path] => Ok(Command::LoadStore { path: path.clone() }), - _ => Err("usage: load-store ;".to_string()), - }, - "list-schema" => { - if parts.len() == 1 { - Ok(Command::ListSchema) - } else { - Err("usage: list-schema;".to_string()) - } - } "add" => parse_add_statement(input), - "dump-table" => match parts.as_slice() { - [_, name] => Ok(Command::DumpTbl { name: name.clone() }), - _ => Err("usage: dump-table
;".to_string()), - }, - "dump-store" => Ok(Command::DumpStore), - "persist" => match parts.as_slice() { - [_, path] => Ok(Command::Persist { path: path.clone() }), - _ => Err("usage: persist ;".to_string()), - }, _ => Err(format!( - "unknown statement: {command}. Statements must end with `;`, or use `/help` for meta commands." + "unknown statement: {command}. Statements must end with `;`, or use `.help` for meta commands." )), } } @@ -416,7 +434,7 @@ mod tests { #[test] fn parses_help() { - assert_eq!(parse_command("/help").unwrap(), Command::Help); + assert_eq!(parse_command(".help").unwrap(), Command::Help); } #[test] @@ -425,10 +443,10 @@ mod tests { } #[test] - fn parses_load_schema_with_quotes() { + fn parses_load_with_quotes() { assert_eq!( - parse_command("load-schema \"tests/data/paths.json\";").unwrap(), - Command::LoadSchema { + parse_command(".load \"tests/data/paths.json\"").unwrap(), + Command::Load { path: "tests/data/paths.json".to_string() } ); @@ -473,7 +491,37 @@ mod tests { #[test] fn parses_quit_without_semicolon() { - assert_eq!(parse_command("/quit").unwrap(), Command::Exit); + assert_eq!(parse_command(".quit").unwrap(), Command::Exit); + } + + #[test] + fn parses_meta_commands_without_semicolons() { + assert_eq!( + parse_command(".open paths.bin").unwrap(), + Command::Open { + path: "paths.bin".to_string() + } + ); + assert_eq!( + parse_command(".save paths.bin").unwrap(), + Command::Save { + path: "paths.bin".to_string() + } + ); + assert_eq!(parse_command(".tables").unwrap(), Command::Tables); + assert_eq!(parse_command(".rules").unwrap(), Command::Rules); + assert_eq!( + parse_command(".schema Path.G.V").unwrap(), + Command::Schema { + table: Some("Path.G.V".to_string()) + } + ); + assert_eq!( + parse_command(".dump Path.G.V").unwrap(), + Command::Dump { + table: "Path.G.V".to_string() + } + ); } #[test] From b89b62dcbd5df400e34b5d9e20ac2ff34575c4bc Mon Sep 17 00:00:00 2001 From: Shuntian Liu Date: Mon, 22 Jun 2026 16:13:40 +0100 Subject: [PATCH 02/19] chore: Rename law to rule --- packages/coln-store/src/solver/bind.rs | 30 +++++----- packages/coln-store/src/solver/compile.rs | 45 +++++++++------ packages/coln-store/src/solver/validate.rs | 64 +++++++++++----------- packages/coln-store/src/store/error.rs | 2 +- packages/coln-store/src/store/mod.rs | 25 +++++---- packages/coln-store/src/txn/inner.rs | 2 +- packages/coln-store/src/txn/mod.rs | 2 +- packages/coln-store/tests/test_path.rs | 6 +- 8 files changed, 94 insertions(+), 82 deletions(-) diff --git a/packages/coln-store/src/solver/bind.rs b/packages/coln-store/src/solver/bind.rs index 22796d61..6f65e7e8 100644 --- a/packages/coln-store/src/solver/bind.rs +++ b/packages/coln-store/src/solver/bind.rs @@ -135,10 +135,10 @@ fn bind_prop(store: &Store, bindings: Vec, prop: &CompProp) -> Vec Vec { - debug!(law_name = %law.path, law=?law, "binding vars for law"); - let initial = vec![vec![None; law.vars.len()]]; - bind_prop(store, initial, &law.antecedent) +pub fn bind_rule(store: &Store, rule: &CompRule) -> Vec { + debug!(rule_name = %rule.path, rule=?rule, "binding vars for rule"); + let initial = vec![vec![None; rule.vars.len()]]; + bind_prop(store, initial, &rule.antecedent) } #[cfg(test)] @@ -149,7 +149,7 @@ mod tests { self, BuiltinTy, ColType, ColumnEntry, EntityVariant, Path, Rule, RuleEntry, RuleVariant, Schema, }, - solver::compile::compile_law, + solver::compile::compile_rule, table::CellValue, }; @@ -195,7 +195,7 @@ mod tests { } #[test] - fn bind_law_joins_antecedent_atoms() { + fn bind_rule_joins_antecedent_atoms() { let path = Path::from("T"); let mut store = Store::new(); store.insert_table( @@ -221,7 +221,7 @@ mod tests { .expect("insert row"); txn.commit().expect("commit rows"); - let law = enforced_rule( + let rule = enforced_rule( "T.chain", vec![int_ty(), int_ty(), int_ty()], vec![ @@ -270,8 +270,8 @@ mod tests { }], ); - let compiled = compile_law(&law).expect("compile law"); - let bindings = bind_law(&store, &compiled); + let compiled = compile_rule(&rule).expect("compile rule"); + let bindings = bind_rule(&store, &compiled); assert_eq!(bindings.len(), 1); assert_eq!(bindings[0][0], Some(BoundValue::Cell(CellValue::Int(1)))); @@ -298,7 +298,7 @@ mod tests { fn eq_in_antecedent_filters_to_matching_rows() { let (store, path) = store_with_int_column(&[1, 2, 3]); - let law = enforced_rule( + let rule = enforced_rule( "T.eq_filter", vec![int_ty(), int_ty()], vec![ @@ -339,8 +339,8 @@ mod tests { }], ); - let compiled = compile_law(&law).expect("compile law"); - let bindings = bind_law(&store, &compiled); + let compiled = compile_rule(&rule).expect("compile rule"); + let bindings = bind_rule(&store, &compiled); assert_eq!(bindings.len(), 3); for b in &bindings { @@ -352,7 +352,7 @@ mod tests { fn eq_with_literal_in_antecedent_pins_var_to_value() { let (store, path) = store_with_int_column(&[1, 2, 3]); - let law = enforced_rule( + let rule = enforced_rule( "T.eq_literal", vec![int_ty()], vec![ @@ -385,8 +385,8 @@ mod tests { }], ); - let compiled = compile_law(&law).expect("compile law"); - let bindings = bind_law(&store, &compiled); + let compiled = compile_rule(&rule).expect("compile rule"); + let bindings = bind_rule(&store, &compiled); assert_eq!(bindings.len(), 1); assert_eq!(bindings[0][0], Some(BoundValue::Cell(CellValue::Int(2)))); diff --git a/packages/coln-store/src/solver/compile.rs b/packages/coln-store/src/solver/compile.rs index c1e0e1ab..ab0e02bb 100644 --- a/packages/coln-store/src/solver/compile.rs +++ b/packages/coln-store/src/solver/compile.rs @@ -19,13 +19,14 @@ pub enum CompileError { InvalidColumnIndex { column: i64 }, } -/// A rule lowered into a small execution-oriented law form. +/// A rule lowered into a small execution-oriented rule form. /// /// The IR stores antecedents and consequents as vectors of propositions; each /// vector is interpreted as a conjunction here. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone)] pub struct CompRule { pub path: ir::Path, + pub rule_variant: ir::RuleVariant, pub vars: Vec, pub antecedent: CompProp, pub consequent: CompProp, @@ -84,7 +85,7 @@ pub enum CompTerm { /// - keeps table references in their source-level `ir::Path` form /// - validates variable and column indices /// - rejects IR forms not yet supported by the solver -pub fn compile_law(rule_entry: &RuleEntry) -> Result { +pub fn compile_rule(rule_entry: &RuleEntry) -> Result { let path = rule_entry.path.clone(); let vars = rule_entry .rule @@ -99,6 +100,8 @@ pub fn compile_law(rule_entry: &RuleEntry) -> Result { let antecedent = compile_props(&rule_entry.rule.antecedents, var_count)?; let consequent = compile_props(&rule_entry.rule.consequents, var_count)?; + let rule_variant = rule_entry.rule.rule_variant.clone(); + let mut seen = HashSet::new(); let mut tables = Vec::new(); collect_atom_tables(&antecedent, &mut seen, &mut tables); @@ -106,6 +109,7 @@ pub fn compile_law(rule_entry: &RuleEntry) -> Result { Ok(CompRule { path, + rule_variant, vars, antecedent, consequent, @@ -202,7 +206,12 @@ fn compile_term(term: &Term, var_count: usize) -> Result impl fmt::Display for CompRule { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{} := forall", DisplayPath(&self.path))?; + write!( + f, + "{} ({:?}) := forall", + DisplayPath(&self.path), + self.rule_variant + )?; for var in &self.vars { write!( f, @@ -306,7 +315,7 @@ impl fmt::Display for DisplayPath<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { for (idx, qname) in self.0.iter().enumerate() { if idx > 0 { - write!(f, " .")?; + write!(f, ".")?; } write!(f, "{}", display_qname(qname))?; } @@ -363,8 +372,8 @@ mod tests { } #[test] - fn compiles_single_atom_law() { - let law = enforced_rule( + fn compiles_single_atom_rule() { + let rule = enforced_rule( "T.total", vec![int_ty()], vec![Prop::Atom { @@ -389,7 +398,7 @@ mod tests { }], ); - let compiled = compile_law(&law).expect("compile law"); + let compiled = compile_rule(&rule).expect("compile rule"); assert!(matches!(compiled.antecedent, CompProp::And(_))); assert!(matches!(compiled.consequent, CompProp::And(_))); assert_eq!(compiled.tables.len(), 1); @@ -407,9 +416,10 @@ mod tests { } #[test] - fn displays_compiled_law_like_lowered_output() { + fn displays_compiled_rule_like_lowered_output() { let compiled = CompRule { path: Path::from("G.E.foreignKeys"), + rule_variant: RuleVariant::Enforced, vars: vec![ VarSpec { index: 0, @@ -465,7 +475,7 @@ mod tests { assert_eq!( compiled.to_string(), - "G .E .foreignKeys := forall (a : Graphs) (b : G .V) (c : G .V) => G .E [a, b, c] |- b @ G .V [a] /\\ c @ G .V [a]" + "G.E.foreignKeys (Enforced) := forall (a : Graphs) (b : G.V) (c : G.V) => G.E [a, b, c] |- b @ G.V [a] /\\ c @ G.V [a]" ); } @@ -473,6 +483,7 @@ mod tests { fn displays_compiled_equality_consequent() { let compiled = CompRule { path: Path::from("PathHom.empty"), + rule_variant: RuleVariant::Enforced, vars: vec![ VarSpec { index: 0, @@ -506,13 +517,13 @@ mod tests { assert_eq!( compiled.to_string(), - "PathHom .empty := forall (a : Path0 .t) (b : Path1 .t) => PathHom .t [a, b] |- a = b" + "PathHom.empty (Enforced) := forall (a : Path0.t) (b : Path1.t) => PathHom.t [a, b] |- a = b" ); } #[test] fn compiles_eq_in_antecedent() { - let law = enforced_rule( + let rule = enforced_rule( "T.eq_antecedent", vec![int_ty(), int_ty()], vec![Prop::Eq { @@ -527,7 +538,7 @@ mod tests { }, }], ); - let compiled = compile_law(&law).expect("compile law"); + let compiled = compile_rule(&rule).expect("compile rule"); assert!(matches!( compiled.antecedent, CompProp::And(ref children) if matches!(children.as_slice(), [CompProp::Eq(_)]) @@ -536,7 +547,7 @@ mod tests { #[test] fn compiles_consequent_equality() { - let law = enforced_rule( + let rule = enforced_rule( "T.eq", vec![int_ty(), int_ty()], vec![Prop::Atom { @@ -561,7 +572,7 @@ mod tests { }], ); - let compiled = compile_law(&law).expect("compile law"); + let compiled = compile_rule(&rule).expect("compile rule"); match compiled.consequent { CompProp::And(children) if matches!(children.as_slice(), [CompProp::Eq(_)]) => { let CompProp::Eq(CompEq { left, right }) = &children[0] else { @@ -579,7 +590,7 @@ mod tests { #[test] fn compiles_conjunction_of_atoms_and_eq() { let t = Path::from("T"); - let law = enforced_rule( + let rule = enforced_rule( "T.mixed", vec![int_ty(), int_ty()], vec![Prop::Atom { @@ -616,7 +627,7 @@ mod tests { ], ); - let compiled = compile_law(&law).expect("compile law"); + let compiled = compile_rule(&rule).expect("compile rule"); match &compiled.consequent { CompProp::And(children) => { assert_eq!(children.len(), 2); diff --git a/packages/coln-store/src/solver/validate.rs b/packages/coln-store/src/solver/validate.rs index d60e8370..608036fd 100644 --- a/packages/coln-store/src/solver/validate.rs +++ b/packages/coln-store/src/solver/validate.rs @@ -8,7 +8,7 @@ use tracing::debug; use crate::{ solver::{ - bind::{Binding, BoundValue, bind_law, eval_term}, + bind::{Binding, BoundValue, bind_rule, eval_term}, compile::{CompAtom, CompEq, CompProp, CompRule, CompVal}, matcher::term_matches, }, @@ -25,9 +25,9 @@ pub enum ViolationCause { UnsatisfiedEq(CompEq), } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone)] pub struct RuleViolation { - pub law: CompRule, + pub rule: CompRule, pub cause: ViolationCause, pub binding: Binding, } @@ -38,12 +38,12 @@ impl fmt::Display for RuleViolation { ViolationCause::MissingAtom(atom) => write!( f, "rule {} violated: missing consequent atom in table {}", - self.law, atom.table + self.rule, atom.table ), ViolationCause::UnsatisfiedEq(eq) => write!( f, "rule {} violated: equality {:?} = {:?} did not hold", - self.law, eq.left, eq.right + self.rule, eq.left, eq.right ), } } @@ -127,21 +127,21 @@ fn prop_holds(store: &Store, binding: &Binding, prop: &CompProp) -> Result<(), V } } -pub fn check_law(store: &Store, law: &CompRule) -> Result<(), Box> { - let bindings = bind_law(store, law); - debug!(law = %law.path, bindings = ?bindings, "checking law with bindings"); +pub fn check_rule(store: &Store, rule: &CompRule) -> Result<(), Box> { + let bindings = bind_rule(store, rule); + debug!(rule = %rule.path, bindings = ?bindings, "checking rule with bindings"); for binding in bindings { - if let Err(cause) = prop_holds(store, &binding, &law.consequent) { - debug!(law = %law.path, cause = ?cause, "law violation detected"); + if let Err(cause) = prop_holds(store, &binding, &rule.consequent) { + debug!(rule = %rule.path, cause = ?cause, "rule violation detected"); return Err(Box::new(RuleViolation { - law: law.clone(), + rule: rule.clone(), cause, binding, })); } } - debug!(law = %law.path, "law satisfied"); + debug!(rule = %rule.path, "rule satisfied"); Ok(()) } @@ -153,7 +153,7 @@ mod tests { self, BuiltinTy, ColType, ColumnEntry, EntityVariant, Path, Rule, RuleEntry, RuleVariant, Schema, }, - solver::compile::compile_law, + solver::compile::compile_rule, table::{CellValue, Table}, }; @@ -207,7 +207,7 @@ mod tests { Table::new(g0_path.clone(), int_schema(&[])), ); - let law = enforced_rule( + let rule = enforced_rule( "G0.total", vec![], vec![], @@ -220,10 +220,10 @@ mod tests { }], ); - let compiled = compile_law(&law).expect("compile law"); - let violation = check_law(&store, &compiled).expect_err("missing G0 row should violate"); + let compiled = compile_rule(&rule).expect("compile rule"); + let violation = check_rule(&store, &compiled).expect_err("missing G0 row should violate"); - assert_eq!(violation.law.path, Path::from("G0.total")); + assert_eq!(violation.rule.path, Path::from("G0.total")); match &violation.cause { ViolationCause::MissingAtom(atom) => assert_eq!(atom.table, g0_path), other => panic!("expected MissingAtom, got {:?}", other), @@ -252,7 +252,7 @@ mod tests { .expect("insert target row"); txn.commit().expect("commit matching rows"); - let law = enforced_rule( + let rule = enforced_rule( "Copy.total", vec![int_ty()], vec![ir::Prop::Atom { @@ -277,12 +277,12 @@ mod tests { }], ); - let compiled = compile_law(&law).expect("compile law"); - assert!(check_law(&store, &compiled).is_ok()); + let compiled = compile_rule(&rule).expect("compile rule"); + assert!(check_rule(&store, &compiled).is_ok()); } #[test] - fn foreign_key_law_fails_when_only_referencing_row_exists() { + fn foreign_key_rule_fails_when_only_referencing_row_exists() { let left = Path::from("Left"); let right = Path::from("Right"); let link = Path::from("Link"); @@ -297,7 +297,7 @@ mod tests { Table::new(link.clone(), int_schema(&["c0", "c1"])), ); - let law = enforced_rule( + let rule = enforced_rule( "Link.foreignKeys", vec![int_ty(), int_ty()], vec![ir::Prop::Atom { @@ -340,7 +340,7 @@ mod tests { ], ); - let compiled = compile_law(&law).expect("compile law"); + let compiled = compile_rule(&rule).expect("compile rule"); let mut txn = store.transaction(); txn.add( @@ -350,8 +350,8 @@ mod tests { .expect("insert referencing row"); txn.commit().expect("commit referencing row"); - let violation = check_law(&store, &compiled).expect_err("missing referenced rows"); - assert_eq!(violation.law.path, Path::from("Link.foreignKeys")); + let violation = check_rule(&store, &compiled).expect_err("missing referenced rows"); + assert_eq!(violation.rule.path, Path::from("Link.foreignKeys")); match &violation.cause { ViolationCause::MissingAtom(atom) => assert_eq!(atom.table, left), other => panic!("expected MissingAtom, got {:?}", other), @@ -371,7 +371,7 @@ mod tests { .expect("insert right row"); txn.commit().expect("commit referenced rows"); - assert!(check_law(&store, &compiled).is_ok()); + assert!(check_rule(&store, &compiled).is_ok()); } #[test] @@ -384,7 +384,7 @@ mod tests { .expect("insert row"); txn.commit().expect("commit row"); - let law = enforced_rule( + let rule = enforced_rule( "T.eq", vec![int_ty(), int_ty()], vec![ir::Prop::Atom { @@ -409,8 +409,8 @@ mod tests { }], ); - let compiled = compile_law(&law).expect("compile law"); - assert!(check_law(&store, &compiled).is_ok()); + let compiled = compile_rule(&rule).expect("compile rule"); + assert!(check_rule(&store, &compiled).is_ok()); } #[test] @@ -423,7 +423,7 @@ mod tests { .expect("insert row"); txn.commit().expect("commit row"); - let law = enforced_rule( + let rule = enforced_rule( "T.eq", vec![int_ty(), int_ty()], vec![ir::Prop::Atom { @@ -448,8 +448,8 @@ mod tests { }], ); - let compiled = compile_law(&law).expect("compile law"); - let violation = check_law(&store, &compiled).expect_err("eq should fail"); + let compiled = compile_rule(&rule).expect("compile rule"); + let violation = check_rule(&store, &compiled).expect_err("eq should fail"); match &violation.cause { ViolationCause::UnsatisfiedEq(eq) => { assert_eq!(eq.left, crate::solver::compile::CompTerm::Var(0)); diff --git a/packages/coln-store/src/store/error.rs b/packages/coln-store/src/store/error.rs index deffa89c..9ca5522b 100644 --- a/packages/coln-store/src/store/error.rs +++ b/packages/coln-store/src/store/error.rs @@ -13,7 +13,7 @@ pub enum StoreIntError { #[error(transparent)] Validation(#[from] ValidationError), #[error(transparent)] - Law(#[from] Box), + Rule(#[from] Box), #[error(transparent)] Compile(#[from] CompileError), #[error(transparent)] diff --git a/packages/coln-store/src/store/mod.rs b/packages/coln-store/src/store/mod.rs index b4c86df7..aca3df9b 100644 --- a/packages/coln-store/src/store/mod.rs +++ b/packages/coln-store/src/store/mod.rs @@ -24,7 +24,7 @@ use crate::txn::{OwnedTransaction, Transaction}; pub mod error; // TODO this should not be cloneable for efficiency reasons. In the future we should -// be able to teach the law validator to check the delta +// be able to teach the rule validator to check the delta #[derive(Debug, Clone)] pub struct Store { pub(crate) next_oid: TableOid, @@ -187,7 +187,7 @@ impl Store { /// Validates and appends a batch to this store. /// - /// This is a low level API that mutates `self` before law checking, so + /// This is a low level API that mutates `self` before rule checking, so /// callers that need atomicity must call it on a preview clone and publish /// that clone only after success. fn apply_batch(&mut self, ops: Vec) -> Result<(), StoreIntError> { @@ -206,7 +206,7 @@ impl Store { t.append_row(values.clone(), *row_id); } - self.check_laws()?; + self.check_rules()?; Ok(()) } @@ -294,17 +294,17 @@ impl Store { debug!(rule_count = rules.len(), "compiling rules"); let comp = rules .iter() - .map(solver::compile::compile_law) + .map(solver::compile::compile_rule) .collect::, CompileError>>()?; debug!(compiled_rule_count = comp.len(), "compiled rules"); Ok(comp) } - pub fn check_laws(&self) -> Result<(), StoreIntError> { + pub fn check_rules(&self) -> Result<(), StoreIntError> { debug!(rule_count = self.rules.len(), "checking rules"); self.rules() .iter() - .map(|law_entry| solver::validate::check_law(self, law_entry)) + .map(|rule| solver::validate::check_rule(self, rule)) .collect::, Box>>()?; debug!(rule_count = self.rules.len(), "all rules satisfied"); Ok(()) @@ -634,7 +634,7 @@ mod tests { use super::test_support::link_foreign_key_theory; use super::*; - use crate::ir::{BuiltinTy, ColType, ColumnEntry, EntityVariant, Path, Schema}; + use crate::ir::{BuiltinTy, ColType, ColumnEntry, EntityVariant, Path, RuleVariant, Schema}; fn single_int_store() -> Store { let path = Path::from("T"); @@ -1007,7 +1007,7 @@ mod tests { } #[test] - fn transaction_leaves_store_unchanged_when_laws_fail() { + fn transaction_leaves_store_unchanged_when_rules_fail() { let theory = link_foreign_key_theory(); let link = Path::from("Link"); let mut store = Store::try_from_theory(theory).expect("theory"); @@ -1020,7 +1020,7 @@ mod tests { .expect("add"); let err = txn.commit().unwrap_err(); - assert!(matches!(err, StoreIntError::Law(_))); + assert!(matches!(err, StoreIntError::Rule(_))); assert_eq!(store.table_at(&link).expect("Link").row_count(), 0); } @@ -1040,13 +1040,14 @@ mod tests { let compiled_rule = solver::compile::CompRule { path: Path::from("T.total"), + rule_variant: RuleVariant::Enforced, vars: vec![], antecedent: solver::compile::CompProp::And(vec![]), consequent: solver::compile::CompProp::And(vec![]), tables: vec![Path::from("T")], }; let violation = RuleViolation { - law: compiled_rule, + rule: compiled_rule, cause: solver::validate::ViolationCause::MissingAtom(solver::compile::CompAtom { table: Path::from("T"), row_id: None, @@ -1054,7 +1055,7 @@ mod tests { }), binding: vec![], }; - let law = StoreIntError::from(Box::new(violation)); - assert!(matches!(law, StoreIntError::Law(_))); + let rule = StoreIntError::from(Box::new(violation)); + assert!(matches!(rule, StoreIntError::Rule(_))); } } diff --git a/packages/coln-store/src/txn/inner.rs b/packages/coln-store/src/txn/inner.rs index c1279395..cd8ed715 100644 --- a/packages/coln-store/src/txn/inner.rs +++ b/packages/coln-store/src/txn/inner.rs @@ -147,7 +147,7 @@ impl TxnInner { // 3. resolve: TxnRowId(k) -> RowId { commit: hash, counter: k } // CellValue::TxnId(k) -> CellValue::Id(RowId { commit: hash, counter: k }) // 4. apply resolved Ops to tables via table.append_row - // 5. check_laws + // 5. check_rules // 6. push CommitMeta into store.commit_graph, advance heads // 7. return hash } diff --git a/packages/coln-store/src/txn/mod.rs b/packages/coln-store/src/txn/mod.rs index 184d0014..0e88c3e9 100644 --- a/packages/coln-store/src/txn/mod.rs +++ b/packages/coln-store/src/txn/mod.rs @@ -170,7 +170,7 @@ mod tests { .expect("add"); let (err, recovered) = tx.commit().unwrap_err(); - assert!(matches!(err, StoreIntError::Law(_))); + assert!(matches!(err, StoreIntError::Rule(_))); assert_eq!(recovered.table_at(&link).expect("Link").row_count(), 0); } diff --git a/packages/coln-store/tests/test_path.rs b/packages/coln-store/tests/test_path.rs index d8b0467d..9abc39fa 100644 --- a/packages/coln-store/tests/test_path.rs +++ b/packages/coln-store/tests/test_path.rs @@ -231,7 +231,7 @@ fn test_missing_graph_witness_rejects_batch_without_mutation() { tx.add(&Path::from("Path.Graphs"), vec![]) .expect("add graph row"); let err = tx.commit().expect_err("missing g0 and g1"); - assert!(matches!(err, StoreIntError::Law(_))); + assert!(matches!(err, StoreIntError::Rule(_))); assert_eq!( store @@ -280,7 +280,7 @@ fn test_fk() { .expect("add edge"); let err = tx.commit().expect_err("missing v2"); - assert!(matches!(err, StoreIntError::Law(_))); + assert!(matches!(err, StoreIntError::Rule(_))); } #[test] @@ -312,7 +312,7 @@ fn test_divergent_commits_merge_between_stores() { let mut base = Store::try_from_theory(theory).expect("valid theory"); add_basic_data_to_path(&mut base).expect("add shared baseline data"); - // Add a second law-free graph so we can add vertices to different graphs to + // Add a second rule-free graph so we can add vertices to different graphs to // make two commits different { let mut tx = base.transaction(); From 3d12241db4152b185a794f55c892d66cc9b0f3b6 Mon Sep 17 00:00:00 2001 From: Shuntian Liu Date: Thu, 25 Jun 2026 12:42:02 +0100 Subject: [PATCH 03/19] feat: initial hashcons template --- packages/coln-store/src/commit/wire/data.rs | 33 +- packages/coln-store/src/commit/wire/prim.rs | 36 ++- packages/coln-store/src/hashdag/mod.rs | 325 ++++++++++++++++++++ packages/coln-store/src/lib.rs | 3 +- packages/coln-store/src/table.rs | 93 ++++-- 5 files changed, 437 insertions(+), 53 deletions(-) create mode 100644 packages/coln-store/src/hashdag/mod.rs diff --git a/packages/coln-store/src/commit/wire/data.rs b/packages/coln-store/src/commit/wire/data.rs index 60a7f815..bd79fae8 100644 --- a/packages/coln-store/src/commit/wire/data.rs +++ b/packages/coln-store/src/commit/wire/data.rs @@ -7,7 +7,9 @@ use std::io::Write; use crate::commit::author::Author; use crate::commit::leb128 as commit_leb128; -use crate::commit::wire::prim::{ValueMeta, decode_prim_value, encode_prim_value}; +use crate::commit::wire::prim::{ + self, ValueMeta, decode_prim_value, encode_path, encode_prim_value, +}; use crate::{ commit::{ error::CodecError, @@ -307,9 +309,8 @@ fn encode_op_group( } let mut buf = Vec::new(); - let path = table.to_string(); - commit_leb128::write_len(&mut buf, path.len()); - buf.write_all(path.as_bytes())?; + let path = encode_path(table); + buf.write_all(&path)?; commit_leb128::write_len(&mut buf, rows.len()); @@ -470,12 +471,7 @@ where { let mut pos = 0usize; - let path_len = commit_leb128::read_len(data, &mut pos, "op group table path length")?; - let path_bytes = read_slice(data, &mut pos, path_len, "op group table path")?; - let table = - Path::from(std::str::from_utf8(path_bytes).map_err(|_| { - CodecError::DataFormatError("op group table path: invalid utf-8".into()) - })?); + let table = prim::decode_path(data, &mut pos)?; let row_count = commit_leb128::read_len(data, &mut pos, "op group row count")?; let column_count = commit_leb128::read_len(data, &mut pos, "op group column count")?; @@ -862,9 +858,8 @@ mod tests { let encoded = encode_op_group(&table, &schema, &ops, &hash_mapper).expect("encode group"); let mut pos = 0usize; - let path_len = commit_leb128::read_len(&encoded, &mut pos, "path length").unwrap(); - let path_bytes = read_slice(&encoded, &mut pos, path_len, "path").unwrap(); - assert_eq!(std::str::from_utf8(path_bytes).unwrap(), "T"); + let path = prim::decode_path(&encoded, &mut pos).unwrap(); + assert_eq!(path.to_string(), "T"); assert_eq!( commit_leb128::read_len(&encoded, &mut pos, "row count").unwrap(), 2 @@ -1016,10 +1011,8 @@ mod tests { let group_a = commit_leb128::read_len_prefixed_bytes(&encoded, &mut pos, "group a").unwrap(); let mut group_pos = 0usize; - let path_len = - commit_leb128::read_len(group_a, &mut group_pos, "group a path length").unwrap(); - let path = read_slice(group_a, &mut group_pos, path_len, "group a path").unwrap(); - assert_eq!(std::str::from_utf8(path).unwrap(), "A"); + let path = prim::decode_path(group_a, &mut group_pos).unwrap(); + assert_eq!(path.to_string(), "A"); assert_eq!( commit_leb128::read_len(group_a, &mut group_pos, "group a rows").unwrap(), 2 @@ -1032,10 +1025,8 @@ mod tests { let group_b = commit_leb128::read_len_prefixed_bytes(&encoded, &mut pos, "group b").unwrap(); let mut group_pos = 0usize; - let path_len = - commit_leb128::read_len(group_b, &mut group_pos, "group b path length").unwrap(); - let path = read_slice(group_b, &mut group_pos, path_len, "group b path").unwrap(); - assert_eq!(std::str::from_utf8(path).unwrap(), "B"); + let path = prim::decode_path(group_b, &mut group_pos).unwrap(); + assert_eq!(path.to_string(), "B"); assert_eq!( commit_leb128::read_len(group_b, &mut group_pos, "group b rows").unwrap(), 1 diff --git a/packages/coln-store/src/commit/wire/prim.rs b/packages/coln-store/src/commit/wire/prim.rs index 28170ab9..3db17298 100644 --- a/packages/coln-store/src/commit/wire/prim.rs +++ b/packages/coln-store/src/commit/wire/prim.rs @@ -4,9 +4,10 @@ use std::io::Write; -use coln_flir_rs::ir::BuiltinTy; +use coln_flir_rs::ir::{self, BuiltinTy}; use hexane::{PackError, lebsize}; +use crate::commit::leb128 as commit_leb128; use crate::{commit::error::CodecError, txn::ops::TxnCellValue}; /// Number of low bits reserved for the [`ValueType`] code in a [`ValueMeta`]. @@ -141,6 +142,8 @@ impl hexane::v1::PrefixValue for ValueMeta { } } +/// Writes to the buffer `out` the encoded data, and returns the corresponding +/// `ValueMeta` representation pub(crate) fn encode_prim_value( value: &TxnCellValue, prim: &BuiltinTy, @@ -208,6 +211,37 @@ pub(crate) fn decode_prim_value( } } +pub(crate) fn encode_path(path: &ir::Path) -> Vec { + let mut out = Vec::new(); + commit_leb128::write_len(&mut out, path.0.len()); + for qname in &path.0 { + commit_leb128::write_len(&mut out, qname.len()); + for part in qname { + commit_leb128::write_len_prefixed_bytes(&mut out, part.as_bytes()); + } + } + out +} + +pub(crate) fn decode_path(data: &[u8], pos: &mut usize) -> Result { + let qname_count = commit_leb128::read_len(data, pos, "path name count")?; + let mut path = Vec::with_capacity(qname_count); + + for _ in 0..qname_count { + let part_count = commit_leb128::read_len(data, pos, "part count")?; + let mut qname = Vec::with_capacity(part_count); + for _ in 0..part_count { + let part_bytes = commit_leb128::read_len_prefixed_bytes(data, pos, "qname part")?; + let part = std::str::from_utf8(part_bytes) + .map_err(|_| CodecError::DataFormatError("path part invalid utf-8".into()))?; + qname.push(part.to_owned()) + } + path.push(qname); + } + + Ok(ir::Path(path)) +} + #[allow(dead_code)] fn encode_bigint(value: &[u8], out: &mut Vec) -> Result { out.write_all(value)?; diff --git a/packages/coln-store/src/hashdag/mod.rs b/packages/coln-store/src/hashdag/mod.rs new file mode 100644 index 00000000..b55afe41 --- /dev/null +++ b/packages/coln-store/src/hashdag/mod.rs @@ -0,0 +1,325 @@ +#![allow(unused)] + +use std::collections::HashMap; + +use crate::commit::leb128 as commit_leb128; +use crate::commit::wire::prim; +use crate::table::CellValue; +use crate::table::RowId; +use crate::table::Table; + +const HASH_SIZE: usize = 16; + +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +pub struct ContentHash([u8; HASH_SIZE]); + +impl ContentHash { + pub(crate) fn as_bytes(&self) -> &[u8] { + &self.0 + } + + fn from_hasher(hasher: blake3::Hasher) -> Self { + let digest = hasher.finalize(); + Self(digest.as_bytes()[..HASH_SIZE].try_into().unwrap()) + } +} + +enum Tag { + Int, + Str, + LeafId, + InternalId, + EntireRow, +} + +impl Tag { + fn num(&self) -> u8 { + match self { + Tag::Int => 0x01, + Tag::Str => 0x02, + Tag::LeafId => 0x03, + Tag::InternalId => 0x04, + Tag::EntireRow => 0x10, + } + } +} + +struct ContentHasher { + inner: blake3::Hasher, +} + +impl ContentHasher { + fn new() -> Self { + Self { + inner: blake3::Hasher::new(), + } + } + + fn tag(&mut self, tag: Tag) { + self.inner.update(&[tag.num()]); + } + + fn bytes(&mut self, bytes: &[u8]) { + self.inner.update(bytes); + } + + fn row_id(&mut self, rid: RowId) { + self.bytes(rid.commit.as_bytes()); + let mut buf = Vec::new(); + commit_leb128::write_u32(&mut buf, rid.counter); + self.bytes(&buf); + } + + fn content_hash(&mut self, hash: ContentHash) { + self.bytes(hash.as_bytes()); + } + + fn finish(self) -> ContentHash { + ContentHash::from_hasher(self.inner) + } +} + +#[derive(Debug)] +pub(crate) enum ObservedOutcome { + Inserted(RowId), + KeptOld(RowId), + Swap { old: RowId, new: RowId }, +} + +pub(crate) struct Index { + by_row: HashMap, + canonical: HashMap, +} + +#[derive(Debug, thiserror::Error)] +pub enum HashDagError { + #[error("child hash missing when constructing parent hash")] + MissingChildHash { row_id: RowId }, +} + +impl Index { + /// Given a `row_id` returns the canonical `row_id` for it. Two rows map to + /// the same id when they contain the same hash, and the canonical id is the + /// lexical smaller one + pub fn canonical(&self, row_id: &RowId) -> RowId { + *self + .by_row + .get(row_id) + .and_then(|h| self.canonical.get(h)) + .unwrap_or(row_id) + } + + pub fn hash_row_id(&self, rid: RowId) -> ContentHash { + let mut hasher = ContentHasher::new(); + hasher.tag(Tag::LeafId); + hasher.row_id(rid); + hasher.finish() + } + + pub(crate) fn hash_cell(&self, val: &CellValue) -> Result { + let mut hasher = ContentHasher::new(); + match val { + CellValue::Id(rid) => { + let h = self + .by_row + .get(rid) + .ok_or(HashDagError::MissingChildHash { row_id: *rid })?; + + hasher.tag(Tag::InternalId); + hasher.content_hash(*h); + } + CellValue::Int(i) => { + hasher.tag(Tag::Int); + let mut buf = Vec::new(); + commit_leb128::write_i64(&mut buf, *i); + hasher.bytes(&buf); + } + CellValue::Str(s) => { + hasher.tag(Tag::Str); + hasher.bytes(s.as_bytes()); + } + } + + Ok(hasher.finish()) + } + + pub(crate) fn hash_row( + &self, + table: &Table, + rid: RowId, + vals: &[CellValue], + ) -> Result { + let mut hasher = ContentHasher::new(); + hasher.tag(Tag::EntireRow); + hasher.bytes(&prim::encode_path(table.path())); + + if !table.hashcons() { + let h = self.hash_row_id(rid); + hasher.content_hash(h); + } + for v in vals { + let h = self.hash_cell(v)?; + hasher.content_hash(h); + } + Ok(hasher.finish()) + } + + pub(crate) fn observe(&mut self, table: &Table, rid: RowId, h: ContentHash) -> ObservedOutcome { + if let Some(existing) = self.by_row.get(&rid) { + debug_assert_eq!( + existing, &h, + "row id was observed with a different content hash" + ); + } + self.by_row.insert(rid, h); + + if !table.hashcons() { + // no need to dedup, this table is not a hashcons table + return ObservedOutcome::Inserted(rid); + } + + match self.canonical.get(&h) { + None => { + self.canonical.insert(h, rid); + ObservedOutcome::Inserted(rid) + } + Some(&old) if rid < old => { + self.canonical.insert(h, rid); + ObservedOutcome::Swap { old, new: rid } + } + Some(&old) if old == rid => ObservedOutcome::KeptOld(rid), + Some(&old) => ObservedOutcome::KeptOld(old), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::commit::hash::CommitHash; + use crate::ir::{BuiltinTy, ColType, ColumnEntry, EntityVariant, Path, Schema}; + + fn row_id(commit_byte: u8, counter: u32) -> RowId { + RowId { + commit: CommitHash([commit_byte; 32]), + counter, + } + } + + fn empty_index() -> Index { + Index { + by_row: HashMap::new(), + canonical: HashMap::new(), + } + } + + fn int_table(path: &str, hashcons: bool) -> Table { + let schema = Schema { + entity_variant: EntityVariant::Table, + columns: vec![ColumnEntry { + path: Path::from("value"), + col_type: ColType::BuiltinTy { + builtin_ty: BuiltinTy::BuiltinInt, + }, + }], + primary_key: None, + }; + let mut table = Table::new(Path::from(path), schema); + table.set_hashcons_for_test(hashcons); + table + } + + #[test] + fn hash_row_id_distinguishes_row_ids() { + let index = empty_index(); + let first = row_id(1, 0); + let same = row_id(1, 0); + let different_counter = row_id(1, 1); + let different_commit = row_id(2, 0); + + assert_eq!(index.hash_row_id(first), index.hash_row_id(same)); + assert_ne!( + index.hash_row_id(first), + index.hash_row_id(different_counter) + ); + assert_ne!( + index.hash_row_id(first), + index.hash_row_id(different_commit) + ); + } + + #[test] + fn hash_cell_missing_child_hash_returns_error() { + let index = empty_index(); + let missing = row_id(1, 0); + + let err = index + .hash_cell(&CellValue::Id(missing)) + .expect_err("missing child hash should be reported"); + + assert!(matches!( + err, + HashDagError::MissingChildHash { row_id } if row_id == missing + )); + } + + #[test] + fn hash_cell_id_uses_child_content_hash() { + let mut index = empty_index(); + let child = row_id(1, 0); + let child_hash = index.hash_row_id(child); + index.by_row.insert(child, child_hash); + + let id_hash = index + .hash_cell(&CellValue::Id(child)) + .expect("child hash is present"); + + assert_ne!(id_hash, index.hash_row_id(child)); + assert_eq!( + id_hash, + index + .hash_cell(&CellValue::Id(child)) + .expect("child hash is still present") + ); + } + + #[test] + fn hash_row_non_hashcons_includes_own_row_id() { + let index = empty_index(); + let table = int_table("T", false); + let values = vec![CellValue::Int(7)]; + + let first = index + .hash_row(&table, row_id(1, 0), &values) + .expect("hash first row"); + let same = index + .hash_row(&table, row_id(1, 0), &values) + .expect("hash same row"); + let different = index + .hash_row(&table, row_id(1, 1), &values) + .expect("hash different row"); + + assert_eq!(first, same); + assert_ne!(first, different); + } + + #[test] + fn hash_row_hashcons_excludes_own_row_id() { + let index = empty_index(); + let table = int_table("T", true); + let values = vec![CellValue::Int(7)]; + + let first = index + .hash_row(&table, row_id(1, 0), &values) + .expect("hash first row"); + let same_content = index + .hash_row(&table, row_id(1, 1), &values) + .expect("hash same content"); + let different_content = index + .hash_row(&table, row_id(1, 1), &[CellValue::Int(8)]) + .expect("hash different content"); + + assert_eq!(first, same_content); + assert_ne!(first, different_content); + } +} diff --git a/packages/coln-store/src/lib.rs b/packages/coln-store/src/lib.rs index 340d672f..4cd136bc 100644 --- a/packages/coln-store/src/lib.rs +++ b/packages/coln-store/src/lib.rs @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 OR MIT -pub use coln_flir_rs::ir; +use coln_flir_rs::ir; pub mod commit; #[cfg(feature = "native")] pub mod repl; @@ -10,3 +10,4 @@ pub mod solver; pub mod store; pub mod table; pub mod txn; +mod hashdag; diff --git a/packages/coln-store/src/table.rs b/packages/coln-store/src/table.rs index 7949ba91..14f2c1c6 100644 --- a/packages/coln-store/src/table.rs +++ b/packages/coln-store/src/table.rs @@ -15,7 +15,7 @@ pub type TableOid = u64; /// The unique id that identifies each row in a table /// It is managed by the database, read-only for the user -#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)] +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug, Hash)] pub struct RowId { pub commit: CommitHash, pub counter: u32, @@ -37,8 +37,8 @@ pub enum ValidationError { #[error("type mismatch at column {column}: expected {expected}, got {got}")] TypeMismatch { column: usize, - expected: &'static str, - got: &'static str, + expected: CellKind, + got: CellKind, }, #[error("tuple columns are not supported yet (column {column})")] UnsupportedTuple { column: usize }, @@ -68,37 +68,59 @@ pub enum CellValue { Str(String), } -impl CellValue { - pub fn kind(&self) -> &'static str { - match self { - CellValue::Id(_) => "id", - CellValue::Int(_) => "int", - CellValue::Str(_) => "string", +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum CellKind { + RowId, + Int, + Str, +} + +impl From<&ColType> for CellKind { + fn from(col_type: &ColType) -> Self { + match col_type { + ColType::RowId { .. } => CellKind::RowId, + ColType::BuiltinTy { + builtin_ty: BuiltinTy::BuiltinInt, + } => CellKind::Int, + ColType::BuiltinTy { + builtin_ty: BuiltinTy::BuiltinStr, + } => CellKind::Str, + } + } +} + +impl From<&CellValue> for CellKind { + fn from(value: &CellValue) -> Self { + match value { + CellValue::Id(_) => CellKind::RowId, + CellValue::Int(_) => CellKind::Int, + CellValue::Str(_) => CellKind::Str, } } +} +impl fmt::Display for CellKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + CellKind::RowId => "entity id", + CellKind::Int => "int", + CellKind::Str => "string", + }) + } +} + +impl CellValue { fn matches_schema(&self, col_type: &ColType, column: usize) -> Result<(), ValidationError> { - match col_type { - ColType::RowId { .. } => match self { - CellValue::Id(_) => Ok(()), - _ => Err(ValidationError::TypeMismatch { - column, - expected: "entity id", - got: self.kind(), - }), - }, - ColType::BuiltinTy { builtin_ty } => match (builtin_ty, self) { - (BuiltinTy::BuiltinInt, CellValue::Int(_)) => Ok(()), - (BuiltinTy::BuiltinStr, CellValue::Str(_)) => Ok(()), - _ => Err(ValidationError::TypeMismatch { - column, - expected: match *builtin_ty { - BuiltinTy::BuiltinInt => "int", - BuiltinTy::BuiltinStr => "string", - }, - got: self.kind(), - }), - }, + let expected = CellKind::from(col_type); + let got = CellKind::from(self); + if expected == got { + Ok(()) + } else { + Err(ValidationError::TypeMismatch { + column, + expected, + got, + }) } } } @@ -128,6 +150,7 @@ pub struct Table { path: ir::Path, schema: Schema, col_index: HashMap, + hashcons: bool, pub(crate) row_ids: Vec, pub(crate) cols: Vec>, } @@ -145,6 +168,7 @@ impl Table { path, col_index, schema, + hashcons: false, row_ids: vec![], cols: vec![Vec::new(); n], } @@ -158,6 +182,15 @@ impl Table { &self.path } + pub(crate) fn hashcons(&self) -> bool { + self.hashcons + } + + #[cfg(test)] + pub(crate) fn set_hashcons_for_test(&mut self, hashcons: bool) { + self.hashcons = hashcons; + } + fn column_index(&self, name: &ir::Path) -> Option { self.col_index.get(name).copied() } From c62c29a68ecf784f6ee8711425f77bdfa8c65450 Mon Sep 17 00:00:00 2001 From: Shuntian Liu Date: Thu, 25 Jun 2026 17:03:20 +0100 Subject: [PATCH 04/19] =?UTF-8?q?feat:=20rowing=20=F0=9F=9A=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 452 +++++++----------- packages/coln-store/Cargo.toml | 1 + packages/coln-store/src/lib.rs | 2 +- packages/coln-store/src/repl/summary.rs | 3 - .../src/{hashdag/mod.rs => roweq/hashdag.rs} | 8 +- packages/coln-store/src/roweq/mod.rs | 12 + packages/coln-store/src/roweq/rowing.rs | 335 +++++++++++++ packages/coln-store/src/store/mod.rs | 1 + packages/coln-store/src/table.rs | 2 +- 9 files changed, 518 insertions(+), 298 deletions(-) delete mode 100644 packages/coln-store/src/repl/summary.rs rename packages/coln-store/src/{hashdag/mod.rs => roweq/hashdag.rs} (98%) create mode 100644 packages/coln-store/src/roweq/mod.rs create mode 100644 packages/coln-store/src/roweq/rowing.rs diff --git a/Cargo.lock b/Cargo.lock index 98c4ef55..c8917bbc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21,9 +21,9 @@ dependencies = [ [[package]] name = "actix-http" -version = "3.12.1" +version = "3.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93acb4a42f64936f9b8cae4a433b237599dd6eb6ed06124eb67132ef8cc90662" +checksum = "48e2faa3e7418ed780cca54829d32782a4008a077230f67457caa063415e99c2" dependencies = [ "actix-codec", "actix-rt", @@ -37,7 +37,7 @@ dependencies = [ "derive_more 2.1.1", "encoding_rs", "flate2", - "foldhash", + "foldhash 0.2.0", "futures-core", "h2 0.3.27", "http 0.2.12", @@ -65,7 +65,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -132,9 +132,9 @@ dependencies = [ [[package]] name = "actix-web" -version = "4.13.0" +version = "4.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff87453bc3b56e9b2b23c1cc0b1be8797184accf51d2abe0f8a33ec275d316bf" +checksum = "df09e2d9239703dd64056359c920c7f3fba6535ec61a0059e0f44e095ffe02b4" dependencies = [ "actix-codec", "actix-http", @@ -151,7 +151,7 @@ dependencies = [ "cookie", "derive_more 2.1.1", "encoding_rs", - "foldhash", + "foldhash 0.2.0", "futures-core", "futures-util", "impl-more", @@ -182,7 +182,7 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -249,9 +249,9 @@ checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" [[package]] name = "alloc-stdlib" -version = "0.2.2" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" dependencies = [ "alloc-no-stdlib", ] @@ -338,9 +338,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "arbitrary" @@ -353,9 +353,9 @@ dependencies = [ [[package]] name = "arc-swap" -version = "1.9.1" +version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" dependencies = [ "rustversion", ] @@ -380,9 +380,9 @@ checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" [[package]] name = "arrayvec" -version = "0.7.6" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe" [[package]] name = "arrow-schema" @@ -436,7 +436,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -447,7 +447,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -537,15 +537,15 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.12.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84d7ced0ae9557296835c32bf1b1e02b44c746701f898460fb000d7eaa84f00a" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" [[package]] name = "bitvec" -version = "1.0.1" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" dependencies = [ "funty", "radium", @@ -578,18 +578,18 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" dependencies = [ "hybrid-array", ] [[package]] name = "brotli" -version = "8.0.3" +version = "8.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8119e4516436f5708bbc474a9d395bf12f1b5395e93a92a56e647ac3388c8610" +checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -598,9 +598,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "5.0.1" +version = "5.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5962523e1b92ce1b5e793d9169b9943eece10d39f62550bc04bb605d75b94924" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -652,7 +652,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -663,9 +663,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" [[package]] name = "bytestring" @@ -693,9 +693,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.63" +version = "1.2.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" +checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" dependencies = [ "find-msvc-tools", "jobserver", @@ -717,9 +717,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chacha20" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ "cfg-if", "cpufeatures 0.3.0", @@ -808,7 +808,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -906,6 +906,7 @@ dependencies = [ "hexane", "js-sys", "leb128", + "petgraph 0.8.3", "postcard", "rustyline", "sedimentree_core", @@ -1241,7 +1242,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1312,7 +1313,7 @@ dependencies = [ "ordered-float 4.6.0", "ouroboros", "paste", - "petgraph", + "petgraph 0.6.5", "pin-project-lite", "ptr_meta 0.2.0", "rand 0.8.6", @@ -1375,7 +1376,7 @@ checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1404,7 +1405,7 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "unicode-xid", ] @@ -1418,7 +1419,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.117", + "syn 2.0.118", "unicode-xid", ] @@ -1439,7 +1440,7 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ - "block-buffer 0.12.0", + "block-buffer 0.12.1", "const-oid 0.10.2", "crypto-common 0.2.2", ] @@ -1452,7 +1453,7 @@ checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1536,7 +1537,7 @@ checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1654,7 +1655,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1764,6 +1765,12 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + [[package]] name = "flate2" version = "1.1.9" @@ -1787,6 +1794,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "foreign-types" version = "0.3.2" @@ -1835,7 +1848,7 @@ checksum = "6658fc211f4fa8521b72c00efe6626be82dd9d5bde5042137bcb90b02caee84d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1894,7 +1907,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1971,16 +1984,14 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", "rand_core 0.10.1", - "wasip2", - "wasip3", ] [[package]] @@ -2017,9 +2028,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" dependencies = [ "atomic-waker", "bytes", @@ -2088,7 +2099,7 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "foldhash", + "foldhash 0.1.5", ] [[package]] @@ -2144,7 +2155,7 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hexane" version = "1.0.0-alpha.1" -source = "git+https://github.com/automerge/automerge?branch=hexane_v1#30b83c03dc9669654153c01fe89f3fb931198324" +source = "git+https://github.com/automerge/automerge?branch=hexane_v1#583114e24ec5f01e7568503099e840168d3735f3" dependencies = [ "leb128", "thiserror 2.0.18", @@ -2232,9 +2243,9 @@ checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" [[package]] name = "hybrid-array" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" dependencies = [ "typenum", ] @@ -2249,7 +2260,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", - "h2 0.4.14", + "h2 0.4.15", "http 1.4.2", "http-body", "httparse", @@ -2405,12 +2416,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "idna" version = "1.1.0" @@ -2434,9 +2439,9 @@ dependencies = [ [[package]] name = "impl-more" -version = "0.1.9" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" +checksum = "35a84fd5aa25fae5c0f4a33d9cac2ca017fc622cbd089be2229993514990f870" [[package]] name = "impl-trait-for-tuples" @@ -2446,7 +2451,7 @@ checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2527,9 +2532,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.100" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2025f20d7a4fa7785846e7b63d10a76d3f1cee98ee5cb79ea59703f95e42162" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ "cfg-if", "futures-util", @@ -2550,15 +2555,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "leb128" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cc46bac87ef8093eed6f272babb833b6443374399985ac8ed28471ee0918545" - -[[package]] -name = "leb128fmt" -version = "0.1.0" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" +checksum = "c83bff1d572d6b9aeef67ddfc8448e4a3737909cb28e81f97c791b9018703e52" [[package]] name = "libbz2-rs-sys" @@ -2618,9 +2617,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.32" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "lru-slab" @@ -2665,9 +2664,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.8.1" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "memory-stats" @@ -2850,7 +2849,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2991,7 +2990,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3054,7 +3053,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3130,10 +3129,22 @@ version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ - "fixedbitset", + "fixedbitset 0.4.2", "indexmap", ] +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset 0.5.7", + "hashbrown 0.15.5", + "indexmap", + "serde", +] + [[package]] name = "pin-project-lite" version = "0.2.17" @@ -3240,7 +3251,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3284,7 +3295,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "version_check", "yansi", ] @@ -3341,9 +3352,9 @@ dependencies = [ [[package]] name = "quick_cache" -version = "0.6.23" +version = "0.6.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3db184a8b66cfe87f0263a1de147a6b554c864d1767c6f7fa4eb0e5497b565" +checksum = "b9c6658afe513a3b484e3abfdaa0d03ef3c0bbf017542c178dd55f94eb3051f9" dependencies = [ "ahash 0.8.12", "equivalent", @@ -3353,9 +3364,9 @@ dependencies = [ [[package]] name = "quinn" -version = "0.11.9" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" dependencies = [ "bytes", "cfg_aliases", @@ -3373,9 +3384,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.14" +version = "0.11.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e" dependencies = [ "bytes", "getrandom 0.3.4", @@ -3408,9 +3419,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.45" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", ] @@ -3472,7 +3483,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ "chacha20", - "getrandom 0.4.2", + "getrandom 0.4.3", "rand_core 0.10.1", ] @@ -3604,7 +3615,7 @@ dependencies = [ "bytes", "futures-core", "futures-util", - "h2 0.4.14", + "h2 0.4.15", "http 1.4.2", "http-body", "http-body-util", @@ -3738,9 +3749,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.40" +version = "0.23.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" dependencies = [ "once_cell", "ring", @@ -3773,9 +3784,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.1" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" dependencies = [ "web-time", "zeroize", @@ -3800,9 +3811,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "rustyline" -version = "18.0.0" +version = "18.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a990b25f351b25139ddc7f21ee3f6f56f86d6846b74ac8fad3a719a287cd4a0" +checksum = "53f6a737db68eb1a8ccff86b584b2fc13eca6a7bb6f78ebc7c529547e3ab9684" dependencies = [ "bitflags", "cfg-if", @@ -3881,7 +3892,7 @@ dependencies = [ [[package]] name = "sedimentree_core" version = "0.13.0" -source = "git+https://github.com/inkandswitch/subduction#9c95d13aff18c860a1582fdc3536686919f09057" +source = "git+https://github.com/inkandswitch/subduction#03d322c0313578e232d989697d0f47f772083f25" dependencies = [ "arbitrary", "async-lock", @@ -3950,7 +3961,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3961,7 +3972,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4115,18 +4126,18 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" dependencies = [ "serde", ] [[package]] name = "smawk" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" +checksum = "e8e2fb0f499abb4d162f2bedad68f5ef91a1682b5a03596ddb67efd37768d100" [[package]] name = "snap" @@ -4194,7 +4205,7 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "subduction_core" version = "0.16.0" -source = "git+https://github.com/inkandswitch/subduction#9c95d13aff18c860a1582fdc3536686919f09057" +source = "git+https://github.com/inkandswitch/subduction#03d322c0313578e232d989697d0f47f772083f25" dependencies = [ "async-channel", "async-lock", @@ -4216,7 +4227,7 @@ dependencies = [ [[package]] name = "subduction_crypto" version = "0.8.0" -source = "git+https://github.com/inkandswitch/subduction#9c95d13aff18c860a1582fdc3536686919f09057" +source = "git+https://github.com/inkandswitch/subduction#03d322c0313578e232d989697d0f47f772083f25" dependencies = [ "ed25519-dalek", "future_form", @@ -4230,7 +4241,7 @@ dependencies = [ [[package]] name = "subduction_websocket" version = "0.11.0" -source = "git+https://github.com/inkandswitch/subduction#9c95d13aff18c860a1582fdc3536686919f09057" +source = "git+https://github.com/inkandswitch/subduction#03d322c0313578e232d989697d0f47f772083f25" dependencies = [ "async-channel", "async-lock", @@ -4270,9 +4281,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.117" +version = "2.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" dependencies = [ "proc-macro2", "quote", @@ -4296,7 +4307,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4312,7 +4323,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.2", + "getrandom 0.4.3", "once_cell", "rustix", "windows-sys 0.61.2", @@ -4374,7 +4385,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4385,7 +4396,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4498,7 +4509,7 @@ checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4600,7 +4611,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4670,7 +4681,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4799,17 +4810,17 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "uuid", ] [[package]] name = "uuid" -version = "1.23.3" +version = "1.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" dependencies = [ - "getrandom 0.4.2", + "getrandom 0.4.3", "js-sys", "serde_core", "wasm-bindgen", @@ -4860,27 +4871,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.3+wasi-0.2.9" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ - "wit-bindgen 0.57.1", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen 0.51.0", + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.123" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a254a4b10c19a76f09a27640e7ffbf9bc30bf67e16a3bf28aaefa4920fe81563" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -4891,9 +4893,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.73" +version = "0.4.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54568702fabf5d4849ce2b90fadfa64168a097eaf4b351ce9df8b687a0086aaf" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" dependencies = [ "js-sys", "wasm-bindgen", @@ -4901,9 +4903,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.123" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a40fc75b0ec6f3746ceb10d36f53a93dcd68a93b11b6445983945d79eba0dc" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4911,48 +4913,26 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.123" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "908f34bd9b9ce3d4caf07b72dfab63d61504d156856c6bd3cd87fa350cf3985b" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.123" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7acbf7616c27b194bbb550bf77ed0c2c3e5b7fd1260a93082b95fb7f47959b92" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - [[package]] name = "wasm-streams" version = "0.4.2" @@ -4966,23 +4946,11 @@ dependencies = [ "web-sys", ] -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - [[package]] name = "web-sys" -version = "0.3.100" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e0871acf327f283dc6da28a1696cdc64fb355ba9f935d052021fa77f35cce69" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" dependencies = [ "js-sys", "wasm-bindgen", @@ -5050,7 +5018,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -5061,7 +5029,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -5244,100 +5212,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - [[package]] name = "wit-bindgen" version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck 0.5.0", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck 0.5.0", - "indexmap", - "prettyplease", - "syn 2.0.117", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn 2.0.117", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - [[package]] name = "writeable" version = "0.6.3" @@ -5384,7 +5264,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "synstructure", ] @@ -5405,7 +5285,7 @@ checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -5425,28 +5305,28 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "synstructure", ] [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -5479,7 +5359,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -5511,9 +5391,9 @@ dependencies = [ [[package]] name = "zlib-rs" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513" +checksum = "977347db8caa080403f6b6b7c1cda9479a8e869316f7e13a59b19076a40f94e3" [[package]] name = "zmij" diff --git a/packages/coln-store/Cargo.toml b/packages/coln-store/Cargo.toml index f538707c..b65c85c1 100644 --- a/packages/coln-store/Cargo.toml +++ b/packages/coln-store/Cargo.toml @@ -25,6 +25,7 @@ hex = "0.4.3" hexane = { git = "https://github.com/automerge/automerge", branch = "hexane_v1", package = "hexane" } js-sys = { version = "0.3.83", optional = true } leb128 = "0.2.6" +petgraph = "0.8.3" postcard = { version = "1.1.3", features = ["alloc"] } rustyline = { version = "18.0.0", optional = true } serde = { workspace = true, features = ["derive"] } diff --git a/packages/coln-store/src/lib.rs b/packages/coln-store/src/lib.rs index 4cd136bc..584301ab 100644 --- a/packages/coln-store/src/lib.rs +++ b/packages/coln-store/src/lib.rs @@ -4,10 +4,10 @@ use coln_flir_rs::ir; pub mod commit; +mod roweq; #[cfg(feature = "native")] pub mod repl; pub mod solver; pub mod store; pub mod table; pub mod txn; -mod hashdag; diff --git a/packages/coln-store/src/repl/summary.rs b/packages/coln-store/src/repl/summary.rs deleted file mode 100644 index c6932f3d..00000000 --- a/packages/coln-store/src/repl/summary.rs +++ /dev/null @@ -1,3 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Coln contributors -// -// SPDX-License-Identifier: Apache-2.0 OR MIT diff --git a/packages/coln-store/src/hashdag/mod.rs b/packages/coln-store/src/roweq/hashdag.rs similarity index 98% rename from packages/coln-store/src/hashdag/mod.rs rename to packages/coln-store/src/roweq/hashdag.rs index b55afe41..2acbeac2 100644 --- a/packages/coln-store/src/hashdag/mod.rs +++ b/packages/coln-store/src/roweq/hashdag.rs @@ -4,6 +4,7 @@ use std::collections::HashMap; use crate::commit::leb128 as commit_leb128; use crate::commit::wire::prim; +use crate::roweq::ObservedOutcome; use crate::table::CellValue; use crate::table::RowId; use crate::table::Table; @@ -79,13 +80,6 @@ impl ContentHasher { } } -#[derive(Debug)] -pub(crate) enum ObservedOutcome { - Inserted(RowId), - KeptOld(RowId), - Swap { old: RowId, new: RowId }, -} - pub(crate) struct Index { by_row: HashMap, canonical: HashMap, diff --git a/packages/coln-store/src/roweq/mod.rs b/packages/coln-store/src/roweq/mod.rs new file mode 100644 index 00000000..6c645f18 --- /dev/null +++ b/packages/coln-store/src/roweq/mod.rs @@ -0,0 +1,12 @@ +use crate::table::RowId; + +mod hashdag; +mod rowing; + +#[derive(Debug)] +pub(crate) enum ObservedOutcome { + Inserted(RowId), + KeptOld(RowId), + Swap { old: RowId, new: RowId }, +} + diff --git a/packages/coln-store/src/roweq/rowing.rs b/packages/coln-store/src/roweq/rowing.rs new file mode 100644 index 00000000..11b4f5a3 --- /dev/null +++ b/packages/coln-store/src/roweq/rowing.rs @@ -0,0 +1,335 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +// Why is it called rowing? Because it is comparing row for equivalence by looking +// at each value of the row one by one. And it sounds cool. +// Anyway way it will probably be renamed in the future. + +use coln_flir_rs::ir; +use petgraph::unionfind::UnionFind; +use std::collections::HashMap; + +use crate::{ + roweq::ObservedOutcome::{self, KeptOld}, + table::{CellValue, RowId, Table}, +}; + +type NodeId = u32; + +#[derive(Debug, Hash, PartialEq, Eq, Clone)] +struct RowKey { + table: ir::Path, + row_id: Option, + cells: Vec, +} + +#[derive(Debug, thiserror::Error)] +pub(crate) enum RowingError { + #[error("Missing child {rid}")] + MissingChild { rid: RowId }, +} + +pub(crate) struct Index { + row_to_node: HashMap, + uf: UnionFind, + // rootnode -> canonical row_id + canonical_row: HashMap, + + // check whether a structurally identical row exists + // return value not necessary canonical + by_key: HashMap, + + by_row: HashMap, +} + +impl Index { + pub(crate) fn new() -> Self { + Self { + row_to_node: HashMap::new(), + uf: UnionFind::new_empty(), + canonical_row: HashMap::new(), + by_key: HashMap::new(), + by_row: HashMap::new(), + } + } + + pub(crate) fn canonical(&self, row_id: &RowId) -> RowId { + *self + .row_to_node + .get(row_id) + .map(|&k| self.uf.find(k)) + .and_then(|root| self.canonical_row.get(&root)) + .unwrap_or(row_id) + } + + fn row_key( + &self, + table: &Table, + rid: &RowId, + values: &[CellValue], + ) -> Result { + // Rows must be observed in dependency order: every referenced child row + // has already been observed, and its canonical id is settled before it + // is embedded in a parent key. This keeps parent row keys stable without + // a reverse-dependency re-keying pass. + let cells = values + .iter() + .map(|cell| match cell { + CellValue::Id(child) => { + if !self.by_row.contains_key(child) { + return Err(RowingError::MissingChild { rid: *child }); + } + Ok(CellValue::Id(self.canonical(child))) + } + CellValue::Int(i) => Ok(CellValue::Int(*i)), + CellValue::Str(s) => Ok(CellValue::Str(s.to_owned())), + }) + .collect::, RowingError>>()?; + Ok(RowKey { + table: table.path().clone(), + row_id: if table.hashcons() { None } else { Some(*rid) }, + cells, + }) + } + + fn node_for(&mut self, rid: &RowId) -> NodeId { + if let Some(nid) = self.row_to_node.get(rid) { + *nid + } else { + let n = self.uf.new_set(); + self.row_to_node.insert(*rid, n); + n + } + } + + pub(crate) fn observe( + &mut self, + table: &Table, + rid: RowId, + values: Vec, + ) -> Result { + let node = self.node_for(&rid); + + let row_key = self.row_key(table, &rid, &values)?; + + // If we already saw this exact row id, it should have the same key. + if let Some(old_key) = self.by_row.get(&rid) { + assert_eq!(old_key, &row_key); + } else { + self.by_row.insert(rid, row_key.clone()); + } + + if !table.hashcons() { + self.by_key.insert(row_key, rid); + return Ok(ObservedOutcome::Inserted(rid)); + } + + match self.by_key.get(&row_key).copied() { + None => { + self.by_key.insert(row_key, rid); + Ok(ObservedOutcome::Inserted(rid)) + } + Some(existing_rid) => { + let old_canonical = self.canonical(&existing_rid); + let new_canonical = std::cmp::min(old_canonical, self.canonical(&rid)); + + let existing_node = self.node_for(&existing_rid); + self.uf.union(node, existing_node); + + let root = self.uf.find(node); + self.canonical_row.entry(root).insert_entry(new_canonical); + + if new_canonical == old_canonical { + Ok(KeptOld(old_canonical)) + } else { + Ok(ObservedOutcome::Swap { + old: old_canonical, + new: new_canonical, + }) + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::commit::hash::CommitHash; + use crate::ir::{BuiltinTy, ColType, ColumnEntry, EntityVariant, Path, Schema}; + use crate::roweq::ObservedOutcome::{Inserted, Swap}; + + fn row_id(commit_byte: u8, counter: u32) -> RowId { + RowId { + commit: CommitHash([commit_byte; 32]), + counter, + } + } + + fn int_table(path: &str, hashcons: bool) -> Table { + let schema = Schema { + entity_variant: EntityVariant::Table, + columns: vec![ColumnEntry { + path: Path::from("value"), + col_type: ColType::BuiltinTy { + builtin_ty: BuiltinTy::BuiltinInt, + }, + }], + primary_key: None, + }; + let mut table = Table::new(Path::from(path), schema); + table.set_hashcons_for_test(hashcons); + table + } + + fn id_table(path: &str, arity: usize, hashcons: bool) -> Table { + let schema = Schema { + entity_variant: EntityVariant::Table, + columns: (0..arity) + .map(|i| ColumnEntry { + path: Path::from(format!("c{i}").as_str()), + col_type: ColType::RowId { + path: Path::from(path), + }, + }) + .collect(), + primary_key: None, + }; + let mut table = Table::new(Path::from(path), schema); + table.set_hashcons_for_test(hashcons); + table + } + + #[test] + fn deduplicates_equal_hashcons_rows() { + let table = int_table("Term", true); + let mut index = Index::new(); + let first = row_id(1, 0); + let second = row_id(2, 0); + + let first_outcome = index + .observe(&table, first, vec![CellValue::Int(7)]) + .expect("first row should observe"); + let second_outcome = index + .observe(&table, second, vec![CellValue::Int(7)]) + .expect("second row should observe"); + + assert!(matches!(first_outcome, Inserted(row) if row == first)); + assert!(matches!(second_outcome, KeptOld(row) if row == first)); + assert_eq!(index.canonical(&first), first); + assert_eq!(index.canonical(&second), first); + } + + #[test] + fn keeps_equal_non_hashcons_rows_distinct() { + let table = int_table("Term", false); + let mut index = Index::new(); + let first = row_id(1, 0); + let second = row_id(2, 0); + + let first_outcome = index + .observe(&table, first, vec![CellValue::Int(7)]) + .expect("first row should observe"); + let second_outcome = index + .observe(&table, second, vec![CellValue::Int(7)]) + .expect("second row should observe"); + + assert!(matches!(first_outcome, Inserted(row) if row == first)); + assert!(matches!(second_outcome, Inserted(row) if row == second)); + assert_eq!(index.canonical(&first), first); + assert_eq!(index.canonical(&second), second); + } + + #[test] + fn uses_canonical_child_ids_in_parent_key() { + let leaf_table = int_table("Term", false); + let plus_table = id_table("Plus", 2, true); + let mut index = Index::new(); + + let a = row_id(1, 0); + let b = row_id(1, 1); + let c = row_id(1, 2); + index + .observe(&leaf_table, a, vec![CellValue::Int(1)]) + .expect("observe a"); + index + .observe(&leaf_table, b, vec![CellValue::Int(2)]) + .expect("observe b"); + index + .observe(&leaf_table, c, vec![CellValue::Int(3)]) + .expect("observe c"); + + let first_child = row_id(2, 0); + let second_child = row_id(3, 0); + index + .observe( + &plus_table, + first_child, + vec![CellValue::Id(a), CellValue::Id(b)], + ) + .expect("observe first child"); + index + .observe( + &plus_table, + second_child, + vec![CellValue::Id(a), CellValue::Id(b)], + ) + .expect("observe equivalent child"); + + let first_parent = row_id(4, 0); + let second_parent = row_id(5, 0); + let first_outcome = index + .observe( + &plus_table, + first_parent, + vec![CellValue::Id(first_child), CellValue::Id(c)], + ) + .expect("observe first parent"); + let second_outcome = index + .observe( + &plus_table, + second_parent, + vec![CellValue::Id(second_child), CellValue::Id(c)], + ) + .expect("observe equivalent parent"); + + assert!(matches!(first_outcome, Inserted(row) if row == first_parent)); + assert!(matches!(second_outcome, KeptOld(row) if row == first_parent)); + assert_eq!(index.canonical(&first_parent), first_parent); + assert_eq!(index.canonical(&second_parent), first_parent); + } + + #[test] + fn returns_error_for_missing_child() { + let table = id_table("Plus", 2, true); + let mut index = Index::new(); + let row = row_id(1, 0); + let missing = row_id(2, 0); + + let err = index + .observe(&table, row, vec![CellValue::Id(missing), CellValue::Int(1)]) + .expect_err("missing child should be rejected"); + + assert!(matches!(err, RowingError::MissingChild { rid } if rid == missing)); + } + + #[test] + fn smaller_later_row_becomes_canonical() { + let table = int_table("Term", true); + let mut index = Index::new(); + let larger = row_id(2, 0); + let smaller = row_id(1, 0); + + index + .observe(&table, larger, vec![CellValue::Int(7)]) + .expect("larger row should observe"); + let outcome = index + .observe(&table, smaller, vec![CellValue::Int(7)]) + .expect("smaller row should observe"); + + assert!(matches!(outcome, Swap { old, new } if old == larger && new == smaller)); + assert_eq!(index.canonical(&larger), smaller); + assert_eq!(index.canonical(&smaller), smaller); + } +} diff --git a/packages/coln-store/src/store/mod.rs b/packages/coln-store/src/store/mod.rs index aca3df9b..7da86304 100644 --- a/packages/coln-store/src/store/mod.rs +++ b/packages/coln-store/src/store/mod.rs @@ -35,6 +35,7 @@ pub struct Store { /// Compiled rule for this instance; table schemas live only on each [`Table`]. rules: Vec, commits: CommitGraph, + } impl Store { diff --git a/packages/coln-store/src/table.rs b/packages/coln-store/src/table.rs index 14f2c1c6..d264d7ff 100644 --- a/packages/coln-store/src/table.rs +++ b/packages/coln-store/src/table.rs @@ -61,7 +61,7 @@ pub enum ValidationError { } /// One cell in columnar storage: entity id, or primitive. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum CellValue { Id(RowId), Int(i64), From c28616e182ca16b230b3414b8779b64fea959cd4 Mon Sep 17 00:00:00 2001 From: Shuntian Liu Date: Tue, 30 Jun 2026 15:39:30 +0100 Subject: [PATCH 05/19] feat: initial integration of hashcons with store --- packages/coln-store/src/lib.rs | 2 +- packages/coln-store/src/roweq/mod.rs | 3 +- packages/coln-store/src/roweq/rowing.rs | 140 +++++++++++++++++------- packages/coln-store/src/store/error.rs | 3 + packages/coln-store/src/store/mod.rs | 23 +++- packages/coln-store/src/table.rs | 20 ++++ 6 files changed, 146 insertions(+), 45 deletions(-) diff --git a/packages/coln-store/src/lib.rs b/packages/coln-store/src/lib.rs index 584301ab..f72c692d 100644 --- a/packages/coln-store/src/lib.rs +++ b/packages/coln-store/src/lib.rs @@ -4,9 +4,9 @@ use coln_flir_rs::ir; pub mod commit; -mod roweq; #[cfg(feature = "native")] pub mod repl; +mod roweq; pub mod solver; pub mod store; pub mod table; diff --git a/packages/coln-store/src/roweq/mod.rs b/packages/coln-store/src/roweq/mod.rs index 6c645f18..fa69bcbf 100644 --- a/packages/coln-store/src/roweq/mod.rs +++ b/packages/coln-store/src/roweq/mod.rs @@ -1,7 +1,7 @@ use crate::table::RowId; mod hashdag; -mod rowing; +pub(crate) mod rowing; #[derive(Debug)] pub(crate) enum ObservedOutcome { @@ -9,4 +9,3 @@ pub(crate) enum ObservedOutcome { KeptOld(RowId), Swap { old: RowId, new: RowId }, } - diff --git a/packages/coln-store/src/roweq/rowing.rs b/packages/coln-store/src/roweq/rowing.rs index 11b4f5a3..9f6b699b 100644 --- a/packages/coln-store/src/roweq/rowing.rs +++ b/packages/coln-store/src/roweq/rowing.rs @@ -25,11 +25,14 @@ struct RowKey { } #[derive(Debug, thiserror::Error)] -pub(crate) enum RowingError { - #[error("Missing child {rid}")] +pub enum RowingError { + #[error("Child row missing {rid}")] MissingChild { rid: RowId }, + #[error("duplicate rowid {rid} with different row values")] + InconsistentRow { rid: RowId }, } +#[derive(Debug, Clone)] pub(crate) struct Index { row_to_node: HashMap, uf: UnionFind, @@ -38,6 +41,7 @@ pub(crate) struct Index { // check whether a structurally identical row exists // return value not necessary canonical + // this is the place hashcons is happening by_key: HashMap, by_row: HashMap, @@ -69,23 +73,26 @@ impl Index { rid: &RowId, values: &[CellValue], ) -> Result { - // Rows must be observed in dependency order: every referenced child row - // has already been observed, and its canonical id is settled before it - // is embedded in a parent key. This keeps parent row keys stable without - // a reverse-dependency re-keying pass. - let cells = values - .iter() - .map(|cell| match cell { - CellValue::Id(child) => { - if !self.by_row.contains_key(child) { - return Err(RowingError::MissingChild { rid: *child }); + let cells = if table.hashcons() { + // Hashcons rows must be observed in dependency order: every + // referenced child row has already been observed, and its canonical + // id is settled before it is embedded in a parent key. + values + .iter() + .map(|cell| match cell { + CellValue::Id(child) => { + if !self.by_row.contains_key(child) { + return Err(RowingError::MissingChild { rid: *child }); + } + Ok(CellValue::Id(self.canonical(child))) } - Ok(CellValue::Id(self.canonical(child))) - } - CellValue::Int(i) => Ok(CellValue::Int(*i)), - CellValue::Str(s) => Ok(CellValue::Str(s.to_owned())), - }) - .collect::, RowingError>>()?; + CellValue::Int(i) => Ok(CellValue::Int(*i)), + CellValue::Str(s) => Ok(CellValue::Str(s.to_owned())), + }) + .collect::, RowingError>>()? + } else { + values.to_vec() + }; Ok(RowKey { table: table.path().clone(), row_id: if table.hashcons() { None } else { Some(*rid) }, @@ -107,21 +114,21 @@ impl Index { &mut self, table: &Table, rid: RowId, - values: Vec, + values: &[CellValue], ) -> Result { + let row_key = self.row_key(table, &rid, values)?; let node = self.node_for(&rid); - let row_key = self.row_key(table, &rid, &values)?; - - // If we already saw this exact row id, it should have the same key. if let Some(old_key) = self.by_row.get(&rid) { - assert_eq!(old_key, &row_key); + // If we already saw this exact row id, it should have the same key. + if old_key != &row_key { + return Err(RowingError::InconsistentRow { rid }); + } } else { self.by_row.insert(rid, row_key.clone()); } if !table.hashcons() { - self.by_key.insert(row_key, rid); return Ok(ObservedOutcome::Inserted(rid)); } @@ -135,10 +142,23 @@ impl Index { let new_canonical = std::cmp::min(old_canonical, self.canonical(&rid)); let existing_node = self.node_for(&existing_rid); + + // Roots before the union; the survivor is one of these two, so + // the loser becomes a stale key in `canonical_row`. + let root_a = self.uf.find(node); + let root_b = self.uf.find(existing_node); self.uf.union(node, existing_node); let root = self.uf.find(node); - self.canonical_row.entry(root).insert_entry(new_canonical); + if root_a != root { + self.canonical_row.remove(&root_a); + } + if root_b != root { + self.canonical_row.remove(&root_b); + } + self.canonical_row.insert(root, new_canonical); + + self.by_key.insert(row_key, new_canonical); if new_canonical == old_canonical { Ok(KeptOld(old_canonical)) @@ -209,10 +229,10 @@ mod tests { let second = row_id(2, 0); let first_outcome = index - .observe(&table, first, vec![CellValue::Int(7)]) + .observe(&table, first, &[CellValue::Int(7)]) .expect("first row should observe"); let second_outcome = index - .observe(&table, second, vec![CellValue::Int(7)]) + .observe(&table, second, &[CellValue::Int(7)]) .expect("second row should observe"); assert!(matches!(first_outcome, Inserted(row) if row == first)); @@ -229,10 +249,10 @@ mod tests { let second = row_id(2, 0); let first_outcome = index - .observe(&table, first, vec![CellValue::Int(7)]) + .observe(&table, first, &[CellValue::Int(7)]) .expect("first row should observe"); let second_outcome = index - .observe(&table, second, vec![CellValue::Int(7)]) + .observe(&table, second, &[CellValue::Int(7)]) .expect("second row should observe"); assert!(matches!(first_outcome, Inserted(row) if row == first)); @@ -251,13 +271,13 @@ mod tests { let b = row_id(1, 1); let c = row_id(1, 2); index - .observe(&leaf_table, a, vec![CellValue::Int(1)]) + .observe(&leaf_table, a, &[CellValue::Int(1)]) .expect("observe a"); index - .observe(&leaf_table, b, vec![CellValue::Int(2)]) + .observe(&leaf_table, b, &[CellValue::Int(2)]) .expect("observe b"); index - .observe(&leaf_table, c, vec![CellValue::Int(3)]) + .observe(&leaf_table, c, &[CellValue::Int(3)]) .expect("observe c"); let first_child = row_id(2, 0); @@ -266,14 +286,14 @@ mod tests { .observe( &plus_table, first_child, - vec![CellValue::Id(a), CellValue::Id(b)], + &[CellValue::Id(a), CellValue::Id(b)], ) .expect("observe first child"); index .observe( &plus_table, second_child, - vec![CellValue::Id(a), CellValue::Id(b)], + &[CellValue::Id(a), CellValue::Id(b)], ) .expect("observe equivalent child"); @@ -283,14 +303,14 @@ mod tests { .observe( &plus_table, first_parent, - vec![CellValue::Id(first_child), CellValue::Id(c)], + &[CellValue::Id(first_child), CellValue::Id(c)], ) .expect("observe first parent"); let second_outcome = index .observe( &plus_table, second_parent, - vec![CellValue::Id(second_child), CellValue::Id(c)], + &[CellValue::Id(second_child), CellValue::Id(c)], ) .expect("observe equivalent parent"); @@ -308,7 +328,7 @@ mod tests { let missing = row_id(2, 0); let err = index - .observe(&table, row, vec![CellValue::Id(missing), CellValue::Int(1)]) + .observe(&table, row, &[CellValue::Id(missing), CellValue::Int(1)]) .expect_err("missing child should be rejected"); assert!(matches!(err, RowingError::MissingChild { rid } if rid == missing)); @@ -322,14 +342,56 @@ mod tests { let smaller = row_id(1, 0); index - .observe(&table, larger, vec![CellValue::Int(7)]) + .observe(&table, larger, &[CellValue::Int(7)]) .expect("larger row should observe"); let outcome = index - .observe(&table, smaller, vec![CellValue::Int(7)]) + .observe(&table, smaller, &[CellValue::Int(7)]) .expect("smaller row should observe"); assert!(matches!(outcome, Swap { old, new } if old == larger && new == smaller)); assert_eq!(index.canonical(&larger), smaller); assert_eq!(index.canonical(&smaller), smaller); } + + #[test] + fn reobserving_row_with_different_values_errors() { + let table = int_table("Term", true); + let mut index = Index::new(); + let rid = row_id(1, 0); + + index + .observe(&table, rid, &[CellValue::Int(7)]) + .expect("first observation should succeed"); + let err = index + .observe(&table, rid, &[CellValue::Int(8)]) + .expect_err("same row id with different values should be rejected"); + + assert!(matches!(err, RowingError::InconsistentRow { rid: got } if got == rid)); + } + + #[test] + fn three_equal_rows_merge_to_smallest() { + let table = int_table("Term", true); + let mut index = Index::new(); + let high = row_id(3, 0); + let mid = row_id(2, 0); + let low = row_id(1, 0); + + let high_outcome = index + .observe(&table, high, &[CellValue::Int(7)]) + .expect("observe high"); + let mid_outcome = index + .observe(&table, mid, &[CellValue::Int(7)]) + .expect("observe mid"); + let low_outcome = index + .observe(&table, low, &[CellValue::Int(7)]) + .expect("observe low"); + + assert!(matches!(high_outcome, Inserted(row) if row == high)); + assert!(matches!(mid_outcome, Swap { old, new } if old == high && new == mid)); + assert!(matches!(low_outcome, Swap { old, new } if old == mid && new == low)); + assert_eq!(index.canonical(&high), low); + assert_eq!(index.canonical(&mid), low); + assert_eq!(index.canonical(&low), low); + } } diff --git a/packages/coln-store/src/store/error.rs b/packages/coln-store/src/store/error.rs index 9ca5522b..7ef862f8 100644 --- a/packages/coln-store/src/store/error.rs +++ b/packages/coln-store/src/store/error.rs @@ -3,6 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 OR MIT use crate::commit::error::CodecError; +use crate::roweq::rowing::RowingError; use crate::solver::compile::CompileError; use crate::solver::validate::RuleViolation; use crate::table::ValidationError; @@ -20,6 +21,8 @@ pub enum StoreIntError { Encode(#[from] CodecError), #[error(transparent)] Commit(#[from] CommitApplyError), + #[error(transparent)] + Rowing(#[from] RowingError), } #[derive(Debug, thiserror::Error)] diff --git a/packages/coln-store/src/store/mod.rs b/packages/coln-store/src/store/mod.rs index 7da86304..27c0a56e 100644 --- a/packages/coln-store/src/store/mod.rs +++ b/packages/coln-store/src/store/mod.rs @@ -13,6 +13,7 @@ use crate::commit::graph::CommitGraph; use crate::commit::hash::CommitHash; use crate::commit::wire::root::{RootCommitData, RootTableEntry}; use crate::ir::{self, FlatRealm, RuleEntry}; +use crate::roweq::{self, rowing}; use crate::solver::compile::{CompRule, CompileError}; use crate::solver::validate::RuleViolation; use crate::solver::{self}; @@ -35,7 +36,7 @@ pub struct Store { /// Compiled rule for this instance; table schemas live only on each [`Table`]. rules: Vec, commits: CommitGraph, - + rowing: rowing::Index, } impl Store { @@ -50,6 +51,7 @@ impl Store { rule_entries: vec![], rules: vec![], commits, + rowing: rowing::Index::new(), } } @@ -150,6 +152,7 @@ impl Store { rule_entries: root.laws, rules, commits: CommitGraph::new(), + rowing: rowing::Index::new(), }) } @@ -202,9 +205,22 @@ impl Store { row_id, } = op; let oid = self.resolve_table(table).expect("validated batch"); - let t = self.table_mut(oid).expect("validated batch"); - t.append_row(values.clone(), *row_id); + // use self.tables to make the borrow checker happy we are accessing separate fields + let t = self.tables.get_mut(&oid).expect("validated batch"); + let observed = self.rowing.observe(t, *row_id, values)?; + + // TODO We are invoking rowing even if tables might not be in hashcons mode + match observed { + roweq::ObservedOutcome::Inserted(rid) => t.append_row(values.clone(), rid), + roweq::ObservedOutcome::KeptOld(_row_id) => { + // equivalent row exists, do nothing + } + + roweq::ObservedOutcome::Swap { old, new } => { + t.replace_row_id(&old, new)?; + } + } } self.check_rules()?; @@ -285,6 +301,7 @@ impl Store { rule_entries: rules, rules: comp_rules, commits, + rowing: rowing::Index::new(), }) } } diff --git a/packages/coln-store/src/table.rs b/packages/coln-store/src/table.rs index d264d7ff..d29b3ff5 100644 --- a/packages/coln-store/src/table.rs +++ b/packages/coln-store/src/table.rs @@ -150,6 +150,7 @@ pub struct Table { path: ir::Path, schema: Schema, col_index: HashMap, + row_index: HashMap, hashcons: bool, pub(crate) row_ids: Vec, pub(crate) cols: Vec>, @@ -167,6 +168,7 @@ impl Table { Self { path, col_index, + row_index: HashMap::new(), schema, hashcons: false, row_ids: vec![], @@ -319,11 +321,29 @@ impl Table { pub(crate) fn append_row(&mut self, values: Vec, row_id: RowId) { debug_assert_eq!(values.len(), self.schema.columns.len()); + self.row_index.insert(row_id, self.row_ids.len()); self.row_ids.push(row_id); for (i, v) in values.into_iter().enumerate() { self.cols[i].push(v); } } + + /// Used for canonicalising row_ids + pub(crate) fn replace_row_id( + &mut self, + old: &RowId, + new: RowId, + ) -> Result<(), ValidationError> { + let i = self + .row_index + .remove(old) + .ok_or(ValidationError::InvalidRowHandle { + reason: format!("attempting to replace non-existing row_id {old}"), + })?; + self.row_ids[i] = new; + self.row_index.insert(new, i); + Ok(()) + } } #[cfg(test)] From b17db8faa360ce8862843974f0e2dffb51df644c Mon Sep 17 00:00:00 2001 From: Shuntian Liu Date: Wed, 1 Jul 2026 15:37:29 +0100 Subject: [PATCH 06/19] refactor: more commands --- Cargo.lock | 72 ++++++ packages/coln-store/Cargo.toml | 1 + packages/coln-store/src/repl/mod.rs | 63 +++-- .../src/repl/{parse.rs => parse/coln.rs} | 231 +----------------- packages/coln-store/src/repl/parse/meta.rs | 78 ++++++ packages/coln-store/src/repl/parse/mod.rs | 182 ++++++++++++++ packages/coln-store/src/repl/parse/sql.rs | 15 ++ packages/coln-store/tests/test_repl.rs | 6 +- 8 files changed, 398 insertions(+), 250 deletions(-) rename packages/coln-store/src/repl/{parse.rs => parse/coln.rs} (61%) create mode 100644 packages/coln-store/src/repl/parse/meta.rs create mode 100644 packages/coln-store/src/repl/parse/mod.rs create mode 100644 packages/coln-store/src/repl/parse/sql.rs diff --git a/Cargo.lock b/Cargo.lock index c8917bbc..96fbe17a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -342,6 +342,15 @@ version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" +[[package]] +name = "ar_archive_writer" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4087686b4b0a3427190bae57a1d9a478dbb2d40c5dc1bd6e2b6d797913bdd348" +dependencies = [ + "object", +] + [[package]] name = "arbitrary" version = "1.4.2" @@ -913,6 +922,7 @@ dependencies = [ "serde", "serde_json", "shlex", + "sqlparser", "subduction_core", "subduction_crypto", "subduction_websocket", @@ -2912,6 +2922,15 @@ dependencies = [ "libc", ] +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + [[package]] name = "object_store" version = "0.12.5" @@ -3300,6 +3319,16 @@ dependencies = [ "yansi", ] +[[package]] +name = "psm" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645dbe486e346d9b5de3ef16ede18c26e6c70ad97418f4874b8b1889d6e761ea" +dependencies = [ + "ar_archive_writer", + "cc", +] + [[package]] name = "ptr_meta" version = "0.1.4" @@ -3552,6 +3581,26 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "recursive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0786a43debb760f491b1bc0269fe5e84155353c67482b9e60d0cfb596054b43e" +dependencies = [ + "recursive-proc-macro-impl", + "stacker", +] + +[[package]] +name = "recursive-proc-macro-impl" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76009fbe0614077fc1a2ce255e3a1881a2e3a3527097d5dc6d8212c585e7e38b" +dependencies = [ + "quote", + "syn 2.0.118", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -4184,12 +4233,35 @@ dependencies = [ "der", ] +[[package]] +name = "sqlparser" +version = "0.62.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c6d1b651dc4edf07eead2a0c6c78016ce971bc2c10da5266861b13f25e7cec" +dependencies = [ + "log", + "recursive", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "stacker" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "640c8cdd92b6b12f5bcb1803ca3bbf5ab96e5e6b6b96b9ab77dabe9e880b3190" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.61.2", +] + [[package]] name = "static_assertions" version = "1.1.0" diff --git a/packages/coln-store/Cargo.toml b/packages/coln-store/Cargo.toml index b65c85c1..8d5915bc 100644 --- a/packages/coln-store/Cargo.toml +++ b/packages/coln-store/Cargo.toml @@ -31,6 +31,7 @@ rustyline = { version = "18.0.0", optional = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } shlex = "2.0.1" +sqlparser = "0.62.0" thiserror = "2.0.18" tracing = { workspace = true } tracing-subscriber = { workspace = true, features = [ diff --git a/packages/coln-store/src/repl/mod.rs b/packages/coln-store/src/repl/mod.rs index e048023e..16505037 100644 --- a/packages/coln-store/src/repl/mod.rs +++ b/packages/coln-store/src/repl/mod.rs @@ -18,8 +18,8 @@ use crate::repl::exe::{ LoadedState, add_rows, load_schema, load_store, render_rules, render_schema_summary, render_table_schema, run_transact, }; -use crate::repl::parse::Command; use crate::repl::parse::parse_command; +use crate::repl::parse::{ColnCommand, Command, MetaCommand, SqlCommand}; pub mod error; pub mod exe; @@ -29,9 +29,20 @@ const COMMANDS: &[&str] = &[ ".help", ".exit", ".quit", ".load", ".open", ".save", ".tables", ".rules", ".schema", ".dump", "add", "begin", ]; + +const SECRET_MODE: &str = "ILOVESQL"; + +#[derive(Debug, Default, PartialEq, Eq, Copy, Clone)] +pub enum ShellMode { + #[default] + Coln, + Sql, +} + #[derive(Default)] struct Session { loaded: Option, + shell_mode: ShellMode, } #[derive(Debug)] @@ -58,9 +69,12 @@ pub fn run() -> Result<(), Box> { loop { let prompt = if pending_statement.is_some() { "....> " + } else if session.shell_mode == ShellMode::Sql { + "coln-sql>" } else { "coln-store> " }; + let line = match editor.readline(prompt) { Ok(line) => line, Err(ReadlineError::Interrupted) => { @@ -81,6 +95,15 @@ pub fn run() -> Result<(), Box> { continue; } + if pending_statement.is_none() + && trimmed == SECRET_MODE + && session.shell_mode == ShellMode::Coln + { + session.shell_mode = ShellMode::Sql; + println!("Welcome to SQL mode!"); + continue; + } + let maybe_command = if pending_statement.is_some() || is_statement_start(trimmed) { let command = push_statement_line(&mut pending_statement, trimmed); if let Some(command) = command { @@ -94,7 +117,7 @@ pub fn run() -> Result<(), Box> { Some(trimmed.to_string()) }; - match parse_command(maybe_command.as_deref().expect("command")) { + match parse_command(session.shell_mode, &maybe_command.expect("command")) { Ok(command) => match execute(&mut session, command) { Ok(Step::Continue(message)) => println!("{message}"), Ok(Step::Exit) => break, @@ -158,8 +181,8 @@ impl Completer for CommandHelper { fn execute(session: &mut Session, command: Command) -> Result { match command { - Command::Help => Ok(Step::Continue(help_text())), - Command::Load { path } => { + Command::MetaCommand(MetaCommand::Help) => Ok(Step::Continue(help_text())), + Command::MetaCommand(MetaCommand::Load { path }) => { let loaded = load_schema(std::path::Path::new(&path))?; info!( source = %loaded.schema.source.display(), @@ -176,7 +199,7 @@ fn execute(session: &mut Session, command: Command) -> Result { session.loaded = Some(loaded); Ok(Step::Continue(message)) } - Command::Open { path } => { + Command::MetaCommand(MetaCommand::Open { path }) => { let loaded = load_store(std::path::Path::new(&path))?; info!( source = %loaded.schema.source.display(), @@ -193,7 +216,7 @@ fn execute(session: &mut Session, command: Command) -> Result { session.loaded = Some(loaded); Ok(Step::Continue(message)) } - Command::Schema { table } => { + Command::MetaCommand(MetaCommand::Schema { table }) => { let schema = session.loaded.as_ref().map(|loaded| &loaded.schema); let message = match table { Some(table) => render_table_schema(schema, &table)?, @@ -201,11 +224,11 @@ fn execute(session: &mut Session, command: Command) -> Result { }; Ok(Step::Continue(message)) } - Command::Rules => { + Command::MetaCommand(MetaCommand::Rules) => { let store = session.loaded.as_ref().map(|loaded| &loaded.store); Ok(Step::Continue(render_rules(store)?)) } - Command::Add { table, rows } => { + Command::ColnCommand(ColnCommand::Add { table, rows }) => { let loaded = session.loaded.as_mut().ok_or(ReplError::NoSchemaLoaded)?; let row_ids = add_rows(&mut loaded.store, &table, &rows)?; let row_ids = row_ids @@ -217,12 +240,12 @@ fn execute(session: &mut Session, command: Command) -> Result { "inserted into {table} rows [{row_ids}]" ))) } - Command::Batch { assignments } => { + Command::ColnCommand(ColnCommand::Batch { assignments }) => { let loaded = session.loaded.as_mut().ok_or(ReplError::NoSchemaLoaded)?; let message = run_transact(&mut loaded.store, &assignments)?; Ok(Step::Continue(message)) } - Command::Dump { table } => { + Command::MetaCommand(MetaCommand::Dump { table }) => { let loaded = session.loaded.as_mut().ok_or(ReplError::NoSchemaLoaded)?; let tbl = loaded .store @@ -231,18 +254,21 @@ fn execute(session: &mut Session, command: Command) -> Result { let message = tbl.dump(); Ok(Step::Continue(message)) } - Command::Tables => { + Command::MetaCommand(MetaCommand::Tables) => { let loaded = session.loaded.as_mut().ok_or(ReplError::NoSchemaLoaded)?; let message = loaded.store.dump(); Ok(Step::Continue(message)) } - Command::Save { path } => { + Command::MetaCommand(MetaCommand::Save { path }) => { let loaded = session.loaded.as_mut().ok_or(ReplError::NoSchemaLoaded)?; let bytes = encode_store(&loaded.store)?; std::fs::write(&path, &bytes)?; Ok(Step::Continue(format!("saved store to {path}"))) } - Command::Exit => Ok(Step::Exit), + Command::MetaCommand(MetaCommand::Exit) => Ok(Step::Exit), + Command::SqlCommand(SqlCommand::CreateTable) => Ok(Step::Continue( + "SQL CREATE TABLE is not implemented yet".to_string(), + )), } } @@ -300,7 +326,7 @@ mod tests { ir::{BuiltinTy, ColType, ColumnEntry, EntityVariant}, repl::{ exe::{PrimaryKeySummary, SchemaSummary, TableSummary}, - parse::Command, + parse::{ColnCommand, Command}, }, store::Store, }; @@ -354,17 +380,18 @@ mod tests { fn add_inserts_rows_into_loaded_store() { let mut session = Session { loaded: Some(test_loaded_state()), + shell_mode: ShellMode::Coln, }; let message = match execute( &mut session, - Command::Add { + Command::ColnCommand(ColnCommand::Add { table: "T".to_string(), rows: vec![ vec!["7".to_string(), "alice".to_string()], vec!["8".to_string(), "bob".to_string()], ], - }, + }), ) .expect("execute add") { @@ -390,10 +417,10 @@ mod tests { fn add_requires_loaded_schema() { let err = execute( &mut Session::default(), - Command::Add { + Command::ColnCommand(ColnCommand::Add { table: "T".to_string(), rows: vec![vec!["7".to_string()]], - }, + }), ) .unwrap_err(); diff --git a/packages/coln-store/src/repl/parse.rs b/packages/coln-store/src/repl/parse/coln.rs similarity index 61% rename from packages/coln-store/src/repl/parse.rs rename to packages/coln-store/src/repl/parse/coln.rs index 257a28bb..24d31e15 100644 --- a/packages/coln-store/src/repl/parse.rs +++ b/packages/coln-store/src/repl/parse/coln.rs @@ -22,33 +22,12 @@ pub struct BatchAssignment { #[derive(Debug, PartialEq, Eq)] pub enum Command { - Help, - Load { - path: String, - }, - Open { - path: String, - }, - Schema { - table: Option, - }, - Tables, - Rules, Add { table: String, rows: Vec>, }, /// `begin transact;` … `commit;` with assignments using previous bindings in entity columns. - Batch { - assignments: Vec, - }, - Dump { - table: String, - }, - Save { - path: String, - }, - Exit, + Batch { assignments: Vec }, } pub fn parse_add_statement(input: &str) -> Result { @@ -72,85 +51,6 @@ pub fn parse_add_statement(input: &str) -> Result { }) } -pub fn parse_command(input: &str) -> Result { - let input = input.trim(); - if input.starts_with('.') { - parse_meta_command(input) - } else { - let Some(input) = input.strip_suffix(';') else { - return Err("statements must end with `;`".to_string()); - }; - let input = input.trim_end(); - parse_statement(input) - } -} - -pub fn parse_meta_command(input: &str) -> Result { - let parts = shlex::split(input).ok_or_else(|| "could not parse input".to_string())?; - let Some(command) = parts.first() else { - return Err("empty command".to_string()); - }; - - match command.as_str() { - ".help" => { - if parts.len() == 1 { - Ok(Command::Help) - } else { - Err("usage: .help".to_string()) - } - } - ".exit" | ".quit" => { - if parts.len() == 1 { - Ok(Command::Exit) - } else { - Err(format!("usage: {command}")) - } - } - ".load" => match parts.as_slice() { - [_, path] => Ok(Command::Load { path: path.clone() }), - _ => Err("usage: .load ".to_string()), - }, - ".open" => match parts.as_slice() { - [_, path] => Ok(Command::Open { path: path.clone() }), - _ => Err("usage: .open ".to_string()), - }, - ".save" => match parts.as_slice() { - [_, path] => Ok(Command::Save { path: path.clone() }), - _ => Err("usage: .save ".to_string()), - }, - ".tables" => { - if parts.len() == 1 { - Ok(Command::Tables) - } else { - Err("usage: .tables".to_string()) - } - } - ".rules" => { - if parts.len() == 1 { - Ok(Command::Rules) - } else { - Err("usage: .rules".to_string()) - } - } - ".schema" => match parts.as_slice() { - [_] => Ok(Command::Schema { table: None }), - [_, table] => Ok(Command::Schema { - table: Some(table.clone()), - }), - _ => Err("usage: .schema [table]".to_string()), - }, - ".dump" => match parts.as_slice() { - [_, table] => Ok(Command::Dump { - table: table.clone(), - }), - _ => Err("usage: .dump
".to_string()), - }, - _ => Err(format!( - "unknown meta command: {command}. Type `.help` for commands." - )), - } -} - pub fn parse_statement(input: &str) -> Result { let input = input.trim(); if input.starts_with("begin transact") { @@ -429,139 +329,10 @@ fn parse_row_id(raw: &str) -> Result { #[cfg(test)] mod tests { - use super::*; - #[test] - fn parses_help() { - assert_eq!(parse_command(".help").unwrap(), Command::Help); - } - #[test] fn valid_binding_name() { assert!(is_binding_ident("g0")); } - - #[test] - fn parses_load_with_quotes() { - assert_eq!( - parse_command(".load \"tests/data/paths.json\"").unwrap(), - Command::Load { - path: "tests/data/paths.json".to_string() - } - ); - } - - #[test] - fn parses_add_command() { - assert_eq!( - parse_command("add T values (7 \"alice\"), (8 \"bob\");").unwrap(), - Command::Add { - table: "T".to_string(), - rows: vec![ - vec!["7".to_string(), "alice".to_string()], - vec!["8".to_string(), "bob".to_string()], - ], - } - ); - } - - #[test] - fn parses_add_command_single_value() { - assert_eq!( - parse_command("add T values (#11);").unwrap(), - Command::Add { - table: "T".to_string(), - rows: vec![vec!["#11".to_string()],], - } - ); - } - - #[test] - fn rejects_unknown_command() { - let err = parse_command("wat;").unwrap_err(); - assert!(err.contains("unknown statement")); - } - - #[test] - fn rejects_missing_semicolon() { - let err = parse_command("help").unwrap_err(); - assert_eq!(err, "statements must end with `;`"); - } - - #[test] - fn parses_quit_without_semicolon() { - assert_eq!(parse_command(".quit").unwrap(), Command::Exit); - } - - #[test] - fn parses_meta_commands_without_semicolons() { - assert_eq!( - parse_command(".open paths.bin").unwrap(), - Command::Open { - path: "paths.bin".to_string() - } - ); - assert_eq!( - parse_command(".save paths.bin").unwrap(), - Command::Save { - path: "paths.bin".to_string() - } - ); - assert_eq!(parse_command(".tables").unwrap(), Command::Tables); - assert_eq!(parse_command(".rules").unwrap(), Command::Rules); - assert_eq!( - parse_command(".schema Path.G.V").unwrap(), - Command::Schema { - table: Some("Path.G.V".to_string()) - } - ); - assert_eq!( - parse_command(".dump Path.G.V").unwrap(), - Command::Dump { - table: "Path.G.V".to_string() - } - ); - } - - #[test] - fn parses_batch_empty() { - assert_eq!( - parse_command("begin transact; commit;").unwrap(), - Command::Batch { - assignments: vec![] - } - ); - } - - #[test] - fn parses_batch_with_bindings() { - let cmd = parse_command( - "begin transact; g = add Graphs values (); x = add G0 values (g); commit;", - ) - .unwrap(); - assert_eq!( - cmd, - Command::Batch { - assignments: vec![ - BatchAssignment { - name: "g".to_string(), - table: "Graphs".to_string(), - row: vec![], - }, - BatchAssignment { - name: "x".to_string(), - table: "G0".to_string(), - row: vec!["g".to_string()], - }, - ] - } - ); - } - - #[test] - fn rejects_batch_without_commit_keyword() { - let err = parse_command("begin transact; g = add T values (1);").unwrap_err(); - assert!(err.contains("transaction block must end with `commit`") || err.contains("commit")); - } } diff --git a/packages/coln-store/src/repl/parse/meta.rs b/packages/coln-store/src/repl/parse/meta.rs new file mode 100644 index 00000000..0efe98c2 --- /dev/null +++ b/packages/coln-store/src/repl/parse/meta.rs @@ -0,0 +1,78 @@ +#[derive(Debug, PartialEq, Eq)] +pub(crate) enum Command { + Help, + Load { path: String }, + Open { path: String }, + Schema { table: Option }, + Tables, + Rules, + Exit, + Dump { table: String }, + Save { path: String }, +} + +pub(crate) fn parse_meta_command(input: &str) -> Result { + let parts = shlex::split(input).ok_or_else(|| "could not parse input".to_string())?; + let Some(command) = parts.first() else { + return Err("empty command".to_string()); + }; + + match command.as_str() { + ".help" => { + if parts.len() == 1 { + Ok(Command::Help) + } else { + Err("usage: .help".to_string()) + } + } + ".exit" | ".quit" => { + if parts.len() == 1 { + Ok(Command::Exit) + } else { + Err(format!("usage: {command}")) + } + } + ".load" => match parts.as_slice() { + [_, path] => Ok(Command::Load { path: path.clone() }), + _ => Err("usage: .load ".to_string()), + }, + ".open" => match parts.as_slice() { + [_, path] => Ok(Command::Open { path: path.clone() }), + _ => Err("usage: .open ".to_string()), + }, + ".save" => match parts.as_slice() { + [_, path] => Ok(Command::Save { path: path.clone() }), + _ => Err("usage: .save ".to_string()), + }, + ".tables" => { + if parts.len() == 1 { + Ok(Command::Tables) + } else { + Err("usage: .tables".to_string()) + } + } + ".rules" => { + if parts.len() == 1 { + Ok(Command::Rules) + } else { + Err("usage: .rules".to_string()) + } + } + ".schema" => match parts.as_slice() { + [_] => Ok(Command::Schema { table: None }), + [_, table] => Ok(Command::Schema { + table: Some(table.clone()), + }), + _ => Err("usage: .schema [table]".to_string()), + }, + ".dump" => match parts.as_slice() { + [_, table] => Ok(Command::Dump { + table: table.clone(), + }), + _ => Err("usage: .dump
".to_string()), + }, + _ => Err(format!( + "unknown meta command: {command}. Type `.help` for commands." + )), + } +} diff --git a/packages/coln-store/src/repl/parse/mod.rs b/packages/coln-store/src/repl/parse/mod.rs new file mode 100644 index 00000000..fa26e64a --- /dev/null +++ b/packages/coln-store/src/repl/parse/mod.rs @@ -0,0 +1,182 @@ +use crate::repl::ShellMode; + +mod coln; +mod meta; +mod sql; + +pub use coln::{BatchAssignment, Command as ColnCommand}; +pub(crate) use coln::{parse_cell_value, parse_cell_value_batch}; +pub(crate) use meta::Command as MetaCommand; +pub(crate) use sql::Command as SqlCommand; + +#[derive(Debug, PartialEq, Eq)] +pub(crate) enum Command { + ColnCommand(ColnCommand), + SqlCommand(SqlCommand), + MetaCommand(MetaCommand), +} + +pub(crate) fn parse_command(mode: ShellMode, input: &str) -> Result { + let input = input.trim(); + if input.starts_with('.') { + meta::parse_meta_command(input).map(|mc| Command::MetaCommand(mc)) + } else { + let Some(input) = input.strip_suffix(';') else { + return Err("statements must end with `;`".to_string()); + }; + let input = input.trim_end(); + + match mode { + ShellMode::Coln => coln::parse_statement(input).map(|cc| Command::ColnCommand(cc)), + ShellMode::Sql => sql::parse_sql_statement(input).map(|sc| Command::SqlCommand(sc)), + } + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn parses_help() { + assert_eq!( + parse_command(ShellMode::Coln, ".help").unwrap(), + Command::MetaCommand(meta::Command::Help) + ); + } + + #[test] + fn parses_load_with_quotes() { + assert_eq!( + parse_command(ShellMode::Coln, ".load \"tests/data/paths.json\"").unwrap(), + Command::MetaCommand(MetaCommand::Load { + path: "tests/data/paths.json".to_string() + }) + ); + } + + #[test] + fn parses_add_command() { + assert_eq!( + parse_command(ShellMode::Coln, "add T values (7 \"alice\"), (8 \"bob\");").unwrap(), + Command::ColnCommand(ColnCommand::Add { + table: "T".to_string(), + rows: vec![ + vec!["7".to_string(), "alice".to_string()], + vec!["8".to_string(), "bob".to_string()], + ], + }) + ); + } + + #[test] + fn parses_add_command_single_value() { + assert_eq!( + parse_command(ShellMode::Coln, "add T values (#11);").unwrap(), + Command::ColnCommand(ColnCommand::Add { + table: "T".to_string(), + rows: vec![vec!["#11".to_string()],], + }) + ); + } + + #[test] + fn rejects_unknown_command() { + let err = parse_command(ShellMode::Coln, "wat;").unwrap_err(); + assert!(err.contains("unknown statement")); + } + + #[test] + fn rejects_missing_semicolon() { + let err = parse_command(ShellMode::Coln, "help").unwrap_err(); + assert_eq!(err, "statements must end with `;`"); + } + + #[test] + fn parses_quit_without_semicolon() { + assert_eq!( + parse_command(ShellMode::Coln, ".quit").unwrap(), + Command::MetaCommand(MetaCommand::Exit) + ); + } + + #[test] + fn parses_meta_commands_without_semicolons() { + assert_eq!( + parse_command(ShellMode::Coln, ".open paths.bin").unwrap(), + Command::MetaCommand(MetaCommand::Open { + path: "paths.bin".to_string() + }) + ); + assert_eq!( + parse_command(ShellMode::Coln, ".save paths.bin").unwrap(), + Command::MetaCommand(MetaCommand::Save { + path: "paths.bin".to_string() + }) + ); + assert_eq!( + parse_command(ShellMode::Coln, ".tables").unwrap(), + Command::MetaCommand(MetaCommand::Tables) + ); + assert_eq!( + parse_command(ShellMode::Coln, ".rules").unwrap(), + Command::MetaCommand(MetaCommand::Rules) + ); + assert_eq!( + parse_command(ShellMode::Coln, ".schema Path.G.V").unwrap(), + Command::MetaCommand(MetaCommand::Schema { + table: Some("Path.G.V".to_string()) + }) + ); + assert_eq!( + parse_command(ShellMode::Coln, ".dump Path.G.V").unwrap(), + Command::MetaCommand(MetaCommand::Dump { + table: "Path.G.V".to_string() + }) + ); + } + + #[test] + fn parses_batch_empty() { + assert_eq!( + parse_command(ShellMode::Coln, "begin transact; commit;").unwrap(), + Command::ColnCommand(ColnCommand::Batch { + assignments: vec![] + }) + ); + } + + #[test] + fn parses_batch_with_bindings() { + let cmd = parse_command( + ShellMode::Coln, + "begin transact; g = add Graphs values (); x = add G0 values (g); commit;", + ) + .unwrap(); + assert_eq!( + cmd, + Command::ColnCommand(ColnCommand::Batch { + assignments: vec![ + BatchAssignment { + name: "g".to_string(), + table: "Graphs".to_string(), + row: vec![], + }, + BatchAssignment { + name: "x".to_string(), + table: "G0".to_string(), + row: vec!["g".to_string()], + }, + ] + }) + ); + } + + #[test] + fn rejects_batch_without_commit_keyword() { + let err = + parse_command(ShellMode::Coln, "begin transact; g = add T values (1);").unwrap_err(); + assert!(err.contains("transaction block must end with `commit`") || err.contains("commit")); + } +} diff --git a/packages/coln-store/src/repl/parse/sql.rs b/packages/coln-store/src/repl/parse/sql.rs new file mode 100644 index 00000000..70466a1c --- /dev/null +++ b/packages/coln-store/src/repl/parse/sql.rs @@ -0,0 +1,15 @@ +use sqlparser::{dialect::GenericDialect, parser::Parser}; + +#[derive(Debug, PartialEq, Eq)] +pub(crate) enum Command { + CreateTable, +} + +pub(crate) fn parse_sql_statement(input: &str) -> Result { + let dialect = GenericDialect {}; + let ast= Parser::parse_sql(&dialect, input)?; + + + + Err(format!("unsupported SQL statement: {input}")) +} diff --git a/packages/coln-store/tests/test_repl.rs b/packages/coln-store/tests/test_repl.rs index b064f8df..e28b85fe 100644 --- a/packages/coln-store/tests/test_repl.rs +++ b/packages/coln-store/tests/test_repl.rs @@ -10,8 +10,9 @@ use std::path::PathBuf; use coln_flir_rs::ir::{FlatRealm, Path}; use coln_store::{ repl::{ + ShellMode, exe::run_transact, - parse::{Command, parse_command}, + parse::{ColnCommand, Command, parse_command}, }, store::Store, }; @@ -32,6 +33,7 @@ fn batch_block_matches_apply_batch_for_paths_fixture() { let mut store = Store::try_from_theory(theory).expect("valid theory"); let cmd = parse_command( + ShellMode::Coln, "begin transact; gid1 = add Path.Graphs values (); gid2 = add Path.Graphs values (); \ g0 = add Path.G0 values (gid2); g1 = add Path.G1 values (gid2); \ v1 = add Path.G.V values (gid1); v2 = add Path.G.V values (gid1); \ @@ -39,7 +41,7 @@ fn batch_block_matches_apply_batch_for_paths_fixture() { ) .expect("parse batch"); - let Command::Batch { assignments } = cmd else { + let Command::ColnCommand(ColnCommand::Batch { assignments }) = cmd else { panic!("expected Command::Batch"); }; From bda5eb4eca570424265debad9ad2897464e6dd78 Mon Sep 17 00:00:00 2001 From: Shuntian Liu Date: Wed, 1 Jul 2026 21:10:56 +0100 Subject: [PATCH 07/19] refactor: repl A bit of refactoring around the repl. But also introducing SQL mode! --- Cargo.lock | 1 + packages/coln-store/Cargo.toml | 1 + packages/coln-store/src/repl/cli.rs | 153 +++++++ packages/coln-store/src/repl/error.rs | 75 +--- .../src/repl/{exe.rs => exe/mod.rs} | 237 ++++++++--- packages/coln-store/src/repl/exe/sql.rs | 81 ++++ packages/coln-store/src/repl/mod.rs | 376 +++++------------- packages/coln-store/src/repl/parse/coln.rs | 57 ++- packages/coln-store/src/repl/parse/meta.rs | 28 +- packages/coln-store/src/repl/parse/mod.rs | 87 ++-- packages/coln-store/src/repl/parse/sql.rs | 107 ++++- packages/coln-store/src/solver/bind.rs | 11 +- packages/coln-store/src/solver/validate.rs | 49 +-- packages/coln-store/src/store/mod.rs | 49 ++- packages/coln-store/src/table.rs | 2 - packages/coln-store/src/txn/mod.rs | 62 +-- packages/coln-store/tests/test_repl.rs | 55 ++- 17 files changed, 861 insertions(+), 570 deletions(-) create mode 100644 packages/coln-store/src/repl/cli.rs rename packages/coln-store/src/repl/{exe.rs => exe/mod.rs} (56%) create mode 100644 packages/coln-store/src/repl/exe/sql.rs diff --git a/Cargo.lock b/Cargo.lock index 96fbe17a..d85200ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -908,6 +908,7 @@ dependencies = [ name = "coln-store" version = "0.1.0" dependencies = [ + "anyhow", "blake3", "coln-flir-rs", "future_form", diff --git a/packages/coln-store/Cargo.toml b/packages/coln-store/Cargo.toml index 8d5915bc..d5db15c5 100644 --- a/packages/coln-store/Cargo.toml +++ b/packages/coln-store/Cargo.toml @@ -18,6 +18,7 @@ exclude = [ ] [dependencies] +anyhow.workspace = true blake3 = "1.8.5" coln-flir-rs = { path = "../coln-flir-rs" } # coln-query = { path = "../coln-query" } diff --git a/packages/coln-store/src/repl/cli.rs b/packages/coln-store/src/repl/cli.rs new file mode 100644 index 00000000..dc17c142 --- /dev/null +++ b/packages/coln-store/src/repl/cli.rs @@ -0,0 +1,153 @@ +use anyhow::Result; +use rustyline::completion::{Completer, Pair}; +use rustyline::error::ReadlineError; +use rustyline::highlight::Highlighter; +use rustyline::hint::Hinter; +use rustyline::validate::Validator; +use rustyline::{Context, Helper}; + +use crate::repl::ShellMode; + +const COMMANDS: &[&str] = &[ + ".help", ".exit", ".quit", ".load", ".open", ".save", ".tables", ".rules", ".schema", ".dump", + "add", "begin", +]; + +pub(super) struct CommandHelper; + +pub(super) fn is_statement_start(input: &str) -> bool { + !input.trim_start().starts_with('.') +} + +pub(super) fn prompt(shell_mode: ShellMode, pending_statement: bool) -> &'static str { + if pending_statement { + "....> " + } else if shell_mode == ShellMode::Sql { + "coln-sql> " + } else { + "coln-store> " + } +} + +pub(super) fn push_statement_line(pending: &mut Option, line: &str) -> Option { + let pending_line = pending.get_or_insert_with(String::new); + if !pending_line.is_empty() { + pending_line.push(' '); + } + pending_line.push_str(line.trim()); + + let buf = pending_line.trim_start(); + if buf.starts_with("begin transact") { + if pending_line.trim_end().ends_with("commit;") { + return pending.take(); + } + return None; + } + + if pending_line.trim_end().ends_with(';') { + pending.take() + } else { + None + } +} + +impl Helper for CommandHelper {} + +impl Hinter for CommandHelper { + type Hint = String; +} + +impl Highlighter for CommandHelper {} + +impl Validator for CommandHelper {} + +impl Completer for CommandHelper { + type Candidate = Pair; + + fn complete( + &self, + line: &str, + pos: usize, + _ctx: &Context<'_>, + ) -> Result<(usize, Vec), ReadlineError> { + let (start, candidates) = complete_command(line, pos); + Ok((start, candidates)) + } +} + +fn complete_command(line: &str, pos: usize) -> (usize, Vec) { + let prefix = &line[..pos]; + if prefix.split_whitespace().count() > 1 || prefix.contains(' ') { + return (0, Vec::new()); + } + + let matches = COMMANDS + .iter() + .filter(|command| command.starts_with(prefix)) + .map(|command| Pair { + display: (*command).to_string(), + replacement: (*command).to_string(), + }) + .collect(); + + (0, matches) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn completes_command_prefix() { + let (start, matches) = complete_command(".q", 2); + assert_eq!(start, 0); + assert_eq!(matches.len(), 1); + assert_eq!(matches[0].replacement, ".quit"); + } + + #[test] + fn does_not_complete_after_first_argument() { + let (_, matches) = complete_command(".load te", ".load te".len()); + assert!(matches.is_empty()); + } + + #[test] + fn statement_buffer_waits_for_semicolon() { + let mut pending = None; + assert_eq!(push_statement_line(&mut pending, "add T 7"), None); + assert_eq!(pending.as_deref(), Some("add T 7")); + assert_eq!( + push_statement_line(&mut pending, "\"alice\";"), + Some("add T 7 \"alice\";".to_string()) + ); + assert_eq!(pending, None); + } + + #[test] + fn batch_buffer_waits_for_commit_semicolon() { + let mut pending = None; + assert_eq!(push_statement_line(&mut pending, "begin transact;"), None); + assert_eq!( + push_statement_line(&mut pending, "x = add T values (1);"), + None + ); + assert_eq!( + push_statement_line(&mut pending, "commit;"), + Some("begin transact; x = add T values (1); commit;".to_string()) + ); + assert_eq!(pending, None); + } + + #[test] + fn meta_command_is_not_statement_start() { + assert!(!is_statement_start(".help")); + assert!(is_statement_start("add T 7")); + } + + #[test] + fn prompt_reflects_mode_and_pending_statement() { + assert_eq!(prompt(ShellMode::Coln, false), "coln-store> "); + assert_eq!(prompt(ShellMode::Sql, false), "coln-sql>"); + assert_eq!(prompt(ShellMode::Sql, true), "....> "); + } +} diff --git a/packages/coln-store/src/repl/error.rs b/packages/coln-store/src/repl/error.rs index da520b04..6253a27e 100644 --- a/packages/coln-store/src/repl/error.rs +++ b/packages/coln-store/src/repl/error.rs @@ -4,24 +4,6 @@ use std::fmt; -use crate::commit::error::CodecError; -use crate::store::error::StoreIntError; -use crate::table::ValidationError; - -#[derive(Debug)] -pub enum ReplError { - NoSchemaLoaded, - UnknownTable(String), - UnknownBinding(String), - DuplicateBinding(String), - ColumnCountMismatch { expected: usize, got: usize }, - BadValue { column: usize, message: String }, - Io(std::io::Error), - Json(serde_json::Error), - Store(StoreIntError), - Persist(CodecError), -} - /// Parse failure for a single cell inside a `begin batch` block (before column index is known). #[derive(Debug, Clone, PartialEq, Eq)] pub enum BatchCellParseError { @@ -29,62 +11,13 @@ pub enum BatchCellParseError { InvalidValue(String), } -impl fmt::Display for ReplError { +impl fmt::Display for BatchCellParseError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - ReplError::NoSchemaLoaded => write!(f, "no schema loaded"), - ReplError::UnknownTable(table) => write!(f, "unknown table: {table}"), - ReplError::UnknownBinding(name) => write!(f, "unknown binding: {name}"), - ReplError::DuplicateBinding(name) => write!(f, "duplicate binding: {name}"), - ReplError::ColumnCountMismatch { expected, got } => { - write!(f, "column count mismatch: expected {expected}, got {got}") - } - ReplError::BadValue { column, message } => write!(f, "column {column}: {message}"), - ReplError::Io(err) => write!(f, "{err}"), - ReplError::Json(err) => write!(f, "{err}"), - ReplError::Store(err) => write!(f, "{err}"), - ReplError::Persist(err) => write!(f, "{err}"), + BatchCellParseError::UnknownBinding(name) => write!(f, "unknown binding: {name}"), + BatchCellParseError::InvalidValue(message) => write!(f, "{message}"), } } } -impl std::error::Error for ReplError {} - -impl From for ReplError { - fn from(value: std::io::Error) -> Self { - Self::Io(value) - } -} - -impl From for ReplError { - fn from(value: serde_json::Error) -> Self { - Self::Json(value) - } -} - -impl From for ReplError { - fn from(value: StoreIntError) -> Self { - Self::Store(value) - } -} - -impl From for ReplError { - fn from(value: CodecError) -> Self { - Self::Persist(value) - } -} - -impl From for ReplError { - fn from(value: ValidationError) -> Self { - Self::Store(value.into()) - } -} - -impl From for ReplError { - fn from(value: BatchCellParseError) -> Self { - match value { - BatchCellParseError::UnknownBinding(name) => Self::UnknownBinding(name), - BatchCellParseError::InvalidValue(message) => Self::BadValue { column: 0, message }, - } - } -} +impl std::error::Error for BatchCellParseError {} diff --git a/packages/coln-store/src/repl/exe.rs b/packages/coln-store/src/repl/exe/mod.rs similarity index 56% rename from packages/coln-store/src/repl/exe.rs rename to packages/coln-store/src/repl/exe/mod.rs index 7ae27586..91e511f7 100644 --- a/packages/coln-store/src/repl/exe.rs +++ b/packages/coln-store/src/repl/exe/mod.rs @@ -2,24 +2,166 @@ // // SPDX-License-Identifier: Apache-2.0 OR MIT +use anyhow::{Context, Result, anyhow, bail}; use std::{ collections::HashMap, fs, path::{Path, PathBuf}, }; +use crate::repl::{ + Session, Step, + error::BatchCellParseError, + parse::{BatchAssignment, parse_cell_value, parse_cell_value_batch}, + parse::{ColnCommand, MetaCommand, SqlCommand}, +}; use crate::{ - commit::pst::decode_store, + commit::pst::{decode_store, encode_store}, ir::{BuiltinTy, ColType, ColumnEntry, FlatRealm}, - repl::{ - error::ReplError, - parse::{BatchAssignment, parse_cell_value, parse_cell_value_batch}, - }, store::Store, table::{RowId, Table}, txn::ops::{TempRowId, TxnCellValue}, }; +mod sql; + +fn help_text() -> String { + [ + "Commands:", + " .help", + " .exit", + " .quit", + " .load ", + " .open ", + " .save ", + " .tables", + " .rules", + " .schema [table]", + " .dump
", + " add
values (...), (...);", + " begin transact; name = add
values (...); ... commit;", + "", + "Examples:", + " .help", + " .load tests/data/paths.json", + " .schema", + " .rules", + " .dump T", + " add T values (7 \"alice\"), (8 \"bob\");", + " begin transact; g = add Graphs values (); e = add G0 values (g); commit;", + ] + .join("\n") +} + +pub(super) fn execute_sql(session: &mut Session, command: SqlCommand) -> Result { + sql::execute_sql(session, command) +} + +pub(super) fn execute_meta(session: &mut Session, command: MetaCommand) -> Result { + match command { + MetaCommand::Help => Ok(Step::Continue(help_text())), + MetaCommand::Load { path } => { + let loaded = load_schema(std::path::Path::new(&path))?; + tracing::info!( + source = %loaded.schema.source.display(), + table_count = loaded.store.table_count(), + law_count = loaded.schema.law_count, + "loaded schema" + ); + let message = format!( + "loaded schema from {} (tables: {}, laws: {})", + loaded.schema.source.display(), + loaded.store.table_count(), + loaded.schema.law_count + ); + session.loaded = Some(loaded); + Ok(Step::Continue(message)) + } + MetaCommand::Open { path } => { + let loaded = load_store(std::path::Path::new(&path))?; + tracing::info!( + source = %loaded.schema.source.display(), + table_count = loaded.store.table_count(), + law_count = loaded.schema.law_count, + "loaded store" + ); + let message = format!( + "loaded store from {} (tables: {}, laws: {})", + loaded.schema.source.display(), + loaded.store.table_count(), + loaded.schema.law_count + ); + session.loaded = Some(loaded); + Ok(Step::Continue(message)) + } + MetaCommand::Schema { table } => { + let schema = session.loaded.as_ref().map(|loaded| &loaded.schema); + let message = match table { + Some(table) => render_table_schema(schema, &table)?, + None => render_schema_summary(schema), + }; + Ok(Step::Continue(message)) + } + MetaCommand::Rules => { + let store = session.loaded.as_ref().map(|loaded| &loaded.store); + Ok(Step::Continue(render_rules(store)?)) + } + MetaCommand::Dump { table } => { + let loaded = session + .loaded + .as_mut() + .ok_or_else(|| anyhow!("no schema loaded"))?; + let tbl = loaded + .store + .table_at(&crate::ir::Path::from(table.clone())) + .ok_or_else(|| anyhow!("unknown table: {table}"))?; + Ok(Step::Continue(tbl.dump())) + } + MetaCommand::Tables => { + let loaded = session + .loaded + .as_mut() + .ok_or_else(|| anyhow!("no schema loaded"))?; + Ok(Step::Continue(loaded.store.dump())) + } + MetaCommand::Save { path } => { + let loaded = session + .loaded + .as_mut() + .ok_or_else(|| anyhow!("no schema loaded"))?; + let bytes = encode_store(&loaded.store)?; + fs::write(&path, &bytes)?; + Ok(Step::Continue(format!("saved store to {path}"))) + } + MetaCommand::Exit => Ok(Step::Exit), + } +} + +pub(super) fn execute_coln(session: &mut Session, command: ColnCommand) -> Result { + match command { + ColnCommand::Add { table, rows } => { + let loaded = session + .loaded + .as_mut() + .ok_or_else(|| anyhow!("no schema loaded"))?; + let row_ids = add_rows(&mut loaded.store, &table, &rows)?; + let row_ids = row_ids + .into_iter() + .map(|row_id| format!("#{row_id}")) + .collect::>() + .join(", "); + Ok(format!("inserted into {table} rows [{row_ids}]")) + } + ColnCommand::Batch { assignments } => { + let loaded = session + .loaded + .as_mut() + .ok_or_else(|| anyhow!("no schema loaded"))?; + run_transact(&mut loaded.store, &assignments) + } + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct SchemaSummary { pub(crate) source: PathBuf, @@ -77,7 +219,7 @@ fn format_column(column: &ColumnEntry) -> String { } impl SchemaSummary { - fn from_store(source: PathBuf, store: &Store) -> Self { + pub(crate) fn from_store(source: PathBuf, store: &Store) -> Self { let mut tables: Vec = store .tables() .map(|(_, table)| TableSummary { @@ -127,16 +269,20 @@ impl SchemaSummary { } } -pub fn load_store(path: &Path) -> Result { - let bytes = fs::read(path)?; - let store = decode_store(&bytes)?; +pub fn load_store(path: &Path) -> Result { + let bytes = + fs::read(path).with_context(|| format!("failed to read store {}", path.display()))?; + let store = decode_store(&bytes) + .with_context(|| format!("failed to decode store {}", path.display()))?; let schema = SchemaSummary::from_store(path.to_path_buf(), &store); Ok(LoadedState { store, schema }) } -pub fn load_schema(path: &Path) -> Result { - let input = fs::read_to_string(path)?; - let theory: FlatRealm = serde_json::from_str(&input)?; +pub fn load_schema(path: &Path) -> Result { + let input = fs::read_to_string(path) + .with_context(|| format!("failed to read schema {}", path.display()))?; + let theory: FlatRealm = serde_json::from_str(&input) + .with_context(|| format!("failed to parse schema {}", path.display()))?; let summary = SchemaSummary::from_theory(path.to_path_buf(), &theory); let store = Store::try_from_theory(theory)?; Ok(LoadedState { @@ -175,22 +321,19 @@ fn render_table_schema_summary(table: &TableSummary) -> String { lines.join("\n") } -pub fn render_table_schema( - schema: Option<&SchemaSummary>, - table_name: &str, -) -> Result { - let schema = schema.ok_or(ReplError::NoSchemaLoaded)?; +pub fn render_table_schema(schema: Option<&SchemaSummary>, table_name: &str) -> Result { + let schema = schema.ok_or_else(|| anyhow!("no schema loaded"))?; let table = schema .tables .iter() .find(|table| table.path == table_name) - .ok_or_else(|| ReplError::UnknownTable(table_name.to_string()))?; + .ok_or_else(|| anyhow!("unknown table: {table_name}"))?; Ok(render_table_schema_summary(table)) } -pub fn render_rules(store: Option<&Store>) -> Result { - let store = store.ok_or(ReplError::NoSchemaLoaded)?; +pub fn render_rules(store: Option<&Store>) -> Result { + let store = store.ok_or_else(|| anyhow!("no schema loaded"))?; if store.rules().is_empty() { return Ok("no rules".to_string()); } @@ -207,15 +350,15 @@ pub fn add_rows( store: &mut Store, table_name: &str, raw_rows: &[Vec], -) -> Result, ReplError> { +) -> Result> { let table_path = crate::ir::Path::from(table_name); let oid = store .resolve_table(&table_path) - .ok_or_else(|| ReplError::UnknownTable(table_name.to_string()))?; + .ok_or_else(|| anyhow!("unknown table: {table_name}"))?; let table = store .table(oid) - .ok_or_else(|| ReplError::UnknownTable(table_name.to_string()))?; + .ok_or_else(|| anyhow!("unknown table: {table_name}"))?; let mut rows = Vec::new(); for raw_values in raw_rows { rows.push(parse_txn_values(table, raw_values)?); @@ -235,41 +378,38 @@ pub fn add_rows( } /// Parse and commit a batch transaction, allowing later rows to refer to earlier bindings. -pub fn run_transact( - store: &mut Store, - assignments: &[BatchAssignment], -) -> Result { +pub fn run_transact(store: &mut Store, assignments: &[BatchAssignment]) -> Result { let mut bindings: HashMap = HashMap::new(); let mut pending = Vec::new(); for (index, a) in assignments.iter().enumerate() { if bindings.contains_key(&a.name) { - return Err(ReplError::DuplicateBinding(a.name.clone())); + bail!("duplicate binding: {}", a.name); } let table_path = crate::ir::Path::from(a.table.as_str()); let table = store .table_at(&table_path) - .ok_or_else(|| ReplError::UnknownTable(a.table.clone()))?; + .ok_or_else(|| anyhow!("unknown table: {}", a.table))?; let expected = table.schema().columns.len(); if a.row.len() != expected { - return Err(ReplError::ColumnCountMismatch { - expected, - got: a.row.len(), - }); + bail!( + "column count mismatch: expected {expected}, got {}", + a.row.len() + ); } let mut values = Vec::with_capacity(expected); for (idx, column) in table.schema().columns.iter().enumerate() { let v = parse_cell_value_batch(&column.col_type, &a.row[idx], &bindings).map_err(|e| { - let err: ReplError = e.into(); - match err { - ReplError::BadValue { message, .. } => ReplError::BadValue { - column: idx, - message, - }, - other => other, + match e { + BatchCellParseError::UnknownBinding(name) => { + anyhow!("unknown binding: {name}") + } + BatchCellParseError::InvalidValue(message) => { + anyhow!("column {idx}: {message}") + } } })?; values.push(v); @@ -294,13 +434,13 @@ pub fn run_transact( Ok(message) } -fn parse_txn_values(table: &Table, raw_values: &[String]) -> Result, ReplError> { +fn parse_txn_values(table: &Table, raw_values: &[String]) -> Result> { let expected = table.schema().columns.len(); if raw_values.len() != expected { - return Err(ReplError::ColumnCountMismatch { - expected, - got: raw_values.len(), - }); + bail!( + "column count mismatch: expected {expected}, got {}", + raw_values.len() + ); } table @@ -308,14 +448,11 @@ fn parse_txn_values(table: &Table, raw_values: &[String]) -> Result Result { + .map(|(idx, column)| -> Result { let raw = &raw_values[idx]; parse_cell_value(&column.col_type, raw) .map(Into::into) - .map_err(|message| ReplError::BadValue { - column: idx, - message, - }) + .map_err(|message| anyhow!("column {idx}: {message}")) }) .collect() } diff --git a/packages/coln-store/src/repl/exe/sql.rs b/packages/coln-store/src/repl/exe/sql.rs new file mode 100644 index 00000000..3ed7b34c --- /dev/null +++ b/packages/coln-store/src/repl/exe/sql.rs @@ -0,0 +1,81 @@ +use anyhow::Result; +use std::path::PathBuf; + +use crate::ir::{BuiltinTy, ColType, ColumnEntry, EntityVariant, Path, Schema}; +use crate::repl::Session; +use crate::repl::exe::{LoadedState, SchemaSummary}; +use crate::repl::parse::{SqlCol as Col, SqlCommand as Command}; +use crate::store::Store; + +#[derive(Debug, thiserror::Error)] +enum SQLModeError { + #[error("table already exists: {table}")] + TableExists { table: String }, + #[error("cannot create table after data commits have been recorded")] + SchemaChangeAfterData, +} + +pub(super) fn execute_sql(session: &mut Session, command: Command) -> Result { + match command { + Command::CreateTable { + table_name, + columns, + } => create_sql_table(session, table_name, columns), + Command::Insert { .. } => Ok("SQL INSERT is not implemented yet".to_string()), + } +} + +fn create_sql_table( + session: &mut Session, + table_name: String, + columns: Vec, +) -> Result { + let loaded = session.loaded.get_or_insert_with(empty_sql_state); + let path = Path::from(table_name.as_str()); + let schema = sql_schema_from_cols(columns); + + if loaded.store.resolve_table(&path).is_some() { + return Err(SQLModeError::TableExists { table: table_name }.into()); + } + if loaded + .store + .commits() + .iter_topological() + .any(|commit| !commit.is_root()) + { + return Err(SQLModeError::SchemaChangeAfterData.into()); + } + + loaded.store.create_table(path, schema)?; + loaded.schema = SchemaSummary::from_store(loaded.schema.source.clone(), &loaded.store); + + Ok(format!("created table {table_name}")) +} + +fn empty_sql_state() -> LoadedState { + let store = Store::new(); + let source = PathBuf::from(""); + let schema = SchemaSummary::from_store(source, &store); + LoadedState { store, schema } +} + +fn sql_schema_from_cols(columns: Vec) -> Schema { + Schema { + entity_variant: EntityVariant::Table, + columns: columns + .into_iter() + .map(|column| ColumnEntry { + path: Path::from(column.col_name.as_str()), + col_type: match column.col_typ { + BuiltinTy::BuiltinInt => ColType::BuiltinTy { + builtin_ty: BuiltinTy::BuiltinInt, + }, + BuiltinTy::BuiltinStr => ColType::BuiltinTy { + builtin_ty: BuiltinTy::BuiltinStr, + }, + }, + }) + .collect(), + primary_key: None, + } +} diff --git a/packages/coln-store/src/repl/mod.rs b/packages/coln-store/src/repl/mod.rs index 16505037..c73c2dd2 100644 --- a/packages/coln-store/src/repl/mod.rs +++ b/packages/coln-store/src/repl/mod.rs @@ -2,34 +2,22 @@ // // SPDX-License-Identifier: Apache-2.0 OR MIT -use rustyline::completion::{Completer, Pair}; +use anyhow::Result; +use rustyline::Editor; use rustyline::error::ReadlineError; -use rustyline::highlight::Highlighter; -use rustyline::hint::Hinter; use rustyline::history::DefaultHistory; -use rustyline::validate::Validator; -use rustyline::{Context, Editor, Helper}; -use tracing::{info, warn}; - -use crate::commit::pst::encode_store; -use crate::ir::Path; -use crate::repl::error::ReplError; -use crate::repl::exe::{ - LoadedState, add_rows, load_schema, load_store, render_rules, render_schema_summary, - render_table_schema, run_transact, -}; +use tracing::warn; + +use crate::repl::cli::{CommandHelper, is_statement_start, prompt, push_statement_line}; +use crate::repl::exe::{LoadedState, execute_coln, execute_meta, execute_sql}; +use crate::repl::parse::Command; use crate::repl::parse::parse_command; -use crate::repl::parse::{ColnCommand, Command, MetaCommand, SqlCommand}; +mod cli; pub mod error; pub mod exe; pub mod parse; -const COMMANDS: &[&str] = &[ - ".help", ".exit", ".quit", ".load", ".open", ".save", ".tables", ".rules", ".schema", ".dump", - "add", "begin", -]; - const SECRET_MODE: &str = "ILOVESQL"; #[derive(Debug, Default, PartialEq, Eq, Copy, Clone)] @@ -51,12 +39,6 @@ enum Step { Exit, } -struct CommandHelper; - -fn is_statement_start(input: &str) -> bool { - !input.trim_start().starts_with('.') -} - pub fn run() -> Result<(), Box> { let mut editor = Editor::::new()?; editor.set_helper(Some(CommandHelper)); @@ -67,13 +49,7 @@ pub fn run() -> Result<(), Box> { println!("Type .help for commands."); loop { - let prompt = if pending_statement.is_some() { - "....> " - } else if session.shell_mode == ShellMode::Sql { - "coln-sql>" - } else { - "coln-store> " - }; + let prompt = prompt(session.shell_mode, pending_statement.is_some()); let line = match editor.readline(prompt) { Ok(line) => line, @@ -100,7 +76,7 @@ pub fn run() -> Result<(), Box> { && session.shell_mode == ShellMode::Coln { session.shell_mode = ShellMode::Sql; - println!("Welcome to SQL mode!"); + println!("Welcome to SQL mode! EXPERIMENTAL ONLY. "); continue; } @@ -133,191 +109,13 @@ pub fn run() -> Result<(), Box> { Ok(()) } -fn push_statement_line(pending: &mut Option, line: &str) -> Option { - let pending_line = pending.get_or_insert_with(String::new); - if !pending_line.is_empty() { - pending_line.push(' '); - } - pending_line.push_str(line.trim()); - - let buf = pending_line.trim_start(); - if buf.starts_with("begin transact") { - if pending_line.trim_end().ends_with("commit;") { - return pending.take(); - } - return None; - } - - if pending_line.trim_end().ends_with(';') { - pending.take() - } else { - None - } -} - -impl Helper for CommandHelper {} - -impl Hinter for CommandHelper { - type Hint = String; -} - -impl Highlighter for CommandHelper {} - -impl Validator for CommandHelper {} - -impl Completer for CommandHelper { - type Candidate = Pair; - - fn complete( - &self, - line: &str, - pos: usize, - _ctx: &Context<'_>, - ) -> Result<(usize, Vec), ReadlineError> { - let (start, candidates) = complete_command(line, pos); - Ok((start, candidates)) - } -} - -fn execute(session: &mut Session, command: Command) -> Result { +fn execute(session: &mut Session, command: Command) -> Result { match command { - Command::MetaCommand(MetaCommand::Help) => Ok(Step::Continue(help_text())), - Command::MetaCommand(MetaCommand::Load { path }) => { - let loaded = load_schema(std::path::Path::new(&path))?; - info!( - source = %loaded.schema.source.display(), - table_count = loaded.store.table_count(), - law_count = loaded.schema.law_count, - "loaded schema" - ); - let message = format!( - "loaded schema from {} (tables: {}, laws: {})", - loaded.schema.source.display(), - loaded.store.table_count(), - loaded.schema.law_count - ); - session.loaded = Some(loaded); - Ok(Step::Continue(message)) - } - Command::MetaCommand(MetaCommand::Open { path }) => { - let loaded = load_store(std::path::Path::new(&path))?; - info!( - source = %loaded.schema.source.display(), - table_count = loaded.store.table_count(), - law_count = loaded.schema.law_count, - "loaded store" - ); - let message = format!( - "loaded store from {} (tables: {}, laws: {})", - loaded.schema.source.display(), - loaded.store.table_count(), - loaded.schema.law_count - ); - session.loaded = Some(loaded); - Ok(Step::Continue(message)) - } - Command::MetaCommand(MetaCommand::Schema { table }) => { - let schema = session.loaded.as_ref().map(|loaded| &loaded.schema); - let message = match table { - Some(table) => render_table_schema(schema, &table)?, - None => render_schema_summary(schema), - }; - Ok(Step::Continue(message)) - } - Command::MetaCommand(MetaCommand::Rules) => { - let store = session.loaded.as_ref().map(|loaded| &loaded.store); - Ok(Step::Continue(render_rules(store)?)) - } - Command::ColnCommand(ColnCommand::Add { table, rows }) => { - let loaded = session.loaded.as_mut().ok_or(ReplError::NoSchemaLoaded)?; - let row_ids = add_rows(&mut loaded.store, &table, &rows)?; - let row_ids = row_ids - .into_iter() - .map(|row_id| format!("#{row_id}")) - .collect::>() - .join(", "); - Ok(Step::Continue(format!( - "inserted into {table} rows [{row_ids}]" - ))) - } - Command::ColnCommand(ColnCommand::Batch { assignments }) => { - let loaded = session.loaded.as_mut().ok_or(ReplError::NoSchemaLoaded)?; - let message = run_transact(&mut loaded.store, &assignments)?; - Ok(Step::Continue(message)) - } - Command::MetaCommand(MetaCommand::Dump { table }) => { - let loaded = session.loaded.as_mut().ok_or(ReplError::NoSchemaLoaded)?; - let tbl = loaded - .store - .table_at(&Path::from(table.clone())) - .ok_or(ReplError::UnknownTable(table))?; - let message = tbl.dump(); - Ok(Step::Continue(message)) - } - Command::MetaCommand(MetaCommand::Tables) => { - let loaded = session.loaded.as_mut().ok_or(ReplError::NoSchemaLoaded)?; - let message = loaded.store.dump(); - Ok(Step::Continue(message)) - } - Command::MetaCommand(MetaCommand::Save { path }) => { - let loaded = session.loaded.as_mut().ok_or(ReplError::NoSchemaLoaded)?; - let bytes = encode_store(&loaded.store)?; - std::fs::write(&path, &bytes)?; - Ok(Step::Continue(format!("saved store to {path}"))) - } - Command::MetaCommand(MetaCommand::Exit) => Ok(Step::Exit), - Command::SqlCommand(SqlCommand::CreateTable) => Ok(Step::Continue( - "SQL CREATE TABLE is not implemented yet".to_string(), - )), + Command::Meta(command) => execute_meta(session, command), + Command::Coln(command) => Ok(Step::Continue(execute_coln(session, command)?)), + Command::Sql(command) => Ok(Step::Continue(execute_sql(session, command)?)), } } - -fn help_text() -> String { - [ - "Commands:", - " .help", - " .exit", - " .quit", - " .load ", - " .open ", - " .save ", - " .tables", - " .rules", - " .schema [table]", - " .dump
", - " add
values (...), (...);", - " begin transact; name = add
values (...); ... commit;", - "", - "Examples:", - " .help", - " .load tests/data/paths.json", - " .schema", - " .rules", - " .dump T", - " add T values (7 \"alice\"), (8 \"bob\");", - " begin transact; g = add Graphs values (); e = add G0 values (g); commit;", - ] - .join("\n") -} - -fn complete_command(line: &str, pos: usize) -> (usize, Vec) { - let prefix = &line[..pos]; - if prefix.split_whitespace().count() > 1 || prefix.contains(' ') { - return (0, Vec::new()); - } - - let matches = COMMANDS - .iter() - .filter(|command| command.starts_with(prefix)) - .map(|command| Pair { - display: (*command).to_string(), - replacement: (*command).to_string(), - }) - .collect(); - - (0, matches) -} - #[cfg(test)] mod tests { use std::path::PathBuf; @@ -325,8 +123,9 @@ mod tests { use crate::{ ir::{BuiltinTy, ColType, ColumnEntry, EntityVariant}, repl::{ + exe::add_rows, exe::{PrimaryKeySummary, SchemaSummary, TableSummary}, - parse::{ColnCommand, Command}, + parse::{ColnCommand, Command, SqlCol, SqlCommand}, }, store::Store, }; @@ -335,7 +134,6 @@ mod tests { fn test_loaded_state() -> LoadedState { use crate::ir::{Path as IrPath, Schema}; - use crate::table::Table; let path = IrPath::from("T"); let schema = Schema { @@ -358,7 +156,9 @@ mod tests { }; let mut store = Store::new(); - store.insert_table(path.clone(), Table::new(path.clone(), schema)); + store + .create_table(path.clone(), schema) + .expect("create test table"); LoadedState { store, @@ -385,7 +185,7 @@ mod tests { let message = match execute( &mut session, - Command::ColnCommand(ColnCommand::Add { + Command::Coln(ColnCommand::Add { table: "T".to_string(), rows: vec![ vec!["7".to_string(), "alice".to_string()], @@ -417,7 +217,7 @@ mod tests { fn add_requires_loaded_schema() { let err = execute( &mut Session::default(), - Command::ColnCommand(ColnCommand::Add { + Command::Coln(ColnCommand::Add { table: "T".to_string(), rows: vec![vec!["7".to_string()]], }), @@ -427,13 +227,90 @@ mod tests { assert_eq!(err.to_string(), "no schema loaded"); } + fn sql_create_table_command(table_name: &str) -> Command { + Command::Sql(SqlCommand::CreateTable { + table_name: table_name.to_string(), + columns: vec![ + SqlCol { + col_name: "name".to_string(), + col_typ: BuiltinTy::BuiltinStr, + }, + SqlCol { + col_name: "age".to_string(), + col_typ: BuiltinTy::BuiltinInt, + }, + ], + }) + } + + #[test] + fn sql_create_table_registers_schema() { + let mut session = Session { + loaded: None, + shell_mode: ShellMode::Sql, + }; + + let message = match execute(&mut session, sql_create_table_command("Person")) + .expect("execute create table") + { + Step::Continue(message) => message, + Step::Exit => panic!("unexpected exit"), + }; + + assert_eq!(message, "created table Person"); + let loaded = session.loaded.as_ref().expect("sql store loaded"); + assert!(loaded.store.table_at(&"Person".parse().unwrap()).is_some()); + assert_eq!(loaded.schema.table_count, 1); + assert_eq!(loaded.schema.tables[0].path, "Person"); + assert_eq!( + loaded.schema.tables[0].columns, + vec!["name: string".to_string(), "age: int".to_string()] + ); + } + + #[test] + fn sql_create_table_rejects_duplicate_name() { + let mut session = Session { + loaded: None, + shell_mode: ShellMode::Sql, + }; + + execute(&mut session, sql_create_table_command("Person")).expect("first create"); + let err = execute(&mut session, sql_create_table_command("Person")).unwrap_err(); + + assert_eq!(err.to_string(), "table already exists: Person"); + } + + #[test] + fn sql_create_table_rejects_schema_change_after_data_commit() { + let mut session = Session { + loaded: None, + shell_mode: ShellMode::Sql, + }; + + execute(&mut session, sql_create_table_command("Person")).expect("create table"); + execute( + &mut session, + Command::Coln(ColnCommand::Add { + table: "Person".to_string(), + rows: vec![vec!["alice".to_string(), "7".to_string()]], + }), + ) + .expect("insert row"); + + let err = execute(&mut session, sql_create_table_command("Other")).unwrap_err(); + assert_eq!( + err.to_string(), + "cannot create table after data commits have been recorded" + ); + } + #[test] fn add_rejects_bad_entity_id() { let mut store = Store::new(); let path: crate::ir::Path = "Ref".parse().unwrap(); - store.insert_table( - path.clone(), - crate::table::Table::new( + store + .create_table( path, crate::ir::Schema { entity_variant: EntityVariant::Table, @@ -445,8 +322,8 @@ mod tests { }], primary_key: None, }, - ), - ); + ) + .expect("create test table"); let err = add_rows(&mut store, "Ref", &[vec!["7".to_string()]]).unwrap_err(); assert_eq!( @@ -454,51 +331,4 @@ mod tests { "column 0: expected entity id like #:" ); } - - #[test] - fn completes_command_prefix() { - let (start, matches) = complete_command(".q", 2); - assert_eq!(start, 0); - assert_eq!(matches.len(), 1); - assert_eq!(matches[0].replacement, ".quit"); - } - - #[test] - fn does_not_complete_after_first_argument() { - let (_, matches) = complete_command(".load te", ".load te".len()); - assert!(matches.is_empty()); - } - - #[test] - fn statement_buffer_waits_for_semicolon() { - let mut pending = None; - assert_eq!(push_statement_line(&mut pending, "add T 7"), None); - assert_eq!(pending.as_deref(), Some("add T 7")); - assert_eq!( - push_statement_line(&mut pending, "\"alice\";"), - Some("add T 7 \"alice\";".to_string()) - ); - assert_eq!(pending, None); - } - - #[test] - fn batch_buffer_waits_for_commit_semicolon() { - let mut pending = None; - assert_eq!(push_statement_line(&mut pending, "begin transact;"), None); - assert_eq!( - push_statement_line(&mut pending, "x = add T values (1);"), - None - ); - assert_eq!( - push_statement_line(&mut pending, "commit;"), - Some("begin transact; x = add T values (1); commit;".to_string()) - ); - assert_eq!(pending, None); - } - - #[test] - fn meta_command_is_not_statement_start() { - assert!(!is_statement_start(".help")); - assert!(is_statement_start("add T 7")); - } } diff --git a/packages/coln-store/src/repl/parse/coln.rs b/packages/coln-store/src/repl/parse/coln.rs index 24d31e15..6742283d 100644 --- a/packages/coln-store/src/repl/parse/coln.rs +++ b/packages/coln-store/src/repl/parse/coln.rs @@ -30,20 +30,20 @@ pub enum Command { Batch { assignments: Vec }, } -pub fn parse_add_statement(input: &str) -> Result { +pub fn parse_add_statement(input: &str) -> anyhow::Result { let Some(rest) = input.strip_prefix("add ") else { - return Err("usage: add
values (...), (...);".to_string()); + anyhow::bail!("usage: add
values (...), (...);"); }; let Some((table, rows_src)) = rest.split_once(" values ") else { - return Err("usage: add
values (...), (...);".to_string()); + anyhow::bail!("usage: add
values (...), (...);"); }; let table = table.trim(); if table.is_empty() { - return Err("usage: add
values (...), (...);".to_string()); + anyhow::bail!("usage: add
values (...), (...);"); } let rows = parse_add_rows(rows_src.trim())?; if rows.is_empty() { - return Err("add requires at least one row".to_string()); + anyhow::bail!("add requires at least one row"); } Ok(Command::Add { table: table.to_string(), @@ -51,22 +51,22 @@ pub fn parse_add_statement(input: &str) -> Result { }) } -pub fn parse_statement(input: &str) -> Result { +pub fn parse_statement(input: &str) -> anyhow::Result { let input = input.trim(); if input.starts_with("begin transact") { return parse_batch_block(input); } - let parts = shlex::split(input).ok_or_else(|| "could not parse input".to_string())?; + let parts = shlex::split(input).ok_or_else(|| anyhow::anyhow!("could not parse input"))?; let Some(command) = parts.first() else { - return Err("empty command".to_string()); + anyhow::bail!("empty command"); }; match command.as_str() { "add" => parse_add_statement(input), - _ => Err(format!( + _ => anyhow::bail!( "unknown statement: {command}. Statements must end with `;`, or use `.help` for meta commands." - )), + ), } } @@ -101,18 +101,18 @@ pub(crate) fn parse_cell_value_batch( } /// Parse `begin transact;` … `commit` (outer `;` already stripped by [`parse_command`]). -fn parse_batch_block(input: &str) -> Result { +fn parse_batch_block(input: &str) -> anyhow::Result { let input = input.trim(); let Some(rest) = input.strip_prefix("begin transact") else { - return Err("internal error: expected begin transact".to_string()); + anyhow::bail!("internal error: expected begin transact"); }; let mut rest = rest.trim_start(); let Some(after_kw) = rest.strip_prefix(';') else { - return Err("expected `begin transact;`".to_string()); + anyhow::bail!("expected `begin transact;`"); }; rest = after_kw.trim(); let Some(inner) = rest.strip_suffix("commit") else { - return Err("transaction block must end with `commit`".to_string()); + anyhow::bail!("transaction block must end with `commit`"); }; let inner = inner.trim().strip_suffix(';').unwrap_or(inner).trim(); let assignments = parse_batch_assignments(inner)?; @@ -155,7 +155,7 @@ fn split_semicolon_statements(s: &str) -> Vec { out } -fn parse_batch_assignments(inner: &str) -> Result, String> { +fn parse_batch_assignments(inner: &str) -> anyhow::Result> { let mut v = Vec::new(); for stmt in split_semicolon_statements(inner) { v.push(parse_batch_assignment(&stmt)?); @@ -163,35 +163,30 @@ fn parse_batch_assignments(inner: &str) -> Result, String> Ok(v) } -fn parse_batch_assignment(line: &str) -> Result { +fn parse_batch_assignment(line: &str) -> anyhow::Result { let line = line.trim(); if line.is_empty() { - return Err("empty statement inside batch block".to_string()); + anyhow::bail!("empty statement inside batch block"); } let Some((name, rhs)) = line.split_once(" = add ") else { - return Err(format!( - "expected `name = add
values (...)`, got: {line}" - )); + anyhow::bail!("expected `name = add
values (...)`, got: {line}"); }; let name = name.trim(); if name.is_empty() || !is_binding_ident(name) { - return Err(format!("invalid binding name: {name}")); + anyhow::bail!("invalid binding name: {name}"); } let rhs = rhs.trim(); let Some((table, rows_src)) = rhs.split_once(" values ") else { - return Err(format!( - "expected `values (...)` after table name in: {line}" - )); + anyhow::bail!("expected `values (...)` after table name in: {line}"); }; let table = table.trim(); if table.is_empty() { - return Err("missing table name".to_string()); + anyhow::bail!("missing table name"); } let rows = parse_add_rows(rows_src.trim())?; if rows.len() != 1 { - return Err( + anyhow::bail!( "each batch assignment must insert exactly one row (one `values (...)` group)" - .to_string(), ); } let row = rows.into_iter().next().expect("one row"); @@ -213,7 +208,7 @@ fn parse_batch_assignment(line: &str) -> Result { /// /// Regex is a poor fit here: a pattern like `\(([^)]*)\)` breaks when a string column contains /// `)`. -pub fn parse_add_rows(input: &str) -> Result>, String> { +pub fn parse_add_rows(input: &str) -> anyhow::Result>> { let mut rows = Vec::new(); let mut chars = input.char_indices().peekable(); @@ -223,7 +218,7 @@ pub fn parse_add_rows(input: &str) -> Result>, String> { continue; } if ch != '(' { - return Err("expected `(` to start a row".to_string()); + anyhow::bail!("expected `(` to start a row"); } chars.next(); @@ -249,7 +244,9 @@ pub fn parse_add_rows(input: &str) -> Result>, String> { } } - let end = end.ok_or_else(|| "unterminated row in add statement".to_string())?; + let Some(end) = end else { + anyhow::bail!("unterminated row in add statement"); + }; let row_src = &input[start..end]; let row = split_add_row_tokens(row_src); rows.push(row); diff --git a/packages/coln-store/src/repl/parse/meta.rs b/packages/coln-store/src/repl/parse/meta.rs index 0efe98c2..a8d2d561 100644 --- a/packages/coln-store/src/repl/parse/meta.rs +++ b/packages/coln-store/src/repl/parse/meta.rs @@ -11,10 +11,10 @@ pub(crate) enum Command { Save { path: String }, } -pub(crate) fn parse_meta_command(input: &str) -> Result { - let parts = shlex::split(input).ok_or_else(|| "could not parse input".to_string())?; +pub(crate) fn parse_meta_command(input: &str) -> anyhow::Result { + let parts = shlex::split(input).ok_or_else(|| anyhow::anyhow!("could not parse input"))?; let Some(command) = parts.first() else { - return Err("empty command".to_string()); + anyhow::bail!("empty command"); }; match command.as_str() { @@ -22,40 +22,40 @@ pub(crate) fn parse_meta_command(input: &str) -> Result { if parts.len() == 1 { Ok(Command::Help) } else { - Err("usage: .help".to_string()) + anyhow::bail!("usage: .help") } } ".exit" | ".quit" => { if parts.len() == 1 { Ok(Command::Exit) } else { - Err(format!("usage: {command}")) + anyhow::bail!("usage: {command}") } } ".load" => match parts.as_slice() { [_, path] => Ok(Command::Load { path: path.clone() }), - _ => Err("usage: .load ".to_string()), + _ => anyhow::bail!("usage: .load "), }, ".open" => match parts.as_slice() { [_, path] => Ok(Command::Open { path: path.clone() }), - _ => Err("usage: .open ".to_string()), + _ => anyhow::bail!("usage: .open "), }, ".save" => match parts.as_slice() { [_, path] => Ok(Command::Save { path: path.clone() }), - _ => Err("usage: .save ".to_string()), + _ => anyhow::bail!("usage: .save "), }, ".tables" => { if parts.len() == 1 { Ok(Command::Tables) } else { - Err("usage: .tables".to_string()) + anyhow::bail!("usage: .tables") } } ".rules" => { if parts.len() == 1 { Ok(Command::Rules) } else { - Err("usage: .rules".to_string()) + anyhow::bail!("usage: .rules") } } ".schema" => match parts.as_slice() { @@ -63,16 +63,14 @@ pub(crate) fn parse_meta_command(input: &str) -> Result { [_, table] => Ok(Command::Schema { table: Some(table.clone()), }), - _ => Err("usage: .schema [table]".to_string()), + _ => anyhow::bail!("usage: .schema [table]"), }, ".dump" => match parts.as_slice() { [_, table] => Ok(Command::Dump { table: table.clone(), }), - _ => Err("usage: .dump
".to_string()), + _ => anyhow::bail!("usage: .dump
"), }, - _ => Err(format!( - "unknown meta command: {command}. Type `.help` for commands." - )), + _ => anyhow::bail!("unknown meta command: {command}. Type `.help` for commands."), } } diff --git a/packages/coln-store/src/repl/parse/mod.rs b/packages/coln-store/src/repl/parse/mod.rs index fa26e64a..a007d739 100644 --- a/packages/coln-store/src/repl/parse/mod.rs +++ b/packages/coln-store/src/repl/parse/mod.rs @@ -1,34 +1,35 @@ use crate::repl::ShellMode; -mod coln; -mod meta; -mod sql; - +use anyhow::{Result, bail}; pub use coln::{BatchAssignment, Command as ColnCommand}; pub(crate) use coln::{parse_cell_value, parse_cell_value_batch}; pub(crate) use meta::Command as MetaCommand; -pub(crate) use sql::Command as SqlCommand; +pub(crate) use sql::{Col as SqlCol, Command as SqlCommand}; + +mod coln; +mod meta; +mod sql; #[derive(Debug, PartialEq, Eq)] pub(crate) enum Command { - ColnCommand(ColnCommand), - SqlCommand(SqlCommand), - MetaCommand(MetaCommand), + Coln(ColnCommand), + Sql(SqlCommand), + Meta(MetaCommand), } -pub(crate) fn parse_command(mode: ShellMode, input: &str) -> Result { +pub(crate) fn parse_command(mode: ShellMode, input: &str) -> Result { let input = input.trim(); if input.starts_with('.') { - meta::parse_meta_command(input).map(|mc| Command::MetaCommand(mc)) + meta::parse_meta_command(input).map(Command::Meta) } else { let Some(input) = input.strip_suffix(';') else { - return Err("statements must end with `;`".to_string()); + bail!("statements must end with `;`"); }; let input = input.trim_end(); match mode { - ShellMode::Coln => coln::parse_statement(input).map(|cc| Command::ColnCommand(cc)), - ShellMode::Sql => sql::parse_sql_statement(input).map(|sc| Command::SqlCommand(sc)), + ShellMode::Coln => coln::parse_statement(input).map(Command::Coln), + ShellMode::Sql => sql::parse_sql_statement(input).map(Command::Sql), } } } @@ -42,7 +43,7 @@ mod tests { fn parses_help() { assert_eq!( parse_command(ShellMode::Coln, ".help").unwrap(), - Command::MetaCommand(meta::Command::Help) + Command::Meta(meta::Command::Help) ); } @@ -50,7 +51,7 @@ mod tests { fn parses_load_with_quotes() { assert_eq!( parse_command(ShellMode::Coln, ".load \"tests/data/paths.json\"").unwrap(), - Command::MetaCommand(MetaCommand::Load { + Command::Meta(MetaCommand::Load { path: "tests/data/paths.json".to_string() }) ); @@ -60,7 +61,7 @@ mod tests { fn parses_add_command() { assert_eq!( parse_command(ShellMode::Coln, "add T values (7 \"alice\"), (8 \"bob\");").unwrap(), - Command::ColnCommand(ColnCommand::Add { + Command::Coln(ColnCommand::Add { table: "T".to_string(), rows: vec![ vec!["7".to_string(), "alice".to_string()], @@ -74,7 +75,7 @@ mod tests { fn parses_add_command_single_value() { assert_eq!( parse_command(ShellMode::Coln, "add T values (#11);").unwrap(), - Command::ColnCommand(ColnCommand::Add { + Command::Coln(ColnCommand::Add { table: "T".to_string(), rows: vec![vec!["#11".to_string()],], }) @@ -84,20 +85,20 @@ mod tests { #[test] fn rejects_unknown_command() { let err = parse_command(ShellMode::Coln, "wat;").unwrap_err(); - assert!(err.contains("unknown statement")); + assert!(err.to_string().contains("unknown statement")); } #[test] fn rejects_missing_semicolon() { let err = parse_command(ShellMode::Coln, "help").unwrap_err(); - assert_eq!(err, "statements must end with `;`"); + assert_eq!(err.to_string(), "statements must end with `;`"); } #[test] fn parses_quit_without_semicolon() { assert_eq!( parse_command(ShellMode::Coln, ".quit").unwrap(), - Command::MetaCommand(MetaCommand::Exit) + Command::Meta(MetaCommand::Exit) ); } @@ -105,33 +106,33 @@ mod tests { fn parses_meta_commands_without_semicolons() { assert_eq!( parse_command(ShellMode::Coln, ".open paths.bin").unwrap(), - Command::MetaCommand(MetaCommand::Open { + Command::Meta(MetaCommand::Open { path: "paths.bin".to_string() }) ); assert_eq!( parse_command(ShellMode::Coln, ".save paths.bin").unwrap(), - Command::MetaCommand(MetaCommand::Save { + Command::Meta(MetaCommand::Save { path: "paths.bin".to_string() }) ); assert_eq!( parse_command(ShellMode::Coln, ".tables").unwrap(), - Command::MetaCommand(MetaCommand::Tables) + Command::Meta(MetaCommand::Tables) ); assert_eq!( parse_command(ShellMode::Coln, ".rules").unwrap(), - Command::MetaCommand(MetaCommand::Rules) + Command::Meta(MetaCommand::Rules) ); assert_eq!( parse_command(ShellMode::Coln, ".schema Path.G.V").unwrap(), - Command::MetaCommand(MetaCommand::Schema { + Command::Meta(MetaCommand::Schema { table: Some("Path.G.V".to_string()) }) ); assert_eq!( parse_command(ShellMode::Coln, ".dump Path.G.V").unwrap(), - Command::MetaCommand(MetaCommand::Dump { + Command::Meta(MetaCommand::Dump { table: "Path.G.V".to_string() }) ); @@ -141,7 +142,7 @@ mod tests { fn parses_batch_empty() { assert_eq!( parse_command(ShellMode::Coln, "begin transact; commit;").unwrap(), - Command::ColnCommand(ColnCommand::Batch { + Command::Coln(ColnCommand::Batch { assignments: vec![] }) ); @@ -156,7 +157,7 @@ mod tests { .unwrap(); assert_eq!( cmd, - Command::ColnCommand(ColnCommand::Batch { + Command::Coln(ColnCommand::Batch { assignments: vec![ BatchAssignment { name: "g".to_string(), @@ -173,10 +174,38 @@ mod tests { ); } + #[test] + fn parses_sql_create_table() { + let cmd = parse_command( + ShellMode::Sql, + "create table Person (name text, age integer);", + ) + .unwrap(); + + let Command::Sql(SqlCommand::CreateTable { + table_name, + columns, + }) = cmd + else { + panic!("expected SQL create table"); + }; + + assert_eq!(table_name, "Person"); + assert_eq!(columns.len(), 2); + assert_eq!(columns[0].col_name, "name"); + assert_eq!(columns[0].col_typ, crate::ir::BuiltinTy::BuiltinStr); + assert_eq!(columns[1].col_name, "age"); + assert_eq!(columns[1].col_typ, crate::ir::BuiltinTy::BuiltinInt); + } + #[test] fn rejects_batch_without_commit_keyword() { let err = parse_command(ShellMode::Coln, "begin transact; g = add T values (1);").unwrap_err(); - assert!(err.contains("transaction block must end with `commit`") || err.contains("commit")); + assert!( + err.to_string() + .contains("transaction block must end with `commit`") + || err.to_string().contains("commit") + ); } } diff --git a/packages/coln-store/src/repl/parse/sql.rs b/packages/coln-store/src/repl/parse/sql.rs index 70466a1c..9a2fd1db 100644 --- a/packages/coln-store/src/repl/parse/sql.rs +++ b/packages/coln-store/src/repl/parse/sql.rs @@ -1,15 +1,112 @@ -use sqlparser::{dialect::GenericDialect, parser::Parser}; +use anyhow::{Context, Result, bail}; +use sqlparser::{ + ast::{DataType, Expr, SetExpr, Statement, TableObject, Value}, + dialect::GenericDialect, + parser::Parser, +}; + +#[derive(Debug, PartialEq, Eq)] +pub(crate) struct Col { + pub(crate) col_name: String, + pub(crate) col_typ: coln_flir_rs::ir::BuiltinTy, +} #[derive(Debug, PartialEq, Eq)] pub(crate) enum Command { - CreateTable, + CreateTable { + table_name: String, + columns: Vec, + }, + Insert { + table_name: String, + col_names: Vec, + values: Vec>, + }, } -pub(crate) fn parse_sql_statement(input: &str) -> Result { +pub(crate) fn parse_sql_statement(input: &str) -> Result { let dialect = GenericDialect {}; - let ast= Parser::parse_sql(&dialect, input)?; + let ast = Parser::parse_sql(&dialect, input).with_context(|| "failed to parse sql")?; + + // TODO support multi statement + let [statement] = ast.as_slice() else { + bail!("expected exactly one SQL statement"); + }; + if let Statement::CreateTable(sqlparser::ast::CreateTable { name, columns, .. }) = statement { + let mut cols = Vec::new(); + for col in columns { + let col_typ = match &col.data_type { + DataType::Int(_) | DataType::Integer(_) => coln_flir_rs::ir::BuiltinTy::BuiltinInt, + DataType::Text | DataType::String(_) | DataType::Varchar(_) => { + coln_flir_rs::ir::BuiltinTy::BuiltinStr + } + other => bail!("unsupported data type: {other}"), + }; + cols.push(Col { + col_name: col.name.to_string(), + col_typ, + }) + } + Ok(Command::CreateTable { + table_name: name.to_string(), + columns: cols, + }) + } else if let Statement::Insert(insert) = statement { + let TableObject::TableName(table) = &insert.table else { + bail!("unsupported INSERT target: {}", insert.table); + }; + if insert.columns.is_empty() { + bail!("INSERT must specify column names"); + } + if !insert.assignments.is_empty() { + bail!("INSERT assignments are not supported"); + } + if insert.returning.is_some() { + bail!("INSERT RETURNING is not supported"); + } + + let source = insert + .source + .as_ref() + .ok_or_else(|| anyhow::anyhow!("INSERT must provide VALUES"))?; + let SetExpr::Values(values) = source.body.as_ref() else { + bail!("only INSERT ... VALUES is supported"); + }; + + let col_names = insert + .columns + .iter() + .map(ToString::to_string) + .collect::>(); + let rows = values + .rows + .iter() + .map(|row| { + row.iter() + .map(sql_expr_to_raw_value) + .collect::>>() + }) + .collect::>>()?; + + Ok(Command::Insert { + table_name: table.to_string(), + col_names, + values: rows, + }) + } else { + bail!("Unsupported statements {}", input); + } +} +fn sql_expr_to_raw_value(expr: &Expr) -> Result { + let Expr::Value(value) = expr else { + bail!("only literal INSERT values are supported"); + }; - Err(format!("unsupported SQL statement: {input}")) + match &value.value { + Value::SingleQuotedString(value) | Value::DoubleQuotedString(value) => Ok(value.clone()), + Value::Number(value, _) => Ok(value.clone()), + other => bail!("unsupported INSERT value: {other}"), + } } diff --git a/packages/coln-store/src/solver/bind.rs b/packages/coln-store/src/solver/bind.rs index 6f65e7e8..f79f38f7 100644 --- a/packages/coln-store/src/solver/bind.rs +++ b/packages/coln-store/src/solver/bind.rs @@ -198,10 +198,9 @@ mod tests { fn bind_rule_joins_antecedent_atoms() { let path = Path::from("T"); let mut store = Store::new(); - store.insert_table( - path.clone(), - Table::new(path.clone(), int_schema(&["c0", "c1"])), - ); + store + .create_table(path.clone(), int_schema(&["c0", "c1"])) + .expect("create table"); let mut txn = store.transaction(); txn.add( @@ -284,7 +283,9 @@ mod tests { fn store_with_int_column(values: &[i64]) -> (Store, Path) { let path = Path::from("T"); let mut store = Store::new(); - store.insert_table(path.clone(), Table::new(path.clone(), int_schema(&["c0"]))); + store + .create_table(path.clone(), int_schema(&["c0"])) + .expect("create table"); let mut txn = store.transaction(); for value in values { txn.add(&path, vec![CellValue::Int(*value).into()]) diff --git a/packages/coln-store/src/solver/validate.rs b/packages/coln-store/src/solver/validate.rs index 608036fd..9577cdb2 100644 --- a/packages/coln-store/src/solver/validate.rs +++ b/packages/coln-store/src/solver/validate.rs @@ -154,7 +154,7 @@ mod tests { RuleVariant, Schema, }, solver::compile::compile_rule, - table::{CellValue, Table}, + table::CellValue, }; fn int_ty() -> ColType { @@ -202,10 +202,9 @@ mod tests { fn true_antecedent_still_checks_consequent() { let g0_path = Path::from("G0"); let mut store = Store::new(); - store.insert_table( - g0_path.clone(), - Table::new(g0_path.clone(), int_schema(&[])), - ); + store + .create_table(g0_path.clone(), int_schema(&[])) + .expect("create table"); let rule = enforced_rule( "G0.total", @@ -236,14 +235,12 @@ mod tests { let source = Path::from("Src"); let target = Path::from("Dst"); let mut store = Store::new(); - store.insert_table( - source.clone(), - Table::new(source.clone(), int_schema(&["c0"])), - ); - store.insert_table( - target.clone(), - Table::new(target.clone(), int_schema(&["c0"])), - ); + store + .create_table(source.clone(), int_schema(&["c0"])) + .expect("create source table"); + store + .create_table(target.clone(), int_schema(&["c0"])) + .expect("create target table"); let mut txn = store.transaction(); txn.add(&source, vec![CellValue::Int(7).into()]) @@ -287,15 +284,15 @@ mod tests { let right = Path::from("Right"); let link = Path::from("Link"); let mut store = Store::new(); - store.insert_table(left.clone(), Table::new(left.clone(), int_schema(&["c0"]))); - store.insert_table( - right.clone(), - Table::new(right.clone(), int_schema(&["c0"])), - ); - store.insert_table( - link.clone(), - Table::new(link.clone(), int_schema(&["c0", "c1"])), - ); + store + .create_table(left.clone(), int_schema(&["c0"])) + .expect("create left table"); + store + .create_table(right.clone(), int_schema(&["c0"])) + .expect("create right table"); + store + .create_table(link.clone(), int_schema(&["c0", "c1"])) + .expect("create link table"); let rule = enforced_rule( "Link.foreignKeys", @@ -378,7 +375,9 @@ mod tests { fn consequent_equality_holds_when_bindings_match() { let t = Path::from("T"); let mut store = Store::new(); - store.insert_table(t.clone(), Table::new(t.clone(), int_schema(&["c0", "c1"]))); + store + .create_table(t.clone(), int_schema(&["c0", "c1"])) + .expect("create table"); let mut txn = store.transaction(); txn.add(&t, vec![CellValue::Int(5).into(), CellValue::Int(5).into()]) .expect("insert row"); @@ -417,7 +416,9 @@ mod tests { fn consequent_equality_violation_surfaces_unsatisfied_eq() { let t = Path::from("T"); let mut store = Store::new(); - store.insert_table(t.clone(), Table::new(t.clone(), int_schema(&["c0", "c1"]))); + store + .create_table(t.clone(), int_schema(&["c0", "c1"])) + .expect("create table"); let mut txn = store.transaction(); txn.add(&t, vec![CellValue::Int(1).into(), CellValue::Int(2).into()]) .expect("insert row"); diff --git a/packages/coln-store/src/store/mod.rs b/packages/coln-store/src/store/mod.rs index 27c0a56e..b7b3d6e9 100644 --- a/packages/coln-store/src/store/mod.rs +++ b/packages/coln-store/src/store/mod.rs @@ -180,13 +180,17 @@ impl Store { Ok(graph) } - #[cfg(test)] - pub(crate) fn insert_table(&mut self, path: ir::Path, table: Table) -> TableOid { + pub(crate) fn create_table( + &mut self, + path: ir::Path, + schema: ir::Schema, + ) -> Result { let oid = self.next_oid; self.next_oid = self.next_oid.saturating_add(1); - self.path_to_oid.insert(path, oid); - self.tables.insert(oid, table); - oid + self.path_to_oid.insert(path.clone(), oid); + self.tables.insert(oid, Table::new(path, schema)); + self.commits = Self::root_commit_graph(&self.tables, &self.rule_entries)?; + Ok(oid) } /// Validates and appends a batch to this store. @@ -667,7 +671,7 @@ mod tests { primary_key: None, }; let mut store = Store::new(); - store.insert_table(path.clone(), Table::new(path, schema)); + store.create_table(path, schema).expect("create test table"); store } @@ -689,10 +693,10 @@ mod tests { }], primary_key: None, }; - let table = Table::new(path.clone(), schema); - let mut store = Store::new(); - let oid0 = store.insert_table(path.clone(), table); + let oid0 = store + .create_table(path.clone(), schema) + .expect("create table"); assert_eq!(oid0, 0); let t = store.table(oid0).expect("table at oid 0"); @@ -710,10 +714,9 @@ mod tests { }], primary_key: None, }; - let oid1 = store.insert_table( - Path::from("Other"), - Table::new(Path::from("table2"), schema2), - ); + let oid1 = store + .create_table(Path::from("Other"), schema2) + .expect("create second table"); assert_eq!(oid1, 1); } @@ -730,7 +733,9 @@ mod tests { }; let mut store = Store::new(); - let oid = store.insert_table(path.clone(), Table::new(path.clone(), schema)); + let oid = store + .create_table(path.clone(), schema) + .expect("create table"); assert_eq!(store.resolve_table(&path), Some(oid)); assert_eq!(store.resolve_table(&Path::from("missing")), None); @@ -750,7 +755,9 @@ mod tests { primary_key: None, }; let mut store = Store::new(); - store.insert_table(path.clone(), Table::new(path.clone(), schema)); + store + .create_table(path.clone(), schema) + .expect("create table"); let mut txn = store.transaction(); txn.add(&path, vec![CellValue::Int(1).into()]) @@ -779,7 +786,9 @@ mod tests { primary_key: None, }; let mut store = Store::new(); - store.insert_table(path.clone(), Table::new(path.clone(), schema)); + store + .create_table(path.clone(), schema) + .expect("create table"); let err = { let mut txn = store.transaction(); @@ -810,7 +819,9 @@ mod tests { primary_key: Some(vec![Path::from("c0")]), }; let mut store = Store::new(); - store.insert_table(path.clone(), Table::new(path.clone(), schema)); + store + .create_table(path.clone(), schema) + .expect("create table"); let mut txn = store.transaction(); txn.add(&path, vec![CellValue::Int(1).into()]) @@ -840,7 +851,9 @@ mod tests { primary_key: None, }; let mut store = Store::new(); - store.insert_table(path.clone(), Table::new(path.clone(), schema)); + store + .create_table(path.clone(), schema) + .expect("create table"); let mut txn = store.transaction(); txn.add(&path, vec![CellValue::Int(42).into()]) diff --git a/packages/coln-store/src/table.rs b/packages/coln-store/src/table.rs index d29b3ff5..f8bef1f0 100644 --- a/packages/coln-store/src/table.rs +++ b/packages/coln-store/src/table.rs @@ -40,8 +40,6 @@ pub enum ValidationError { expected: CellKind, got: CellKind, }, - #[error("tuple columns are not supported yet (column {column})")] - UnsupportedTuple { column: usize }, #[error("primary key references unknown column: {name}")] InvalidPrimaryKeyName { name: ColName }, #[error("duplicate primary key")] diff --git a/packages/coln-store/src/txn/mod.rs b/packages/coln-store/src/txn/mod.rs index 0e88c3e9..f6e23152 100644 --- a/packages/coln-store/src/txn/mod.rs +++ b/packages/coln-store/src/txn/mod.rs @@ -96,7 +96,7 @@ mod tests { use super::*; use crate::ir::{BuiltinTy, ColType, ColumnEntry, EntityVariant, Path, Schema}; use crate::store::test_support::link_foreign_key_theory; - use crate::table::{CellValue, Table, ValidationError}; + use crate::table::{CellValue, ValidationError}; fn table_schema(columns: Vec, primary_key: Option>) -> Schema { Schema { @@ -127,7 +127,9 @@ mod tests { let path = Path::from("T"); let schema = table_schema(vec![int_col("c0")], None); let mut store = Store::new(); - store.insert_table(path.clone(), Table::new(path.clone(), schema)); + store + .create_table(path.clone(), schema) + .expect("create table"); let mut tx = OwnedTransaction::new(store); tx.add(&path, vec![42_i64.into()]).expect("add"); @@ -141,7 +143,9 @@ mod tests { let path = Path::from("T"); let schema = table_schema(vec![int_col("c0")], None); let mut store = Store::new(); - store.insert_table(path.clone(), Table::new(path.clone(), schema)); + store + .create_table(path.clone(), schema) + .expect("create table"); let mut tx = OwnedTransaction::new(store); let err = tx @@ -179,17 +183,15 @@ mod tests { let nodes = Path::from("Nodes"); let edges = Path::from("Edges"); let mut store = Store::new(); - store.insert_table( - nodes.clone(), - Table::new(nodes.clone(), table_schema(vec![], None)), - ); - store.insert_table( - edges.clone(), - Table::new( + store + .create_table(nodes.clone(), table_schema(vec![], None)) + .expect("create nodes table"); + store + .create_table( edges.clone(), table_schema(vec![row_id_col("node", nodes.clone())], None), - ), - ); + ) + .expect("create edges table"); let mut tx = store.transaction(); let node_temp = tx.add(&nodes, vec![]).expect("add node"); @@ -216,17 +218,15 @@ mod tests { let nodes = Path::from("Nodes"); let edges = Path::from("Edges"); let mut store = Store::new(); - store.insert_table( - nodes.clone(), - Table::new(nodes.clone(), table_schema(vec![], None)), - ); - store.insert_table( - edges.clone(), - Table::new( + store + .create_table(nodes.clone(), table_schema(vec![], None)) + .expect("create nodes table"); + store + .create_table( edges.clone(), table_schema(vec![row_id_col("node", nodes.clone())], None), - ), - ); + ) + .expect("create edges table"); let mut tx = store.transaction(); let node = tx.add(&nodes, vec![]).expect("add node"); @@ -248,17 +248,15 @@ mod tests { let nodes = Path::from("Nodes"); let edges = Path::from("Edges"); let mut store = Store::new(); - store.insert_table( - nodes.clone(), - Table::new(nodes.clone(), table_schema(vec![], Some(vec![]))), - ); - store.insert_table( - edges.clone(), - Table::new( + store + .create_table(nodes.clone(), table_schema(vec![], Some(vec![]))) + .expect("create nodes table"); + store + .create_table( edges.clone(), table_schema(vec![row_id_col("node", nodes.clone())], None), - ), - ); + ) + .expect("create edges table"); let mut tx = store.transaction(); let node = tx.add(&nodes, vec![]).expect("add first node"); @@ -292,7 +290,9 @@ mod tests { let path = Path::from("T"); let schema = table_schema(vec![int_col("c0")], None); let mut store = Store::new(); - store.insert_table(path.clone(), Table::new(path.clone(), schema)); + store + .create_table(path.clone(), schema) + .expect("create table"); let root = store.commits().root_commit().expect("root commit").hash(); let mut tx = store.transaction(); diff --git a/packages/coln-store/tests/test_repl.rs b/packages/coln-store/tests/test_repl.rs index e28b85fe..bf3e364b 100644 --- a/packages/coln-store/tests/test_repl.rs +++ b/packages/coln-store/tests/test_repl.rs @@ -9,11 +9,7 @@ use std::path::PathBuf; use coln_flir_rs::ir::{FlatRealm, Path}; use coln_store::{ - repl::{ - ShellMode, - exe::run_transact, - parse::{ColnCommand, Command, parse_command}, - }, + repl::{exe::run_transact, parse::BatchAssignment}, store::Store, }; @@ -32,18 +28,43 @@ fn batch_block_matches_apply_batch_for_paths_fixture() { let theory = fixture_theory(PATHS_IR); let mut store = Store::try_from_theory(theory).expect("valid theory"); - let cmd = parse_command( - ShellMode::Coln, - "begin transact; gid1 = add Path.Graphs values (); gid2 = add Path.Graphs values (); \ - g0 = add Path.G0 values (gid2); g1 = add Path.G1 values (gid2); \ - v1 = add Path.G.V values (gid1); v2 = add Path.G.V values (gid1); \ - ge = add Path.G.E values (gid1 v1 v2); commit;", - ) - .expect("parse batch"); - - let Command::ColnCommand(ColnCommand::Batch { assignments }) = cmd else { - panic!("expected Command::Batch"); - }; + let assignments = vec![ + BatchAssignment { + name: "gid1".to_string(), + table: "Path.Graphs".to_string(), + row: vec![], + }, + BatchAssignment { + name: "gid2".to_string(), + table: "Path.Graphs".to_string(), + row: vec![], + }, + BatchAssignment { + name: "g0".to_string(), + table: "Path.G0".to_string(), + row: vec!["gid2".to_string()], + }, + BatchAssignment { + name: "g1".to_string(), + table: "Path.G1".to_string(), + row: vec!["gid2".to_string()], + }, + BatchAssignment { + name: "v1".to_string(), + table: "Path.G.V".to_string(), + row: vec!["gid1".to_string()], + }, + BatchAssignment { + name: "v2".to_string(), + table: "Path.G.V".to_string(), + row: vec!["gid1".to_string()], + }, + BatchAssignment { + name: "ge".to_string(), + table: "Path.G.E".to_string(), + row: vec!["gid1".to_string(), "v1".to_string(), "v2".to_string()], + }, + ]; let msg = run_transact(&mut store, &assignments).expect("run batch"); assert!(msg.contains("gid1=#"), "expected binding summary: {msg}"); From f6440b78cf8f2ab31ef55c1fa96c9168a94aa54b Mon Sep 17 00:00:00 2001 From: Shuntian Liu Date: Wed, 1 Jul 2026 21:15:16 +0100 Subject: [PATCH 08/19] refactor: reorganise imports --- packages/coln-store/src/commit/mod.rs | 3 ++- packages/coln-store/src/commit/pst.rs | 3 ++- packages/coln-store/src/commit/utils.rs | 3 ++- packages/coln-store/src/commit/wire/data.rs | 3 ++- packages/coln-store/src/repl/cli.rs | 2 +- packages/coln-store/src/repl/exe/mod.rs | 3 ++- packages/coln-store/src/repl/exe/sql.rs | 3 ++- packages/coln-store/src/repl/mod.rs | 3 +-- packages/coln-store/src/repl/parse/mod.rs | 4 ++-- packages/coln-store/src/roweq/rowing.rs | 3 ++- packages/coln-store/src/solver/validate.rs | 1 + packages/coln-store/src/txn/inner.rs | 3 ++- packages/coln-store/src/txn/mod.rs | 5 ++--- 13 files changed, 23 insertions(+), 16 deletions(-) diff --git a/packages/coln-store/src/commit/mod.rs b/packages/coln-store/src/commit/mod.rs index 7504c1f3..55271723 100644 --- a/packages/coln-store/src/commit/mod.rs +++ b/packages/coln-store/src/commit/mod.rs @@ -203,6 +203,8 @@ fn collect_op_hashes(pending: &[PendingOp], hash_mapper: &mut HashMapper) { #[cfg(test)] mod tests { + use std::sync::LazyLock; + use super::*; use crate::commit::chunk::{Chunk, hash}; use crate::commit::hash::HASH_SIZE; @@ -210,7 +212,6 @@ mod tests { use crate::ir::{BuiltinTy, ColType, ColumnEntry, EntityVariant, Path}; use crate::table::RowId; use crate::txn::ops::{RowRef, TempRowId}; - use std::sync::LazyLock; fn zero_hash() -> CommitHash { CommitHash([0u8; HASH_SIZE]) diff --git a/packages/coln-store/src/commit/pst.rs b/packages/coln-store/src/commit/pst.rs index ecb83a1e..bf3fe209 100644 --- a/packages/coln-store/src/commit/pst.rs +++ b/packages/coln-store/src/commit/pst.rs @@ -125,6 +125,8 @@ fn decode_store_chunks(next_oid: TableOid, chunks: Vec) -> Result Schema { Schema { diff --git a/packages/coln-store/src/commit/utils.rs b/packages/coln-store/src/commit/utils.rs index 0f08f019..906b7a10 100644 --- a/packages/coln-store/src/commit/utils.rs +++ b/packages/coln-store/src/commit/utils.rs @@ -2,9 +2,10 @@ // // SPDX-License-Identifier: Apache-2.0 OR MIT -use crate::commit::error::CodecError; use std::io::Write; +use crate::commit::error::CodecError; + // ── Write helpers ─────────────────────────────────────────────────────────── pub(crate) fn write_u8(buf: &mut Vec, value: u8) -> Result<(), CodecError> { diff --git a/packages/coln-store/src/commit/wire/data.rs b/packages/coln-store/src/commit/wire/data.rs index bd79fae8..56dadb09 100644 --- a/packages/coln-store/src/commit/wire/data.rs +++ b/packages/coln-store/src/commit/wire/data.rs @@ -2,9 +2,10 @@ // // SPDX-License-Identifier: Apache-2.0 OR MIT -use hexane::v1::{Column, DeltaColumn}; use std::io::Write; +use hexane::v1::{Column, DeltaColumn}; + use crate::commit::author::Author; use crate::commit::leb128 as commit_leb128; use crate::commit::wire::prim::{ diff --git a/packages/coln-store/src/repl/cli.rs b/packages/coln-store/src/repl/cli.rs index dc17c142..f3ab1949 100644 --- a/packages/coln-store/src/repl/cli.rs +++ b/packages/coln-store/src/repl/cli.rs @@ -147,7 +147,7 @@ mod tests { #[test] fn prompt_reflects_mode_and_pending_statement() { assert_eq!(prompt(ShellMode::Coln, false), "coln-store> "); - assert_eq!(prompt(ShellMode::Sql, false), "coln-sql>"); + assert_eq!(prompt(ShellMode::Sql, false), "coln-sql> "); assert_eq!(prompt(ShellMode::Sql, true), "....> "); } } diff --git a/packages/coln-store/src/repl/exe/mod.rs b/packages/coln-store/src/repl/exe/mod.rs index 91e511f7..0d6a1857 100644 --- a/packages/coln-store/src/repl/exe/mod.rs +++ b/packages/coln-store/src/repl/exe/mod.rs @@ -2,13 +2,14 @@ // // SPDX-License-Identifier: Apache-2.0 OR MIT -use anyhow::{Context, Result, anyhow, bail}; use std::{ collections::HashMap, fs, path::{Path, PathBuf}, }; +use anyhow::{Context, Result, anyhow, bail}; + use crate::repl::{ Session, Step, error::BatchCellParseError, diff --git a/packages/coln-store/src/repl/exe/sql.rs b/packages/coln-store/src/repl/exe/sql.rs index 3ed7b34c..463d615d 100644 --- a/packages/coln-store/src/repl/exe/sql.rs +++ b/packages/coln-store/src/repl/exe/sql.rs @@ -1,6 +1,7 @@ -use anyhow::Result; use std::path::PathBuf; +use anyhow::Result; + use crate::ir::{BuiltinTy, ColType, ColumnEntry, EntityVariant, Path, Schema}; use crate::repl::Session; use crate::repl::exe::{LoadedState, SchemaSummary}; diff --git a/packages/coln-store/src/repl/mod.rs b/packages/coln-store/src/repl/mod.rs index c73c2dd2..9811850a 100644 --- a/packages/coln-store/src/repl/mod.rs +++ b/packages/coln-store/src/repl/mod.rs @@ -120,6 +120,7 @@ fn execute(session: &mut Session, command: Command) -> Result { mod tests { use std::path::PathBuf; + use super::*; use crate::{ ir::{BuiltinTy, ColType, ColumnEntry, EntityVariant}, repl::{ @@ -130,8 +131,6 @@ mod tests { store::Store, }; - use super::*; - fn test_loaded_state() -> LoadedState { use crate::ir::{Path as IrPath, Schema}; diff --git a/packages/coln-store/src/repl/parse/mod.rs b/packages/coln-store/src/repl/parse/mod.rs index a007d739..a69d27ca 100644 --- a/packages/coln-store/src/repl/parse/mod.rs +++ b/packages/coln-store/src/repl/parse/mod.rs @@ -1,11 +1,11 @@ -use crate::repl::ShellMode; - use anyhow::{Result, bail}; pub use coln::{BatchAssignment, Command as ColnCommand}; pub(crate) use coln::{parse_cell_value, parse_cell_value_batch}; pub(crate) use meta::Command as MetaCommand; pub(crate) use sql::{Col as SqlCol, Command as SqlCommand}; +use crate::repl::ShellMode; + mod coln; mod meta; mod sql; diff --git a/packages/coln-store/src/roweq/rowing.rs b/packages/coln-store/src/roweq/rowing.rs index 9f6b699b..ce22c383 100644 --- a/packages/coln-store/src/roweq/rowing.rs +++ b/packages/coln-store/src/roweq/rowing.rs @@ -6,9 +6,10 @@ // at each value of the row one by one. And it sounds cool. // Anyway way it will probably be renamed in the future. +use std::collections::HashMap; + use coln_flir_rs::ir; use petgraph::unionfind::UnionFind; -use std::collections::HashMap; use crate::{ roweq::ObservedOutcome::{self, KeptOld}, diff --git a/packages/coln-store/src/solver/validate.rs b/packages/coln-store/src/solver/validate.rs index 9577cdb2..f3c7b863 100644 --- a/packages/coln-store/src/solver/validate.rs +++ b/packages/coln-store/src/solver/validate.rs @@ -4,6 +4,7 @@ use std::error::Error; use std::fmt; + use tracing::debug; use crate::{ diff --git a/packages/coln-store/src/txn/inner.rs b/packages/coln-store/src/txn/inner.rs index cd8ed715..cf26fd58 100644 --- a/packages/coln-store/src/txn/inner.rs +++ b/packages/coln-store/src/txn/inner.rs @@ -2,8 +2,9 @@ // // SPDX-License-Identifier: Apache-2.0 OR MIT -use coln_flir_rs::ir; use std::sync::atomic::{AtomicU64, Ordering}; + +use coln_flir_rs::ir; use tracing::info; use crate::{ diff --git a/packages/coln-store/src/txn/mod.rs b/packages/coln-store/src/txn/mod.rs index f6e23152..f6d16792 100644 --- a/packages/coln-store/src/txn/mod.rs +++ b/packages/coln-store/src/txn/mod.rs @@ -4,15 +4,14 @@ use coln_flir_rs::ir; +#[cfg(feature = "native")] +use crate::txn::ops::{TempRowId, TxnCellValue}; use crate::{ commit::hash::CommitHash, store::{Store, error::StoreIntError}, txn::ops::{RowHandle, TxnValue}, }; -#[cfg(feature = "native")] -use crate::txn::ops::{TempRowId, TxnCellValue}; - mod inner; pub mod ops; mod timestamp; From 2bea9802a2a186240eeae0bd7d9afba39705dd6d Mon Sep 17 00:00:00 2001 From: Shuntian Liu Date: Wed, 1 Jul 2026 21:31:20 +0100 Subject: [PATCH 09/19] ai: ask agent to run just coln-store tests --- packages/coln-store/AGENTS.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/coln-store/AGENTS.md b/packages/coln-store/AGENTS.md index 1bcb9258..935f3f5a 100644 --- a/packages/coln-store/AGENTS.md +++ b/packages/coln-store/AGENTS.md @@ -95,9 +95,9 @@ Use this sequence for your first change: 1. Read `src/lib.rs`, and the relevant module files. 2. Implement the smallest possible code change. 3. Add or update tests that fail before and pass after. -4. Run `cargo test --workspace --all-targets`. -5. Run `cargo clippy --workspace --all-targets --all-features -- -D warnings`. -6. Run `cargo fmt --all --check`. +4. Run `cargo test -p coln-store --all-targets`. +5. Run `cargo clippy -p coln-store --all-targets --all-features -- -D warnings`. +6. Run `cargo fmt -p coln-store --check`. 7. Update docs if public API or user-facing behavior changed. Example scopes that are good first tasks: @@ -135,9 +135,9 @@ Before coding: Before submitting: -1. Passing `cargo test --workspace --all-targets`. -2. Passing `cargo clippy --workspace --all-targets --all-features -- -D warnings`. -3. Passing `cargo fmt --all --check`. +1. Passing `cargo test -p coln-store --all-targets`. +2. Passing `cargo clippy -p coln-store --all-targets --all-features -- -D warnings`. +3. Passing `cargo fmt -p coln-store --check`. 4. Tests added or updated where behavior changed. 5. Docs updated where public behavior changed. @@ -184,6 +184,6 @@ Use this review format: Suggested PR checklist: - [ ] Tests added or updated for behavior changes -- [ ] `cargo test --workspace --all-targets` passes -- [ ] `cargo clippy --workspace --all-targets --all-features -- -D warnings` passes -- [ ] `cargo fmt --all --check` passes +- [ ] `cargo test -p coln-store --all-targets` passes +- [ ] `cargo clippy -p coln-store --all-targets --all-features -- -D warnings` passes +- [ ] `cargo fmt -p coln-store --check` passes From 5ecbc42fa59e9464b0275914b7ffbd797a9b690a Mon Sep 17 00:00:00 2001 From: Shuntian Liu Date: Thu, 2 Jul 2026 15:51:26 +0100 Subject: [PATCH 10/19] feat: copy from csv in SQL mode --- Cargo.lock | 1 + packages/coln-store/Cargo.toml | 1 + packages/coln-store/src/repl/exe/mod.rs | 73 +++++-- packages/coln-store/src/repl/exe/sql.rs | 201 +++++++++++++++++- packages/coln-store/src/repl/mod.rs | 37 +++- packages/coln-store/src/repl/parse/mod.rs | 89 ++++++++ packages/coln-store/src/repl/parse/sql.rs | 84 +++++++- packages/coln-store/tests/data/people.csv | 3 + packages/coln-store/tests/data/people.tsv | 3 + .../coln-store/tests/data/people_bad_int.csv | 2 + packages/coln-store/tests/data/people_dup.csv | 2 + 11 files changed, 475 insertions(+), 21 deletions(-) create mode 100644 packages/coln-store/tests/data/people.csv create mode 100644 packages/coln-store/tests/data/people.tsv create mode 100644 packages/coln-store/tests/data/people_bad_int.csv create mode 100644 packages/coln-store/tests/data/people_dup.csv diff --git a/Cargo.lock b/Cargo.lock index d85200ce..fceed88f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -911,6 +911,7 @@ dependencies = [ "anyhow", "blake3", "coln-flir-rs", + "csv", "future_form", "hex", "hexane", diff --git a/packages/coln-store/Cargo.toml b/packages/coln-store/Cargo.toml index d5db15c5..c50b5f0a 100644 --- a/packages/coln-store/Cargo.toml +++ b/packages/coln-store/Cargo.toml @@ -21,6 +21,7 @@ exclude = [ anyhow.workspace = true blake3 = "1.8.5" coln-flir-rs = { path = "../coln-flir-rs" } +csv = "1.4.0" # coln-query = { path = "../coln-query" } hex = "0.4.3" hexane = { git = "https://github.com/automerge/automerge", branch = "hexane_v1", package = "hexane" } diff --git a/packages/coln-store/src/repl/exe/mod.rs b/packages/coln-store/src/repl/exe/mod.rs index 0d6a1857..9106a5bc 100644 --- a/packages/coln-store/src/repl/exe/mod.rs +++ b/packages/coln-store/src/repl/exe/mod.rs @@ -11,7 +11,7 @@ use std::{ use anyhow::{Context, Result, anyhow, bail}; use crate::repl::{ - Session, Step, + Session, ShellMode, Step, error::BatchCellParseError, parse::{BatchAssignment, parse_cell_value, parse_cell_value_batch}, parse::{ColnCommand, MetaCommand, SqlCommand}, @@ -26,8 +26,8 @@ use crate::{ mod sql; -fn help_text() -> String { - [ +fn help_text(mode: ShellMode) -> String { + let mut lines = vec![ "Commands:", " .help", " .exit", @@ -39,19 +39,38 @@ fn help_text() -> String { " .rules", " .schema [table]", " .dump
", - " add
values (...), (...);", - " begin transact; name = add
values (...); ... commit;", - "", - "Examples:", - " .help", - " .load tests/data/paths.json", - " .schema", - " .rules", - " .dump T", - " add T values (7 \"alice\"), (8 \"bob\");", - " begin transact; g = add Graphs values (); e = add G0 values (g); commit;", - ] - .join("\n") + ]; + + match mode { + ShellMode::Coln => lines.extend([ + " add
values (...), (...);", + " begin transact; name = add
values (...); ... commit;", + "", + "Examples:", + " .help", + " .load tests/data/paths.json", + " .schema", + " .rules", + " .dump T", + " add T values (7 \"alice\"), (8 \"bob\");", + " begin transact; g = add Graphs values (); e = add G0 values (g); commit;", + ]), + ShellMode::Sql => lines.extend([ + " create table
( , ...);", + " copy
from '' with (format csv, header true);", + " copy
from '' with (format csv, header true, delimiter E'\\t');", + "", + "Examples:", + " .help", + " .schema", + " .dump Person", + " create table Person (name text, age integer);", + " copy Person from 'tests/data/people.csv' with (format csv, header true);", + " copy Decl from 'decl.csv' with (format csv, header true, delimiter E'\\t');", + ]), + } + + lines.join("\n") } pub(super) fn execute_sql(session: &mut Session, command: SqlCommand) -> Result { @@ -60,7 +79,7 @@ pub(super) fn execute_sql(session: &mut Session, command: SqlCommand) -> Result< pub(super) fn execute_meta(session: &mut Session, command: MetaCommand) -> Result { match command { - MetaCommand::Help => Ok(Step::Continue(help_text())), + MetaCommand::Help => Ok(Step::Continue(help_text(session.shell_mode))), MetaCommand::Load { path } => { let loaded = load_schema(std::path::Path::new(&path))?; tracing::info!( @@ -469,6 +488,26 @@ mod tests { assert_eq!(render_schema_summary(None), "no schema loaded"); } + #[test] + fn coln_help_lists_only_coln_statements() { + let help = help_text(ShellMode::Coln); + assert!(help.contains(".load ")); + assert!(help.contains("add
values")); + assert!(help.contains("begin transact;")); + assert!(!help.contains("create table")); + assert!(!help.contains("copy
from")); + } + + #[test] + fn sql_help_lists_only_sql_statements() { + let help = help_text(ShellMode::Sql); + assert!(help.contains(".load ")); + assert!(help.contains("create table
")); + assert!(help.contains("copy
from '' with (format csv, header true);")); + assert!(!help.contains("add
values")); + assert!(!help.contains("begin transact;")); + } + #[test] fn renders_all_table_schemas() { let loaded = load_schema(&Path::new("tests/data/").join(PATHS_IR)).expect("load schema"); diff --git a/packages/coln-store/src/repl/exe/sql.rs b/packages/coln-store/src/repl/exe/sql.rs index 463d615d..94e1a158 100644 --- a/packages/coln-store/src/repl/exe/sql.rs +++ b/packages/coln-store/src/repl/exe/sql.rs @@ -1,10 +1,10 @@ use std::path::PathBuf; -use anyhow::Result; +use anyhow::{Context, Result, anyhow, bail}; use crate::ir::{BuiltinTy, ColType, ColumnEntry, EntityVariant, Path, Schema}; use crate::repl::Session; -use crate::repl::exe::{LoadedState, SchemaSummary}; +use crate::repl::exe::{LoadedState, SchemaSummary, add_rows}; use crate::repl::parse::{SqlCol as Col, SqlCommand as Command}; use crate::store::Store; @@ -23,7 +23,80 @@ pub(super) fn execute_sql(session: &mut Session, command: Command) -> Result create_sql_table(session, table_name, columns), Command::Insert { .. } => Ok("SQL INSERT is not implemented yet".to_string()), + Command::CopyFromCsv { + table_name, + path, + delimiter, + } => copy_from_csv(session, &table_name, &path, delimiter), + } +} + +/// Load a delimited file with a header row into an existing table, mapping +/// file columns to schema columns by name. +fn copy_from_csv( + session: &mut Session, + table_name: &str, + path: &str, + delimiter: u8, +) -> Result { + let loaded = session + .loaded + .as_mut() + .ok_or_else(|| anyhow!("no schema loaded"))?; + let column_names: Vec = loaded + .store + .table_at(&Path::from(table_name)) + .ok_or_else(|| anyhow!("unknown table: {table_name}"))? + .schema() + .columns + .iter() + .map(|column| column.path.to_string()) + .collect(); + + let mut reader = csv::ReaderBuilder::new() + .has_headers(true) + .delimiter(delimiter) + .from_path(path) + .with_context(|| format!("failed to open csv {path}"))?; + let headers = reader + .headers() + .with_context(|| format!("failed to read csv header from {path}"))? + .clone(); + + let mut indices = Vec::with_capacity(column_names.len()); + for name in &column_names { + let mut matches = headers + .iter() + .enumerate() + .filter(|(_, header)| header == name); + let Some((index, _)) = matches.next() else { + bail!("csv {path} is missing column: {name}"); + }; + if matches.next().is_some() { + bail!("csv {path} has duplicate column: {name}"); + } + indices.push(index); } + + let mut rows = Vec::new(); + for (row_index, record) in reader.records().enumerate() { + let record = record + .with_context(|| format!("failed to read csv row {} from {path}", row_index + 1))?; + let mut row = Vec::with_capacity(indices.len()); + for (&index, name) in indices.iter().zip(&column_names) { + let field = record.get(index).ok_or_else(|| { + anyhow!("csv row {}: missing field for column {name}", row_index + 1) + })?; + row.push(field.to_string()); + } + rows.push(row); + } + + if rows.is_empty() { + return Ok(format!("copied 0 rows into {table_name}")); + } + let row_ids = add_rows(&mut loaded.store, table_name, &rows)?; + Ok(format!("copied {} rows into {table_name}", row_ids.len())) } fn create_sql_table( @@ -80,3 +153,127 @@ fn sql_schema_from_cols(columns: Vec) -> Schema { primary_key: None, } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::repl::ShellMode; + + fn sql_session_with_person() -> Session { + let mut session = Session { + loaded: None, + shell_mode: ShellMode::Sql, + }; + create_sql_table( + &mut session, + "Person".to_string(), + vec![ + Col { + col_name: "name".to_string(), + col_typ: BuiltinTy::BuiltinStr, + }, + Col { + col_name: "age".to_string(), + col_typ: BuiltinTy::BuiltinInt, + }, + ], + ) + .expect("create table"); + session + } + + #[test] + fn copy_requires_loaded_schema() { + let err = copy_from_csv( + &mut Session::default(), + "Person", + "tests/data/people.csv", + b',', + ) + .unwrap_err(); + assert_eq!(err.to_string(), "no schema loaded"); + } + + #[test] + fn copy_rejects_unknown_table() { + let mut session = sql_session_with_person(); + let err = + copy_from_csv(&mut session, "Missing", "tests/data/people.csv", b',').unwrap_err(); + assert_eq!(err.to_string(), "unknown table: Missing"); + } + + #[test] + fn copy_rejects_missing_file() { + let mut session = sql_session_with_person(); + let err = + copy_from_csv(&mut session, "Person", "tests/data/missing.csv", b',').unwrap_err(); + assert!( + err.to_string() + .contains("failed to open csv tests/data/missing.csv") + ); + } + + #[test] + fn copy_imports_tab_delimited_file() { + let mut session = sql_session_with_person(); + let message = copy_from_csv(&mut session, "Person", "tests/data/people.tsv", b'\t') + .expect("copy tsv"); + assert_eq!(message, "copied 2 rows into Person"); + + let loaded = session.loaded.as_ref().expect("loaded session"); + let dump = loaded + .store + .table_at(&"Person".parse().unwrap()) + .expect("Person table") + .dump(); + assert!(dump.contains("alice")); + assert!(dump.contains("41")); + } + + #[test] + fn copy_rejects_missing_csv_column() { + let mut session = Session { + loaded: None, + shell_mode: ShellMode::Sql, + }; + create_sql_table( + &mut session, + "Person".to_string(), + vec![Col { + col_name: "email".to_string(), + col_typ: BuiltinTy::BuiltinStr, + }], + ) + .expect("create table"); + + let err = copy_from_csv(&mut session, "Person", "tests/data/people.csv", b',').unwrap_err(); + assert_eq!( + err.to_string(), + "csv tests/data/people.csv is missing column: email" + ); + } + + #[test] + fn copy_rejects_duplicate_csv_column() { + let mut session = sql_session_with_person(); + let err = + copy_from_csv(&mut session, "Person", "tests/data/people_dup.csv", b',').unwrap_err(); + assert_eq!( + err.to_string(), + "csv tests/data/people_dup.csv has duplicate column: name" + ); + } + + #[test] + fn copy_rejects_invalid_int_value() { + let mut session = sql_session_with_person(); + let err = copy_from_csv( + &mut session, + "Person", + "tests/data/people_bad_int.csv", + b',', + ) + .unwrap_err(); + assert_eq!(err.to_string(), "column 1: invalid int: not-a-number"); + } +} diff --git a/packages/coln-store/src/repl/mod.rs b/packages/coln-store/src/repl/mod.rs index 9811850a..3e51dbe2 100644 --- a/packages/coln-store/src/repl/mod.rs +++ b/packages/coln-store/src/repl/mod.rs @@ -99,7 +99,7 @@ pub fn run() -> Result<(), Box> { Ok(Step::Exit) => break, Err(err) => { warn!(error = %err, "repl command failed"); - eprintln!("error: {err}"); + eprintln!("error: {err:#}"); } }, Err(err) => eprintln!("error: {err}"), @@ -304,6 +304,41 @@ mod tests { ); } + #[test] + fn sql_copy_from_csv_inserts_rows() { + let mut session = Session { + loaded: None, + shell_mode: ShellMode::Sql, + }; + execute(&mut session, sql_create_table_command("Person")).expect("create table"); + + let message = match execute( + &mut session, + Command::Sql(SqlCommand::CopyFromCsv { + table_name: "Person".to_string(), + path: "tests/data/people.csv".to_string(), + delimiter: b',', + }), + ) + .expect("copy csv") + { + Step::Continue(message) => message, + Step::Exit => panic!("unexpected exit"), + }; + + assert_eq!(message, "copied 2 rows into Person"); + let loaded = session.loaded.as_ref().expect("loaded session"); + let table = loaded + .store + .table_at(&"Person".parse().unwrap()) + .expect("Person table"); + assert_eq!(table.row_count(), 2); + // The fixture header order (age, name) differs from the schema order. + let dump = table.dump(); + assert!(dump.contains("alice")); + assert!(dump.contains("30")); + } + #[test] fn add_rejects_bad_entity_id() { let mut store = Store::new(); diff --git a/packages/coln-store/src/repl/parse/mod.rs b/packages/coln-store/src/repl/parse/mod.rs index a69d27ca..2062fd72 100644 --- a/packages/coln-store/src/repl/parse/mod.rs +++ b/packages/coln-store/src/repl/parse/mod.rs @@ -198,6 +198,95 @@ mod tests { assert_eq!(columns[1].col_typ, crate::ir::BuiltinTy::BuiltinInt); } + #[test] + fn parses_sql_copy_from_csv() { + assert_eq!( + parse_command( + ShellMode::Sql, + "copy Person from 'people.csv' with (format csv, header true);", + ) + .unwrap(), + Command::Sql(SqlCommand::CopyFromCsv { + table_name: "Person".to_string(), + path: "people.csv".to_string(), + delimiter: b',', + }) + ); + } + + #[test] + fn parses_sql_copy_with_tab_delimiter() { + assert_eq!( + parse_command( + ShellMode::Sql, + "copy Decl from 'decl.csv' with (format csv, header true, delimiter E'\\t');", + ) + .unwrap(), + Command::Sql(SqlCommand::CopyFromCsv { + table_name: "Decl".to_string(), + path: "decl.csv".to_string(), + delimiter: b'\t', + }) + ); + } + + #[test] + fn rejects_sql_copy_with_non_ascii_delimiter() { + // sqlparser rejects multi-byte char literals before our option checks. + let err = parse_command( + ShellMode::Sql, + "copy Decl from 'decl.csv' with (format csv, header true, delimiter '\u{00e9}');", + ) + .unwrap_err(); + assert_eq!(err.to_string(), "failed to parse sql"); + } + + #[test] + fn rejects_sql_copy_without_csv_format() { + let err = parse_command( + ShellMode::Sql, + "copy Person from 'people.csv' with (header true);", + ) + .unwrap_err(); + assert_eq!(err.to_string(), "COPY requires FORMAT csv"); + } + + #[test] + fn rejects_sql_copy_without_header() { + let err = parse_command( + ShellMode::Sql, + "copy Person from 'people.csv' with (format csv, header false);", + ) + .unwrap_err(); + assert_eq!(err.to_string(), "COPY requires HEADER true"); + } + + #[test] + fn rejects_sql_copy_to() { + let err = parse_command( + ShellMode::Sql, + "copy Person to 'people.csv' with (format csv, header true);", + ) + .unwrap_err(); + assert_eq!(err.to_string(), "COPY TO is not supported"); + } + + #[test] + fn rejects_sql_copy_from_stdin() { + let err = parse_command(ShellMode::Sql, "copy Person from stdin;").unwrap_err(); + assert_eq!(err.to_string(), "COPY FROM only supports a file source"); + } + + #[test] + fn rejects_sql_copy_with_column_list() { + let err = parse_command( + ShellMode::Sql, + "copy Person (name) from 'people.csv' with (format csv, header true);", + ) + .unwrap_err(); + assert_eq!(err.to_string(), "COPY column lists are not supported"); + } + #[test] fn rejects_batch_without_commit_keyword() { let err = diff --git a/packages/coln-store/src/repl/parse/sql.rs b/packages/coln-store/src/repl/parse/sql.rs index 9a2fd1db..2481e8e0 100644 --- a/packages/coln-store/src/repl/parse/sql.rs +++ b/packages/coln-store/src/repl/parse/sql.rs @@ -1,6 +1,9 @@ use anyhow::{Context, Result, bail}; use sqlparser::{ - ast::{DataType, Expr, SetExpr, Statement, TableObject, Value}, + ast::{ + CopyLegacyOption, CopyOption, CopySource, CopyTarget, DataType, Expr, SetExpr, Statement, + TableObject, Value, + }, dialect::GenericDialect, parser::Parser, }; @@ -22,6 +25,14 @@ pub(crate) enum Command { col_names: Vec, values: Vec>, }, + /// `COPY
FROM '' WITH (FORMAT csv, HEADER true)`, with an + /// optional `DELIMITER` such as `E'\t'` for tab-separated files. + // TODO use duckdb style import + CopyFromCsv { + table_name: String, + path: String, + delimiter: u8, + }, } pub(crate) fn parse_sql_statement(input: &str) -> Result { @@ -94,11 +105,82 @@ pub(crate) fn parse_sql_statement(input: &str) -> Result { col_names, values: rows, }) + } else if let Statement::Copy { + source, + to, + target, + options, + legacy_options, + values, + } = statement + { + parse_copy_from_csv(source, *to, target, options, legacy_options, values) } else { bail!("Unsupported statements {}", input); } } +fn parse_copy_from_csv( + source: &CopySource, + to: bool, + target: &CopyTarget, + options: &[CopyOption], + legacy_options: &[CopyLegacyOption], + values: &[Option], +) -> Result { + if to { + bail!("COPY TO is not supported"); + } + let CopySource::Table { + table_name, + columns, + } = source + else { + bail!("COPY FROM only supports a table source"); + }; + if !columns.is_empty() { + bail!("COPY column lists are not supported"); + } + let CopyTarget::File { filename } = target else { + bail!("COPY FROM only supports a file source"); + }; + if !legacy_options.is_empty() { + bail!("legacy COPY options are not supported; use WITH (FORMAT csv, HEADER true)"); + } + if !values.is_empty() { + bail!("inline COPY values are not supported"); + } + + let mut format_csv = false; + let mut header = false; + let mut delimiter = b','; + for option in options { + match option { + CopyOption::Format(format) => { + if !format.value.eq_ignore_ascii_case("csv") { + bail!("unsupported COPY format: {format}"); + } + format_csv = true; + } + CopyOption::Header(value) => header = *value, + CopyOption::Delimiter(value) => delimiter = *value as u8, + other => bail!("unsupported COPY option: {other}"), + } + } + if !format_csv { + bail!("COPY requires FORMAT csv"); + } + if !header { + bail!("COPY requires HEADER true"); + } + + Ok(Command::CopyFromCsv { + table_name: table_name.to_string(), + path: filename.clone(), + delimiter, + }) +} + fn sql_expr_to_raw_value(expr: &Expr) -> Result { let Expr::Value(value) = expr else { bail!("only literal INSERT values are supported"); diff --git a/packages/coln-store/tests/data/people.csv b/packages/coln-store/tests/data/people.csv new file mode 100644 index 00000000..826d5ae6 --- /dev/null +++ b/packages/coln-store/tests/data/people.csv @@ -0,0 +1,3 @@ +age,name +30,alice +41,bob diff --git a/packages/coln-store/tests/data/people.tsv b/packages/coln-store/tests/data/people.tsv new file mode 100644 index 00000000..7f1c2e72 --- /dev/null +++ b/packages/coln-store/tests/data/people.tsv @@ -0,0 +1,3 @@ +age name +30 alice +41 bob diff --git a/packages/coln-store/tests/data/people_bad_int.csv b/packages/coln-store/tests/data/people_bad_int.csv new file mode 100644 index 00000000..07a5edf7 --- /dev/null +++ b/packages/coln-store/tests/data/people_bad_int.csv @@ -0,0 +1,2 @@ +name,age +carol,not-a-number diff --git a/packages/coln-store/tests/data/people_dup.csv b/packages/coln-store/tests/data/people_dup.csv new file mode 100644 index 00000000..efd2c918 --- /dev/null +++ b/packages/coln-store/tests/data/people_dup.csv @@ -0,0 +1,2 @@ +name,name,age +alice,bob,30 From 3fa176c8a49b9d0cba73632157138a3e51d99e93 Mon Sep 17 00:00:00 2001 From: Shuntian Liu Date: Thu, 2 Jul 2026 15:53:38 +0100 Subject: [PATCH 11/19] test: remove old version of ssa.json --- packages/coln-store/tests/data/ssa.json | 1 - 1 file changed, 1 deletion(-) delete mode 100644 packages/coln-store/tests/data/ssa.json diff --git a/packages/coln-store/tests/data/ssa.json b/packages/coln-store/tests/data/ssa.json deleted file mode 100644 index 6b1dc1cf..00000000 --- a/packages/coln-store/tests/data/ssa.json +++ /dev/null @@ -1 +0,0 @@ -{"laws":[{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":3,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":4,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["follows"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":5,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["follows"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":3,"tag":"var"}},{"column":3,"term":{"index":2,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"left":{"index":2,"tag":"var"},"right":{"index":3,"tag":"var"},"tag":"eq"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["follows"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["follows"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["args"],["acyclic"]]},{"law":{"antecedent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}}]},"tag":"atom"},"consequent":{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["args"],["car"],["foreignKeys"]]},{"law":{"antecedent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["first"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}}]},"tag":"atom"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["args"],["first"],["foreignKeys"]]},{"law":{"antecedent":{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},"consequent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["first"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}}]},"tag":"atom"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["args"],["first"],["total"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["first-is-top"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":4,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["first"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":3,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":4,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["follows"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":3,"tag":"var"}},{"column":3,"term":{"index":2,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["follows"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["args"],["first-is-top"],["foreignKeys"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["first-is-top"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}}]},"tag":"atom"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["args"],["first-is-top"],["total"]]},{"law":{"antecedent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["follows"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":3,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["args"],["follows"],["foreignKeys"]]},{"law":{"antecedent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["last"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}}]},"tag":"atom"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["args"],["last"],["foreignKeys"]]},{"law":{"antecedent":{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},"consequent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["last"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}}]},"tag":"atom"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["args"],["last"],["total"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["last-is-bot"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":4,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["last"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":3,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":4,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["follows"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["follows"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["args"],["last-is-bot"],["foreignKeys"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["last-is-bot"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}}]},"tag":"atom"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["args"],["last-is-bot"],["total"]]},{"law":{"antecedent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["next"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":3,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["args"],["next"],["foreignKeys"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["next"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}}]},"tag":"atom"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["args"],["next"],["total"]]},{"law":{"antecedent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["prev"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":3,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["args"],["prev"],["foreignKeys"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["prev"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}}]},"tag":"atom"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["args"],["prev"],["total"]]},{"law":{"antecedent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["follows","is-trans-closure"],["refl"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":3,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["follows"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":2,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["follows"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["args"],["follows","is-trans-closure"],["refl"],["foreignKeys"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["follows","is-trans-closure"],["refl"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}}]},"tag":"atom"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["args"],["follows","is-trans-closure"],["refl"],["total"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["follows","is-trans-closure"],["snoc"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}},{"column":4,"term":{"index":4,"tag":"var"}},{"column":5,"term":{"index":5,"tag":"var"}},{"column":6,"term":{"index":7,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["next"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":3,"tag":"var"}},{"column":3,"term":{"index":6,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":3,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":4,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":5,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["follows"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},{"left":{"index":6,"tag":"var"},"right":{"index":4,"tag":"var"},"tag":"eq"},{"atom":{"rowId":{"index":7,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["follows"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":4,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["follows"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["follows"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["args"],["follows","is-trans-closure"],["snoc"],["foreignKeys"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":3,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":4,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":5,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["follows"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["next"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":3,"tag":"var"}},{"column":3,"term":{"index":6,"tag":"var"}}]},"tag":"atom"},{"left":{"index":6,"tag":"var"},"right":{"index":4,"tag":"var"},"tag":"eq"}],"tag":"and"},"consequent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["follows","is-trans-closure"],["snoc"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}},{"column":4,"term":{"index":4,"tag":"var"}},{"column":5,"term":{"index":5,"tag":"var"}}]},"tag":"atom"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["follows"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["args"],["follows","is-trans-closure"],["snoc"],["total"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["prev"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["next"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":3,"tag":"var"}},{"column":3,"term":{"index":4,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"left":{"index":4,"tag":"var"},"right":{"index":2,"tag":"var"},"tag":"eq"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["args"],["next","prev"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["args"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["next"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["args"],["prev"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":3,"tag":"var"}},{"column":3,"term":{"index":4,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"left":{"index":4,"tag":"var"},"right":{"index":2,"tag":"var"},"tag":"eq"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["args"],["prev","next"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":3,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":4,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["follows"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":5,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["follows"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":3,"tag":"var"}},{"column":3,"term":{"index":2,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"left":{"index":2,"tag":"var"},"right":{"index":3,"tag":"var"},"tag":"eq"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["follows"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["follows"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["ops"],["acyclic"]]},{"law":{"antecedent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}}]},"tag":"atom"},"consequent":{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["ops"],["car"],["foreignKeys"]]},{"law":{"antecedent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["first"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}}]},"tag":"atom"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["ops"],["first"],["foreignKeys"]]},{"law":{"antecedent":{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},"consequent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["first"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}}]},"tag":"atom"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["ops"],["first"],["total"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["first-is-top"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":4,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["first"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":3,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":4,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["follows"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":3,"tag":"var"}},{"column":3,"term":{"index":2,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["follows"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["ops"],["first-is-top"],["foreignKeys"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["first-is-top"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}}]},"tag":"atom"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["ops"],["first-is-top"],["total"]]},{"law":{"antecedent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["follows"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":3,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["ops"],["follows"],["foreignKeys"]]},{"law":{"antecedent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["last"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}}]},"tag":"atom"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["ops"],["last"],["foreignKeys"]]},{"law":{"antecedent":{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},"consequent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["last"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}}]},"tag":"atom"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["ops"],["last"],["total"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["last-is-bot"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":4,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["last"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":3,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":4,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["follows"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["follows"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["ops"],["last-is-bot"],["foreignKeys"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["last-is-bot"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}}]},"tag":"atom"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["ops"],["last-is-bot"],["total"]]},{"law":{"antecedent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["next"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":3,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["ops"],["next"],["foreignKeys"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["next"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}}]},"tag":"atom"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["ops"],["next"],["total"]]},{"law":{"antecedent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["prev"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":3,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["ops"],["prev"],["foreignKeys"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["prev"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}}]},"tag":"atom"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["ops"],["prev"],["total"]]},{"law":{"antecedent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["follows","is-trans-closure"],["refl"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":3,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["follows"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":2,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["follows"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["ops"],["follows","is-trans-closure"],["refl"],["foreignKeys"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["follows","is-trans-closure"],["refl"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}}]},"tag":"atom"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["ops"],["follows","is-trans-closure"],["refl"],["total"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["follows","is-trans-closure"],["snoc"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}},{"column":4,"term":{"index":4,"tag":"var"}},{"column":5,"term":{"index":5,"tag":"var"}},{"column":6,"term":{"index":7,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["next"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":3,"tag":"var"}},{"column":3,"term":{"index":6,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":3,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":4,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":5,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["follows"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},{"left":{"index":6,"tag":"var"},"right":{"index":4,"tag":"var"},"tag":"eq"},{"atom":{"rowId":{"index":7,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["follows"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":4,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["follows"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["follows"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["ops"],["follows","is-trans-closure"],["snoc"],["foreignKeys"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":3,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":4,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":5,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["follows"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["next"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":3,"tag":"var"}},{"column":3,"term":{"index":6,"tag":"var"}}]},"tag":"atom"},{"left":{"index":6,"tag":"var"},"right":{"index":4,"tag":"var"},"tag":"eq"}],"tag":"and"},"consequent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["follows","is-trans-closure"],["snoc"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}},{"column":4,"term":{"index":4,"tag":"var"}},{"column":5,"term":{"index":5,"tag":"var"}}]},"tag":"atom"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["follows"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["ops"],["follows","is-trans-closure"],["snoc"],["total"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["prev"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["next"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":3,"tag":"var"}},{"column":3,"term":{"index":4,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"left":{"index":4,"tag":"var"},"right":{"index":2,"tag":"var"},"tag":"eq"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["ops"],["next","prev"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["next"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["ops"],["prev"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":3,"tag":"var"}},{"column":3,"term":{"index":4,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"left":{"index":4,"tag":"var"},"right":{"index":2,"tag":"var"},"tag":"eq"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["ops"],["prev","next"]]},{"law":{"antecedent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["outputs"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}}]},"tag":"atom"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["outputs"],["foreignKeys"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opOutputs"],["bwd"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":4,"tag":"var"}},{"column":4,"term":{"index":5,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opType"]],"values":[{"column":0,"term":{"index":2,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":0,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":4,"tag":"var"},"table":[["opinfo"],["outputArity"]],"values":[{"column":0,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":5,"tag":"var"},"table":[["ssainsta"],["ops"],["outputs"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["opinfo"],["OpType"]],"tag":"entityType"},{"path":[["opinfo"],["outputArity"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["outputs"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["payload"],["opOutputs"],["bwd"],["foreignKeys"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opType"]],"values":[{"column":0,"term":{"index":2,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":0,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":4,"tag":"var"},"table":[["opinfo"],["outputArity"]],"values":[{"column":0,"term":{"index":3,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opOutputs"],["bwd"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":4,"tag":"var"}}]},"tag":"atom"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["opinfo"],["OpType"]],"tag":"entityType"},{"path":[["opinfo"],["outputArity"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["payload"],["opOutputs"],["bwd"],["total"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opOutputs"],["fwd"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}},{"column":4,"term":{"index":5,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opType"]],"values":[{"column":0,"term":{"index":2,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":0,"tag":"var"}},{"column":3,"term":{"index":4,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":3,"tag":"var"},"table":[["ssainsta"],["ops"],["outputs"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":5,"tag":"var"},"table":[["opinfo"],["outputArity"]],"values":[{"column":0,"term":{"index":4,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["outputs"]],"tag":"entityType"},{"path":[["opinfo"],["OpType"]],"tag":"entityType"},{"path":[["opinfo"],["outputArity"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["payload"],["opOutputs"],["fwd"],["foreignKeys"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":3,"tag":"var"},"table":[["ssainsta"],["ops"],["outputs"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opOutputs"],["fwd"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["outputs"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["payload"],["opOutputs"],["fwd"],["total"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opType"]],"values":[{"column":0,"term":{"index":2,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":0,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":4,"tag":"var"},"table":[["opinfo"],["outputArity"]],"values":[{"column":0,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opOutputs"],["bwd"]],"values":[{"column":0,"term":{"index":2,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":0,"tag":"var"}},{"column":3,"term":{"index":4,"tag":"var"}},{"column":4,"term":{"index":5,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opOutputs"],["fwd"]],"values":[{"column":0,"term":{"index":2,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":0,"tag":"var"}},{"column":3,"term":{"index":5,"tag":"var"}},{"column":4,"term":{"index":6,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"left":{"index":6,"tag":"var"},"right":{"index":4,"tag":"var"},"tag":"eq"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["opinfo"],["OpType"]],"tag":"entityType"},{"path":[["opinfo"],["outputArity"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["outputs"]],"tag":"entityType"},{"path":[["opinfo"],["outputArity"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["payload"],["opOutputs"],["bwd","fwd"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":3,"tag":"var"},"table":[["ssainsta"],["ops"],["outputs"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opOutputs"],["fwd"]],"values":[{"column":0,"term":{"index":2,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":0,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}},{"column":4,"term":{"index":4,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opOutputs"],["bwd"]],"values":[{"column":0,"term":{"index":2,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":0,"tag":"var"}},{"column":3,"term":{"index":4,"tag":"var"}},{"column":4,"term":{"index":5,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"left":{"index":5,"tag":"var"},"right":{"index":3,"tag":"var"},"tag":"eq"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["outputs"]],"tag":"entityType"},{"path":[["opinfo"],["outputArity"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["outputs"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["payload"],["opOutputs"],["fwd","bwd"]]},{"law":{"antecedent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opType"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["opinfo"],["OpType"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["payload"],["opType"],["foreignKeys"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opType"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}}]},"tag":"atom"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["payload"],["opType"],["total"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opInputs","X"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":4,"tag":"var"}},{"column":4,"term":{"index":5,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opType"]],"values":[{"column":0,"term":{"index":2,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":0,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":4,"tag":"var"},"table":[["opinfo"],["inputArity"]],"values":[{"column":0,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":5,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["opinfo"],["OpType"]],"tag":"entityType"},{"path":[["opinfo"],["inputArity"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["payload"],["opInputs","X"],["foreignKeys"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opType"]],"values":[{"column":0,"term":{"index":2,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":0,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":4,"tag":"var"},"table":[["opinfo"],["inputArity"]],"values":[{"column":0,"term":{"index":3,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opInputs","X"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":4,"tag":"var"}}]},"tag":"atom"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["opinfo"],["OpType"]],"tag":"entityType"},{"path":[["opinfo"],["inputArity"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["payload"],["opInputs","X"],["total"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opInputs","output"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":4,"tag":"var"}},{"column":4,"term":{"index":5,"tag":"var"}},{"column":5,"term":{"index":6,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opType"]],"values":[{"column":0,"term":{"index":2,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":0,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":4,"tag":"var"},"table":[["opinfo"],["inputArity"]],"values":[{"column":0,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":5,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":6,"tag":"var"},"table":[["ssainsta"],["ops"],["outputs"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":5,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["opinfo"],["OpType"]],"tag":"entityType"},{"path":[["opinfo"],["inputArity"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["outputs"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["payload"],["opInputs","output"],["foreignKeys"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opType"]],"values":[{"column":0,"term":{"index":2,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":0,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":4,"tag":"var"},"table":[["opinfo"],["inputArity"]],"values":[{"column":0,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":5,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opInputs","output"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":4,"tag":"var"}},{"column":4,"term":{"index":5,"tag":"var"}}]},"tag":"atom"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["opinfo"],["OpType"]],"tag":"entityType"},{"path":[["opinfo"],["inputArity"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["payload"],["opInputs","output"],["total"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opInputs","rel"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":4,"tag":"var"}},{"column":4,"term":{"index":5,"tag":"var"}},{"column":5,"term":{"index":6,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opType"]],"values":[{"column":0,"term":{"index":2,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":0,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":4,"tag":"var"},"table":[["opinfo"],["inputArity"]],"values":[{"column":0,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":5,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":6,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["follows"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}},{"column":2,"term":{"index":5,"tag":"var"}},{"column":3,"term":{"index":2,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["opinfo"],["OpType"]],"tag":"entityType"},{"path":[["opinfo"],["inputArity"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["follows"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["payload"],["opInputs","rel"],["foreignKeys"]]},{"law":{"antecedent":{"props":[{"atom":{"rowId":{"index":1,"tag":"var"},"table":[["ssainsta"],["BlockId"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":2,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opType"]],"values":[{"column":0,"term":{"index":2,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":0,"tag":"var"}},{"column":3,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":4,"tag":"var"},"table":[["opinfo"],["inputArity"]],"values":[{"column":0,"term":{"index":3,"tag":"var"}}]},"tag":"atom"},{"atom":{"rowId":{"index":5,"tag":"var"},"table":[["ssainsta"],["ops"],["ops"],["car"]],"values":[{"column":0,"term":{"index":1,"tag":"var"}},{"column":1,"term":{"index":0,"tag":"var"}}]},"tag":"atom"}],"tag":"and"},"consequent":{"atom":{"rowId":null,"table":[["ssainsta"],["ops"],["payload"],["opInputs","rel"]],"values":[{"column":0,"term":{"index":0,"tag":"var"}},{"column":1,"term":{"index":1,"tag":"var"}},{"column":2,"term":{"index":2,"tag":"var"}},{"column":3,"term":{"index":4,"tag":"var"}},{"column":4,"term":{"index":5,"tag":"var"}}]},"tag":"atom"},"variables":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["opinfo"],["OpType"]],"tag":"entityType"},{"path":[["opinfo"],["inputArity"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}]},"path":[["ssainsta"],["ops"],["payload"],["opInputs","rel"],["total"]]}],"tables":[{"path":[["opinfo"],["OpType"]],"table":{"columns":[],"primaryKey":null}},{"path":[["opinfo"],["inputArity"]],"table":{"columns":[{"path":[["opinfo"],["OpType"]],"tag":"entityType"}],"primaryKey":null}},{"path":[["opinfo"],["outputArity"]],"table":{"columns":[{"path":[["opinfo"],["OpType"]],"tag":"entityType"}],"primaryKey":null}},{"path":[["ssainsta"],["BlockId"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"}],"primaryKey":null}},{"path":[["ssainsta"],["RegionId"]],"table":{"columns":[],"primaryKey":null}},{"path":[["ssainsta"],["ops"],["args"],["car"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"}],"primaryKey":null}},{"path":[["ssainsta"],["ops"],["args"],["first"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"}],"primaryKey":[0,1]}},{"path":[["ssainsta"],["ops"],["args"],["first-is-top"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["follows"]],"tag":"entityType"}],"primaryKey":[0,1,2]}},{"path":[["ssainsta"],["ops"],["args"],["follows"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"}],"primaryKey":null}},{"path":[["ssainsta"],["ops"],["args"],["last"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"}],"primaryKey":[0,1]}},{"path":[["ssainsta"],["ops"],["args"],["last-is-bot"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["follows"]],"tag":"entityType"}],"primaryKey":[0,1,2]}},{"path":[["ssainsta"],["ops"],["args"],["next"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"}],"primaryKey":[0,1,2]}},{"path":[["ssainsta"],["ops"],["args"],["prev"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"}],"primaryKey":[0,1,2]}},{"path":[["ssainsta"],["ops"],["args"],["follows","is-trans-closure"],["refl"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["follows"]],"tag":"entityType"}],"primaryKey":[0,1,2]}},{"path":[["ssainsta"],["ops"],["args"],["follows","is-trans-closure"],["snoc"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["follows"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["args"],["follows"]],"tag":"entityType"}],"primaryKey":[0,1,2,3,4,5]}},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"}],"primaryKey":null}},{"path":[["ssainsta"],["ops"],["ops"],["first"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}],"primaryKey":[0,1]}},{"path":[["ssainsta"],["ops"],["ops"],["first-is-top"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["follows"]],"tag":"entityType"}],"primaryKey":[0,1,2]}},{"path":[["ssainsta"],["ops"],["ops"],["follows"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}],"primaryKey":null}},{"path":[["ssainsta"],["ops"],["ops"],["last"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}],"primaryKey":[0,1]}},{"path":[["ssainsta"],["ops"],["ops"],["last-is-bot"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["follows"]],"tag":"entityType"}],"primaryKey":[0,1,2]}},{"path":[["ssainsta"],["ops"],["ops"],["next"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}],"primaryKey":[0,1,2]}},{"path":[["ssainsta"],["ops"],["ops"],["prev"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}],"primaryKey":[0,1,2]}},{"path":[["ssainsta"],["ops"],["ops"],["follows","is-trans-closure"],["refl"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["follows"]],"tag":"entityType"}],"primaryKey":[0,1,2]}},{"path":[["ssainsta"],["ops"],["ops"],["follows","is-trans-closure"],["snoc"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["follows"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["follows"]],"tag":"entityType"}],"primaryKey":[0,1,2,3,4,5]}},{"path":[["ssainsta"],["ops"],["outputs"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}],"primaryKey":null}},{"path":[["ssainsta"],["ops"],["payload"],["opOutputs"],["bwd"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["opinfo"],["outputArity"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["outputs"]],"tag":"entityType"}],"primaryKey":[0,1,2,3]}},{"path":[["ssainsta"],["ops"],["payload"],["opOutputs"],["fwd"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["outputs"]],"tag":"entityType"},{"path":[["opinfo"],["outputArity"]],"tag":"entityType"}],"primaryKey":[0,1,2,3]}},{"path":[["ssainsta"],["ops"],["payload"],["opType"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["opinfo"],["OpType"]],"tag":"entityType"}],"primaryKey":[0,1,2]}},{"path":[["ssainsta"],["ops"],["payload"],["opInputs","X"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["opinfo"],["inputArity"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"}],"primaryKey":[0,1,2,3]}},{"path":[["ssainsta"],["ops"],["payload"],["opInputs","output"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["opinfo"],["inputArity"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["outputs"]],"tag":"entityType"}],"primaryKey":[0,1,2,3,4]}},{"path":[["ssainsta"],["ops"],["payload"],["opInputs","rel"]],"table":{"columns":[{"path":[["ssainsta"],["RegionId"]],"tag":"entityType"},{"path":[["ssainsta"],["BlockId"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["opinfo"],["inputArity"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["car"]],"tag":"entityType"},{"path":[["ssainsta"],["ops"],["ops"],["follows"]],"tag":"entityType"}],"primaryKey":[0,1,2,3,4]}}]} \ No newline at end of file From 328cfa5b5861d80a43d13f9d11e49d66de694ead Mon Sep 17 00:00:00 2001 From: Shuntian Liu Date: Thu, 2 Jul 2026 16:39:47 +0100 Subject: [PATCH 12/19] feat: add suffix --- packages/coln-store/.gitignore | 3 +++ packages/coln-store/src/repl/exe/mod.rs | 12 ++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/coln-store/.gitignore b/packages/coln-store/.gitignore index def30136..20be2411 100644 --- a/packages/coln-store/.gitignore +++ b/packages/coln-store/.gitignore @@ -64,3 +64,6 @@ tmp/ .claude/ .codex .agent/ + +# colnstore +*.colnstore diff --git a/packages/coln-store/src/repl/exe/mod.rs b/packages/coln-store/src/repl/exe/mod.rs index 9106a5bc..fd731fdd 100644 --- a/packages/coln-store/src/repl/exe/mod.rs +++ b/packages/coln-store/src/repl/exe/mod.rs @@ -150,8 +150,16 @@ pub(super) fn execute_meta(session: &mut Session, command: MetaCommand) -> Resul .as_mut() .ok_or_else(|| anyhow!("no schema loaded"))?; let bytes = encode_store(&loaded.store)?; - fs::write(&path, &bytes)?; - Ok(Step::Continue(format!("saved store to {path}"))) + let mut path = PathBuf::from(&path); + if path.extension().is_none() { + path.add_extension("colnstore"); + } + + fs::write(&path, &bytes).with_context(|| format!("cannot find path {path:?}"))?; + Ok(Step::Continue(format!( + "saved store to {}", + path.as_os_str().display() + ))) } MetaCommand::Exit => Ok(Step::Exit), } From bd120addcc6d7e7ad2bf6be4351a315a26c05508 Mon Sep 17 00:00:00 2001 From: Shuntian Liu Date: Thu, 2 Jul 2026 17:08:35 +0100 Subject: [PATCH 13/19] feat: path completion cli flags --- Cargo.lock | 1 + packages/coln-store/Cargo.toml | 3 +- packages/coln-store/src/main.rs | 11 ++- packages/coln-store/src/repl/cli.rs | 128 ++++++++++++++++++++-------- packages/coln-store/src/repl/mod.rs | 19 ++++- 5 files changed, 123 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fceed88f..6c5d2ba5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -910,6 +910,7 @@ version = "0.1.0" dependencies = [ "anyhow", "blake3", + "clap", "coln-flir-rs", "csv", "future_form", diff --git a/packages/coln-store/Cargo.toml b/packages/coln-store/Cargo.toml index c50b5f0a..2db5fa30 100644 --- a/packages/coln-store/Cargo.toml +++ b/packages/coln-store/Cargo.toml @@ -20,6 +20,7 @@ exclude = [ [dependencies] anyhow.workspace = true blake3 = "1.8.5" +clap = { version = "4.6.1", features = ["derive"], optional = true } coln-flir-rs = { path = "../coln-flir-rs" } csv = "1.4.0" # coln-query = { path = "../coln-query" } @@ -68,7 +69,7 @@ tokio = { version = "1.52.3", features = [ [features] default = ["native"] -native = ["dep:rustyline", "dep:tracing-subscriber"] +native = ["dep:rustyline", "dep:tracing-subscriber", "dep:clap"] wasm = ["dep:js-sys"] [[bin]] diff --git a/packages/coln-store/src/main.rs b/packages/coln-store/src/main.rs index 6ac4d01c..278e1c16 100644 --- a/packages/coln-store/src/main.rs +++ b/packages/coln-store/src/main.rs @@ -4,6 +4,14 @@ use coln_store::repl; use tracing_subscriber::EnvFilter; +use clap::Parser; + +#[derive(Parser, Debug)] +struct Args { + #[arg(long, default_value_t = false, hide = true)] + enable_sql_mode: bool + +} fn main() -> Result<(), Box> { tracing_subscriber::fmt() @@ -12,5 +20,6 @@ fn main() -> Result<(), Box> { ) .init(); - repl::run() + let args = Args::parse(); + repl::run(args.enable_sql_mode) } diff --git a/packages/coln-store/src/repl/cli.rs b/packages/coln-store/src/repl/cli.rs index f3ab1949..8bb1e973 100644 --- a/packages/coln-store/src/repl/cli.rs +++ b/packages/coln-store/src/repl/cli.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use rustyline::completion::{Completer, Pair}; +use rustyline::completion::{Completer, FilenameCompleter, Pair}; use rustyline::error::ReadlineError; use rustyline::highlight::Highlighter; use rustyline::hint::Hinter; @@ -13,44 +13,19 @@ const COMMANDS: &[&str] = &[ "add", "begin", ]; -pub(super) struct CommandHelper; +const PATH_COMMANDS: &[&str] = &[".load", ".open", ".save"]; -pub(super) fn is_statement_start(input: &str) -> bool { - !input.trim_start().starts_with('.') +pub(super) struct CommandHelper { + filename_completer: FilenameCompleter, } -pub(super) fn prompt(shell_mode: ShellMode, pending_statement: bool) -> &'static str { - if pending_statement { - "....> " - } else if shell_mode == ShellMode::Sql { - "coln-sql> " - } else { - "coln-store> " - } -} - -pub(super) fn push_statement_line(pending: &mut Option, line: &str) -> Option { - let pending_line = pending.get_or_insert_with(String::new); - if !pending_line.is_empty() { - pending_line.push(' '); - } - pending_line.push_str(line.trim()); - - let buf = pending_line.trim_start(); - if buf.starts_with("begin transact") { - if pending_line.trim_end().ends_with("commit;") { - return pending.take(); +impl CommandHelper { + pub(super) fn new() -> Self { + Self { + filename_completer: FilenameCompleter::new(), } - return None; - } - - if pending_line.trim_end().ends_with(';') { - pending.take() - } else { - None } } - impl Helper for CommandHelper {} impl Hinter for CommandHelper { @@ -68,13 +43,24 @@ impl Completer for CommandHelper { &self, line: &str, pos: usize, - _ctx: &Context<'_>, + ctx: &Context<'_>, ) -> Result<(usize, Vec), ReadlineError> { + if wants_path_completion(line, pos) { + return self.filename_completer.complete(line, pos, ctx); + } let (start, candidates) = complete_command(line, pos); Ok((start, candidates)) } } +fn wants_path_completion(line: &str, pos: usize) -> bool { + let prefix = &line[..pos]; + let Some(command) = prefix.split_whitespace().next() else { + return false; + }; + PATH_COMMANDS.contains(&command) && prefix.len() > command.len() +} + fn complete_command(line: &str, pos: usize) -> (usize, Vec) { let prefix = &line[..pos]; if prefix.split_whitespace().count() > 1 || prefix.contains(' ') { @@ -93,6 +79,42 @@ fn complete_command(line: &str, pos: usize) -> (usize, Vec) { (0, matches) } +pub(super) fn prompt(shell_mode: ShellMode, pending_statement: bool) -> &'static str { + if pending_statement { + "....> " + } else if shell_mode == ShellMode::Sql { + "coln-sql> " + } else { + "coln-store> " + } +} + +pub(super) fn is_statement_start(input: &str) -> bool { + !input.trim_start().starts_with('.') +} + +pub(super) fn push_statement_line(pending: &mut Option, line: &str) -> Option { + let pending_line = pending.get_or_insert_with(String::new); + if !pending_line.is_empty() { + pending_line.push(' '); + } + pending_line.push_str(line.trim()); + + let buf = pending_line.trim_start(); + if buf.starts_with("begin transact") { + if pending_line.trim_end().ends_with("commit;") { + return pending.take(); + } + return None; + } + + if pending_line.trim_end().ends_with(';') { + pending.take() + } else { + None + } +} + #[cfg(test)] mod tests { use super::*; @@ -111,6 +133,44 @@ mod tests { assert!(matches.is_empty()); } + #[test] + fn path_completion_applies_after_path_command() { + assert!(wants_path_completion(".load ", ".load ".len())); + assert!(wants_path_completion( + ".open tests/da", + ".open tests/da".len() + )); + assert!(wants_path_completion( + ".save out.bin", + ".save out.bin".len() + )); + } + + #[test] + fn path_completion_does_not_apply_elsewhere() { + assert!(!wants_path_completion(".load", ".load".len())); + assert!(!wants_path_completion(".dump te", ".dump te".len())); + assert!(!wants_path_completion("add T 7", "add T 7".len())); + assert!(!wants_path_completion("", 0)); + } + + #[test] + fn path_completion_lists_matching_files() { + let helper = CommandHelper::new(); + let history = rustyline::history::DefaultHistory::new(); + let ctx = Context::new(&history); + let line = ".load tests/data/people.c"; + let (start, matches) = helper.complete(line, line.len(), &ctx).unwrap(); + assert_eq!(start, ".load ".len()); + assert!( + matches + .iter() + .any(|pair| pair.replacement == "tests/data/people.csv"), + "expected people.csv in completions, got: {:?}", + matches.iter().map(|p| &p.replacement).collect::>() + ); + } + #[test] fn statement_buffer_waits_for_semicolon() { let mut pending = None; diff --git a/packages/coln-store/src/repl/mod.rs b/packages/coln-store/src/repl/mod.rs index 3e51dbe2..174edcd2 100644 --- a/packages/coln-store/src/repl/mod.rs +++ b/packages/coln-store/src/repl/mod.rs @@ -33,16 +33,29 @@ struct Session { shell_mode: ShellMode, } +impl Session { + pub fn new(mode: ShellMode) -> Self { + Self { + loaded: None, + shell_mode: mode, + } + } +} + #[derive(Debug)] enum Step { Continue(String), Exit, } -pub fn run() -> Result<(), Box> { +pub fn run(enable_sql: bool) -> Result<(), Box> { let mut editor = Editor::::new()?; - editor.set_helper(Some(CommandHelper)); - let mut session = Session::default(); + editor.set_helper(Some(CommandHelper::new())); + let mut session = if enable_sql { + Session::new(ShellMode::Sql) + } else { + Session::new(ShellMode::Coln) + }; let mut pending_statement: Option = None; println!("coln-store repl"); From d9d60f6ee709ccac394f5bb9809ba1913723e49f Mon Sep 17 00:00:00 2001 From: Shuntian Liu Date: Fri, 3 Jul 2026 15:29:20 +0100 Subject: [PATCH 14/19] some repl churn --- packages/coln-store/AGENTS.md | 2 +- packages/coln-store/src/main.rs | 7 +- packages/coln-store/src/repl/error.rs | 23 ---- packages/coln-store/src/repl/exe/mod.rs | 10 +- packages/coln-store/src/repl/exe/sql.rs | 25 ++-- packages/coln-store/src/repl/mod.rs | 15 ++- packages/coln-store/src/repl/parse/coln.rs | 144 ++++++++++++--------- packages/coln-store/src/repl/parse/mod.rs | 23 +++- packages/coln-store/src/repl/parse/sql.rs | 141 +++++++++++--------- packages/coln-store/tests/test_repl.rs | 2 +- 10 files changed, 211 insertions(+), 181 deletions(-) delete mode 100644 packages/coln-store/src/repl/error.rs diff --git a/packages/coln-store/AGENTS.md b/packages/coln-store/AGENTS.md index 935f3f5a..051a67c4 100644 --- a/packages/coln-store/AGENTS.md +++ b/packages/coln-store/AGENTS.md @@ -79,7 +79,7 @@ Quick examples: ## Required Validation -Run these checks for any non-trivial change: +Run these checks for non-trivial change when you are told to. Otherwise there is no need to run checks. `just check` diff --git a/packages/coln-store/src/main.rs b/packages/coln-store/src/main.rs index 278e1c16..df43e659 100644 --- a/packages/coln-store/src/main.rs +++ b/packages/coln-store/src/main.rs @@ -2,18 +2,17 @@ // // SPDX-License-Identifier: Apache-2.0 OR MIT +use clap::Parser; use coln_store::repl; use tracing_subscriber::EnvFilter; -use clap::Parser; #[derive(Parser, Debug)] struct Args { #[arg(long, default_value_t = false, hide = true)] - enable_sql_mode: bool - + enable_sql_mode: bool, } -fn main() -> Result<(), Box> { +fn main() -> anyhow::Result<()> { tracing_subscriber::fmt() .with_env_filter( EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("coln_store=info")), diff --git a/packages/coln-store/src/repl/error.rs b/packages/coln-store/src/repl/error.rs deleted file mode 100644 index 6253a27e..00000000 --- a/packages/coln-store/src/repl/error.rs +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Coln contributors -// -// SPDX-License-Identifier: Apache-2.0 OR MIT - -use std::fmt; - -/// Parse failure for a single cell inside a `begin batch` block (before column index is known). -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum BatchCellParseError { - UnknownBinding(String), - InvalidValue(String), -} - -impl fmt::Display for BatchCellParseError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - BatchCellParseError::UnknownBinding(name) => write!(f, "unknown binding: {name}"), - BatchCellParseError::InvalidValue(message) => write!(f, "{message}"), - } - } -} - -impl std::error::Error for BatchCellParseError {} diff --git a/packages/coln-store/src/repl/exe/mod.rs b/packages/coln-store/src/repl/exe/mod.rs index fd731fdd..4ee20aa3 100644 --- a/packages/coln-store/src/repl/exe/mod.rs +++ b/packages/coln-store/src/repl/exe/mod.rs @@ -12,8 +12,7 @@ use anyhow::{Context, Result, anyhow, bail}; use crate::repl::{ Session, ShellMode, Step, - error::BatchCellParseError, - parse::{BatchAssignment, parse_cell_value, parse_cell_value_batch}, + parse::coln::{self, BatchAssignment, parse_cell_value, parse_cell_value_batch}, parse::{ColnCommand, MetaCommand, SqlCommand}, }; use crate::{ @@ -432,12 +431,15 @@ pub fn run_transact(store: &mut Store, assignments: &[BatchAssignment]) -> Resul let v = parse_cell_value_batch(&column.col_type, &a.row[idx], &bindings).map_err(|e| { match e { - BatchCellParseError::UnknownBinding(name) => { + coln::ParserError::UnknownBinding(name) => { anyhow!("unknown binding: {name}") } - BatchCellParseError::InvalidValue(message) => { + coln::ParserError::InvalidValue(message) => { anyhow!("column {idx}: {message}") } + coln::ParserError::InvalidInput(message) => { + anyhow!("invalid intput: {message}") + } } })?; values.push(v); diff --git a/packages/coln-store/src/repl/exe/sql.rs b/packages/coln-store/src/repl/exe/sql.rs index 94e1a158..7e44bf1b 100644 --- a/packages/coln-store/src/repl/exe/sql.rs +++ b/packages/coln-store/src/repl/exe/sql.rs @@ -2,7 +2,7 @@ use std::path::PathBuf; use anyhow::{Context, Result, anyhow, bail}; -use crate::ir::{BuiltinTy, ColType, ColumnEntry, EntityVariant, Path, Schema}; +use crate::ir::{ColumnEntry, EntityVariant, Path, Schema}; use crate::repl::Session; use crate::repl::exe::{LoadedState, SchemaSummary, add_rows}; use crate::repl::parse::{SqlCol as Col, SqlCommand as Command}; @@ -140,14 +140,7 @@ fn sql_schema_from_cols(columns: Vec) -> Schema { .into_iter() .map(|column| ColumnEntry { path: Path::from(column.col_name.as_str()), - col_type: match column.col_typ { - BuiltinTy::BuiltinInt => ColType::BuiltinTy { - builtin_ty: BuiltinTy::BuiltinInt, - }, - BuiltinTy::BuiltinStr => ColType::BuiltinTy { - builtin_ty: BuiltinTy::BuiltinStr, - }, - }, + col_type: column.col_typ, }) .collect(), primary_key: None, @@ -156,6 +149,8 @@ fn sql_schema_from_cols(columns: Vec) -> Schema { #[cfg(test)] mod tests { + use coln_flir_rs::ir::ColType; + use super::*; use crate::repl::ShellMode; @@ -170,11 +165,15 @@ mod tests { vec![ Col { col_name: "name".to_string(), - col_typ: BuiltinTy::BuiltinStr, + col_typ: ColType::BuiltinTy { + builtin_ty: coln_flir_rs::ir::BuiltinTy::BuiltinStr, + }, }, Col { col_name: "age".to_string(), - col_typ: BuiltinTy::BuiltinInt, + col_typ: ColType::BuiltinTy { + builtin_ty: coln_flir_rs::ir::BuiltinTy::BuiltinInt, + }, }, ], ) @@ -241,7 +240,9 @@ mod tests { "Person".to_string(), vec![Col { col_name: "email".to_string(), - col_typ: BuiltinTy::BuiltinStr, + col_typ: ColType::BuiltinTy { + builtin_ty: coln_flir_rs::ir::BuiltinTy::BuiltinStr, + }, }], ) .expect("create table"); diff --git a/packages/coln-store/src/repl/mod.rs b/packages/coln-store/src/repl/mod.rs index 174edcd2..e7eb3e1a 100644 --- a/packages/coln-store/src/repl/mod.rs +++ b/packages/coln-store/src/repl/mod.rs @@ -14,7 +14,6 @@ use crate::repl::parse::Command; use crate::repl::parse::parse_command; mod cli; -pub mod error; pub mod exe; pub mod parse; @@ -34,7 +33,7 @@ struct Session { } impl Session { - pub fn new(mode: ShellMode) -> Self { + fn new(mode: ShellMode) -> Self { Self { loaded: None, shell_mode: mode, @@ -48,7 +47,7 @@ enum Step { Exit, } -pub fn run(enable_sql: bool) -> Result<(), Box> { +pub fn run(enable_sql: bool) -> Result<()> { let mut editor = Editor::::new()?; editor.set_helper(Some(CommandHelper::new())); let mut session = if enable_sql { @@ -245,11 +244,15 @@ mod tests { columns: vec![ SqlCol { col_name: "name".to_string(), - col_typ: BuiltinTy::BuiltinStr, + col_typ: ColType::BuiltinTy { + builtin_ty: BuiltinTy::BuiltinStr, + }, }, SqlCol { col_name: "age".to_string(), - col_typ: BuiltinTy::BuiltinInt, + col_typ: ColType::BuiltinTy { + builtin_ty: BuiltinTy::BuiltinInt, + }, }, ], }) @@ -375,7 +378,7 @@ mod tests { let err = add_rows(&mut store, "Ref", &[vec!["7".to_string()]]).unwrap_err(); assert_eq!( err.to_string(), - "column 0: expected entity id like #:" + "column 0: invalid input value expected entity id like #:" ); } } diff --git a/packages/coln-store/src/repl/parse/coln.rs b/packages/coln-store/src/repl/parse/coln.rs index 6742283d..914dd7f1 100644 --- a/packages/coln-store/src/repl/parse/coln.rs +++ b/packages/coln-store/src/repl/parse/coln.rs @@ -7,11 +7,21 @@ use std::collections::HashMap; use crate::{ commit::hash::{CommitHash, HASH_SIZE}, ir::{BuiltinTy, ColType}, - repl::error::BatchCellParseError, table::{CellValue, RowId}, txn::ops::{TempRowId, TxnCellValue}, }; +/// Parse failure for a single cell inside a `begin batch` block (before column index is known). +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +pub(crate) enum ParserError { + #[error("unknown binding {0}")] + UnknownBinding(String), + #[error("{0}")] + InvalidValue(String), + #[error("invalid input value {0}")] + InvalidInput(String), +} + /// One `name = add
values (...);` step inside a batch block (exactly one row). #[derive(Debug, Clone, PartialEq, Eq)] pub struct BatchAssignment { @@ -21,7 +31,7 @@ pub struct BatchAssignment { } #[derive(Debug, PartialEq, Eq)] -pub enum Command { +pub(crate) enum Command { Add { table: String, rows: Vec>, @@ -30,20 +40,48 @@ pub enum Command { Batch { assignments: Vec }, } -pub fn parse_add_statement(input: &str) -> anyhow::Result { +fn invalid_input_err(s: &str) -> Result { + Err(ParserError::InvalidInput(s.into())) +} + +fn invalid_value_err(s: impl Into) -> Result { + Err(ParserError::InvalidValue(s.into())) +} + +pub(crate) fn parse_statement(input: &str) -> anyhow::Result { + let input = input.trim(); + if input.starts_with("begin transact") { + return Ok(parse_batch_block(input)?); + } + + let parts = shlex::split(input) + .ok_or_else(|| ParserError::InvalidInput("could not parse input".into()))?; + let Some(command) = parts.first() else { + return Ok(invalid_input_err("empty command")?); + }; + + match command.as_str() { + "add" => Ok(parse_add_statement(input)?), + _ => invalid_input_err(&format!( + "unknown statement: {command}. Statements must end with `;`, or use `.help` for meta commands." + ))?, + } +} + +fn parse_add_statement(input: &str) -> Result { let Some(rest) = input.strip_prefix("add ") else { - anyhow::bail!("usage: add
values (...), (...);"); + return invalid_input_err("usage: add
values (...), (...);"); }; let Some((table, rows_src)) = rest.split_once(" values ") else { - anyhow::bail!("usage: add
values (...), (...);"); + return invalid_input_err("usage: add
values (...), (...);"); }; let table = table.trim(); if table.is_empty() { - anyhow::bail!("usage: add
values (...), (...);"); + return invalid_input_err("usage: add
values (...), (...);"); } let rows = parse_add_rows(rows_src.trim())?; if rows.is_empty() { - anyhow::bail!("add requires at least one row"); + return invalid_input_err("add requires at least one row"); } Ok(Command::Add { table: table.to_string(), @@ -51,75 +89,52 @@ pub fn parse_add_statement(input: &str) -> anyhow::Result { }) } -pub fn parse_statement(input: &str) -> anyhow::Result { - let input = input.trim(); - if input.starts_with("begin transact") { - return parse_batch_block(input); - } - - let parts = shlex::split(input).ok_or_else(|| anyhow::anyhow!("could not parse input"))?; - let Some(command) = parts.first() else { - anyhow::bail!("empty command"); - }; - - match command.as_str() { - "add" => parse_add_statement(input), - _ => anyhow::bail!( - "unknown statement: {command}. Statements must end with `;`, or use `.help` for meta commands." - ), - } -} - /// Resolve one cell for a batch insert: entity columns accept `#id` or a prior binding name. pub(crate) fn parse_cell_value_batch( col_type: &ColType, raw: &str, bindings: &HashMap, -) -> Result { +) -> Result { match col_type { ColType::RowId { .. } => { if raw.starts_with('#') { - parse_cell_value(col_type, raw) - .map(Into::into) - .map_err(BatchCellParseError::InvalidValue) + parse_cell_value(col_type, raw).map(Into::into) } else if is_binding_ident(raw) { let id = bindings .get(raw) .copied() - .ok_or_else(|| BatchCellParseError::UnknownBinding(raw.to_string()))?; + .ok_or_else(|| ParserError::UnknownBinding(raw.to_string()))?; Ok(TxnCellValue::from(id)) } else { - Err(BatchCellParseError::InvalidValue(format!( + Err(ParserError::InvalidValue(format!( "expected entity id like #: or a binding name, got {raw}" ))) } } - _ => parse_cell_value(col_type, raw) - .map(Into::into) - .map_err(BatchCellParseError::InvalidValue), + _ => parse_cell_value(col_type, raw).map(Into::into), } } /// Parse `begin transact;` … `commit` (outer `;` already stripped by [`parse_command`]). -fn parse_batch_block(input: &str) -> anyhow::Result { +fn parse_batch_block(input: &str) -> Result { let input = input.trim(); let Some(rest) = input.strip_prefix("begin transact") else { - anyhow::bail!("internal error: expected begin transact"); + return invalid_input_err("internal error: expected begin transact"); }; let mut rest = rest.trim_start(); let Some(after_kw) = rest.strip_prefix(';') else { - anyhow::bail!("expected `begin transact;`"); + return invalid_input_err("expected `begin transact;`"); }; rest = after_kw.trim(); let Some(inner) = rest.strip_suffix("commit") else { - anyhow::bail!("transaction block must end with `commit`"); + return invalid_input_err("transaction block must end with `commit`"); }; let inner = inner.trim().strip_suffix(';').unwrap_or(inner).trim(); let assignments = parse_batch_assignments(inner)?; Ok(Command::Batch { assignments }) } -pub fn is_binding_ident(s: &str) -> bool { +fn is_binding_ident(s: &str) -> bool { let mut chars = s.chars(); let Some(first) = chars.next() else { return false; @@ -155,7 +170,7 @@ fn split_semicolon_statements(s: &str) -> Vec { out } -fn parse_batch_assignments(inner: &str) -> anyhow::Result> { +fn parse_batch_assignments(inner: &str) -> Result, ParserError> { let mut v = Vec::new(); for stmt in split_semicolon_statements(inner) { v.push(parse_batch_assignment(&stmt)?); @@ -163,30 +178,34 @@ fn parse_batch_assignments(inner: &str) -> anyhow::Result> Ok(v) } -fn parse_batch_assignment(line: &str) -> anyhow::Result { +fn parse_batch_assignment(line: &str) -> Result { let line = line.trim(); if line.is_empty() { - anyhow::bail!("empty statement inside batch block"); + return invalid_input_err("empty statement inside batch block"); } let Some((name, rhs)) = line.split_once(" = add ") else { - anyhow::bail!("expected `name = add
values (...)`, got: {line}"); + return invalid_input_err(&format!( + "expected `name = add
values (...)`, got: {line}" + )); }; let name = name.trim(); if name.is_empty() || !is_binding_ident(name) { - anyhow::bail!("invalid binding name: {name}"); + return invalid_input_err(&format!("invalid binding name: {name}")); } let rhs = rhs.trim(); let Some((table, rows_src)) = rhs.split_once(" values ") else { - anyhow::bail!("expected `values (...)` after table name in: {line}"); + return invalid_input_err(&format!( + "expected `values (...)` after table name in: {line}" + )); }; let table = table.trim(); if table.is_empty() { - anyhow::bail!("missing table name"); + return invalid_input_err("missing table name"); } let rows = parse_add_rows(rows_src.trim())?; if rows.len() != 1 { - anyhow::bail!( - "each batch assignment must insert exactly one row (one `values (...)` group)" + return invalid_input_err( + "each batch assignment must insert exactly one row (one `values (...)` group)", ); } let row = rows.into_iter().next().expect("one row"); @@ -208,7 +227,7 @@ fn parse_batch_assignment(line: &str) -> anyhow::Result { /// /// Regex is a poor fit here: a pattern like `\(([^)]*)\)` breaks when a string column contains /// `)`. -pub fn parse_add_rows(input: &str) -> anyhow::Result>> { +fn parse_add_rows(input: &str) -> Result>, ParserError> { let mut rows = Vec::new(); let mut chars = input.char_indices().peekable(); @@ -218,7 +237,7 @@ pub fn parse_add_rows(input: &str) -> anyhow::Result>> { continue; } if ch != '(' { - anyhow::bail!("expected `(` to start a row"); + return invalid_input_err("expected `(` to start a row"); } chars.next(); @@ -245,7 +264,7 @@ pub fn parse_add_rows(input: &str) -> anyhow::Result>> { } let Some(end) = end else { - anyhow::bail!("unterminated row in add statement"); + return invalid_input_err("unterminated row in add statement"); }; let row_src = &input[start..end]; let row = split_add_row_tokens(row_src); @@ -289,35 +308,36 @@ fn split_add_row_tokens(row_src: &str) -> Vec { out } -pub fn parse_cell_value(col_type: &ColType, raw: &str) -> Result { +pub(crate) fn parse_cell_value(col_type: &ColType, raw: &str) -> Result { match col_type { ColType::RowId { .. } => parse_row_id(raw).map(CellValue::Id), ColType::BuiltinTy { builtin_ty } => match builtin_ty { BuiltinTy::BuiltinInt => raw .parse::() .map(CellValue::Int) - .map_err(|_| format!("invalid int: {raw}")), + .map_err(|_| ParserError::InvalidValue(format!("invalid int: {raw}"))), BuiltinTy::BuiltinStr => Ok(CellValue::Str(raw.to_string())), }, } } -fn parse_row_id(raw: &str) -> Result { - let rest = raw - .strip_prefix('#') - .ok_or_else(|| "expected entity id like #:".to_string())?; +fn parse_row_id(raw: &str) -> Result { + let Some(rest) = raw.strip_prefix('#') else { + return invalid_input_err("expected entity id like #:"); + }; let Some((hash_hex, counter_raw)) = rest.split_once(':') else { - return Err("expected entity id like #:".to_string()); + return invalid_input_err("expected entity id like #:"); }; - let hash_bytes = hex::decode(hash_hex).map_err(|_| format!("invalid entity id: {raw}"))?; + let hash_bytes = hex::decode(hash_hex) + .map_err(|_| ParserError::InvalidValue(format!("invalid entity id: {raw}")))?; if hash_bytes.len() != HASH_SIZE { - return Err(format!("invalid entity id: {raw}")); + return invalid_value_err(format!("invalid entity id: {raw}")); } let mut hash = [0; HASH_SIZE]; hash.copy_from_slice(&hash_bytes); let counter = counter_raw .parse::() - .map_err(|_| format!("invalid entity id: {raw}"))?; + .map_err(|_| ParserError::InvalidValue(format!("invalid entity id: {raw}")))?; Ok(RowId { commit: CommitHash(hash), counter, diff --git a/packages/coln-store/src/repl/parse/mod.rs b/packages/coln-store/src/repl/parse/mod.rs index 2062fd72..eb878166 100644 --- a/packages/coln-store/src/repl/parse/mod.rs +++ b/packages/coln-store/src/repl/parse/mod.rs @@ -1,12 +1,11 @@ use anyhow::{Result, bail}; -pub use coln::{BatchAssignment, Command as ColnCommand}; -pub(crate) use coln::{parse_cell_value, parse_cell_value_batch}; +pub(crate) use coln::Command as ColnCommand; pub(crate) use meta::Command as MetaCommand; pub(crate) use sql::{Col as SqlCol, Command as SqlCommand}; use crate::repl::ShellMode; -mod coln; +pub mod coln; mod meta; mod sql; @@ -37,6 +36,10 @@ pub(crate) fn parse_command(mode: ShellMode, input: &str) -> Result { #[cfg(test)] mod tests { + use coln_flir_rs::ir::BuiltinTy; + + use crate::repl::parse::coln::BatchAssignment; + use super::*; #[test] @@ -193,9 +196,19 @@ mod tests { assert_eq!(table_name, "Person"); assert_eq!(columns.len(), 2); assert_eq!(columns[0].col_name, "name"); - assert_eq!(columns[0].col_typ, crate::ir::BuiltinTy::BuiltinStr); + assert_eq!( + columns[0].col_typ, + crate::ir::ColType::BuiltinTy { + builtin_ty: BuiltinTy::BuiltinStr + } + ); assert_eq!(columns[1].col_name, "age"); - assert_eq!(columns[1].col_typ, crate::ir::BuiltinTy::BuiltinInt); + assert_eq!( + columns[1].col_typ, + crate::ir::ColType::BuiltinTy { + builtin_ty: BuiltinTy::BuiltinInt + } + ); } #[test] diff --git a/packages/coln-store/src/repl/parse/sql.rs b/packages/coln-store/src/repl/parse/sql.rs index 2481e8e0..c7e4e4c5 100644 --- a/packages/coln-store/src/repl/parse/sql.rs +++ b/packages/coln-store/src/repl/parse/sql.rs @@ -1,8 +1,9 @@ use anyhow::{Context, Result, bail}; +use coln_flir_rs::ir::BuiltinTy::{BuiltinInt, BuiltinStr}; use sqlparser::{ ast::{ - CopyLegacyOption, CopyOption, CopySource, CopyTarget, DataType, Expr, SetExpr, Statement, - TableObject, Value, + ColumnDef, CopyLegacyOption, CopyOption, CopySource, CopyTarget, DataType, Expr, Insert, + ObjectName, SetExpr, Statement, TableObject, Value, }, dialect::GenericDialect, parser::Parser, @@ -11,7 +12,7 @@ use sqlparser::{ #[derive(Debug, PartialEq, Eq)] pub(crate) struct Col { pub(crate) col_name: String, - pub(crate) col_typ: coln_flir_rs::ir::BuiltinTy, + pub(crate) col_typ: coln_flir_rs::ir::ColType, } #[derive(Debug, PartialEq, Eq)] @@ -27,7 +28,6 @@ pub(crate) enum Command { }, /// `COPY
FROM '' WITH (FORMAT csv, HEADER true)`, with an /// optional `DELIMITER` such as `E'\t'` for tab-separated files. - // TODO use duckdb style import CopyFromCsv { table_name: String, path: String, @@ -45,66 +45,9 @@ pub(crate) fn parse_sql_statement(input: &str) -> Result { }; if let Statement::CreateTable(sqlparser::ast::CreateTable { name, columns, .. }) = statement { - let mut cols = Vec::new(); - for col in columns { - let col_typ = match &col.data_type { - DataType::Int(_) | DataType::Integer(_) => coln_flir_rs::ir::BuiltinTy::BuiltinInt, - DataType::Text | DataType::String(_) | DataType::Varchar(_) => { - coln_flir_rs::ir::BuiltinTy::BuiltinStr - } - other => bail!("unsupported data type: {other}"), - }; - cols.push(Col { - col_name: col.name.to_string(), - col_typ, - }) - } - Ok(Command::CreateTable { - table_name: name.to_string(), - columns: cols, - }) + parse_create_table(name, columns) } else if let Statement::Insert(insert) = statement { - let TableObject::TableName(table) = &insert.table else { - bail!("unsupported INSERT target: {}", insert.table); - }; - if insert.columns.is_empty() { - bail!("INSERT must specify column names"); - } - if !insert.assignments.is_empty() { - bail!("INSERT assignments are not supported"); - } - if insert.returning.is_some() { - bail!("INSERT RETURNING is not supported"); - } - - let source = insert - .source - .as_ref() - .ok_or_else(|| anyhow::anyhow!("INSERT must provide VALUES"))?; - let SetExpr::Values(values) = source.body.as_ref() else { - bail!("only INSERT ... VALUES is supported"); - }; - - let col_names = insert - .columns - .iter() - .map(ToString::to_string) - .collect::>(); - let rows = values - .rows - .iter() - .map(|row| { - row.iter() - .map(sql_expr_to_raw_value) - .collect::>>() - }) - .collect::>>()?; - - Ok(Command::Insert { - table_name: table.to_string(), - col_names, - values: rows, - }) + parse_insert(insert) } else if let Statement::Copy { source, to, @@ -120,6 +63,78 @@ pub(crate) fn parse_sql_statement(input: &str) -> Result { } } +fn parse_create_table(name: &ObjectName, columns: &[ColumnDef]) -> Result { + let mut cols = Vec::new(); + for col in columns { + let col_typ = match &col.data_type { + DataType::Int(_) | DataType::Integer(_) => coln_flir_rs::ir::ColType::BuiltinTy { + builtin_ty: BuiltinInt, + }, + DataType::Text | DataType::String(_) | DataType::Varchar(_) => { + coln_flir_rs::ir::ColType::BuiltinTy { + builtin_ty: BuiltinStr, + } + } + DataType::Uuid => coln_flir_rs::ir::ColType::RowId { + path: "dummy".into(), + }, + other => bail!("unsupported data type: {other}"), + }; + cols.push(Col { + col_name: col.name.to_string(), + col_typ, + }) + } + Ok(Command::CreateTable { + table_name: name.to_string(), + columns: cols, + }) +} + +fn parse_insert(insert: &Insert) -> Result { + let TableObject::TableName(table) = &insert.table else { + bail!("unsupported INSERT target: {}", insert.table); + }; + if insert.columns.is_empty() { + bail!("INSERT must specify column names"); + } + if !insert.assignments.is_empty() { + bail!("INSERT assignments are not supported"); + } + if insert.returning.is_some() { + bail!("INSERT RETURNING is not supported"); + } + + let source = insert + .source + .as_ref() + .ok_or_else(|| anyhow::anyhow!("INSERT must provide VALUES"))?; + let SetExpr::Values(values) = source.body.as_ref() else { + bail!("only INSERT ... VALUES is supported"); + }; + + let col_names = insert + .columns + .iter() + .map(ToString::to_string) + .collect::>(); + let rows = values + .rows + .iter() + .map(|row| { + row.iter() + .map(sql_expr_to_raw_value) + .collect::>>() + }) + .collect::>>()?; + + Ok(Command::Insert { + table_name: table.to_string(), + col_names, + values: rows, + }) +} + fn parse_copy_from_csv( source: &CopySource, to: bool, diff --git a/packages/coln-store/tests/test_repl.rs b/packages/coln-store/tests/test_repl.rs index bf3e364b..f7219e11 100644 --- a/packages/coln-store/tests/test_repl.rs +++ b/packages/coln-store/tests/test_repl.rs @@ -9,7 +9,7 @@ use std::path::PathBuf; use coln_flir_rs::ir::{FlatRealm, Path}; use coln_store::{ - repl::{exe::run_transact, parse::BatchAssignment}, + repl::{exe::run_transact, parse::coln::BatchAssignment}, store::Store, }; From b847f306d910bf6eb80718c87eb7eee5acdc4cbc Mon Sep 17 00:00:00 2001 From: Shuntian Liu Date: Fri, 3 Jul 2026 17:58:17 +0100 Subject: [PATCH 15/19] feat: repl history persistence --- packages/coln-store/src/main.rs | 8 +++++++- packages/coln-store/src/repl/cli.rs | 11 +++++++++++ packages/coln-store/src/repl/mod.rs | 7 ++++++- packages/coln-store/src/repl/parse/coln.rs | 5 +++-- 4 files changed, 27 insertions(+), 4 deletions(-) diff --git a/packages/coln-store/src/main.rs b/packages/coln-store/src/main.rs index df43e659..b2a9effe 100644 --- a/packages/coln-store/src/main.rs +++ b/packages/coln-store/src/main.rs @@ -13,9 +13,15 @@ struct Args { } fn main() -> anyhow::Result<()> { + const DEFAULT_FILTER: &str = if cfg!(debug_assertions) { + "coln_store=debug" + } else { + "coln_store=info" + }; + tracing_subscriber::fmt() .with_env_filter( - EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("coln_store=info")), + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(DEFAULT_FILTER)), ) .init(); diff --git a/packages/coln-store/src/repl/cli.rs b/packages/coln-store/src/repl/cli.rs index 8bb1e973..2476ec9f 100644 --- a/packages/coln-store/src/repl/cli.rs +++ b/packages/coln-store/src/repl/cli.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use anyhow::Result; use rustyline::completion::{Completer, FilenameCompleter, Pair}; use rustyline::error::ReadlineError; @@ -15,6 +17,15 @@ const COMMANDS: &[&str] = &[ const PATH_COMMANDS: &[&str] = &[".load", ".open", ".save"]; +const HISTORY_FILE: &str = ".coln-store_history"; + +pub(super) fn history_path() -> PathBuf { + std::env::var_os("HOME") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from(".")) + .join(HISTORY_FILE) +} + pub(super) struct CommandHelper { filename_completer: FilenameCompleter, } diff --git a/packages/coln-store/src/repl/mod.rs b/packages/coln-store/src/repl/mod.rs index e7eb3e1a..8b65fd8d 100644 --- a/packages/coln-store/src/repl/mod.rs +++ b/packages/coln-store/src/repl/mod.rs @@ -8,7 +8,9 @@ use rustyline::error::ReadlineError; use rustyline::history::DefaultHistory; use tracing::warn; -use crate::repl::cli::{CommandHelper, is_statement_start, prompt, push_statement_line}; +use crate::repl::cli::{ + CommandHelper, history_path, is_statement_start, prompt, push_statement_line, +}; use crate::repl::exe::{LoadedState, execute_coln, execute_meta, execute_sql}; use crate::repl::parse::Command; use crate::repl::parse::parse_command; @@ -50,6 +52,8 @@ enum Step { pub fn run(enable_sql: bool) -> Result<()> { let mut editor = Editor::::new()?; editor.set_helper(Some(CommandHelper::new())); + let _ = editor.load_history(&history_path()); + let mut session = if enable_sql { Session::new(ShellMode::Sql) } else { @@ -118,6 +122,7 @@ pub fn run(enable_sql: bool) -> Result<()> { } } + let _ = editor.append_history(&history_path()); Ok(()) } diff --git a/packages/coln-store/src/repl/parse/coln.rs b/packages/coln-store/src/repl/parse/coln.rs index 914dd7f1..1e8be7bb 100644 --- a/packages/coln-store/src/repl/parse/coln.rs +++ b/packages/coln-store/src/repl/parse/coln.rs @@ -330,11 +330,12 @@ fn parse_row_id(raw: &str) -> Result { }; let hash_bytes = hex::decode(hash_hex) .map_err(|_| ParserError::InvalidValue(format!("invalid entity id: {raw}")))?; - if hash_bytes.len() != HASH_SIZE { + if hash_bytes.len() > HASH_SIZE { return invalid_value_err(format!("invalid entity id: {raw}")); } let mut hash = [0; HASH_SIZE]; - hash.copy_from_slice(&hash_bytes); + let ofs = HASH_SIZE - hash_bytes.len(); + hash[ofs..].copy_from_slice(&hash_bytes); let counter = counter_raw .parse::() .map_err(|_| ParserError::InvalidValue(format!("invalid entity id: {raw}")))?; From f174f89de470a880dbcc93273087f33ba6b194d2 Mon Sep 17 00:00:00 2001 From: Shuntian Liu Date: Fri, 3 Jul 2026 18:13:44 +0100 Subject: [PATCH 16/19] fix: licenses header --- packages/coln-js-runtime/src/rust/handles.rs | 1 + packages/coln-store/src/repl/cli.rs | 4 ++++ packages/coln-store/src/repl/exe/sql.rs | 5 +++++ packages/coln-store/src/repl/parse/meta.rs | 4 ++++ packages/coln-store/src/repl/parse/mod.rs | 4 ++++ packages/coln-store/src/repl/parse/sql.rs | 4 ++++ packages/coln-store/src/roweq/mod.rs | 5 ++++- 7 files changed, 26 insertions(+), 1 deletion(-) diff --git a/packages/coln-js-runtime/src/rust/handles.rs b/packages/coln-js-runtime/src/rust/handles.rs index 4efe2be1..c1e3a6a5 100644 --- a/packages/coln-js-runtime/src/rust/handles.rs +++ b/packages/coln-js-runtime/src/rust/handles.rs @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2026 Coln contributors // // SPDX-License-Identifier: Apache-2.0 OR MIT + use coln_flir_rs::ir; use coln_store::{ commit::hash::CommitHash as StoreCommitHash, diff --git a/packages/coln-store/src/repl/cli.rs b/packages/coln-store/src/repl/cli.rs index 2476ec9f..8b350691 100644 --- a/packages/coln-store/src/repl/cli.rs +++ b/packages/coln-store/src/repl/cli.rs @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + use std::path::PathBuf; use anyhow::Result; diff --git a/packages/coln-store/src/repl/exe/sql.rs b/packages/coln-store/src/repl/exe/sql.rs index 7e44bf1b..e76760be 100644 --- a/packages/coln-store/src/repl/exe/sql.rs +++ b/packages/coln-store/src/repl/exe/sql.rs @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + use std::path::PathBuf; use anyhow::{Context, Result, anyhow, bail}; @@ -95,6 +99,7 @@ fn copy_from_csv( if rows.is_empty() { return Ok(format!("copied 0 rows into {table_name}")); } + tracing::debug!("ready to copy {} rows into the store", rows.len()); let row_ids = add_rows(&mut loaded.store, table_name, &rows)?; Ok(format!("copied {} rows into {table_name}", row_ids.len())) } diff --git a/packages/coln-store/src/repl/parse/meta.rs b/packages/coln-store/src/repl/parse/meta.rs index a8d2d561..650d588a 100644 --- a/packages/coln-store/src/repl/parse/meta.rs +++ b/packages/coln-store/src/repl/parse/meta.rs @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + #[derive(Debug, PartialEq, Eq)] pub(crate) enum Command { Help, diff --git a/packages/coln-store/src/repl/parse/mod.rs b/packages/coln-store/src/repl/parse/mod.rs index eb878166..72d745f3 100644 --- a/packages/coln-store/src/repl/parse/mod.rs +++ b/packages/coln-store/src/repl/parse/mod.rs @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + use anyhow::{Result, bail}; pub(crate) use coln::Command as ColnCommand; pub(crate) use meta::Command as MetaCommand; diff --git a/packages/coln-store/src/repl/parse/sql.rs b/packages/coln-store/src/repl/parse/sql.rs index c7e4e4c5..9651713c 100644 --- a/packages/coln-store/src/repl/parse/sql.rs +++ b/packages/coln-store/src/repl/parse/sql.rs @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + use anyhow::{Context, Result, bail}; use coln_flir_rs::ir::BuiltinTy::{BuiltinInt, BuiltinStr}; use sqlparser::{ diff --git a/packages/coln-store/src/roweq/mod.rs b/packages/coln-store/src/roweq/mod.rs index fa69bcbf..5f451d64 100644 --- a/packages/coln-store/src/roweq/mod.rs +++ b/packages/coln-store/src/roweq/mod.rs @@ -1,6 +1,9 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + use crate::table::RowId; -mod hashdag; pub(crate) mod rowing; #[derive(Debug)] From 3280d82d7ee5517654f49c31e9eb69c228fbdf5d Mon Sep 17 00:00:00 2001 From: Shuntian Liu Date: Fri, 3 Jul 2026 18:14:10 +0100 Subject: [PATCH 17/19] clean: remove hashdag Probably will not be used in the future --- packages/coln-store/src/roweq/hashdag.rs | 319 ----------------------- 1 file changed, 319 deletions(-) delete mode 100644 packages/coln-store/src/roweq/hashdag.rs diff --git a/packages/coln-store/src/roweq/hashdag.rs b/packages/coln-store/src/roweq/hashdag.rs deleted file mode 100644 index 2acbeac2..00000000 --- a/packages/coln-store/src/roweq/hashdag.rs +++ /dev/null @@ -1,319 +0,0 @@ -#![allow(unused)] - -use std::collections::HashMap; - -use crate::commit::leb128 as commit_leb128; -use crate::commit::wire::prim; -use crate::roweq::ObservedOutcome; -use crate::table::CellValue; -use crate::table::RowId; -use crate::table::Table; - -const HASH_SIZE: usize = 16; - -#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] -pub struct ContentHash([u8; HASH_SIZE]); - -impl ContentHash { - pub(crate) fn as_bytes(&self) -> &[u8] { - &self.0 - } - - fn from_hasher(hasher: blake3::Hasher) -> Self { - let digest = hasher.finalize(); - Self(digest.as_bytes()[..HASH_SIZE].try_into().unwrap()) - } -} - -enum Tag { - Int, - Str, - LeafId, - InternalId, - EntireRow, -} - -impl Tag { - fn num(&self) -> u8 { - match self { - Tag::Int => 0x01, - Tag::Str => 0x02, - Tag::LeafId => 0x03, - Tag::InternalId => 0x04, - Tag::EntireRow => 0x10, - } - } -} - -struct ContentHasher { - inner: blake3::Hasher, -} - -impl ContentHasher { - fn new() -> Self { - Self { - inner: blake3::Hasher::new(), - } - } - - fn tag(&mut self, tag: Tag) { - self.inner.update(&[tag.num()]); - } - - fn bytes(&mut self, bytes: &[u8]) { - self.inner.update(bytes); - } - - fn row_id(&mut self, rid: RowId) { - self.bytes(rid.commit.as_bytes()); - let mut buf = Vec::new(); - commit_leb128::write_u32(&mut buf, rid.counter); - self.bytes(&buf); - } - - fn content_hash(&mut self, hash: ContentHash) { - self.bytes(hash.as_bytes()); - } - - fn finish(self) -> ContentHash { - ContentHash::from_hasher(self.inner) - } -} - -pub(crate) struct Index { - by_row: HashMap, - canonical: HashMap, -} - -#[derive(Debug, thiserror::Error)] -pub enum HashDagError { - #[error("child hash missing when constructing parent hash")] - MissingChildHash { row_id: RowId }, -} - -impl Index { - /// Given a `row_id` returns the canonical `row_id` for it. Two rows map to - /// the same id when they contain the same hash, and the canonical id is the - /// lexical smaller one - pub fn canonical(&self, row_id: &RowId) -> RowId { - *self - .by_row - .get(row_id) - .and_then(|h| self.canonical.get(h)) - .unwrap_or(row_id) - } - - pub fn hash_row_id(&self, rid: RowId) -> ContentHash { - let mut hasher = ContentHasher::new(); - hasher.tag(Tag::LeafId); - hasher.row_id(rid); - hasher.finish() - } - - pub(crate) fn hash_cell(&self, val: &CellValue) -> Result { - let mut hasher = ContentHasher::new(); - match val { - CellValue::Id(rid) => { - let h = self - .by_row - .get(rid) - .ok_or(HashDagError::MissingChildHash { row_id: *rid })?; - - hasher.tag(Tag::InternalId); - hasher.content_hash(*h); - } - CellValue::Int(i) => { - hasher.tag(Tag::Int); - let mut buf = Vec::new(); - commit_leb128::write_i64(&mut buf, *i); - hasher.bytes(&buf); - } - CellValue::Str(s) => { - hasher.tag(Tag::Str); - hasher.bytes(s.as_bytes()); - } - } - - Ok(hasher.finish()) - } - - pub(crate) fn hash_row( - &self, - table: &Table, - rid: RowId, - vals: &[CellValue], - ) -> Result { - let mut hasher = ContentHasher::new(); - hasher.tag(Tag::EntireRow); - hasher.bytes(&prim::encode_path(table.path())); - - if !table.hashcons() { - let h = self.hash_row_id(rid); - hasher.content_hash(h); - } - for v in vals { - let h = self.hash_cell(v)?; - hasher.content_hash(h); - } - Ok(hasher.finish()) - } - - pub(crate) fn observe(&mut self, table: &Table, rid: RowId, h: ContentHash) -> ObservedOutcome { - if let Some(existing) = self.by_row.get(&rid) { - debug_assert_eq!( - existing, &h, - "row id was observed with a different content hash" - ); - } - self.by_row.insert(rid, h); - - if !table.hashcons() { - // no need to dedup, this table is not a hashcons table - return ObservedOutcome::Inserted(rid); - } - - match self.canonical.get(&h) { - None => { - self.canonical.insert(h, rid); - ObservedOutcome::Inserted(rid) - } - Some(&old) if rid < old => { - self.canonical.insert(h, rid); - ObservedOutcome::Swap { old, new: rid } - } - Some(&old) if old == rid => ObservedOutcome::KeptOld(rid), - Some(&old) => ObservedOutcome::KeptOld(old), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::commit::hash::CommitHash; - use crate::ir::{BuiltinTy, ColType, ColumnEntry, EntityVariant, Path, Schema}; - - fn row_id(commit_byte: u8, counter: u32) -> RowId { - RowId { - commit: CommitHash([commit_byte; 32]), - counter, - } - } - - fn empty_index() -> Index { - Index { - by_row: HashMap::new(), - canonical: HashMap::new(), - } - } - - fn int_table(path: &str, hashcons: bool) -> Table { - let schema = Schema { - entity_variant: EntityVariant::Table, - columns: vec![ColumnEntry { - path: Path::from("value"), - col_type: ColType::BuiltinTy { - builtin_ty: BuiltinTy::BuiltinInt, - }, - }], - primary_key: None, - }; - let mut table = Table::new(Path::from(path), schema); - table.set_hashcons_for_test(hashcons); - table - } - - #[test] - fn hash_row_id_distinguishes_row_ids() { - let index = empty_index(); - let first = row_id(1, 0); - let same = row_id(1, 0); - let different_counter = row_id(1, 1); - let different_commit = row_id(2, 0); - - assert_eq!(index.hash_row_id(first), index.hash_row_id(same)); - assert_ne!( - index.hash_row_id(first), - index.hash_row_id(different_counter) - ); - assert_ne!( - index.hash_row_id(first), - index.hash_row_id(different_commit) - ); - } - - #[test] - fn hash_cell_missing_child_hash_returns_error() { - let index = empty_index(); - let missing = row_id(1, 0); - - let err = index - .hash_cell(&CellValue::Id(missing)) - .expect_err("missing child hash should be reported"); - - assert!(matches!( - err, - HashDagError::MissingChildHash { row_id } if row_id == missing - )); - } - - #[test] - fn hash_cell_id_uses_child_content_hash() { - let mut index = empty_index(); - let child = row_id(1, 0); - let child_hash = index.hash_row_id(child); - index.by_row.insert(child, child_hash); - - let id_hash = index - .hash_cell(&CellValue::Id(child)) - .expect("child hash is present"); - - assert_ne!(id_hash, index.hash_row_id(child)); - assert_eq!( - id_hash, - index - .hash_cell(&CellValue::Id(child)) - .expect("child hash is still present") - ); - } - - #[test] - fn hash_row_non_hashcons_includes_own_row_id() { - let index = empty_index(); - let table = int_table("T", false); - let values = vec![CellValue::Int(7)]; - - let first = index - .hash_row(&table, row_id(1, 0), &values) - .expect("hash first row"); - let same = index - .hash_row(&table, row_id(1, 0), &values) - .expect("hash same row"); - let different = index - .hash_row(&table, row_id(1, 1), &values) - .expect("hash different row"); - - assert_eq!(first, same); - assert_ne!(first, different); - } - - #[test] - fn hash_row_hashcons_excludes_own_row_id() { - let index = empty_index(); - let table = int_table("T", true); - let values = vec![CellValue::Int(7)]; - - let first = index - .hash_row(&table, row_id(1, 0), &values) - .expect("hash first row"); - let same_content = index - .hash_row(&table, row_id(1, 1), &values) - .expect("hash same content"); - let different_content = index - .hash_row(&table, row_id(1, 1), &[CellValue::Int(8)]) - .expect("hash different content"); - - assert_eq!(first, same_content); - assert_ne!(first, different_content); - } -} From b22b1dcffa9111d3b0232d2111bf2782cd9fd463 Mon Sep 17 00:00:00 2001 From: Shuntian Liu Date: Fri, 3 Jul 2026 18:14:18 +0100 Subject: [PATCH 18/19] chore: update gitignore --- packages/coln-store/.gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/coln-store/.gitignore b/packages/coln-store/.gitignore index 20be2411..26eb7d1f 100644 --- a/packages/coln-store/.gitignore +++ b/packages/coln-store/.gitignore @@ -67,3 +67,6 @@ tmp/ # colnstore *.colnstore + +# compressed data +*.zst From e9375dfbf3685c07c6be56aa825e9cdf10578c56 Mon Sep 17 00:00:00 2001 From: Shuntian Liu Date: Fri, 3 Jul 2026 18:20:53 +0100 Subject: [PATCH 19/19] fix: feature flag `create_table` Because this is only used in SQL mode right now. --- packages/coln-store/src/store/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/coln-store/src/store/mod.rs b/packages/coln-store/src/store/mod.rs index b7b3d6e9..5f86ac66 100644 --- a/packages/coln-store/src/store/mod.rs +++ b/packages/coln-store/src/store/mod.rs @@ -180,6 +180,8 @@ impl Store { Ok(graph) } + #[cfg(feature = "native")] + // used in SQL mode only pub(crate) fn create_table( &mut self, path: ir::Path,