javascript: Implement safe integers

This commit is contained in:
Pekka Enberg
2025-08-19 14:39:05 +03:00
parent 6b59bcd51e
commit 5002539b04
11 changed files with 145 additions and 21 deletions

View File

@@ -210,6 +210,15 @@ class Database {
throw new Error("not implemented");
}
/**
* Sets the default safe integers mode for all statements from this database.
*
* @param {boolean} [toggle] - Whether to use safe integers by default.
*/
defaultSafeIntegers(toggle) {
this.db.defaultSafeIntegers(toggle);
}
/**
* Closes the database connection.
*/
@@ -250,6 +259,16 @@ class Statement {
return this;
}
/**
* Sets safe integers mode for this statement.
*
* @param {boolean} [toggle] - Whether to use safe integers.
*/
safeIntegers(toggle) {
this.stmt.safeIntegers(toggle);
return this;
}
get source() {
throw new Error("not implemented");
}

View File

@@ -67,6 +67,14 @@ export declare class Database {
* `Ok(())` if the database is closed successfully.
*/
close(): void
/**
* Sets the default safe integers mode for all statements from this database.
*
* # Arguments
*
* * `toggle` - Whether to use safe integers by default.
*/
defaultSafeIntegers(toggle?: boolean | undefined | null): void
/** Runs the I/O loop synchronously. */
ioLoopSync(): void
/** Runs the I/O loop asynchronously, returning a Promise. */
@@ -107,6 +115,14 @@ export declare class Statement {
raw(raw?: boolean | undefined | null): void
/** Sets the presentation mode to pluck. */
pluck(pluck?: boolean | undefined | null): void
/**
* Sets safe integers mode for this statement.
*
* # Arguments
*
* * `toggle` - Whether to use safe integers.
*/
safeIntegers(toggle?: boolean | undefined | null): void
/** Finalizes the statement. */
finalize(): void
}

View File

@@ -214,6 +214,15 @@ class Database {
throw new Error("not implemented");
}
/**
* Sets the default safe integers mode for all statements from this database.
*
* @param {boolean} [toggle] - Whether to use safe integers by default.
*/
defaultSafeIntegers(toggle) {
this.db.defaultSafeIntegers(toggle);
}
/**
* Closes the database connection.
*/
@@ -253,6 +262,16 @@ class Statement {
return this;
}
/**
* Sets safe integers mode for this statement.
*
* @param {boolean} [toggle] - Whether to use safe integers.
*/
safeIntegers(toggle) {
this.stmt.safeIntegers(toggle);
return this;
}
get source() {
throw new Error("not implemented");
}

View File

@@ -41,6 +41,7 @@ pub struct Database {
conn: Arc<turso_core::Connection>,
is_memory: bool,
is_open: Cell<bool>,
default_safe_integers: Cell<bool>,
}
#[napi]
@@ -92,6 +93,7 @@ impl Database {
conn,
is_memory,
is_open: Cell::new(true),
default_safe_integers: Cell::new(false),
}
}
@@ -147,6 +149,7 @@ impl Database {
stmt: RefCell::new(Some(stmt)),
column_names,
mode: RefCell::new(PresentationMode::Expanded),
safe_integers: Cell::new(self.default_safe_integers.get()),
})
}
@@ -192,6 +195,16 @@ impl Database {
Ok(())
}
/// Sets the default safe integers mode for all statements from this database.
///
/// # Arguments
///
/// * `toggle` - Whether to use safe integers by default.
#[napi(js_name = "defaultSafeIntegers")]
pub fn default_safe_integers(&self, toggle: Option<bool>) {
self.default_safe_integers.set(toggle.unwrap_or(true));
}
/// Runs the I/O loop synchronously.
#[napi]
pub fn io_loop_sync(&self) -> Result<()> {
@@ -215,6 +228,7 @@ pub struct Statement {
stmt: RefCell<Option<turso_core::Statement>>,
column_names: Vec<std::ffi::CString>,
mode: RefCell<PresentationMode>,
safe_integers: Cell<bool>,
}
#[napi]
@@ -290,7 +304,10 @@ impl Statement {
ValueType::BigInt => {
let bigint_str = value.coerce_to_string()?.into_utf8()?.as_str()?.to_owned();
let bigint_value = bigint_str.parse::<i64>().map_err(|e| {
Error::new(Status::NumberExpected, format!("Failed to parse BigInt: {e}"))
Error::new(
Status::NumberExpected,
format!("Failed to parse BigInt: {e}"),
)
})?;
turso_core::Value::Integer(bigint_value)
}
@@ -362,11 +379,12 @@ impl Statement {
.ok_or_else(|| Error::new(Status::GenericFailure, "No row data available"))?;
let mode = self.mode.borrow();
let safe_integers = self.safe_integers.get();
let row_value = match *mode {
PresentationMode::Raw => {
let mut raw_array = env.create_array(row_data.len() as u32)?;
for (idx, value) in row_data.get_values().enumerate() {
let js_value = to_js_value(env, value)?;
let js_value = to_js_value(env, value, safe_integers)?;
raw_array.set(idx as u32, js_value)?;
}
raw_array.coerce_to_object()?.to_unknown()
@@ -381,7 +399,7 @@ impl Statement {
napi::Status::GenericFailure,
"Pluck mode requires at least one column in the result",
))?;
to_js_value(env, value)?
to_js_value(env, value, safe_integers)?
}
PresentationMode::Expanded => {
let row = Object::new(env)?;
@@ -390,7 +408,7 @@ impl Statement {
for idx in 0..row_data.len() {
let value = row_data.get_value(idx);
let column_name = &self.column_names[idx];
let js_value = to_js_value(env, value)?;
let js_value = to_js_value(env, value, safe_integers)?;
unsafe {
napi::sys::napi_set_named_property(
raw_env,
@@ -425,6 +443,16 @@ impl Statement {
});
}
/// Sets safe integers mode for this statement.
///
/// # Arguments
///
/// * `toggle` - Whether to use safe integers.
#[napi(js_name = "safeIntegers")]
pub fn safe_integers(&self, toggle: Option<bool>) {
self.safe_integers.set(toggle.unwrap_or(true));
}
/// Finalizes the statement.
#[napi]
pub fn finalize(&self) -> Result<()> {
@@ -456,11 +484,22 @@ impl Task for IoLoopTask {
}
/// Convert a Turso value to a JavaScript value.
fn to_js_value<'a>(env: &'a napi::Env, value: &turso_core::Value) -> napi::Result<Unknown<'a>> {
fn to_js_value<'a>(
env: &'a napi::Env,
value: &turso_core::Value,
safe_integers: bool,
) -> napi::Result<Unknown<'a>> {
match value {
turso_core::Value::Null => ToNapiValue::into_unknown(Null, env),
turso_core::Value::Integer(i) => ToNapiValue::into_unknown(i, env),
turso_core::Value::Float(f) => ToNapiValue::into_unknown(f, env),
turso_core::Value::Integer(i) => {
if safe_integers {
let bigint = BigInt::from(*i);
ToNapiValue::into_unknown(bigint, env)
} else {
ToNapiValue::into_unknown(*i as f64, env)
}
}
turso_core::Value::Float(f) => ToNapiValue::into_unknown(*f, env),
turso_core::Value::Text(s) => ToNapiValue::into_unknown(s.as_str(), env),
turso_core::Value::Blob(b) => ToNapiValue::into_unknown(b, env),
}

View File

@@ -129,6 +129,7 @@ export interface Client {
class LibSQLClient implements Client {
private session: Session;
private _closed = false;
private _defaultSafeIntegers = false;
constructor(config: Config) {
this.validateConfig(config);

View File

@@ -16,6 +16,7 @@ export class Connection {
private config: Config;
private session: Session;
private isOpen: boolean = true;
private defaultSafeIntegerMode: boolean = false;
constructor(config: Config) {
if (!config.url) {
@@ -44,7 +45,11 @@ export class Connection {
if (!this.isOpen) {
throw new TypeError("The database connection is not open");
}
return new Statement(this.config, sql);
const stmt = new Statement(this.config, sql);
if (this.defaultSafeIntegerMode) {
stmt.safeIntegers(true);
}
return stmt;
}
/**
@@ -101,6 +106,15 @@ export class Connection {
return this.session.execute(sql);
}
/**
* Sets the default safe integers mode for all statements from this connection.
*
* @param toggle - Whether to use safe integers by default.
*/
defaultSafeIntegers(toggle?: boolean): void {
this.defaultSafeIntegerMode = toggle === false ? false : true;
}
/**
* Close the connection.
*

View File

@@ -115,11 +115,14 @@ export function encodeValue(value: any): Value {
return { type: 'text', value: String(value) };
}
export function decodeValue(value: Value): any {
export function decodeValue(value: Value, safeIntegers: boolean = false): any {
switch (value.type) {
case 'null':
return null;
case 'integer':
if (safeIntegers) {
return BigInt(value.value as string);
}
return parseInt(value.value as string, 10);
case 'float':
return value.value as number;

View File

@@ -53,11 +53,12 @@ export class Session {
*
* @param sql - The SQL statement to execute
* @param args - Optional array of parameter values or object with named parameters
* @param safeIntegers - Whether to return integers as BigInt
* @returns Promise resolving to the complete result set
*/
async execute(sql: string, args: any[] | Record<string, any> = []): Promise<any> {
async execute(sql: string, args: any[] | Record<string, any> = [], safeIntegers: boolean = false): Promise<any> {
const { response, entries } = await this.executeRaw(sql, args);
const result = await this.processCursorEntries(entries);
const result = await this.processCursorEntries(entries, safeIntegers);
return result;
}
@@ -137,7 +138,7 @@ export class Session {
* @param entries - Async generator of cursor entries
* @returns Promise resolving to the processed result
*/
async processCursorEntries(entries: AsyncGenerator<CursorEntry>): Promise<any> {
async processCursorEntries(entries: AsyncGenerator<CursorEntry>, safeIntegers: boolean = false): Promise<any> {
let columns: string[] = [];
let columnTypes: string[] = [];
let rows: any[] = [];
@@ -154,7 +155,7 @@ export class Session {
break;
case 'row':
if (entry.row) {
const decodedRow = entry.row.map(decodeValue);
const decodedRow = entry.row.map(value => decodeValue(value, safeIntegers));
const rowObject = this.createRowObject(decodedRow, columns);
rows.push(rowObject);
}

View File

@@ -18,6 +18,7 @@ export class Statement {
private session: Session;
private sql: string;
private presentationMode: 'expanded' | 'raw' | 'pluck' = 'expanded';
private safeIntegerMode: boolean = false;
constructor(sessionConfig: SessionConfig, sql: string) {
this.session = new Session(sessionConfig);
@@ -61,6 +62,17 @@ export class Statement {
return this;
}
/**
* Sets safe integers mode for this statement.
*
* @param toggle Whether to use safe integers. If you don't pass the parameter, safe integers mode is enabled.
* @returns This statement instance for chaining
*/
safeIntegers(toggle?: boolean): Statement {
this.safeIntegerMode = toggle === false ? false : true;
return this;
}
/**
* Executes the prepared statement.
*
@@ -76,7 +88,7 @@ export class Statement {
*/
async run(args?: any): Promise<any> {
const normalizedArgs = this.normalizeArgs(args);
const result = await this.session.execute(this.sql, normalizedArgs);
const result = await this.session.execute(this.sql, normalizedArgs, this.safeIntegerMode);
return { changes: result.rowsAffected, lastInsertRowid: result.lastInsertRowid };
}
@@ -97,7 +109,7 @@ export class Statement {
*/
async get(args?: any): Promise<any> {
const normalizedArgs = this.normalizeArgs(args);
const result = await this.session.execute(this.sql, normalizedArgs);
const result = await this.session.execute(this.sql, normalizedArgs, this.safeIntegerMode);
const row = result.rows[0];
if (!row) {
return undefined;
@@ -132,7 +144,7 @@ export class Statement {
*/
async all(args?: any): Promise<any[]> {
const normalizedArgs = this.normalizeArgs(args);
const result = await this.session.execute(this.sql, normalizedArgs);
const result = await this.session.execute(this.sql, normalizedArgs, this.safeIntegerMode);
if (this.presentationMode === 'pluck') {
// In pluck mode, return only the first column value from each row
@@ -184,7 +196,7 @@ export class Statement {
break;
case 'row':
if (entry.row) {
const decodedRow = entry.row.map(decodeValue);
const decodedRow = entry.row.map(value => decodeValue(value, this.safeIntegerMode));
if (this.presentationMode === 'pluck') {
// In pluck mode, yield only the first column value
yield decodedRow[0];

View File

@@ -350,7 +350,7 @@ test.serial("Statement.all() [pluck]", async (t) => {
t.deepEqual(await stmt.pluck().all(), expected);
});
test.skip("Statement.all() [default safe integers]", async (t) => {
test.serial("Statement.all() [default safe integers]", async (t) => {
const db = t.context.db;
db.defaultSafeIntegers();
const stmt = await db.prepare("SELECT * FROM users");
@@ -361,7 +361,7 @@ test.skip("Statement.all() [default safe integers]", async (t) => {
t.deepEqual(await stmt.raw().all(), expected);
});
test.skip("Statement.all() [statement safe integers]", async (t) => {
test.serial("Statement.all() [statement safe integers]", async (t) => {
const db = t.context.db;
const stmt = await db.prepare("SELECT * FROM users");
stmt.safeIntegers();

View File

@@ -406,7 +406,7 @@ test.serial("Statement.all() [pluck]", async (t) => {
t.deepEqual(stmt.pluck().all(), expected);
});
test.skip("Statement.all() [default safe integers]", async (t) => {
test.serial("Statement.all() [default safe integers]", async (t) => {
const db = t.context.db;
db.defaultSafeIntegers();
const stmt = db.prepare("SELECT * FROM users");
@@ -417,7 +417,7 @@ test.skip("Statement.all() [default safe integers]", async (t) => {
t.deepEqual(stmt.raw().all(), expected);
});
test.skip("Statement.all() [statement safe integers]", async (t) => {
test.serial("Statement.all() [statement safe integers]", async (t) => {
const db = t.context.db;
const stmt = db.prepare("SELECT * FROM users");
stmt.safeIntegers();