Skip to main content

zvec_rust/
collection.rs

1use std::ffi::CStr;
2use std::marker::PhantomData;
3use std::ptr;
4
5use crate::doc::Doc;
6use crate::error::{check_error, to_cstring, Error, ErrorCode, Result};
7use crate::multi_query::MultiQuery;
8use crate::query::SearchQuery;
9use crate::schema::{CollectionSchema, FieldSchema, IndexParams};
10
11/// Options for creating or opening a collection.
12pub struct CollectionOptions {
13    pub(crate) handle: *mut zvec_rust_sys::zvec_collection_options_t,
14}
15
16impl CollectionOptions {
17    /// Creates new collection options with default values.
18    pub fn new() -> Result<Self> {
19        let handle = unsafe { zvec_rust_sys::zvec_collection_options_create() };
20        if handle.is_null() {
21            return Err(Error {
22                code: ErrorCode::InternalError,
23                message: "failed to create collection options".into(),
24            });
25        }
26        Ok(CollectionOptions { handle })
27    }
28
29    /// Sets whether to enable memory mapping.
30    pub fn set_enable_mmap(&mut self, enable: bool) -> Result<()> {
31        check_error(unsafe {
32            zvec_rust_sys::zvec_collection_options_set_enable_mmap(self.handle, enable)
33        })
34    }
35
36    /// Returns whether memory mapping is enabled.
37    pub fn enable_mmap(&self) -> bool {
38        unsafe { zvec_rust_sys::zvec_collection_options_get_enable_mmap(self.handle) }
39    }
40
41    /// Sets the maximum buffer size in bytes.
42    pub fn set_max_buffer_size(&mut self, size: u64) -> Result<()> {
43        check_error(unsafe {
44            zvec_rust_sys::zvec_collection_options_set_max_buffer_size(self.handle, size as usize)
45        })
46    }
47
48    /// Returns the maximum buffer size in bytes.
49    pub fn max_buffer_size(&self) -> u64 {
50        unsafe { zvec_rust_sys::zvec_collection_options_get_max_buffer_size(self.handle) as u64 }
51    }
52
53    /// Sets whether the collection is read-only.
54    pub fn set_read_only(&mut self, read_only: bool) -> Result<()> {
55        check_error(unsafe {
56            zvec_rust_sys::zvec_collection_options_set_read_only(self.handle, read_only)
57        })
58    }
59
60    /// Returns whether the collection is read-only.
61    pub fn read_only(&self) -> bool {
62        unsafe { zvec_rust_sys::zvec_collection_options_get_read_only(self.handle) }
63    }
64}
65
66impl Drop for CollectionOptions {
67    fn drop(&mut self) {
68        if !self.handle.is_null() {
69            unsafe { zvec_rust_sys::zvec_collection_options_destroy(self.handle) };
70        }
71    }
72}
73
74/// Statistics about a single index in a collection.
75#[derive(Debug, Clone)]
76pub struct IndexStat {
77    pub name: String,
78    pub completeness: f32,
79}
80
81/// Statistics about a collection.
82#[derive(Debug, Clone)]
83pub struct CollectionStats {
84    pub doc_count: u64,
85    pub indexes: Vec<IndexStat>,
86}
87
88/// Per-document result of a write operation.
89#[derive(Debug, Clone)]
90pub struct DocWriteResult {
91    pub success: bool,
92    pub code: ErrorCode,
93    pub message: String,
94}
95
96impl DocWriteResult {
97    /// Returns `true` if this individual write succeeded.
98    pub fn is_success(&self) -> bool {
99        self.success
100    }
101}
102
103/// Result of a write operation (insert/update/upsert/delete).
104#[derive(Debug, Clone)]
105pub struct WriteResult {
106    pub success_count: u64,
107    pub error_count: u64,
108    pub results: Vec<DocWriteResult>,
109}
110
111/// A zvec collection for storing and querying vector data.
112///
113/// Collections are the primary data container in zvec. They hold documents
114/// with typed fields and support vector similarity search.
115///
116/// The collection is automatically closed when dropped.
117pub struct Collection {
118    handle: *mut zvec_rust_sys::zvec_collection_t,
119}
120
121impl Collection {
122    /// Returns the raw FFI handle.
123    ///
124    /// # Safety
125    /// The caller must not use the handle after the `Collection` is dropped.
126    pub unsafe fn as_raw(&self) -> *mut zvec_rust_sys::zvec_collection_t {
127        self.handle
128    }
129
130    /// Creates a `Collection` from a raw FFI handle.
131    ///
132    /// # Safety
133    /// The caller must ensure the handle is valid and was created by the zvec C API.
134    /// The `Collection` takes ownership and will call `zvec_collection_close` on drop.
135    pub unsafe fn from_raw(handle: *mut zvec_rust_sys::zvec_collection_t) -> Self {
136        Collection { handle }
137    }
138
139    /// Creates a new collection and opens it.
140    pub fn create_and_open(
141        path: &str,
142        schema: &CollectionSchema,
143        options: Option<&CollectionOptions>,
144    ) -> Result<Self> {
145        let c_path = to_cstring(path)?;
146        let c_options = options.map(|o| o.handle as *const _).unwrap_or(ptr::null());
147
148        let mut handle: *mut zvec_rust_sys::zvec_collection_t = ptr::null_mut();
149        check_error(unsafe {
150            zvec_rust_sys::zvec_collection_create_and_open(
151                c_path.as_ptr(),
152                schema.handle,
153                c_options,
154                &mut handle,
155            )
156        })?;
157
158        Ok(Collection { handle })
159    }
160
161    /// Opens an existing collection.
162    pub fn open(path: &str, options: Option<&CollectionOptions>) -> Result<Self> {
163        let c_path = to_cstring(path)?;
164        let c_options = options.map(|o| o.handle as *const _).unwrap_or(ptr::null());
165
166        let mut handle: *mut zvec_rust_sys::zvec_collection_t = ptr::null_mut();
167        check_error(unsafe {
168            zvec_rust_sys::zvec_collection_open(c_path.as_ptr(), c_options, &mut handle)
169        })?;
170
171        Ok(Collection { handle })
172    }
173
174    /// Flushes collection data to disk.
175    pub fn flush(&self) -> Result<()> {
176        check_error(unsafe { zvec_rust_sys::zvec_collection_flush(self.handle) })
177    }
178
179    /// Returns the collection schema.
180    pub fn schema(&self) -> Result<CollectionSchema> {
181        let mut schema_handle: *mut zvec_rust_sys::zvec_collection_schema_t = ptr::null_mut();
182        check_error(unsafe {
183            zvec_rust_sys::zvec_collection_get_schema(self.handle, &mut schema_handle)
184        })?;
185        Ok(CollectionSchema::from_owned(schema_handle))
186    }
187
188    /// Returns collection statistics.
189    pub fn stats(&self) -> Result<CollectionStats> {
190        let mut stats_handle: *mut zvec_rust_sys::zvec_collection_stats_t = ptr::null_mut();
191        check_error(unsafe {
192            zvec_rust_sys::zvec_collection_get_stats(self.handle, &mut stats_handle)
193        })?;
194
195        let doc_count = unsafe { zvec_rust_sys::zvec_collection_stats_get_doc_count(stats_handle) };
196        let index_count =
197            unsafe { zvec_rust_sys::zvec_collection_stats_get_index_count(stats_handle) };
198
199        let mut indexes = Vec::with_capacity(index_count);
200
201        for i in 0..index_count {
202            let name_ptr =
203                unsafe { zvec_rust_sys::zvec_collection_stats_get_index_name(stats_handle, i) };
204            let name = if name_ptr.is_null() {
205                String::new()
206            } else {
207                unsafe { CStr::from_ptr(name_ptr).to_string_lossy().into_owned() }
208            };
209
210            let completeness = unsafe {
211                zvec_rust_sys::zvec_collection_stats_get_index_completeness(stats_handle, i)
212            };
213            indexes.push(IndexStat { name, completeness });
214        }
215
216        unsafe { zvec_rust_sys::zvec_collection_stats_destroy(stats_handle) };
217
218        Ok(CollectionStats { doc_count, indexes })
219    }
220
221    // =========================================================================
222    // DML Operations
223    // =========================================================================
224
225    /// Inserts documents into the collection.
226    pub fn insert(&self, docs: &[&Doc]) -> Result<WriteResult> {
227        let ptrs: Vec<*const zvec_rust_sys::zvec_doc_t> =
228            docs.iter().map(|d| d.handle as *const _).collect();
229        let mut results: *mut zvec_rust_sys::zvec_write_result_t = ptr::null_mut();
230        let mut result_count: usize = 0;
231
232        check_error(unsafe {
233            zvec_rust_sys::zvec_collection_insert_with_results(
234                self.handle,
235                ptrs.as_ptr(),
236                ptrs.len(),
237                &mut results,
238                &mut result_count,
239            )
240        })?;
241
242        Ok(collect_write_results(results, result_count))
243    }
244
245    /// Updates documents in the collection.
246    pub fn update(&self, docs: &[&Doc]) -> Result<WriteResult> {
247        let ptrs: Vec<*const zvec_rust_sys::zvec_doc_t> =
248            docs.iter().map(|d| d.handle as *const _).collect();
249        let mut results: *mut zvec_rust_sys::zvec_write_result_t = ptr::null_mut();
250        let mut result_count: usize = 0;
251
252        check_error(unsafe {
253            zvec_rust_sys::zvec_collection_update_with_results(
254                self.handle,
255                ptrs.as_ptr(),
256                ptrs.len(),
257                &mut results,
258                &mut result_count,
259            )
260        })?;
261
262        Ok(collect_write_results(results, result_count))
263    }
264
265    /// Inserts or updates documents (upsert).
266    pub fn upsert(&self, docs: &[&Doc]) -> Result<WriteResult> {
267        let ptrs: Vec<*const zvec_rust_sys::zvec_doc_t> =
268            docs.iter().map(|d| d.handle as *const _).collect();
269        let mut results: *mut zvec_rust_sys::zvec_write_result_t = ptr::null_mut();
270        let mut result_count: usize = 0;
271
272        check_error(unsafe {
273            zvec_rust_sys::zvec_collection_upsert_with_results(
274                self.handle,
275                ptrs.as_ptr(),
276                ptrs.len(),
277                &mut results,
278                &mut result_count,
279            )
280        })?;
281
282        Ok(collect_write_results(results, result_count))
283    }
284
285    /// Deletes documents by primary keys.
286    pub fn delete(&self, pks: &[&str]) -> Result<WriteResult> {
287        let c_pks: Vec<_> = pks
288            .iter()
289            .map(|pk| to_cstring(pk))
290            .collect::<Result<Vec<_>>>()?;
291        let c_ptrs: Vec<_> = c_pks.iter().map(|pk| pk.as_ptr()).collect();
292        let mut results: *mut zvec_rust_sys::zvec_write_result_t = ptr::null_mut();
293        let mut result_count: usize = 0;
294
295        check_error(unsafe {
296            zvec_rust_sys::zvec_collection_delete_with_results(
297                self.handle,
298                c_ptrs.as_ptr(),
299                c_ptrs.len(),
300                &mut results,
301                &mut result_count,
302            )
303        })?;
304
305        Ok(collect_write_results(results, result_count))
306    }
307
308    /// Deletes documents matching a filter expression.
309    pub fn delete_by_filter(&self, filter: &str) -> Result<()> {
310        let c_filter = to_cstring(filter)?;
311        check_error(unsafe {
312            zvec_rust_sys::zvec_collection_delete_by_filter(self.handle, c_filter.as_ptr())
313        })
314    }
315
316    // =========================================================================
317    // DQL Operations
318    // =========================================================================
319
320    /// Performs a vector similarity search.
321    pub fn query(&self, query: &SearchQuery) -> Result<Vec<Doc>> {
322        let mut results: *mut *mut zvec_rust_sys::zvec_doc_t = ptr::null_mut();
323        let mut result_count: usize = 0;
324
325        check_error(unsafe {
326            zvec_rust_sys::zvec_collection_query(
327                self.handle,
328                query.handle,
329                &mut results,
330                &mut result_count,
331            )
332        })?;
333
334        let docs = unsafe { collect_docs(results, result_count) };
335        Ok(docs)
336    }
337
338    /// Performs a multi-query that combines several sub-queries with a rerank
339    /// strategy (RRF or weighted).
340    pub fn multi_query(&self, query: &MultiQuery) -> Result<Vec<Doc>> {
341        let mut results: *mut *mut zvec_rust_sys::zvec_doc_t = ptr::null_mut();
342        let mut result_count: usize = 0;
343
344        check_error(unsafe {
345            zvec_rust_sys::zvec_collection_multi_query(
346                self.handle,
347                query.handle,
348                &mut results,
349                &mut result_count,
350            )
351        })?;
352
353        let docs = unsafe { collect_docs(results, result_count) };
354        Ok(docs)
355    }
356
357    /// Fetches documents by primary keys, returning all fields including vectors.
358    pub fn fetch(&self, pks: &[&str]) -> Result<Vec<Doc>> {
359        self.fetch_with_options(pks, None, true)
360    }
361
362    /// Fetches documents by primary keys with control over which fields to return.
363    ///
364    /// # Projections are destructive when written back
365    ///
366    /// A projected fetch (`output_fields` set, or `include_vector == false`)
367    /// returns a document shaped like the full schema in which the columns you did
368    /// not ask for are empty rather than absent. Passing such a document to
369    /// [`Collection::upsert`] or [`Collection::update`] therefore overwrites those
370    /// columns: nullable ones are silently set to null, while a missing
371    /// non-nullable one is rejected with [`ErrorCode::InvalidArgument`].
372    ///
373    /// A read-modify-write cycle must fetch the complete document:
374    ///
375    /// ```no_run
376    /// use zvec_rust::*;
377    ///
378    /// # fn f(collection: &Collection) -> Result<()> {
379    /// let mut docs = collection.fetch_with_options(&["doc1"], None, true)?;
380    /// docs[0].add_string("payload", "tombstone")?;
381    /// let refs: Vec<&Doc> = docs.iter().collect();
382    /// collection.upsert(&refs)?;
383    /// # Ok(())
384    /// # }
385    /// ```
386    ///
387    /// Keep projections for read-only paths.
388    ///
389    /// # Row order is not guaranteed
390    ///
391    /// The returned documents are not guaranteed to come back in the order the
392    /// primary keys were requested in, and the order can differ between
393    /// platforms and between the in-memory writing segment and the persisted
394    /// forward store. Match rows by primary key (or by a projected field)
395    /// instead of by position.
396    pub fn fetch_with_options(
397        &self,
398        pks: &[&str],
399        output_fields: Option<&[&str]>,
400        include_vector: bool,
401    ) -> Result<Vec<Doc>> {
402        let c_pks: Vec<_> = pks
403            .iter()
404            .map(|pk| to_cstring(pk))
405            .collect::<Result<Vec<_>>>()?;
406        let c_pk_ptrs: Vec<_> = c_pks.iter().map(|pk| pk.as_ptr()).collect();
407
408        let c_fields = output_fields
409            .map(|fields| {
410                fields
411                    .iter()
412                    .map(|f| to_cstring(f))
413                    .collect::<Result<Vec<_>>>()
414            })
415            .transpose()?;
416        let c_field_ptrs: Option<Vec<_>> = c_fields
417            .as_ref()
418            .map(|f| f.iter().map(|s| s.as_ptr()).collect());
419        let (fields_ptr, fields_count) = match &c_field_ptrs {
420            Some(ptrs) => (ptrs.as_ptr(), ptrs.len()),
421            None => (ptr::null(), 0),
422        };
423
424        let mut documents: *mut *mut zvec_rust_sys::zvec_doc_t = ptr::null_mut();
425        let mut found_count: usize = 0;
426
427        check_error(unsafe {
428            zvec_rust_sys::zvec_collection_fetch(
429                self.handle,
430                c_pk_ptrs.as_ptr(),
431                c_pk_ptrs.len(),
432                fields_ptr,
433                fields_count,
434                include_vector,
435                &mut documents,
436                &mut found_count,
437            )
438        })?;
439
440        let docs = unsafe { collect_docs(documents, found_count) };
441        Ok(docs)
442    }
443
444    // =========================================================================
445    // Document Iteration
446    // =========================================================================
447
448    /// Creates an iterator over all documents in the collection using default
449    /// options (all scalar fields, vectors included).
450    ///
451    /// The iterator traverses an isolated snapshot taken at creation time;
452    /// documents written afterwards are not visible.
453    pub fn iter(&self) -> Result<DocIterator<'_>> {
454        self.iter_with_options(None, true)
455    }
456
457    /// Creates an iterator over all documents with control over which fields
458    /// to return.
459    ///
460    /// - `output_fields`: scalar fields to return; `None` returns all fields.
461    ///   An empty slice returns no scalar fields (primary key only).
462    /// - `include_vector`: whether to include vector fields.
463    ///
464    /// The iterator traverses an isolated snapshot taken at creation time;
465    /// documents written afterwards are not visible. While the iterator is
466    /// open, schema changes (create/drop index, add/alter/drop column) and
467    /// destroy are rejected by the C library.
468    pub fn iter_with_options(
469        &self,
470        output_fields: Option<&[&str]>,
471        include_vector: bool,
472    ) -> Result<DocIterator<'_>> {
473        let options = unsafe { zvec_rust_sys::zvec_iterator_options_create() };
474        if options.is_null() {
475            return Err(Error {
476                code: ErrorCode::InternalError,
477                message: "failed to create iterator options".into(),
478            });
479        }
480
481        // Build C string fields before the first FFI call that must be cleaned
482        // up on failure; `options` is destroyed on every early return.
483        let result = (|| -> Result<()> {
484            let c_fields = output_fields
485                .map(|fields| {
486                    fields
487                        .iter()
488                        .map(|f| to_cstring(f))
489                        .collect::<Result<Vec<_>>>()
490                })
491                .transpose()?;
492            let c_field_ptrs: Option<Vec<_>> = c_fields
493                .as_ref()
494                .map(|f| f.iter().map(|s| s.as_ptr()).collect());
495            let (fields_ptr, fields_count) = match &c_field_ptrs {
496                Some(ptrs) => (ptrs.as_ptr(), ptrs.len()),
497                None => (ptr::null(), 0),
498            };
499
500            check_error(unsafe {
501                zvec_rust_sys::zvec_iterator_options_set_output_fields(
502                    options,
503                    fields_ptr,
504                    fields_count,
505                )
506            })?;
507            check_error(unsafe {
508                zvec_rust_sys::zvec_iterator_options_set_include_vector(options, include_vector)
509            })?;
510            Ok(())
511        })();
512        if let Err(e) = result {
513            unsafe { zvec_rust_sys::zvec_iterator_options_destroy(options) };
514            return Err(e);
515        }
516
517        let mut iter: *mut zvec_rust_sys::zvec_doc_iterator_t = ptr::null_mut();
518        let rc = check_error(unsafe {
519            zvec_rust_sys::zvec_collection_create_iterator(self.handle, options, &mut iter)
520        });
521        unsafe { zvec_rust_sys::zvec_iterator_options_destroy(options) };
522        rc?;
523
524        Ok(DocIterator {
525            handle: iter,
526            _collection: PhantomData,
527        })
528    }
529
530    // =========================================================================
531    // Index Management
532    // =========================================================================
533
534    /// Creates an index on a field.
535    pub fn create_index(&self, field_name: &str, params: &IndexParams) -> Result<()> {
536        let c_name = to_cstring(field_name)?;
537        check_error(unsafe {
538            zvec_rust_sys::zvec_collection_create_index(self.handle, c_name.as_ptr(), params.handle)
539        })
540    }
541
542    /// Drops an index from a field.
543    pub fn drop_index(&self, field_name: &str) -> Result<()> {
544        let c_name = to_cstring(field_name)?;
545        check_error(unsafe {
546            zvec_rust_sys::zvec_collection_drop_index(self.handle, c_name.as_ptr())
547        })
548    }
549
550    /// Optimizes the collection (rebuild indexes, merge segments, etc.).
551    pub fn optimize(&self) -> Result<()> {
552        check_error(unsafe { zvec_rust_sys::zvec_collection_optimize(self.handle) })
553    }
554
555    // =========================================================================
556    // DDL Operations
557    // =========================================================================
558
559    /// Adds a column to the collection.
560    pub fn add_column(&self, field_schema: &FieldSchema, default_expr: Option<&str>) -> Result<()> {
561        let c_expr_owned = default_expr.map(to_cstring).transpose()?;
562        let c_expr_ptr = c_expr_owned
563            .as_ref()
564            .map(|s| s.as_ptr())
565            .unwrap_or(ptr::null());
566        check_error(unsafe {
567            zvec_rust_sys::zvec_collection_add_column(self.handle, field_schema.handle, c_expr_ptr)
568        })
569    }
570
571    /// Drops a column from the collection.
572    pub fn drop_column(&self, name: &str) -> Result<()> {
573        let c_name = to_cstring(name)?;
574        check_error(unsafe {
575            zvec_rust_sys::zvec_collection_drop_column(self.handle, c_name.as_ptr())
576        })
577    }
578
579    /// Alters a column: renames it, or replaces its schema.
580    ///
581    /// `new_name` and `new_schema` are mutually exclusive and at least one must be
582    /// given; any other combination is rejected with
583    /// [`ErrorCode::InvalidArgument`]. Renaming keeps the current schema, while a
584    /// new schema replaces the field definition outright — its own name becomes the
585    /// resulting column name, so pass `name` again to keep it unchanged. A nullable
586    /// field cannot be made non-nullable.
587    ///
588    /// The schema is deep-copied by the native layer, so `new_schema` stays owned
589    /// by the caller and is dropped as usual.
590    pub fn alter_column(
591        &self,
592        name: &str,
593        new_name: Option<&str>,
594        new_schema: Option<&FieldSchema>,
595    ) -> Result<()> {
596        let c_name = to_cstring(name)?;
597        let c_new_name = new_name.map(to_cstring).transpose()?;
598        let c_new_name_ptr = c_new_name
599            .as_ref()
600            .map(|s| s.as_ptr())
601            .unwrap_or(ptr::null());
602        let new_schema_ptr = new_schema
603            .map(|schema| schema.handle as *const zvec_rust_sys::zvec_field_schema_t)
604            .unwrap_or(ptr::null());
605        check_error(unsafe {
606            zvec_rust_sys::zvec_collection_alter_column(
607                self.handle,
608                c_name.as_ptr(),
609                c_new_name_ptr,
610                new_schema_ptr,
611            )
612        })
613    }
614
615    /// Closes the collection explicitly.
616    ///
617    /// The native handle is released by [`Drop`], which reports a failed
618    /// `zvec_collection_close` only as a warning on stderr, so this always
619    /// returns `Ok(())`. Use [`Collection::try_close`] to observe a failed
620    /// close.
621    pub fn close(self) -> Result<()> {
622        // Drop will handle the close
623        drop(self);
624        Ok(())
625    }
626
627    /// Closes the collection explicitly and reports a failed close.
628    ///
629    /// Consumes the collection exactly like [`Collection::close`], but releases
630    /// the native handle here instead of leaving it to [`Drop`].
631    ///
632    /// `zvec_collection_close` releases its handle whether it succeeds or
633    /// fails — a failure only reports an exception raised while the handle was
634    /// being released — so the handle is gone on both paths and a second close
635    /// cannot be requested. Callers therefore get no handle back to retry with,
636    /// and the collection's data directory is left as the C library left it.
637    ///
638    /// Open iterators borrow the collection, so none can be alive at this
639    /// point; the C library then waits for the remaining handles before it
640    /// closes the collection itself.
641    ///
642    /// A null handle, reachable only through [`Collection::from_raw`], is
643    /// reported as [`ErrorCode::InvalidArgument`] rather than being ignored.
644    ///
645    /// # Errors
646    ///
647    /// Returns whatever `zvec_collection_close` reported, together with the
648    /// message the C library stored for that failure.
649    ///
650    /// # Examples
651    ///
652    /// ```no_run
653    /// use zvec_rust::*;
654    ///
655    /// # fn f(collection: Collection) -> Result<()> {
656    /// collection.try_close()?;
657    /// # Ok(())
658    /// # }
659    /// ```
660    ///
661    /// Consuming the collection is what keeps a second close out of reach:
662    ///
663    /// ```compile_fail
664    /// use zvec_rust::*;
665    ///
666    /// # fn f(collection: Collection) -> Result<()> {
667    /// collection.try_close()?;
668    /// collection.try_close()?;
669    /// # Ok(())
670    /// # }
671    /// ```
672    pub fn try_close(mut self) -> Result<()> {
673        // Release the handle exactly once: `Drop` skips a null handle, so
674        // clearing the field first turns the implicit close into a no-op.
675        let handle = std::mem::replace(&mut self.handle, ptr::null_mut());
676        check_error(unsafe { zvec_rust_sys::zvec_collection_close(handle) })
677    }
678}
679
680impl Drop for Collection {
681    fn drop(&mut self) {
682        if !self.handle.is_null() {
683            // Safety: handle was created by zvec_collection_create_and_open or zvec_collection_open
684            let rc = unsafe { zvec_rust_sys::zvec_collection_close(self.handle) };
685            if rc != zvec_rust_sys::ZVEC_OK {
686                eprintln!(
687                    "zvec warning: failed to close collection (error code {})",
688                    rc
689                );
690            }
691        }
692    }
693}
694
695// Safety: The zvec C-API documents that collection operations are internally
696// thread-safe. All mutable state is protected by internal locks in the C library.
697// See: https://github.com/alibaba/zvec — C-API thread-safety guarantees.
698unsafe impl Send for Collection {}
699unsafe impl Sync for Collection {}
700
701/// Iterator over all documents in a collection.
702///
703/// Created by [`Collection::iter`] or [`Collection::iter_with_options`].
704/// Traverses an isolated snapshot taken at creation time. Each `next` call
705/// returns `Some(Ok(doc))` for a document, `Some(Err(e))` on failure, and
706/// `None` at the end of the collection.
707///
708/// The iterator borrows its collection, so the collection cannot be dropped
709/// while the iterator is alive. Dropping the iterator closes it via
710/// `zvec_doc_iterator_close`.
711pub struct DocIterator<'a> {
712    handle: *mut zvec_rust_sys::zvec_doc_iterator_t,
713    _collection: PhantomData<&'a Collection>,
714}
715
716impl<'a> Iterator for DocIterator<'a> {
717    type Item = Result<Doc>;
718
719    fn next(&mut self) -> Option<Self::Item> {
720        let mut doc: *mut zvec_rust_sys::zvec_doc_t = ptr::null_mut();
721        let rc = unsafe { zvec_rust_sys::zvec_doc_iterator_next(self.handle, &mut doc) };
722        if rc != zvec_rust_sys::ZVEC_OK {
723            return Some(Err(Error {
724                code: ErrorCode::from(rc),
725                message: format!("document iterator failed with error code {}", rc),
726            }));
727        }
728        if doc.is_null() {
729            return None; // EOF
730        }
731        // Take ownership: Rust will call zvec_doc_destroy on drop.
732        Some(Ok(unsafe { Doc::from_raw(doc) }))
733    }
734}
735
736impl<'a> Drop for DocIterator<'a> {
737    fn drop(&mut self) {
738        if !self.handle.is_null() {
739            unsafe { zvec_rust_sys::zvec_doc_iterator_close(self.handle) };
740        }
741    }
742}
743
744/// Parses a C array of `zvec_write_result_t` into a `WriteResult`.
745fn collect_write_results(
746    results: *mut zvec_rust_sys::zvec_write_result_t,
747    count: usize,
748) -> WriteResult {
749    let mut doc_results = Vec::with_capacity(count);
750    let mut success_count: u64 = 0;
751    let mut error_count: u64 = 0;
752
753    if !results.is_null() && count > 0 {
754        for i in 0..count {
755            let wr = unsafe { &*results.add(i) };
756            let is_ok = wr.code == zvec_rust_sys::ZVEC_OK;
757            let message = if wr.message.is_null() {
758                String::new()
759            } else {
760                unsafe { CStr::from_ptr(wr.message).to_string_lossy().into_owned() }
761            };
762            if is_ok {
763                success_count += 1;
764            } else {
765                error_count += 1;
766            }
767            doc_results.push(DocWriteResult {
768                success: is_ok,
769                code: ErrorCode::from(wr.code),
770                message,
771            });
772        }
773        unsafe { zvec_rust_sys::zvec_write_results_free(results, count) };
774    }
775
776    WriteResult {
777        success_count,
778        error_count,
779        results: doc_results,
780    }
781}
782
783/// Collects document pointers from a C array into a Vec<Doc>.
784///
785/// # Safety
786/// `results` must point to a valid array of `count` document pointers
787/// allocated by the zvec C library.
788unsafe fn collect_docs(results: *mut *mut zvec_rust_sys::zvec_doc_t, count: usize) -> Vec<Doc> {
789    if results.is_null() || count == 0 {
790        return Vec::new();
791    }
792
793    let mut docs = Vec::with_capacity(count);
794    for i in 0..count {
795        let doc_ptr = *results.add(i);
796        if !doc_ptr.is_null() {
797            // Take ownership: Rust will call zvec_doc_destroy on drop
798            docs.push(Doc::from_raw(doc_ptr));
799        }
800    }
801
802    // Free only the pointer array itself (not the individual docs, which are now
803    // owned by the Doc wrappers above and will be freed via their Drop impls).
804    zvec_rust_sys::zvec_free(results as *mut std::os::raw::c_void);
805
806    docs
807}
808
809#[cfg(test)]
810mod tests {
811    use super::*;
812
813    #[test]
814    fn collect_docs_handles_null_pointer() {
815        let docs = unsafe { collect_docs(ptr::null_mut(), 0) };
816        assert!(docs.is_empty());
817    }
818
819    #[test]
820    fn collect_docs_handles_zero_count() {
821        // Even with a non-null pointer, zero count should return empty
822        let mut fake: *mut zvec_rust_sys::zvec_doc_t = ptr::null_mut();
823        let docs = unsafe { collect_docs(&mut fake as *mut _, 0) };
824        assert!(docs.is_empty());
825    }
826}