/src/rocksdb/util/udt_util.cc
Line | Count | Source |
1 | | // Copyright (c) Meta Platforms, Inc. and affiliates. |
2 | | // |
3 | | // This source code is licensed under both the GPLv2 (found in the |
4 | | // COPYING file in the root directory) and Apache 2.0 License |
5 | | // (found in the LICENSE.Apache file in the root directory). |
6 | | |
7 | | #include "util/udt_util.h" |
8 | | |
9 | | #include "db/dbformat.h" |
10 | | #include "rocksdb/types.h" |
11 | | #include "util/coding.h" |
12 | | #include "util/write_batch_util.h" |
13 | | |
14 | | namespace ROCKSDB_NAMESPACE { |
15 | | namespace { |
16 | | enum class RecoveryType { |
17 | | kNoop, |
18 | | kUnrecoverable, |
19 | | kStripTimestamp, |
20 | | kPadTimestamp, |
21 | | }; |
22 | | |
23 | | RecoveryType GetRecoveryType(const size_t running_ts_sz, |
24 | 825k | const std::optional<size_t>& recorded_ts_sz) { |
25 | 825k | if (running_ts_sz == 0) { |
26 | 825k | if (!recorded_ts_sz.has_value()) { |
27 | | // A column family id not recorded is equivalent to that column family has |
28 | | // zero timestamp size. |
29 | 825k | return RecoveryType::kNoop; |
30 | 825k | } |
31 | 0 | return RecoveryType::kStripTimestamp; |
32 | 825k | } |
33 | | |
34 | 825k | assert(running_ts_sz != 0); |
35 | |
|
36 | 0 | if (!recorded_ts_sz.has_value()) { |
37 | 0 | return RecoveryType::kPadTimestamp; |
38 | 0 | } |
39 | | |
40 | 0 | if (running_ts_sz != *recorded_ts_sz) { |
41 | 0 | return RecoveryType::kUnrecoverable; |
42 | 0 | } |
43 | | |
44 | 0 | return RecoveryType::kNoop; |
45 | 0 | } |
46 | | |
47 | | bool AllRunningColumnFamiliesConsistent( |
48 | | const UnorderedMap<uint32_t, size_t>& running_ts_sz, |
49 | 809k | const UnorderedMap<uint32_t, size_t>& record_ts_sz) { |
50 | 825k | for (const auto& [cf_id, ts_sz] : running_ts_sz) { |
51 | 825k | auto record_it = record_ts_sz.find(cf_id); |
52 | 825k | RecoveryType recovery_type = |
53 | 825k | GetRecoveryType(ts_sz, record_it != record_ts_sz.end() |
54 | 825k | ? std::optional<size_t>(record_it->second) |
55 | 825k | : std::nullopt); |
56 | 825k | if (recovery_type != RecoveryType::kNoop) { |
57 | 0 | return false; |
58 | 0 | } |
59 | 825k | } |
60 | 809k | return true; |
61 | 809k | } |
62 | | |
63 | | Status CheckWriteBatchTimestampSizeConsistency( |
64 | | const WriteBatch* batch, |
65 | | const UnorderedMap<uint32_t, size_t>& running_ts_sz, |
66 | | const UnorderedMap<uint32_t, size_t>& record_ts_sz, |
67 | 0 | TimestampSizeConsistencyMode check_mode, bool* ts_need_recovery) { |
68 | 0 | std::vector<uint32_t> column_family_ids; |
69 | 0 | Status status = |
70 | 0 | CollectColumnFamilyIdsFromWriteBatch(*batch, &column_family_ids); |
71 | 0 | if (!status.ok()) { |
72 | 0 | return status; |
73 | 0 | } |
74 | 0 | for (const auto& cf_id : column_family_ids) { |
75 | 0 | auto running_iter = running_ts_sz.find(cf_id); |
76 | 0 | if (running_iter == running_ts_sz.end()) { |
77 | | // Ignore dropped column family referred to in a WriteBatch regardless of |
78 | | // its consistency. |
79 | 0 | continue; |
80 | 0 | } |
81 | 0 | auto record_iter = record_ts_sz.find(cf_id); |
82 | 0 | RecoveryType recovery_type = GetRecoveryType( |
83 | 0 | running_iter->second, record_iter != record_ts_sz.end() |
84 | 0 | ? std::optional<size_t>(record_iter->second) |
85 | 0 | : std::nullopt); |
86 | 0 | if (recovery_type != RecoveryType::kNoop) { |
87 | 0 | if (check_mode == TimestampSizeConsistencyMode::kVerifyConsistency) { |
88 | 0 | return Status::InvalidArgument( |
89 | 0 | "WriteBatch contains timestamp size inconsistency."); |
90 | 0 | } |
91 | | |
92 | 0 | if (recovery_type == RecoveryType::kUnrecoverable) { |
93 | 0 | return Status::InvalidArgument( |
94 | 0 | "WriteBatch contains unrecoverable timestamp size inconsistency."); |
95 | 0 | } |
96 | | |
97 | | // If any column family needs reconciliation, it will mark the whole |
98 | | // WriteBatch to need recovery and rebuilt. |
99 | 0 | *ts_need_recovery = true; |
100 | 0 | } |
101 | 0 | } |
102 | 0 | return Status::OK(); |
103 | 0 | } |
104 | | |
105 | | enum class ToggleUDT { |
106 | | kUnchanged, |
107 | | kEnableUDT, |
108 | | kDisableUDT, |
109 | | kInvalidChange, |
110 | | }; |
111 | | |
112 | | ToggleUDT CompareComparator(const Comparator* new_comparator, |
113 | 63.7k | const std::string& old_comparator_name) { |
114 | 63.7k | static const char* kUDTSuffix = ".u64ts"; |
115 | 63.7k | static const Slice kSuffixSlice = kUDTSuffix; |
116 | 63.7k | static const size_t kSuffixSize = 6; |
117 | 63.7k | size_t ts_sz = new_comparator->timestamp_size(); |
118 | 63.7k | (void)ts_sz; |
119 | 63.7k | Slice new_ucmp_name(new_comparator->Name()); |
120 | 63.7k | Slice old_ucmp_name(old_comparator_name); |
121 | 63.7k | if (new_ucmp_name.compare(old_ucmp_name) == 0) { |
122 | 63.7k | return ToggleUDT::kUnchanged; |
123 | 63.7k | } |
124 | 0 | if (new_ucmp_name.size() == old_ucmp_name.size() + kSuffixSize && |
125 | 0 | new_ucmp_name.starts_with(old_ucmp_name) && |
126 | 0 | new_ucmp_name.ends_with(kSuffixSlice)) { |
127 | 0 | assert(ts_sz == 8); |
128 | 0 | return ToggleUDT::kEnableUDT; |
129 | 0 | } |
130 | 0 | if (old_ucmp_name.size() == new_ucmp_name.size() + kSuffixSize && |
131 | 0 | old_ucmp_name.starts_with(new_ucmp_name) && |
132 | 0 | old_ucmp_name.ends_with(kSuffixSlice)) { |
133 | 0 | assert(ts_sz == 0); |
134 | 0 | return ToggleUDT::kDisableUDT; |
135 | 0 | } |
136 | 0 | return ToggleUDT::kInvalidChange; |
137 | 0 | } |
138 | | } // namespace |
139 | | |
140 | | TimestampRecoveryHandler::TimestampRecoveryHandler( |
141 | | const UnorderedMap<uint32_t, size_t>& running_ts_sz, |
142 | | const UnorderedMap<uint32_t, size_t>& record_ts_sz, bool seq_per_batch, |
143 | | bool batch_per_txn) |
144 | 0 | : running_ts_sz_(running_ts_sz), |
145 | 0 | record_ts_sz_(record_ts_sz), |
146 | | // Write after commit currently uses one seq per key (instead of per |
147 | | // batch). So seq_per_batch being false indicates write_after_commit |
148 | | // approach. |
149 | 0 | write_after_commit_(!seq_per_batch), |
150 | | // WriteUnprepared can write multiple WriteBatches per transaction, so |
151 | | // batch_per_txn being false indicates write_before_prepare. |
152 | 0 | write_before_prepare_(!batch_per_txn), |
153 | 0 | new_batch_(new WriteBatch()), |
154 | 0 | handler_valid_(true), |
155 | 0 | new_batch_diff_from_orig_batch_(false) {} |
156 | | |
157 | | Status TimestampRecoveryHandler::PutCF(uint32_t cf, const Slice& key, |
158 | 0 | const Slice& value) { |
159 | 0 | std::string new_key_buf; |
160 | 0 | Slice new_key; |
161 | 0 | Status status = |
162 | 0 | ReconcileTimestampDiscrepancy(cf, key, &new_key_buf, &new_key); |
163 | 0 | if (!status.ok()) { |
164 | 0 | return status; |
165 | 0 | } |
166 | 0 | return WriteBatchInternal::Put(new_batch_.get(), cf, new_key, value); |
167 | 0 | } |
168 | | |
169 | | Status TimestampRecoveryHandler::PutEntityCF(uint32_t cf, const Slice& key, |
170 | 0 | const Slice& entity) { |
171 | 0 | std::string new_key_buf; |
172 | 0 | Slice new_key; |
173 | 0 | Status status = TimestampRecoveryHandler::ReconcileTimestampDiscrepancy( |
174 | 0 | cf, key, &new_key_buf, &new_key); |
175 | 0 | if (!status.ok()) { |
176 | 0 | return status; |
177 | 0 | } |
178 | 0 | Slice entity_copy = entity; |
179 | 0 | WideColumns columns; |
180 | 0 | if (!WideColumnSerialization::DeserializeSimple(entity_copy, columns).ok()) { |
181 | 0 | return Status::Corruption("Unable to deserialize entity", |
182 | 0 | entity.ToString(/* hex */ true)); |
183 | 0 | } |
184 | | |
185 | 0 | return WriteBatchInternal::PutEntity(new_batch_.get(), cf, new_key, columns); |
186 | 0 | } |
187 | | |
188 | | Status TimestampRecoveryHandler::TimedPutCF(uint32_t cf, const Slice& key, |
189 | | const Slice& value, |
190 | 0 | uint64_t write_time) { |
191 | 0 | std::string new_key_buf; |
192 | 0 | Slice new_key; |
193 | 0 | Status status = |
194 | 0 | ReconcileTimestampDiscrepancy(cf, key, &new_key_buf, &new_key); |
195 | 0 | if (!status.ok()) { |
196 | 0 | return status; |
197 | 0 | } |
198 | 0 | return WriteBatchInternal::TimedPut(new_batch_.get(), cf, new_key, value, |
199 | 0 | write_time); |
200 | 0 | } |
201 | | |
202 | 0 | Status TimestampRecoveryHandler::DeleteCF(uint32_t cf, const Slice& key) { |
203 | 0 | std::string new_key_buf; |
204 | 0 | Slice new_key; |
205 | 0 | Status status = |
206 | 0 | ReconcileTimestampDiscrepancy(cf, key, &new_key_buf, &new_key); |
207 | 0 | if (!status.ok()) { |
208 | 0 | return status; |
209 | 0 | } |
210 | 0 | return WriteBatchInternal::Delete(new_batch_.get(), cf, new_key); |
211 | 0 | } |
212 | | |
213 | 0 | Status TimestampRecoveryHandler::SingleDeleteCF(uint32_t cf, const Slice& key) { |
214 | 0 | std::string new_key_buf; |
215 | 0 | Slice new_key; |
216 | 0 | Status status = |
217 | 0 | ReconcileTimestampDiscrepancy(cf, key, &new_key_buf, &new_key); |
218 | 0 | if (!status.ok()) { |
219 | 0 | return status; |
220 | 0 | } |
221 | 0 | return WriteBatchInternal::SingleDelete(new_batch_.get(), cf, new_key); |
222 | 0 | } |
223 | | |
224 | | Status TimestampRecoveryHandler::DeleteRangeCF(uint32_t cf, |
225 | | const Slice& begin_key, |
226 | 0 | const Slice& end_key) { |
227 | 0 | std::string new_begin_key_buf; |
228 | 0 | Slice new_begin_key; |
229 | 0 | std::string new_end_key_buf; |
230 | 0 | Slice new_end_key; |
231 | 0 | Status status = ReconcileTimestampDiscrepancy( |
232 | 0 | cf, begin_key, &new_begin_key_buf, &new_begin_key); |
233 | 0 | if (!status.ok()) { |
234 | 0 | return status; |
235 | 0 | } |
236 | 0 | status = ReconcileTimestampDiscrepancy(cf, end_key, &new_end_key_buf, |
237 | 0 | &new_end_key); |
238 | 0 | if (!status.ok()) { |
239 | 0 | return status; |
240 | 0 | } |
241 | 0 | return WriteBatchInternal::DeleteRange(new_batch_.get(), cf, new_begin_key, |
242 | 0 | new_end_key); |
243 | 0 | } |
244 | | |
245 | | Status TimestampRecoveryHandler::MergeCF(uint32_t cf, const Slice& key, |
246 | 0 | const Slice& value) { |
247 | 0 | std::string new_key_buf; |
248 | 0 | Slice new_key; |
249 | 0 | Status status = |
250 | 0 | ReconcileTimestampDiscrepancy(cf, key, &new_key_buf, &new_key); |
251 | 0 | if (!status.ok()) { |
252 | 0 | return status; |
253 | 0 | } |
254 | 0 | return WriteBatchInternal::Merge(new_batch_.get(), cf, new_key, value); |
255 | 0 | } |
256 | | |
257 | | Status TimestampRecoveryHandler::PutBlobIndexCF(uint32_t cf, const Slice& key, |
258 | 0 | const Slice& value) { |
259 | 0 | std::string new_key_buf; |
260 | 0 | Slice new_key; |
261 | 0 | Status status = |
262 | 0 | ReconcileTimestampDiscrepancy(cf, key, &new_key_buf, &new_key); |
263 | 0 | if (!status.ok()) { |
264 | 0 | return status; |
265 | 0 | } |
266 | 0 | return WriteBatchInternal::PutBlobIndex(new_batch_.get(), cf, new_key, value); |
267 | 0 | } |
268 | | |
269 | 0 | Status TimestampRecoveryHandler::MarkBeginPrepare(bool unprepare) { |
270 | | // Transaction policy change requires empty WAL and User-defined timestamp is |
271 | | // only supported for write committed txns. |
272 | | // WriteBatch::Iterate has will handle this based on |
273 | | // handler->WriteAfterCommit() and handler->WriteBeforePrepare(). |
274 | 0 | if (unprepare) { |
275 | 0 | return Status::InvalidArgument( |
276 | 0 | "Handle user defined timestamp setting change is not supported for" |
277 | 0 | "write unprepared policy. The WAL must be emptied."); |
278 | 0 | } |
279 | 0 | return WriteBatchInternal::InsertBeginPrepare(new_batch_.get(), |
280 | 0 | write_after_commit_, |
281 | 0 | /* unprepared_batch */ false); |
282 | 0 | } |
283 | | |
284 | 0 | Status TimestampRecoveryHandler::MarkEndPrepare(const Slice& name) { |
285 | 0 | return WriteBatchInternal::InsertEndPrepare(new_batch_.get(), name); |
286 | 0 | } |
287 | | |
288 | 0 | Status TimestampRecoveryHandler::MarkCommit(const Slice& name) { |
289 | 0 | return WriteBatchInternal::MarkCommit(new_batch_.get(), name); |
290 | 0 | } |
291 | | |
292 | | Status TimestampRecoveryHandler::MarkCommitWithTimestamp( |
293 | 0 | const Slice& name, const Slice& commit_ts) { |
294 | 0 | return WriteBatchInternal::MarkCommitWithTimestamp(new_batch_.get(), name, |
295 | 0 | commit_ts); |
296 | 0 | } |
297 | | |
298 | 0 | Status TimestampRecoveryHandler::MarkRollback(const Slice& name) { |
299 | 0 | return WriteBatchInternal::MarkRollback(new_batch_.get(), name); |
300 | 0 | } |
301 | | |
302 | 0 | Status TimestampRecoveryHandler::MarkNoop(bool /*empty_batch*/) { |
303 | 0 | return WriteBatchInternal::InsertNoop(new_batch_.get()); |
304 | 0 | } |
305 | | |
306 | | Status TimestampRecoveryHandler::ReconcileTimestampDiscrepancy( |
307 | 0 | uint32_t cf, const Slice& key, std::string* new_key_buf, Slice* new_key) { |
308 | 0 | assert(handler_valid_); |
309 | 0 | auto running_iter = running_ts_sz_.find(cf); |
310 | 0 | if (running_iter == running_ts_sz_.end()) { |
311 | | // The column family referred to by the WriteBatch is no longer running. |
312 | | // Copy over the entry as is to the new WriteBatch. |
313 | 0 | *new_key = key; |
314 | 0 | return Status::OK(); |
315 | 0 | } |
316 | 0 | size_t running_ts_sz = running_iter->second; |
317 | 0 | auto record_iter = record_ts_sz_.find(cf); |
318 | 0 | std::optional<size_t> record_ts_sz = |
319 | 0 | record_iter != record_ts_sz_.end() |
320 | 0 | ? std::optional<size_t>(record_iter->second) |
321 | 0 | : std::nullopt; |
322 | 0 | RecoveryType recovery_type = GetRecoveryType(running_ts_sz, record_ts_sz); |
323 | |
|
324 | 0 | switch (recovery_type) { |
325 | 0 | case RecoveryType::kNoop: |
326 | 0 | *new_key = key; |
327 | 0 | break; |
328 | 0 | case RecoveryType::kStripTimestamp: |
329 | 0 | assert(record_ts_sz.has_value()); |
330 | 0 | *new_key = StripTimestampFromUserKey(key, *record_ts_sz); |
331 | 0 | new_batch_diff_from_orig_batch_ = true; |
332 | 0 | break; |
333 | 0 | case RecoveryType::kPadTimestamp: |
334 | 0 | AppendKeyWithMinTimestamp(new_key_buf, key, running_ts_sz); |
335 | 0 | *new_key = *new_key_buf; |
336 | 0 | new_batch_diff_from_orig_batch_ = true; |
337 | 0 | break; |
338 | 0 | case RecoveryType::kUnrecoverable: |
339 | 0 | return Status::InvalidArgument( |
340 | 0 | "Unrecoverable timestamp size inconsistency encountered by " |
341 | 0 | "TimestampRecoveryHandler."); |
342 | 0 | default: |
343 | 0 | assert(false); |
344 | 0 | } |
345 | 0 | return Status::OK(); |
346 | 0 | } |
347 | | |
348 | | Status HandleWriteBatchTimestampSizeDifference( |
349 | | const WriteBatch* batch, |
350 | | const UnorderedMap<uint32_t, size_t>& running_ts_sz, |
351 | | const UnorderedMap<uint32_t, size_t>& record_ts_sz, |
352 | | TimestampSizeConsistencyMode check_mode, bool seq_per_batch, |
353 | 809k | bool batch_per_txn, std::unique_ptr<WriteBatch>* new_batch) { |
354 | | // Quick path to bypass checking the WriteBatch. |
355 | 809k | if (AllRunningColumnFamiliesConsistent(running_ts_sz, record_ts_sz)) { |
356 | 809k | return Status::OK(); |
357 | 809k | } |
358 | 0 | bool need_recovery = false; |
359 | 0 | Status status = CheckWriteBatchTimestampSizeConsistency( |
360 | 0 | batch, running_ts_sz, record_ts_sz, check_mode, &need_recovery); |
361 | 0 | if (!status.ok()) { |
362 | 0 | return status; |
363 | 0 | } else if (need_recovery) { |
364 | 0 | assert(new_batch); |
365 | 0 | SequenceNumber sequence = WriteBatchInternal::Sequence(batch); |
366 | 0 | TimestampRecoveryHandler recovery_handler(running_ts_sz, record_ts_sz, |
367 | 0 | seq_per_batch, batch_per_txn); |
368 | 0 | status = batch->Iterate(&recovery_handler); |
369 | 0 | if (!status.ok()) { |
370 | 0 | return status; |
371 | 0 | } else { |
372 | 0 | *new_batch = recovery_handler.TransferNewBatch(); |
373 | 0 | WriteBatchInternal::SetSequence(new_batch->get(), sequence); |
374 | 0 | } |
375 | 0 | } |
376 | 0 | return Status::OK(); |
377 | 0 | } |
378 | | |
379 | | Status ValidateUserDefinedTimestampsOptions( |
380 | | const Comparator* new_comparator, const std::string& old_comparator_name, |
381 | | bool new_persist_udt, bool old_persist_udt, |
382 | 63.7k | bool* mark_sst_files_has_no_udt) { |
383 | 63.7k | size_t ts_sz = new_comparator->timestamp_size(); |
384 | 63.7k | ToggleUDT res = CompareComparator(new_comparator, old_comparator_name); |
385 | 63.7k | switch (res) { |
386 | 63.7k | case ToggleUDT::kUnchanged: |
387 | 63.7k | if (old_persist_udt == new_persist_udt) { |
388 | 63.7k | return Status::OK(); |
389 | 63.7k | } |
390 | 0 | if (ts_sz == 0) { |
391 | 0 | return Status::OK(); |
392 | 0 | } |
393 | 0 | return Status::InvalidArgument( |
394 | 0 | "Cannot toggle the persist_user_defined_timestamps flag for a column " |
395 | 0 | "family with user-defined timestamps feature enabled."); |
396 | 0 | case ToggleUDT::kEnableUDT: |
397 | 0 | if (!new_persist_udt) { |
398 | 0 | *mark_sst_files_has_no_udt = true; |
399 | 0 | return Status::OK(); |
400 | 0 | } |
401 | 0 | return Status::InvalidArgument( |
402 | 0 | "Cannot open a column family and enable user-defined timestamps " |
403 | 0 | "feature without setting persist_user_defined_timestamps flag to " |
404 | 0 | "false."); |
405 | 0 | case ToggleUDT::kDisableUDT: |
406 | 0 | if (!old_persist_udt) { |
407 | 0 | return Status::OK(); |
408 | 0 | } |
409 | 0 | return Status::InvalidArgument( |
410 | 0 | "Cannot open a column family and disable user-defined timestamps " |
411 | 0 | "feature if its existing persist_user_defined_timestamps flag is not " |
412 | 0 | "false."); |
413 | 0 | case ToggleUDT::kInvalidChange: |
414 | 0 | return Status::InvalidArgument( |
415 | 0 | new_comparator->Name(), |
416 | 0 | "does not match existing comparator " + old_comparator_name); |
417 | 0 | default: |
418 | 0 | break; |
419 | 63.7k | } |
420 | 0 | return Status::InvalidArgument( |
421 | 0 | "Unsupported user defined timestamps settings change."); |
422 | 63.7k | } |
423 | | |
424 | | void GetFullHistoryTsLowFromU64CutoffTs(Slice* cutoff_ts, |
425 | 0 | std::string* full_history_ts_low) { |
426 | 0 | uint64_t cutoff_udt_ts = 0; |
427 | 0 | [[maybe_unused]] bool format_res = GetFixed64(cutoff_ts, &cutoff_udt_ts); |
428 | 0 | assert(format_res); |
429 | 0 | PutFixed64(full_history_ts_low, cutoff_udt_ts + 1); |
430 | 0 | } |
431 | | |
432 | | void GetU64CutoffTsFromFullHistoryTsLow(Slice* full_history_ts_low, |
433 | 0 | std::string* cutoff_ts) { |
434 | 0 | uint64_t full_history_ts_low_int = 0; |
435 | 0 | [[maybe_unused]] bool format_res = |
436 | 0 | GetFixed64(full_history_ts_low, &full_history_ts_low_int); |
437 | 0 | assert(format_res); |
438 | 0 | assert(full_history_ts_low_int > 0); |
439 | 0 | if (full_history_ts_low_int > 0) { |
440 | 0 | PutFixed64(cutoff_ts, full_history_ts_low_int - 1); |
441 | 0 | } else { |
442 | 0 | PutFixed64(cutoff_ts, 0); |
443 | 0 | } |
444 | 0 | } |
445 | | |
446 | | std::tuple<OptSlice, OptSlice> MaybeAddTimestampsToRange( |
447 | | const OptSlice& start, const OptSlice& end, size_t ts_sz, |
448 | 0 | std::string* start_with_ts, std::string* end_with_ts, bool exclusive_end) { |
449 | 0 | OptSlice ret_start; |
450 | 0 | OptSlice ret_end; |
451 | 0 | if (start) { |
452 | 0 | if (ts_sz == 0) { |
453 | 0 | ret_start = *start; |
454 | 0 | } else { |
455 | | // Maximum timestamp means including all keys with any timestamp for start |
456 | 0 | AppendKeyWithMaxTimestamp(start_with_ts, *start, ts_sz); |
457 | 0 | ret_start = Slice(*start_with_ts); |
458 | 0 | } |
459 | 0 | } |
460 | 0 | if (end) { |
461 | 0 | if (ts_sz == 0) { |
462 | 0 | ret_end = *end; |
463 | 0 | } else { |
464 | 0 | if (exclusive_end) { |
465 | | // Append a maximum timestamp as the range limit is exclusive: |
466 | | // [start, end) |
467 | 0 | AppendKeyWithMaxTimestamp(end_with_ts, *end, ts_sz); |
468 | 0 | } else { |
469 | | // Append a minimum timestamp to end so the range limit is inclusive: |
470 | | // [start, end] |
471 | 0 | AppendKeyWithMinTimestamp(end_with_ts, *end, ts_sz); |
472 | 0 | } |
473 | 0 | ret_end = Slice(*end_with_ts); |
474 | 0 | } |
475 | 0 | } |
476 | 0 | return std::make_tuple(ret_start, ret_end); |
477 | 0 | } |
478 | | } // namespace ROCKSDB_NAMESPACE |