1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
//! Liveness checks, projected reads, and the offline snapshot iterator.
use std::sync::atomic::Ordering;
use std::sync::Arc;
use super::VertexTable;
use crate::vertex::{IdKey, Timestamp, VertexId, VertexRecord};
use linkrs_core::{StorageError, StorageResult, Value};
impl VertexTable {
pub fn get_by_internal_id_offline(
&self,
internal_id: u32,
ts: Timestamp,
) -> Option<VertexRecord> {
self.get_projected_by_internal_id(internal_id, ts, None)
}
/// Single row-liveness entry for plain timestamp reads.
///
/// Row life and death resolve here only: every content-serving read
/// checks liveness exactly once through this entry and column version
/// chains never recheck it. Pending-aware paths use the guard form
/// instead; the two share the unified interval semantics. Online reads
/// go through the transaction guard while this bare timestamp probe
/// remains for offline barrier tools only.
pub fn is_row_live_at(&self, internal_id: u32, ts: Timestamp) -> bool {
self.timestamps.read().is_valid(internal_id, ts)
}
/// Row survival stamps for pending-aware rechecks.
///
/// Returns `(create_ts, delete_ts)` with `None` for a live row. `None`
/// (unknown row) lets the caller fall back to the plain predicate result.
pub fn row_timestamps(&self, internal_id: u32) -> Option<(Timestamp, Option<Timestamp>)> {
let stamps = self.timestamps.read();
let create_ts = stamps.get_start_ts(internal_id)?;
Some((create_ts, stamps.get_end_ts(internal_id)))
}
/// Per-column covering version stamps for cache fences.
///
/// Companion of the column values read by
/// [`VertexTable::get_projected_by_internal_id`]: the caller seeds the
/// record cache with these stamps and revalidates a hit by comparing
/// them against live storage.
pub fn row_picked_starts(&self, internal_id: u32, ts: Timestamp) -> Vec<Timestamp> {
self.columns.picked_starts_at(internal_id as usize, ts)
}
/// Snapshot-visible live IDs at `ts` in allocation order.
///
/// Enumeration and point reads share the single liveness entry
/// ([`Self::is_row_live_at`]): rows invisible at `ts` (including
/// timestamp-deleted rows awaiting watermark-gated GC) are excluded.
/// There is no unfiltered variant; sizing callers use `total_count` or
/// `id_hole_stats` instead. Used by lazy paginated scans. Online
/// enumeration goes through the guarded sharded cursor while this bare
/// timestamp form remains for offline barrier tools only.
pub fn live_ids(&self, ts: Timestamp) -> Vec<u32> {
self.id_indexer
.live_ids()
.into_iter()
.filter(|&id| self.is_row_live_at(id, ts))
.collect()
}
/// Checkpoint-epoch floor below which attribute history is not retained.
#[allow(dead_code)]
pub fn history_floor(&self) -> Timestamp {
self.history_floor
}
fn check_history_floor(&self, internal_id: u32, ts: Timestamp) -> StorageResult<()> {
let floor = self.history_floor;
if ts < floor {
let created = self
.timestamps
.read()
.get_start_ts(internal_id)
.unwrap_or(0);
if created <= ts {
return Err(StorageError::history_before_floor(internal_id, ts, floor));
}
}
Ok(())
}
/// Strict fenced batch read with explicit decode errors.
///
/// Corrupt payloads fail instead of reading as missing. Attribute time travel ends at
/// the last load: version chains do not survive checkpoints, so a
/// query below the history floor for a row created at or below
/// the query timestamp may need dropped before-images and fails
/// instead of returning the current value. Rows created after the
/// query timestamp still read as missing.
pub fn try_get_projected_batch(
&self,
internal_ids: &[u32],
ts: Timestamp,
projection: Option<&[Arc<str>]>,
) -> StorageResult<Vec<Option<VertexRecord>>> {
if !self.is_open.load(Ordering::Acquire) {
return Ok(internal_ids.iter().map(|_| None).collect());
}
let mut positions: Vec<(usize, u32)> = Vec::with_capacity(internal_ids.len());
for (pos, &id) in internal_ids.iter().enumerate() {
if self.is_row_live_at(id, ts) {
positions.push((pos, id));
}
}
for &(_, id) in &positions {
self.check_history_floor(id, ts)?;
}
let mut out: Vec<Option<VertexRecord>> = internal_ids.iter().map(|_| None).collect();
if positions.is_empty() {
return Ok(out);
}
let row_indices: Vec<usize> = positions.iter().map(|&(_, id)| id as usize).collect();
let props = match projection {
Some(names) => self
.columns
.try_get_projected_batch_at_ts(&row_indices, names, ts)?,
None => self.columns.try_get_batch_at_ts(&row_indices, ts)?,
};
for ((pos, id), prop_row) in positions.into_iter().zip(props) {
let key = match self.id_indexer.get_key(id) {
Some(key) => key,
None => continue,
};
let vid = match key {
IdKey::Int(i) => match VertexId::try_from_int64(i).ok() {
Some(vid) => vid,
None => continue,
},
IdKey::Text(s) => match VertexId::try_from_string(&s).ok() {
Some(vid) => vid,
None => continue,
},
};
let properties: Vec<(Arc<str>, Value)> = prop_row
.into_iter()
.filter_map(|(name, opt_val)| opt_val.map(|v| (name, v)))
.collect();
out[pos] = Some(VertexRecord {
vid,
internal_id: id,
properties,
});
}
Ok(out)
}
/// Column-major batch decode (A1). Decodes the requested columns for
/// `internal_ids` column-at-a-time into typed [`ColumnValues`] arrays.
/// The ids must already be valid at `ts`; validity is not re-checked here.
pub fn get_projected_columns_offline(
&self,
internal_ids: &[u32],
ts: Timestamp,
names: &[Arc<str>],
) -> Vec<(Arc<str>, crate::cursor::ColumnValues)> {
if !self.is_open.load(Ordering::Acquire) {
return names
.iter()
.map(|n| {
(
n.clone(),
crate::cursor::ColumnValues::General(vec![None; internal_ids.len()]),
)
})
.collect();
}
let row_indices: Vec<usize> = internal_ids.iter().map(|&id| id as usize).collect();
self.columns
.get_projected_columns_at_ts(&row_indices, names, ts)
}
pub fn get_projected_by_internal_id(
&self,
internal_id: u32,
ts: Timestamp,
projection: Option<&[Arc<str>]>,
) -> Option<VertexRecord> {
if !self.is_open.load(Ordering::Acquire) {
return None;
}
// Single liveness gate: column decodes below assume a live row and
// never recheck the timestamp interval.
if !self.is_row_live_at(internal_id, ts) {
return None;
}
debug_assert!(self.is_row_live_at(internal_id, ts));
let external_id = self.id_indexer.get_key(internal_id)?;
let props = projection.map_or_else(
|| self.columns.get_at_ts(internal_id as usize, ts),
|names| {
self.columns
.get_projected_at_ts(internal_id as usize, names, ts)
},
);
let properties: Vec<(Arc<str>, Value)> = props
.into_iter()
.filter_map(|(name, opt_val)| opt_val.map(|v| (name, v)))
.collect();
let vid = match external_id {
IdKey::Int(i) => VertexId::try_from_int64(i).ok()?,
IdKey::Text(s) => VertexId::try_from_string(&s).ok()?,
};
Some(VertexRecord {
vid,
internal_id,
properties,
})
}
/// Strict single-row projection with explicit history and decode errors.
///
/// Non-strict reads return missing below the history floor; this entry
/// fails instead so callers can distinguish dropped history from a row
/// created after the query timestamp. Row liveness stays with the caller
/// through the single gate; never-written windows yield `Ok(None)` and
/// corrupt payloads yield `Err` with the column and row.
#[allow(dead_code)]
pub fn try_get_projected_by_internal_id(
&self,
internal_id: u32,
ts: Timestamp,
projection: Option<&[Arc<str>]>,
) -> StorageResult<Option<VertexRecord>> {
if !self.is_open.load(Ordering::Acquire) {
return Ok(None);
}
if !self.is_row_live_at(internal_id, ts) {
return Ok(None);
}
self.check_history_floor(internal_id, ts)?;
let names: Vec<Arc<str>> = match projection {
Some(names) => names.to_vec(),
None => self
.schema
.properties
.iter()
.map(|prop| prop.name.clone())
.collect(),
};
let row_idx = internal_id as usize;
let props = self
.columns
.try_get_projected_batch_at_ts(&[row_idx], &names, ts)
.map_err(|e| {
StorageError::deserialize_error(format!(
"vertex row {} decode failed: {}",
internal_id, e
))
})?;
let Some(prop_row) = props.into_iter().next() else {
return Ok(None);
};
let key = match self.id_indexer.get_key(internal_id) {
Some(key) => key,
None => return Ok(None),
};
let vid = match key {
IdKey::Int(i) => match VertexId::try_from_int64(i).ok() {
Some(vid) => vid,
None => return Ok(None),
},
IdKey::Text(s) => match VertexId::try_from_string(&s).ok() {
Some(vid) => vid,
None => return Ok(None),
},
};
let properties: Vec<(Arc<str>, Value)> = prop_row
.into_iter()
.filter_map(|(name, opt_val)| opt_val.map(|v| (name, v)))
.collect();
Ok(Some(VertexRecord {
vid,
internal_id,
properties,
}))
}
/// Fenced point read merging the projected decode with the per-column
/// covering stamps in one pass per column. Single liveness gate like
/// [`Self::get_projected_by_internal_id`]; the stamps travel with the
/// record as its cache fence.
pub fn get_projected_with_stamps(
&self,
internal_id: u32,
ts: Timestamp,
projection: Option<&[Arc<str>]>,
) -> Option<(VertexRecord, Vec<Timestamp>)> {
if !self.is_open.load(Ordering::Acquire) {
return None;
}
if !self.is_row_live_at(internal_id, ts) {
return None;
}
let external_id = self.id_indexer.get_key(internal_id)?;
let names: Vec<Arc<str>> = match projection {
Some(names) => names.to_vec(),
None => self
.schema
.properties
.iter()
.map(|prop| prop.name.clone())
.collect(),
};
let (props, stamps) =
self.columns
.get_projected_with_stamps_at_ts(internal_id as usize, &names, ts);
let properties: Vec<(Arc<str>, Value)> = props
.into_iter()
.filter_map(|(name, opt_val)| opt_val.map(|v| (name, v)))
.collect();
let vid = match external_id {
IdKey::Int(i) => VertexId::try_from_int64(i).ok()?,
IdKey::Text(s) => VertexId::try_from_string(&s).ok()?,
};
Some((
VertexRecord {
vid,
internal_id,
properties,
},
stamps,
))
}
/// Whether `col_name` is the primary-key mirror column.
pub fn is_pk_column(&self, col_name: &str) -> bool {
self.schema
.properties
.get(self.schema.primary_key_index)
.is_some_and(|pk| &*pk.name == col_name)
}
/// Offline snapshot scan at `ts` for barrier-held maintenance tools.
///
/// Online reads enumerate through the guarded sharded cursor instead.
pub fn scan(&self, ts: Timestamp) -> VertexIterator<'_> {
VertexIterator::new(self, ts)
}
}
pub struct VertexIterator<'a> {
table: &'a VertexTable,
ts: Timestamp,
live_ids: std::vec::IntoIter<u32>,
}
impl<'a> VertexIterator<'a> {
pub fn new(table: &'a VertexTable, ts: Timestamp) -> Self {
Self {
table,
ts,
live_ids: table.live_ids(ts).into_iter(),
}
}
}
impl<'a> Iterator for VertexIterator<'a> {
type Item = VertexRecord;
fn next(&mut self) -> Option<Self::Item> {
for id in self.live_ids.by_ref() {
if let Some(record) = self.table.get_by_internal_id_offline(id, self.ts) {
return Some(record);
}
}
None
}
}