mirror of
https://github.com/lancedb/lancedb.git
synced 2026-10-14 15:52:26 +00:00
## What's wrong
`toSQL` renders numbers with `toString()`, so `NaN` becomes `NaN` and
`Infinity` becomes `Infinity`. SQL reads those as column names:
```ts
await table.update({ values: { price: NaN }, where: "id = 1" });
// Error: Invalid input, Schema error: No field named "NaN". Valid fields are id, price.
```
`Infinity` and `-Infinity` fail the same way. The only workaround today
is `valuesSql`.
## Fix
Emit `CAST('NaN' AS DOUBLE)`, `CAST('Infinity' AS DOUBLE)` and
`CAST('-Infinity' AS DOUBLE)` for these values. Finite numbers render as
before.
## Tests
- `__test__/util.test.ts` adds `toSQL` cases for `NaN`, `Infinity` and
`-Infinity`.
- `__test__/table.test.ts` adds "should let me update float values to
NaN and Infinity", which updates a float column and reads the values
back.
- On `main`, the new cases fail with `No field named "NaN"`.
- With the fix, all of `util.test.ts` and `table.test.ts` pass (344
tests). These were run against the published
`@lancedb/lancedb@0.40.0-beta.11` native binary.
- `biome check` is clean on the changed files.
The Python SDK has the same issue in `value_to_sql`. That fix is in
#4326.
The fix was found and written by an AI agent (Breken) and checked
against the tests above.
Co-authored-by: breken-ai <312387581+breken-ai@users.noreply.github.com>
91 lines
2.4 KiB
TypeScript
91 lines
2.4 KiB
TypeScript
// SPDX-License-Identifier: Apache-2.0
|
|
// SPDX-FileCopyrightText: Copyright The LanceDB Authors
|
|
|
|
export type IntoSql =
|
|
| string
|
|
| number
|
|
| boolean
|
|
| null
|
|
| Date
|
|
| ArrayBufferLike
|
|
| Buffer
|
|
| IntoSql[];
|
|
|
|
export function toSQL(value: IntoSql): string {
|
|
if (typeof value === "string") {
|
|
return `'${value.replace(/'/g, "''")}'`;
|
|
} else if (typeof value === "number") {
|
|
// toString() gives "NaN" / "Infinity", which SQL reads as column names.
|
|
if (Number.isNaN(value)) {
|
|
return "CAST('NaN' AS DOUBLE)";
|
|
} else if (!Number.isFinite(value)) {
|
|
return value > 0
|
|
? "CAST('Infinity' AS DOUBLE)"
|
|
: "CAST('-Infinity' AS DOUBLE)";
|
|
}
|
|
return value.toString();
|
|
} else if (typeof value === "boolean") {
|
|
return value ? "TRUE" : "FALSE";
|
|
} else if (value === null) {
|
|
return "NULL";
|
|
} else if (value instanceof Date) {
|
|
return `'${value.toISOString()}'`;
|
|
} else if (Array.isArray(value)) {
|
|
return `[${value.map(toSQL).join(", ")}]`;
|
|
} else if (Buffer.isBuffer(value)) {
|
|
return `X'${value.toString("hex")}'`;
|
|
} else if (value instanceof ArrayBuffer) {
|
|
return `X'${Buffer.from(value).toString("hex")}'`;
|
|
} else {
|
|
throw new Error(
|
|
`Unsupported value type: ${typeof value} value: (${value})`,
|
|
);
|
|
}
|
|
}
|
|
|
|
export function packBits(data: Array<number>): Array<number> {
|
|
const packed = Array(data.length >> 3).fill(0);
|
|
for (let i = 0; i < data.length; i++) {
|
|
const byte = i >> 3;
|
|
const bit = i & 7;
|
|
packed[byte] |= data[i] << bit;
|
|
}
|
|
return packed;
|
|
}
|
|
|
|
export class TTLCache {
|
|
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
|
private readonly cache: Map<string, { value: any; expires: number }>;
|
|
|
|
/**
|
|
* @param ttl Time to live in milliseconds
|
|
*/
|
|
constructor(private readonly ttl: number) {
|
|
this.cache = new Map();
|
|
}
|
|
|
|
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
|
get(key: string): any | undefined {
|
|
const entry = this.cache.get(key);
|
|
if (entry === undefined) {
|
|
return undefined;
|
|
}
|
|
|
|
if (entry.expires < Date.now()) {
|
|
this.cache.delete(key);
|
|
return undefined;
|
|
}
|
|
|
|
return entry.value;
|
|
}
|
|
|
|
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
|
set(key: string, value: any): void {
|
|
this.cache.set(key, { value, expires: Date.now() + this.ttl });
|
|
}
|
|
|
|
delete(key: string): void {
|
|
this.cache.delete(key);
|
|
}
|
|
}
|