From deedd323aa4c34125cab92cfef5f87b93dabf38a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Donny/=EA=B0=95=EB=8F=99=EC=9C=A4?= Date: Thu, 12 Mar 2026 21:32:06 +0900 Subject: [PATCH 1/3] chore: Add C bindings (#1) --- .gitignore | 5 +- CMakeLists.txt | 3 + src/CMakeLists.txt | 2 +- src/binding/CMakeLists.txt | 8 +- src/binding/c/CMakeLists.txt | 70 +++ src/binding/c/zvec_c.cc | 799 +++++++++++++++++++++++++++++++++++ src/binding/c/zvec_c.h | 225 ++++++++++ 7 files changed, 1109 insertions(+), 3 deletions(-) create mode 100644 src/binding/c/CMakeLists.txt create mode 100644 src/binding/c/zvec_c.cc create mode 100644 src/binding/c/zvec_c.h diff --git a/.gitignore b/.gitignore index 0827e539..42a59140 100644 --- a/.gitignore +++ b/.gitignore @@ -48,4 +48,7 @@ yarn-error.log* allure-* -!build_android.sh \ No newline at end of file +!build_android.sh + +# Rust crate build artifacts +zvec-rs/target/ \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 52a59754..5533c13b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,6 +25,9 @@ include_directories(${PROJECT_ROOT_DIR}/src) option(BUILD_PYTHON_BINDINGS "Build Python bindings using pybind11" OFF) message(STATUS "BUILD_PYTHON_BINDINGS:${BUILD_PYTHON_BINDINGS}") +option(BUILD_C_BINDINGS "Build C bindings (libzvec_c) for Rust/FFI consumers" OFF) +message(STATUS "BUILD_C_BINDINGS:${BUILD_C_BINDINGS}") + option(BUILD_TOOLS "Build tools" ON) message(STATUS "BUILD_TOOLS:${BUILD_TOOLS}") diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c516187c..0be5335e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -8,6 +8,6 @@ git_version(ZVEC_VERSION ${CMAKE_CURRENT_SOURCE_DIR}) cc_directory(ailego) cc_directory(core) cc_directory(db) -if(BUILD_PYTHON_BINDINGS) +if(BUILD_PYTHON_BINDINGS OR BUILD_C_BINDINGS) cc_directory(binding) endif() diff --git a/src/binding/CMakeLists.txt b/src/binding/CMakeLists.txt index 7dab04ad..0934715a 100644 --- a/src/binding/CMakeLists.txt +++ b/src/binding/CMakeLists.txt @@ -5,4 +5,10 @@ include(${PROJECT_ROOT_DIR}/cmake/option.cmake) git_version(ZVEC_VERSION ${CMAKE_CURRENT_SOURCE_DIR}) # Add repository -cc_directory(python) \ No newline at end of file +if(BUILD_PYTHON_BINDINGS) + cc_directory(python) +endif() + +if(BUILD_C_BINDINGS) + cc_directory(c) +endif() \ No newline at end of file diff --git a/src/binding/c/CMakeLists.txt b/src/binding/c/CMakeLists.txt new file mode 100644 index 00000000..ab69dec9 --- /dev/null +++ b/src/binding/c/CMakeLists.txt @@ -0,0 +1,70 @@ +include(${PROJECT_ROOT_DIR}/cmake/bazel.cmake) +include(${PROJECT_ROOT_DIR}/cmake/option.cmake) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# ── nlohmann/json (header-only) ────────────────────────────────────────────── +include(FetchContent) +FetchContent_Declare( + nlohmann_json + GIT_REPOSITORY https://github.com/nlohmann/json.git + GIT_TAG v3.11.3 + GIT_SHALLOW TRUE +) +set(JSON_BuildTests OFF CACHE BOOL "" FORCE) +set(JSON_Install OFF CACHE BOOL "" FORCE) +set(JSON_MultipleHeaders OFF CACHE BOOL "" FORCE) +FetchContent_MakeAvailable(nlohmann_json) + +# ── Build zvec_c as a shared library ───────────────────────────────────────── +add_library(zvec_c SHARED zvec_c.cc) + +target_include_directories(zvec_c + PRIVATE + ${PROJECT_ROOT_DIR}/src + ${PROJECT_ROOT_DIR}/src/include +) + +target_link_libraries(zvec_c PRIVATE nlohmann_json::nlohmann_json) + +# Replicate the same whole-archive / force_load strategy used by the Python +# binding so every algorithm implementation is included in the shared lib. +if (CMAKE_SYSTEM_NAME STREQUAL "Linux") + target_link_libraries(zvec_c PRIVATE + -Wl,--whole-archive + $ + $ + $ + $ + $ + $ + $ + $ + $ + $ + -Wl,--no-whole-archive + zvec_db + ) +elseif (APPLE) + target_link_libraries(zvec_c PRIVATE + -Wl,-force_load,$ + -Wl,-force_load,$ + -Wl,-force_load,$ + -Wl,-force_load,$ + -Wl,-force_load,$ + -Wl,-force_load,$ + -Wl,-force_load,$ + -Wl,-force_load,$ + -Wl,-force_load,$ + -Wl,-force_load,$ + zvec_db + ) +endif() + +# Install so that Rust's build.rs (cmake crate) can find it under $OUT_DIR/lib +install(TARGETS zvec_c + LIBRARY DESTINATION lib + RUNTIME DESTINATION lib # Windows DLL goes here too +) +install(FILES zvec_c.h DESTINATION include) diff --git a/src/binding/c/zvec_c.cc b/src/binding/c/zvec_c.cc new file mode 100644 index 00000000..7d811d17 --- /dev/null +++ b/src/binding/c/zvec_c.cc @@ -0,0 +1,799 @@ +// Copyright 2025-present the zvec project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "zvec_c.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +using json = nlohmann::json; +using namespace zvec; + +// ============================================================================ +// Internal helpers: status +// ============================================================================ + +static zvec_status_t make_ok() { return {0, nullptr}; } + +static zvec_status_t make_error(const Status& s) { + if (s.ok()) return make_ok(); + const std::string& msg = s.message(); + char* buf = new char[msg.size() + 1]; + std::memcpy(buf, msg.c_str(), msg.size() + 1); + return {static_cast(s.code()), buf}; +} + +static zvec_status_t make_exception_error(const std::exception& e) { + std::string msg = e.what(); + char* buf = new char[msg.size() + 1]; + std::memcpy(buf, msg.c_str(), msg.size() + 1); + return {ZVEC_UNKNOWN, buf}; +} + +static char* strdup_new(const std::string& s) { + char* buf = new char[s.size() + 1]; + std::memcpy(buf, s.c_str(), s.size() + 1); + return buf; +} + +// ============================================================================ +// Internal helpers: type parsing +// ============================================================================ + +static DataType parse_data_type(const std::string& s) { + if (s == "BINARY") return DataType::BINARY; + if (s == "STRING") return DataType::STRING; + if (s == "BOOL") return DataType::BOOL; + if (s == "INT32") return DataType::INT32; + if (s == "INT64") return DataType::INT64; + if (s == "UINT32") return DataType::UINT32; + if (s == "UINT64") return DataType::UINT64; + if (s == "FLOAT") return DataType::FLOAT; + if (s == "DOUBLE") return DataType::DOUBLE; + if (s == "VECTOR_BINARY32") return DataType::VECTOR_BINARY32; + if (s == "VECTOR_BINARY64") return DataType::VECTOR_BINARY64; + if (s == "VECTOR_FP16") return DataType::VECTOR_FP16; + if (s == "VECTOR_FP32") return DataType::VECTOR_FP32; + if (s == "VECTOR_FP64") return DataType::VECTOR_FP64; + if (s == "VECTOR_INT4") return DataType::VECTOR_INT4; + if (s == "VECTOR_INT8") return DataType::VECTOR_INT8; + if (s == "VECTOR_INT16") return DataType::VECTOR_INT16; + if (s == "SPARSE_VECTOR_FP16") return DataType::SPARSE_VECTOR_FP16; + if (s == "SPARSE_VECTOR_FP32") return DataType::SPARSE_VECTOR_FP32; + if (s == "ARRAY_BINARY") return DataType::ARRAY_BINARY; + if (s == "ARRAY_STRING") return DataType::ARRAY_STRING; + if (s == "ARRAY_BOOL") return DataType::ARRAY_BOOL; + if (s == "ARRAY_INT32") return DataType::ARRAY_INT32; + if (s == "ARRAY_INT64") return DataType::ARRAY_INT64; + if (s == "ARRAY_UINT32") return DataType::ARRAY_UINT32; + if (s == "ARRAY_UINT64") return DataType::ARRAY_UINT64; + if (s == "ARRAY_FLOAT") return DataType::ARRAY_FLOAT; + if (s == "ARRAY_DOUBLE") return DataType::ARRAY_DOUBLE; + return DataType::UNDEFINED; +} + +static MetricType parse_metric_type(const std::string& s) { + if (s == "L2") return MetricType::L2; + if (s == "IP") return MetricType::IP; + if (s == "COSINE") return MetricType::COSINE; + if (s == "MIPSL2") return MetricType::MIPSL2; + return MetricType::UNDEFINED; +} + +static QuantizeType parse_quantize_type(const json& j) { + if (!j.contains("quantize")) return QuantizeType::UNDEFINED; + const std::string s = j["quantize"].get(); + if (s == "FP16") return QuantizeType::FP16; + if (s == "INT8") return QuantizeType::INT8; + if (s == "INT4") return QuantizeType::INT4; + return QuantizeType::UNDEFINED; +} + +static IndexParams::Ptr parse_index_params(const json& j) { + std::string type = j["type"].get(); + if (type == "HNSW") { + auto metric = parse_metric_type(j.value("metric", std::string("L2"))); + int m = j.value("m", 16); + int ef = j.value("ef_construction", 200); + auto quant = parse_quantize_type(j); + return std::make_shared(metric, m, ef, quant); + } + if (type == "FLAT") { + auto metric = parse_metric_type(j.value("metric", std::string("L2"))); + auto quant = parse_quantize_type(j); + return std::make_shared(metric, quant); + } + if (type == "IVF") { + auto metric = parse_metric_type(j.value("metric", std::string("L2"))); + int n_list = j.value("n_list", 1024); + int n_iters = j.value("n_iters", 10); + bool use_soar = j.value("use_soar", false); + auto quant = parse_quantize_type(j); + return std::make_shared(metric, n_list, n_iters, use_soar, + quant); + } + if (type == "INVERT") { + bool range_opt = j.value("enable_range_optimization", true); + bool ext_wildcard = j.value("enable_extended_wildcard", false); + return std::make_shared(range_opt, ext_wildcard); + } + return nullptr; +} + +// ============================================================================ +// Internal helpers: schema parsing +// ============================================================================ + +static FieldSchema::Ptr parse_field_schema(const json& j) { + std::string name = j["name"].get(); + DataType data_type = parse_data_type(j["data_type"].get()); + bool nullable = j.value("nullable", false); + uint32_t dimension = j.value("dimension", 0); + IndexParams::Ptr index_params = nullptr; + if (j.contains("index")) { + index_params = parse_index_params(j["index"]); + } + return std::make_shared(name, data_type, dimension, nullable, + index_params); +} + +static CollectionSchema parse_schema(const json& j) { + CollectionSchema schema(j["name"].get()); + for (const auto& field_j : j["fields"]) { + schema.add_field(parse_field_schema(field_j)); + } + return schema; +} + +// ============================================================================ +// Internal helpers: Doc serialization/deserialization +// ============================================================================ + +static void set_doc_field(Doc& doc, const std::string& name, const json& val, + DataType dt) { + if (val.is_null()) { + doc.set_null(name); + return; + } + switch (dt) { + case DataType::BOOL: + doc.set(name, val.get()); + break; + case DataType::INT32: + doc.set(name, val.get()); + break; + case DataType::INT64: + doc.set(name, val.get()); + break; + case DataType::UINT32: + doc.set(name, val.get()); + break; + case DataType::UINT64: + doc.set(name, val.get()); + break; + case DataType::FLOAT: + doc.set(name, val.get()); + break; + case DataType::DOUBLE: + doc.set(name, val.get()); + break; + case DataType::STRING: + case DataType::BINARY: + doc.set(name, val.get()); + break; + case DataType::VECTOR_FP32: + doc.set(name, val.get>()); + break; + case DataType::VECTOR_FP16: { + auto floats = val.get>(); + std::vector fp16(floats.size()); + for (size_t i = 0; i < floats.size(); ++i) fp16[i] = float16_t(floats[i]); + doc.set(name, fp16); + break; + } + case DataType::VECTOR_INT8: { + doc.set(name, val.get>()); + break; + } + case DataType::VECTOR_INT16: { + doc.set(name, val.get>()); + break; + } + case DataType::SPARSE_VECTOR_FP32: { + auto indices = val["indices"].get>(); + auto values = val["values"].get>(); + doc.set(name, std::make_pair(indices, values)); + break; + } + case DataType::SPARSE_VECTOR_FP16: { + auto indices = val["indices"].get>(); + auto fvals = val["values"].get>(); + std::vector fp16v(fvals.size()); + for (size_t i = 0; i < fvals.size(); ++i) fp16v[i] = float16_t(fvals[i]); + doc.set(name, std::make_pair(indices, fp16v)); + break; + } + case DataType::ARRAY_BOOL: + doc.set(name, val.get>()); + break; + case DataType::ARRAY_INT32: + doc.set(name, val.get>()); + break; + case DataType::ARRAY_INT64: + doc.set(name, val.get>()); + break; + case DataType::ARRAY_UINT32: + doc.set(name, val.get>()); + break; + case DataType::ARRAY_UINT64: + doc.set(name, val.get>()); + break; + case DataType::ARRAY_FLOAT: + doc.set(name, val.get>()); + break; + case DataType::ARRAY_DOUBLE: + doc.set(name, val.get>()); + break; + case DataType::ARRAY_STRING: + doc.set(name, val.get>()); + break; + default: + break; + } +} + +static std::vector parse_docs(const json& docs_j, + const CollectionSchema& schema) { + std::vector docs; + docs.reserve(docs_j.size()); + for (const auto& doc_j : docs_j) { + Doc doc; + if (doc_j.contains("pk")) { + doc.set_pk(doc_j["pk"].get()); + } + if (doc_j.contains("fields")) { + for (const auto& [field_name, value_j] : doc_j["fields"].items()) { + const FieldSchema* field = schema.get_field(field_name); + if (!field) continue; + set_doc_field(doc, field_name, value_j, field->data_type()); + } + } + docs.push_back(std::move(doc)); + } + return docs; +} + +static json field_to_json(const Doc& doc, const std::string& name, + DataType dt) { + switch (dt) { + case DataType::BOOL: { + auto v = doc.get(name); + return v ? json(*v) : json(nullptr); + } + case DataType::INT32: { + auto v = doc.get(name); + return v ? json(*v) : json(nullptr); + } + case DataType::INT64: { + auto v = doc.get(name); + return v ? json(*v) : json(nullptr); + } + case DataType::UINT32: { + auto v = doc.get(name); + return v ? json(*v) : json(nullptr); + } + case DataType::UINT64: { + auto v = doc.get(name); + return v ? json(*v) : json(nullptr); + } + case DataType::FLOAT: { + auto v = doc.get(name); + return v ? json(*v) : json(nullptr); + } + case DataType::DOUBLE: { + auto v = doc.get(name); + return v ? json(*v) : json(nullptr); + } + case DataType::STRING: + case DataType::BINARY: { + auto v = doc.get(name); + return v ? json(*v) : json(nullptr); + } + case DataType::VECTOR_FP32: { + auto v = doc.get>(name); + return v ? json(*v) : json(nullptr); + } + case DataType::VECTOR_FP16: { + auto v = doc.get>(name); + if (!v) return json(nullptr); + std::vector floats(v->size()); + for (size_t i = 0; i < v->size(); ++i) + floats[i] = static_cast((*v)[i]); + return json(floats); + } + case DataType::VECTOR_INT8: { + auto v = doc.get>(name); + return v ? json(*v) : json(nullptr); + } + case DataType::VECTOR_INT16: { + auto v = doc.get>(name); + return v ? json(*v) : json(nullptr); + } + case DataType::SPARSE_VECTOR_FP32: { + auto v = + doc.get, std::vector>>(name); + if (!v) return json(nullptr); + return json{{"indices", v->first}, {"values", v->second}}; + } + case DataType::SPARSE_VECTOR_FP16: { + auto v = doc.get< + std::pair, std::vector>>(name); + if (!v) return json(nullptr); + std::vector fvals(v->second.size()); + for (size_t i = 0; i < v->second.size(); ++i) + fvals[i] = static_cast(v->second[i]); + return json{{"indices", v->first}, {"values", fvals}}; + } + case DataType::ARRAY_BOOL: { + auto v = doc.get>(name); + return v ? json(*v) : json(nullptr); + } + case DataType::ARRAY_INT32: { + auto v = doc.get>(name); + return v ? json(*v) : json(nullptr); + } + case DataType::ARRAY_INT64: { + auto v = doc.get>(name); + return v ? json(*v) : json(nullptr); + } + case DataType::ARRAY_FLOAT: { + auto v = doc.get>(name); + return v ? json(*v) : json(nullptr); + } + case DataType::ARRAY_DOUBLE: { + auto v = doc.get>(name); + return v ? json(*v) : json(nullptr); + } + case DataType::ARRAY_STRING: { + auto v = doc.get>(name); + return v ? json(*v) : json(nullptr); + } + default: + return json(nullptr); + } +} + +static json doc_to_json(const Doc& doc, const CollectionSchema& schema) { + json j; + j["pk"] = doc.pk(); + j["score"] = doc.score(); + json fields = json::object(); + for (const auto& name : doc.field_names()) { + const FieldSchema* field = schema.get_field(name); + if (field) { + fields[name] = field_to_json(doc, name, field->data_type()); + } + } + j["fields"] = fields; + return j; +} + +static json write_results_to_json(const WriteResults& results, + const std::vector& docs) { + json arr = json::array(); + for (size_t i = 0; i < results.size(); ++i) { + json r; + r["pk"] = (i < docs.size()) ? docs[i].pk() : ""; + r["code"] = static_cast(results[i].code()); + r["message"] = results[i].message(); + arr.push_back(r); + } + return arr; +} + +// ============================================================================ +// C API implementation +// ============================================================================ + +extern "C" { + +zvec_status_t zvec_create_and_open(const char* path, const char* schema_json, + zvec_collection_t* out) { + if (!path || !schema_json || !out) + return make_error(Status::InvalidArgument("null argument")); + try { + CollectionSchema schema = parse_schema(json::parse(schema_json)); + auto result = Collection::CreateAndOpen(path, schema, CollectionOptions{}); + if (!result) return make_error(result.error()); + *out = static_cast( + new Collection::Ptr(std::move(*result))); + return make_ok(); + } catch (const std::exception& e) { + return make_exception_error(e); + } +} + +zvec_status_t zvec_open(const char* path, zvec_collection_t* out) { + if (!path || !out) + return make_error(Status::InvalidArgument("null argument")); + try { + auto result = Collection::Open(path, CollectionOptions{}); + if (!result) return make_error(result.error()); + *out = static_cast( + new Collection::Ptr(std::move(*result))); + return make_ok(); + } catch (const std::exception& e) { + return make_exception_error(e); + } +} + +void zvec_collection_free(zvec_collection_t col) { + delete static_cast(col); +} + +zvec_status_t zvec_collection_destroy(zvec_collection_t col) { + if (!col) return make_error(Status::InvalidArgument("null collection")); + try { + return make_error((*static_cast(col))->Destroy()); + } catch (const std::exception& e) { + return make_exception_error(e); + } +} + +zvec_status_t zvec_collection_flush(zvec_collection_t col) { + if (!col) return make_error(Status::InvalidArgument("null collection")); + try { + return make_error((*static_cast(col))->Flush()); + } catch (const std::exception& e) { + return make_exception_error(e); + } +} + +// ── DML ────────────────────────────────────────────────────────────────────── + +enum class WriteOp { INSERT, UPSERT, UPDATE }; + +static zvec_status_t do_write(zvec_collection_t col, const char* docs_json, + char** results_json, WriteOp op) { + if (!col || !docs_json) + return make_error(Status::InvalidArgument("null argument")); + try { + auto& collection = *static_cast(col); + auto schema_result = collection->Schema(); + if (!schema_result) return make_error(schema_result.error()); + const CollectionSchema& schema = *schema_result; + + auto docs = parse_docs(json::parse(docs_json), schema); + + Result result{tl::unexpect, + Status::InternalError("uninitialized")}; + switch (op) { + case WriteOp::INSERT: + result = collection->Insert(docs); + break; + case WriteOp::UPSERT: + result = collection->Upsert(docs); + break; + case WriteOp::UPDATE: + result = collection->Update(docs); + break; + } + if (!result) return make_error(result.error()); + + if (results_json) { + *results_json = strdup_new(write_results_to_json(*result, docs).dump()); + } + return make_ok(); + } catch (const std::exception& e) { + return make_exception_error(e); + } +} + +zvec_status_t zvec_insert(zvec_collection_t col, const char* docs_json, + char** results_json) { + return do_write(col, docs_json, results_json, WriteOp::INSERT); +} + +zvec_status_t zvec_upsert(zvec_collection_t col, const char* docs_json, + char** results_json) { + return do_write(col, docs_json, results_json, WriteOp::UPSERT); +} + +zvec_status_t zvec_update(zvec_collection_t col, const char* docs_json, + char** results_json) { + return do_write(col, docs_json, results_json, WriteOp::UPDATE); +} + +zvec_status_t zvec_delete_by_pks(zvec_collection_t col, const char* pks_json, + char** results_json) { + if (!col || !pks_json) + return make_error(Status::InvalidArgument("null argument")); + try { + auto& collection = *static_cast(col); + auto pks = json::parse(pks_json).get>(); + + auto result = collection->Delete(pks); + if (!result) return make_error(result.error()); + + if (results_json) { + json arr = json::array(); + for (size_t i = 0; i < result->size(); ++i) { + arr.push_back({{"pk", i < pks.size() ? pks[i] : ""}, + {"code", static_cast((*result)[i].code())}, + {"message", (*result)[i].message()}}); + } + *results_json = strdup_new(arr.dump()); + } + return make_ok(); + } catch (const std::exception& e) { + return make_exception_error(e); + } +} + +zvec_status_t zvec_delete_by_filter(zvec_collection_t col, + const char* filter) { + if (!col || !filter) + return make_error(Status::InvalidArgument("null argument")); + try { + return make_error( + (*static_cast(col))->DeleteByFilter(filter)); + } catch (const std::exception& e) { + return make_exception_error(e); + } +} + +// ── DQL ────────────────────────────────────────────────────────────────────── + +zvec_status_t zvec_query(zvec_collection_t col, const char* query_json, + char** results_json) { + if (!col || !query_json) + return make_error(Status::InvalidArgument("null argument")); + try { + auto& collection = *static_cast(col); + auto schema_result = collection->Schema(); + if (!schema_result) return make_error(schema_result.error()); + const CollectionSchema& schema = *schema_result; + + const auto q = json::parse(query_json); + + VectorQuery query; + query.field_name_ = q["field_name"].get(); + query.topk_ = q["topk"].get(); + + auto floats = q["vector"].get>(); + const FieldSchema* vfield = schema.get_vector_field(query.field_name_); + if (vfield && vfield->data_type() == DataType::VECTOR_FP16) { + std::vector fp16(floats.size()); + for (size_t i = 0; i < floats.size(); ++i) fp16[i] = float16_t(floats[i]); + query.query_vector_ = std::string( + reinterpret_cast(fp16.data()), + fp16.size() * sizeof(float16_t)); + } else { + query.query_vector_ = std::string( + reinterpret_cast(floats.data()), + floats.size() * sizeof(float)); + } + + if (q.contains("filter") && q["filter"].is_string()) { + query.filter_ = q["filter"].get(); + } + if (q.contains("include_vector")) { + query.include_vector_ = q["include_vector"].get(); + } + if (q.contains("output_fields") && q["output_fields"].is_array()) { + query.output_fields_ = + q["output_fields"].get>(); + } + + auto result = collection->Query(query); + if (!result) return make_error(result.error()); + + json arr = json::array(); + for (const auto& doc_ptr : *result) { + if (doc_ptr) arr.push_back(doc_to_json(*doc_ptr, schema)); + } + if (results_json) *results_json = strdup_new(arr.dump()); + return make_ok(); + } catch (const std::exception& e) { + return make_exception_error(e); + } +} + +zvec_status_t zvec_fetch(zvec_collection_t col, const char* pks_json, + char** results_json) { + if (!col || !pks_json) + return make_error(Status::InvalidArgument("null argument")); + try { + auto& collection = *static_cast(col); + auto schema_result = collection->Schema(); + if (!schema_result) return make_error(schema_result.error()); + const CollectionSchema& schema = *schema_result; + + auto pks = json::parse(pks_json).get>(); + auto result = collection->Fetch(pks); + if (!result) return make_error(result.error()); + + json arr = json::array(); + for (const auto& [pk, doc_ptr] : *result) { + if (doc_ptr) arr.push_back(doc_to_json(*doc_ptr, schema)); + } + if (results_json) *results_json = strdup_new(arr.dump()); + return make_ok(); + } catch (const std::exception& e) { + return make_exception_error(e); + } +} + +zvec_status_t zvec_sparse_query(zvec_collection_t col, const char* query_json, + char** results_json) { + if (!col || !query_json) + return make_error(Status::InvalidArgument("null argument")); + try { + auto& collection = *static_cast(col); + auto schema_result = collection->Schema(); + if (!schema_result) return make_error(schema_result.error()); + const CollectionSchema& schema = *schema_result; + + const auto q = json::parse(query_json); + + VectorQuery query; + query.field_name_ = q["field_name"].get(); + query.topk_ = q["topk"].get(); + + // Pack sparse indices as raw uint32_t bytes. + auto indices = q["indices"].get>(); + query.query_sparse_indices_ = std::string( + reinterpret_cast(indices.data()), + indices.size() * sizeof(uint32_t)); + + // Pack sparse values as raw float bytes, converting to fp16 if needed. + const FieldSchema* vfield = schema.get_vector_field(query.field_name_); + auto fvals = q["values"].get>(); + if (vfield && vfield->data_type() == DataType::SPARSE_VECTOR_FP16) { + std::vector fp16(fvals.size()); + for (size_t i = 0; i < fvals.size(); ++i) fp16[i] = float16_t(fvals[i]); + query.query_sparse_values_ = std::string( + reinterpret_cast(fp16.data()), + fp16.size() * sizeof(float16_t)); + } else { + query.query_sparse_values_ = std::string( + reinterpret_cast(fvals.data()), + fvals.size() * sizeof(float)); + } + + if (q.contains("filter") && q["filter"].is_string()) { + query.filter_ = q["filter"].get(); + } + if (q.contains("include_vector")) { + query.include_vector_ = q["include_vector"].get(); + } + if (q.contains("output_fields") && q["output_fields"].is_array()) { + query.output_fields_ = + q["output_fields"].get>(); + } + + auto result = collection->Query(query); + if (!result) return make_error(result.error()); + + json arr = json::array(); + for (const auto& doc_ptr : *result) { + if (doc_ptr) arr.push_back(doc_to_json(*doc_ptr, schema)); + } + if (results_json) *results_json = strdup_new(arr.dump()); + return make_ok(); + } catch (const std::exception& e) { + return make_exception_error(e); + } +} + +// ── DDL ────────────────────────────────────────────────────────────────────── + +zvec_status_t zvec_create_index(zvec_collection_t col, + const char* column_name, + const char* index_params_json) { + if (!col || !column_name || !index_params_json) + return make_error(Status::InvalidArgument("null argument")); + try { + auto& collection = *static_cast(col); + auto params = parse_index_params(json::parse(index_params_json)); + if (!params) + return make_error(Status::InvalidArgument("unknown index type")); + return make_error(collection->CreateIndex(column_name, params)); + } catch (const std::exception& e) { + return make_exception_error(e); + } +} + +zvec_status_t zvec_drop_index(zvec_collection_t col, + const char* column_name) { + if (!col || !column_name) + return make_error(Status::InvalidArgument("null argument")); + try { + return make_error( + (*static_cast(col))->DropIndex(column_name)); + } catch (const std::exception& e) { + return make_exception_error(e); + } +} + +zvec_status_t zvec_add_column(zvec_collection_t col, + const char* field_schema_json, + const char* expression) { + if (!col || !field_schema_json) + return make_error(Status::InvalidArgument("null argument")); + try { + auto& collection = *static_cast(col); + auto field = parse_field_schema(json::parse(field_schema_json)); + std::string expr = expression ? expression : ""; + return make_error(collection->AddColumn(field, expr)); + } catch (const std::exception& e) { + return make_exception_error(e); + } +} + +zvec_status_t zvec_drop_column(zvec_collection_t col, + const char* column_name) { + if (!col || !column_name) + return make_error(Status::InvalidArgument("null argument")); + try { + return make_error( + (*static_cast(col))->DropColumn(column_name)); + } catch (const std::exception& e) { + return make_exception_error(e); + } +} + +zvec_status_t zvec_alter_column(zvec_collection_t col, + const char* column_name, const char* new_name, + const char* field_schema_json) { + if (!col || !column_name) + return make_error(Status::InvalidArgument("null argument")); + try { + auto& collection = *static_cast(col); + std::string rename = (new_name && *new_name) ? new_name : ""; + FieldSchema::Ptr new_schema; + if (field_schema_json && *field_schema_json) { + new_schema = parse_field_schema(json::parse(field_schema_json)); + } + return make_error(collection->AlterColumn(column_name, rename, new_schema)); + } catch (const std::exception& e) { + return make_exception_error(e); + } +} + +// ── Memory management ──────────────────────────────────────────────────────── + +void zvec_free_string(char* s) { delete[] s; } + +void zvec_status_free(zvec_status_t* status) { + if (status && status->message) { + delete[] status->message; + status->message = nullptr; + } +} + +} // extern "C" diff --git a/src/binding/c/zvec_c.h b/src/binding/c/zvec_c.h new file mode 100644 index 00000000..58605cbc --- /dev/null +++ b/src/binding/c/zvec_c.h @@ -0,0 +1,225 @@ +// Copyright 2025-present the zvec project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/** Opaque handle to an open Collection. */ +typedef void* zvec_collection_t; + +/** + * Status returned from every API call. + * When code == 0 (ZVEC_OK), message is NULL. + * When code != 0, message is a heap-allocated C string. + * Call zvec_free_string(status.message) after reading the error. + */ +typedef struct { + int code; + const char* message; +} zvec_status_t; + +/* Status codes (mirror zvec::StatusCode) */ +#define ZVEC_OK 0 +#define ZVEC_NOT_FOUND 1 +#define ZVEC_ALREADY_EXISTS 2 +#define ZVEC_INVALID_ARGUMENT 3 +#define ZVEC_PERMISSION_DENIED 4 +#define ZVEC_FAILED_PRECONDITION 5 +#define ZVEC_RESOURCE_EXHAUSTED 6 +#define ZVEC_UNAVAILABLE 7 +#define ZVEC_INTERNAL_ERROR 8 +#define ZVEC_NOT_SUPPORTED 9 +#define ZVEC_UNKNOWN 10 + +/* ── Lifecycle ─────────────────────────────────────────────────────────────── + * + * schema_json format: + * { + * "name": "my_collection", + * "fields": [ + * { "name": "embedding", "data_type": "VECTOR_FP32", "dimension": 128, + * "nullable": false, + * "index": { "type": "HNSW", "metric": "L2", "m": 16, + * "ef_construction": 200 } }, + * { "name": "title", "data_type": "STRING", "nullable": false, + * "index": { "type": "INVERT" } }, + * { "name": "score", "data_type": "FLOAT", "nullable": true } + * ] + * } + * + * Supported data_type values: BINARY, STRING, BOOL, INT32, INT64, UINT32, + * UINT64, FLOAT, DOUBLE, VECTOR_FP16, VECTOR_FP32, VECTOR_FP64, + * VECTOR_INT4, VECTOR_INT8, VECTOR_INT16, SPARSE_VECTOR_FP16, + * SPARSE_VECTOR_FP32, ARRAY_BOOL, ARRAY_INT32, ARRAY_INT64, ARRAY_UINT32, + * ARRAY_UINT64, ARRAY_FLOAT, ARRAY_DOUBLE, ARRAY_STRING + * + * Supported index types: HNSW, IVF, FLAT, INVERT + * Supported metrics: L2, IP, COSINE, MIPSL2 + * Supported quantize: FP16, INT8, INT4 (optional field) + */ +zvec_status_t zvec_create_and_open(const char* path, + const char* schema_json, + zvec_collection_t* out); + +zvec_status_t zvec_open(const char* path, zvec_collection_t* out); + +/** Release the in-memory handle without deleting on-disk data. */ +void zvec_collection_free(zvec_collection_t col); + +/** Permanently delete the collection from disk, then free the handle. */ +zvec_status_t zvec_collection_destroy(zvec_collection_t col); + +zvec_status_t zvec_collection_flush(zvec_collection_t col); + +/* ── DML ───────────────────────────────────────────────────────────────────── + * + * docs_json format (array of doc objects): + * [ + * { "pk": "doc_1", + * "fields": { + * "embedding": [0.1, 0.2, 0.3, 0.4], + * "title": "hello", + * "score": 9.5, + * "sparse_field": { "indices": [0, 5], "values": [0.3, 0.7] } + * } + * } + * ] + * + * results_json output (must be freed with zvec_free_string): + * [ { "pk": "doc_1", "code": 0, "message": "" }, ... ] + */ +zvec_status_t zvec_insert(zvec_collection_t col, + const char* docs_json, + char** results_json); + +zvec_status_t zvec_upsert(zvec_collection_t col, + const char* docs_json, + char** results_json); + +zvec_status_t zvec_update(zvec_collection_t col, + const char* docs_json, + char** results_json); + +/** + * pks_json: JSON array of string PKs, e.g. ["doc_1", "doc_2"] + * results_json output: [ { "pk": "doc_1", "code": 0, "message": "" }, ... ] + */ +zvec_status_t zvec_delete_by_pks(zvec_collection_t col, + const char* pks_json, + char** results_json); + +/** filter: SQL-like filter expression, e.g. "category = 'tech'" */ +zvec_status_t zvec_delete_by_filter(zvec_collection_t col, + const char* filter); + +/* ── DQL ───────────────────────────────────────────────────────────────────── + * + * query_json format: + * { + * "field_name": "embedding", + * "vector": [0.1, 0.2, 0.3, 0.4], -- float32 values + * "topk": 10, + * "filter": "category = 'tech'", -- optional + * "include_vector": false, -- optional, default false + * "output_fields": ["title", "score"] -- optional, null = all fields + * } + * + * results_json output (must be freed with zvec_free_string): + * [ { "pk": "doc_1", "score": 0.42, "fields": { "title": "hello" } }, ... ] + */ +zvec_status_t zvec_query(zvec_collection_t col, + const char* query_json, + char** results_json); + +/** + * pks_json: JSON array of string PKs. + * results_json output: same format as zvec_query results. + */ +zvec_status_t zvec_fetch(zvec_collection_t col, + const char* pks_json, + char** results_json); + +/** + * Sparse vector similarity query. + * + * query_json format: + * { + * "field_name": "sparse_emb", + * "indices": [101, 205, 307], -- uint32 token indices + * "values": [0.9, 0.6, 0.4], -- float32 scores (same length as indices) + * "topk": 10, + * "filter": "kind = 'function'", -- optional scalar pre-filter + * "output_fields": ["name"] -- optional, null = all fields + * } + * + * The field named by "field_name" must be SPARSE_VECTOR_FP32 or + * SPARSE_VECTOR_FP16 with a FLAT or HNSW_SPARSE index. + * + * results_json output: same format as zvec_query. + */ +zvec_status_t zvec_sparse_query(zvec_collection_t col, + const char* query_json, + char** results_json); + +/* ── DDL ───────────────────────────────────────────────────────────────────── + * + * index_params_json: same as the "index" object in field schema, e.g. + * { "type": "HNSW", "metric": "L2", "m": 16, "ef_construction": 200 } + * { "type": "FLAT", "metric": "COSINE" } + * { "type": "IVF", "metric": "L2", "n_list": 1024 } + * { "type": "INVERT", "enable_range_optimization": true } + */ +zvec_status_t zvec_create_index(zvec_collection_t col, + const char* column_name, + const char* index_params_json); + +zvec_status_t zvec_drop_index(zvec_collection_t col, + const char* column_name); + +/** + * field_schema_json: single field object (same format as entries in + * schema.fields[]). expression: default-value expression (may be ""). + */ +zvec_status_t zvec_add_column(zvec_collection_t col, + const char* field_schema_json, + const char* expression); + +zvec_status_t zvec_drop_column(zvec_collection_t col, + const char* column_name); + +/** + * new_name: new column name, or NULL / "" to keep current name. + * field_schema_json: updated schema, or NULL / "" to keep current schema. + */ +zvec_status_t zvec_alter_column(zvec_collection_t col, + const char* column_name, + const char* new_name, + const char* field_schema_json); + +/* ── Memory management ───────────────────────────────────────────────────── */ + +/** Free a string returned by any API function. */ +void zvec_free_string(char* s); + +/** Free the message inside a non-OK status. Safe to call on OK statuses. */ +void zvec_status_free(zvec_status_t* status); + +#ifdef __cplusplus +} +#endif From 6eb838dee7a83810ac161f4a5a5dd810f92f43bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Donny/=EA=B0=95=EB=8F=99=EC=9C=A4?= Date: Fri, 13 Mar 2026 12:19:10 +0900 Subject: [PATCH 2/3] fix(locking): prevent lock fd inheritance and add read-only C open (#2) ## Summary - set close-on-exec for file open/create paths to prevent lock FD inheritance across exec - add `zvec_open_read_only(const char* path, zvec_collection_t* out)` C API and open collection with `read_only_=true` - add regression tests for CLOEXEC behavior and lock FD inheritance across fork+exec - fix the lock-inheritance regression test to move ownership from the create result and avoid shared ownership false negatives ## Testing - `./build-1349/bin/file_test --gtest_filter=File.CreateAndOpen_SetsCloseOnExecFlag` - `./build-1349/bin/collection_test --gtest_filter=CollectionTest.Feature_LockFdIsNotInheritedAcrossExec` --- src/ailego/io/file.cc | 50 ++++++- src/binding/c/zvec_c.cc | 270 ++++++++++++++++++----------------- src/binding/c/zvec_c.h | 83 +++++------ tests/ailego/io/file_test.cc | 30 ++++ tests/db/collection_test.cc | 51 ++++++- 5 files changed, 307 insertions(+), 177 deletions(-) diff --git a/src/ailego/io/file.cc b/src/ailego/io/file.cc index 79e58a1c..c8eccad0 100644 --- a/src/ailego/io/file.cc +++ b/src/ailego/io/file.cc @@ -39,6 +39,50 @@ static inline int OpenSafely(const char *path, int flags) { return fd; } +static inline bool SetCloseOnExec(int fd) { + int current_flags = fcntl(fd, F_GETFD); + while (current_flags == -1 && errno == EINTR) { + current_flags = fcntl(fd, F_GETFD); + } + if (current_flags == -1) { + return false; + } + if ((current_flags & FD_CLOEXEC) != 0) { + return true; + } + + int ret = fcntl(fd, F_SETFD, current_flags | FD_CLOEXEC); + while (ret == -1 && errno == EINTR) { + ret = fcntl(fd, F_SETFD, current_flags | FD_CLOEXEC); + } + return ret == 0; +} + +static inline int OpenWithCloseOnExec(const char *path, int flags) { +#ifdef O_CLOEXEC + int cloexec_fd = OpenSafely(path, flags | O_CLOEXEC); + if (cloexec_fd != -1) { + return cloexec_fd; + } + if (errno != EINVAL) { + return -1; + } +#endif + + int fd = OpenSafely(path, flags); + if (fd == -1) { + return -1; + } + if (!SetCloseOnExec(fd)) { + int ret = close(fd); + while (ret == -1 && errno == EINTR) { + ret = close(fd); + } + return -1; + } + return fd; +} + static inline void CloseSafely(int fd) { int ret = close(fd); while (ret == -1 && errno == EINTR) { @@ -144,7 +188,7 @@ bool File::create(const char *path, size_t len, bool direct) { (void)direct; #endif - int fd = OpenSafely(path, flags); + int fd = OpenWithCloseOnExec(path, flags); ailego_false_if_lt_zero(fd); #ifdef F_NOCACHE @@ -178,7 +222,7 @@ bool File::open(const char *path, bool rdonly, bool direct) { (void)direct; #endif - int fd = OpenSafely(path, flags); + int fd = OpenWithCloseOnExec(path, flags); ailego_false_if_lt_zero(fd); #ifdef F_NOCACHE @@ -724,4 +768,4 @@ void File::MemoryWarmup(void *addr, size_t len) { } } // namespace ailego -} // namespace zvec \ No newline at end of file +} // namespace zvec diff --git a/src/binding/c/zvec_c.cc b/src/binding/c/zvec_c.cc index 7d811d17..450eb5be 100644 --- a/src/binding/c/zvec_c.cc +++ b/src/binding/c/zvec_c.cc @@ -13,12 +13,11 @@ // limitations under the License. #include "zvec_c.h" - #include #include #include #include - +#include #include #include #include @@ -28,8 +27,6 @@ #include #include -#include - using json = nlohmann::json; using namespace zvec; @@ -37,25 +34,27 @@ using namespace zvec; // Internal helpers: status // ============================================================================ -static zvec_status_t make_ok() { return {0, nullptr}; } +static zvec_status_t make_ok() { + return {0, nullptr}; +} -static zvec_status_t make_error(const Status& s) { +static zvec_status_t make_error(const Status &s) { if (s.ok()) return make_ok(); - const std::string& msg = s.message(); - char* buf = new char[msg.size() + 1]; + const std::string &msg = s.message(); + char *buf = new char[msg.size() + 1]; std::memcpy(buf, msg.c_str(), msg.size() + 1); return {static_cast(s.code()), buf}; } -static zvec_status_t make_exception_error(const std::exception& e) { +static zvec_status_t make_exception_error(const std::exception &e) { std::string msg = e.what(); - char* buf = new char[msg.size() + 1]; + char *buf = new char[msg.size() + 1]; std::memcpy(buf, msg.c_str(), msg.size() + 1); return {ZVEC_UNKNOWN, buf}; } -static char* strdup_new(const std::string& s) { - char* buf = new char[s.size() + 1]; +static char *strdup_new(const std::string &s) { + char *buf = new char[s.size() + 1]; std::memcpy(buf, s.c_str(), s.size() + 1); return buf; } @@ -64,7 +63,7 @@ static char* strdup_new(const std::string& s) { // Internal helpers: type parsing // ============================================================================ -static DataType parse_data_type(const std::string& s) { +static DataType parse_data_type(const std::string &s) { if (s == "BINARY") return DataType::BINARY; if (s == "STRING") return DataType::STRING; if (s == "BOOL") return DataType::BOOL; @@ -96,7 +95,7 @@ static DataType parse_data_type(const std::string& s) { return DataType::UNDEFINED; } -static MetricType parse_metric_type(const std::string& s) { +static MetricType parse_metric_type(const std::string &s) { if (s == "L2") return MetricType::L2; if (s == "IP") return MetricType::IP; if (s == "COSINE") return MetricType::COSINE; @@ -104,7 +103,7 @@ static MetricType parse_metric_type(const std::string& s) { return MetricType::UNDEFINED; } -static QuantizeType parse_quantize_type(const json& j) { +static QuantizeType parse_quantize_type(const json &j) { if (!j.contains("quantize")) return QuantizeType::UNDEFINED; const std::string s = j["quantize"].get(); if (s == "FP16") return QuantizeType::FP16; @@ -113,7 +112,7 @@ static QuantizeType parse_quantize_type(const json& j) { return QuantizeType::UNDEFINED; } -static IndexParams::Ptr parse_index_params(const json& j) { +static IndexParams::Ptr parse_index_params(const json &j) { std::string type = j["type"].get(); if (type == "HNSW") { auto metric = parse_metric_type(j.value("metric", std::string("L2"))); @@ -148,7 +147,7 @@ static IndexParams::Ptr parse_index_params(const json& j) { // Internal helpers: schema parsing // ============================================================================ -static FieldSchema::Ptr parse_field_schema(const json& j) { +static FieldSchema::Ptr parse_field_schema(const json &j) { std::string name = j["name"].get(); DataType data_type = parse_data_type(j["data_type"].get()); bool nullable = j.value("nullable", false); @@ -161,9 +160,9 @@ static FieldSchema::Ptr parse_field_schema(const json& j) { index_params); } -static CollectionSchema parse_schema(const json& j) { +static CollectionSchema parse_schema(const json &j) { CollectionSchema schema(j["name"].get()); - for (const auto& field_j : j["fields"]) { + for (const auto &field_j : j["fields"]) { schema.add_field(parse_field_schema(field_j)); } return schema; @@ -173,7 +172,7 @@ static CollectionSchema parse_schema(const json& j) { // Internal helpers: Doc serialization/deserialization // ============================================================================ -static void set_doc_field(Doc& doc, const std::string& name, const json& val, +static void set_doc_field(Doc &doc, const std::string &name, const json &val, DataType dt) { if (val.is_null()) { doc.set_null(name); @@ -266,18 +265,18 @@ static void set_doc_field(Doc& doc, const std::string& name, const json& val, } } -static std::vector parse_docs(const json& docs_j, - const CollectionSchema& schema) { +static std::vector parse_docs(const json &docs_j, + const CollectionSchema &schema) { std::vector docs; docs.reserve(docs_j.size()); - for (const auto& doc_j : docs_j) { + for (const auto &doc_j : docs_j) { Doc doc; if (doc_j.contains("pk")) { doc.set_pk(doc_j["pk"].get()); } if (doc_j.contains("fields")) { - for (const auto& [field_name, value_j] : doc_j["fields"].items()) { - const FieldSchema* field = schema.get_field(field_name); + for (const auto &[field_name, value_j] : doc_j["fields"].items()) { + const FieldSchema *field = schema.get_field(field_name); if (!field) continue; set_doc_field(doc, field_name, value_j, field->data_type()); } @@ -287,7 +286,7 @@ static std::vector parse_docs(const json& docs_j, return docs; } -static json field_to_json(const Doc& doc, const std::string& name, +static json field_to_json(const Doc &doc, const std::string &name, DataType dt) { switch (dt) { case DataType::BOOL: { @@ -350,8 +349,9 @@ static json field_to_json(const Doc& doc, const std::string& name, return json{{"indices", v->first}, {"values", v->second}}; } case DataType::SPARSE_VECTOR_FP16: { - auto v = doc.get< - std::pair, std::vector>>(name); + auto v = + doc.get, std::vector>>( + name); if (!v) return json(nullptr); std::vector fvals(v->second.size()); for (size_t i = 0; i < v->second.size(); ++i) @@ -387,13 +387,13 @@ static json field_to_json(const Doc& doc, const std::string& name, } } -static json doc_to_json(const Doc& doc, const CollectionSchema& schema) { +static json doc_to_json(const Doc &doc, const CollectionSchema &schema) { json j; j["pk"] = doc.pk(); j["score"] = doc.score(); json fields = json::object(); - for (const auto& name : doc.field_names()) { - const FieldSchema* field = schema.get_field(name); + for (const auto &name : doc.field_names()) { + const FieldSchema *field = schema.get_field(name); if (field) { fields[name] = field_to_json(doc, name, field->data_type()); } @@ -402,8 +402,8 @@ static json doc_to_json(const Doc& doc, const CollectionSchema& schema) { return j; } -static json write_results_to_json(const WriteResults& results, - const std::vector& docs) { +static json write_results_to_json(const WriteResults &results, + const std::vector &docs) { json arr = json::array(); for (size_t i = 0; i < results.size(); ++i) { json r; @@ -421,45 +421,61 @@ static json write_results_to_json(const WriteResults& results, extern "C" { -zvec_status_t zvec_create_and_open(const char* path, const char* schema_json, - zvec_collection_t* out) { +zvec_status_t zvec_create_and_open(const char *path, const char *schema_json, + zvec_collection_t *out) { if (!path || !schema_json || !out) return make_error(Status::InvalidArgument("null argument")); try { CollectionSchema schema = parse_schema(json::parse(schema_json)); auto result = Collection::CreateAndOpen(path, schema, CollectionOptions{}); if (!result) return make_error(result.error()); - *out = static_cast( - new Collection::Ptr(std::move(*result))); + *out = + static_cast(new Collection::Ptr(std::move(*result))); return make_ok(); - } catch (const std::exception& e) { + } catch (const std::exception &e) { return make_exception_error(e); } } -zvec_status_t zvec_open(const char* path, zvec_collection_t* out) { +zvec_status_t zvec_open(const char *path, zvec_collection_t *out) { if (!path || !out) return make_error(Status::InvalidArgument("null argument")); try { auto result = Collection::Open(path, CollectionOptions{}); if (!result) return make_error(result.error()); - *out = static_cast( - new Collection::Ptr(std::move(*result))); + *out = + static_cast(new Collection::Ptr(std::move(*result))); return make_ok(); - } catch (const std::exception& e) { + } catch (const std::exception &e) { + return make_exception_error(e); + } +} + +zvec_status_t zvec_open_read_only(const char *path, zvec_collection_t *out) { + if (!path || !out) + return make_error(Status::InvalidArgument("null argument")); + try { + CollectionOptions options{}; + options.read_only_ = true; + auto result = Collection::Open(path, options); + if (!result) return make_error(result.error()); + *out = + static_cast(new Collection::Ptr(std::move(*result))); + return make_ok(); + } catch (const std::exception &e) { return make_exception_error(e); } } void zvec_collection_free(zvec_collection_t col) { - delete static_cast(col); + delete static_cast(col); } zvec_status_t zvec_collection_destroy(zvec_collection_t col) { if (!col) return make_error(Status::InvalidArgument("null collection")); try { - return make_error((*static_cast(col))->Destroy()); - } catch (const std::exception& e) { + return make_error((*static_cast(col))->Destroy()); + } catch (const std::exception &e) { return make_exception_error(e); } } @@ -467,8 +483,8 @@ zvec_status_t zvec_collection_destroy(zvec_collection_t col) { zvec_status_t zvec_collection_flush(zvec_collection_t col) { if (!col) return make_error(Status::InvalidArgument("null collection")); try { - return make_error((*static_cast(col))->Flush()); - } catch (const std::exception& e) { + return make_error((*static_cast(col))->Flush()); + } catch (const std::exception &e) { return make_exception_error(e); } } @@ -477,15 +493,15 @@ zvec_status_t zvec_collection_flush(zvec_collection_t col) { enum class WriteOp { INSERT, UPSERT, UPDATE }; -static zvec_status_t do_write(zvec_collection_t col, const char* docs_json, - char** results_json, WriteOp op) { +static zvec_status_t do_write(zvec_collection_t col, const char *docs_json, + char **results_json, WriteOp op) { if (!col || !docs_json) return make_error(Status::InvalidArgument("null argument")); try { - auto& collection = *static_cast(col); + auto &collection = *static_cast(col); auto schema_result = collection->Schema(); if (!schema_result) return make_error(schema_result.error()); - const CollectionSchema& schema = *schema_result; + const CollectionSchema &schema = *schema_result; auto docs = parse_docs(json::parse(docs_json), schema); @@ -508,32 +524,32 @@ static zvec_status_t do_write(zvec_collection_t col, const char* docs_json, *results_json = strdup_new(write_results_to_json(*result, docs).dump()); } return make_ok(); - } catch (const std::exception& e) { + } catch (const std::exception &e) { return make_exception_error(e); } } -zvec_status_t zvec_insert(zvec_collection_t col, const char* docs_json, - char** results_json) { +zvec_status_t zvec_insert(zvec_collection_t col, const char *docs_json, + char **results_json) { return do_write(col, docs_json, results_json, WriteOp::INSERT); } -zvec_status_t zvec_upsert(zvec_collection_t col, const char* docs_json, - char** results_json) { +zvec_status_t zvec_upsert(zvec_collection_t col, const char *docs_json, + char **results_json) { return do_write(col, docs_json, results_json, WriteOp::UPSERT); } -zvec_status_t zvec_update(zvec_collection_t col, const char* docs_json, - char** results_json) { +zvec_status_t zvec_update(zvec_collection_t col, const char *docs_json, + char **results_json) { return do_write(col, docs_json, results_json, WriteOp::UPDATE); } -zvec_status_t zvec_delete_by_pks(zvec_collection_t col, const char* pks_json, - char** results_json) { +zvec_status_t zvec_delete_by_pks(zvec_collection_t col, const char *pks_json, + char **results_json) { if (!col || !pks_json) return make_error(Status::InvalidArgument("null argument")); try { - auto& collection = *static_cast(col); + auto &collection = *static_cast(col); auto pks = json::parse(pks_json).get>(); auto result = collection->Delete(pks); @@ -549,34 +565,33 @@ zvec_status_t zvec_delete_by_pks(zvec_collection_t col, const char* pks_json, *results_json = strdup_new(arr.dump()); } return make_ok(); - } catch (const std::exception& e) { + } catch (const std::exception &e) { return make_exception_error(e); } } -zvec_status_t zvec_delete_by_filter(zvec_collection_t col, - const char* filter) { +zvec_status_t zvec_delete_by_filter(zvec_collection_t col, const char *filter) { if (!col || !filter) return make_error(Status::InvalidArgument("null argument")); try { return make_error( - (*static_cast(col))->DeleteByFilter(filter)); - } catch (const std::exception& e) { + (*static_cast(col))->DeleteByFilter(filter)); + } catch (const std::exception &e) { return make_exception_error(e); } } // ── DQL ────────────────────────────────────────────────────────────────────── -zvec_status_t zvec_query(zvec_collection_t col, const char* query_json, - char** results_json) { +zvec_status_t zvec_query(zvec_collection_t col, const char *query_json, + char **results_json) { if (!col || !query_json) return make_error(Status::InvalidArgument("null argument")); try { - auto& collection = *static_cast(col); + auto &collection = *static_cast(col); auto schema_result = collection->Schema(); if (!schema_result) return make_error(schema_result.error()); - const CollectionSchema& schema = *schema_result; + const CollectionSchema &schema = *schema_result; const auto q = json::parse(query_json); @@ -585,17 +600,17 @@ zvec_status_t zvec_query(zvec_collection_t col, const char* query_json, query.topk_ = q["topk"].get(); auto floats = q["vector"].get>(); - const FieldSchema* vfield = schema.get_vector_field(query.field_name_); + const FieldSchema *vfield = schema.get_vector_field(query.field_name_); if (vfield && vfield->data_type() == DataType::VECTOR_FP16) { std::vector fp16(floats.size()); for (size_t i = 0; i < floats.size(); ++i) fp16[i] = float16_t(floats[i]); - query.query_vector_ = std::string( - reinterpret_cast(fp16.data()), - fp16.size() * sizeof(float16_t)); + query.query_vector_ = + std::string(reinterpret_cast(fp16.data()), + fp16.size() * sizeof(float16_t)); } else { - query.query_vector_ = std::string( - reinterpret_cast(floats.data()), - floats.size() * sizeof(float)); + query.query_vector_ = + std::string(reinterpret_cast(floats.data()), + floats.size() * sizeof(float)); } if (q.contains("filter") && q["filter"].is_string()) { @@ -605,58 +620,57 @@ zvec_status_t zvec_query(zvec_collection_t col, const char* query_json, query.include_vector_ = q["include_vector"].get(); } if (q.contains("output_fields") && q["output_fields"].is_array()) { - query.output_fields_ = - q["output_fields"].get>(); + query.output_fields_ = q["output_fields"].get>(); } auto result = collection->Query(query); if (!result) return make_error(result.error()); json arr = json::array(); - for (const auto& doc_ptr : *result) { + for (const auto &doc_ptr : *result) { if (doc_ptr) arr.push_back(doc_to_json(*doc_ptr, schema)); } if (results_json) *results_json = strdup_new(arr.dump()); return make_ok(); - } catch (const std::exception& e) { + } catch (const std::exception &e) { return make_exception_error(e); } } -zvec_status_t zvec_fetch(zvec_collection_t col, const char* pks_json, - char** results_json) { +zvec_status_t zvec_fetch(zvec_collection_t col, const char *pks_json, + char **results_json) { if (!col || !pks_json) return make_error(Status::InvalidArgument("null argument")); try { - auto& collection = *static_cast(col); + auto &collection = *static_cast(col); auto schema_result = collection->Schema(); if (!schema_result) return make_error(schema_result.error()); - const CollectionSchema& schema = *schema_result; + const CollectionSchema &schema = *schema_result; auto pks = json::parse(pks_json).get>(); auto result = collection->Fetch(pks); if (!result) return make_error(result.error()); json arr = json::array(); - for (const auto& [pk, doc_ptr] : *result) { + for (const auto &[pk, doc_ptr] : *result) { if (doc_ptr) arr.push_back(doc_to_json(*doc_ptr, schema)); } if (results_json) *results_json = strdup_new(arr.dump()); return make_ok(); - } catch (const std::exception& e) { + } catch (const std::exception &e) { return make_exception_error(e); } } -zvec_status_t zvec_sparse_query(zvec_collection_t col, const char* query_json, - char** results_json) { +zvec_status_t zvec_sparse_query(zvec_collection_t col, const char *query_json, + char **results_json) { if (!col || !query_json) return make_error(Status::InvalidArgument("null argument")); try { - auto& collection = *static_cast(col); + auto &collection = *static_cast(col); auto schema_result = collection->Schema(); if (!schema_result) return make_error(schema_result.error()); - const CollectionSchema& schema = *schema_result; + const CollectionSchema &schema = *schema_result; const auto q = json::parse(query_json); @@ -666,23 +680,23 @@ zvec_status_t zvec_sparse_query(zvec_collection_t col, const char* query_json, // Pack sparse indices as raw uint32_t bytes. auto indices = q["indices"].get>(); - query.query_sparse_indices_ = std::string( - reinterpret_cast(indices.data()), - indices.size() * sizeof(uint32_t)); + query.query_sparse_indices_ = + std::string(reinterpret_cast(indices.data()), + indices.size() * sizeof(uint32_t)); // Pack sparse values as raw float bytes, converting to fp16 if needed. - const FieldSchema* vfield = schema.get_vector_field(query.field_name_); + const FieldSchema *vfield = schema.get_vector_field(query.field_name_); auto fvals = q["values"].get>(); if (vfield && vfield->data_type() == DataType::SPARSE_VECTOR_FP16) { std::vector fp16(fvals.size()); for (size_t i = 0; i < fvals.size(); ++i) fp16[i] = float16_t(fvals[i]); - query.query_sparse_values_ = std::string( - reinterpret_cast(fp16.data()), - fp16.size() * sizeof(float16_t)); + query.query_sparse_values_ = + std::string(reinterpret_cast(fp16.data()), + fp16.size() * sizeof(float16_t)); } else { - query.query_sparse_values_ = std::string( - reinterpret_cast(fvals.data()), - fvals.size() * sizeof(float)); + query.query_sparse_values_ = + std::string(reinterpret_cast(fvals.data()), + fvals.size() * sizeof(float)); } if (q.contains("filter") && q["filter"].is_string()) { @@ -692,104 +706,102 @@ zvec_status_t zvec_sparse_query(zvec_collection_t col, const char* query_json, query.include_vector_ = q["include_vector"].get(); } if (q.contains("output_fields") && q["output_fields"].is_array()) { - query.output_fields_ = - q["output_fields"].get>(); + query.output_fields_ = q["output_fields"].get>(); } auto result = collection->Query(query); if (!result) return make_error(result.error()); json arr = json::array(); - for (const auto& doc_ptr : *result) { + for (const auto &doc_ptr : *result) { if (doc_ptr) arr.push_back(doc_to_json(*doc_ptr, schema)); } if (results_json) *results_json = strdup_new(arr.dump()); return make_ok(); - } catch (const std::exception& e) { + } catch (const std::exception &e) { return make_exception_error(e); } } // ── DDL ────────────────────────────────────────────────────────────────────── -zvec_status_t zvec_create_index(zvec_collection_t col, - const char* column_name, - const char* index_params_json) { +zvec_status_t zvec_create_index(zvec_collection_t col, const char *column_name, + const char *index_params_json) { if (!col || !column_name || !index_params_json) return make_error(Status::InvalidArgument("null argument")); try { - auto& collection = *static_cast(col); + auto &collection = *static_cast(col); auto params = parse_index_params(json::parse(index_params_json)); if (!params) return make_error(Status::InvalidArgument("unknown index type")); return make_error(collection->CreateIndex(column_name, params)); - } catch (const std::exception& e) { + } catch (const std::exception &e) { return make_exception_error(e); } } -zvec_status_t zvec_drop_index(zvec_collection_t col, - const char* column_name) { +zvec_status_t zvec_drop_index(zvec_collection_t col, const char *column_name) { if (!col || !column_name) return make_error(Status::InvalidArgument("null argument")); try { return make_error( - (*static_cast(col))->DropIndex(column_name)); - } catch (const std::exception& e) { + (*static_cast(col))->DropIndex(column_name)); + } catch (const std::exception &e) { return make_exception_error(e); } } zvec_status_t zvec_add_column(zvec_collection_t col, - const char* field_schema_json, - const char* expression) { + const char *field_schema_json, + const char *expression) { if (!col || !field_schema_json) return make_error(Status::InvalidArgument("null argument")); try { - auto& collection = *static_cast(col); + auto &collection = *static_cast(col); auto field = parse_field_schema(json::parse(field_schema_json)); std::string expr = expression ? expression : ""; return make_error(collection->AddColumn(field, expr)); - } catch (const std::exception& e) { + } catch (const std::exception &e) { return make_exception_error(e); } } -zvec_status_t zvec_drop_column(zvec_collection_t col, - const char* column_name) { +zvec_status_t zvec_drop_column(zvec_collection_t col, const char *column_name) { if (!col || !column_name) return make_error(Status::InvalidArgument("null argument")); try { return make_error( - (*static_cast(col))->DropColumn(column_name)); - } catch (const std::exception& e) { + (*static_cast(col))->DropColumn(column_name)); + } catch (const std::exception &e) { return make_exception_error(e); } } -zvec_status_t zvec_alter_column(zvec_collection_t col, - const char* column_name, const char* new_name, - const char* field_schema_json) { +zvec_status_t zvec_alter_column(zvec_collection_t col, const char *column_name, + const char *new_name, + const char *field_schema_json) { if (!col || !column_name) return make_error(Status::InvalidArgument("null argument")); try { - auto& collection = *static_cast(col); + auto &collection = *static_cast(col); std::string rename = (new_name && *new_name) ? new_name : ""; FieldSchema::Ptr new_schema; if (field_schema_json && *field_schema_json) { new_schema = parse_field_schema(json::parse(field_schema_json)); } return make_error(collection->AlterColumn(column_name, rename, new_schema)); - } catch (const std::exception& e) { + } catch (const std::exception &e) { return make_exception_error(e); } } // ── Memory management ──────────────────────────────────────────────────────── -void zvec_free_string(char* s) { delete[] s; } +void zvec_free_string(char *s) { + delete[] s; +} -void zvec_status_free(zvec_status_t* status) { +void zvec_status_free(zvec_status_t *status) { if (status && status->message) { delete[] status->message; status->message = nullptr; diff --git a/src/binding/c/zvec_c.h b/src/binding/c/zvec_c.h index 58605cbc..780c79af 100644 --- a/src/binding/c/zvec_c.h +++ b/src/binding/c/zvec_c.h @@ -21,7 +21,7 @@ extern "C" { #include /** Opaque handle to an open Collection. */ -typedef void* zvec_collection_t; +typedef void *zvec_collection_t; /** * Status returned from every API call. @@ -31,7 +31,7 @@ typedef void* zvec_collection_t; */ typedef struct { int code; - const char* message; + const char *message; } zvec_status_t; /* Status codes (mirror zvec::StatusCode) */ @@ -73,11 +73,18 @@ typedef struct { * Supported metrics: L2, IP, COSINE, MIPSL2 * Supported quantize: FP16, INT8, INT4 (optional field) */ -zvec_status_t zvec_create_and_open(const char* path, - const char* schema_json, - zvec_collection_t* out); +zvec_status_t zvec_create_and_open(const char *path, const char *schema_json, + zvec_collection_t *out); -zvec_status_t zvec_open(const char* path, zvec_collection_t* out); +zvec_status_t zvec_open(const char *path, zvec_collection_t *out); + +/** + * Open an existing collection in read-only mode. + * + * Multiple read-only handles can coexist with one active writer. + * Write APIs invoked through this handle will return read-only errors. + */ +zvec_status_t zvec_open_read_only(const char *path, zvec_collection_t *out); /** Release the in-memory handle without deleting on-disk data. */ void zvec_collection_free(zvec_collection_t col); @@ -104,29 +111,24 @@ zvec_status_t zvec_collection_flush(zvec_collection_t col); * results_json output (must be freed with zvec_free_string): * [ { "pk": "doc_1", "code": 0, "message": "" }, ... ] */ -zvec_status_t zvec_insert(zvec_collection_t col, - const char* docs_json, - char** results_json); +zvec_status_t zvec_insert(zvec_collection_t col, const char *docs_json, + char **results_json); -zvec_status_t zvec_upsert(zvec_collection_t col, - const char* docs_json, - char** results_json); +zvec_status_t zvec_upsert(zvec_collection_t col, const char *docs_json, + char **results_json); -zvec_status_t zvec_update(zvec_collection_t col, - const char* docs_json, - char** results_json); +zvec_status_t zvec_update(zvec_collection_t col, const char *docs_json, + char **results_json); /** * pks_json: JSON array of string PKs, e.g. ["doc_1", "doc_2"] * results_json output: [ { "pk": "doc_1", "code": 0, "message": "" }, ... ] */ -zvec_status_t zvec_delete_by_pks(zvec_collection_t col, - const char* pks_json, - char** results_json); +zvec_status_t zvec_delete_by_pks(zvec_collection_t col, const char *pks_json, + char **results_json); /** filter: SQL-like filter expression, e.g. "category = 'tech'" */ -zvec_status_t zvec_delete_by_filter(zvec_collection_t col, - const char* filter); +zvec_status_t zvec_delete_by_filter(zvec_collection_t col, const char *filter); /* ── DQL ───────────────────────────────────────────────────────────────────── * @@ -143,17 +145,15 @@ zvec_status_t zvec_delete_by_filter(zvec_collection_t col, * results_json output (must be freed with zvec_free_string): * [ { "pk": "doc_1", "score": 0.42, "fields": { "title": "hello" } }, ... ] */ -zvec_status_t zvec_query(zvec_collection_t col, - const char* query_json, - char** results_json); +zvec_status_t zvec_query(zvec_collection_t col, const char *query_json, + char **results_json); /** * pks_json: JSON array of string PKs. * results_json output: same format as zvec_query results. */ -zvec_status_t zvec_fetch(zvec_collection_t col, - const char* pks_json, - char** results_json); +zvec_status_t zvec_fetch(zvec_collection_t col, const char *pks_json, + char **results_json); /** * Sparse vector similarity query. @@ -173,9 +173,8 @@ zvec_status_t zvec_fetch(zvec_collection_t col, * * results_json output: same format as zvec_query. */ -zvec_status_t zvec_sparse_query(zvec_collection_t col, - const char* query_json, - char** results_json); +zvec_status_t zvec_sparse_query(zvec_collection_t col, const char *query_json, + char **results_json); /* ── DDL ───────────────────────────────────────────────────────────────────── * @@ -185,40 +184,36 @@ zvec_status_t zvec_sparse_query(zvec_collection_t col, * { "type": "IVF", "metric": "L2", "n_list": 1024 } * { "type": "INVERT", "enable_range_optimization": true } */ -zvec_status_t zvec_create_index(zvec_collection_t col, - const char* column_name, - const char* index_params_json); +zvec_status_t zvec_create_index(zvec_collection_t col, const char *column_name, + const char *index_params_json); -zvec_status_t zvec_drop_index(zvec_collection_t col, - const char* column_name); +zvec_status_t zvec_drop_index(zvec_collection_t col, const char *column_name); /** * field_schema_json: single field object (same format as entries in * schema.fields[]). expression: default-value expression (may be ""). */ zvec_status_t zvec_add_column(zvec_collection_t col, - const char* field_schema_json, - const char* expression); + const char *field_schema_json, + const char *expression); -zvec_status_t zvec_drop_column(zvec_collection_t col, - const char* column_name); +zvec_status_t zvec_drop_column(zvec_collection_t col, const char *column_name); /** * new_name: new column name, or NULL / "" to keep current name. * field_schema_json: updated schema, or NULL / "" to keep current schema. */ -zvec_status_t zvec_alter_column(zvec_collection_t col, - const char* column_name, - const char* new_name, - const char* field_schema_json); +zvec_status_t zvec_alter_column(zvec_collection_t col, const char *column_name, + const char *new_name, + const char *field_schema_json); /* ── Memory management ───────────────────────────────────────────────────── */ /** Free a string returned by any API function. */ -void zvec_free_string(char* s); +void zvec_free_string(char *s); /** Free the message inside a non-OK status. Safe to call on OK statuses. */ -void zvec_status_free(zvec_status_t* status); +void zvec_status_free(zvec_status_t *status); #ifdef __cplusplus } diff --git a/tests/ailego/io/file_test.cc b/tests/ailego/io/file_test.cc index b7187361..30c40e12 100644 --- a/tests/ailego/io/file_test.cc +++ b/tests/ailego/io/file_test.cc @@ -15,6 +15,9 @@ #include #include #include +#if !defined(_WIN64) && !defined(_WIN32) +#include +#endif using namespace zvec::ailego; @@ -197,6 +200,33 @@ TEST(File, CreateAndOpen) { File::Delete(file_path); } +#if !defined(_WIN64) && !defined(_WIN32) +TEST(File, CreateAndOpen_SetsCloseOnExecFlag) { + const char *file_path = "file_cloexec_testing.tmp"; + + File::Delete(file_path); + EXPECT_FALSE(File::IsRegular(file_path)); + + { + File file; + ASSERT_TRUE(file.create(file_path, 64)); + const int flags = fcntl(file.native_handle(), F_GETFD); + ASSERT_NE(flags, -1); + EXPECT_NE(flags & FD_CLOEXEC, 0); + } + + { + File file; + ASSERT_TRUE(file.open(file_path, true)); + const int flags = fcntl(file.native_handle(), F_GETFD); + ASSERT_NE(flags, -1); + EXPECT_NE(flags & FD_CLOEXEC, 0); + } + + File::Delete(file_path); +} +#endif + TEST(File, ReadAndWrite) { const char *file_path = "file_read_testing.tmp"; size_t file_size = 2u * 1024u * 1024u + 12u * 1024; diff --git a/tests/db/collection_test.cc b/tests/db/collection_test.cc index e39c8615..cf88d1ee 100644 --- a/tests/db/collection_test.cc +++ b/tests/db/collection_test.cc @@ -14,12 +14,14 @@ #include "zvec/db/collection.h" #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -37,6 +39,10 @@ #include "zvec/db/schema.h" #include "zvec/db/status.h" #include "zvec/db/type.h" +#if !defined(_WIN64) && !defined(_WIN32) +#include +#include +#endif using namespace zvec; using namespace zvec::test; @@ -176,6 +182,49 @@ TEST_F(CollectionTest, Feature_CreateAndOpen_General) { auto col1 = result1.value(); } +#if !defined(_WIN64) && !defined(_WIN32) +TEST_F(CollectionTest, Feature_LockFdIsNotInheritedAcrossExec) { + CollectionOptions options; + options.read_only_ = false; + options.enable_mmap_ = true; + + auto schema = TestHelper::CreateNormalSchema(); + auto created = Collection::CreateAndOpen(col_path, *schema, options); + ASSERT_TRUE(created.has_value()); + auto collection = std::move(created.value()); + + pid_t pid = fork(); + ASSERT_GE(pid, 0); + + if (pid == 0) { + char cmd[] = "sleep 5"; + char *args[] = {const_cast("/bin/sh"), + const_cast("-c"), cmd, nullptr}; + execvp(args[0], args); + _exit(127); + } + + collection.reset(); + + bool reopened_while_child_running = false; + for (int attempt = 0; attempt < 20; ++attempt) { + auto reopened = Collection::Open(col_path, options); + if (reopened.has_value()) { + reopened_while_child_running = true; + reopened.value().reset(); + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + int status = 0; + waitpid(pid, &status, 0); + ASSERT_TRUE(WIFEXITED(status)); + ASSERT_EQ(WEXITSTATUS(status), 0); + ASSERT_TRUE(reopened_while_child_running); +} +#endif + TEST_F(CollectionTest, Feature_CreateAndOpen_Empty) { int doc_count = 0; int loop_count = 100; @@ -4262,4 +4311,4 @@ TEST_F(CollectionTest, CornerCase_CreateIndex) { std::make_shared(MetricType::IP)); ASSERT_FALSE(s.ok()); ASSERT_EQ(s.code(), StatusCode::INVALID_ARGUMENT); -} \ No newline at end of file +} From 27d34acca815817f80671fea899262aa87a3e866 Mon Sep 17 00:00:00 2001 From: DongYun Kang Date: Tue, 17 Mar 2026 08:19:02 +0900 Subject: [PATCH 3/3] revert --- .gitignore | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 42a59140..7c5d5df8 100644 --- a/.gitignore +++ b/.gitignore @@ -46,9 +46,4 @@ npm-debug.log* yarn-debug.log* yarn-error.log* -allure-* - -!build_android.sh - -# Rust crate build artifacts -zvec-rs/target/ \ No newline at end of file +allure-* \ No newline at end of file