mirror of
https://github.com/lancedb/lancedb.git
synced 2026-11-01 16:26:52 +00:00
feat(nodejs): add col() expression builder for camelCase column names
## Summary Fixes #3211 by exposing the shared Rust/DataFusion expression builder through Node bindings. Case-sensitive references such as `col("firstName")` can be composed and passed to `where()`, while SQL strings remain supported. The expression-object representation follows [the design requested on #3290](https://github.com/lancedb/lancedb/pull/3290#discussion_r3632371998). That earlier PR remains open; no claim is made that the issue was unclaimed or that maintainers have selected this implementation. ## Review corrections - Construct the named native struct correctly and parse casts through `arrow_schema::DataType`. - Preserve numeric values: use Int64 only for JavaScript safe integers; retain Float64 otherwise. Explicit `litInt` rejects unsupported numbers with a documented `RangeError`. - Correct test syntax/API calls and align SQL assertions with the shared dialect, including reserved and case-sensitive names. - Use typed, isolated temporary test tables with cleanup, and cover an actual large-number filter threshold. - Document the public methods and commit generated API documentation. Synced with current main `0bceaaf3` through a normal merge. The diff is limited to seven Node source/test/config files and eleven generated documentation files; no Rust core change. ## Validation [Linux validation run](https://github.com/Ganesh1729-ui/lancedb/actions/runs/38244794288) on Node 24 / Rust 1.97.0 with the repository's `ci` profile and `remote,lancedb/metrics-otel` features: - Rebuilt native addon, TypeScript build, Biome, Rust formatting and native Clippy pass. - Expression/query/Arrow/table suites: **1,129 passed**; expression suite repeated: **42 passed**. - Using the original JavaScript numeric dispatch with the rebuilt addon: **8 regressions fail, 2 controls pass**. - Strict TypeDoc generation passes. Final local typing/docs/lint checks pass; the PR source equals the validated source except for the separate validation workflow, which is excluded from this PR. Full provider/model/S3, other feature combinations and OS/Node matrices were not run in this follow-up. Upstream checks and maintainer review remain separate. ## AI assistance AI was used for implementation, review and testing. No prior human review is claimed.
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright The LanceDB Authors
|
||||
|
||||
//! Napi bindings for the LanceDB expression builder API.
|
||||
|
||||
use std::ops::{Add, Div, Mul, Not, Sub};
|
||||
|
||||
use lancedb::expr::{
|
||||
DfExpr, col as ldb_col, contains, expr_cast, is_in, lit as df_lit, lower, upper,
|
||||
};
|
||||
use napi::bindgen_prelude::*;
|
||||
use napi_derive::napi;
|
||||
|
||||
#[napi]
|
||||
#[derive(Clone)]
|
||||
pub struct Expr {
|
||||
pub(crate) inner: DfExpr,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl Expr {
|
||||
#[napi]
|
||||
#[allow(clippy::use_self)] // NAPI doesn't allow Self here but clippy reports it
|
||||
pub fn eq(&self, other: &Expr) -> Self {
|
||||
Self {
|
||||
inner: self.inner.clone().eq(other.inner.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
#[allow(clippy::use_self)] // NAPI doesn't allow Self here but clippy reports it
|
||||
pub fn ne(&self, other: &Expr) -> Self {
|
||||
Self {
|
||||
inner: self.inner.clone().not_eq(other.inner.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
#[allow(clippy::use_self)] // NAPI doesn't allow Self here but clippy reports it
|
||||
pub fn lt(&self, other: &Expr) -> Self {
|
||||
Self {
|
||||
inner: self.inner.clone().lt(other.inner.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
#[allow(clippy::use_self)] // NAPI doesn't allow Self here but clippy reports it
|
||||
pub fn lte(&self, other: &Expr) -> Self {
|
||||
Self {
|
||||
inner: self.inner.clone().lt_eq(other.inner.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
#[allow(clippy::use_self)] // NAPI doesn't allow Self here but clippy reports it
|
||||
pub fn gt(&self, other: &Expr) -> Self {
|
||||
Self {
|
||||
inner: self.inner.clone().gt(other.inner.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
#[allow(clippy::use_self)] // NAPI doesn't allow Self here but clippy reports it
|
||||
pub fn gte(&self, other: &Expr) -> Self {
|
||||
Self {
|
||||
inner: self.inner.clone().gt_eq(other.inner.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
#[allow(clippy::use_self)] // NAPI doesn't allow Self here but clippy reports it
|
||||
pub fn and(&self, other: &Expr) -> Self {
|
||||
Self {
|
||||
inner: self.inner.clone().and(other.inner.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
#[allow(clippy::use_self)] // NAPI doesn't allow Self here but clippy reports it
|
||||
pub fn or(&self, other: &Expr) -> Self {
|
||||
Self {
|
||||
inner: self.inner.clone().or(other.inner.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn not(&self) -> Self {
|
||||
Self {
|
||||
inner: self.inner.clone().not(),
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
#[allow(clippy::use_self)] // NAPI doesn't allow Self here but clippy reports it
|
||||
pub fn add(&self, other: &Expr) -> Self {
|
||||
Self {
|
||||
inner: self.inner.clone().add(other.inner.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
#[allow(clippy::use_self)] // NAPI doesn't allow Self here but clippy reports it
|
||||
pub fn sub(&self, other: &Expr) -> Self {
|
||||
Self {
|
||||
inner: self.inner.clone().sub(other.inner.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
#[allow(clippy::use_self)] // NAPI doesn't allow Self here but clippy reports it
|
||||
pub fn mul(&self, other: &Expr) -> Self {
|
||||
Self {
|
||||
inner: self.inner.clone().mul(other.inner.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
#[allow(clippy::use_self)] // NAPI doesn't allow Self here but clippy reports it
|
||||
pub fn div(&self, other: &Expr) -> Self {
|
||||
Self {
|
||||
inner: self.inner.clone().div(other.inner.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn lower(&self) -> Self {
|
||||
Self {
|
||||
inner: lower(self.inner.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn upper(&self) -> Self {
|
||||
Self {
|
||||
inner: upper(self.inner.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
#[allow(clippy::use_self)] // NAPI doesn't allow Self here but clippy reports it
|
||||
pub fn contains(&self, substr: &Expr) -> Self {
|
||||
Self {
|
||||
inner: contains(self.inner.clone(), substr.inner.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
#[allow(clippy::use_self)] // NAPI doesn't allow Self here but clippy reports it
|
||||
pub fn is_in(&self, list: Vec<&Expr>) -> Self {
|
||||
let items: Vec<DfExpr> = list.into_iter().map(|e| e.inner.clone()).collect();
|
||||
Self {
|
||||
inner: is_in(self.inner.clone(), items),
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn cast(&self, data_type: String) -> napi::Result<Self> {
|
||||
let arrow_type = data_type
|
||||
.parse::<arrow_schema::DataType>()
|
||||
.map_err(|e| napi::Error::from_reason(format!("Invalid data type: {}", e)))?;
|
||||
Ok(Self {
|
||||
inner: expr_cast(self.inner.clone(), arrow_type),
|
||||
})
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn to_sql(&self) -> napi::Result<String> {
|
||||
lancedb::expr::expr_to_sql_string(&self.inner)
|
||||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a column reference expression.
|
||||
///
|
||||
/// The column name is preserved exactly as given (case-sensitive), so
|
||||
/// `col("firstName")` correctly references a field named `firstName`.
|
||||
#[napi]
|
||||
#[allow(dead_code)]
|
||||
pub fn col(name: String) -> Expr {
|
||||
Expr {
|
||||
inner: ldb_col(name),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a literal (constant) value expression.
|
||||
#[napi]
|
||||
#[allow(dead_code)]
|
||||
pub fn lit(value: napi::bindgen_prelude::Either<String, f64>) -> napi::Result<Expr> {
|
||||
match value {
|
||||
Either::A(s) => Ok(Expr { inner: df_lit(s) }),
|
||||
Either::B(f) => Ok(Expr { inner: df_lit(f) }),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a literal boolean value expression.
|
||||
#[napi]
|
||||
#[allow(dead_code)]
|
||||
pub fn lit_bool(value: bool) -> Expr {
|
||||
Expr {
|
||||
inner: df_lit(value),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a literal integer value expression.
|
||||
#[napi]
|
||||
#[allow(dead_code)]
|
||||
pub fn lit_int(value: i64) -> Expr {
|
||||
Expr {
|
||||
inner: df_lit(value),
|
||||
}
|
||||
}
|
||||
|
||||
/// Call an arbitrary registered SQL function by name.
|
||||
#[napi]
|
||||
#[allow(dead_code)]
|
||||
pub fn func(name: String, args: Vec<&Expr>) -> napi::Result<Expr> {
|
||||
let df_args: Vec<DfExpr> = args.into_iter().map(|e| e.inner.clone()).collect();
|
||||
lancedb::expr::func(&name, df_args)
|
||||
.map(|inner| Expr { inner })
|
||||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||||
}
|
||||
@@ -14,6 +14,7 @@ mod blob;
|
||||
mod catalog;
|
||||
mod connection;
|
||||
mod error;
|
||||
mod expr;
|
||||
mod header;
|
||||
mod index;
|
||||
mod iterator;
|
||||
|
||||
Reference in New Issue
Block a user