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
use linkrs_core::types::Timestamp;
use linkrs_core::Value;
use crate::edge::RecordFormPreference;
/// Thresholds deciding when a bundled-eligible table is narrow and
/// read-heavy enough for the inline form.
///
/// Single source of truth for the width and access gates: shape admission
/// still vetoes first, then the observed profile must pass all three gates.
/// Recommended ranges are documented on each field; values never adapt to
/// load at runtime.
#[derive(Debug, Clone, Copy)]
pub struct RecordFormProfileThresholds {
/// Maximum average property-byte width for the inline form.
/// Recommended range 8..=16. The bundled slot holds one 8-byte scalar
/// word plus a validity bit, so wider averages stay columnar.
pub max_inline_avg_bytes: u64,
/// Maximum write share `writes / (reads + writes)` for the inline form.
/// Recommended range 0.2..=0.4. Above this the table is write-hot and
/// stays columnar.
pub max_write_share: f64,
/// Minimum read-to-write ratio for the inline form. Recommended range
/// 2.0..=4.0. Only read-heavy tables migrate inline.
pub min_read_to_write_ratio: f64,
}
impl Default for RecordFormProfileThresholds {
fn default() -> Self {
Self {
max_inline_avg_bytes: 8,
max_write_share: 0.3,
min_read_to_write_ratio: 3.0,
}
}
}
#[derive(Debug, Clone)]
pub struct EdgeTableConfig {
pub initial_vertex_capacity: usize,
/// Fixed number of edges allocated per high-degree overflow chunk.
pub overflow_chunk_edges: usize,
/// Address bits per topology node group: one group covers
/// `1 << node_group_bits` bound-endpoint rows. Out groups partition by
/// source, in groups by destination.
///
/// Locked at table creation: there is no online width-change branch.
/// The only adjustment outlet is the offline `EdgeStore::reshard` tool,
/// which rebuilds the table group by group and switches the manifest on
/// the next checkpoint. Endpoints are expected dense; sparse large
/// endpoints must be densified offline via vertex remapping first,
/// otherwise only existing groups consume memory and files while the
/// span stays wide.
pub node_group_bits: u32,
/// Write backpressure: max size of the node-group CSR (in bytes)
/// before background compaction is requested. Set to 0 to disable.
pub max_mutable_csr_bytes: usize,
/// Bound for one group append sidecar: disk plus memory op count at or
/// above this limit forces a base rewrite on the next checkpoint. Set to
/// 0 to disable, leaving sidecar growth unbounded.
pub max_append_ops_per_group: usize,
/// Region density at or above which a dirty region merges as a whole.
/// Below it only rows holding reclaimable entries are visited. Tune
/// against checkpoint flushed bytes per live edge; raising it narrows
/// merges and never changes the live set.
pub region_merge_min_density: f32,
/// Group density at or above which a multi-region dirty span merges at
/// group scope. Below it merges stay region-scoped. Same tuning source
/// and safety as the region threshold.
pub group_merge_min_density: f32,
/// Automatic maintenance: property compaction on the
/// write path when the configured thresholds are exceeded.
pub auto_maintenance: AutoMaintenanceConfig,
/// User-facing preference for record form selection at table creation.
/// `Auto` derives the safe default (pure for empty schemas, otherwise
/// columnar); `Columnar` forces the standard multi/single/none strategy
/// path; `Bundled` explicitly opts into the single-scalar inline form.
pub record_form: RecordFormPreference,
/// Opt-in background record-form migration. When enabled, background
/// maintenance switches a table to its recommended form once (narrow
/// single-scalar columnar tables move inline); the switch fences
/// pre-switch WAL redo and requires the next checkpoint, so it stays off
/// by default and manual migration remains the primary path.
pub auto_migrate_record_form: bool,
/// Adapt property column encodings during checkpoints from dirty-column
/// values. Enabled by default; disable only to pin encodings manually.
pub auto_encode_on_checkpoint: bool,
/// Minimum rows for checkpoint-time encoding inference. Smaller columns
/// skip inference to avoid fitting elaborate encodings to noise.
pub auto_encode_min_rows: usize,
/// Declared memory intent for read-heavy paths. `HeapDefault` keeps the
/// historical behavior; `ReadSnapshot` marks the read-only snapshot path
/// and `BulkLoad` the batch-ingest path so mapping hints follow intent.
pub memory_intent: MemoryIntent,
/// Width and access gates for automatic bundled migration. Shape
/// admission still vetoes first; the observed profile must then pass all
/// gates before the background path recommends the inline form.
pub record_form_profile: RecordFormProfileThresholds,
}
/// Declared memory intent for read-heavy paths.
///
/// Explicit configuration rather than a best-effort hint: the read-only
/// snapshot open and the bulk-load path declare which behavior they want, so
/// huge-page and prefetch choices stay reviewable instead of implicit.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum MemoryIntent {
/// Default heap behavior: no huge-page hint, mappings stay on base pages.
#[default]
HeapDefault,
/// Read-only snapshot scans: explicitly request transparent huge pages on
/// Linux (still best-effort, rejection falls back without failing).
ReadSnapshot,
/// Batch ingest: skip the huge-page hint to avoid THP pressure during
/// sequential bulk writes; mappings stay on base pages.
BulkLoad,
}
/// Thresholds that trigger automatic maintenance on the write path.
#[derive(Debug, Clone, Copy)]
pub struct AutoMaintenanceConfig {
/// Run property compaction when deleted-but-not-reclaimed property rows
/// exceed this ratio of total rows. Recommended range 0.1..=0.25.
/// Set to 0.0 to disable.
pub property_compact_ratio: f32,
/// Minimum tracked tombstones arming the write-path reclaim pass when the
/// watermark did not advance. Recommended range 2..=8. Below this count
/// with an unchanged watermark the commit skips the group scan entirely.
pub reclaim_tombstone_threshold: usize,
/// Rebuild the secondary property index when lag since the last rebuild
/// reaches this failure count. Set to 0 to disable the count trigger.
pub index_rebuild_failure_threshold: u64,
/// Rebuild the secondary property index when any lag persists longer
/// than this many seconds. Set to 0 to disable the age trigger.
pub index_max_stale_secs: u64,
}
impl Default for AutoMaintenanceConfig {
fn default() -> Self {
Self {
property_compact_ratio: 0.15,
reclaim_tombstone_threshold: 4,
index_rebuild_failure_threshold: 64,
index_max_stale_secs: 300,
}
}
}
impl Default for EdgeTableConfig {
fn default() -> Self {
Self {
initial_vertex_capacity: 4096,
overflow_chunk_edges: 4096,
node_group_bits: crate::edge::node_group::DEFAULT_NODE_GROUP_BITS,
max_mutable_csr_bytes: 100 * 1024 * 1024,
max_append_ops_per_group: 4096,
region_merge_min_density: crate::edge::node_group::REGION_MERGE_MIN_DENSITY,
group_merge_min_density: crate::edge::node_group::GROUP_MERGE_MIN_DENSITY,
auto_maintenance: AutoMaintenanceConfig::default(),
record_form: RecordFormPreference::default(),
auto_migrate_record_form: false,
auto_encode_on_checkpoint: true,
auto_encode_min_rows: 128,
memory_intent: MemoryIntent::default(),
record_form_profile: RecordFormProfileThresholds::default(),
}
}
}
/// Parameters for keyed edge property update
pub struct UpdateEdgePropertyByKeyParams {
pub src: u32,
pub dst: u32,
pub rank: i64,
pub prop_id: u16,
pub value: Value,
pub ts: Timestamp,
}