diff --git a/cozo-core/src/cozoscript.pest b/cozo-core/src/cozoscript.pest index db56b861..47dc58f3 100644 --- a/cozo-core/src/cozoscript.pest +++ b/cozo-core/src/cozoscript.pest @@ -135,9 +135,10 @@ limit_option = {":limit" ~ expr} offset_option = {":offset" ~ expr} sort_option = {(":sort" | ":order") ~ (sort_arg ~ ",")* ~ sort_arg } relation_option = {relation_op ~ (compound_ident | underscore_ident) ~ table_schema?} -relation_op = _{relation_create | relation_replace | relation_put | relation_update | relation_rm | relation_ensure_not | relation_ensure } +relation_op = _{relation_create | relation_replace | relation_insert | relation_put | relation_update | relation_rm | relation_ensure_not | relation_ensure } relation_create = {":create"} relation_replace = {":replace"} +relation_insert = {":insert"} relation_put = {":put"} relation_update = {":update"} relation_rm = {":rm"} diff --git a/cozo-core/src/data/program.rs b/cozo-core/src/data/program.rs index a2684c37..0482079f 100644 --- a/cozo-core/src/data/program.rs +++ b/cozo-core/src/data/program.rs @@ -76,15 +76,15 @@ impl Display for QueryOutOptions { writeln!(f, "{symb};")?; } if let Some(( - InputRelationHandle { - name, - metadata: StoredRelationMetadata { keys, non_keys }, - key_bindings, - dep_bindings, - .. - }, - op, - )) = &self.store_relation + InputRelationHandle { + name, + metadata: StoredRelationMetadata { keys, non_keys }, + key_bindings, + dep_bindings, + .. + }, + op, + )) = &self.store_relation { match op { RelationOp::Create => { @@ -93,6 +93,9 @@ impl Display for QueryOutOptions { RelationOp::Replace => { write!(f, ":replace ")?; } + RelationOp::Insert => { + write!(f, ":insert ")?; + } RelationOp::Put => { write!(f, ":put ")?; } @@ -178,6 +181,7 @@ pub(crate) enum RelationOp { Create, Replace, Put, + Insert, Update, Rm, Ensure, @@ -486,13 +490,13 @@ impl Display for InputProgram { } InputInlineRulesOrFixed::Fixed { fixed: - FixedRuleApply { - fixed_handle: handle, - rule_args, - options, - head, - .. - }, + FixedRuleApply { + fixed_handle: handle, + rule_args, + options, + head, + .. + }, } => { write!(f, "{name}")?; f.debug_list().entries(head).finish()?; @@ -627,7 +631,7 @@ impl InputProgram { inner: rule.body, span: rule.span, } - .disjunctive_normal_form(tx)?; + .disjunctive_normal_form(tx)?; let mut new_head = Vec::with_capacity(rule.head.len()); let mut seen: BTreeMap<&Symbol, Vec> = BTreeMap::default(); for symb in rule.head.iter() { @@ -989,7 +993,7 @@ pub(crate) struct FtsSearch { } impl HnswSearch { - pub(crate) fn all_bindings(&self) -> impl Iterator { + pub(crate) fn all_bindings(&self) -> impl Iterator { self.bindings .iter() .chain(self.bind_field.iter()) @@ -1000,7 +1004,7 @@ impl HnswSearch { } impl FtsSearch { - pub(crate) fn all_bindings(&self) -> impl Iterator { + pub(crate) fn all_bindings(&self) -> impl Iterator { self.bindings.iter().chain(self.bind_score.iter()) } } @@ -1652,12 +1656,12 @@ impl Display for InputAtom { } InputAtom::Unification { inner: - Unification { - binding, - expr, - one_many_unif, - .. - }, + Unification { + binding, + expr, + one_many_unif, + .. + }, } => { write!(f, "{binding}")?; if *one_many_unif { diff --git a/cozo-core/src/parse/query.rs b/cozo-core/src/parse/query.rs index 62588b52..0a3f5a59 100644 --- a/cozo-core/src/parse/query.rs +++ b/cozo-core/src/parse/query.rs @@ -317,6 +317,7 @@ pub(crate) fn parse_query( Rule::relation_create => RelationOp::Create, Rule::relation_replace => RelationOp::Replace, Rule::relation_put => RelationOp::Put, + Rule::relation_insert => RelationOp::Insert, Rule::relation_update => RelationOp::Update, Rule::relation_rm => RelationOp::Rm, Rule::relation_ensure => RelationOp::Ensure, diff --git a/cozo-core/src/query/stored.rs b/cozo-core/src/query/stored.rs index 90054df3..fe8ad32a 100644 --- a/cozo-core/src/query/stored.rs +++ b/cozo-core/src/query/stored.rs @@ -44,7 +44,7 @@ impl<'a> SessionTx<'a> { pub(crate) fn execute_relation<'s, S: Storage<'s>>( &mut self, db: &Db, - res_iter: impl Iterator, + res_iter: impl Iterator, op: RelationOp, meta: &InputRelationHandle, headers: &[Symbol], @@ -88,7 +88,7 @@ impl<'a> SessionTx<'a> { &db.fixed_rules.read().unwrap(), cur_vld, )? - .get_single_program()?; + .get_single_program()?; let (_, cleanups) = db .run_query( @@ -178,7 +178,7 @@ impl<'a> SessionTx<'a> { key_bindings, *span, )?, - RelationOp::Create | RelationOp::Replace | RelationOp::Put => self.put_into_relation( + RelationOp::Create | RelationOp::Replace | RelationOp::Put | RelationOp::Insert => self.put_into_relation( db, res_iter, headers, @@ -191,6 +191,7 @@ impl<'a> SessionTx<'a> { metadata, key_bindings, dep_bindings, + op == RelationOp::Insert, *span, )?, }; @@ -201,7 +202,7 @@ impl<'a> SessionTx<'a> { fn put_into_relation<'s, S: Storage<'s>>( &mut self, db: &Db, - res_iter: impl Iterator, + res_iter: impl Iterator, headers: &[Symbol], cur_vld: ValidityTs, callback_targets: &BTreeSet>, @@ -212,6 +213,7 @@ impl<'a> SessionTx<'a> { metadata: &StoredRelationMetadata, key_bindings: &[Symbol], dep_bindings: &[Symbol], + is_insert: bool, span: SourceSpan, ) -> Result<()> { let is_callback_target = callback_targets.contains(&relation_store.name); @@ -233,7 +235,7 @@ impl<'a> SessionTx<'a> { let need_to_collect = !relation_store.is_temp && (is_callback_target - || (propagate_triggers && !relation_store.put_triggers.is_empty())); + || (propagate_triggers && !relation_store.put_triggers.is_empty())); let has_indices = !relation_store.indices.is_empty(); let has_hnsw_indices = !relation_store.hnsw_indices.is_empty(); let has_fts_indices = !relation_store.fts_indices.is_empty(); @@ -268,6 +270,16 @@ impl<'a> SessionTx<'a> { .map(|ex| ex.extract_data(&tuple, cur_vld)) .try_collect()?; + if is_insert { + if relation_store.exists(self, &extracted[..relation_store.metadata.keys.len()])? { + bail!(TransactAssertionFailure { + relation: relation_store.name.to_string(), + key: extracted, + notice: "key exists in database".to_string() + }); + } + } + let key = relation_store.encode_key_for_store(&extracted, span)?; let val = relation_store.encode_val_for_store(&extracted, span)?; @@ -496,7 +508,7 @@ impl<'a> SessionTx<'a> { fn update_in_relation<'s, S: Storage<'s>>( &mut self, db: &Db, - res_iter: impl Iterator, + res_iter: impl Iterator, headers: &[Symbol], cur_vld: ValidityTs, callback_targets: &BTreeSet>, @@ -527,7 +539,7 @@ impl<'a> SessionTx<'a> { let need_to_collect = !relation_store.is_temp && (is_callback_target - || (propagate_triggers && !relation_store.put_triggers.is_empty())); + || (propagate_triggers && !relation_store.put_triggers.is_empty())); let has_indices = !relation_store.indices.is_empty(); let has_hnsw_indices = !relation_store.hnsw_indices.is_empty(); let has_fts_indices = !relation_store.fts_indices.is_empty(); @@ -674,7 +686,7 @@ impl<'a> SessionTx<'a> { &db.fixed_rules.read().unwrap(), cur_vld, )? - .get_single_program()?; + .get_single_program()?; make_const_rule( &mut program, @@ -770,7 +782,7 @@ impl<'a> SessionTx<'a> { fn ensure_not_in_relation( &mut self, - res_iter: impl Iterator, + res_iter: impl Iterator, headers: &[Symbol], cur_vld: ValidityTs, relation_store: &mut RelationHandle, @@ -817,7 +829,7 @@ impl<'a> SessionTx<'a> { fn ensure_in_relation( &mut self, - res_iter: impl Iterator, + res_iter: impl Iterator, headers: &[Symbol], cur_vld: ValidityTs, relation_store: &mut RelationHandle, @@ -887,7 +899,7 @@ impl<'a> SessionTx<'a> { fn remove_from_relation<'s, S: Storage<'s>>( &mut self, db: &Db, - res_iter: impl Iterator, + res_iter: impl Iterator, headers: &[Symbol], cur_vld: ValidityTs, callback_targets: &BTreeSet>, @@ -917,7 +929,7 @@ impl<'a> SessionTx<'a> { let need_to_collect = !relation_store.is_temp && (is_callback_target - || (propagate_triggers && !relation_store.rm_triggers.is_empty())); + || (propagate_triggers && !relation_store.rm_triggers.is_empty())); let has_indices = !relation_store.indices.is_empty(); let has_hnsw_indices = !relation_store.hnsw_indices.is_empty(); let has_fts_indices = !relation_store.fts_indices.is_empty(); @@ -992,7 +1004,7 @@ impl<'a> SessionTx<'a> { &db.fixed_rules.read().unwrap(), cur_vld, )? - .get_single_program()?; + .get_single_program()?; make_const_rule(&mut program, "_new", k_bindings.clone(), new_tuples.clone()); diff --git a/cozo-core/src/runtime/tests.rs b/cozo-core/src/runtime/tests.rs index fa2ceaf2..f107b2a7 100644 --- a/cozo-core/src/runtime/tests.rs +++ b/cozo-core/src/runtime/tests.rs @@ -1177,6 +1177,16 @@ fn ensure_not() { ", Default::default()).unwrap(); } +#[test] +fn insertion() { + let db = DbInstance::new("mem", "", "").unwrap(); + db.run_script(r":create a {x => y}", Default::default()).unwrap(); + assert!(db.run_script(r"?[x, y] <- [[1, 2]] :insert a {x => y}", Default::default()) + .is_ok()); + assert!(db.run_script(r"?[x, y] <- [[1, 3]] :insert a {x => y}", Default::default()) + .is_err()); +} + #[test] fn parser_corner_case() { let db = DbInstance::new("mem", "", "").unwrap();