rust_rocksdb/table_properties.rs
1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License at
4//
5// http://www.apache.org/licenses/LICENSE-2.0
6//
7// Unless required by applicable law or agreed to in writing, software
8// distributed under the License is distributed on an "AS IS" BASIS,
9// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10// See the License for the specific language governing permissions and
11// limitations under the License.
12//
13
14//! Read only properties of a single SST file.
15//!
16//! [`TableProperties`] is the wrapper over RocksDB's `TableProperties`: the sizes, counts,
17//! and names recorded when the file was written, plus anything a `TablePropertiesCollector`
18//! added on top.
19//!
20//! You never build or own one of these. RocksDB hands out a borrowed pointer from a flush
21//! job info, a compaction job info, or an external file ingestion info, and the properties
22//! stay alive only as long as that event object does. The `'a` lifetime ties the wrapper
23//! and every byte slice it hands back to that borrow, so nothing here outlives the callback
24//! it came from.
25//!
26//! String-like getters return raw bytes rather than `str`. RocksDB does not guarantee UTF-8
27//! for user collected properties, and the built in names are only ASCII by convention. The
28//! slices point straight into the C++ strings, so reading them copies and allocates nothing.
29
30use std::marker::PhantomData;
31
32use libc::c_char;
33
34use crate::ffi;
35use crate::ffi_util::bytes_from_raw;
36
37/// Shared signature of the `rocksdb_table_properties_*` string getters.
38type StringGetter =
39 unsafe extern "C" fn(*const ffi::rocksdb_table_properties_t, *mut usize) -> *const c_char;
40
41/// Shared signature of the property map key and value accessors.
42type MapEntryGetter = unsafe extern "C" fn(
43 *const ffi::rocksdb_table_properties_t,
44 usize,
45 *mut usize,
46) -> *const c_char;
47
48/// Properties of one SST file, borrowed from the event that produced it.
49pub struct TableProperties<'a> {
50 inner: *const ffi::rocksdb_table_properties_t,
51 _marker: PhantomData<&'a ()>,
52}
53
54impl<'a> TableProperties<'a> {
55 /// Wraps a table properties pointer owned by RocksDB.
56 ///
57 /// # Safety
58 ///
59 /// `inner` must point to a live `rocksdb_table_properties_t` that stays valid for all of
60 /// `'a`. RocksDB owns the object, so the caller must never free it and must not pick an
61 /// `'a` that outlives the flush, compaction, or ingestion info it was read from.
62 pub(crate) unsafe fn from_ptr(
63 inner: *const ffi::rocksdb_table_properties_t,
64 ) -> TableProperties<'a> {
65 TableProperties {
66 inner,
67 _marker: PhantomData,
68 }
69 }
70
71 /// File number at creation time, or 0 when unknown. When known it identifies the SST file
72 /// uniquely in combination with [`Self::db_session_id`].
73 pub fn orig_file_number(&self) -> u64 {
74 unsafe { ffi::rocksdb_table_properties_orig_file_number(self.inner) }
75 }
76
77 /// Total size of all data blocks.
78 pub fn data_size(&self) -> u64 {
79 unsafe { ffi::rocksdb_table_properties_data_size(self.inner) }
80 }
81
82 /// Total uncompressed size of all data blocks. Recorded since RocksDB 10.7.
83 pub fn uncompressed_data_size(&self) -> u64 {
84 unsafe { ffi::rocksdb_table_properties_uncompressed_data_size(self.inner) }
85 }
86
87 /// Size of the index block.
88 pub fn index_size(&self) -> u64 {
89 unsafe { ffi::rocksdb_table_properties_index_size(self.inner) }
90 }
91
92 /// Number of index partitions, set only when the two level index search is used.
93 pub fn index_partitions(&self) -> u64 {
94 unsafe { ffi::rocksdb_table_properties_index_partitions(self.inner) }
95 }
96
97 /// Size of the top level index, set only when the two level index search is used.
98 pub fn top_level_index_size(&self) -> u64 {
99 unsafe { ffi::rocksdb_table_properties_top_level_index_size(self.inner) }
100 }
101
102 /// Whether index keys are plain user keys. When false they also carry the 8 byte sequence
103 /// number of the internal key format.
104 pub fn index_key_is_user_key(&self) -> bool {
105 unsafe { ffi::rocksdb_table_properties_index_key_is_user_key(self.inner) != 0 }
106 }
107
108 /// Whether index values are delta encoded.
109 pub fn index_value_is_delta_encoded(&self) -> bool {
110 unsafe { ffi::rocksdb_table_properties_index_value_is_delta_encoded(self.inner) != 0 }
111 }
112
113 /// Whether the UDI is the primary index for reads. The standard index is still fully
114 /// populated alongside it.
115 pub fn udi_is_primary_index(&self) -> bool {
116 unsafe { ffi::rocksdb_table_properties_udi_is_primary_index(self.inner) != 0 }
117 }
118
119 /// Size of the filter block.
120 pub fn filter_size(&self) -> u64 {
121 unsafe { ffi::rocksdb_table_properties_filter_size(self.inner) }
122 }
123
124 /// Total key size before compression and block encoding.
125 pub fn raw_key_size(&self) -> u64 {
126 unsafe { ffi::rocksdb_table_properties_raw_key_size(self.inner) }
127 }
128
129 /// Total value size before compression and block encoding.
130 pub fn raw_value_size(&self) -> u64 {
131 unsafe { ffi::rocksdb_table_properties_raw_value_size(self.inner) }
132 }
133
134 /// Number of data blocks in this file.
135 pub fn num_data_blocks(&self) -> u64 {
136 unsafe { ffi::rocksdb_table_properties_num_data_blocks(self.inner) }
137 }
138
139 /// Data blocks stored uncompressed because the compressed output blew past the ratio limit
140 /// in `CompressionOptions::max_compressed_bytes_per_kb`.
141 pub fn num_data_blocks_compression_rejected(&self) -> u64 {
142 unsafe { ffi::rocksdb_table_properties_num_data_blocks_compression_rejected(self.inner) }
143 }
144
145 /// Data blocks stored uncompressed because compression was never attempted, for example
146 /// with `kNoCompression` or with no compressor available.
147 pub fn num_data_blocks_compression_bypassed(&self) -> u64 {
148 unsafe { ffi::rocksdb_table_properties_num_data_blocks_compression_bypassed(self.inner) }
149 }
150
151 /// Number of uniform blocks in this file.
152 pub fn num_uniform_blocks(&self) -> u64 {
153 unsafe { ffi::rocksdb_table_properties_num_uniform_blocks(self.inner) }
154 }
155
156 /// Number of entries in this file.
157 pub fn num_entries(&self) -> u64 {
158 unsafe { ffi::rocksdb_table_properties_num_entries(self.inner) }
159 }
160
161 /// Number of unique entries, keys or prefixes, added to the filter.
162 pub fn num_filter_entries(&self) -> u64 {
163 unsafe { ffi::rocksdb_table_properties_num_filter_entries(self.inner) }
164 }
165
166 /// Number of deletions in this file.
167 pub fn num_deletions(&self) -> u64 {
168 unsafe { ffi::rocksdb_table_properties_num_deletions(self.inner) }
169 }
170
171 /// Number of merge operands in this file.
172 pub fn num_merge_operands(&self) -> u64 {
173 unsafe { ffi::rocksdb_table_properties_num_merge_operands(self.inner) }
174 }
175
176 /// Number of range deletions in this file.
177 pub fn num_range_deletions(&self) -> u64 {
178 unsafe { ffi::rocksdb_table_properties_num_range_deletions(self.inner) }
179 }
180
181 /// SST format version, reserved for backward compatibility.
182 pub fn format_version(&self) -> u64 {
183 unsafe { ffi::rocksdb_table_properties_format_version(self.inner) }
184 }
185
186 /// Byte length shared by every key, or 0 when keys are variable length.
187 pub fn fixed_key_len(&self) -> u64 {
188 unsafe { ffi::rocksdb_table_properties_fixed_key_len(self.inner) }
189 }
190
191 /// Id of the column family this file belongs to, matching [`Self::column_family_name`]. An
192 /// unknown column family reads back as `i32::MAX`.
193 pub fn column_family_id(&self) -> u64 {
194 unsafe { ffi::rocksdb_table_properties_column_family_id(self.inner) }
195 }
196
197 /// Oldest ancestor time, 0 when unknown. For a flush this is the oldest key time in the
198 /// file, falling back to the flush time. For a compaction it is the oldest such time across
199 /// all input files, falling back to when this output file was created.
200 pub fn creation_time(&self) -> u64 {
201 unsafe { ffi::rocksdb_table_properties_creation_time(self.inner) }
202 }
203
204 /// Timestamp of the earliest key, 0 when unknown.
205 pub fn oldest_key_time(&self) -> u64 {
206 unsafe { ffi::rocksdb_table_properties_oldest_key_time(self.inner) }
207 }
208
209 /// Timestamp of the newest key, 0 when unknown.
210 pub fn newest_key_time(&self) -> u64 {
211 unsafe { ffi::rocksdb_table_properties_newest_key_time(self.inner) }
212 }
213
214 /// Time the SST file was actually created, 0 when unknown.
215 pub fn file_creation_time(&self) -> u64 {
216 unsafe { ffi::rocksdb_table_properties_file_creation_time(self.inner) }
217 }
218
219 /// Estimated size of the data blocks under a relatively slower compression algorithm, 0
220 /// when unknown. Comes from `ColumnFamilyOptions::sample_for_compression`.
221 pub fn slow_compression_estimated_data_size(&self) -> u64 {
222 unsafe { ffi::rocksdb_table_properties_slow_compression_estimated_data_size(self.inner) }
223 }
224
225 /// Estimated size of the data blocks under a relatively faster compression algorithm, 0
226 /// when unknown. Comes from `ColumnFamilyOptions::sample_for_compression`.
227 pub fn fast_compression_estimated_data_size(&self) -> u64 {
228 unsafe { ffi::rocksdb_table_properties_fast_compression_estimated_data_size(self.inner) }
229 }
230
231 /// Offset within the file of the external SST file global seqno value, 0 when the file has
232 /// no such property.
233 pub fn external_sst_file_global_seqno_offset(&self) -> u64 {
234 unsafe { ffi::rocksdb_table_properties_external_sst_file_global_seqno_offset(self.inner) }
235 }
236
237 /// Offset where the tail of the file begins, meaning every block after the data blocks.
238 pub fn tail_start_offset(&self) -> u64 {
239 unsafe { ffi::rocksdb_table_properties_tail_start_offset(self.inner) }
240 }
241
242 /// Value of `AdvancedColumnFamilyOptions::persist_user_defined_timestamps` when the file
243 /// was written. Defaults to true and is only recorded in the file when false.
244 pub fn user_defined_timestamps_persisted(&self) -> bool {
245 unsafe { ffi::rocksdb_table_properties_user_defined_timestamps_persisted(self.inner) != 0 }
246 }
247
248 /// Largest sequence number among the keys in this file. Only meaningful when
249 /// [`Self::has_key_largest_seqno`] is true, otherwise it reads back as `u64::MAX`.
250 pub fn key_largest_seqno(&self) -> u64 {
251 unsafe { ffi::rocksdb_table_properties_key_largest_seqno(self.inner) }
252 }
253
254 /// Smallest sequence number among the keys in this file. Only meaningful when
255 /// [`Self::has_key_smallest_seqno`] is true, otherwise it reads back as `u64::MAX`.
256 pub fn key_smallest_seqno(&self) -> u64 {
257 unsafe { ffi::rocksdb_table_properties_key_smallest_seqno(self.inner) }
258 }
259
260 /// Whether [`Self::key_largest_seqno`] holds a real sequence number. It should be true
261 /// unless the file is empty.
262 pub fn has_key_largest_seqno(&self) -> bool {
263 unsafe { ffi::rocksdb_table_properties_has_key_largest_seqno(self.inner) != 0 }
264 }
265
266 /// Whether [`Self::key_smallest_seqno`] holds a real sequence number. It should be true
267 /// unless the file is empty.
268 pub fn has_key_smallest_seqno(&self) -> bool {
269 unsafe { ffi::rocksdb_table_properties_has_key_smallest_seqno(self.inner) != 0 }
270 }
271
272 /// Restart interval used for data blocks when the file was written, 0 when unknown.
273 pub fn data_block_restart_interval(&self) -> u64 {
274 unsafe { ffi::rocksdb_table_properties_data_block_restart_interval(self.inner) }
275 }
276
277 /// Restart interval used for index blocks when the file was written, 0 when unknown.
278 pub fn index_block_restart_interval(&self) -> u64 {
279 unsafe { ffi::rocksdb_table_properties_index_block_restart_interval(self.inner) }
280 }
281
282 /// Whether data blocks store keys and values separately. The block footer is the real
283 /// source of truth, this property exists for debugging and validation.
284 pub fn separate_key_value_in_data_block(&self) -> bool {
285 unsafe { ffi::rocksdb_table_properties_separate_key_value_in_data_block(self.inner) != 0 }
286 }
287
288 /// DB identity, generated the first time the DB was created. Empty when unset.
289 pub fn db_id(&self) -> &'a [u8] {
290 self.string_field(ffi::rocksdb_table_properties_db_id)
291 }
292
293 /// DB session identity, regenerated every time the DB is opened. Empty when unset.
294 pub fn db_session_id(&self) -> &'a [u8] {
295 self.string_field(ffi::rocksdb_table_properties_db_session_id)
296 }
297
298 /// Location of the machine hosting the DB, the hostname by default. It can change whenever
299 /// the DB is reopened.
300 pub fn db_host_id(&self) -> &'a [u8] {
301 self.string_field(ffi::rocksdb_table_properties_db_host_id)
302 }
303
304 /// Name of the column family this file belongs to. Empty when the column family is unknown.
305 pub fn column_family_name(&self) -> &'a [u8] {
306 self.string_field(ffi::rocksdb_table_properties_column_family_name)
307 }
308
309 /// Name of the filter policy used for this file. Empty when no filter policy was used.
310 pub fn filter_policy_name(&self) -> &'a [u8] {
311 self.string_field(ffi::rocksdb_table_properties_filter_policy_name)
312 }
313
314 /// Name of the comparator used for this file.
315 pub fn comparator_name(&self) -> &'a [u8] {
316 self.string_field(ffi::rocksdb_table_properties_comparator_name)
317 }
318
319 /// Name of the merge operator used for this file. Reads back as `nullptr` when no merge
320 /// operator was used.
321 pub fn merge_operator_name(&self) -> &'a [u8] {
322 self.string_field(ffi::rocksdb_table_properties_merge_operator_name)
323 }
324
325 /// Name of the prefix extractor used for this file. Reads back as `nullptr` when no prefix
326 /// extractor was used.
327 pub fn prefix_extractor_name(&self) -> &'a [u8] {
328 self.string_field(ffi::rocksdb_table_properties_prefix_extractor_name)
329 }
330
331 /// Comma separated names of the property collector factories used for this file.
332 pub fn property_collectors_names(&self) -> &'a [u8] {
333 self.string_field(ffi::rocksdb_table_properties_property_collectors_names)
334 }
335
336 /// Identifies the compression algorithm or schema used for this file. Below format version
337 /// 7 it is a built in compression type name, from version 7 on it is
338 /// `<compatibility_name>;<hex coded compression types>;<future use>`.
339 pub fn compression_name(&self) -> &'a [u8] {
340 self.string_field(ffi::rocksdb_table_properties_compression_name)
341 }
342
343 /// Compression options used to compress this file.
344 pub fn compression_options(&self) -> &'a [u8] {
345 self.string_field(ffi::rocksdb_table_properties_compression_options)
346 }
347
348 /// Delta encoded sequence number to time mapping.
349 pub fn seqno_to_time_mapping(&self) -> &'a [u8] {
350 self.string_field(ffi::rocksdb_table_properties_seqno_to_time_mapping)
351 }
352
353 /// Number of user collected properties recorded for this file.
354 pub fn user_collected_properties_count(&self) -> usize {
355 unsafe { ffi::rocksdb_table_properties_user_collected_properties_count(self.inner) }
356 }
357
358 /// The user collected property at `pos` in key order, or `None` once `pos` runs past the
359 /// end.
360 ///
361 /// Costs O(pos): the C API walks the underlying `std::map` from the beginning on every
362 /// call, so random access here is not cheap.
363 pub fn user_collected_property_at(&self, pos: usize) -> Option<(&'a [u8], &'a [u8])> {
364 self.map_entry_at(
365 pos,
366 ffi::rocksdb_table_properties_user_collected_properties_key_at,
367 ffi::rocksdb_table_properties_user_collected_properties_value_at,
368 )
369 }
370
371 /// Walks the user collected properties in key order, borrowing every key and value.
372 ///
373 /// Lazy and allocation free, but each step costs O(pos) because the C API walks the map
374 /// from the beginning for every lookup. A full pass over n properties is therefore O(n^2),
375 /// which is fine for the handful of entries most collectors emit and slow if you have
376 /// thousands.
377 pub fn user_collected_properties(&self) -> impl Iterator<Item = (&'a [u8], &'a [u8])> + '_ {
378 (0..self.user_collected_properties_count())
379 .map_while(move |pos| self.user_collected_property_at(pos))
380 }
381
382 /// Number of human readable properties recorded for this file. These are what collectors
383 /// return from `GetReadableProperties` and exist for logging.
384 pub fn readable_properties_count(&self) -> usize {
385 unsafe { ffi::rocksdb_table_properties_readable_properties_count(self.inner) }
386 }
387
388 /// The readable property at `pos` in key order, or `None` once `pos` runs past the end.
389 ///
390 /// Costs O(pos): the C API walks the underlying `std::map` from the beginning on every
391 /// call, so random access here is not cheap.
392 pub fn readable_property_at(&self, pos: usize) -> Option<(&'a [u8], &'a [u8])> {
393 self.map_entry_at(
394 pos,
395 ffi::rocksdb_table_properties_readable_properties_key_at,
396 ffi::rocksdb_table_properties_readable_properties_value_at,
397 )
398 }
399
400 /// Walks the human readable properties in key order, borrowing every key and value.
401 ///
402 /// Lazy and allocation free, but each step costs O(pos) because the C API walks the map
403 /// from the beginning for every lookup. A full pass over n properties is therefore O(n^2),
404 /// which is fine for the handful of entries most collectors emit and slow if you have
405 /// thousands.
406 pub fn readable_properties(&self) -> impl Iterator<Item = (&'a [u8], &'a [u8])> + '_ {
407 (0..self.readable_properties_count()).map_while(move |pos| self.readable_property_at(pos))
408 }
409
410 /// Reads one of the borrowed string fields as raw bytes.
411 fn string_field(&self, getter: StringGetter) -> &'a [u8] {
412 let mut len: usize = 0;
413 // SAFETY: `self.inner` is valid for `'a` and the getter writes the byte length through
414 // `len`, returning an interior pointer into a string RocksDB owns.
415 unsafe {
416 let ptr = getter(self.inner, &raw mut len);
417 bytes_from_raw(ptr, len)
418 }
419 }
420
421 /// Reads one key and value pair out of a property map, or `None` when `pos` is out of range.
422 ///
423 /// The C API signals out of range with a null key pointer, and the value pointer of an
424 /// entry that exists is never null even when the value is empty.
425 fn map_entry_at(
426 &self,
427 pos: usize,
428 key_at: MapEntryGetter,
429 value_at: MapEntryGetter,
430 ) -> Option<(&'a [u8], &'a [u8])> {
431 let mut key_len: usize = 0;
432 let mut value_len: usize = 0;
433 // SAFETY: `self.inner` is valid for `'a`, both getters bounds check `pos` themselves,
434 // and they write the byte lengths through the out params they are given.
435 unsafe {
436 let key_ptr = key_at(self.inner, pos, &raw mut key_len);
437 if key_ptr.is_null() {
438 return None;
439 }
440 let value_ptr = value_at(self.inner, pos, &raw mut value_len);
441 Some((
442 bytes_from_raw(key_ptr, key_len),
443 bytes_from_raw(value_ptr, value_len),
444 ))
445 }
446 }
447}