Compare commits

..

10 Commits

Author SHA1 Message Date
Gatefixer 8d3185dec6 fix(node): reject non-ascii cfg keys 2026-08-06 09:24:53 +00:00
Gatefixer bb14667c81 fix(node): reject commented cfg arguments 2026-08-06 09:07:59 +00:00
Gatefixer 983ad2c011 fix(node): reject target feature cfg overrides 2026-08-06 08:35:42 +00:00
Gatefixer bcff95b109 fix(node): reject rustc response files 2026-08-06 08:08:07 +00:00
Gatefixer dfbb5f5ade fix(node): validate underscore rustc options 2026-08-06 07:25:21 +00:00
Gatefixer 12ee5ca626 fix(node): allow default CPU in lint builds 2026-08-06 06:46:48 +00:00
Gatefixer fcef32bfd3 fix(node): validate encoded CPU build flags 2026-08-06 06:35:58 +00:00
Gatefixer 3863189b16 fix(node): validate complete x86-64-v2 feature set 2026-08-06 05:26:53 +00:00
Gatefixer c858e25494 fix(node): enforce x86-64-v2 artifact baseline 2026-08-06 04:48:31 +00:00
Gatefixer cf9a8bf91d fix(node): support pre-Haswell x86_64 CPUs 2026-08-06 04:09:09 +00:00
17 changed files with 585 additions and 120 deletions
+21
View File
@@ -0,0 +1,21 @@
# Keep Node's Linux x64 addons compatible with pre-Haswell CPUs.
# lance-linalg dispatches hot vector kernels to newer SIMD tiers at runtime.
[env]
LANCEDB_NODE_ENFORCE_X86_64_V2 = "1"
[target.x86_64-unknown-linux-gnu]
rustflags = [
"-C",
"target-cpu=x86-64-v2",
"-C",
"target-feature=-avx,-avx2,-fma,-f16c",
]
# Preserve the workspace's dynamic C runtime configuration for musl.
[target.x86_64-unknown-linux-musl]
rustflags = [
"-C",
"target-cpu=x86-64-v2",
"-C",
"target-feature=-crt-static,-avx,-avx2,-fma,-f16c",
]
+4
View File
@@ -12,6 +12,10 @@ categories.workspace = true
[lib]
crate-type = ["cdylib"]
[[test]]
name = "x86_64_v2_build_flags"
path = "build_support/x86_64_v2.rs"
[dependencies]
async-trait.workspace = true
arrow-ipc.workspace = true
+50
View File
@@ -0,0 +1,50 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright The LanceDB Authors
import * as tmp from "tmp";
import { connect } from "../lancedb";
import {
Field,
FixedSizeList,
Float32,
Int32,
Schema,
makeArrowTable,
} from "../lancedb/arrow";
test("cosine vector search runs on the pre-Haswell build baseline", async () => {
const tmpDir = tmp.dirSync({ unsafeCleanup: true });
try {
const db = await connect(tmpDir.name);
const schema = new Schema([
new Field("id", new Int32(), false),
new Field(
"vector",
new FixedSizeList(3, new Field("item", new Float32(), false)),
false,
),
]);
const data = makeArrowTable(
[
{ id: 1, vector: [1, 0, 0] },
{ id: 2, vector: [0, 1, 0] },
],
{ schema },
);
const table = await db.createTable("vectors", data);
const results = await table
.vectorSearch([1, 0, 0])
.distanceType("cosine")
.limit(1)
.toArray();
expect(results).toHaveLength(1);
expect(results[0].id).toBe(1);
expect(results[0]._distance).toBeCloseTo(0);
} finally {
tmpDir.removeCallback();
}
}, 60_000);
+23
View File
@@ -3,6 +3,29 @@
extern crate napi_build;
use std::env;
#[path = "build_support/x86_64_v2.rs"]
mod x86_64_v2;
const ENFORCE_BASELINE: &str = "LANCEDB_NODE_ENFORCE_X86_64_V2";
fn main() {
napi_build::setup();
println!("cargo:rerun-if-env-changed={ENFORCE_BASELINE}");
println!("cargo:rerun-if-env-changed=CARGO_ENCODED_RUSTFLAGS");
let is_linux_x64 = env::var("CARGO_CFG_TARGET_ARCH").as_deref() == Ok("x86_64")
&& env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("linux");
let is_release = env::var("PROFILE").as_deref() == Ok("release");
let is_node_build = env::var(ENFORCE_BASELINE).as_deref() == Ok("1");
if !is_linux_x64 || (!is_release && !is_node_build) {
return;
}
let encoded_rustflags = env::var("CARGO_ENCODED_RUSTFLAGS").unwrap_or_default();
x86_64_v2::validate_encoded_rustflags(&encoded_rustflags).unwrap_or_else(|error| {
panic!("Linux x64 Node addons must use the x86-64-v2 baseline; {error}")
});
}
+430
View File
@@ -0,0 +1,430 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright The LanceDB Authors
use std::collections::{BTreeMap, BTreeSet};
const SAFE_TARGET_CPUS: [&str; 2] = ["x86-64", "x86-64-v2"];
const BASELINE_FEATURES: [&str; 9] = [
"cmpxchg16b",
"fxsr",
"popcnt",
"sse",
"sse2",
"sse3",
"sse4.1",
"sse4.2",
"ssse3",
];
pub(crate) fn validate_encoded_rustflags(encoded: &str) -> Result<(), String> {
let mut target_cpu = None;
let mut feature_states = BTreeMap::new();
let mut required_disables = BTreeSet::new();
let mut unsupported_features = BTreeSet::new();
for option in codegen_options(encoded)? {
let Some((name, value)) = option.split_once('=') else {
continue;
};
let name = name.replace('_', "-");
match name.as_str() {
"target-cpu" => target_cpu = Some(value),
"target-feature" => {
for toggle in value.split(',').filter(|toggle| !toggle.is_empty()) {
let (enabled, feature) = match toggle.as_bytes()[0] {
b'+' => (true, &toggle[1..]),
b'-' => (false, &toggle[1..]),
_ => return Err(format!("invalid target feature flag: {toggle}")),
};
feature_states.insert(feature, enabled);
if enabled && !BASELINE_FEATURES.contains(&feature) {
match feature {
// These are inherited from the workspace configuration and
// explicitly canceled by the Node configuration. Account for
// their implied AVX prerequisite as well as the named feature.
"avx" => {
required_disables.insert("avx");
}
"avx2" => {
required_disables.extend(["avx", "avx2"]);
}
"f16c" => {
required_disables.extend(["avx", "f16c"]);
}
"fma" => {
required_disables.extend(["avx", "fma"]);
}
_ => {
unsupported_features.insert(feature);
}
}
}
}
}
// LLVM arguments can independently alter the target feature set and
// cannot be proven safe by inspecting rustc's target options.
"llvm-args" => return Err("LLVM arguments can override the CPU baseline".to_owned()),
_ => {}
}
}
if let Some(cpu) = target_cpu.filter(|cpu| !SAFE_TARGET_CPUS.contains(cpu)) {
return Err(format!(
"effective target CPU is {}, expected x86-64-v2 or lower",
cpu
));
}
if !unsupported_features.is_empty() {
return Err(format!(
"features above v2: {}",
unsupported_features
.into_iter()
.collect::<Vec<_>>()
.join(", ")
));
}
let not_disabled = required_disables
.into_iter()
.filter(|feature| feature_states.get(feature) != Some(&false))
.collect::<Vec<_>>();
if !not_disabled.is_empty() {
return Err(format!(
"inherited features not fully disabled: {}",
not_disabled.join(", ")
));
}
Ok(())
}
fn codegen_options(encoded: &str) -> Result<Vec<&str>, String> {
let arguments = encoded.split('\u{1f}').collect::<Vec<_>>();
if arguments.iter().any(|argument| argument.starts_with('@')) {
return Err("rustc response-file arguments cannot be validated".to_owned());
}
let mut options = Vec::new();
let mut index = 0;
while index < arguments.len() {
let argument = arguments[index];
if argument == "--cfg" {
index += 1;
let cfg = arguments
.get(index)
.copied()
.ok_or_else(|| "missing value after --cfg".to_owned())?;
reject_builtin_target_feature_cfg(cfg)?;
} else if let Some(cfg) = argument.strip_prefix("--cfg=") {
reject_builtin_target_feature_cfg(cfg)?;
} else if argument == "-C" || argument == "--codegen" {
index += 1;
let option = arguments
.get(index)
.copied()
.ok_or_else(|| format!("missing value after {argument}"))?;
options.push(option.trim_start_matches('='));
} else if let Some(option) = argument.strip_prefix("-C") {
if !option.is_empty() {
options.push(option.trim_start_matches('='));
}
} else if let Some(option) = argument.strip_prefix("--codegen=") {
options.push(option);
}
index += 1;
}
Ok(options)
}
fn reject_builtin_target_feature_cfg(cfg: &str) -> Result<(), String> {
let key = cfg.split_once('=').map_or(cfg, |(key, _)| key);
if key.contains("/*") || key.contains("//") {
return Err("comment-bearing cfgs cannot be validated".to_owned());
}
if !key.is_ascii() {
return Err("non-ASCII cfg keys cannot be validated".to_owned());
}
let name = key.trim();
let name = name.strip_prefix("r#").unwrap_or(name);
if name == "target_feature" {
return Err("built-in target_feature cfgs can override runtime CPU detection".to_owned());
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn encoded(arguments: &[&str]) -> String {
arguments.join("\u{1f}")
}
#[test]
fn accepts_merged_workspace_and_node_flags() {
let flags = encoded(&[
"-C",
"target-cpu=haswell",
"-C",
"target-feature=+avx2,+fma,+f16c",
"-C",
"target-cpu=x86-64-v2",
"-C",
"target-feature=-avx,-avx2,-fma,-f16c",
]);
assert_eq!(validate_encoded_rustflags(&flags), Ok(()));
}
#[test]
fn accepts_non_merging_v2_boundary() {
let flags = encoded(&["-Ctarget-cpu=x86-64-v2"]);
assert_eq!(validate_encoded_rustflags(&flags), Ok(()));
}
#[test]
fn accepts_default_cpu_with_non_codegen_flags() {
let flags = encoded(&["-D", "warnings"]);
assert_eq!(validate_encoded_rustflags(&flags), Ok(()));
}
#[test]
fn accepts_unrelated_custom_cfg() {
let flags = encoded(&["--cfg=tokio_unstable"]);
assert_eq!(validate_encoded_rustflags(&flags), Ok(()));
}
#[test]
fn accepts_comment_like_syntax_in_cfg_value() {
let flags = encoded(&[r#"--cfg=endpoint="https://example.com/*""#]);
assert_eq!(validate_encoded_rustflags(&flags), Ok(()));
}
#[test]
fn accepts_explicit_x86_64_v1_cpu() {
let flags = encoded(&["-Ctarget-cpu=x86-64"]);
assert_eq!(validate_encoded_rustflags(&flags), Ok(()));
}
#[test]
fn accepts_musl_dynamic_crt_configuration() {
let flags = encoded(&[
"-C",
"target-cpu=haswell",
"-C",
"target-feature=-crt-static,+avx2,+fma,+f16c",
"-C",
"target-cpu=x86-64-v2",
"-C",
"target-feature=-crt-static,-avx,-avx2,-fma,-f16c",
]);
assert_eq!(validate_encoded_rustflags(&flags), Ok(()));
}
#[test]
fn rejects_feature_omitted_from_target_cfg() {
let flags = encoded(&["-Ctarget-cpu=x86-64-v2", "-Ctarget-feature=+apxf"]);
assert_eq!(
validate_encoded_rustflags(&flags),
Err("features above v2: apxf".to_owned())
);
}
#[test]
fn rejects_underscore_spelling_above_baseline_feature() {
let flags = encoded(&["-Ctarget_cpu=x86-64-v2", "-Ctarget_feature=+apxf"]);
assert_eq!(
validate_encoded_rustflags(&flags),
Err("features above v2: apxf".to_owned())
);
}
#[test]
fn rejects_unexpected_above_baseline_feature() {
let flags = encoded(&[
"--codegen=target-cpu=x86-64-v2",
"--codegen",
"target-feature=+bmi2",
]);
assert_eq!(
validate_encoded_rustflags(&flags),
Err("features above v2: bmi2".to_owned())
);
}
#[test]
fn rejects_cpu_above_v2() {
let flags = encoded(&["-Ctarget-cpu=haswell"]);
assert_eq!(
validate_encoded_rustflags(&flags),
Err("effective target CPU is haswell, expected x86-64-v2 or lower".to_owned())
);
}
#[test]
fn rejects_incompletely_disabled_feature_implications() {
let flags = encoded(&["-Ctarget-cpu=x86-64-v2", "-Ctarget-feature=+avx2,-avx2"]);
assert_eq!(
validate_encoded_rustflags(&flags),
Err("inherited features not fully disabled: avx".to_owned())
);
}
#[test]
fn rejects_llvm_feature_overrides() {
let flags = encoded(&["-Ctarget-cpu=x86-64-v2", "-Cllvm-args=-mattr=+apxf"]);
assert_eq!(
validate_encoded_rustflags(&flags),
Err("LLVM arguments can override the CPU baseline".to_owned())
);
}
#[test]
fn rejects_underscore_spelling_llvm_feature_overrides() {
let flags = encoded(&["-Ctarget_cpu=x86-64-v2", "-Cllvm_args=-mattr=+apxf"]);
assert_eq!(
validate_encoded_rustflags(&flags),
Err("LLVM arguments can override the CPU baseline".to_owned())
);
}
#[test]
fn rejects_response_file_arguments() {
let flags = encoded(&["-Ctarget-cpu=x86-64-v2", "@flags.rsp"]);
assert_eq!(
validate_encoded_rustflags(&flags),
Err("rustc response-file arguments cannot be validated".to_owned())
);
}
#[test]
fn rejects_split_builtin_target_feature_cfg() {
let flags = encoded(&[
"-Ctarget-cpu=x86-64-v2",
"--cfg",
r#"target_feature="avx2""#,
"-Aexplicit_builtin_cfgs_in_flags",
]);
assert_eq!(
validate_encoded_rustflags(&flags),
Err("built-in target_feature cfgs can override runtime CPU detection".to_owned())
);
}
#[test]
fn rejects_equals_builtin_target_feature_cfg() {
let flags = encoded(&[
"-Ctarget-cpu=x86-64-v2",
r#"--cfg=target_feature="avx2""#,
"-Aexplicit_builtin_cfgs_in_flags",
]);
assert_eq!(
validate_encoded_rustflags(&flags),
Err("built-in target_feature cfgs can override runtime CPU detection".to_owned())
);
}
#[test]
fn rejects_raw_identifier_builtin_target_feature_cfg() {
let flags = encoded(&[
"-Ctarget-cpu=x86-64-v2",
r#"--cfg=r#target_feature="avx2""#,
"-Aexplicit_builtin_cfgs_in_flags",
]);
assert_eq!(
validate_encoded_rustflags(&flags),
Err("built-in target_feature cfgs can override runtime CPU detection".to_owned())
);
}
#[test]
fn rejects_block_comment_after_cfg_name() {
let flags = encoded(&[
"--cfg",
r#"target_feature/*gate*/="avx2""#,
"-Aexplicit_builtin_cfgs_in_flags",
]);
assert_eq!(
validate_encoded_rustflags(&flags),
Err("comment-bearing cfgs cannot be validated".to_owned())
);
}
#[test]
fn rejects_block_comment_before_cfg_name() {
let flags = encoded(&[
r#"--cfg=/*gate*/target_feature="avx2""#,
"-Aexplicit_builtin_cfgs_in_flags",
]);
assert_eq!(
validate_encoded_rustflags(&flags),
Err("comment-bearing cfgs cannot be validated".to_owned())
);
}
#[test]
fn rejects_line_comment_cfg_trivia() {
let flags = encoded(&[
"--cfg",
"target_feature// gate\n=\"avx2\"",
"-Aexplicit_builtin_cfgs_in_flags",
]);
assert_eq!(
validate_encoded_rustflags(&flags),
Err("comment-bearing cfgs cannot be validated".to_owned())
);
}
#[test]
fn rejects_leading_bom_in_cfg_key() {
let flags = encoded(&[
"--cfg",
"\u{feff}target_feature=\"avx2\"",
"-Aexplicit_builtin_cfgs_in_flags",
]);
assert_eq!(
validate_encoded_rustflags(&flags),
Err("non-ASCII cfg keys cannot be validated".to_owned())
);
}
#[test]
fn rejects_non_ascii_pattern_whitespace_in_cfg_key() {
for whitespace in ['\u{200e}', '\u{200f}'] {
let cfg = format!("{whitespace}target_feature=\"avx2\"");
let flags = encoded(&["--cfg", &cfg, "-Aexplicit_builtin_cfgs_in_flags"]);
assert_eq!(
validate_encoded_rustflags(&flags),
Err("non-ASCII cfg keys cannot be validated".to_owned())
);
}
}
}
+37
View File
@@ -0,0 +1,37 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright The LanceDB Authors
const assert = require("node:assert/strict");
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const { connect } = require("../dist");
async function main() {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "lancedb-cpu-"));
try {
const db = await connect(tmpDir);
const table = await db.createTable("vectors", [
{ id: 1, vector: [1, 0, 0] },
{ id: 2, vector: [0, 1, 0] },
]);
const results = await table
.vectorSearch([1, 0, 0])
.distanceType("cosine")
.limit(1)
.toArray();
assert.equal(results.length, 1);
assert.equal(results[0].id, 1);
assert.ok(Math.abs(results[0]._distance) < 1e-6);
} finally {
fs.rmSync(tmpDir, { force: true, recursive: true });
}
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
+1
View File
@@ -88,6 +88,7 @@
"lint-fix": "biome check --write . && biome format --write .",
"prepublishOnly": "napi prepublish -t npm",
"test": "jest --verbose",
"test:pre-haswell": "node ci/pre_haswell_smoke.js",
"integration": "S3_TEST=1 pnpm test",
"universal": "napi universalize",
"version": "napi version"
-1
View File
@@ -269,7 +269,6 @@ class Table:
mode: Literal["append", "overwrite"],
progress: Optional[Any] = None,
write_parallelism: Optional[int] = None,
on_nan_vectors: Optional[Literal["error", "keep"]] = None,
) -> AddResult: ...
async def update(
self, updates: Dict[str, str], where: Optional[str]
+2 -6
View File
@@ -333,9 +333,7 @@ class DBConnection(EnforceOverrides):
schema that's specified.
on_bad_vectors: str, default "error"
What to do if any of the vectors are not the same size or contains NaNs.
One of "error", "drop", "fill", "null", or "keep". With "keep",
vectors containing NaNs are preserved, but vectors with the wrong
dimension still raise an error.
One of "error", "drop", "fill".
fill_value: float
The value to use when filling vectors. Only used if on_bad_vectors="fill".
storage_options: dict, optional
@@ -1597,9 +1595,7 @@ class AsyncConnection(object):
schema that's specified.
on_bad_vectors: str, default "error"
What to do if any of the vectors are not the same size or contains NaNs.
One of "error", "drop", "fill", "null", or "keep". With "keep",
vectors containing NaNs are preserved, but vectors with the wrong
dimension still raise an error.
One of "error", "drop", "fill".
fill_value: float
The value to use when filling vectors. Only used if on_bad_vectors="fill".
storage_options: dict, optional
+1 -3
View File
@@ -175,9 +175,7 @@ class LanceMergeInsertBuilder(object):
can be anything you use for [`add`][lancedb.table.Table.add]
on_bad_vectors: str, default "error"
What to do if any of the vectors are not the same size or contains NaNs.
One of "error", "drop", "fill", "null", or "keep". With "keep",
vectors containing NaNs are preserved, but vectors with the wrong
dimension still raise an error.
One of "error", "drop", "fill".
fill_value: float, default 0.
The value to use when filling vectors. Only used if on_bad_vectors="fill".
timeout: Optional[timedelta], default None
+1 -3
View File
@@ -543,9 +543,7 @@ class RemoteDBConnection(DBConnection):
to "exist_ok".
on_bad_vectors: str, default "error"
What to do if any of the vectors are not the same size or contains NaNs.
One of "error", "drop", "fill", "null", or "keep". With "keep",
vectors containing NaNs are preserved, but vectors with the wrong
dimension still raise an error.
One of "error", "drop", "fill".
fill_value: float
The value to use when filling vectors. Only used if on_bad_vectors="fill".
+1 -3
View File
@@ -629,9 +629,7 @@ class RemoteTable(Table):
"append" and "overwrite".
on_bad_vectors: str, default "error"
What to do if any of the vectors are not the same size or contains NaNs.
One of "error", "drop", "fill", "null", or "keep". With "keep",
vectors containing NaNs are preserved, but vectors with the wrong
dimension still raise an error.
One of "error", "drop", "fill".
fill_value: float, default 0.
The value to use when filling vectors. Only used if on_bad_vectors="fill".
progress: bool, callable, or tqdm-like, optional
+8 -32
View File
@@ -350,10 +350,8 @@ def _sanitize_data(
in the input table before casting.
metadata : Optional[dict], default None
The embedding metadata to add to the schema.
on_bad_vectors : Literal["error", "drop", "fill", "null", "keep"], default "error"
on_bad_vectors : Literal["error", "drop", "fill", "null"], default "error"
What to do if any of the vectors are not the same size or contains NaNs.
With "keep", vectors containing NaNs are preserved, but vectors with the
wrong dimension still raise an error.
fill_value : float, default 0.0
The value to use when filling vectors. Only used if on_bad_vectors="fill".
All entries in the vector will be set to this value.
@@ -1249,9 +1247,7 @@ class Table(ABC):
"append" and "overwrite".
on_bad_vectors: str, default "error"
What to do if any of the vectors are not the same size or contains NaNs.
One of "error", "drop", "fill", "null", or "keep". With "keep",
vectors containing NaNs are preserved but are not indexed for vector
search; vectors with the wrong dimension still raise an error.
One of "error", "drop", "fill".
fill_value: float, default 0.
The value to use when filling vectors. Only used if on_bad_vectors="fill".
progress: bool, callable, or tqdm-like, optional
@@ -3273,9 +3269,7 @@ class LanceTable(Table):
"append" and "overwrite".
on_bad_vectors: str, default "error"
What to do if any of the vectors are not the same size or contains NaNs.
One of "error", "drop", "fill", "null", or "keep". With "keep",
vectors containing NaNs are preserved, but vectors with the wrong
dimension still raise an error.
One of "error", "drop", "fill", "null".
fill_value: float, default 0.
The value to use when filling vectors. Only used if on_bad_vectors="fill".
progress: bool, callable, or tqdm-like, optional
@@ -3588,9 +3582,7 @@ class LanceTable(Table):
data but will validate against any schema that's specified.
on_bad_vectors: str, default "error"
What to do if any of the vectors are not the same size or contains NaNs.
One of "error", "drop", "fill", "null", or "keep". With "keep",
vectors containing NaNs are preserved, but vectors with the wrong
dimension still raise an error.
One of "error", "drop", "fill", "null".
fill_value: float, default 0.
The value to use when filling vectors. Only used if on_bad_vectors="fill".
embedding_functions: list of EmbeddingFunctionModel, default None
@@ -4026,7 +4018,7 @@ class LanceTable(Table):
def _handle_bad_vectors(
reader: pa.RecordBatchReader,
on_bad_vectors: OnBadVectorsType = "error",
on_bad_vectors: Literal["error", "drop", "fill", "null"] = "error",
fill_value: float = 0.0,
target_schema: Optional[pa.Schema] = None,
metadata: Optional[dict] = None,
@@ -4200,9 +4192,7 @@ def _handle_bad_vector_column(
The name of the vector column.
on_bad_vectors: str, default "error"
What to do if any of the vectors are not the same size or contains NaNs.
One of "error", "drop", "fill", "null", or "keep". With "keep",
vectors containing NaNs are preserved, but vectors with the wrong dimension
still raise an error.
One of "error", "drop", "fill", "null".
fill_value: float, default 0.0
The value to use when filling vectors. Only used if on_bad_vectors="fill".
"""
@@ -4267,8 +4257,7 @@ def _handle_bad_vector_column(
f"Vector column '{vector_column_name}' has NaNs. "
"Set on_bad_vectors='drop' to remove them, "
"set on_bad_vectors='fill' and fill_value=<value> to replace them, "
"set on_bad_vectors='null' to replace them with null, "
"or set on_bad_vectors='keep' to preserve them."
"or set on_bad_vectors='null' to replace them with null."
)
elif on_bad_vectors == "null":
vec_arr = pc.if_else(
@@ -4285,16 +4274,6 @@ def _handle_bad_vector_column(
"`fill_value` must not be None if `on_bad_vectors` is 'fill'"
)
vec_arr = _fill_bad_vector_values(vec_arr, dim, fill_value)
elif on_bad_vectors == "keep":
if pc.any(has_wrong_dim).as_py():
raise ValueError(
f"Vector column '{vector_column_name}' has variable length "
"vectors. on_bad_vectors='keep' only preserves vectors "
"containing NaNs. Set on_bad_vectors='drop' to remove "
"wrong-size vectors, set on_bad_vectors='fill' and "
"fill_value=<value> to replace them, or set "
"on_bad_vectors='null' to replace them with null."
)
else:
raise ValueError(f"Invalid value for on_bad_vectors: {on_bad_vectors}")
@@ -5139,9 +5118,7 @@ class AsyncTable:
"append" and "overwrite".
on_bad_vectors: str, default "error"
What to do if any of the vectors are not the same size or contains NaNs.
One of "error", "drop", "fill", "null", or "keep". With "keep",
vectors containing NaNs are preserved but are not indexed for vector
search; vectors with the wrong dimension still raise an error.
One of "error", "drop", "fill", "null".
fill_value: float, default 0.
The value to use when filling vectors. Only used if on_bad_vectors="fill".
progress: callable or tqdm-like, optional
@@ -5189,7 +5166,6 @@ class AsyncTable:
mode or "append",
progress=progress,
write_parallelism=write_parallelism,
on_nan_vectors="keep" if on_bad_vectors == "keep" else None,
)
except RuntimeError as e:
if "Cast error" in str(e):
+1 -1
View File
@@ -24,7 +24,7 @@ DistanceType = Literal["l2", "cosine", "dot"]
DistanceTypeWithHamming = Literal["l2", "cosine", "dot", "hamming"]
# Vector handling literals
OnBadVectorsType = Literal["error", "drop", "fill", "null", "keep"]
OnBadVectorsType = Literal["error", "drop", "fill", "null"]
# Mode literals
AddMode = Literal["append", "overwrite"]
-34
View File
@@ -1767,40 +1767,6 @@ def test_add_with_nans(mem_db: DBConnection):
assert np.allclose(filled_vectors[22.0], np.array([5.0, 0.0]))
def test_add_with_non_finite_values_keep(mem_db: DBConnection):
schema = pa.schema([pa.field("data", pa.list_(pa.float32(), 4))])
table = mem_db.create_table("test", schema=schema)
batch = pa.table(
{
"data": pa.array(
[[np.nan, np.inf, -np.inf, -0.0]],
type=schema.field("data").type,
)
},
schema=schema,
)
with pytest.raises(ValueError, match="NaN"):
table.add(batch)
table.add(batch, on_bad_vectors="keep")
values = table.to_arrow()["data"][0].as_py()
assert np.isnan(values[0])
assert np.isposinf(values[1])
assert np.isneginf(values[2])
assert values[3] == 0.0
assert np.signbit(values[3])
def test_add_keep_rejects_wrong_dimension(mem_db: DBConnection):
schema = pa.schema([pa.field("vector", pa.list_(pa.float32(), 2))])
table = mem_db.create_table("test", schema=schema)
with pytest.raises((ValueError, RuntimeError), match="variable length"):
table.add([{"vector": [1.0]}], on_bad_vectors="keep")
def test_add_with_empty_fixed_size_list_drops_bad_rows(mem_db: DBConnection):
class Schema(LanceModel):
text: str
+3 -21
View File
@@ -2,7 +2,6 @@
# SPDX-FileCopyrightText: Copyright The LanceDB Authors
import math
import os
import pathlib
from typing import Optional
@@ -365,7 +364,7 @@ def test_fill_bad_vector_values_arrow_types(vector_type, vectors, expected):
assert actual.to_pylist() == expected
@pytest.mark.parametrize("on_bad_vectors", ["error", "drop", "fill", "null", "keep"])
@pytest.mark.parametrize("on_bad_vectors", ["error", "drop", "fill", "null"])
def test_handle_bad_vectors_nan(on_bad_vectors):
vector = pa.array([[1.0, float("nan")], [3.0, 4.0]])
data = pa.table({"vector": vector})
@@ -380,9 +379,8 @@ def test_handle_bad_vectors_nan(on_bad_vectors):
assert output == (
"ValueError: Vector column 'vector' has NaNs. Set "
"on_bad_vectors='drop' to remove them, set on_bad_vectors='fill' "
"and fill_value=<value> to replace them, set on_bad_vectors='null' "
"to replace them with null, or set on_bad_vectors='keep' to preserve "
"them."
"and fill_value=<value> to replace them, or set on_bad_vectors='null' "
"to replace them with null."
)
return
else:
@@ -398,26 +396,10 @@ def test_handle_bad_vectors_nan(on_bad_vectors):
expected = pa.array([[1.0, 42.0], [3.0, 4.0]])
elif on_bad_vectors == "null":
expected = pa.array([None, [3.0, 4.0]])
elif on_bad_vectors == "keep":
actual = output["vector"].to_pylist()
assert actual[0][0] == 1.0
assert math.isnan(actual[0][1])
assert actual[1] == [3.0, 4.0]
return
assert output["vector"].combine_chunks() == expected
def test_handle_bad_vectors_keep_rejects_wrong_dimension():
data = pa.table({"vector": [[1.0, 2.0], [3.0]]})
with pytest.raises(ValueError, match="only preserves vectors containing NaNs"):
_handle_bad_vectors(
data.to_reader(),
on_bad_vectors="keep",
).read_all()
def test_handle_bad_vectors_noop():
# ChunkedArray should be preserved as-is
vector = pa.chunked_array(
+2 -16
View File
@@ -21,8 +21,7 @@ use lancedb::blob::{BlobFile, BlobRangeRequest};
use lancedb::index::scalar::FtsIndexBuilder;
use lancedb::table::{
AddDataMode, ColumnAlteration, Duration, FieldMetadataUpdate, FtsToken as LanceDbFtsToken,
NaNVectorBehavior, NewColumnTransform, OptimizeAction, OptimizeOptions, Ref,
Table as LanceDbTable,
NewColumnTransform, OptimizeAction, OptimizeOptions, Ref, Table as LanceDbTable,
};
use lancedb::tokenize as lancedb_tokenize;
use pyo3::{
@@ -643,14 +642,13 @@ impl Table {
})
}
#[pyo3(signature = (data, mode, progress=None, write_parallelism=None, on_nan_vectors=None))]
#[pyo3(signature = (data, mode, progress=None, write_parallelism=None))]
pub fn add<'a>(
self_: PyRef<'a, Self>,
data: PyScannable,
mode: String,
progress: Option<Py<PyAny>>,
write_parallelism: Option<usize>,
on_nan_vectors: Option<String>,
) -> PyResult<Bound<'a, PyAny>> {
let mut op = self_.inner_ref()?.add(data);
if mode == "append" {
@@ -660,18 +658,6 @@ impl Table {
} else {
return Err(PyValueError::new_err(format!("Invalid mode: {}", mode)));
}
match on_nan_vectors.as_deref() {
None | Some("error") => {}
Some("keep") => {
op = op.on_nan_vectors(NaNVectorBehavior::Keep);
}
Some(other) => {
return Err(PyValueError::new_err(format!(
"Invalid on_nan_vectors: {}",
other
)));
}
}
if let Some(write_parallelism) = write_parallelism {
op = op.write_parallelism(write_parallelism);
}