use crate::envelope::EnvelopeCodecError;
use crate::sst_blocks::BlockHandle;
use crate::WriterEpoch;
use crate::{
ChangeSeq, CommitId, ContentRef, DisplayName, InodeId, InodeKind, ManifestId, ManifestObjectId,
MetadataTableId, NameKey, NamespaceId, RevisionNo,
};
use serde::{Deserialize, Serialize};
pub const NAMESPACE_MANIFEST_FORMAT_VERSION: u32 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum NamespaceManifestKind {
NamespaceManifest,
}
impl NamespaceManifestKind {
pub const fn as_str(self) -> &'static str {
match self {
Self::NamespaceManifest => "namespace_manifest",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MetadataTableFamily {
Inodes,
DirentryBinds,
DirentryChildBinds,
DirentryUnbinds,
Revisions,
RevisionsByInodeDesc,
Tombstones,
ActiveDeletions,
CommitReceipts,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MetadataFileRef {
pub owner_namespace_id: NamespaceId,
pub table_id: MetadataTableId,
pub object_key: String,
pub run_seq: ChangeSeq,
pub level: u32,
pub family: MetadataTableFamily,
pub segment_index: u32,
pub row_count: u64,
pub min_key: String,
pub max_key: String,
pub index_block: BlockHandle,
pub filter_block: BlockHandle,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub filter_inline: Option<String>,
pub payload_checksum: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum MetadataRow {
Inode {
inode_id: InodeId,
inode_kind: InodeKind,
created_seq: ChangeSeq,
},
DirentryBind {
parent_inode_id: InodeId,
name_key: NameKey,
display_name: DisplayName,
child_inode_id: InodeId,
bind_seq: ChangeSeq,
bind_delta_index: u32,
},
DirentryUnbind {
parent_inode_id: InodeId,
name_key: NameKey,
display_name: DisplayName,
child_inode_id: InodeId,
bind_seq: ChangeSeq,
bind_delta_index: u32,
unbind_seq: ChangeSeq,
unbind_delta_index: u32,
},
Revision {
inode_id: InodeId,
revision_no: RevisionNo,
committed_seq: ChangeSeq,
committed_at_ms: u64,
revision_delta_index: u32,
content_ref: ContentRef,
},
Tombstone {
root_inode_id: InodeId,
tombstone_seq: ChangeSeq,
tombstone_delta_index: u32,
action: TombstoneRowAction,
deleted_at_ms: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
parent_inode_id: Option<InodeId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
name_key: Option<NameKey>,
#[serde(default, skip_serializing_if = "Option::is_none")]
display_name: Option<DisplayName>,
},
ActiveDeletion {
root_inode_id: InodeId,
deleted_at_seq: ChangeSeq,
action: ActiveDeletionRowAction,
},
CommitReceipt {
commit_id: CommitId,
semantic_commit_fingerprint: String,
committed_seq: ChangeSeq,
committed_at_ms: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
message: Option<String>,
},
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum TombstoneRowAction {
Set,
Revoke {
target_seq: ChangeSeq,
target_delta_index: u32,
},
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ActiveDeletionRowAction {
Listed {
deleted_at_ms: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
parent_inode_id: Option<InodeId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
name_key: Option<NameKey>,
#[serde(default, skip_serializing_if = "Option::is_none")]
display_name: Option<DisplayName>,
},
Removed {
revoked_at_seq: ChangeSeq,
},
}
impl ActiveDeletionRowAction {
fn sort_rank(&self) -> u8 {
match self {
Self::Removed { .. } => lookup_keys::ACTIVE_DELETION_RANK_REMOVED,
Self::Listed { .. } => lookup_keys::ACTIVE_DELETION_RANK_LISTED,
}
}
}
impl MetadataRow {
pub fn row_key(&self) -> String {
self.row_key_for_family(match self {
Self::Inode { .. } => MetadataTableFamily::Inodes,
Self::DirentryBind { .. } => MetadataTableFamily::DirentryBinds,
Self::DirentryUnbind { .. } => MetadataTableFamily::DirentryUnbinds,
Self::Revision { .. } => MetadataTableFamily::Revisions,
Self::Tombstone { .. } => MetadataTableFamily::Tombstones,
Self::ActiveDeletion { .. } => MetadataTableFamily::ActiveDeletions,
Self::CommitReceipt { .. } => MetadataTableFamily::CommitReceipts,
})
}
pub fn row_key_for_family(&self, family: MetadataTableFamily) -> String {
match self {
Self::Inode { inode_id, .. } => format!("inode-{:020}", inode_id.0),
Self::DirentryBind {
parent_inode_id,
name_key,
child_inode_id,
bind_seq,
bind_delta_index,
..
} => match family {
MetadataTableFamily::DirentryChildBinds => {
let name_key = hex_encode_row_key_component(name_key.as_str());
format!(
"direntry-child-{:020}-{:020}-{:010}-{:020}-{name_key}",
child_inode_id.0, bind_seq.0, bind_delta_index, parent_inode_id.0
)
}
_ => {
let name_key = hex_encode_row_key_component(name_key.as_str());
format!(
"direntry-{:020}-{name_key}-{:020}-{:010}",
parent_inode_id.0, bind_seq.0, bind_delta_index
)
}
},
Self::DirentryUnbind {
parent_inode_id,
name_key,
bind_seq,
bind_delta_index,
unbind_seq,
unbind_delta_index,
..
} => {
let name_key = hex_encode_row_key_component(name_key.as_str());
format!(
"direntry-unbind-{:020}-{name_key}-{:020}-{:010}-{:020}-{:010}",
parent_inode_id.0,
bind_seq.0,
bind_delta_index,
unbind_seq.0,
unbind_delta_index
)
}
Self::Revision {
inode_id,
revision_no,
committed_seq,
revision_delta_index,
..
} => match family {
MetadataTableFamily::RevisionsByInodeDesc => {
let reverse_revision_no = u64::MAX - revision_no.0;
let reverse_committed_seq = u64::MAX - committed_seq.0;
let reverse_delta_index = u32::MAX - revision_delta_index;
format!(
"revision-by-inode-desc-{:020}-{:020}-{:020}-{:010}",
inode_id.0, reverse_revision_no, reverse_committed_seq, reverse_delta_index
)
}
_ => {
format!(
"revision-{:020}-{:020}-{:010}",
inode_id.0, revision_no.0, revision_delta_index
)
}
},
Self::Tombstone {
root_inode_id,
tombstone_seq,
tombstone_delta_index,
..
} => {
format!(
"tombstone-{:020}-{:020}-{:010}",
root_inode_id.0, tombstone_seq.0, tombstone_delta_index
)
}
Self::ActiveDeletion {
root_inode_id,
deleted_at_seq,
action,
} => lookup_keys::active_deletion_row_key(
*deleted_at_seq,
*root_inode_id,
action.sort_rank(),
),
Self::CommitReceipt {
committed_seq,
commit_id,
..
} => {
let commit_id = hex_encode_row_key_component(commit_id.as_str());
format!("commit-receipt-{commit_id}-{:020}", committed_seq.0)
}
}
}
pub fn filter_key_for_family(&self, family: MetadataTableFamily) -> String {
match self {
Self::Inode { .. } => self.row_key_for_family(family),
Self::DirentryBind {
parent_inode_id,
name_key,
child_inode_id,
..
} => match family {
MetadataTableFamily::DirentryChildBinds => {
format!("direntry-child-{:020}", child_inode_id.0)
}
_ => {
let name_key = hex_encode_row_key_component(name_key.as_str());
format!("direntry-{:020}-{name_key}", parent_inode_id.0)
}
},
Self::DirentryUnbind {
parent_inode_id,
name_key,
..
} => {
let name_key = hex_encode_row_key_component(name_key.as_str());
format!("direntry-unbind-{:020}-{name_key}", parent_inode_id.0)
}
Self::Revision { inode_id, .. } => match family {
MetadataTableFamily::RevisionsByInodeDesc => {
format!("revision-by-inode-desc-{:020}", inode_id.0)
}
_ => format!("revision-{:020}", inode_id.0),
},
Self::Tombstone { root_inode_id, .. } => {
format!("tombstone-{:020}", root_inode_id.0)
}
Self::ActiveDeletion { .. } => self.row_key_for_family(family),
Self::CommitReceipt { commit_id, .. } => {
let commit_id = hex_encode_row_key_component(commit_id.as_str());
format!("commit-receipt-{commit_id}")
}
}
}
}
pub fn hex_encode_row_key_component(value: &str) -> String {
crate::hex::hex_encode_bytes(value.as_bytes())
}
pub mod lookup_keys {
use super::hex_encode_row_key_component;
use crate::{ChangeSeq, InodeId, RevisionNo};
pub const INODE_ROW_PREFIX: &str = "inode-";
pub const REVISION_ROW_PREFIX: &str = "revision-";
pub fn inode_key(inode_id: InodeId) -> String {
format!("{INODE_ROW_PREFIX}{:020}", inode_id.0)
}
pub fn inode_key_after(inode_id: InodeId) -> String {
format!("{}\0", inode_key(inode_id))
}
pub fn direntry_parent_prefix(parent_inode_id: InodeId) -> String {
format!("direntry-{:020}-", parent_inode_id.0)
}
pub fn direntry_bind_probe(parent_inode_id: InodeId, name_key: &str) -> String {
format!(
"direntry-{:020}-{}",
parent_inode_id.0,
hex_encode_row_key_component(name_key)
)
}
pub fn direntry_bind_prefix(parent_inode_id: InodeId, name_key: &str) -> String {
format!("{}-", direntry_bind_probe(parent_inode_id, name_key))
}
pub fn direntry_child_probe(child_inode_id: InodeId) -> String {
format!("direntry-child-{:020}", child_inode_id.0)
}
pub fn direntry_child_prefix(child_inode_id: InodeId) -> String {
format!("{}-", direntry_child_probe(child_inode_id))
}
pub fn direntry_unbind_probe(parent_inode_id: InodeId, name_key: &str) -> String {
format!(
"direntry-unbind-{:020}-{}",
parent_inode_id.0,
hex_encode_row_key_component(name_key)
)
}
pub fn direntry_unbind_binding_prefix(
parent_inode_id: InodeId,
name_key: &str,
bind_seq: ChangeSeq,
bind_delta_index: u32,
) -> String {
format!(
"{}-{:020}-{:010}-",
direntry_unbind_probe(parent_inode_id, name_key),
bind_seq.0,
bind_delta_index
)
}
pub fn direntry_unbind_parent_prefix(parent_inode_id: InodeId) -> String {
format!("direntry-unbind-{:020}-", parent_inode_id.0)
}
pub fn direntry_unbind_name_prefix(parent_inode_id: InodeId, name_key: &str) -> String {
format!(
"{}{}-",
direntry_unbind_parent_prefix(parent_inode_id),
hex_encode_row_key_component(name_key)
)
}
pub fn tombstone_probe(root_inode_id: InodeId) -> String {
format!("tombstone-{:020}", root_inode_id.0)
}
pub fn tombstone_prefix(root_inode_id: InodeId) -> String {
format!("{}-", tombstone_probe(root_inode_id))
}
pub const ACTIVE_DELETION_ROW_PREFIX: &str = "active-deletion-";
pub const ACTIVE_DELETION_RANK_REMOVED: u8 = 0;
pub const ACTIVE_DELETION_RANK_LISTED: u8 = 1;
pub fn active_deletion_row_key(
deleted_at_seq: ChangeSeq,
root_inode_id: InodeId,
sort_rank: u8,
) -> String {
format!(
"{ACTIVE_DELETION_ROW_PREFIX}{:020}-{:020}-{sort_rank}",
deleted_at_seq.0, root_inode_id.0
)
}
pub fn active_deletion_key_after(deleted_at_seq: ChangeSeq, root_inode_id: InodeId) -> String {
format!(
"{}\0",
active_deletion_row_key(deleted_at_seq, root_inode_id, ACTIVE_DELETION_RANK_LISTED)
)
}
pub fn commit_receipt_probe(commit_id: &str) -> String {
format!("commit-receipt-{}", hex_encode_row_key_component(commit_id))
}
pub fn commit_receipt_prefix(commit_id: &str) -> String {
format!("{}-", commit_receipt_probe(commit_id))
}
pub fn revision_by_inode_desc_probe(inode_id: InodeId) -> String {
format!("revision-by-inode-desc-{:020}", inode_id.0)
}
pub fn revision_by_inode_desc_prefix(inode_id: InodeId) -> String {
format!("{}-", revision_by_inode_desc_probe(inode_id))
}
pub fn revision_by_inode_desc_revision_prefix(
inode_id: InodeId,
revision_no: RevisionNo,
) -> String {
format!(
"{}{:020}-",
revision_by_inode_desc_prefix(inode_id),
u64::MAX - revision_no.0
)
}
pub fn revision_by_inode_desc_row_key(
inode_id: InodeId,
revision_no: RevisionNo,
committed_seq: ChangeSeq,
revision_delta_index: u32,
) -> String {
format!(
"{}{:020}-{:020}-{:010}",
revision_by_inode_desc_prefix(inode_id),
u64::MAX - revision_no.0,
u64::MAX - committed_seq.0,
u32::MAX - revision_delta_index
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NamespaceManifestPayload {
pub namespace_id: NamespaceId,
pub manifest_id: ManifestId,
pub manifest_object_id: ManifestObjectId,
pub head_seq: ChangeSeq,
pub head_commit_id: CommitId,
pub base_seq: ChangeSeq,
pub writer_epoch: WriterEpoch,
pub next_inode_id: InodeId,
pub retention_floor_seq: ChangeSeq,
pub metadata_files: Vec<MetadataFileRef>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NamespaceManifestEnvelope {
pub kind: NamespaceManifestKind,
pub format_version: u32,
pub payload_checksum: String,
pub payload: NamespaceManifestPayload,
}
impl NamespaceManifestEnvelope {
pub fn from_payload(payload: NamespaceManifestPayload) -> Result<Self, EnvelopeCodecError> {
Ok(Self {
kind: NamespaceManifestKind::NamespaceManifest,
format_version: NAMESPACE_MANIFEST_FORMAT_VERSION,
payload_checksum: namespace_manifest_payload_checksum(&payload)?,
payload,
})
}
}
fn namespace_manifest_payload_checksum(
payload: &NamespaceManifestPayload,
) -> Result<String, EnvelopeCodecError> {
crate::envelope::json_payload_checksum(payload)
}
pub fn encode_namespace_manifest_json(
envelope: &NamespaceManifestEnvelope,
) -> Result<Vec<u8>, EnvelopeCodecError> {
crate::envelope::encode_json_envelope(
envelope.kind.as_str(),
envelope.format_version,
NAMESPACE_MANIFEST_FORMAT_VERSION,
&envelope.payload_checksum,
&envelope.payload,
)
}
pub fn decode_namespace_manifest_json(
bytes: &[u8],
) -> Result<NamespaceManifestEnvelope, EnvelopeCodecError> {
let expected_kind = NamespaceManifestKind::NamespaceManifest;
let decoded =
crate::envelope::decode_json_envelope(bytes, NAMESPACE_MANIFEST_FORMAT_VERSION, |found| {
crate::envelope::verify_kind(expected_kind.as_str(), found)
})?;
Ok(NamespaceManifestEnvelope {
kind: expected_kind,
format_version: decoded.format_version,
payload_checksum: decoded.payload_checksum,
payload: decoded.payload,
})
}
#[cfg(test)]
mod tests {
use super::{
decode_namespace_manifest_json, encode_namespace_manifest_json, BlockHandle,
MetadataFileRef, MetadataTableFamily, NamespaceManifestEnvelope, NamespaceManifestPayload,
};
use crate::{
ChangeSeq, CommitId, InodeId, ManifestId, ManifestObjectId, MetadataTableId, NameKey,
NamespaceId, WriterEpoch,
};
#[test]
fn inode_row_keys_sort_by_ascending_inode_id() {
let ids = [9_u64, 1, 100, 10, 2];
let key_of = |id: u64| super::lookup_keys::inode_key(InodeId(id));
let mut keys: Vec<String> = ids.iter().copied().map(key_of).collect();
keys.sort();
let mut ascending_ids = ids;
ascending_ids.sort_unstable();
assert_eq!(
keys,
ascending_ids
.iter()
.copied()
.map(key_of)
.collect::<Vec<_>>(),
"row-key order must agree with inode-id order"
);
assert!(keys
.iter()
.all(|key| key.starts_with(super::lookup_keys::INODE_ROW_PREFIX)));
}
#[test]
fn the_inode_resume_bound_skips_its_own_row_and_nothing_after_it() {
let resume = super::lookup_keys::inode_key_after(InodeId(7));
assert!(resume > super::lookup_keys::inode_key(InodeId(7)));
assert!(resume < super::lookup_keys::inode_key(InodeId(8)));
}
#[test]
fn namespace_manifest_kind_string_matches_serde() {
let kind = super::NamespaceManifestKind::NamespaceManifest;
let serialized = serde_json::to_value(kind).expect("serialize kind");
assert_eq!(serialized, serde_json::Value::from(kind.as_str()));
}
#[test]
fn namespace_manifest_codec_round_trips_base_only_materialization() {
let envelope = NamespaceManifestEnvelope::from_payload(NamespaceManifestPayload {
namespace_id: NamespaceId::parse("demo").expect("valid namespace id"),
manifest_id: ManifestId(10),
manifest_object_id: ManifestObjectId::parse("00000000000000000010-0123456789abcdef")
.expect("valid manifest object id"),
head_seq: ChangeSeq(10),
head_commit_id: CommitId::parse("c_00000000000000000000000000000001")
.expect("commit id"),
base_seq: ChangeSeq(10),
writer_epoch: WriterEpoch(2),
next_inode_id: InodeId(42),
retention_floor_seq: ChangeSeq(0),
metadata_files: vec![metadata_file_ref(
"demo",
"tbl_00000000000000000000000000000001",
ChangeSeq(10),
1,
"namespaces/demo/metadata/tables/tbl_00000000000000000000000000000001.sst.zst",
)],
})
.expect("manifest");
let encoded = encode_namespace_manifest_json(&envelope).expect("encode manifest");
let decoded = decode_namespace_manifest_json(&encoded).expect("decode manifest");
assert_eq!(decoded, envelope);
assert_eq!(decoded.payload.base_seq, ChangeSeq(10));
assert_eq!(decoded.payload.metadata_files.len(), 1);
assert_eq!(decoded.payload.metadata_files[0].run_seq, ChangeSeq(10));
}
#[test]
fn namespace_manifest_codec_round_trips_inherited_source_tables() {
let envelope = NamespaceManifestEnvelope::from_payload(
NamespaceManifestPayload {
namespace_id: NamespaceId::parse("demo").expect("valid namespace id"),
manifest_id: ManifestId(12),
manifest_object_id: ManifestObjectId::parse(
"00000000000000000012-0123456789abcdef",
)
.expect("valid manifest object id"),
head_seq: ChangeSeq(12),
head_commit_id: CommitId::parse("c_00000000000000000000000000000002")
.expect("commit id"),
base_seq: ChangeSeq(10),
writer_epoch: WriterEpoch(2),
next_inode_id: InodeId(42),
retention_floor_seq: ChangeSeq(0),
metadata_files: vec![
metadata_file_ref(
"source",
"tbl_00000000000000000000000000000001",
ChangeSeq(10),
1,
"namespaces/source/tables/metadata/tbl_00000000000000000000000000000001.sst.zst",
),
metadata_file_ref(
"demo",
"tbl_00000000000000000000000000000002",
ChangeSeq(12),
0,
"namespaces/demo/metadata/tables/tbl_00000000000000000000000000000002.sst.zst",
),
],
},
)
.expect("manifest");
let encoded = encode_namespace_manifest_json(&envelope).expect("encode manifest");
let decoded = decode_namespace_manifest_json(&encoded).expect("decode manifest");
assert_eq!(decoded, envelope);
assert_eq!(decoded.payload.metadata_files[0].level, 1);
assert_eq!(decoded.payload.metadata_files[1].level, 0);
assert_eq!(decoded.payload.metadata_files[1].run_seq, ChangeSeq(12));
assert_eq!(
decoded.payload.metadata_files[0].owner_namespace_id,
NamespaceId::parse("source").expect("valid namespace id")
);
}
#[test]
fn direntry_bind_row_key_supports_parent_and_child_indexes() {
let row = super::MetadataRow::DirentryBind {
parent_inode_id: InodeId(9),
name_key: NameKey::parse("report.txt").expect("valid name key"),
display_name: crate::DisplayName::parse("Report.txt").expect("valid display name"),
child_inode_id: InodeId(42),
bind_seq: ChangeSeq(17),
bind_delta_index: 3,
};
assert_eq!(
row.row_key_for_family(MetadataTableFamily::DirentryBinds),
"direntry-00000000000000000009-7265706f72742e747874-00000000000000000017-0000000003"
);
assert_eq!(
row.row_key_for_family(MetadataTableFamily::DirentryChildBinds),
"direntry-child-00000000000000000042-00000000000000000017-0000000003-00000000000000000009-7265706f72742e747874"
);
}
#[test]
fn row_keys_hex_encode_dash_containing_variable_components() {
let row = super::MetadataRow::DirentryBind {
parent_inode_id: InodeId(9),
name_key: NameKey::parse("report-2024").expect("valid name key"),
display_name: crate::DisplayName::parse("report-2024").expect("valid display name"),
child_inode_id: InodeId(42),
bind_seq: ChangeSeq(17),
bind_delta_index: 3,
};
assert_eq!(
row.row_key_for_family(MetadataTableFamily::DirentryBinds),
"direntry-00000000000000000009-7265706f72742d32303234-00000000000000000017-0000000003"
);
}
#[test]
fn revision_row_key_supports_newest_first_inode_index() {
let row = super::MetadataRow::Revision {
inode_id: InodeId(42),
revision_no: crate::RevisionNo(7),
committed_seq: ChangeSeq(12),
committed_at_ms: 12_000,
revision_delta_index: 3,
content_ref: crate::ContentRef::blob_v1(
crate::ContentId::parse("con_0123456789abcdef0123456789abcdef")
.expect("valid content id"),
b"row key sample",
),
};
assert_eq!(
row.row_key_for_family(MetadataTableFamily::Revisions),
"revision-00000000000000000042-00000000000000000007-0000000003"
);
assert_eq!(
row.row_key_for_family(MetadataTableFamily::RevisionsByInodeDesc),
"revision-by-inode-desc-00000000000000000042-18446744073709551608-18446744073709551603-4294967292"
);
}
fn metadata_file_ref(
owner_namespace_id: &str,
table_id: &str,
run_seq: ChangeSeq,
level: u32,
object_key: &str,
) -> MetadataFileRef {
MetadataFileRef {
owner_namespace_id: NamespaceId::parse(owner_namespace_id).expect("valid namespace id"),
table_id: MetadataTableId::parse(table_id).expect("valid table id"),
object_key: object_key.to_owned(),
run_seq,
level,
family: MetadataTableFamily::Inodes,
segment_index: 0,
row_count: 0,
min_key: String::new(),
max_key: String::new(),
index_block: BlockHandle {
offset: 0,
stored_len: 0,
decoded_len: 0,
crc32c: 0,
},
filter_block: BlockHandle {
offset: 0,
stored_len: 0,
decoded_len: 0,
crc32c: 0,
},
filter_inline: None,
payload_checksum: "sha256:unused".to_owned(),
}
}
}