1use std::sync::Arc;
10
11use super::{expect, expect_eq};
12use crate::{
13 key_value::{
14 codec::vector_field_prefix,
15 index_keys::{
16 hnsw_metadata_key, hnsw_node_prefix, ivf_assignment_prefix, ivf_centroid_prefix,
17 ivf_metadata_key,
18 },
19 KeyValueHNSWIndex, KeyValueIVFIndex,
20 },
21 HNSWIndexParams, IVFIndexParams, KeyValueStore, MemoryKeyValueStore, StorageBackendResult,
22 VectorIndex,
23};
24
25const FIELD: &str = "embedding\0日本語";
26#[derive(Clone, Copy, Debug)]
27pub enum VectorMergeKind {
28 IVF,
29 HNSW,
30}
31impl VectorMergeKind {
32 pub(super) fn open(
33 self,
34 store: Arc<dyn KeyValueStore>,
35 table: &str,
36 restore: bool,
37 alternate: bool,
38 ) -> StorageBackendResult<Box<dyn VectorIndex>> {
39 Ok(match self {
40 Self::IVF => {
41 let params = IVFIndexParams {
42 nlist: if alternate { 3 } else { 2 },
43 nprobe: 2,
44 train_threshold: 3,
45 };
46 Box::new(if restore {
47 KeyValueIVFIndex::restore(store, table, FIELD, 2, params)?
48 } else {
49 KeyValueIVFIndex::create(store, table, FIELD, 2, params)?
50 })
51 }
52 Self::HNSW => {
53 let params = HNSWIndexParams {
54 m: if alternate { 3 } else { 2 },
55 ef_construction: 8,
56 ef_search: 64,
57 rebuild_threshold: 1,
58 seed: 7,
59 };
60 Box::new(if restore {
61 KeyValueHNSWIndex::restore(store, table, FIELD, 2, params)?
62 } else {
63 KeyValueHNSWIndex::create(store, table, FIELD, 2, params)?
64 })
65 }
66 })
67 }
68}
69fn create(
70 store: Arc<dyn KeyValueStore>,
71 table: &str,
72 seed: u64,
73 kind: VectorMergeKind,
74) -> StorageBackendResult<Box<dyn VectorIndex>> {
75 let mut index = kind.open(store, table, false, false)?;
76 for document in 1..=seed {
77 index.add(document, vec![1.0, document as f32])?;
78 }
79 index.initialize()?;
80 Ok(index)
81}
82fn left_changes(index: &mut dyn VectorIndex) -> StorageBackendResult<()> {
83 index.add_many(11, vec![vec![1.0, 0.0], vec![0.0, 1.0]])?;
84 index.delete(1)?;
85 index.add(1, vec![0.25, 0.75])?;
86 index.add_many(11, vec![])?;
87 index.add_many(11, vec![vec![0.75, 0.25], vec![0.0, 1.0]])?;
88 index.add(13, vec![0.5, 0.5])
89}
90fn right_changes(index: &mut dyn VectorIndex) -> StorageBackendResult<()> {
91 index.add(12, vec![0.4, 0.6])?;
92 index.delete(2)
93}
94fn compare(
95 actual: &dyn KeyValueStore,
96 expected: &dyn KeyValueStore,
97 table: &str,
98 kind: VectorMergeKind,
99) -> StorageBackendResult<()> {
100 let derived = match kind {
101 VectorMergeKind::IVF => vec![
102 ivf_metadata_key(table, FIELD)?,
103 ivf_centroid_prefix(table, FIELD)?,
104 ivf_assignment_prefix(table, FIELD)?,
105 ],
106 VectorMergeKind::HNSW => vec![
107 hnsw_metadata_key(table, FIELD)?,
108 hnsw_node_prefix(table, FIELD)?,
109 ],
110 };
111 for prefix in std::iter::once(vector_field_prefix(table, FIELD)?).chain(derived) {
112 expect_eq(
113 &actual.scan_prefix(&prefix)?,
114 &expected.scan_prefix(&prefix)?,
115 "merged canonical vectors and complete vector generation equal serial execution",
116 )?;
117 }
118 Ok(())
119}
120fn serial(
121 table: &str,
122 seed: u64,
123 kind: VectorMergeKind,
124) -> StorageBackendResult<Arc<dyn KeyValueStore>> {
125 let store: Arc<dyn KeyValueStore> = Arc::new(MemoryKeyValueStore::new());
126 let mut index = create(store.clone(), table, seed, kind)?;
127 right_changes(&mut *index)?;
128 left_changes(&mut *index)?;
129 Ok(store)
130}
131
132pub fn verify_vector_document_merges(
134 a: &Arc<dyn KeyValueStore>,
135 b: &Arc<dyn KeyValueStore>,
136 kind: VectorMergeKind,
137) -> StorageBackendResult<()> {
138 for seed in [0, 2, 8] {
139 let table = format!("{kind:?}-merge\0{seed}");
140 let mut left = create(a.clone(), &table, seed, kind)?;
141 let mut right = kind.open(b.clone(), &table, true, false)?;
142 let baseline = left.snapshot()?;
143 a.begin_transaction()?;
144 b.begin_transaction()?;
145 a.savepoint("vector_branch")?;
146 left.add(99, vec![1.0, 0.0])?;
147 let discarded = left.snapshot()?;
148 a.rollback_to_savepoint("vector_branch")?;
149 a.release_savepoint("vector_branch")?;
150 left_changes(&mut *left)?;
151 let private = left.snapshot()?;
152 right_changes(&mut *right)?;
153 b.commit_transaction()?;
154 expect(
155 a.in_transaction(),
156 "second vector writer commits before the first writer ends",
157 )?;
158 expect_eq(
159 &left.count()?,
160 &private.count()?,
161 "private vector view survives other writer publication",
162 )?;
163 a.commit_transaction()?;
164 compare(
165 a.as_ref(),
166 serial(&table, seed, kind)?.as_ref(),
167 &table,
168 kind,
169 )?;
170 expect_eq(
171 &baseline.count()?,
172 &(seed as usize),
173 "old vector generation remains pinned",
174 )?;
175 expect_eq(
176 &discarded.count()?,
177 &(seed as usize + 1),
178 "discarded savepoint snapshot remains readable",
179 )?;
180 expect_eq(
181 &left.count()?,
182 &right.count()?,
183 "both live vector handles see the merged generation",
184 )?;
185 }
186 Ok(())
187}
188
189pub fn verify_vector_merge_reopen(
191 store: &Arc<dyn KeyValueStore>,
192 kind: VectorMergeKind,
193) -> StorageBackendResult<()> {
194 for seed in [0, 2, 8] {
195 let table = format!("{kind:?}-merge\0{seed}");
196 compare(
197 store.as_ref(),
198 serial(&table, seed, kind)?.as_ref(),
199 &table,
200 kind,
201 )?;
202 let index = kind.open(store.clone(), &table, true, false)?;
203 let mut documents = index
204 .search_knn(&[1.0, 0.0], 100)?
205 .iter()
206 .map(|posting| posting.doc_id)
207 .collect::<Vec<_>>();
208 documents.sort_unstable();
209 let mut expected = (3..=seed).chain([1, 11, 12, 13]).collect::<Vec<_>>();
210 expected.sort_unstable();
211 expect_eq(&documents, &expected, "reopened merged vector search")?;
212 }
213 Ok(())
214}
215
216pub fn verify_vector_merge_conflicts(
218 a: &Arc<dyn KeyValueStore>,
219 b: &Arc<dyn KeyValueStore>,
220 kind: VectorMergeKind,
221) -> StorageBackendResult<()> {
222 for case in 0..6 {
223 for reverse in [false, true] {
224 let table = format!("{kind:?}-conflict-{case}-{reverse}");
225 let mut left = create(a.clone(), &table, 3, kind)?;
226 let mut right = kind.open(b.clone(), &table, true, false)?;
227 a.begin_transaction()?;
228 b.begin_transaction()?;
229 match case {
230 0 => left.add_many(11, vec![])?,
231 1 => left.delete(11)?,
232 _ => left.add(11, vec![1.0, 0.0])?,
233 }
234 match case {
235 0 | 1 => right.add(11, vec![0.0, 1.0])?,
236 2 => right.clear()?,
237 3 => right.initialize()?,
238 _ => {
239 KeyValueIVFIndex::drop_metadata(b.as_ref(), &table, FIELD)?;
240 right = kind.open(b.clone(), &table, false, case == 5)?;
241 right.initialize()?;
242 }
243 }
244 let (winner, loser) = if reverse { (a, b) } else { (b, a) };
245 winner.commit_transaction()?;
246 let stable = winner.scan_prefix(b"")?;
247 expect(
248 loser.commit_transaction().is_err(),
249 "overlapping or structural vector change conflicts",
250 )?;
251 loser.rollback_transaction()?;
252 expect_eq(
253 &winner.scan_prefix(b"")?,
254 &stable,
255 "rejected vector writer publishes no partial state",
256 )?;
257 }
258 }
259 verify_catalog_lifecycle(a, b, kind)
260}
261
262fn verify_catalog_lifecycle(
263 a: &Arc<dyn KeyValueStore>,
264 b: &Arc<dyn KeyValueStore>,
265 kind: VectorMergeKind,
266) -> StorageBackendResult<()> {
267 use crate::{CatalogFacade, KeyValueCatalog, RelationIdentity, TableSchema, VectorFieldSchema};
268 let catalog = KeyValueCatalog::new(b.clone());
269 catalog.save_schema("public")?;
270 for case in 0..5 {
271 for reverse in [false, true] {
272 let table = format!("public.{kind:?}_lifecycle_{case}_{reverse}");
273 let destination = format!("{table}_renamed");
274 let schema = TableSchema {
275 relation: RelationIdentity::from_legacy_name(&table)
276 .expect("valid fixture relation"),
277 security: crate::RelationSecurityRow::legacy("uqa"),
278 object_id: [1
279 + case * 4
280 + u8::from(reverse) * 2
281 + u8::from(matches!(kind, VectorMergeKind::HNSW));
282 16],
283 storage_generation: [case + 1; 16],
284 analyzer_json: serde_json::to_string(&uqa_analysis::whitespace_analyzer())?,
285 fts_fields: vec![],
286 vector_fields: vec![VectorFieldSchema {
287 field: FIELD.into(),
288 dimensions: 2,
289 }],
290 columns_json: "[]".into(),
291 constraints_json: String::new(),
292 };
293 catalog.save_table(&schema)?;
294 let mut left = create(a.clone(), &table, 3, kind)?;
295 a.begin_transaction()?;
296 b.begin_transaction()?;
297 left.add(11, vec![0.5, 0.5])?;
298 match case {
299 0 => catalog.drop_table_and_data(&table)?,
300 1 => catalog.purge_table_data(&table)?,
301 2 => catalog.rename_table_data(&table, &destination)?,
302 3 => catalog.drop_column_data(&table, FIELD)?,
303 _ => catalog.rename_column_data(&table, FIELD, "replacement")?,
304 }
305 if case <= 1 {
306 catalog.save_table(&schema)?;
307 create(b.clone(), &table, 3, kind)?;
308 }
309 let (winner, loser) = if reverse { (a, b) } else { (b, a) };
310 winner.commit_transaction()?;
311 let stable = winner.scan_prefix(b"")?;
312 expect(
313 loser.commit_transaction().is_err(),
314 "vector source and catalog lifecycle preserve conflicts in both orders",
315 )?;
316 loser.rollback_transaction()?;
317 expect_eq(
318 &winner.scan_prefix(b"")?,
319 &stable,
320 "vector catalog conflict preserves the winner including recreated names",
321 )?;
322 }
323 }
324 Ok(())
325}