use anyhow macros

main
Ziyang Hu 2 years ago
parent a233730253
commit 32f1d42f17

@ -8,7 +8,6 @@ use crate::data::attr::AttributeCardinality;
use crate::data::json::JsonValue; use crate::data::json::JsonValue;
use crate::data::keyword::Keyword; use crate::data::keyword::Keyword;
use crate::data::value::DataValue; use crate::data::value::DataValue;
use crate::parse::triple::TxError;
use crate::query::pull::{AttrPullSpec, PullSpec, PullSpecs}; use crate::query::pull::{AttrPullSpec, PullSpec, PullSpecs};
use crate::runtime::transact::SessionTx; use crate::runtime::transact::SessionTx;
@ -42,7 +41,9 @@ impl SessionTx {
} else { } else {
input_kw.clone() input_kw.clone()
}; };
let attr = self.attr_by_kw(&kw)?.ok_or(TxError::AttrNotFound(kw))?; let attr = self
.attr_by_kw(&kw)?
.ok_or_else(|| anyhow!("attribute {} not found", kw))?;
let cardinality = attr.cardinality; let cardinality = attr.cardinality;
Ok(PullSpec::Attr(AttrPullSpec { Ok(PullSpec::Attr(AttrPullSpec {
attr, attr,
@ -160,7 +161,9 @@ impl SessionTx {
} else { } else {
input_kw.clone() input_kw.clone()
}; };
let attr = self.attr_by_kw(&kw)?.ok_or(TxError::AttrNotFound(kw))?; let attr = self
.attr_by_kw(&kw)?
.ok_or_else(|| anyhow!("attribute not found: {}", kw))?;
let cardinality = cardinality_override.unwrap_or(attr.cardinality); let cardinality = cardinality_override.unwrap_or(attr.cardinality);
let nested = self.parse_pull(&JsonValue::Array(sub_target), depth + 1)?; let nested = self.parse_pull(&JsonValue::Array(sub_target), depth + 1)?;

@ -10,7 +10,6 @@ use crate::data::expr::{get_op, Expr};
use crate::data::json::JsonValue; use crate::data::json::JsonValue;
use crate::data::keyword::{Keyword, PROG_ENTRY}; use crate::data::keyword::{Keyword, PROG_ENTRY};
use crate::data::value::DataValue; use crate::data::value::DataValue;
use crate::parse::triple::TxError;
use crate::query::compile::{ use crate::query::compile::{
Atom, AttrTripleAtom, BindingHeadTerm, DatalogProgram, QueryCompilationError, Rule, Atom, AttrTripleAtom, BindingHeadTerm, DatalogProgram, QueryCompilationError, Rule,
RuleApplyAtom, RuleSet, Term, RuleApplyAtom, RuleSet, Term,
@ -418,7 +417,9 @@ impl SessionTx {
); );
let (k, v) = m.iter().next().unwrap(); let (k, v) = m.iter().next().unwrap();
let kw = Keyword::from(k as &str); let kw = Keyword::from(k as &str);
let attr = self.attr_by_kw(&kw)?.ok_or(TxError::AttrNotFound(kw))?; let attr = self
.attr_by_kw(&kw)?
.ok_or_else(|| anyhow!("attribute {} not found", kw))?;
ensure!( ensure!(
attr.indexing.is_unique_index(), attr.indexing.is_unique_index(),
"pull inside query must use unique index, of which {} is not", "pull inside query must use unique index, of which {} is not",
@ -495,7 +496,9 @@ impl SessionTx {
match attr_rep { match attr_rep {
JsonValue::String(s) => { JsonValue::String(s) => {
let kw = Keyword::from(s as &str); let kw = Keyword::from(s as &str);
let attr = self.attr_by_kw(&kw)?.ok_or(TxError::AttrNotFound(kw))?; let attr = self
.attr_by_kw(&kw)?
.ok_or_else(|| anyhow!("attribute {} not found", kw))?;
Ok(attr) Ok(attr)
} }
v => bail!("expect attribute keyword for triple atom, got {}", v), v => bail!("expect attribute keyword for triple atom, got {}", v),

@ -1,4 +1,4 @@
use anyhow::Result; use anyhow::{anyhow, bail, ensure, Result};
use itertools::Itertools; use itertools::Itertools;
use crate::data::attr::Attribute; use crate::data::attr::Attribute;
@ -15,58 +15,43 @@ impl AttrTxItem {
pub fn parse_request(req: &JsonValue) -> Result<(Vec<AttrTxItem>, String)> { pub fn parse_request(req: &JsonValue) -> Result<(Vec<AttrTxItem>, String)> {
let map = req let map = req
.as_object() .as_object()
.ok_or_else(|| AttrTxItemError::Decoding(req.clone(), "expected object".to_string()))?; .ok_or_else(|| anyhow!("expect object, got {}", req))?;
let comment = match map.get("comment") { let comment = match map.get("comment") {
None => "".to_string(), None => "".to_string(),
Some(c) => c.to_string(), Some(c) => c.to_string(),
}; };
let items = map.get("attrs").ok_or_else(|| { let items = map
AttrTxItemError::Decoding(req.clone(), "expected key 'attrs'".to_string()) .get("attrs")
})?; .ok_or_else(|| anyhow!("expect key 'attrs' in {:?}", map))?;
let items = items.as_array().ok_or_else(|| { let items = items
AttrTxItemError::Decoding(items.clone(), "expected array".to_string()) .as_array()
})?; .ok_or_else(|| anyhow!("expect array for value of key 'attrs', got {:?}", items))?;
if items.is_empty() { ensure!(
return Err(AttrTxItemError::Decoding( !items.is_empty(),
req.clone(), "array for value of key 'attrs' must be non-empty"
"'attrs' cannot be empty".to_string(), );
)
.into());
}
let res = items.iter().map(AttrTxItem::try_from).try_collect()?; let res = items.iter().map(AttrTxItem::try_from).try_collect()?;
Ok((res, comment)) Ok((res, comment))
} }
} }
#[derive(Debug, thiserror::Error)]
pub enum AttrTxItemError {
#[error("Error decoding {0}: {1}")]
Decoding(JsonValue, String),
}
impl TryFrom<&'_ JsonValue> for AttrTxItem { impl TryFrom<&'_ JsonValue> for AttrTxItem {
type Error = anyhow::Error; type Error = anyhow::Error;
fn try_from(value: &'_ JsonValue) -> Result<Self, Self::Error> { fn try_from(value: &'_ JsonValue) -> Result<Self, Self::Error> {
let map = value.as_object().ok_or_else(|| { let map = value
AttrTxItemError::Decoding(value.clone(), "expected object".to_string()) .as_object()
})?; .ok_or_else(|| anyhow!("expect object for attribute tx, got {}", value))?;
if map.len() != 1 { ensure!(
return Err(AttrTxItemError::Decoding( map.len() == 1,
value.clone(), "attr definition must have exactly one pair, got {}",
"object must have exactly one field".to_string(), value
) );
.into());
}
let (k, v) = map.into_iter().next().unwrap(); let (k, v) = map.into_iter().next().unwrap();
let op = match k as &str { let op = match k as &str {
"put" => StoreOp::Assert, "put" => StoreOp::Assert,
"retract" => StoreOp::Retract, "retract" => StoreOp::Retract,
_ => { _ => bail!("unknown op {} for attribute tx", k),
return Err(
AttrTxItemError::Decoding(value.clone(), format!("unknown op {}", k)).into(),
);
}
}; };
let attr = Attribute::try_from(v)?; let attr = Attribute::try_from(v)?;

@ -2,7 +2,7 @@ use std::collections::btree_map::Entry;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use anyhow::Result; use anyhow::{anyhow, bail, ensure, Result};
use serde_json::Map; use serde_json::Map;
use crate::data::attr::{Attribute, AttributeIndex, AttributeTyping}; use crate::data::attr::{Attribute, AttributeIndex, AttributeTyping};
@ -42,22 +42,6 @@ impl Display for TxAction {
} }
} }
#[derive(Debug, thiserror::Error)]
pub enum TxError {
#[error("Error decoding {0}: {1}")]
Decoding(JsonValue, String),
#[error("triple length error")]
TripleLength,
#[error("attribute not found: {0}")]
AttrNotFound(Keyword),
#[error("wrong specification of entity id {0}: {1}")]
EntityId(u64, String),
#[error("invalid action {0:?}: {1}")]
InvalidAction(TxAction, String),
#[error("temp id does not occur in head position: {0}")]
TempIdNoHead(String),
}
#[derive(Default)] #[derive(Default)]
pub(crate) struct TempIdCtx { pub(crate) struct TempIdCtx {
store: BTreeMap<String, (EntityId, bool)>, store: BTreeMap<String, (EntityId, bool)>,
@ -67,9 +51,11 @@ pub(crate) struct TempIdCtx {
impl TempIdCtx { impl TempIdCtx {
fn validate_usage(&self) -> Result<()> { fn validate_usage(&self) -> Result<()> {
for (k, (_, b)) in self.store.iter() { for (k, (_, b)) in self.store.iter() {
if !*b { ensure!(
return Err(TxError::TempIdNoHead(k.to_string()).into()); *b,
} "defining temp id {} in non-head position is not allowed",
k
);
} }
Ok(()) Ok(())
} }
@ -124,23 +110,15 @@ impl SessionTx {
/// } /// }
/// ``` /// ```
/// nesting is allowed for values of type `ref` and `component` /// nesting is allowed for values of type `ref` and `component`
pub fn parse_tx_requests( pub fn parse_tx_requests(&mut self, req: &JsonValue) -> Result<(Vec<Quintuple>, String)> {
&mut self,
req: &JsonValue,
) -> Result<(Vec<Quintuple>, String)> {
let map = req let map = req
.as_object() .as_object()
.ok_or_else(|| TxError::Decoding(req.clone(), "expected object".to_string()))?; .ok_or_else(|| anyhow!("expect tx request to be an object, got {}", req))?;
let items = map let items = map
.get("tx") .get("tx")
.ok_or_else(|| TxError::Decoding(req.clone(), "expected field 'tx'".to_string()))? .ok_or_else(|| anyhow!("expect field 'tx' in tx request object {}", req))?
.as_array() .as_array()
.ok_or_else(|| { .ok_or_else(|| anyhow!("expect field 'tx' to be an array in {}", req))?;
TxError::Decoding(
req.clone(),
"expected field 'tx' to be an array".to_string(),
)
})?;
let default_since = match map.get("since") { let default_since = match map.get("since") {
None => Validity::current(), None => Validity::current(),
Some(v) => v.try_into()?, Some(v) => v.try_into()?,
@ -166,7 +144,7 @@ impl SessionTx {
) -> Result<()> { ) -> Result<()> {
let item = item let item = item
.as_object() .as_object()
.ok_or_else(|| TxError::Decoding(item.clone(), "expected object".to_string()))?; .ok_or_else(|| anyhow!("expect tx request item to be an object, got {}", item))?;
let (inner, action) = { let (inner, action) = {
if let Some(inner) = item.get("put") { if let Some(inner) = item.get("put") {
(inner, TxAction::Put) (inner, TxAction::Put)
@ -175,11 +153,10 @@ impl SessionTx {
} else if let Some(inner) = item.get("ensure") { } else if let Some(inner) = item.get("ensure") {
(inner, TxAction::Ensure) (inner, TxAction::Ensure)
} else { } else {
return Err(TxError::Decoding( bail!(
JsonValue::Object(item.clone()), "expect key 'put', 'retract', 'erase' or 'ensure' in tx request object, got {:?}",
"expect any of the keys 'put', 'retract', 'erase', 'ensure'".to_string(), item
) );
.into());
} }
}; };
let since = match item.get("since") { let since = match item.get("since") {
@ -196,7 +173,7 @@ impl SessionTx {
.map(|_| ()); .map(|_| ());
} }
Err(TxError::Decoding(inner.clone(), "expected object or array".to_string()).into()) bail!("expect object or array for tx object item, got {}", inner);
} }
fn parse_tx_request_inner<'a>( fn parse_tx_request_inner<'a>(
&mut self, &mut self,
@ -224,13 +201,11 @@ impl SessionTx {
return Ok(()); return Ok(());
} }
if !eid.is_perm() && action != TxAction::Put { ensure!(
return Err(TxError::InvalidAction( action == TxAction::Put || eid.is_perm(),
action, "using temp id instead of perm id for op {} is not allow",
"using temp id instead of perm id".to_string(), action
) );
.into());
}
let v = if let JsonValue::Object(inner) = value { let v = if let JsonValue::Object(inner) = value {
self.parse_tx_component(attr, inner, action, since, temp_id_ctx, collected)? self.parse_tx_component(attr, inner, action, since, temp_id_ctx, collected)?
@ -259,19 +234,15 @@ impl SessionTx {
temp_id_ctx: &mut TempIdCtx, temp_id_ctx: &mut TempIdCtx,
collected: &mut Vec<Quintuple>, collected: &mut Vec<Quintuple>,
) -> Result<DataValue> { ) -> Result<DataValue> {
if action != TxAction::Put { ensure!(
return Err(TxError::InvalidAction( action == TxAction::Put,
action, "component shorthand can only be use for 'put', got {}",
"component shorthand cannot be used".to_string(), action
) );
.into());
}
let (eid, has_unique_attr) = let (eid, has_unique_attr) =
self.parse_tx_request_obj(comp, true, action, since, temp_id_ctx, collected)?; self.parse_tx_request_obj(comp, true, action, since, temp_id_ctx, collected)?;
if !has_unique_attr && parent_attr.val_type != AttributeTyping::Component { ensure!(has_unique_attr || parent_attr.val_type == AttributeTyping::Component,
return Err(TxError::InvalidAction(action, "component shorthand must contain at least one unique/identity field for non-component refs");
"component shorthand must contain at least one unique/identity field for non-component refs".to_string()).into());
}
Ok(DataValue::EnId(eid)) Ok(DataValue::EnId(eid))
} }
fn parse_tx_request_arr<'a>( fn parse_tx_request_arr<'a>(
@ -284,22 +255,16 @@ impl SessionTx {
) -> Result<()> { ) -> Result<()> {
match item { match item {
[eid] => { [eid] => {
if action != TxAction::Retract { ensure!(
return Err(TxError::InvalidAction( action == TxAction::Retract,
action, "singlet action only allowed for 'retract', got {}",
"singlet only allowed for 'retract'".to_string(), action
)
.into());
}
let eid = eid.as_u64().ok_or_else(|| {
TxError::Decoding(eid.clone(), "cannot parse as entity id".to_string())
})?;
let eid = EntityId(eid);
if !eid.is_perm() {
return Err(
TxError::EntityId(eid.0, "expected perm entity id".to_string()).into(),
); );
} let eid = eid
.as_u64()
.ok_or_else(|| anyhow!("cannot parse {} as entity id", eid))?;
let eid = EntityId(eid);
ensure!(eid.is_perm(), "expected perm entity id, got {:?}", eid);
collected.push(Quintuple { collected.push(Quintuple {
triple: Triple { triple: Triple {
id: eid, id: eid,
@ -312,25 +277,21 @@ impl SessionTx {
Ok(()) Ok(())
} }
[eid, attr] => { [eid, attr] => {
if action != TxAction::Retract { ensure!(
return Err(TxError::InvalidAction( action == TxAction::Retract,
action, "double only allowed for 'retract', got {}",
"doublet only allowed for 'retract'".to_string(), action
) );
.into());
}
let kw: Keyword = attr.try_into()?; let kw: Keyword = attr.try_into()?;
let attr = self.attr_by_kw(&kw)?.ok_or(TxError::AttrNotFound(kw))?; let attr = self
.attr_by_kw(&kw)?
.ok_or_else(|| anyhow!("attribute not found {}", kw))?;
let eid = eid.as_u64().ok_or_else(|| { let eid = eid
TxError::Decoding(eid.clone(), "cannot parse as entity id".to_string()) .as_u64()
})?; .ok_or_else(|| anyhow!("cannot parse {} as entity id", eid))?;
let eid = EntityId(eid); let eid = EntityId(eid);
if !eid.is_perm() { ensure!(eid.is_perm(), "expect perm entity id, got {:?}", eid);
return Err(
TxError::EntityId(eid.0, "expected perm entity id".to_string()).into(),
);
}
collected.push(Quintuple { collected.push(Quintuple {
triple: Triple { triple: Triple {
id: eid, id: eid,
@ -345,7 +306,7 @@ impl SessionTx {
[eid, attr_kw, val] => { [eid, attr_kw, val] => {
self.parse_tx_triple(eid, attr_kw, val, action, since, temp_id_ctx, collected) self.parse_tx_triple(eid, attr_kw, val, action, since, temp_id_ctx, collected)
} }
_ => Err(TxError::TripleLength.into()), arr => bail!("bad triple in tx: {:?}", arr),
} }
} }
fn parse_tx_triple<'a>( fn parse_tx_triple<'a>(
@ -359,7 +320,9 @@ impl SessionTx {
collected: &mut Vec<Quintuple>, collected: &mut Vec<Quintuple>,
) -> Result<()> { ) -> Result<()> {
let kw: Keyword = attr_kw.try_into()?; let kw: Keyword = attr_kw.try_into()?;
let attr = self.attr_by_kw(&kw)?.ok_or(TxError::AttrNotFound(kw))?; let attr = self
.attr_by_kw(&kw)?
.ok_or_else(|| anyhow!("attribute not found: {}", kw))?;
if attr.cardinality.is_many() && attr.val_type != AttributeTyping::List && val.is_array() { if attr.cardinality.is_many() && attr.val_type != AttributeTyping::List && val.is_array() {
for cur_val in val.as_array().unwrap() { for cur_val in val.as_array().unwrap() {
self.parse_tx_triple(eid, attr_kw, cur_val, action, since, temp_id_ctx, collected)?; self.parse_tx_triple(eid, attr_kw, cur_val, action, since, temp_id_ctx, collected)?;
@ -378,9 +341,7 @@ impl SessionTx {
None => { None => {
if let Some(i) = eid.as_u64() { if let Some(i) = eid.as_u64() {
let id = EntityId(i); let id = EntityId(i);
if !id.is_perm() { ensure!(id.is_perm(), "temp id not allowed here, found {:?}", id);
return Err(TxError::EntityId(id.0, "temp id specified".into()).into());
}
id id
} else if let Some(s) = eid.as_str() { } else if let Some(s) = eid.as_str() {
temp_id_ctx.str2tempid(s, true) temp_id_ctx.str2tempid(s, true)
@ -391,23 +352,19 @@ impl SessionTx {
Some(existing_id) => { Some(existing_id) => {
if let Some(i) = eid.as_u64() { if let Some(i) = eid.as_u64() {
let id = EntityId(i); let id = EntityId(i);
if !id.is_perm() { ensure!(id.is_perm(), "temp id not allowed here, found {:?}", id);
return Err(TxError::EntityId(id.0, "temp id specified".into()).into()); ensure!(
} existing_id == id,
if existing_id != id { "conflicting id for identity value: {:?} vs {:?}",
return Err(TxError::EntityId( existing_id,
id.0, id
"conflicting id for identity value".into(), );
)
.into());
}
id id
} else if eid.is_string() { } else if eid.is_string() {
return Err(TxError::EntityId( bail!(
existing_id.0, "specifying temp_id string {} together with unique constraint",
"specifying temp_id string together with unique constraint".into(), eid
) );
.into());
} else { } else {
existing_id existing_id
} }
@ -415,9 +372,7 @@ impl SessionTx {
} }
} else if let Some(i) = eid.as_u64() { } else if let Some(i) = eid.as_u64() {
let id = EntityId(i); let id = EntityId(i);
if !id.is_perm() { ensure!(id.is_perm(), "temp id not allowed here, found {:?}", id);
return Err(TxError::EntityId(id.0, "temp id specified".into()).into());
}
id id
} else if let Some(s) = eid.as_str() { } else if let Some(s) = eid.as_str() {
temp_id_ctx.str2tempid(s, true) temp_id_ctx.str2tempid(s, true)
@ -453,7 +408,7 @@ impl SessionTx {
let kw = (k as &str).into(); let kw = (k as &str).into();
let attr = self let attr = self
.attr_by_kw(&kw)? .attr_by_kw(&kw)?
.ok_or_else(|| TxError::AttrNotFound(kw.clone()))?; .ok_or_else(|| anyhow!("attribute {} not found", kw))?;
has_unique_attr = has_unique_attr || attr.indexing.is_unique_index(); has_unique_attr = has_unique_attr || attr.indexing.is_unique_index();
has_identity_attr = has_identity_attr || attr.indexing == AttributeIndex::Identity; has_identity_attr = has_identity_attr || attr.indexing == AttributeIndex::Identity;
if attr.indexing == AttributeIndex::Identity { if attr.indexing == AttributeIndex::Identity {
@ -474,13 +429,12 @@ impl SessionTx {
None => {} None => {}
Some(existing_eid) => { Some(existing_eid) => {
if let Some(prev_eid) = eid { if let Some(prev_eid) = eid {
if existing_eid != prev_eid { ensure!(
return Err(TxError::EntityId( existing_eid == prev_eid,
existing_eid.0, "conflicting entity id: {:?} vs {:?}",
"conflicting entity id given".to_string(), existing_eid,
) prev_eid
.into()); );
}
} }
eid = Some(existing_eid) eid = Some(existing_eid)
} }
@ -490,62 +444,52 @@ impl SessionTx {
} }
} }
if let Some(given_id) = item.get(PERM_ID_FIELD) { if let Some(given_id) = item.get(PERM_ID_FIELD) {
let given_id = given_id.as_u64().ok_or_else(|| { let given_id = given_id
TxError::Decoding( .as_u64()
given_id.clone(), .ok_or_else(|| anyhow!("unable to interpret {} as entity id", given_id))?;
"unable to interpret as entity id".to_string(),
)
})?;
let given_id = EntityId(given_id); let given_id = EntityId(given_id);
if !given_id.is_perm() { ensure!(
return Err(TxError::EntityId( given_id.is_perm(),
given_id.0, "temp id not allowed here, found {:?}",
"temp id given where perm id is required".to_string(), given_id
) );
.into());
}
if let Some(prev_id) = eid { if let Some(prev_id) = eid {
if prev_id != given_id { ensure!(
return Err(TxError::EntityId( prev_id == given_id,
given_id.0, "conflicting entity id give: {:?} vs {:?}",
"conflicting entity id given".to_string(), prev_id,
) given_id
.into()); );
}
} }
eid = Some(given_id); eid = Some(given_id);
} }
if let Some(temp_id) = item.get(TEMP_ID_FIELD) { if let Some(temp_id) = item.get(TEMP_ID_FIELD) {
if let Some(eid_inner) = eid { ensure!(
return Err(TxError::EntityId( eid.is_none(),
eid_inner.0, "conflicting entity id given: {:?} vs {}",
"conflicting entity id given".to_string(), eid.unwrap(),
) temp_id
.into()); );
} let temp_id_str = temp_id
let temp_id_str = temp_id.as_str().ok_or_else(|| { .as_str()
TxError::Decoding( .ok_or_else(|| anyhow!("unable to interpret {} as temp id", temp_id))?;
temp_id.clone(),
"unable to interpret as temp id".to_string(),
)
})?;
eid = Some(temp_id_ctx.str2tempid(temp_id_str, true)); eid = Some(temp_id_ctx.str2tempid(temp_id_str, true));
} }
let eid = match eid { let eid = match eid {
Some(eid) => eid, Some(eid) => eid,
None => temp_id_ctx.unnamed_tempid(), None => temp_id_ctx.unnamed_tempid(),
}; };
if action != TxAction::Put && !eid.is_perm() { ensure!(
return Err(TxError::InvalidAction(action, "temp id not allowed".to_string()).into()); action == TxAction::Put || eid.is_perm(),
} "temp id {:?} not allowed for {}",
eid,
action
);
if !is_sub_component { if !is_sub_component {
if action == TxAction::Put && eid.is_perm() && !has_identity_attr { ensure!(
return Err(TxError::InvalidAction( action != TxAction::Put || !eid.is_perm() || has_identity_attr,
action, "upsert requires identity attribute present"
"upsert requires identity attribute present".to_string(), );
)
.into());
}
for (attr, v) in pairs { for (attr, v) in pairs {
self.parse_tx_request_inner(eid, &attr, v, action, since, temp_id_ctx, collected)?; self.parse_tx_request_inner(eid, &attr, v, action, since, temp_id_ctx, collected)?;
} }
@ -555,13 +499,11 @@ impl SessionTx {
} }
} else { } else {
for (attr, _v) in pairs { for (attr, _v) in pairs {
if !attr.indexing.is_unique_index() { ensure!(
return Err(TxError::InvalidAction( attr.indexing.is_unique_index(),
action, "cannot use non-unique attribute {} to specify entity",
"cannot use non-unique fields to specify entity".to_string(), attr.keyword
) );
.into());
}
} }
} }
Ok((eid, has_unique_attr)) Ok((eid, has_unique_attr))

Loading…
Cancel
Save