Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ integration-tests/*.db
.AppleDouble
.LSOverride

# Icon must end with two
# Icon must end with two
Icon


Expand Down Expand Up @@ -197,6 +197,7 @@ Cargo.lock
*.node

auth.d.ts
limits.d.ts
compat.d.ts
promise.d.ts
sqlite-error.d.ts
32 changes: 32 additions & 0 deletions compat.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
const { Database: NativeDb, databasePrepareSync, databaseSyncSync, databaseExecSync, statementRunSync, statementGetSync, statementIterateSync, iteratorNextSync } = require("./index.js");
const SqliteError = require("./sqlite-error.js");
const { Authorization, Action } = require("./auth");
const { Limit } = require("./limits");

function convertError(err) {
// Handle errors from Rust with JSON-encoded message
Expand Down Expand Up @@ -290,6 +291,36 @@ class Database {
return this;
}

/**
* Returns the current value of a run-time limit for this connection.
*
* @param {number} id - The limit category, one of the `Limit` constants.
* @returns {number}
*/
limit(id) {
try {
return this.db.limit(id);
} catch (err) {
throw convertError(err);
}
}

/**
* Changes a run-time limit for this connection and returns its previous value.
*
* @param {number} id - The limit category, one of the `Limit` constants.
* @param {number} value - The new limit. SQLite never raises a limit past its
* compile-time maximum, and a negative value leaves the limit unchanged.
* @returns {number}
*/
setLimit(id, value) {
try {
return this.db.setLimit(id, value);
} catch (err) {
throw convertError(err);
}
}

/**
* Toggle 64-bit integer support.
*/
Expand Down Expand Up @@ -463,6 +494,7 @@ module.exports = Database;
module.exports.SqliteError = SqliteError;
module.exports.Authorization = Authorization;
module.exports.Action = Action;
module.exports.Limit = Limit;

