From 9a634ff9d76e9c62b6ec501e68bacfcd6489f761 Mon Sep 17 00:00:00 2001 From: Ziyang Hu Date: Sat, 13 May 2023 12:02:13 +0800 Subject: [PATCH] store query results in script as ephemeral relations, as discussed in https://github.com/cozodb/cozo/issues/110 --- cozo-core/src/cozoscript.pest | 12 ++- cozo-core/src/parse/imperative.rs | 60 +++++++++--- cozo-core/src/parse/mod.rs | 20 ++-- cozo-core/src/runtime/imperative.rs | 81 ++++++++++++++++- cozo-core/src/runtime/tests.rs | 136 ++++++++++++++++++---------- 5 files changed, 231 insertions(+), 78 deletions(-) diff --git a/cozo-core/src/cozoscript.pest b/cozo-core/src/cozoscript.pest index 90adaa8a..16a871eb 100644 --- a/cozo-core/src/cozoscript.pest +++ b/cozo-core/src/cozoscript.pest @@ -55,6 +55,7 @@ var = @{(XID_START | "_") ~ (XID_CONTINUE | "_")*} param = @{"$" ~ (XID_CONTINUE | "_")*} ident = @{XID_START ~ ("_" | XID_CONTINUE)*} underscore_ident = @{("_" | XID_START) ~ ("_" | XID_CONTINUE)*} +definitely_underscore_ident = @{"_" ~ XID_CONTINUE+} relation_ident = @{"*" ~ (compound_or_index_ident | underscore_ident)} search_index_ident = _{"~" ~ compound_or_index_ident} compound_ident = @{ident ~ ("." ~ ident)*} @@ -95,7 +96,7 @@ not_op = @{"not" ~ !XID_CONTINUE} apply = {ident ~ "(" ~ apply_args ~ ")"} apply_args = {(expr ~ ",")* ~ expr?} named_apply_args = {(named_apply_pair ~ ",")* ~ named_apply_pair?} -named_apply_pair = {ident ~ (":" ~ expr)?} +named_apply_pair = {underscore_ident ~ (":" ~ expr)?} grouped = _{"(" ~ rule_body ~ ")"} expr = {unary_op* ~ term ~ (operation ~ unary_op* ~ term)*} @@ -232,9 +233,10 @@ vec_el_type = {"F32" | "F64" | "Float" | "Double" } imperative_stmt = _{ break_stmt | continue_stmt | return_stmt | debug_stmt | - query_script_inner | ignore_error_script | if_chain | if_not_chain | loop_block | temp_swap + imperative_clause | ignore_error_script | if_chain | if_not_chain | loop_block | temp_swap } -imperative_condition = _{underscore_ident | query_script_inner} +imperative_clause = {query_script_inner ~ ("as" ~ definitely_underscore_ident)?} +imperative_condition = _{underscore_ident | imperative_clause} if_chain = {"%if" ~ imperative_condition ~ "%then"? ~ imperative_block ~ ("%else" ~ imperative_block)? ~ "%end" } @@ -243,9 +245,9 @@ if_not_chain = {"%if_not" ~ imperative_condition ~ ("%else" ~ imperative_block)? ~ "%end" } imperative_block = {imperative_stmt+} break_stmt = {"%break" ~ ident?} -ignore_error_script = {"%ignore_error" ~ query_script_inner} +ignore_error_script = {"%ignore_error" ~ imperative_clause} continue_stmt = {"%continue" ~ ident?} -return_stmt = {"%return" ~ (ident | underscore_ident | query_script_inner)*} +return_stmt = {"%return" ~ (ident | underscore_ident | imperative_clause)*} loop_block = {("%mark" ~ ident)? ~ "%loop" ~ imperative_block ~ "%end"} temp_swap = {"%swap" ~ underscore_ident ~ underscore_ident} debug_stmt = {"%debug" ~ (ident | underscore_ident)} diff --git a/cozo-core/src/parse/imperative.rs b/cozo-core/src/parse/imperative.rs index d1811ce7..c4373826 100644 --- a/cozo-core/src/parse/imperative.rs +++ b/cozo-core/src/parse/imperative.rs @@ -17,7 +17,9 @@ use smartstring::SmartString; use thiserror::Error; use crate::parse::query::parse_query; -use crate::parse::{ExtractSpan, ImperativeProgram, ImperativeStmt, Pair, Rule, SourceSpan}; +use crate::parse::{ + ExtractSpan, ImperativeProgram, ImperativeStmt, ImperativeStmtClause, Pair, Rule, SourceSpan, +}; use crate::{DataValue, FixedRule, ValidityTs}; pub(crate) fn parse_imperative_block( @@ -86,8 +88,15 @@ fn parse_imperative_stmt( rets.push(Right(rel)); } Rule::query_script_inner => { - let prog = parse_query(p.into_inner(), param_pool, fixed_rules, cur_vld)?; - rets.push(Left(prog)) + let mut src = p.into_inner(); + let prog = parse_query( + src.next().unwrap().into_inner(), + param_pool, + fixed_rules, + cur_vld, + )?; + let store_as = src.next().map(|p| SmartString::from(p.as_str().trim())); + rets.push(Left(ImperativeStmtClause { prog, store_as })) } _ => unreachable!(), } @@ -100,12 +109,17 @@ fn parse_imperative_stmt( let condition = inner.next().unwrap(); let cond = match condition.as_rule() { Rule::underscore_ident => Left(SmartString::from(condition.as_str())), - Rule::query_script_inner => Right(parse_query( - condition.into_inner(), - param_pool, - fixed_rules, - cur_vld, - )?), + Rule::query_script_inner => { + let mut src = condition.into_inner(); + let prog = parse_query( + src.next().unwrap().into_inner(), + param_pool, + fixed_rules, + cur_vld, + )?; + let store_as = src.next().map(|p| SmartString::from(p.as_str().trim())); + Right(ImperativeStmtClause { prog, store_as }) + } _ => unreachable!(), }; let body = inner @@ -161,14 +175,32 @@ fn parse_imperative_stmt( temp: SmartString::from(name), } } - Rule::query_script_inner => { - let prog = parse_query(pair.into_inner(), param_pool, fixed_rules, cur_vld)?; - ImperativeStmt::Program { prog } + Rule::imperative_clause => { + let mut src = pair.into_inner(); + let prog = parse_query( + src.next().unwrap().into_inner(), + param_pool, + fixed_rules, + cur_vld, + )?; + let store_as = src.next().map(|p| SmartString::from(p.as_str().trim())); + ImperativeStmt::Program { + prog: ImperativeStmtClause { prog, store_as }, + } } Rule::ignore_error_script => { let pair = pair.into_inner().next().unwrap(); - let prog = parse_query(pair.into_inner(), param_pool, fixed_rules, cur_vld)?; - ImperativeStmt::IgnoreErrorProgram { prog } + let mut src = pair.into_inner(); + let prog = parse_query( + src.next().unwrap().into_inner(), + param_pool, + fixed_rules, + cur_vld, + )?; + let store_as = src.next().map(|p| SmartString::from(p.as_str().trim())); + ImperativeStmt::IgnoreErrorProgram { + prog: ImperativeStmtClause { prog, store_as }, + } } r => unreachable!("{r:?}"), }) diff --git a/cozo-core/src/parse/mod.rs b/cozo-core/src/parse/mod.rs index 6c46e0a1..42190ac2 100644 --- a/cozo-core/src/parse/mod.rs +++ b/cozo-core/src/parse/mod.rs @@ -47,6 +47,12 @@ pub(crate) enum CozoScript { Sys(SysOp), } +#[derive(Debug)] +pub(crate) struct ImperativeStmtClause { + pub(crate) prog: InputProgram, + pub(crate) store_as: Option>, +} + #[derive(Debug)] pub(crate) enum ImperativeStmt { Break { @@ -58,13 +64,13 @@ pub(crate) enum ImperativeStmt { span: SourceSpan, }, Return { - returns: Vec>>, + returns: Vec>>, }, Program { - prog: InputProgram, + prog: ImperativeStmtClause, }, IgnoreErrorProgram { - prog: InputProgram, + prog: ImperativeStmtClause, }, If { condition: ImperativeCondition, @@ -86,7 +92,7 @@ pub(crate) enum ImperativeStmt { }, } -pub(crate) type ImperativeCondition = Either, InputProgram>; +pub(crate) type ImperativeCondition = Either, ImperativeStmtClause>; pub(crate) type ImperativeProgram = Vec; @@ -95,14 +101,14 @@ impl ImperativeStmt { match self { ImperativeStmt::Program { prog, .. } | ImperativeStmt::IgnoreErrorProgram { prog, .. } => { - if let Some(name) = prog.needs_write_lock() { + if let Some(name) = prog.prog.needs_write_lock() { collector.insert(name); } } ImperativeStmt::Return { returns, .. } => { for ret in returns { if let Left(prog) = ret { - if let Some(name) = prog.needs_write_lock() { + if let Some(name) = prog.prog.needs_write_lock() { collector.insert(name); } } @@ -115,7 +121,7 @@ impl ImperativeStmt { .. } => { if let ImperativeCondition::Right(prog) = condition { - if let Some(name) = prog.needs_write_lock() { + if let Some(name) = prog.prog.needs_write_lock() { collector.insert(name); } } diff --git a/cozo-core/src/runtime/imperative.rs b/cozo-core/src/runtime/imperative.rs index f26f2045..a2ce96f5 100644 --- a/cozo-core/src/runtime/imperative.rs +++ b/cozo-core/src/runtime/imperative.rs @@ -15,10 +15,13 @@ use miette::{bail, Diagnostic, Report, Result}; use smartstring::{LazyCompact, SmartString}; use thiserror::Error; +use crate::data::program::RelationOp; +use crate::data::relation::{ColType, ColumnDef, NullableColType, StoredRelationMetadata}; use crate::data::symb::Symbol; use crate::parse::{ImperativeCondition, ImperativeProgram, ImperativeStmt, SourceSpan}; use crate::runtime::callback::CallbackCollector; use crate::runtime::db::{seconds_since_the_epoch, RunningQueryCleanup, RunningQueryHandle}; +use crate::runtime::relation::InputRelationHandle; use crate::runtime::transact::SessionTx; use crate::{DataValue, Db, NamedRows, Poison, Storage, ValidityTs}; @@ -44,7 +47,7 @@ impl<'s, S: Storage<'s>> Db { relation.as_named_rows(tx)? } Right(p) => self.execute_single_program( - p.clone(), + p.prog.clone(), tx, cleanups, cur_vld, @@ -52,6 +55,11 @@ impl<'s, S: Storage<'s>> Db { callback_collector, )?, }; + if let Right(pg) = &p { + if let Some(store_as) = &pg.store_as { + tx.script_store_as_relation(self, store_as, &res, cur_vld)?; + } + } Ok(!res.rows.is_empty()) } @@ -83,7 +91,7 @@ impl<'s, S: Storage<'s>> Db { for nxt in returns.iter().rev() { let mut nr = match nxt { Left(prog) => self.execute_single_program( - prog.clone(), + prog.prog.clone(), tx, cleanups, cur_vld, @@ -95,6 +103,11 @@ impl<'s, S: Storage<'s>> Db { relation.as_named_rows(tx)? } }; + if let Left(pg) = nxt { + if let Some(store_as) = &pg.store_as { + tx.script_store_as_relation(self, store_as, &nr, cur_vld)?; + } + } nr.next = current; current = Some(Box::new(nr)) } @@ -107,24 +120,32 @@ impl<'s, S: Storage<'s>> Db { } ImperativeStmt::Program { prog, .. } => { ret = self.execute_single_program( - prog.clone(), + prog.prog.clone(), tx, cleanups, cur_vld, callback_targets, callback_collector, )?; + if let Some(store_as) = &prog.store_as { + tx.script_store_as_relation(self, store_as, &ret, cur_vld)?; + } } ImperativeStmt::IgnoreErrorProgram { prog, .. } => { match self.execute_single_program( - prog.clone(), + prog.prog.clone(), tx, cleanups, cur_vld, callback_targets, callback_collector, ) { - Ok(res) => ret = res, + Ok(res) => { + if let Some(store_as) = &prog.store_as { + tx.script_store_as_relation(self, store_as, &res, cur_vld)?; + } + ret = res + } Err(_) => { ret = NamedRows::new( vec!["status".to_string()], @@ -303,3 +324,53 @@ impl<'s, S: Storage<'s>> Db { Ok(ret) } } + +impl SessionTx<'_> { + fn script_store_as_relation<'s, S: Storage<'s>>( + &mut self, + db: &Db, + name: &str, + rels: &NamedRows, + cur_vld: ValidityTs, + ) -> Result<()> { + let meta = InputRelationHandle { + name: Symbol::new(name, Default::default()), + metadata: StoredRelationMetadata { + keys: rels + .headers + .iter() + .map(|s| ColumnDef { + name: s.into(), + typing: NullableColType { + coltype: ColType::Any, + nullable: true, + }, + default_gen: None, + }) + .collect_vec(), + non_keys: vec![], + }, + key_bindings: rels + .headers + .iter() + .map(|s| Symbol::new(s.clone(), Default::default())) + .collect_vec(), + dep_bindings: vec![], + span: Default::default(), + }; + let headers = meta.key_bindings.clone(); + self.execute_relation( + db, + rels.rows.iter().cloned(), + RelationOp::Replace, + &meta, + &headers, + cur_vld, + &Default::default(), + &mut Default::default(), + true, + "", + )?; + Ok(()) + } +} diff --git a/cozo-core/src/runtime/tests.rs b/cozo-core/src/runtime/tests.rs index 991954c0..3365b269 100644 --- a/cozo-core/src/runtime/tests.rs +++ b/cozo-core/src/runtime/tests.rs @@ -122,7 +122,7 @@ fn test_conditions() { "#, Default::default(), ) - .unwrap(); + .unwrap(); debug!("real test begins"); let res = db .run_script( @@ -168,7 +168,7 @@ fn default_columns() { "#, Default::default(), ) - .unwrap(); + .unwrap(); db.run_script( r#" @@ -177,7 +177,7 @@ fn default_columns() { "#, Default::default(), ) - .unwrap(); + .unwrap(); } #[test] @@ -391,12 +391,12 @@ fn test_trigger() { ":create friends {fr: Int, to: Int => data: Any}", Default::default(), ) - .unwrap(); + .unwrap(); db.run_script( ":create friends.rev {to: Int, fr: Int => data: Any}", Default::default(), ) - .unwrap(); + .unwrap(); db.run_script( r#" ::set_triggers friends @@ -414,12 +414,12 @@ fn test_trigger() { "#, Default::default(), ) - .unwrap(); + .unwrap(); db.run_script( r"?[fr, to, data] <- [[1,2,3]] :put friends {fr, to => data}", Default::default(), ) - .unwrap(); + .unwrap(); let ret = db .export_relations(["friends", "friends.rev"].into_iter()) .unwrap(); @@ -438,7 +438,7 @@ fn test_trigger() { r"?[fr, to] <- [[1,2], [2,3]] :rm friends {fr, to}", Default::default(), ) - .unwrap(); + .unwrap(); let ret = db .export_relations(["friends", "friends.rev"].into_iter()) .unwrap(); @@ -455,22 +455,22 @@ fn test_callback() { ":create friends {fr: Int, to: Int => data: Any}", Default::default(), ) - .unwrap(); + .unwrap(); db.run_script( r"?[fr, to, data] <- [[1,2,3],[4,5,6]] :put friends {fr, to => data}", Default::default(), ) - .unwrap(); + .unwrap(); db.run_script( r"?[fr, to, data] <- [[1,2,4],[4,7,6]] :put friends {fr, to => data}", Default::default(), ) - .unwrap(); + .unwrap(); db.run_script( r"?[fr, to] <- [[1,9],[4,5]] :rm friends {fr, to}", Default::default(), ) - .unwrap(); + .unwrap(); std::thread::sleep(Duration::from_secs_f64(0.01)); while let Ok(d) = receiver.try_recv() { collected.push(d); @@ -502,12 +502,12 @@ fn test_update() { ":create friends {fr: Int, to: Int => a: Any, b: Any, c: Any}", Default::default(), ) - .unwrap(); + .unwrap(); db.run_script( "?[fr, to, a, b, c] <- [[1,2,3,4,5]] :put friends {fr, to => a, b, c}", Default::default(), ) - .unwrap(); + .unwrap(); let res = db .run_script( "?[fr, to, a, b, c] := *friends{fr, to, a, b, c}", @@ -520,7 +520,7 @@ fn test_update() { "?[fr, to, b] <- [[1, 2, 100]] :update friends {fr, to => b}", Default::default(), ) - .unwrap(); + .unwrap(); let res = db .run_script( "?[fr, to, a, b, c] := *friends{fr, to, a, b, c}", @@ -538,13 +538,13 @@ fn test_index() { ":create friends {fr: Int, to: Int => data: Any}", Default::default(), ) - .unwrap(); + .unwrap(); db.run_script( r"?[fr, to, data] <- [[1,2,3],[4,5,6]] :put friends {fr, to, data}", Default::default(), ) - .unwrap(); + .unwrap(); assert!(db .run_script("::index create friends:rev {to, no}", Default::default()) @@ -556,12 +556,12 @@ fn test_index() { r"?[fr, to, data] <- [[1,2,5],[6,5,7]] :put friends {fr, to => data}", Default::default(), ) - .unwrap(); + .unwrap(); db.run_script( r"?[fr, to] <- [[4,5]] :rm friends {fr, to}", Default::default(), ) - .unwrap(); + .unwrap(); let rels_data = db .export_relations(["friends", "friends:rev"].into_iter()) @@ -629,7 +629,7 @@ fn test_json_objects() { }", Default::default(), ) - .unwrap(); + .unwrap(); } #[test] @@ -690,13 +690,13 @@ fn test_index_short() { ":create friends {fr: Int, to: Int => data: Any}", Default::default(), ) - .unwrap(); + .unwrap(); db.run_script( r"?[fr, to, data] <- [[1,2,3],[4,5,6]] :put friends {fr, to => data}", Default::default(), ) - .unwrap(); + .unwrap(); db.run_script("::index create friends:rev {to}", Default::default()) .unwrap(); @@ -705,12 +705,12 @@ fn test_index_short() { r"?[fr, to, data] <- [[1,2,5],[6,5,7]] :put friends {fr, to => data}", Default::default(), ) - .unwrap(); + .unwrap(); db.run_script( r"?[fr, to] <- [[4,5]] :rm friends {fr, to}", Default::default(), ) - .unwrap(); + .unwrap(); let rels_data = db .export_relations(["friends", "friends:rev"].into_iter()) @@ -806,7 +806,7 @@ fn test_vec_types() { "?[k, v] <- [['k', [1,2,3,4,5,6,7,8]]] :put a {k => v}", Default::default(), ) - .unwrap(); + .unwrap(); let res = db .run_script("?[k, v] := *a{k, v}", Default::default()) .unwrap(); @@ -851,7 +851,7 @@ fn test_vec_index() { ", Default::default(), ) - .unwrap(); + .unwrap(); db.run_script( r" ::hnsw create a:vec { @@ -867,7 +867,7 @@ fn test_vec_index() { }", Default::default(), ) - .unwrap(); + .unwrap(); db.run_script( r" ?[k, v] <- [ @@ -882,7 +882,7 @@ fn test_vec_index() { ", Default::default(), ) - .unwrap(); + .unwrap(); println!("all links"); for (_, nrows) in db.export_relations(["a:vec"].iter()).unwrap() { @@ -917,7 +917,7 @@ fn test_fts_indexing() { r"?[k, v] <- [['a', 'hello world!'], ['b', 'the world is round']] :put a {k => v}", Default::default(), ) - .unwrap(); + .unwrap(); db.run_script( r"::fts create a:fts { extractor: v, @@ -926,7 +926,7 @@ fn test_fts_indexing() { }", Default::default(), ) - .unwrap(); + .unwrap(); db.run_script( r"?[k, v] <- [ ['b', 'the world is square!'], @@ -935,7 +935,7 @@ fn test_fts_indexing() { ] :put a {k => v}", Default::default(), ) - .unwrap(); + .unwrap(); let res = db .run_script( r" @@ -969,12 +969,12 @@ fn test_lsh_indexing() { r"?[k, v] <- [['a', 'hello world!'], ['b', 'the world is round']] :put a {k => v}", Default::default(), ) - .unwrap(); + .unwrap(); db.run_script( r"::lsh create a:lsh {extractor: v, tokenizer: Simple, n_gram: 3, target_threshold: 0.3 }", Default::default(), ) - .unwrap(); + .unwrap(); db.run_script( r"?[k, v] <- [ ['b', 'the world is square!'], @@ -984,7 +984,7 @@ fn test_lsh_indexing() { ] :put a {k => v}", Default::default(), ) - .unwrap(); + .unwrap(); let res = db .run_script("::columns a:lsh", Default::default()) .unwrap(); @@ -1043,7 +1043,7 @@ fn test_insertions() { r":create a {k => v: default rand_vec(1536)}", Default::default(), ) - .unwrap(); + .unwrap(); db.run_script(r"?[k] <- [[1]] :put a {k}", Default::default()) .unwrap(); db.run_script(r"?[k, v] := *a{k, v}", Default::default()) @@ -1055,7 +1055,7 @@ fn test_insertions() { }", Default::default(), ) - .unwrap(); + .unwrap(); db.run_script(r"?[count(fr_k)] := *a:i{fr_k}", Default::default()) .unwrap(); db.run_script(r"?[k] <- [[1]] :put a {k}", Default::default()) @@ -1064,7 +1064,7 @@ fn test_insertions() { r"?[k] := k in int_range(300) :put a {k}", Default::default(), ) - .unwrap(); + .unwrap(); let res = db .run_script( r"?[dist, k] := ~a:i{k | query: v, bind_distance: dist, k:10, ef: 50, filter: k % 2 == 0, radius: 245}, *a{k: 96, v}", @@ -1136,7 +1136,7 @@ fn multi_index_vec() { "#, Default::default(), ) - .unwrap(); + .unwrap(); db.run_script( r#" ::hnsw create product:semantic{ @@ -1148,7 +1148,7 @@ fn multi_index_vec() { "#, Default::default(), ) - .unwrap(); + .unwrap(); db.run_script( r#" ?[id, name, description, price, name_vec, description_vec] <- [[1, "name", "description", 100, [1], [1]]] @@ -1178,7 +1178,7 @@ fn ensure_not() { ", Default::default(), ) - .unwrap(); + .unwrap(); } #[test] @@ -1214,8 +1214,8 @@ fn deletion() { Default::default(), ) .is_ok()); - db - .run_script(r"?[x] <- [[1]] :delete a {x}", Default::default()).unwrap(); + db.run_script(r"?[x] <- [[1]] :delete a {x}", Default::default()) + .unwrap(); } #[test] @@ -1240,7 +1240,10 @@ fn returning() { Default::default(), ) .unwrap(); - assert_eq!(res.into_json()["rows"], json!([["inserted", 1, 3], ["inserted", 2, 4], ["replaced", 1, 2]])); + assert_eq!( + res.into_json()["rows"], + json!([["inserted", 1, 3], ["inserted", 2, 4], ["replaced", 1, 2]]) + ); // println!("{:?}", res.headers); // for row in res.into_json()["rows"].as_array().unwrap() { // println!("{}", row); @@ -1256,9 +1259,19 @@ fn returning() { // for row in res.into_json()["rows"].as_array().unwrap() { // println!("{}", row); // } - assert_eq!(res.into_json()["rows"], json!([["requested", 1, null], ["requested", 4, null], ["deleted", 1, 3]])); - db.run_script(r":create todo{id:Uuid default rand_uuid_v1() => label: String, done: Bool}", Default::default()) - .unwrap(); + assert_eq!( + res.into_json()["rows"], + json!([ + ["requested", 1, null], + ["requested", 4, null], + ["deleted", 1, 3] + ]) + ); + db.run_script( + r":create todo{id:Uuid default rand_uuid_v1() => label: String, done: Bool}", + Default::default(), + ) + .unwrap(); let res = db .run_script( r"?[label,done] <- [['milk',false]] :put todo{label,done} :returning", @@ -1286,12 +1299,41 @@ fn parser_corner_case() { r#"?[C] := C = true, C inx[C] := C = 1"#, Default::default(), ) - .unwrap(); + .unwrap(); db.run_script(r#"?[k] := k in int_range(300)"#, Default::default()) .unwrap(); db.run_script( r#"ywcc[a] <- [[1]] noto[A] := ywcc[A] ?[A] := noto[A]"#, Default::default(), ) + .unwrap(); +} + +#[test] +fn as_store_in_imperative_script() { + let db = DbInstance::new("mem", "", "").unwrap(); + let res = db + .run_script( + r#" + { ?[x, y, z] <- [[1, 2, 3], [4, 5, 6]] } as _store + { ?[x, y, z] := *_store{x, y, z} } + "#, + Default::default(), + ) .unwrap(); + assert_eq!(res.into_json()["rows"], json!([[1, 2, 3], [4, 5, 6]])); + let res = db.run_script(r#" + { + ?[y] <- [[1], [2], [3]] + :create a {x default rand_uuid_v1() => y} + :returning + } as _last + { + ?[x] := *_last{_kind: 'inserted', x} + } + "#, Default::default()).unwrap(); + assert_eq!(3, res.rows.len()); + for row in res.into_json()["rows"].as_array().unwrap() { + println!("{}", row); + } }