1use bytes::Buf;
19use bytes::BufMut;
20use sui_consistent_store::Decode;
21use sui_consistent_store::Encode;
22use sui_consistent_store::Iter;
23use sui_consistent_store::Protobuf;
24use sui_consistent_store::error::DecodeError;
25use sui_consistent_store::error::EncodeError;
26use sui_consistent_store::error::Error;
27use sui_consistent_store::reader::Reader;
28use sui_types::base_types::ObjectID;
29
30use crate::proto::PackageVersionInfo;
31
32pub const NAME: &str = "package_versions";
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
35pub struct Key {
36 pub original_id: ObjectID,
37 pub version: u64,
38}
39
40pub type Value = Protobuf<PackageVersionInfo>;
41
42impl Encode for Key {
43 fn encode_into<B: BufMut>(&self, buf: &mut B) -> Result<(), EncodeError> {
44 buf.put_slice(self.original_id.as_ref());
45 buf.put_slice(&self.version.to_be_bytes());
46 Ok(())
47 }
48}
49
50impl Decode for Key {
51 fn decode<B: Buf>(buf: &mut B) -> Result<Self, DecodeError> {
52 if buf.remaining() != ObjectID::LENGTH + 8 {
53 return Err(DecodeError::msg(format!(
54 "expected {} bytes for {NAME} Key, got {}",
55 ObjectID::LENGTH + 8,
56 buf.remaining(),
57 )));
58 }
59 let mut id = [0u8; ObjectID::LENGTH];
60 buf.copy_to_slice(&mut id);
61 let version = buf.get_u64();
62 Ok(Key {
63 original_id: ObjectID::new(id),
64 version,
65 })
66 }
67}
68
69pub fn options(resolver: &sui_consistent_store::CfOptionsResolver) -> rocksdb::Options {
70 resolver.options(NAME)
71}
72
73pub fn store(
78 original_id: ObjectID,
79 version: u64,
80 storage_id: ObjectID,
81 checkpoint: u64,
82) -> (Key, Value) {
83 (
84 Key {
85 original_id,
86 version,
87 },
88 Protobuf(PackageVersionInfo {
89 storage_id: storage_id.to_vec().into(),
90 checkpoint: Some(checkpoint),
91 }),
92 )
93}
94
95pub fn store_restored(original_id: ObjectID, version: u64, storage_id: ObjectID) -> (Key, Value) {
101 (
102 Key {
103 original_id,
104 version,
105 },
106 Protobuf(PackageVersionInfo {
107 storage_id: storage_id.to_vec().into(),
108 checkpoint: None,
109 }),
110 )
111}
112
113pub struct OriginalIdPrefix(pub ObjectID);
118
119impl Encode for OriginalIdPrefix {
120 fn encode_into<B: BufMut>(&self, buf: &mut B) -> Result<(), EncodeError> {
121 buf.put_slice(self.0.as_ref());
122 Ok(())
123 }
124}
125
126fn decode_storage_id(bytes: &[u8]) -> Result<ObjectID, DecodeError> {
129 let array: [u8; ObjectID::LENGTH] = bytes.try_into().map_err(|_| {
130 DecodeError::msg(format!(
131 "expected {} bytes for storage_id, got {}",
132 ObjectID::LENGTH,
133 bytes.len(),
134 ))
135 })?;
136 Ok(ObjectID::new(array))
137}
138
139impl<R: Reader> super::RpcStoreSchema<R> {
140 pub fn get_package_storage_id(
143 &self,
144 original_id: ObjectID,
145 version: u64,
146 ) -> Result<Option<ObjectID>, Error> {
147 let Some(stored) = self.package_versions.get(&Key {
148 original_id,
149 version,
150 })?
151 else {
152 return Ok(None);
153 };
154 Ok(Some(decode_storage_id(&stored.into_inner().storage_id)?))
155 }
156
157 pub fn iter_package_versions(
160 &self,
161 original_id: ObjectID,
162 ) -> Result<Iter<'_, Key, Value>, Error> {
163 self.package_versions
164 .iter_prefix(&OriginalIdPrefix(original_id))
165 }
166
167 pub fn get_package_at_checkpoint(
184 &self,
185 original_id: ObjectID,
186 checkpoint: u64,
187 ) -> Result<Option<(u64, ObjectID)>, Error> {
188 for row in self
189 .package_versions
190 .iter_rev_prefix(&OriginalIdPrefix(original_id))?
191 {
192 let (key, value) = row?;
193 let info = value.into_inner();
194 if info
195 .checkpoint
196 .is_none_or(|published| published <= checkpoint)
197 {
198 return Ok(Some((key.version, decode_storage_id(&info.storage_id)?)));
199 }
200 }
201 Ok(None)
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use sui_consistent_store::Db;
208 use sui_consistent_store::DbOptions;
209
210 use super::*;
211 use crate::RpcStoreSchema;
212
213 fn fresh_db() -> (tempfile::TempDir, sui_consistent_store::Db, RpcStoreSchema) {
214 let dir = tempfile::tempdir().unwrap();
215 let (db, schema) = Db::open::<RpcStoreSchema>(dir.path(), DbOptions::default()).unwrap();
216 (dir, db, schema)
217 }
218
219 #[test]
220 fn get_returns_none_for_unknown_version() {
221 let (_dir, _db, schema) = fresh_db();
222 let original = ObjectID::random();
223 assert!(
224 schema
225 .get_package_storage_id(original, 1)
226 .unwrap()
227 .is_none()
228 );
229 }
230
231 #[test]
232 fn store_then_get_round_trips() {
233 let (_dir, db, schema) = fresh_db();
234 let original = ObjectID::random();
235 let storage = ObjectID::random();
236
237 let (k, v) = store(original, 3, storage, 100);
238 let mut batch = db.batch();
239 batch.put(&schema.package_versions, &k, &v).unwrap();
240 batch.commit().unwrap();
241
242 assert_eq!(
243 schema.get_package_storage_id(original, 3).unwrap(),
244 Some(storage),
245 );
246 }
247
248 #[test]
249 fn iter_walks_versions_for_one_package() {
250 let (_dir, db, schema) = fresh_db();
251 let original = ObjectID::random();
252 let other = ObjectID::random();
253
254 let mut batch = db.batch();
255 for version in [1u64, 2, 5] {
257 let (k, v) = store(original, version, ObjectID::random(), version);
258 batch.put(&schema.package_versions, &k, &v).unwrap();
259 }
260 let (k, v) = store(other, 1, ObjectID::random(), 1);
262 batch.put(&schema.package_versions, &k, &v).unwrap();
263 batch.commit().unwrap();
264
265 let versions: Vec<u64> = schema
266 .iter_package_versions(original)
267 .unwrap()
268 .map(|res| res.unwrap().0.version)
269 .collect();
270 assert_eq!(versions, vec![1, 2, 5]);
271 }
272
273 #[test]
274 fn get_package_at_checkpoint_resolves_latest_in_window() {
275 let (_dir, db, schema) = fresh_db();
276 let original = ObjectID::random();
277 let (s1, s2, s3) = (ObjectID::random(), ObjectID::random(), ObjectID::random());
278
279 let mut batch = db.batch();
280 let (k, v) = store_restored(original, 1, s1);
283 batch.put(&schema.package_versions, &k, &v).unwrap();
284 let (k, v) = store(original, 2, s2, 10);
285 batch.put(&schema.package_versions, &k, &v).unwrap();
286 let (k, v) = store(original, 3, s3, 20);
287 batch.put(&schema.package_versions, &k, &v).unwrap();
288 batch.commit().unwrap();
289
290 assert_eq!(
292 schema.get_package_at_checkpoint(original, 5).unwrap(),
293 Some((1, s1)),
294 );
295 assert_eq!(
297 schema.get_package_at_checkpoint(original, 10).unwrap(),
298 Some((2, s2)),
299 );
300 assert_eq!(
301 schema.get_package_at_checkpoint(original, 15).unwrap(),
302 Some((2, s2)),
303 );
304 assert_eq!(
306 schema.get_package_at_checkpoint(original, 20).unwrap(),
307 Some((3, s3)),
308 );
309 assert_eq!(
310 schema.get_package_at_checkpoint(original, 9_999).unwrap(),
311 Some((3, s3)),
312 );
313 }
314
315 #[test]
316 fn get_package_at_checkpoint_returns_none_before_first_publish() {
317 let (_dir, db, schema) = fresh_db();
318 let original = ObjectID::random();
319
320 let mut batch = db.batch();
321 let (k, v) = store(original, 1, ObjectID::random(), 50);
322 batch.put(&schema.package_versions, &k, &v).unwrap();
323 batch.commit().unwrap();
324
325 assert_eq!(
328 schema.get_package_at_checkpoint(original, 49).unwrap(),
329 None,
330 );
331 assert!(
332 schema
333 .get_package_at_checkpoint(original, 50)
334 .unwrap()
335 .is_some()
336 );
337 assert_eq!(
339 schema
340 .get_package_at_checkpoint(ObjectID::random(), 100)
341 .unwrap(),
342 None,
343 );
344 }
345}