mirror of
https://github.com/aljazceru/turso.git
synced 2026-02-22 16:35:30 +01:00
javascript: Implement safe integers
This commit is contained in:
@@ -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");
|
||||
}
|
||||
|
||||
16
bindings/javascript/index.d.ts
vendored
16
bindings/javascript/index.d.ts
vendored
@@ -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
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user