sqlite: add limits property to DatabaseSync

PR-URL: https://github.com/nodejs/node/pull/61298
Fixes: https://github.com/nodejs/node/issues/61268
Reviewed-By: René <contact.9a5d6388@renegade334.me.uk>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
This commit is contained in:
Mert Can Altin authored and GitHub committed 2026-02-27 19:00:28 +00:00
1 parent cc6c18802d
commit 35d3bc8fe8
5 files changed
+707

No files matched your search

+49
View File
@@ -163,6 +163,23 @@ changes:
language features that allow ordinary SQL to deliberately corrupt the database file are disabled.
The defensive flag can also be set using `enableDefensive()`.
**Default:** `true`.
* `limits` {Object} Configuration for various SQLite limits. These limits
can be used to prevent excessive resource consumption when handling
potentially malicious input. See [Run-Time Limits][] and [Limit Constants][]
in the SQLite documentation for details. Default values are determined by
SQLite's compile-time defaults and may vary depending on how SQLite was
built. The following properties are supported:
* `length` {number} Maximum length of a string or BLOB.
* `sqlLength` {number} Maximum length of an SQL statement.
* `column` {number} Maximum number of columns.
* `exprDepth` {number} Maximum depth of an expression tree.
* `compoundSelect` {number} Maximum number of terms in a compound SELECT.
* `vdbeOp` {number} Maximum number of VDBE instructions.
* `functionArg` {number} Maximum number of function arguments.
* `attach` {number} Maximum number of attached databases.
* `likePatternLength` {number} Maximum length of a LIKE pattern.
* `variableNumber` {number} Maximum number of SQL variables.
* `triggerDepth` {number} Maximum trigger recursion depth.
Constructs a new `DatabaseSync` instance.
@@ -451,6 +468,36 @@ added:
* Type: {boolean} Whether the database is currently within a transaction. This method
is a wrapper around [`sqlite3_get_autocommit()`][].
### `database.limits`
<!-- YAML
added: REPLACEME
-->
* Type: {Object}
An object for getting and setting SQLite database limits at runtime.
Each property corresponds to an SQLite limit and can be read or written.
```js
const db = new DatabaseSync(':memory:');
// Read current limit
console.log(db.limits.length);
// Set a new limit
db.limits.sqlLength = 100000;
// Reset a limit to its compile-time maximum
db.limits.sqlLength = Infinity;
```
Available properties: `length`, `sqlLength`, `column`, `exprDepth`,
`compoundSelect`, `vdbeOp`, `functionArg`, `attach`, `likePatternLength`,
`variableNumber`, `triggerDepth`.
Setting a property to `Infinity` resets the limit to its compile-time maximum value.
### `database.open()`
<!-- YAML
@@ -1478,6 +1525,8 @@ callback function to indicate what type of operation is being authorized.
[Changesets and Patchsets]: https://www.sqlite.org/sessionintro.html#changesets_and_patchsets
[Constants Passed To The Conflict Handler]: https://www.sqlite.org/session/c_changeset_conflict.html
[Constants Returned From The Conflict Handler]: https://www.sqlite.org/session/c_changeset_abort.html
[Limit Constants]: https://www.sqlite.org/c3ref/c_limit_attached.html
[Run-Time Limits]: https://www.sqlite.org/c3ref/limit.html
[SQL injection]: https://en.wikipedia.org/wiki/SQL_injection
[Type conversion between JavaScript and SQLite]: #type-conversion-between-javascript-and-sqlite
[`ATTACH DATABASE`]: https://www.sqlite.org/lang_attach.html
+2
View File
@@ -237,6 +237,7 @@
V(kind_string, "kind") \
V(last_insert_rowid_string, "lastInsertRowid") \
V(length_string, "length") \
V(limits_string, "limits") \
V(library_string, "library") \
V(loop_count, "loopCount") \
V(max_buffer_string, "maxBuffer") \
@@ -435,6 +436,7 @@
V(socketaddress_constructor_template, v8::FunctionTemplate) \
V(space_stats_template, v8::DictionaryTemplate) \
V(sqlite_column_template, v8::DictionaryTemplate) \
V(sqlite_limits_template, v8::ObjectTemplate) \
V(sqlite_run_result_template, v8::DictionaryTemplate) \
V(sqlite_statement_sync_constructor_template, v8::FunctionTemplate) \
V(sqlite_statement_sync_iterator_constructor_template, v8::FunctionTemplate) \
+272
View File
@@ -12,7 +12,10 @@
#include "threadpoolwork-inl.h"
#include "util-inl.h"
#include <array>
#include <cinttypes>
#include <cmath>
#include <limits>
namespace node {
namespace sqlite {
@@ -36,6 +39,7 @@ using v8::Global;
using v8::HandleScope;
using v8::Int32;
using v8::Integer;
using v8::Intercepted;
using v8::Isolate;
using v8::JustVoid;
using v8::Local;
@@ -43,12 +47,16 @@ using v8::LocalVector;
using v8::Maybe;
using v8::MaybeLocal;
using v8::Name;
using v8::NamedPropertyHandlerConfiguration;
using v8::NewStringType;
using v8::Nothing;
using v8::Null;
using v8::Number;
using v8::Object;
using v8::ObjectTemplate;
using v8::Promise;
using v8::PropertyCallbackInfo;
using v8::PropertyHandlerFlags;
using v8::SideEffectType;
using v8::String;
using v8::TryCatch;
@@ -133,6 +141,16 @@ Local<DictionaryTemplate> getLazyIterTemplate(Environment* env) {
}
} // namespace
// Helper function to find limit info from JS property name
static constexpr const LimitInfo* GetLimitInfoFromName(std::string_view name) {
for (const auto& info : kLimitMapping) {
if (name == info.js_name) {
return &info;
}
}
return nullptr;
}
inline MaybeLocal<Object> CreateSQLiteError(Isolate* isolate,
const char* message) {
Local<String> js_msg;
@@ -668,6 +686,175 @@ void UserDefinedFunction::xDestroy(void* self) {
delete static_cast<UserDefinedFunction*>(self);
}
DatabaseSyncLimits::DatabaseSyncLimits(Environment* env,
Local<Object> object,
BaseObjectWeakPtr<DatabaseSync> database)
: BaseObject(env, object), database_(std::move(database)) {
MakeWeak();
}
DatabaseSyncLimits::~DatabaseSyncLimits() = default;
void DatabaseSyncLimits::MemoryInfo(MemoryTracker* tracker) const {
tracker->TrackField("database", database_);
}
Local<ObjectTemplate> DatabaseSyncLimits::GetTemplate(Environment* env) {
Local<ObjectTemplate> tmpl = env->sqlite_limits_template();
if (!tmpl.IsEmpty()) return tmpl;
Isolate* isolate = env->isolate();
tmpl = ObjectTemplate::New(isolate);
tmpl->SetInternalFieldCount(DatabaseSyncLimits::kInternalFieldCount);
tmpl->SetHandler(NamedPropertyHandlerConfiguration(
LimitsGetter,
LimitsSetter,
LimitsQuery,
nullptr, // deleter - not allowed
LimitsEnumerator,
nullptr, // definer
nullptr,
Local<Value>(),
PropertyHandlerFlags::kHasNoSideEffect));
env->set_sqlite_limits_template(tmpl);
return tmpl;
}
Intercepted DatabaseSyncLimits::LimitsGetter(
Local<Name> property, const PropertyCallbackInfo<Value>& info) {
// Skip symbols
if (!property->IsString()) {
return Intercepted::kNo;
}
DatabaseSyncLimits* limits;
ASSIGN_OR_RETURN_UNWRAP(&limits, info.This(), Intercepted::kNo);
Environment* env = limits->env();
Isolate* isolate = env->isolate();
Utf8Value prop_name(isolate, property);
const LimitInfo* limit_info = GetLimitInfoFromName(prop_name.ToStringView());
if (limit_info == nullptr) {
return Intercepted::kNo; // Unknown property, let default handling occur
}
if (!limits->database_ || !limits->database_->IsOpen()) {
THROW_ERR_INVALID_STATE(env, "database is not open");
return Intercepted::kYes;
}
int current_value = sqlite3_limit(
limits->database_->Connection(), limit_info->sqlite_limit_id, -1);
info.GetReturnValue().Set(Integer::New(isolate, current_value));
return Intercepted::kYes;
}
Intercepted DatabaseSyncLimits::LimitsSetter(
Local<Name> property,
Local<Value> value,
const PropertyCallbackInfo<void>& info) {
if (!property->IsString()) {
return Intercepted::kNo;
}
DatabaseSyncLimits* limits;
ASSIGN_OR_RETURN_UNWRAP(&limits, info.This(), Intercepted::kNo);
Environment* env = limits->env();
Isolate* isolate = env->isolate();
Utf8Value prop_name(isolate, property);
const LimitInfo* limit_info = GetLimitInfoFromName(prop_name.ToStringView());
if (limit_info == nullptr) {
return Intercepted::kNo;
}
if (!limits->database_ || !limits->database_->IsOpen()) {
THROW_ERR_INVALID_STATE(env, "database is not open");
return Intercepted::kYes;
}
if (!value->IsNumber()) {
THROW_ERR_INVALID_ARG_TYPE(
isolate, "Limit value must be a non-negative integer or Infinity.");
return Intercepted::kYes;
}
const double num_value = value.As<Number>()->Value();
int new_value;
if (std::isinf(num_value) && num_value > 0) {
// Positive Infinity resets the limit to the compile-time maximum
new_value = std::numeric_limits<int>::max();
} else if (!value->IsInt32()) {
THROW_ERR_INVALID_ARG_TYPE(
isolate, "Limit value must be a non-negative integer or Infinity.");
return Intercepted::kYes;
} else {
new_value = value.As<Int32>()->Value();
if (new_value < 0) {
THROW_ERR_OUT_OF_RANGE(isolate, "Limit value must be non-negative.");
return Intercepted::kYes;
}
}
sqlite3_limit(
limits->database_->Connection(), limit_info->sqlite_limit_id, new_value);
return Intercepted::kYes;
}
Intercepted DatabaseSyncLimits::LimitsQuery(
Local<Name> property, const PropertyCallbackInfo<Integer>& info) {
if (!property->IsString()) {
return Intercepted::kNo;
}
Isolate* isolate = info.GetIsolate();
Utf8Value prop_name(isolate, property);
const LimitInfo* limit_info = GetLimitInfoFromName(prop_name.ToStringView());
if (!limit_info) {
return Intercepted::kNo;
}
// Property exists and is writable
info.GetReturnValue().Set(
Integer::New(isolate, v8::PropertyAttribute::DontDelete));
return Intercepted::kYes;
}
void DatabaseSyncLimits::LimitsEnumerator(
const PropertyCallbackInfo<Array>& info) {
Isolate* isolate = info.GetIsolate();
LocalVector<Value> names(isolate);
for (const auto& [js_name, sqlite_limit_id] : kLimitMapping) {
Local<String> name;
if (!String::NewFromUtf8(
isolate, js_name.data(), NewStringType::kNormal, js_name.size())
.ToLocal(&name)) {
return;
}
names.push_back(name);
}
info.GetReturnValue().Set(Array::New(isolate, names.data(), names.size()));
}
BaseObjectPtr<DatabaseSyncLimits> DatabaseSyncLimits::Create(
Environment* env, BaseObjectWeakPtr<DatabaseSync> database) {
Local<Object> obj;
if (!GetTemplate(env)->NewInstance(env->context()).ToLocal(&obj)) {
return nullptr;
}
return MakeBaseObject<DatabaseSyncLimits>(env, obj, std::move(database));
}
DatabaseSync::DatabaseSync(Environment* env,
Local<Object> object,
DatabaseOpenConfiguration&& open_config,
@@ -766,6 +953,14 @@ bool DatabaseSync::Open() {
sqlite3_busy_timeout(connection_, open_config_.get_timeout());
// Apply initial limits
for (const auto& [js_name, sqlite_limit_id] : kLimitMapping) {
const auto& limit_value = open_config_.initial_limits()[sqlite_limit_id];
if (limit_value.has_value()) {
sqlite3_limit(connection_, sqlite_limit_id, *limit_value);
}
}
if (allow_load_extension_) {
if (env()->permission()->enabled()) [[unlikely]] {
THROW_ERR_LOAD_SQLITE_EXTENSION(env(),
@@ -1091,6 +1286,59 @@ void DatabaseSync::New(const FunctionCallbackInfo<Value>& args) {
}
open_config.set_enable_defensive(defensive_v.As<Boolean>()->Value());
}
// Parse limits option
Local<Value> limits_v;
if (!options->Get(env->context(), env->limits_string())
.ToLocal(&limits_v)) {
return;
}
if (!limits_v->IsUndefined()) {
if (!limits_v->IsObject()) {
THROW_ERR_INVALID_ARG_TYPE(
env->isolate(),
"The \"options.limits\" argument must be an object.");
return;
}
Local<Object> limits_obj = limits_v.As<Object>();
// Iterate through known limit names and extract values
for (const auto& [js_name, sqlite_limit_id] : kLimitMapping) {
Local<String> key;
if (!String::NewFromUtf8(env->isolate(),
js_name.data(),
NewStringType::kNormal,
js_name.size())
.ToLocal(&key)) {
return;
}
Local<Value> val;
if (!limits_obj->Get(env->context(), key).ToLocal(&val)) {
return;
}
if (!val->IsUndefined()) {
if (!val->IsInt32()) {
std::string msg = "The \"options.limits." + std::string(js_name) +
"\" argument must be an integer.";
THROW_ERR_INVALID_ARG_TYPE(env->isolate(), msg);
return;
}
int limit_val = val.As<Int32>()->Value();
if (limit_val < 0) {
std::string msg = "The \"options.limits." + std::string(js_name) +
"\" argument must be non-negative.";
THROW_ERR_OUT_OF_RANGE(env->isolate(), msg);
return;
}
open_config.set_initial_limit(sqlite_limit_id, limit_val);
}
}
}
}
new DatabaseSync(
@@ -1118,6 +1366,26 @@ void DatabaseSync::IsTransactionGetter(
args.GetReturnValue().Set(sqlite3_get_autocommit(db->connection_) == 0);
}
void DatabaseSync::LimitsGetter(const FunctionCallbackInfo<Value>& args) {
DatabaseSync* db;
ASSIGN_OR_RETURN_UNWRAP(&db, args.This());
Environment* env = Environment::GetCurrent(args);
Local<Value> limits_val =
db->object()->GetInternalField(kLimitsObject).template As<Value>();
if (limits_val->IsUndefined()) {
BaseObjectPtr<DatabaseSyncLimits> limits =
DatabaseSyncLimits::Create(env, BaseObjectWeakPtr<DatabaseSync>(db));
if (limits) {
db->object()->SetInternalField(kLimitsObject, limits->object());
args.GetReturnValue().Set(limits->object());
}
} else {
args.GetReturnValue().Set(limits_val);
}
}
void DatabaseSync::Close(const FunctionCallbackInfo<Value>& args) {
DatabaseSync* db;
ASSIGN_OR_RETURN_UNWRAP(&db, args.This());
@@ -3496,6 +3764,10 @@ static void Initialize(Local<Object> target,
db_tmpl,
FIXED_ONE_BYTE_STRING(isolate, "isTransaction"),
DatabaseSync::IsTransactionGetter);
SetSideEffectFreeGetter(isolate,
db_tmpl,
FIXED_ONE_BYTE_STRING(isolate, "limits"),
DatabaseSync::LimitsGetter);
Local<String> sqlite_type_key = FIXED_ONE_BYTE_STRING(isolate, "sqlite-type");
Local<v8::Symbol> sqlite_type_symbol =
v8::Symbol::For(isolate, sqlite_type_key);
+80
View File
@@ -9,13 +9,48 @@
#include "sqlite3.h"
#include "util.h"
#include <array>
#include <list>
#include <map>
#include <optional>
#include <string_view>
#include <unordered_set>
namespace node {
namespace sqlite {
// Mapping from JavaScript property names to SQLite limit constants
struct LimitInfo {
std::string_view js_name;
int sqlite_limit_id;
};
inline constexpr std::array<LimitInfo, 11> kLimitMapping = {{
{"length", SQLITE_LIMIT_LENGTH},
{"sqlLength", SQLITE_LIMIT_SQL_LENGTH},
{"column", SQLITE_LIMIT_COLUMN},
{"exprDepth", SQLITE_LIMIT_EXPR_DEPTH},
{"compoundSelect", SQLITE_LIMIT_COMPOUND_SELECT},
{"vdbeOp", SQLITE_LIMIT_VDBE_OP},
{"functionArg", SQLITE_LIMIT_FUNCTION_ARG},
{"attach", SQLITE_LIMIT_ATTACHED},
{"likePatternLength", SQLITE_LIMIT_LIKE_PATTERN_LENGTH},
{"variableNumber", SQLITE_LIMIT_VARIABLE_NUMBER},
{"triggerDepth", SQLITE_LIMIT_TRIGGER_DEPTH},
}};
constexpr bool CheckLimitIndices() {
for (size_t i = 0; i < kLimitMapping.size(); ++i) {
if (kLimitMapping[i].sqlite_limit_id != static_cast<int>(i)) {
return false;
}
}
return true;
}
static_assert(
CheckLimitIndices(),
"Each kLimitMapping entry's sqlite_limit_id must match its index");
class DatabaseOpenConfiguration {
public:
explicit DatabaseOpenConfiguration(std::string&& location)
@@ -69,6 +104,15 @@ class DatabaseOpenConfiguration {
inline bool get_enable_defensive() const { return defensive_; }
inline void set_initial_limit(int sqlite_limit_id, int value) {
initial_limits_.at(sqlite_limit_id) = value;
}
inline const std::array<std::optional<int>, kLimitMapping.size()>&
initial_limits() const {
return initial_limits_;
}
private:
std::string location_;
bool read_only_ = false;
@@ -80,9 +124,11 @@ class DatabaseOpenConfiguration {
bool allow_bare_named_params_ = true;
bool allow_unknown_named_params_ = false;
bool defensive_ = true;
std::array<std::optional<int>, kLimitMapping.size()> initial_limits_{};
};
class DatabaseSync;
class DatabaseSyncLimits;
class StatementSyncIterator;
class StatementSync;
class BackupJob;
@@ -118,6 +164,7 @@ class DatabaseSync : public BaseObject {
public:
enum InternalFields {
kAuthorizerCallback = BaseObject::kInternalFieldCount,
kLimitsObject,
kInternalFieldCount
};
@@ -146,6 +193,7 @@ class DatabaseSync : public BaseObject {
static void EnableLoadExtension(
const v8::FunctionCallbackInfo<v8::Value>& args);
static void EnableDefensive(const v8::FunctionCallbackInfo<v8::Value>& args);
static void LimitsGetter(const v8::FunctionCallbackInfo<v8::Value>& args);
static void LoadExtension(const v8::FunctionCallbackInfo<v8::Value>& args);
static void SetAuthorizer(const v8::FunctionCallbackInfo<v8::Value>& args);
static int AuthorizerCallback(void* user_data,
@@ -195,6 +243,7 @@ class DatabaseSync : public BaseObject {
std::set<sqlite3_session*> sessions_;
std::unordered_set<StatementSync*> statements_;
friend class DatabaseSyncLimits;
friend class Session;
friend class SQLTagStore;
friend class StatementExecutionHelper;
@@ -356,6 +405,37 @@ class UserDefinedFunction {
bool use_bigint_args_;
};
class DatabaseSyncLimits : public BaseObject {
public:
DatabaseSyncLimits(Environment* env,
v8::Local<v8::Object> object,
BaseObjectWeakPtr<DatabaseSync> database);
~DatabaseSyncLimits() override;
void MemoryInfo(MemoryTracker* tracker) const override;
static v8::Local<v8::ObjectTemplate> GetTemplate(Environment* env);
static BaseObjectPtr<DatabaseSyncLimits> Create(
Environment* env, BaseObjectWeakPtr<DatabaseSync> database);
static v8::Intercepted LimitsGetter(
v8::Local<v8::Name> property,
const v8::PropertyCallbackInfo<v8::Value>& info);
static v8::Intercepted LimitsSetter(
v8::Local<v8::Name> property,
v8::Local<v8::Value> value,
const v8::PropertyCallbackInfo<void>& info);
static v8::Intercepted LimitsQuery(
v8::Local<v8::Name> property,
const v8::PropertyCallbackInfo<v8::Integer>& info);
static void LimitsEnumerator(const v8::PropertyCallbackInfo<v8::Array>& info);
SET_MEMORY_INFO_NAME(DatabaseSyncLimits)
SET_SELF_SIZE(DatabaseSyncLimits)
private:
BaseObjectWeakPtr<DatabaseSync> database_;
};
} // namespace sqlite
} // namespace node
+304
View File
@@ -0,0 +1,304 @@
'use strict';
const { skipIfSQLiteMissing } = require('../common');
skipIfSQLiteMissing();
const { DatabaseSync } = require('node:sqlite');
const { suite, test } = require('node:test');
suite('DatabaseSync limits', () => {
test('limits object has expected properties with positive values', (t) => {
const db = new DatabaseSync(':memory:');
const expectedProperties = [
'length',
'sqlLength',
'column',
'exprDepth',
'compoundSelect',
'vdbeOp',
'functionArg',
'attach',
'likePatternLength',
'variableNumber',
'triggerDepth',
];
for (const prop of expectedProperties) {
t.assert.strictEqual(typeof db.limits[prop], 'number',
`${prop} should be a number`);
t.assert.ok(db.limits[prop] > 0,
`${prop} should be positive`);
}
});
test('constructor accepts limits option', (t) => {
const db = new DatabaseSync(':memory:', {
limits: {
length: 500000,
sqlLength: 50000,
column: 100,
exprDepth: 50,
compoundSelect: 10,
vdbeOp: 10000,
functionArg: 8,
attach: 5,
likePatternLength: 1000,
variableNumber: 100,
triggerDepth: 5,
}
});
t.assert.strictEqual(db.limits.length, 500000);
t.assert.strictEqual(db.limits.sqlLength, 50000);
t.assert.strictEqual(db.limits.column, 100);
t.assert.strictEqual(db.limits.exprDepth, 50);
t.assert.strictEqual(db.limits.compoundSelect, 10);
t.assert.strictEqual(db.limits.vdbeOp, 10000);
t.assert.strictEqual(db.limits.functionArg, 8);
t.assert.strictEqual(db.limits.attach, 5);
t.assert.strictEqual(db.limits.likePatternLength, 1000);
t.assert.strictEqual(db.limits.variableNumber, 100);
t.assert.strictEqual(db.limits.triggerDepth, 5);
});
test('getter returns current limit value', (t) => {
const db = new DatabaseSync(':memory:');
t.assert.strictEqual(typeof db.limits.length, 'number');
t.assert.ok(db.limits.length > 0);
t.assert.strictEqual(typeof db.limits.sqlLength, 'number');
t.assert.ok(db.limits.sqlLength > 0);
});
test('setter modifies limit value', (t) => {
const db = new DatabaseSync(':memory:');
db.limits.length = 100000;
t.assert.strictEqual(db.limits.length, 100000);
db.limits.sqlLength = 50000;
t.assert.strictEqual(db.limits.sqlLength, 50000);
db.limits.column = 50;
t.assert.strictEqual(db.limits.column, 50);
});
test('Infinity resets limit to maximum', (t) => {
const db = new DatabaseSync(':memory:');
const originalLength = db.limits.length;
// Set to a lower value
db.limits.length = 100;
t.assert.strictEqual(db.limits.length, 100);
// Reset to maximum using Infinity
db.limits.length = Infinity;
t.assert.strictEqual(db.limits.length, originalLength);
});
test('throws on invalid argument type', (t) => {
const db = new DatabaseSync(':memory:');
t.assert.throws(() => {
db.limits.length = 'invalid';
}, {
name: 'TypeError',
message: /Limit value must be a non-negative integer or Infinity/,
});
});
test('throws on negative value', (t) => {
const db = new DatabaseSync(':memory:');
t.assert.throws(() => {
db.limits.length = -1;
}, {
name: 'RangeError',
message: /Limit value must be non-negative/,
});
});
test('throws on null value', (t) => {
const db = new DatabaseSync(':memory:');
t.assert.throws(() => {
db.limits.length = null;
}, {
name: 'TypeError',
message: /Limit value must be a non-negative integer or Infinity/,
});
});
test('throws on negative Infinity', (t) => {
const db = new DatabaseSync(':memory:');
t.assert.throws(() => {
db.limits.length = -Infinity;
}, {
name: 'TypeError',
message: /Limit value must be a non-negative integer or Infinity/,
});
});
test('throws on getter access after close', (t) => {
const db = new DatabaseSync(':memory:');
db.close();
t.assert.throws(() => {
return db.limits.length;
}, {
code: 'ERR_INVALID_STATE',
message: /database is not open/,
});
});
test('throws on setter access after close', (t) => {
const db = new DatabaseSync(':memory:');
db.close();
t.assert.throws(() => {
db.limits.length = 100;
}, {
code: 'ERR_INVALID_STATE',
message: /database is not open/,
});
});
test('limits object is enumerable', (t) => {
const db = new DatabaseSync(':memory:');
const keys = Object.keys(db.limits);
t.assert.ok(keys.includes('length'));
t.assert.ok(keys.includes('sqlLength'));
t.assert.ok(keys.includes('column'));
t.assert.ok(keys.includes('exprDepth'));
t.assert.ok(keys.includes('compoundSelect'));
t.assert.ok(keys.includes('vdbeOp'));
t.assert.ok(keys.includes('functionArg'));
t.assert.ok(keys.includes('attach'));
t.assert.ok(keys.includes('likePatternLength'));
t.assert.ok(keys.includes('variableNumber'));
t.assert.ok(keys.includes('triggerDepth'));
});
test('throws on invalid limits option type', (t) => {
t.assert.throws(() => {
new DatabaseSync(':memory:', { limits: 'invalid' });
}, {
name: 'TypeError',
message: /options\.limits.*must be an object/,
});
});
test('throws on invalid limit value type in constructor', (t) => {
t.assert.throws(() => {
new DatabaseSync(':memory:', { limits: { length: 'invalid' } });
}, {
name: 'TypeError',
message: /options\.limits\.length.*must be an integer/,
});
});
test('throws on negative limit value in constructor', (t) => {
t.assert.throws(() => {
new DatabaseSync(':memory:', { limits: { length: -100 } });
}, {
name: 'RangeError',
message: /options\.limits\.length.*must be non-negative/,
});
});
test('throws on Infinity limit value in constructor', (t) => {
t.assert.throws(() => {
new DatabaseSync(':memory:', { limits: { length: Infinity } });
}, {
name: 'TypeError',
message: /options\.limits\.length.*must be an integer/,
});
});
test('partial limits in constructor', (t) => {
const db = new DatabaseSync(':memory:', {
limits: {
length: 100000,
}
});
t.assert.strictEqual(db.limits.length, 100000);
t.assert.strictEqual(typeof db.limits.sqlLength, 'number');
});
test('throws when exceeding column limit', (t) => {
const db = new DatabaseSync(':memory:', {
limits: {
column: 10,
}
});
db.exec('CREATE TABLE t1 (c1, c2, c3, c4, c5, c6, c7, c8, c9, c10)');
t.assert.throws(() => {
db.exec('CREATE TABLE t2 (c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11)');
}, {
message: /too many columns/,
});
});
test('throws when exceeding attach limit', (t) => {
const db = new DatabaseSync(':memory:', {
limits: {
attach: 0,
}
});
t.assert.throws(() => {
db.exec("ATTACH DATABASE ':memory:' AS db1");
}, {
message: /too many attached databases/,
});
});
test('throws when exceeding variable number limit', (t) => {
const db = new DatabaseSync(':memory:', {
limits: {
variableNumber: 2,
}
});
t.assert.throws(() => {
const stmt = db.prepare('SELECT ?, ?, ?');
stmt.all(1, 2, 3);
}, {
message: /too many SQL variables/,
});
});
test('throws when exceeding compound select limit', (t) => {
const db = new DatabaseSync(':memory:', {
limits: {
compoundSelect: 1,
}
});
t.assert.throws(() => {
db.exec('SELECT 1 UNION SELECT 2 UNION SELECT 3');
}, {
message: /too many terms in compound SELECT/,
});
});
test('throws when exceeding function arg limit', (t) => {
const db = new DatabaseSync(':memory:', {
limits: {
functionArg: 2,
}
});
t.assert.throws(() => {
db.exec('SELECT max(1, 2, 3)');
}, {
message: /too many arguments on function max/,
});
});
test('setter applies limit to SQLite immediately', (t) => {
const db = new DatabaseSync(':memory:');
db.limits.attach = 0;
t.assert.throws(() => {
db.exec("ATTACH DATABASE ':memory:' AS db1");
}, {
message: /too many attached databases/,
});
});
});