#![cfg(feature = "node")]
#![allow(clippy::needless_pass_by_value)]
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::RwLock;
use napi::Result;
use napi_derive::napi;
use crate::ffi_types::{
FfiCompiledGroupedQuery, FfiCompiledQuery, FfiGroupedQueryRows, FfiIntegrityReport,
FfiLastAccessTouchReport, FfiLastAccessTouchRequest, FfiProjectionRepairReport, FfiQueryAst,
FfiQueryPlan, FfiQueryRows, FfiSafeExportManifest, FfiSemanticReport, FfiTraceReport,
FfiWriteReceipt, FfiWriteRequest,
};
use crate::node_types::{
MAX_AST_JSON_BYTES, MAX_REQUEST_JSON_BYTES, MAX_WRITE_JSON_BYTES, check_json_size, encode_json,
invalid_argument, map_admin_ffi_error, map_compile_error, map_engine_error,
map_search_ffi_error, parse_embedder_choice, parse_projection_target, parse_provenance_mode,
parse_telemetry_level,
};
use crate::{
Engine, EngineOptions, OperationalReadRequest, OperationalRegisterRequest, ProjectionTarget,
ProvenancePurgeOptions, SafeExportOptions, compile_grouped_query, compile_query, new_id,
new_row_id,
};
use fathomdb_engine::VectorRegenerationConfig;
#[napi(js_name = "EngineCore")]
pub struct NodeEngineCore {
engine: RwLock<Option<Engine>>,
}
impl NodeEngineCore {
fn with_engine<F, R>(&self, f: F) -> Result<R>
where
F: FnOnce(&Engine) -> Result<R>,
{
let guard = self
.engine
.read()
.map_err(|_| invalid_argument("engine lock poisoned"))?;
match guard.as_ref() {
Some(engine) => f(engine),
None => Err(invalid_argument("engine is closed")),
}
}
}
#[napi]
impl NodeEngineCore {
#[napi(factory)]
pub fn open(
database_path: String,
provenance_mode: String,
vector_dimension: Option<u32>,
telemetry_level: Option<String>,
embedder: Option<String>,
) -> Result<Self> {
let options = EngineOptions {
database_path: PathBuf::from(database_path),
provenance_mode: parse_provenance_mode(&provenance_mode)?,
vector_dimension: vector_dimension.map(|value| value as usize),
read_pool_size: None,
telemetry_level: parse_telemetry_level(telemetry_level.as_deref())?,
embedder: parse_embedder_choice(embedder.as_deref())?,
};
let engine = Engine::open(options).map_err(map_engine_error)?;
Ok(Self {
engine: RwLock::new(Some(engine)),
})
}
#[napi]
pub fn close(&self) -> Result<()> {
let mut guard = self
.engine
.write()
.map_err(|_| invalid_argument("engine lock poisoned"))?;
let _ = guard.take();
Ok(())
}
#[napi]
pub fn telemetry_snapshot(&self) -> Result<String> {
self.with_engine(|engine| {
let snap = engine.telemetry_snapshot();
encode_json(serde_json::json!({
"queries_total": snap.queries_total,
"writes_total": snap.writes_total,
"write_rows_total": snap.write_rows_total,
"errors_total": snap.errors_total,
"admin_ops_total": snap.admin_ops_total,
"cache_hits": snap.sqlite_cache.cache_hits,
"cache_misses": snap.sqlite_cache.cache_misses,
"cache_writes": snap.sqlite_cache.cache_writes,
"cache_spills": snap.sqlite_cache.cache_spills,
}))
})
}
#[allow(clippy::unused_self)]
#[napi]
pub fn compile_ast(&self, ast_json: String) -> Result<String> {
let ast = parse_ast(&ast_json)?;
let compiled = compile_query(&ast).map_err(map_compile_error)?;
encode_json(FfiCompiledQuery::from(compiled))
}
#[allow(clippy::unused_self)]
#[napi]
pub fn compile_grouped_ast(&self, ast_json: String) -> Result<String> {
let ast = parse_ast(&ast_json)?;
let compiled = compile_grouped_query(&ast).map_err(map_compile_error)?;
encode_json(FfiCompiledGroupedQuery::from(compiled))
}
#[napi]
pub fn explain_ast(&self, ast_json: String) -> Result<String> {
let ast = parse_ast(&ast_json)?;
let compiled = compile_query(&ast).map_err(map_compile_error)?;
self.with_engine(|engine| {
let plan = engine.coordinator().explain_compiled_read(&compiled);
encode_json(FfiQueryPlan::from(plan))
})
}
#[napi]
pub fn execute_ast(&self, ast_json: String) -> Result<String> {
let ast = parse_ast(&ast_json)?;
let compiled = compile_query(&ast).map_err(map_compile_error)?;
self.with_engine(|engine| {
let rows = engine
.coordinator()
.execute_compiled_read(&compiled)
.map_err(map_engine_error)?;
encode_json(FfiQueryRows::from(rows))
})
}
#[napi]
pub fn execute_search(&self, request_json: String) -> Result<String> {
self.with_engine(|engine| {
crate::search_ffi::execute_search_json(engine, &request_json)
.map_err(map_search_ffi_error)
})
}
#[napi]
pub fn execute_grouped_ast(&self, ast_json: String) -> Result<String> {
let ast = parse_ast(&ast_json)?;
let compiled = compile_grouped_query(&ast).map_err(map_compile_error)?;
self.with_engine(|engine| {
let rows = engine
.coordinator()
.execute_compiled_grouped_read(&compiled)
.map_err(map_engine_error)?;
encode_json(FfiGroupedQueryRows::from(rows))
})
}
#[napi]
pub fn submit_write(&self, request_json: String) -> Result<String> {
let request = parse_write_request(&request_json)?;
self.with_engine(|engine| {
let receipt = engine.writer().submit(request).map_err(map_engine_error)?;
encode_json(FfiWriteReceipt::from(receipt))
})
}
#[napi]
pub fn touch_last_accessed(&self, request_json: String) -> Result<String> {
let request = parse_last_access_touch_request(&request_json)?;
self.with_engine(|engine| {
let report = engine
.touch_last_accessed(request)
.map_err(map_engine_error)?;
encode_json(FfiLastAccessTouchReport::from(report))
})
}
#[napi]
pub fn check_integrity(&self) -> Result<String> {
self.with_engine(|engine| {
let report = engine
.admin()
.service()
.check_integrity()
.map_err(map_engine_error)?;
encode_json(FfiIntegrityReport::from(report))
})
}
#[napi]
pub fn check_semantics(&self) -> Result<String> {
self.with_engine(|engine| {
let report = engine
.admin()
.service()
.check_semantics()
.map_err(map_engine_error)?;
encode_json(FfiSemanticReport::from(report))
})
}
#[napi]
pub fn rebuild_projections(&self, target: String) -> Result<String> {
let target: ProjectionTarget = parse_projection_target(&target)?;
self.with_engine(|engine| {
let report = engine
.admin()
.service()
.rebuild_projections(target)
.map_err(map_engine_error)?;
encode_json(FfiProjectionRepairReport::from(report))
})
}
#[napi]
pub fn rebuild_missing_projections(&self) -> Result<String> {
self.with_engine(|engine| {
let report = engine
.admin()
.service()
.rebuild_missing_projections()
.map_err(map_engine_error)?;
encode_json(FfiProjectionRepairReport::from(report))
})
}
#[napi]
pub fn trace_source(&self, source_ref: String) -> Result<String> {
self.with_engine(|engine| {
let report = engine
.admin()
.service()
.trace_source(&source_ref)
.map_err(map_engine_error)?;
encode_json(FfiTraceReport::from(report))
})
}
#[napi]
pub fn excise_source(&self, source_ref: String) -> Result<String> {
self.with_engine(|engine| {
let report = engine
.admin()
.service()
.excise_source(&source_ref)
.map_err(map_engine_error)?;
encode_json(FfiTraceReport::from(report))
})
}
#[napi]
pub fn restore_logical_id(&self, logical_id: String) -> Result<String> {
self.with_engine(|engine| {
let report = engine
.restore_logical_id(&logical_id)
.map_err(map_engine_error)?;
encode_json(report)
})
}
#[napi]
pub fn purge_logical_id(&self, logical_id: String) -> Result<String> {
self.with_engine(|engine| {
let report = engine
.purge_logical_id(&logical_id)
.map_err(map_engine_error)?;
encode_json(report)
})
}
#[napi]
pub fn safe_export(&self, destination_path: String, force_checkpoint: bool) -> Result<String> {
self.with_engine(|engine| {
let manifest = engine
.admin()
.service()
.safe_export(&destination_path, SafeExportOptions { force_checkpoint })
.map_err(map_engine_error)?;
encode_json(FfiSafeExportManifest::from(manifest))
})
}
#[napi]
pub fn register_fts_property_schema(
&self,
kind: String,
property_paths_json: String,
separator: Option<String>,
) -> Result<String> {
let paths: Vec<String> = serde_json::from_str(&property_paths_json)
.map_err(|error| invalid_argument(format!("invalid property paths JSON: {error}")))?;
self.with_engine(|engine| {
let record = engine
.register_fts_property_schema(&kind, &paths, separator.as_deref())
.map_err(map_engine_error)?;
encode_json(record)
})
}
#[napi]
pub fn register_fts_property_schema_with_entries(
&self,
request_json: String,
) -> Result<String> {
self.with_engine(|engine| {
crate::admin_ffi::register_fts_property_schema_with_entries_json(engine, &request_json)
.map_err(map_admin_ffi_error)
})
}
#[napi]
pub fn register_fts_property_schema_async(
&self,
kind: String,
property_paths_json: String,
separator: Option<String>,
) -> Result<String> {
let paths: Vec<String> = serde_json::from_str(&property_paths_json)
.map_err(|error| invalid_argument(format!("invalid property paths JSON: {error}")))?;
self.with_engine(|engine| {
let record = engine
.register_fts_property_schema_async(&kind, &paths, separator.as_deref())
.map_err(map_engine_error)?;
encode_json(record)
})
}
#[napi]
pub fn get_property_fts_rebuild_progress(&self, kind: String) -> Result<String> {
self.with_engine(|engine| {
let progress = engine
.get_property_fts_rebuild_progress(&kind)
.map_err(map_engine_error)?;
encode_json(progress)
})
}
#[napi]
pub fn describe_fts_property_schema(&self, kind: String) -> Result<String> {
self.with_engine(|engine| {
let record = engine
.describe_fts_property_schema(&kind)
.map_err(map_engine_error)?;
encode_json(record)
})
}
#[napi]
pub fn list_fts_property_schemas(&self) -> Result<String> {
self.with_engine(|engine| {
let records = engine
.list_fts_property_schemas()
.map_err(map_engine_error)?;
encode_json(records)
})
}
#[napi]
pub fn remove_fts_property_schema(&self, kind: String) -> Result<String> {
self.with_engine(|engine| {
engine
.remove_fts_property_schema(&kind)
.map_err(map_engine_error)?;
encode_json(serde_json::json!({"removed": true}))
})
}
#[napi]
pub fn register_operational_collection(&self, request_json: String) -> Result<String> {
check_json_size(
&request_json,
MAX_REQUEST_JSON_BYTES,
"operational collection",
)?;
let request: OperationalRegisterRequest =
serde_json::from_str(&request_json).map_err(|error| {
invalid_argument(format!("invalid operational collection JSON: {error}"))
})?;
self.with_engine(|engine| {
let record = engine
.register_operational_collection(&request)
.map_err(map_engine_error)?;
encode_json(record)
})
}
#[napi]
pub fn describe_operational_collection(&self, name: String) -> Result<String> {
self.with_engine(|engine| {
let record = engine
.describe_operational_collection(&name)
.map_err(map_engine_error)?;
encode_json(record)
})
}
#[napi]
pub fn update_operational_collection_filters(
&self,
name: String,
filter_fields_json: String,
) -> Result<String> {
check_json_size(&filter_fields_json, MAX_REQUEST_JSON_BYTES, "filter fields")?;
self.with_engine(|engine| {
let record = engine
.update_operational_collection_filters(&name, &filter_fields_json)
.map_err(map_engine_error)?;
encode_json(record)
})
}
#[napi]
pub fn update_operational_collection_validation(
&self,
name: String,
validation_json: String,
) -> Result<String> {
check_json_size(&validation_json, MAX_REQUEST_JSON_BYTES, "validation")?;
self.with_engine(|engine| {
let record = engine
.update_operational_collection_validation(&name, &validation_json)
.map_err(map_engine_error)?;
encode_json(record)
})
}
#[napi]
pub fn update_operational_collection_secondary_indexes(
&self,
name: String,
secondary_indexes_json: String,
) -> Result<String> {
check_json_size(
&secondary_indexes_json,
MAX_REQUEST_JSON_BYTES,
"secondary indexes",
)?;
self.with_engine(|engine| {
let record = engine
.update_operational_collection_secondary_indexes(&name, &secondary_indexes_json)
.map_err(map_engine_error)?;
encode_json(record)
})
}
#[napi]
pub fn trace_operational_collection(
&self,
collection_name: String,
record_key: Option<String>,
) -> Result<String> {
self.with_engine(|engine| {
let report = engine
.trace_operational_collection(&collection_name, record_key.as_deref())
.map_err(map_engine_error)?;
encode_json(report)
})
}
#[napi]
pub fn read_operational_collection(&self, request_json: String) -> Result<String> {
check_json_size(&request_json, MAX_REQUEST_JSON_BYTES, "operational read")?;
let request: OperationalReadRequest = serde_json::from_str(&request_json)
.map_err(|error| invalid_argument(format!("invalid operational read JSON: {error}")))?;
self.with_engine(|engine| {
let report = engine
.read_operational_collection(&request)
.map_err(map_engine_error)?;
encode_json(report)
})
}
#[napi]
pub fn rebuild_operational_current(&self, collection_name: Option<String>) -> Result<String> {
self.with_engine(|engine| {
let report = engine
.rebuild_operational_current(collection_name.as_deref())
.map_err(map_engine_error)?;
encode_json(report)
})
}
#[napi]
pub fn validate_operational_collection_history(
&self,
collection_name: String,
) -> Result<String> {
self.with_engine(|engine| {
let report = engine
.validate_operational_collection_history(&collection_name)
.map_err(map_engine_error)?;
encode_json(report)
})
}
#[napi]
pub fn rebuild_operational_secondary_indexes(&self, collection_name: String) -> Result<String> {
self.with_engine(|engine| {
let report = engine
.rebuild_operational_secondary_indexes(&collection_name)
.map_err(map_engine_error)?;
encode_json(report)
})
}
#[napi]
pub fn plan_operational_retention(
&self,
now_timestamp: i64,
collection_names_json: Option<String>,
max_collections: Option<u32>,
) -> Result<String> {
let collection_names: Option<Vec<String>> = collection_names_json
.map(|json| {
serde_json::from_str(&json).map_err(|error| {
invalid_argument(format!("invalid collection_names JSON: {error}"))
})
})
.transpose()?;
self.with_engine(|engine| {
let report = engine
.plan_operational_retention(
now_timestamp,
collection_names.as_deref(),
max_collections.map(|v| v as usize),
)
.map_err(map_engine_error)?;
encode_json(report)
})
}
#[napi]
pub fn run_operational_retention(
&self,
now_timestamp: i64,
collection_names_json: Option<String>,
max_collections: Option<u32>,
dry_run: bool,
) -> Result<String> {
let collection_names: Option<Vec<String>> = collection_names_json
.map(|json| {
serde_json::from_str(&json).map_err(|error| {
invalid_argument(format!("invalid collection_names JSON: {error}"))
})
})
.transpose()?;
self.with_engine(|engine| {
let report = engine
.run_operational_retention(
now_timestamp,
collection_names.as_deref(),
max_collections.map(|v| v as usize),
dry_run,
)
.map_err(map_engine_error)?;
encode_json(report)
})
}
#[napi]
pub fn disable_operational_collection(&self, name: String) -> Result<String> {
self.with_engine(|engine| {
let record = engine
.disable_operational_collection(&name)
.map_err(map_engine_error)?;
encode_json(record)
})
}
#[napi]
pub fn compact_operational_collection(&self, name: String, dry_run: bool) -> Result<String> {
self.with_engine(|engine| {
let report = engine
.compact_operational_collection(&name, dry_run)
.map_err(map_engine_error)?;
encode_json(report)
})
}
#[napi]
pub fn purge_operational_collection(
&self,
name: String,
before_timestamp: i64,
) -> Result<String> {
self.with_engine(|engine| {
let report = engine
.purge_operational_collection(&name, before_timestamp)
.map_err(map_engine_error)?;
encode_json(report)
})
}
#[napi]
pub fn purge_provenance_events(
&self,
before_timestamp: i64,
options_json: String,
) -> Result<String> {
check_json_size(
&options_json,
MAX_REQUEST_JSON_BYTES,
"provenance purge options",
)?;
let options: ProvenancePurgeOptions = serde_json::from_str(&options_json)
.map_err(|error| invalid_argument(format!("invalid options JSON: {error}")))?;
self.with_engine(|engine| {
let report = engine
.purge_provenance_events(before_timestamp, &options)
.map_err(map_engine_error)?;
encode_json(report)
})
}
#[napi]
pub fn set_fts_profile(&self, request_json: String) -> Result<String> {
self.with_engine(|engine| {
crate::admin_ffi::set_fts_profile_json(engine, &request_json)
.map_err(map_admin_ffi_error)
})
}
#[napi]
pub fn get_fts_profile(&self, kind: String) -> Result<String> {
self.with_engine(|engine| {
crate::admin_ffi::get_fts_profile_json(engine, &kind).map_err(map_admin_ffi_error)
})
}
#[napi]
pub fn set_vec_profile(&self, request_json: String) -> Result<String> {
self.with_engine(|engine| {
crate::admin_ffi::set_vec_profile_json(engine, &request_json)
.map_err(map_admin_ffi_error)
})
}
#[napi]
pub fn get_vec_profile(&self, kind: String) -> Result<String> {
self.with_engine(|engine| {
crate::admin_ffi::get_vec_profile_json(engine, &kind).map_err(map_admin_ffi_error)
})
}
#[napi]
pub fn preview_projection_impact(&self, kind: String, facet: String) -> Result<String> {
self.with_engine(|engine| {
crate::admin_ffi::preview_projection_impact_json(engine, &kind, &facet)
.map_err(map_admin_ffi_error)
})
}
#[napi]
pub fn restore_vector_profiles(&self) -> Result<String> {
self.with_engine(|engine| {
let report = engine
.admin()
.service()
.restore_vector_profiles()
.map_err(map_engine_error)?;
encode_json(FfiProjectionRepairReport::from(report))
})
}
#[napi]
pub fn regenerate_vector_embeddings(&self, config_json: String) -> Result<String> {
check_json_size(
&config_json,
MAX_REQUEST_JSON_BYTES,
"vector regeneration config",
)?;
let config: VectorRegenerationConfig = serde_json::from_str(&config_json)
.map_err(|error| invalid_argument(format!("invalid regen config: {error}")))?;
self.with_engine(|engine| {
let report = engine
.regenerate_vector_embeddings(&config)
.map_err(map_engine_error)?;
encode_json(report)
})
}
}
fn parse_ast(ast_json: &str) -> Result<crate::QueryAst> {
check_json_size(ast_json, MAX_AST_JSON_BYTES, "AST")?;
let ast: FfiQueryAst = serde_json::from_str(ast_json)
.map_err(|error| invalid_argument(format!("invalid query AST JSON: {error}")))?;
Ok(ast.into())
}
fn parse_write_request(request_json: &str) -> Result<crate::WriteRequest> {
check_json_size(request_json, MAX_WRITE_JSON_BYTES, "write request")?;
let request: FfiWriteRequest = serde_json::from_str(request_json)
.map_err(|error| invalid_argument(format!("invalid write request JSON: {error}")))?;
Ok(request.into())
}
fn parse_last_access_touch_request(request_json: &str) -> Result<crate::LastAccessTouchRequest> {
check_json_size(
request_json,
MAX_REQUEST_JSON_BYTES,
"lastAccess touch request",
)?;
let request: FfiLastAccessTouchRequest =
serde_json::from_str(request_json).map_err(|error| {
invalid_argument(format!("invalid lastAccess touch request JSON: {error}"))
})?;
Ok(request.into())
}
#[allow(dead_code)]
#[napi(js_name = "newId")]
pub fn js_new_id() -> String {
new_id()
}
#[allow(dead_code)]
#[napi(js_name = "newRowId")]
pub fn js_new_row_id() -> String {
new_row_id()
}
#[allow(dead_code)]
#[napi(js_name = "version")]
pub fn js_version() -> String {
env!("CARGO_PKG_VERSION").to_owned()
}
#[allow(dead_code)]
#[napi(js_name = "listTokenizerPresets")]
pub fn js_list_tokenizer_presets() -> HashMap<String, String> {
fathomdb_engine::TOKENIZER_PRESETS
.iter()
.map(|(name, value)| ((*name).to_owned(), (*value).to_owned()))
.collect()
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::panic)]
mod tests {
use tempfile::NamedTempFile;
use super::{NodeEngineCore, js_list_tokenizer_presets};
#[test]
fn list_tokenizer_presets_matches_engine_constant() {
let presets = js_list_tokenizer_presets();
let expected: std::collections::HashMap<String, String> =
fathomdb_engine::TOKENIZER_PRESETS
.iter()
.map(|(name, value)| ((*name).to_owned(), (*value).to_owned()))
.collect();
assert_eq!(presets, expected);
assert_eq!(presets.len(), 5);
assert_eq!(
presets.get("recall-optimized-english").map(String::as_str),
Some("porter unicode61 remove_diacritics 2")
);
}
#[test]
fn open_constructs_engine_options_with_all_fields() {
let db = NamedTempFile::new().expect("temp db");
let engine = NodeEngineCore::open(
db.path().to_str().expect("db path").to_owned(),
"warn".to_owned(),
None,
None,
None,
);
assert!(engine.is_ok(), "open must succeed: {:?}", engine.err());
}
#[test]
fn close_is_idempotent() {
let db = NamedTempFile::new().expect("temp db");
let engine = NodeEngineCore::open(
db.path().to_str().expect("db path").to_owned(),
"warn".to_owned(),
None,
None,
None,
)
.expect("open");
engine.close().expect("first close");
engine.close().expect("second close");
}
#[test]
fn close_makes_subsequent_calls_fail() {
let db = NamedTempFile::new().expect("temp db");
let engine = NodeEngineCore::open(
db.path().to_str().expect("db path").to_owned(),
"warn".to_owned(),
None,
None,
None,
)
.expect("open");
engine.close().expect("close");
let result = engine.check_integrity();
assert!(result.is_err(), "call after close must fail");
}
#[test]
fn get_fts_profile_returns_null_when_unset() {
let db = NamedTempFile::new().expect("temp db");
let engine = NodeEngineCore::open(
db.path().to_str().expect("db path").to_owned(),
"warn".to_owned(),
None,
None,
None,
)
.expect("open");
let result = engine
.get_fts_profile("Article".to_owned())
.expect("get_fts_profile");
assert_eq!(result, "null", "unset FTS profile must serialize as null");
}
#[test]
fn set_and_get_fts_profile_round_trip() {
let db = NamedTempFile::new().expect("temp db");
let engine = NodeEngineCore::open(
db.path().to_str().expect("db path").to_owned(),
"warn".to_owned(),
None,
None,
None,
)
.expect("open");
let set_result = engine
.set_fts_profile(r#"{"kind":"Article","tokenizer":"unicode61"}"#.to_owned())
.expect("set_fts_profile");
let parsed: serde_json::Value =
serde_json::from_str(&set_result).expect("set result is valid JSON");
assert_eq!(parsed["kind"], "Article");
assert_eq!(parsed["tokenizer"], "unicode61");
let get_result = engine
.get_fts_profile("Article".to_owned())
.expect("get_fts_profile");
let parsed_get: serde_json::Value =
serde_json::from_str(&get_result).expect("get result is valid JSON");
assert_eq!(parsed_get["kind"], "Article");
assert_eq!(parsed_get["tokenizer"], "unicode61");
}
#[test]
fn get_vec_profile_returns_null_when_unset() {
let db = NamedTempFile::new().expect("temp db");
let engine = NodeEngineCore::open(
db.path().to_str().expect("db path").to_owned(),
"warn".to_owned(),
None,
None,
None,
)
.expect("open");
let result = engine
.get_vec_profile("Document".to_owned())
.expect("get_vec_profile");
assert_eq!(result, "null", "unset vec profile must serialize as null");
}
#[test]
fn preview_projection_impact_returns_valid_json() {
let db = NamedTempFile::new().expect("temp db");
let engine = NodeEngineCore::open(
db.path().to_str().expect("db path").to_owned(),
"warn".to_owned(),
None,
None,
None,
)
.expect("open");
let result = engine
.preview_projection_impact("Article".to_owned(), "fts".to_owned())
.expect("preview_projection_impact");
let parsed: serde_json::Value =
serde_json::from_str(&result).expect("impact result is valid JSON");
assert!(
parsed.get("rows_to_rebuild").is_some(),
"must have rows_to_rebuild field"
);
}
#[test]
fn restore_vector_profiles_returns_repair_report_json() {
let db = NamedTempFile::new().expect("temp db");
let engine = NodeEngineCore::open(
db.path().to_str().expect("db path").to_owned(),
"warn".to_owned(),
None,
None,
None,
)
.expect("open");
let result = engine
.restore_vector_profiles()
.expect("restore_vector_profiles");
let parsed: serde_json::Value =
serde_json::from_str(&result).expect("repair report is valid JSON");
assert!(
parsed.get("rebuilt_rows").is_some(),
"must have rebuilt_rows field"
);
}
}