-
Notifications
You must be signed in to change notification settings - Fork 115
(tx) Implement :db/retractEntity. (#378) #650
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -549,6 +549,9 @@ pub trait MentatStoring { | |
| fn insert_non_fts_searches<'a>(&self, entities: &'a [ReducedEntity], search_type: SearchType) -> Result<()>; | ||
| fn insert_fts_searches<'a>(&self, entities: &'a [ReducedEntity], search_type: SearchType) -> Result<()>; | ||
|
|
||
| // :db/retractEntity the given entids. | ||
| fn insert_retract_entities<'a>(&self, entids: &'a [Entid]) -> Result<()>; | ||
|
|
||
| /// Finalize the underlying storage layer after a Mentat transaction. | ||
| /// | ||
| /// Use this to finalize temporary tables, complete indices, revert pragmas, etc, after the | ||
|
|
@@ -982,6 +985,48 @@ impl MentatStoring for rusqlite::Connection { | |
| results.map(|_| ()) | ||
| } | ||
|
|
||
| /// Insert datoms corresponding to :db/retractEntity entities into the search results. | ||
| fn insert_retract_entities(&self, entids: &[Entid]) -> Result<()> { | ||
| let max_vars = self.limit(Limit::SQLITE_LIMIT_VARIABLE_NUMBER) as usize; | ||
| let bindings_per_statement = 2; | ||
|
|
||
| let chunks: itertools::IntoChunks<_> = entids.into_iter().chunks(max_vars / bindings_per_statement); | ||
|
|
||
| // We'd like to flat_map here, but it's not obvious how to flat_map across Result. | ||
| let results: Result<Vec<()>> = chunks.into_iter().map(|chunk| -> Result<()> { | ||
| let mut count = 0; | ||
| let mut params: Vec<&ToSql> = chunk.flat_map(|e| { | ||
| count += 1; | ||
| once(e as &ToSql) | ||
| }).collect(); | ||
|
|
||
| // Two copies, first for testing e, then for testing v. | ||
| // TODO: perhaps we can reference the same named parameter multiple times, making this | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can: |
||
| // more efficient. | ||
| let mut params2 = params.clone(); | ||
| params.append(&mut params2); | ||
|
|
||
| let values: String = repeat_values(count, 1); | ||
|
|
||
| // Note that the value for flags (-1) is nonsense. Since these rows are being removed | ||
| // from datoms, flags/flags0 is not referenced and this is fine. The value for the | ||
| // search type is also ignored. | ||
| let s: String = format!(r#" | ||
| INSERT INTO temp.search_results | ||
| SELECT d.e, d.a, d.v, d.value_type_tag, 0, -1, ':db.cardinality/many', d.rowid, d.v | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is |
||
| FROM datoms AS d | ||
| WHERE d.e IN ({}) OR (d.value_type_tag = 0 AND d.v IN ({})) | ||
| "#, values, values); | ||
|
|
||
| let mut stmt = self.prepare_cached(s.as_str())?; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're preparing the statement here but only using it once. Your entity IDs are always safe to inline in the SQL, so I think it's better to do that (and thus avoid the |
||
| stmt.execute(¶ms) | ||
| .map(|_c| ()) | ||
| .chain_err(|| "Could not retract entities!") | ||
| }).collect::<Result<Vec<()>>>(); | ||
|
|
||
| results.and(Ok(())) | ||
| } | ||
|
|
||
| fn commit_transaction(&self, tx_id: Entid) -> Result<()> { | ||
| search(&self)?; | ||
| insert_transaction(&self, tx_id)?; | ||
|
|
@@ -2387,6 +2432,71 @@ mod tests { | |
| Err("EDN value \'1.23\' is not the expected Mentat value type Ref")); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_retract_entity() { | ||
| let mut conn = TestConn::default(); | ||
|
|
||
| // Start by installing a few attributes. | ||
| assert_transact!(conn, "[[:db/add 111 :db/ident :test/many] | ||
| [:db/add 111 :db/valueType :db.type/long] | ||
| [:db/add 111 :db/cardinality :db.cardinality/many] | ||
| [:db/add 222 :db/ident :test/component] | ||
| [:db/add 222 :db/isComponent true] | ||
| [:db/add 222 :db/valueType :db.type/ref] | ||
| [:db/add 333 :db/ident :test/dangling] | ||
| [:db/add 333 :db/valueType :db.type/ref]]"); | ||
|
|
||
| // Verify that we can retract simple entities by entid. | ||
| assert_transact!(conn, "[[:db/retractEntity 111]]"); | ||
| assert_matches!(conn.last_transaction(), | ||
| "[[111 :db/ident :test/many ?tx false] | ||
| [111 :db/valueType :db.type/long ?tx false] | ||
| [111 :db/cardinality :db.cardinality/many ?tx false] | ||
| [?tx :db/txInstant ?ms ?tx true]]"); | ||
|
|
||
| // Verify that we can retract entities that don't exist. | ||
| assert_transact!(conn, "[[:db/retractEntity 111]]"); | ||
|
|
||
| // Verify that we can retract simple entities by keyword. | ||
| assert_transact!(conn, "[[:db/retractEntity :test/component]]"); | ||
| assert_matches!(conn.last_transaction(), | ||
| "[[222 :db/ident :test/component ?tx false] | ||
| [222 :db/valueType :db.type/ref ?tx false] | ||
| [222 :db/isComponent true ?tx false] | ||
| [?tx :db/txInstant ?ms ?tx true]]"); | ||
|
|
||
| // Some data for testing associated references. | ||
| assert_transact!(conn, "[[:db/add 555 :test/dangling 666] | ||
| [:db/add 666 :test/dangling 777] | ||
| [:db/add 777 :test/dangling 777] | ||
| [:db/add 888 :test/dangling 999]]"); | ||
|
|
||
| // Verify that associated references are also retracted. | ||
| assert_transact!(conn, "[[:db/retractEntity 666]]"); | ||
| assert_matches!(conn.last_transaction(), | ||
| "[[555 :test/dangling 666 ?tx false] | ||
| [666 :test/dangling 777 ?tx false] | ||
| [?tx :db/txInstant ?ms ?tx true]]"); | ||
|
|
||
| // What happens if we have a self reference? | ||
| assert_transact!(conn, "[[:db/retractEntity 777]]"); | ||
| assert_matches!(conn.last_transaction(), | ||
| "[[777 :test/dangling 777 ?tx false] | ||
| [?tx :db/txInstant ?ms ?tx true]]"); | ||
|
|
||
| // Verify that we can retract entities that aren't recognized, but that appear as dangling | ||
| // references. | ||
| assert_transact!(conn, "[[:db/retractEntity 999]]"); | ||
| assert_matches!(conn.last_transaction(), | ||
| "[[888 :test/dangling 999 ?tx false] | ||
| [?tx :db/txInstant ?ms ?tx true]]"); | ||
|
|
||
| // Let's make sure we actually retracted from the datoms table. | ||
| assert_matches!(conn.datoms(), | ||
| "[[333 :db/ident :test/dangling] | ||
| [333 :db/valueType :db.type/ref]]"); | ||
| } | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add a test to make sure I can't retract a tx-id or an attribute. |
||
|
|
||
| #[test] | ||
| fn test_cardinality_one_violation_existing_entity() { | ||
| let mut conn = TestConn::default(); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -381,6 +381,11 @@ impl<'conn, 'a, W> Tx<'conn, 'a, W> where W: TransactWatcher { | |
| } | ||
| }, | ||
|
|
||
| Entity::RetractEntity(e) => { | ||
| let e = in_process.entity_e_into_term_e(e.into())?; | ||
| terms.push(Term::RetractEntity(e)); | ||
| }, | ||
|
|
||
| Entity::AddOrRetract { op, e, a, v } => { | ||
| if let Some(reversed_a) = a.unreversed() { | ||
| let reversed_e = in_process.entity_v_into_term_e(v, &a)?; | ||
|
|
@@ -519,6 +524,10 @@ impl<'conn, 'a, W> Tx<'conn, 'a, W> where W: TransactWatcher { | |
| let v = replace_lookup_ref(&lookup_ref_map, v, |x| TypedValue::Ref(x))?; | ||
| Ok(Term::AddOrRetract(op, e, a, v)) | ||
| }, | ||
| Term::RetractEntity(e) => { | ||
| let e = replace_lookup_ref(&lookup_ref_map, e, |x| KnownEntid(x))?; | ||
| Ok(Term::RetractEntity(e)) | ||
| }, | ||
| } | ||
| }).collect::<Result<Vec<_>>>() | ||
| } | ||
|
|
@@ -629,6 +638,9 @@ impl<'conn, 'a, W> Tx<'conn, 'a, W> where W: TransactWatcher { | |
| // Assertions that are :db.cardinality/many and :db.fulltext. | ||
| let mut fts_many: Vec<db::ReducedEntity> = vec![]; | ||
|
|
||
| // :db/retractEntity entities. | ||
| let mut retract_entities: Vec<Entid> = vec![]; | ||
|
|
||
| // We need to ensure that callers can't blindly transact entities that haven't been | ||
| // allocated by this store. | ||
|
|
||
|
|
@@ -677,6 +689,12 @@ impl<'conn, 'a, W> Tx<'conn, 'a, W> where W: TransactWatcher { | |
| (true, true) => fts_many.push(reduced), | ||
| } | ||
| }, | ||
|
|
||
| Term::RetractEntity(KnownEntid(e)) => { | ||
| // TODO: Might update metadata? | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We shouldn't allow retracting vocabulary, so this might update idents… but that's a can of worms. |
||
| // TODO: Watching/counting datoms? | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep, the observer service needs to be extended to be told about retracted entities. |
||
| retract_entities.push(e); | ||
| }, | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -705,6 +723,8 @@ impl<'conn, 'a, W> Tx<'conn, 'a, W> where W: TransactWatcher { | |
| self.store.insert_fts_searches(&fts_many[..], db::SearchType::Exact)?; | ||
| } | ||
|
|
||
| self.store.insert_retract_entities(&retract_entities[..])?; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do this first, if only for conceptual clarity? |
||
|
|
||
| self.store.commit_transaction(self.tx_id)?; | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we want to allow retracting:
Could you add validation here?