diff --git a/.gitignore b/.gitignore
index 3bfd06a..7f88f88 100644
--- a/.gitignore
+++ b/.gitignore
@@ -121,7 +121,7 @@ integration-tests/*.db
.AppleDouble
.LSOverride
-# Icon must end with two
+# Icon must end with two
Icon
@@ -197,6 +197,7 @@ Cargo.lock
*.node
auth.d.ts
+limits.d.ts
compat.d.ts
promise.d.ts
sqlite-error.d.ts
diff --git a/compat.js b/compat.js
index f3f922d..82e2ab7 100644
--- a/compat.js
+++ b/compat.js
@@ -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
@@ -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.
*/
@@ -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()) {
diff --git a/docs/api.md b/docs/api.md
index 195439e..db9c315 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -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 | number | The limit category, one of the `Limit` constants. |
+| value | number | 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
diff --git a/index.d.ts b/index.d.ts
index f78f970..0dc4550 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -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
/**
diff --git a/integration-tests/tests/async.test.js b/integration-tests/tests/async.test.js
index f6536a7..abbd3d4 100644
--- a/integration-tests/tests/async.test.js
+++ b/integration-tests/tests/async.test.js
@@ -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;
diff --git a/integration-tests/tests/extensions.test.js b/integration-tests/tests/extensions.test.js
index 09d8265..7086b6d 100644
--- a/integration-tests/tests/extensions.test.js
+++ b/integration-tests/tests/extensions.test.js
@@ -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;
@@ -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");
diff --git a/limits.js b/limits.js
new file mode 100644
index 0000000..1c656de
--- /dev/null
+++ b/limits.js
@@ -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 };
diff --git a/package.json b/package.json
index b2963a6..97fb155 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/promise.js b/promise.js
index f507a52..c1a7607 100644
--- a/promise.js
+++ b/promise.js
@@ -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'
@@ -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.
@@ -590,6 +621,7 @@ module.exports = {
Action,
Authorization,
Database,
+ Limit,
SqliteError,
Statement,
connect,
diff --git a/src/lib.rs b/src/lib.rs
index d4c0270..cc57508 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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 {
+ 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 {
+ 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<()> {
@@ -708,6 +743,26 @@ impl Database {
}
}
+fn int_to_limit(val: i32) -> Result {
+ 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 {
match val {
0 => Ok(libsql::Authorization::Allow),