function normalizeBatchMode(mode) {
switch (String(mode).toLowerCase()) {
Expand Down
32 changes: 32 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,38 @@ Pass `null` to remove the authorizer and allow all operations:
db.authorizer(null);
```

### limit(id) ⇒ number

Returns the current value of a run-time limit for this connection. `id` is one of the `Limit` constants.

### setLimit(id, value) ⇒ number

Changes a run-time limit for this connection and returns its previous value. The limit applies only to this connection.

SQLite never raises a limit past its compile-time maximum, so `setLimit()` can only lower a limit below that maximum. A negative `value` leaves the limit unchanged.

| Param | Type | Description |
| ----- | ------------------- | -------------------------------------------- |
| id | <code>number</code> | The limit category, one of the `Limit` constants. |
| value | <code>number</code> | The new limit. |

For example, to cap the size of any string or BLOB at 1 MiB:

```javascript
const { Limit } = require('libsql');

db.setLimit(Limit.LENGTH, 1024 * 1024);

// Throws SQLITE_TOOBIG.
db.prepare("SELECT zeroblob(2000000)").get();
```

SQLite's `printf()` and `format()` functions return `NULL` instead of throwing when their result would exceed `Limit.LENGTH`.

The `Limit` constants match SQLite's `SQLITE_LIMIT_*` values: `LENGTH`, `SQL_LENGTH`, `COLUMN`, `EXPR_DEPTH`, `COMPOUND_SELECT`, `VDBE_OP`, `FUNCTION_ARG`, `ATTACHED`, `LIKE_PATTERN_LENGTH`, `VARIABLE_NUMBER`, `TRIGGER_DEPTH` and `WORKER_THREADS`. See the [SQLite documentation](https://sqlite.org/c3ref/c_limit_attached.html) for what each one controls.

**Note:** This is an extension in libSQL and not available in `better-sqlite3`.

### loadExtension(path, [entryPoint]) ⇒ this

Loads a SQLite3 extension
Expand Down
21 changes: 21 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,27 @@ export declare class Database {
* * `env` - The environment.
*/
interrupt(): void
/**
* Returns the current value of a run-time limit for this connection.
*
* # Arguments
*
* * `id` - The limit category, one of the `Limit` constants.
*/
limit(id: number): number
/**
* Changes a run-time limit for this connection and returns its previous value.
*
* SQLite caps the new value at the limit's compile-time maximum, so this can
* lower a limit but never raise it past that maximum. A negative value leaves
* the limit unchanged.
*
* # Arguments
*
* * `id` - The limit category, one of the `Limit` constants.
* * `value` - The new limit.
*/
setLimit(id: number, value: number): number
/** Closes the database connection. */
close(): void
/**
Expand Down
20 changes: 20 additions & 0 deletions integration-tests/tests/async.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -987,6 +987,26 @@ test.serial("Database.batch() rejects non-array argument", async (t) => {
await t.throwsAsync(() => db.batch("SELECT 1"), { instanceOf: TypeError });
});

test.serial("Database.setLimit() lowers the string length limit", async (t) => {
const db = t.context.db;
const { Limit } = await import("libsql/promise");
const bigFormat = "SELECT length(format('%2000000s', 'x')) AS n";
t.is((await (await db.prepare(bigFormat)).get()).n, 2000000);

const defaultLength = db.limit(Limit.LENGTH);
t.is(db.setLimit(Limit.LENGTH, 1024 * 1024), defaultLength);
t.is(db.limit(Limit.LENGTH), 1024 * 1024);

t.is((await (await db.prepare(bigFormat)).get()).n, null);
await t.throwsAsync(
async () => {
const stmt = await db.prepare("SELECT zeroblob(2000000)");
await stmt.get();
},
{ instanceOf: t.context.errorType, code: "SQLITE_TOOBIG" }
);
});

const connect = async (path_opt, options = {}) => {
const path = path_opt ?? "hello.db";
const provider = process.env.PROVIDER;
Expand Down
42 changes: 41 additions & 1 deletion integration-tests/tests/extensions.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import test from "ava";
import { Authorization, Action } from "libsql";
import { Authorization, Action, Limit } from "libsql";

test.serial("Statement.run() returning duration", async (t) => {
const db = t.context.db;
Expand Down Expand Up @@ -550,6 +550,46 @@ test.serial("Accessor: blocks subquery escape to underlying table", async (t) =>

// ---- Setup ----

// ---- Limits ----

test.serial("Database.setLimit() lowers the string length limit", async (t) => {
const db = t.context.db;
const bigFormat = "SELECT length(format('%2000000s', 'x')) AS n";
t.is(db.prepare(bigFormat).get().n, 2000000);

const defaultLength = db.limit(Limit.LENGTH);
t.is(db.setLimit(Limit.LENGTH, 1024 * 1024), defaultLength);
t.is(db.limit(Limit.LENGTH), 1024 * 1024);

// SQLite's printf() family stops at the limit and returns NULL. Other
// functions fail with SQLITE_TOOBIG.
t.is(db.prepare(bigFormat).get().n, null);
t.throws(() => db.prepare("SELECT zeroblob(2000000)").get(), {
instanceOf: t.context.errorType,
code: "SQLITE_TOOBIG",
});
t.is(db.prepare("SELECT length(format('%1000s', 'x')) AS n").get().n, 1000);
});

test.serial("Database.setLimit() with a negative value leaves the limit unchanged", async (t) => {
const db = t.context.db;
db.setLimit(Limit.LENGTH, 4096);
t.is(db.setLimit(Limit.LENGTH, -1), 4096);
t.is(db.limit(Limit.LENGTH), 4096);
});

test.serial("Database.limit() rejects unknown limit IDs", async (t) => {
const db = t.context.db;
t.throws(() => db.limit(99), { message: /Invalid limit '99'/ });
t.throws(() => db.setLimit(-5, 10), { message: /Invalid limit '-5'/ });
});

test.serial("Database.limit() throws after close", async (t) => {
const [db] = await connect(":memory:");
db.close();
t.throws(() => db.limit(Limit.LENGTH), { instanceOf: TypeError });
});

const connect = async (path_opt) => {
const path = path_opt ?? "hello.db";
const x = await import("libsql");
Expand Down
38 changes: 38 additions & 0 deletions limits.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* SQLite run-time limit categories, for use with `Database.limit()` and
* `Database.setLimit()`.
*
* The values match SQLite's `SQLITE_LIMIT_*` constants. See
* https://sqlite.org/c3ref/c_limit_attached.html for details.
*
* @readonly
* @enum {number}
*/
const Limit = {
/** The maximum size of any string or BLOB or table row, in bytes. */
LENGTH: 0,
/** The maximum length of an SQL statement, in bytes. */
SQL_LENGTH: 1,
/** The maximum number of columns in a table, index, result set, ORDER BY or GROUP BY. */
COLUMN: 2,
/** The maximum depth of the parse tree on any expression. */
EXPR_DEPTH: 3,
/** The maximum number of terms in a compound SELECT statement. */
COMPOUND_SELECT: 4,
/** The maximum number of instructions in a virtual machine program. */
VDBE_OP: 5,
/** The maximum number of arguments on a function. */
FUNCTION_ARG: 6,
/** The maximum number of attached databases. */
ATTACHED: 7,
/** The maximum length of the pattern argument to LIKE or GLOB. */
LIKE_PATTERN_LENGTH: 8,
/** The maximum index number of any parameter in an SQL statement. */
VARIABLE_NUMBER: 9,
/** The maximum depth of recursion for triggers. */
TRIGGER_DEPTH: 10,
/** The maximum number of auxiliary worker threads per prepared statement. */
WORKER_THREADS: 11,
};

module.exports = { Limit };
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
"compat.js",
"index.d.ts",
"index.js",
"limits.d.ts",
"limits.js",
"promise.d.ts",
"promise.js",
"sqlite-error.d.ts",
Expand Down
32 changes: 32 additions & 0 deletions promise.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
const { Database: NativeDb, connect: nativeConnect } = require("./index.js");
const SqliteError = require("./sqlite-error.js");
const { Authorization, Action } = require("./auth");
const { Limit } = require("./limits");

/**
* @import {Options as NativeOptions, Statement as NativeStatement} from './index.js'
Expand Down Expand Up @@ -382,6 +383,36 @@ class Database {
return this;
}

/**
* Returns the current value of a run-time limit for this connection.
*
* @param {number} id - The limit category, one of the `Limit` constants.
* @returns {number}
*/
limit(id) {
try {
return this.db.limit(id);
} catch (err) {
throw convertError(err);
}
}

/**
* Changes a run-time limit for this connection and returns its previous value.
*
* @param {number} id - The limit category, one of the `Limit` constants.
* @param {number} value - The new limit. SQLite never raises a limit past its
* compile-time maximum, and a negative value leaves the limit unchanged.
* @returns {number}
*/
setLimit(id, value) {
try {
return this.db.setLimit(id, value);
} catch (err) {
throw convertError(err);
}
}

/**
* Toggle 64-bit integer support.
* @param {boolean} [toggle] - Whether to use safe integers by default.
Expand Down Expand Up @@ -590,6 +621,7 @@ module.exports = {
Action,
Authorization,
Database,
Limit,
SqliteError,
Statement,
connect,
Expand Down
55 changes: 55 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -688,6 +688,41 @@ impl Database {
Ok(())
}

/// Returns the current value of a run-time limit for this connection.
///
/// # Arguments
///
/// * `id` - The limit category, one of the `Limit` constants.
#[napi]
pub fn limit(&self, env: Env, id: i32) -> Result<i32> {
let conn = match &self.conn {
Some(conn) => conn.clone(),
None => return Err(throw_database_closed_error(&env)),
};
let limit = int_to_limit(id)?;
Ok(conn.limit(limit).map_err(Error::from)?)
}

/// Changes a run-time limit for this connection and returns its previous value.
///
/// SQLite caps the new value at the limit's compile-time maximum, so this can
/// lower a limit but never raise it past that maximum. A negative value leaves
/// the limit unchanged.
///
/// # Arguments
///
/// * `id` - The limit category, one of the `Limit` constants.
/// * `value` - The new limit.
#[napi]
pub fn set_limit(&self, env: Env, id: i32, value: i32) -> Result<i32> {
let conn = match &self.conn {
Some(conn) => conn.clone(),
None => return Err(throw_database_closed_error(&env)),
};
let limit = int_to_limit(id)?;
Ok(conn.set_limit(limit, value).map_err(Error::from)?)
}

/// Closes the database connection.
#[napi]
pub fn close(&mut self) -> Result<()> {
Expand All @@ -708,6 +743,26 @@ impl Database {
}
}

fn int_to_limit(val: i32) -> Result<libsql::Limit> {
match val {
0 => Ok(libsql::Limit::Length),
1 => Ok(libsql::Limit::SqlLength),
2 => Ok(libsql::Limit::Column),
3 => Ok(libsql::Limit::ExprDepth),
4 => Ok(libsql::Limit::CompoundSelect),
5 => Ok(libsql::Limit::VdbeOp),
6 => Ok(libsql::Limit::FunctionArg),
7 => Ok(libsql::Limit::Attached),
8 => Ok(libsql::Limit::LikePatternLength),
9 => Ok(libsql::Limit::VariableNumber),
10 => Ok(libsql::Limit::TriggerDepth),
11 => Ok(libsql::Limit::WorkerThreads),
_ => Err(napi::Error::from_reason(format!(
"Invalid limit '{val}'. Expected one of the Limit constants (0 to 11).",
))),
}
}

fn int_to_authorization(val: i32) -> Result<libsql::Authorization> {
match val {
0 => Ok(libsql::Authorization::Allow),
Expand Down