mirror of
https://github.com/nodejs/node.git
synced 2026-10-14 10:26:26 -04:00
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:
1 parent
cc6c18802d
commit
35d3bc8fe8
5 files changed
+707
No files matched your search
@@ -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
|
||||
|
||||
@@ -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) \
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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/,
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user