sqlite: add tagged template

This pr introduces the support for tagged templates
And an LRU to cache the templates. We introduced a
new object called SqlTagStore that holds the ref
to Lru. This acts as the main object that allows
us to use tagged templates.

PR-URL: https://github.com/nodejs/node/pull/58748
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Edy Silva <edigleyssonsilva@gmail.com>
This commit is contained in:
0hm☘️ 2025-09-17 22:24:11 +05:30 committed by GitHub
parent 65bee02cca
commit 97e55f8ea2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 1091 additions and 142 deletions

View File

@ -361,6 +361,53 @@ added: v22.5.0
Compiles a SQL statement into a [prepared statement][]. This method is a wrapper Compiles a SQL statement into a [prepared statement][]. This method is a wrapper
around [`sqlite3_prepare_v2()`][]. around [`sqlite3_prepare_v2()`][].
### `database.createSQLTagStore([maxSize])`
<!-- YAML
added: REPLACEME
-->
* `maxSize` {integer} The maximum number of prepared statements to cache.
**Default:** `1000`.
* Returns: {SQLTagStore} A new SQL tag store for caching prepared statements.
Creates a new `SQLTagStore`, which is an LRU (Least Recently Used) cache for
storing prepared statements. This allows for the efficient reuse of prepared
statements by tagging them with a unique identifier.
When a tagged SQL literal is executed, the `SQLTagStore` checks if a prepared
statement for that specific SQL string already exists in the cache. If it does,
the cached statement is used. If not, a new prepared statement is created,
executed, and then stored in the cache for future use. This mechanism helps to
avoid the overhead of repeatedly parsing and preparing the same SQL statements.
```mjs
import { DatabaseSync } from 'node:sqlite';
const db = new DatabaseSync(':memory:');
const sql = db.createSQLTagStore();
db.exec('CREATE TABLE users (id INT, name TEXT)');
// Using the 'run' method to insert data.
// The tagged literal is used to identify the prepared statement.
sql.run`INSERT INTO users VALUES (1, 'Alice')`;
sql.run`INSERT INTO users VALUES (2, 'Bob')`;
// Using the 'get' method to retrieve a single row.
const id = 1;
const user = sql.get`SELECT * FROM users WHERE id = ${id}`;
console.log(user); // { id: 1, name: 'Alice' }
// Using the 'all' method to retrieve all rows.
const allUsers = sql.all`SELECT * FROM users ORDER BY id`;
console.log(allUsers);
// [
// { id: 1, name: 'Alice' },
// { id: 2, name: 'Bob' }
// ]
```
### `database.createSession([options])` ### `database.createSession([options])`
<!-- YAML <!-- YAML
@ -504,6 +551,120 @@ times with different bound values. Parameters also offer protection against
[SQL injection][] attacks. For these reasons, prepared statements are preferred [SQL injection][] attacks. For these reasons, prepared statements are preferred
over hand-crafted SQL strings when handling user input. over hand-crafted SQL strings when handling user input.
## Class: `SQLTagStore`
<!-- YAML
added: REPLACEME
-->
This class represents a single LRU (Least Recently Used) cache for storing
prepared statements.
Instances of this class are created via the database.createSQLTagStore() method,
not by using a constructor. The store caches prepared statements based on the
provided SQL query string. When the same query is seen again, the store
retrieves the cached statement and safely applies the new values through
parameter binding, thereby preventing attacks like SQL injection.
The cache has a maxSize that defaults to 1000 statements, but a custom size can
be provided (e.g., database.createSQLTagStore(100)). All APIs exposed by this
class execute synchronously.
### `sqlTagStore.all(sqlTemplate[, ...values])`
<!-- YAML
added: REPLACEME
-->
* `sqlTemplate` {Template Literal} A template literal containing the SQL query.
* `...values` {any} Values to be interpolated into the template literal.
* Returns: {Array} An array of objects representing the rows returned by the query.
Executes the given SQL query and returns all resulting rows as an array of objects.
### `sqlTagStore.get(sqlTemplate[, ...values])`
<!-- YAML
added: REPLACEME
-->
* `sqlTemplate` {Template Literal} A template literal containing the SQL query.
* `...values` {any} Values to be interpolated into the template literal.
* Returns: {Object | undefined} An object representing the first row returned by
the query, or `undefined` if no rows are returned.
Executes the given SQL query and returns the first resulting row as an object.
### `sqlTagStore.iterate(sqlTemplate[, ...values])`
<!-- YAML
added: REPLACEME
-->
* `sqlTemplate` {Template Literal} A template literal containing the SQL query.
* `...values` {any} Values to be interpolated into the template literal.
* Returns: {Iterator} An iterator that yields objects representing the rows returned by the query.
Executes the given SQL query and returns an iterator over the resulting rows.
### `sqlTagStore.run(sqlTemplate[, ...values])`
<!-- YAML
added: REPLACEME
-->
* `sqlTemplate` {Template Literal} A template literal containing the SQL query.
* `...values` {any} Values to be interpolated into the template literal.
* Returns: {Object} An object containing information about the execution, including `changes` and `lastInsertRowid`.
Executes the given SQL query, which is expected to not return any rows (e.g., INSERT, UPDATE, DELETE).
### `sqlTagStore.size()`
<!-- YAML
added: REPLACEME
-->
* Returns: {integer} The number of prepared statements currently in the cache.
A read-only property that returns the number of prepared statements currently in the cache.
### `sqlTagStore.capacity`
<!-- YAML
added: REPLACEME
-->
* Returns: {integer} The maximum number of prepared statements the cache can hold.
A read-only property that returns the maximum number of prepared statements the cache can hold.
### `sqlTagStore.db`
<!-- YAML
added: REPLACEME
-->
* {DatabaseSync} The `DatabaseSync` instance that created this `SQLTagStore`.
A read-only property that returns the `DatabaseSync` object associated with this `SQLTagStore`.
### `sqlTagStore.reset()`
<!-- YAML
added: REPLACEME
-->
Resets the LRU cache, clearing all stored prepared statements.
### `sqlTagStore.clear()`
<!-- YAML
added: REPLACEME
-->
An alias for `sqlTagStore.reset()`.
### `statement.all([namedParameters][, ...anonymousParameters])` ### `statement.all([namedParameters][, ...anonymousParameters])`
<!-- YAML <!-- YAML

69
src/lru_cache-inl.h Normal file
View File

@ -0,0 +1,69 @@
#ifndef SRC_LRU_CACHE_INL_H_
#define SRC_LRU_CACHE_INL_H_
#include <list>
#include <unordered_map>
#include <utility>
template <typename key_t, typename value_t>
class LRUCache {
public:
using key_value_pair_t = typename std::pair<key_t, value_t>;
using iterator = typename std::list<key_value_pair_t>::iterator;
using const_iterator = typename std::list<key_value_pair_t>::const_iterator;
const_iterator begin() const { return lru_list_.begin(); }
const_iterator end() const { return lru_list_.end(); }
explicit LRUCache(size_t capacity) : capacity_(capacity) {}
void Put(const key_t& key, const value_t& value) {
auto it = lookup_map_.find(key);
if (it != lookup_map_.end()) {
lru_list_.erase(it->second);
lookup_map_.erase(it);
}
lru_list_.push_front(std::make_pair(key, value));
lookup_map_[key] = lru_list_.begin();
if (lookup_map_.size() > capacity_) {
auto last = lru_list_.end();
last--;
lookup_map_.erase(last->first);
lru_list_.pop_back();
}
}
value_t& Get(const key_t& key) {
auto it = lookup_map_.find(key);
lru_list_.splice(lru_list_.begin(), lru_list_, it->second);
return it->second->second;
}
void Erase(const key_t& key) {
auto it = lookup_map_.find(key);
if (it != lookup_map_.end()) {
lru_list_.erase(it->second);
lookup_map_.erase(it);
}
}
bool Exists(const key_t& key) const { return lookup_map_.count(key) > 0; }
size_t Size() const { return lookup_map_.size(); }
size_t Capacity() const { return capacity_; }
void Clear() {
lru_list_.clear();
lookup_map_.clear();
}
private:
std::list<key_value_pair_t> lru_list_;
std::unordered_map<key_t, iterator> lookup_map_;
size_t capacity_;
};
#endif // SRC_LRU_CACHE_INL_H_

View File

@ -809,6 +809,29 @@ bool DatabaseSync::ShouldIgnoreSQLiteError() {
return ignore_next_sqlite_error_; return ignore_next_sqlite_error_;
} }
void DatabaseSync::CreateTagStore(const FunctionCallbackInfo<Value>& args) {
DatabaseSync* db = BaseObject::Unwrap<DatabaseSync>(args.This());
Environment* env = Environment::GetCurrent(args);
if (!db->IsOpen()) {
THROW_ERR_INVALID_STATE(env, "database is not open");
return;
}
int capacity = 1000;
if (args.Length() > 0 && args[0]->IsNumber()) {
capacity = args[0].As<Number>()->Value();
}
BaseObjectPtr<SQLTagStore> session =
SQLTagStore::Create(env, BaseObjectWeakPtr<DatabaseSync>(db), capacity);
if (!session) {
// Handle error if creation failed
THROW_ERR_SQLITE_ERROR(env->isolate(), "Failed to create SQLTagStore");
return;
}
args.GetReturnValue().Set(session->object());
}
std::optional<std::string> ValidateDatabasePath(Environment* env, std::optional<std::string> ValidateDatabasePath(Environment* env,
Local<Value> path, Local<Value> path,
const std::string& field_name) { const std::string& field_name) {
@ -2011,11 +2034,8 @@ bool StatementSync::BindValue(const Local<Value>& value, const int index) {
} }
MaybeLocal<Value> StatementSync::ColumnToValue(const int column) { MaybeLocal<Value> StatementSync::ColumnToValue(const int column) {
Isolate* isolate = env()->isolate(); return StatementExecutionHelper::ColumnToValue(
MaybeLocal<Value> js_val = MaybeLocal<Value>(); env(), statement_, column, use_big_ints_);
SQLITE_VALUE_TO_JS(
column, isolate, use_big_ints_, js_val, statement_, column);
return js_val;
} }
MaybeLocal<Name> StatementSync::ColumnNameToName(const int column) { MaybeLocal<Name> StatementSync::ColumnNameToName(const int column) {
@ -2028,22 +2048,203 @@ MaybeLocal<Name> StatementSync::ColumnNameToName(const int column) {
return String::NewFromUtf8(env()->isolate(), col_name).As<Name>(); return String::NewFromUtf8(env()->isolate(), col_name).As<Name>();
} }
MaybeLocal<Value> StatementExecutionHelper::ColumnToValue(Environment* env,
sqlite3_stmt* stmt,
const int column,
bool use_big_ints) {
Isolate* isolate = env->isolate();
MaybeLocal<Value> js_val = MaybeLocal<Value>();
SQLITE_VALUE_TO_JS(column, isolate, use_big_ints, js_val, stmt, column);
return js_val;
}
MaybeLocal<Name> StatementExecutionHelper::ColumnNameToName(Environment* env,
sqlite3_stmt* stmt,
const int column) {
const char* col_name = sqlite3_column_name(stmt, column);
if (col_name == nullptr) {
THROW_ERR_INVALID_STATE(env, "Cannot get name of column %d", column);
return MaybeLocal<Name>();
}
return String::NewFromUtf8(env->isolate(), col_name).As<Name>();
}
void StatementSync::MemoryInfo(MemoryTracker* tracker) const {} void StatementSync::MemoryInfo(MemoryTracker* tracker) const {}
Maybe<void> ExtractRowValues(Isolate* isolate, Maybe<void> ExtractRowValues(Environment* env,
sqlite3_stmt* stmt,
int num_cols, int num_cols,
StatementSync* stmt, bool use_big_ints,
LocalVector<Value>* row_values) { LocalVector<Value>* row_values) {
row_values->clear(); row_values->clear();
row_values->reserve(num_cols); row_values->reserve(num_cols);
for (int i = 0; i < num_cols; ++i) { for (int i = 0; i < num_cols; ++i) {
Local<Value> val; Local<Value> val;
if (!stmt->ColumnToValue(i).ToLocal(&val)) return Nothing<void>(); if (!StatementExecutionHelper::ColumnToValue(env, stmt, i, use_big_ints)
.ToLocal(&val)) {
return Nothing<void>();
}
row_values->emplace_back(val); row_values->emplace_back(val);
} }
return JustVoid(); return JustVoid();
} }
Local<Value> StatementExecutionHelper::All(Environment* env,
DatabaseSync* db,
sqlite3_stmt* stmt,
bool return_arrays,
bool use_big_ints) {
Isolate* isolate = env->isolate();
int r;
int num_cols = sqlite3_column_count(stmt);
LocalVector<Value> rows(isolate);
LocalVector<Value> row_values(isolate);
LocalVector<Name> row_keys(isolate);
while ((r = sqlite3_step(stmt)) == SQLITE_ROW) {
if (ExtractRowValues(env, stmt, num_cols, use_big_ints, &row_values)
.IsNothing()) {
return Undefined(isolate);
}
if (return_arrays) {
Local<Array> row_array =
Array::New(isolate, row_values.data(), row_values.size());
rows.emplace_back(row_array);
} else {
if (row_keys.size() == 0) {
row_keys.reserve(num_cols);
for (int i = 0; i < num_cols; ++i) {
Local<Name> key;
if (!ColumnNameToName(env, stmt, i).ToLocal(&key))
return Undefined(isolate);
row_keys.emplace_back(key);
}
}
DCHECK_EQ(row_keys.size(), row_values.size());
Local<Object> row_obj = Object::New(
isolate, Null(isolate), row_keys.data(), row_values.data(), num_cols);
rows.emplace_back(row_obj);
}
}
CHECK_ERROR_OR_THROW(isolate, db, r, SQLITE_DONE, Undefined(isolate));
return Array::New(isolate, rows.data(), rows.size());
}
Local<Object> StatementExecutionHelper::Run(Environment* env,
DatabaseSync* db,
sqlite3_stmt* stmt,
bool use_big_ints) {
Isolate* isolate = env->isolate();
sqlite3_step(stmt);
int r = sqlite3_reset(stmt);
CHECK_ERROR_OR_THROW(isolate, db, r, SQLITE_OK, Object::New(isolate));
Local<Object> result = Object::New(isolate);
sqlite3_int64 last_insert_rowid = sqlite3_last_insert_rowid(db->Connection());
sqlite3_int64 changes = sqlite3_changes64(db->Connection());
Local<Value> last_insert_rowid_val;
Local<Value> changes_val;
if (use_big_ints) {
last_insert_rowid_val = BigInt::New(isolate, last_insert_rowid);
changes_val = BigInt::New(isolate, changes);
} else {
last_insert_rowid_val = Number::New(isolate, last_insert_rowid);
changes_val = Number::New(isolate, changes);
}
if (result
->Set(env->context(),
env->last_insert_rowid_string(),
last_insert_rowid_val)
.IsNothing() ||
result->Set(env->context(), env->changes_string(), changes_val)
.IsNothing()) {
return Object::New(isolate);
}
return result;
}
BaseObjectPtr<StatementSyncIterator> StatementExecutionHelper::Iterate(
Environment* env, BaseObjectPtr<StatementSync> stmt) {
Local<Context> context = env->context();
Local<Object> global = context->Global();
Local<Value> js_iterator;
Local<Value> js_iterator_prototype;
if (!global->Get(context, env->iterator_string()).ToLocal(&js_iterator)) {
return BaseObjectPtr<StatementSyncIterator>();
}
if (!js_iterator.As<Object>()
->Get(context, env->prototype_string())
.ToLocal(&js_iterator_prototype)) {
return BaseObjectPtr<StatementSyncIterator>();
}
BaseObjectPtr<StatementSyncIterator> iter =
StatementSyncIterator::Create(env, stmt);
if (!iter) {
// Error in iterator creation, likely already threw in Create
return BaseObjectPtr<StatementSyncIterator>();
}
if (iter->object()
->GetPrototypeV2()
.As<Object>()
->SetPrototypeV2(context, js_iterator_prototype)
.IsNothing()) {
return BaseObjectPtr<StatementSyncIterator>();
}
return iter;
}
Local<Value> StatementExecutionHelper::Get(Environment* env,
DatabaseSync* db,
sqlite3_stmt* stmt,
bool return_arrays,
bool use_big_ints) {
Isolate* isolate = env->isolate();
auto reset = OnScopeLeave([&]() { sqlite3_reset(stmt); });
int r = sqlite3_step(stmt);
if (r == SQLITE_DONE) return Undefined(isolate);
if (r != SQLITE_ROW) {
THROW_ERR_SQLITE_ERROR(isolate, db);
return Undefined(isolate);
}
int num_cols = sqlite3_column_count(stmt);
if (num_cols == 0) {
return Undefined(isolate);
}
LocalVector<Value> row_values(isolate);
if (ExtractRowValues(env, stmt, num_cols, use_big_ints, &row_values)
.IsNothing()) {
return Undefined(isolate);
}
if (return_arrays) {
return Array::New(isolate, row_values.data(), row_values.size());
} else {
LocalVector<Name> keys(isolate);
keys.reserve(num_cols);
for (int i = 0; i < num_cols; ++i) {
MaybeLocal<Name> key = ColumnNameToName(env, stmt, i);
if (key.IsEmpty()) return Undefined(isolate);
keys.emplace_back(key.ToLocalChecked());
}
DCHECK_EQ(keys.size(), row_values.size());
return Object::New(
isolate, Null(isolate), keys.data(), row_values.data(), num_cols);
}
}
void StatementSync::All(const FunctionCallbackInfo<Value>& args) { void StatementSync::All(const FunctionCallbackInfo<Value>& args) {
StatementSync* stmt; StatementSync* stmt;
ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This()); ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This());
@ -2059,39 +2260,11 @@ void StatementSync::All(const FunctionCallbackInfo<Value>& args) {
} }
auto reset = OnScopeLeave([&]() { sqlite3_reset(stmt->statement_); }); auto reset = OnScopeLeave([&]() { sqlite3_reset(stmt->statement_); });
int num_cols = sqlite3_column_count(stmt->statement_); args.GetReturnValue().Set(StatementExecutionHelper::All(env,
LocalVector<Value> rows(isolate); stmt->db_.get(),
LocalVector<Value> row_values(isolate); stmt->statement_,
LocalVector<Name> row_keys(isolate); stmt->return_arrays_,
stmt->use_big_ints_));
while ((r = sqlite3_step(stmt->statement_)) == SQLITE_ROW) {
auto maybe_row_values =
ExtractRowValues(env->isolate(), num_cols, stmt, &row_values);
if (maybe_row_values.IsNothing()) return;
if (stmt->return_arrays_) {
Local<Array> row_array =
Array::New(isolate, row_values.data(), row_values.size());
rows.emplace_back(row_array);
} else {
if (row_keys.size() == 0) {
row_keys.reserve(num_cols);
for (int i = 0; i < num_cols; ++i) {
Local<Name> key;
if (!stmt->ColumnNameToName(i).ToLocal(&key)) return;
row_keys.emplace_back(key);
}
}
DCHECK_EQ(row_keys.size(), row_values.size());
Local<Object> row_obj = Object::New(
isolate, Null(isolate), row_keys.data(), row_values.data(), num_cols);
rows.emplace_back(row_obj);
}
}
CHECK_ERROR_OR_THROW(isolate, stmt->db_.get(), r, SQLITE_DONE, void());
args.GetReturnValue().Set(Array::New(isolate, rows.data(), rows.size()));
} }
void StatementSync::Iterate(const FunctionCallbackInfo<Value>& args) { void StatementSync::Iterate(const FunctionCallbackInfo<Value>& args) {
@ -2100,35 +2273,17 @@ void StatementSync::Iterate(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args); Environment* env = Environment::GetCurrent(args);
THROW_AND_RETURN_ON_BAD_STATE( THROW_AND_RETURN_ON_BAD_STATE(
env, stmt->IsFinalized(), "statement has been finalized"); env, stmt->IsFinalized(), "statement has been finalized");
auto isolate = env->isolate();
auto context = env->context();
int r = sqlite3_reset(stmt->statement_); int r = sqlite3_reset(stmt->statement_);
CHECK_ERROR_OR_THROW(isolate, stmt->db_.get(), r, SQLITE_OK, void()); CHECK_ERROR_OR_THROW(env->isolate(), stmt->db_.get(), r, SQLITE_OK, void());
if (!stmt->BindParams(args)) { if (!stmt->BindParams(args)) {
return; return;
} }
Local<Object> global = context->Global(); BaseObjectPtr<StatementSyncIterator> iter = StatementExecutionHelper::Iterate(
Local<Value> js_iterator; env, BaseObjectPtr<StatementSync>(stmt));
Local<Value> js_iterator_prototype;
if (!global->Get(context, env->iterator_string()).ToLocal(&js_iterator)) {
return;
}
if (!js_iterator.As<Object>()
->Get(context, env->prototype_string())
.ToLocal(&js_iterator_prototype)) {
return;
}
BaseObjectPtr<StatementSyncIterator> iter = if (!iter) {
StatementSyncIterator::Create(env, BaseObjectPtr<StatementSync>(stmt));
if (iter->object()
->GetPrototypeV2()
.As<Object>()
->SetPrototypeV2(context, js_iterator_prototype)
.IsNothing()) {
return; return;
} }
@ -2141,59 +2296,18 @@ void StatementSync::Get(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args); Environment* env = Environment::GetCurrent(args);
THROW_AND_RETURN_ON_BAD_STATE( THROW_AND_RETURN_ON_BAD_STATE(
env, stmt->IsFinalized(), "statement has been finalized"); env, stmt->IsFinalized(), "statement has been finalized");
Isolate* isolate = env->isolate();
int r = sqlite3_reset(stmt->statement_); int r = sqlite3_reset(stmt->statement_);
CHECK_ERROR_OR_THROW(isolate, stmt->db_.get(), r, SQLITE_OK, void()); CHECK_ERROR_OR_THROW(env->isolate(), stmt->db_.get(), r, SQLITE_OK, void());
if (!stmt->BindParams(args)) { if (!stmt->BindParams(args)) {
return; return;
} }
auto reset = OnScopeLeave([&]() { sqlite3_reset(stmt->statement_); }); args.GetReturnValue().Set(StatementExecutionHelper::Get(env,
r = sqlite3_step(stmt->statement_); stmt->db_.get(),
if (r == SQLITE_DONE) return; stmt->statement_,
if (r != SQLITE_ROW) { stmt->return_arrays_,
THROW_ERR_SQLITE_ERROR(isolate, stmt->db_.get()); stmt->use_big_ints_));
return;
}
int num_cols = sqlite3_column_count(stmt->statement_);
if (num_cols == 0) {
return;
}
if (stmt->return_arrays_) {
LocalVector<Value> array_values(isolate);
array_values.reserve(num_cols);
for (int i = 0; i < num_cols; ++i) {
Local<Value> val;
if (!stmt->ColumnToValue(i).ToLocal(&val)) return;
array_values.emplace_back(val);
}
Local<Array> result =
Array::New(isolate, array_values.data(), array_values.size());
args.GetReturnValue().Set(result);
} else {
LocalVector<Name> keys(isolate);
keys.reserve(num_cols);
LocalVector<Value> values(isolate);
values.reserve(num_cols);
for (int i = 0; i < num_cols; ++i) {
Local<Name> key;
if (!stmt->ColumnNameToName(i).ToLocal(&key)) return;
Local<Value> val;
if (!stmt->ColumnToValue(i).ToLocal(&val)) return;
keys.emplace_back(key);
values.emplace_back(val);
}
DCHECK_EQ(keys.size(), values.size());
Local<Object> result = Object::New(
isolate, Null(isolate), keys.data(), values.data(), num_cols);
args.GetReturnValue().Set(result);
}
} }
void StatementSync::Run(const FunctionCallbackInfo<Value>& args) { void StatementSync::Run(const FunctionCallbackInfo<Value>& args) {
@ -2209,35 +2323,8 @@ void StatementSync::Run(const FunctionCallbackInfo<Value>& args) {
return; return;
} }
sqlite3_step(stmt->statement_); args.GetReturnValue().Set(StatementExecutionHelper::Run(
r = sqlite3_reset(stmt->statement_); env, stmt->db_.get(), stmt->statement_, stmt->use_big_ints_));
CHECK_ERROR_OR_THROW(env->isolate(), stmt->db_.get(), r, SQLITE_OK, void());
Local<Object> result = Object::New(env->isolate());
sqlite3_int64 last_insert_rowid =
sqlite3_last_insert_rowid(stmt->db_->Connection());
sqlite3_int64 changes = sqlite3_changes64(stmt->db_->Connection());
Local<Value> last_insert_rowid_val;
Local<Value> changes_val;
if (stmt->use_big_ints_) {
last_insert_rowid_val = BigInt::New(env->isolate(), last_insert_rowid);
changes_val = BigInt::New(env->isolate(), changes);
} else {
last_insert_rowid_val = Number::New(env->isolate(), last_insert_rowid);
changes_val = Number::New(env->isolate(), changes);
}
if (result
->Set(env->context(),
env->last_insert_rowid_string(),
last_insert_rowid_val)
.IsNothing() ||
result->Set(env->context(), env->changes_string(), changes_val)
.IsNothing()) {
return;
}
args.GetReturnValue().Set(result);
} }
void StatementSync::Columns(const FunctionCallbackInfo<Value>& args) { void StatementSync::Columns(const FunctionCallbackInfo<Value>& args) {
@ -2390,6 +2477,17 @@ void IllegalConstructor(const FunctionCallbackInfo<Value>& args) {
THROW_ERR_ILLEGAL_CONSTRUCTOR(Environment::GetCurrent(args)); THROW_ERR_ILLEGAL_CONSTRUCTOR(Environment::GetCurrent(args));
} }
SQLTagStore::SQLTagStore(Environment* env,
Local<Object> object,
BaseObjectWeakPtr<DatabaseSync> database,
int capacity)
: BaseObject(env, object),
database_(std::move(database)),
sql_tags_(capacity),
capacity_(capacity) {
MakeWeak();
}
static inline void SetSideEffectFreeGetter( static inline void SetSideEffectFreeGetter(
Isolate* isolate, Isolate* isolate,
Local<FunctionTemplate> class_template, Local<FunctionTemplate> class_template,
@ -2407,6 +2505,291 @@ static inline void SetSideEffectFreeGetter(
name, getter, Local<FunctionTemplate>(), DontDelete); name, getter, Local<FunctionTemplate>(), DontDelete);
} }
SQLTagStore::~SQLTagStore() {}
Local<FunctionTemplate> SQLTagStore::GetConstructorTemplate(Environment* env) {
Isolate* isolate = env->isolate();
Local<FunctionTemplate> tmpl =
NewFunctionTemplate(isolate, IllegalConstructor);
tmpl->SetClassName(FIXED_ONE_BYTE_STRING(isolate, "SQLTagStore"));
tmpl->InstanceTemplate()->SetInternalFieldCount(
SQLTagStore::kInternalFieldCount);
SetProtoMethod(isolate, tmpl, "get", Get);
SetProtoMethod(isolate, tmpl, "all", All);
SetProtoMethod(isolate, tmpl, "iterate", Iterate);
SetProtoMethod(isolate, tmpl, "run", Run);
SetProtoMethod(isolate, tmpl, "clear", Clear);
SetProtoMethod(isolate, tmpl, "size", Size);
SetSideEffectFreeGetter(
isolate, tmpl, FIXED_ONE_BYTE_STRING(isolate, "capacity"), Capacity);
SetSideEffectFreeGetter(
isolate, tmpl, FIXED_ONE_BYTE_STRING(isolate, "db"), DatabaseGetter);
return tmpl;
}
BaseObjectPtr<SQLTagStore> SQLTagStore::Create(
Environment* env, BaseObjectWeakPtr<DatabaseSync> database, int capacity) {
Local<Object> obj;
if (!GetConstructorTemplate(env)
->InstanceTemplate()
->NewInstance(env->context())
.ToLocal(&obj)) {
return nullptr;
}
return MakeBaseObject<SQLTagStore>(env, obj, std::move(database), capacity);
}
void SQLTagStore::DatabaseGetter(const FunctionCallbackInfo<Value>& args) {
SQLTagStore* store;
ASSIGN_OR_RETURN_UNWRAP(&store, args.This());
args.GetReturnValue().Set(store->database_->object());
}
void SQLTagStore::Run(const FunctionCallbackInfo<Value>& info) {
SQLTagStore* session;
ASSIGN_OR_RETURN_UNWRAP(&session, info.This());
Environment* env = Environment::GetCurrent(info);
THROW_AND_RETURN_ON_BAD_STATE(
env, !session->database_->IsOpen(), "database is not open");
BaseObjectPtr<StatementSync> stmt = PrepareStatement(info);
if (!stmt) {
return;
}
uint32_t n_params = info.Length() - 1;
int r = sqlite3_reset(stmt->statement_);
CHECK_ERROR_OR_THROW(env->isolate(), stmt->db_.get(), r, SQLITE_OK, void());
int param_count = sqlite3_bind_parameter_count(stmt->statement_);
for (int i = 0; i < static_cast<int>(n_params) && i < param_count; ++i) {
Local<Value> value = info[i + 1];
if (!stmt->BindValue(value, i + 1)) {
return;
}
}
info.GetReturnValue().Set(StatementExecutionHelper::Run(
env, stmt->db_.get(), stmt->statement_, stmt->use_big_ints_));
}
void SQLTagStore::Iterate(const FunctionCallbackInfo<Value>& args) {
SQLTagStore* session;
ASSIGN_OR_RETURN_UNWRAP(&session, args.This());
Environment* env = Environment::GetCurrent(args);
THROW_AND_RETURN_ON_BAD_STATE(
env, !session->database_->IsOpen(), "database is not open");
BaseObjectPtr<StatementSync> stmt = PrepareStatement(args);
if (!stmt) {
return;
}
uint32_t n_params = args.Length() - 1;
int r = sqlite3_reset(stmt->statement_);
CHECK_ERROR_OR_THROW(env->isolate(), stmt->db_.get(), r, SQLITE_OK, void());
int param_count = sqlite3_bind_parameter_count(stmt->statement_);
for (int i = 0; i < static_cast<int>(n_params) && i < param_count; ++i) {
Local<Value> value = args[i + 1];
if (!stmt->BindValue(value, i + 1)) {
return;
}
}
BaseObjectPtr<StatementSyncIterator> iter = StatementExecutionHelper::Iterate(
env, BaseObjectPtr<StatementSync>(stmt));
if (!iter) {
return;
}
args.GetReturnValue().Set(iter->object());
}
void SQLTagStore::Get(const FunctionCallbackInfo<Value>& args) {
SQLTagStore* session;
ASSIGN_OR_RETURN_UNWRAP(&session, args.This());
Environment* env = Environment::GetCurrent(args);
THROW_AND_RETURN_ON_BAD_STATE(
env, !session->database_->IsOpen(), "database is not open");
BaseObjectPtr<StatementSync> stmt = PrepareStatement(args);
if (!stmt) {
return;
}
uint32_t n_params = args.Length() - 1;
Isolate* isolate = env->isolate();
int r = sqlite3_reset(stmt->statement_);
CHECK_ERROR_OR_THROW(isolate, stmt->db_.get(), r, SQLITE_OK, void());
int param_count = sqlite3_bind_parameter_count(stmt->statement_);
for (int i = 0; i < static_cast<int>(n_params) && i < param_count; ++i) {
Local<Value> value = args[i + 1];
if (!stmt->BindValue(value, i + 1)) {
return;
}
}
args.GetReturnValue().Set(StatementExecutionHelper::Get(env,
stmt->db_.get(),
stmt->statement_,
stmt->return_arrays_,
stmt->use_big_ints_));
}
void SQLTagStore::All(const FunctionCallbackInfo<Value>& args) {
SQLTagStore* session;
ASSIGN_OR_RETURN_UNWRAP(&session, args.This());
Environment* env = Environment::GetCurrent(args);
THROW_AND_RETURN_ON_BAD_STATE(
env, !session->database_->IsOpen(), "database is not open");
BaseObjectPtr<StatementSync> stmt = PrepareStatement(args);
if (!stmt) {
return;
}
uint32_t n_params = args.Length() - 1;
Isolate* isolate = env->isolate();
int r = sqlite3_reset(stmt->statement_);
CHECK_ERROR_OR_THROW(isolate, stmt->db_.get(), r, SQLITE_OK, void());
int param_count = sqlite3_bind_parameter_count(stmt->statement_);
for (int i = 0; i < static_cast<int>(n_params) && i < param_count; ++i) {
Local<Value> value = args[i + 1];
if (!stmt->BindValue(value, i + 1)) {
return;
}
}
auto reset = OnScopeLeave([&]() { sqlite3_reset(stmt->statement_); });
args.GetReturnValue().Set(StatementExecutionHelper::All(env,
stmt->db_.get(),
stmt->statement_,
stmt->return_arrays_,
stmt->use_big_ints_));
}
void SQLTagStore::Size(const FunctionCallbackInfo<Value>& info) {
SQLTagStore* store;
ASSIGN_OR_RETURN_UNWRAP(&store, info.This());
info.GetReturnValue().Set(
Integer::New(info.GetIsolate(), store->sql_tags_.Size()));
}
void SQLTagStore::Capacity(const FunctionCallbackInfo<Value>& info) {
SQLTagStore* store;
ASSIGN_OR_RETURN_UNWRAP(&store, info.This());
info.GetReturnValue().Set(
Integer::New(info.GetIsolate(), store->sql_tags_.Capacity()));
}
void SQLTagStore::Clear(const FunctionCallbackInfo<Value>& info) {
SQLTagStore* store;
ASSIGN_OR_RETURN_UNWRAP(&store, info.This());
store->sql_tags_.Clear();
}
BaseObjectPtr<StatementSync> SQLTagStore::PrepareStatement(
const FunctionCallbackInfo<Value>& args) {
SQLTagStore* session = BaseObject::FromJSObject<SQLTagStore>(args.This());
if (!session) {
THROW_ERR_INVALID_ARG_TYPE(
Environment::GetCurrent(args)->isolate(),
"This method can only be called on SQLTagStore instances.");
return BaseObjectPtr<StatementSync>();
}
Environment* env = Environment::GetCurrent(args);
Isolate* isolate = env->isolate();
Local<Context> context = isolate->GetCurrentContext();
if (args.Length() < 1 || !args[0]->IsArray()) {
THROW_ERR_INVALID_ARG_TYPE(
isolate,
"First argument must be an array of strings (template literal).");
return BaseObjectPtr<StatementSync>();
}
Local<Array> strings = args[0].As<Array>();
uint32_t n_strings = strings->Length();
uint32_t n_params = args.Length() - 1;
std::string sql;
for (uint32_t i = 0; i < n_strings; ++i) {
Local<Value> str_val;
if (!strings->Get(context, i).ToLocal(&str_val) || !str_val->IsString()) {
THROW_ERR_INVALID_ARG_TYPE(isolate,
"Template literal parts must be strings.");
return BaseObjectPtr<StatementSync>();
}
Utf8Value part(isolate, str_val);
sql += *part;
if (i < n_params) {
sql += "?";
}
}
BaseObjectPtr<StatementSync> stmt = nullptr;
if (session->sql_tags_.Exists(sql)) {
stmt = session->sql_tags_.Get(sql);
if (stmt->IsFinalized()) {
session->sql_tags_.Erase(sql);
stmt = nullptr;
}
}
if (stmt == nullptr) {
sqlite3_stmt* s = nullptr;
Local<String> sql_str =
String::NewFromUtf8(isolate, sql.c_str()).ToLocalChecked();
Utf8Value sql_utf8(isolate, sql_str);
int r = sqlite3_prepare_v2(
session->database_->connection_, *sql_utf8, -1, &s, 0);
if (r != SQLITE_OK) {
THROW_ERR_SQLITE_ERROR(isolate, "Failed to prepare statement");
sqlite3_finalize(s);
return BaseObjectPtr<StatementSync>();
}
BaseObjectPtr<StatementSync> stmt_obj = StatementSync::Create(
env, BaseObjectPtr<DatabaseSync>(session->database_), s);
if (!stmt_obj) {
THROW_ERR_SQLITE_ERROR(isolate, "Failed to create StatementSync");
sqlite3_finalize(s);
return BaseObjectPtr<StatementSync>();
}
session->sql_tags_.Put(sql, stmt_obj);
stmt = stmt_obj;
}
return stmt;
}
void SQLTagStore::MemoryInfo(MemoryTracker* tracker) const {
tracker->TrackFieldWithSize(MemoryInfoName(), SelfSize());
tracker->TrackField("database", database_);
size_t cache_content_size = 0;
for (const auto& pair : sql_tags_) {
cache_content_size += pair.first.capacity();
cache_content_size += sizeof(pair.second);
}
tracker->TrackFieldWithSize("sql_tags_cache", cache_content_size);
}
Local<FunctionTemplate> StatementSync::GetConstructorTemplate( Local<FunctionTemplate> StatementSync::GetConstructorTemplate(
Environment* env) { Environment* env) {
Local<FunctionTemplate> tmpl = Local<FunctionTemplate> tmpl =
@ -2544,9 +2927,14 @@ void StatementSyncIterator::Next(const FunctionCallbackInfo<Value>& args) {
LocalVector<Name> row_keys(isolate); LocalVector<Name> row_keys(isolate);
LocalVector<Value> row_values(isolate); LocalVector<Value> row_values(isolate);
auto maybe_row_values = if (ExtractRowValues(env,
ExtractRowValues(isolate, num_cols, iter->stmt_.get(), &row_values); iter->stmt_->statement_,
if (maybe_row_values.IsNothing()) return; num_cols,
iter->stmt_->use_big_ints_,
&row_values)
.IsNothing()) {
return;
}
if (iter->stmt_->return_arrays_) { if (iter->stmt_->return_arrays_) {
row_value = Array::New(isolate, row_values.data(), row_values.size()); row_value = Array::New(isolate, row_values.data(), row_values.size());
@ -2727,6 +3115,8 @@ static void Initialize(Local<Object> target,
SetProtoMethod(isolate, db_tmpl, "prepare", DatabaseSync::Prepare); SetProtoMethod(isolate, db_tmpl, "prepare", DatabaseSync::Prepare);
SetProtoMethod(isolate, db_tmpl, "exec", DatabaseSync::Exec); SetProtoMethod(isolate, db_tmpl, "exec", DatabaseSync::Exec);
SetProtoMethod(isolate, db_tmpl, "function", DatabaseSync::CustomFunction); SetProtoMethod(isolate, db_tmpl, "function", DatabaseSync::CustomFunction);
SetProtoMethod(
isolate, db_tmpl, "createTagStore", DatabaseSync::CreateTagStore);
SetProtoMethodNoSideEffect( SetProtoMethodNoSideEffect(
isolate, db_tmpl, "location", DatabaseSync::Location); isolate, db_tmpl, "location", DatabaseSync::Location);
SetProtoMethod( SetProtoMethod(

View File

@ -4,10 +4,12 @@
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
#include "base_object.h" #include "base_object.h"
#include "lru_cache-inl.h"
#include "node_mem.h" #include "node_mem.h"
#include "sqlite3.h" #include "sqlite3.h"
#include "util.h" #include "util.h"
#include <list>
#include <map> #include <map>
#include <unordered_set> #include <unordered_set>
@ -75,9 +77,38 @@ class DatabaseOpenConfiguration {
bool allow_unknown_named_params_ = false; bool allow_unknown_named_params_ = false;
}; };
class DatabaseSync;
class StatementSyncIterator;
class StatementSync; class StatementSync;
class BackupJob; class BackupJob;
class StatementExecutionHelper {
public:
static v8::Local<v8::Value> All(Environment* env,
DatabaseSync* db,
sqlite3_stmt* stmt,
bool return_arrays,
bool use_big_ints);
static v8::Local<v8::Object> Run(Environment* env,
DatabaseSync* db,
sqlite3_stmt* stmt,
bool use_big_ints);
static BaseObjectPtr<StatementSyncIterator> Iterate(
Environment* env, BaseObjectPtr<StatementSync> stmt);
static v8::MaybeLocal<v8::Value> ColumnToValue(Environment* env,
sqlite3_stmt* stmt,
const int column,
bool use_big_ints);
static v8::MaybeLocal<v8::Name> ColumnNameToName(Environment* env,
sqlite3_stmt* stmt,
const int column);
static v8::Local<v8::Value> Get(Environment* env,
DatabaseSync* db,
sqlite3_stmt* stmt,
bool return_arrays,
bool use_big_ints);
};
class DatabaseSync : public BaseObject { class DatabaseSync : public BaseObject {
public: public:
DatabaseSync(Environment* env, DatabaseSync(Environment* env,
@ -95,6 +126,7 @@ class DatabaseSync : public BaseObject {
static void Dispose(const v8::FunctionCallbackInfo<v8::Value>& args); static void Dispose(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Prepare(const v8::FunctionCallbackInfo<v8::Value>& args); static void Prepare(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Exec(const v8::FunctionCallbackInfo<v8::Value>& args); static void Exec(const v8::FunctionCallbackInfo<v8::Value>& args);
static void CreateTagStore(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Location(const v8::FunctionCallbackInfo<v8::Value>& args); static void Location(const v8::FunctionCallbackInfo<v8::Value>& args);
static void CustomFunction(const v8::FunctionCallbackInfo<v8::Value>& args); static void CustomFunction(const v8::FunctionCallbackInfo<v8::Value>& args);
static void AggregateFunction( static void AggregateFunction(
@ -146,6 +178,8 @@ class DatabaseSync : public BaseObject {
std::unordered_set<StatementSync*> statements_; std::unordered_set<StatementSync*> statements_;
friend class Session; friend class Session;
friend class SQLTagStore;
friend class StatementExecutionHelper;
}; };
class StatementSync : public BaseObject { class StatementSync : public BaseObject {
@ -195,6 +229,8 @@ class StatementSync : public BaseObject {
bool BindValue(const v8::Local<v8::Value>& value, const int index); bool BindValue(const v8::Local<v8::Value>& value, const int index);
friend class StatementSyncIterator; friend class StatementSyncIterator;
friend class SQLTagStore;
friend class StatementExecutionHelper;
}; };
class StatementSyncIterator : public BaseObject { class StatementSyncIterator : public BaseObject {
@ -248,6 +284,39 @@ class Session : public BaseObject {
BaseObjectWeakPtr<DatabaseSync> database_; // The Parent Database BaseObjectWeakPtr<DatabaseSync> database_; // The Parent Database
}; };
class SQLTagStore : public BaseObject {
public:
SQLTagStore(Environment* env,
v8::Local<v8::Object> object,
BaseObjectWeakPtr<DatabaseSync> database,
int capacity);
~SQLTagStore() override;
static BaseObjectPtr<SQLTagStore> Create(
Environment* env, BaseObjectWeakPtr<DatabaseSync> database, int capacity);
static v8::Local<v8::FunctionTemplate> GetConstructorTemplate(
Environment* env);
static void All(const v8::FunctionCallbackInfo<v8::Value>& info);
static void Get(const v8::FunctionCallbackInfo<v8::Value>& info);
static void Iterate(const v8::FunctionCallbackInfo<v8::Value>& info);
static void Run(const v8::FunctionCallbackInfo<v8::Value>& info);
static void Size(const v8::FunctionCallbackInfo<v8::Value>& info);
static void Capacity(const v8::FunctionCallbackInfo<v8::Value>& info);
static void Reset(const v8::FunctionCallbackInfo<v8::Value>& info);
static void Clear(const v8::FunctionCallbackInfo<v8::Value>& info);
static void DatabaseGetter(const v8::FunctionCallbackInfo<v8::Value>& info);
void MemoryInfo(MemoryTracker* tracker) const override;
SET_MEMORY_INFO_NAME(SQLTagStore)
SET_SELF_SIZE(SQLTagStore)
private:
static BaseObjectPtr<StatementSync> PrepareStatement(
const v8::FunctionCallbackInfo<v8::Value>& args);
BaseObjectWeakPtr<DatabaseSync> database_;
LRUCache<std::string, BaseObjectPtr<StatementSync>> sql_tags_;
int capacity_;
friend class StatementExecutionHelper;
};
class UserDefinedFunction { class UserDefinedFunction {
public: public:
UserDefinedFunction(Environment* env, UserDefinedFunction(Environment* env,

View File

@ -0,0 +1,155 @@
#include <string>
#include <vector>
#include "gtest/gtest.h"
#include "lru_cache-inl.h"
#include "node_internals.h"
// Test basic Put and Get operations
TEST(LRUCache, PutAndGet) {
LRUCache<int, std::string> cache(2);
cache.Put(1, "one");
cache.Put(2, "two");
EXPECT_TRUE(cache.Exists(1));
EXPECT_EQ(cache.Get(1), "one");
EXPECT_TRUE(cache.Exists(2));
EXPECT_EQ(cache.Get(2), "two");
EXPECT_FALSE(cache.Exists(3));
}
// Test that Putting an existing key updates its value and moves it to the front
TEST(LRUCache, PutUpdatesExisting) {
LRUCache<int, std::string> cache(2);
cache.Put(1, "one");
cache.Put(2, "two");
cache.Put(1, "updated one");
EXPECT_EQ(cache.Size(), 2u);
EXPECT_EQ(cache.Get(1), "updated one");
// Now, if we add another element, key 2 should be evicted, not key 1
cache.Put(3, "three");
EXPECT_FALSE(cache.Exists(2));
EXPECT_TRUE(cache.Exists(1));
EXPECT_TRUE(cache.Exists(3));
}
// Test the eviction of the least recently used item
TEST(LRUCache, Eviction) {
LRUCache<int, int> cache(3);
cache.Put(1, 10);
cache.Put(2, 20);
cache.Put(3, 30);
// At this point, the order of use is 3, 2, 1
cache.Put(4, 40); // This should evict key 1
EXPECT_EQ(cache.Size(), 3u);
EXPECT_FALSE(cache.Exists(1));
EXPECT_TRUE(cache.Exists(2));
EXPECT_TRUE(cache.Exists(3));
EXPECT_TRUE(cache.Exists(4));
}
// Test that Get() moves an item to the front (most recently used)
TEST(LRUCache, GetMovesToFront) {
LRUCache<char, int> cache(2);
cache.Put('a', 1);
cache.Put('b', 2);
// Access 'a', making it the most recently used
cache.Get('a');
// Add 'c', which should evict 'b'
cache.Put('c', 3);
EXPECT_EQ(cache.Size(), 2u);
EXPECT_TRUE(cache.Exists('a'));
EXPECT_FALSE(cache.Exists('b'));
EXPECT_TRUE(cache.Exists('c'));
}
// Test the Erase() method
TEST(LRUCache, Erase) {
LRUCache<int, std::string> cache(2);
cache.Put(1, "one");
cache.Put(2, "two");
cache.Erase(1);
EXPECT_EQ(cache.Size(), 1u);
EXPECT_FALSE(cache.Exists(1));
EXPECT_TRUE(cache.Exists(2));
// Erasing a non-existent key should not fail
cache.Erase(99);
EXPECT_EQ(cache.Size(), 1u);
}
// Test the Exists() method
TEST(LRUCache, Exists) {
LRUCache<int, int> cache(1);
cache.Put(1, 100);
EXPECT_TRUE(cache.Exists(1));
EXPECT_FALSE(cache.Exists(2));
}
// Test the Size() method
TEST(LRUCache, Size) {
LRUCache<int, int> cache(5);
EXPECT_EQ(cache.Size(), 0u);
cache.Put(1, 1);
EXPECT_EQ(cache.Size(), 1u);
cache.Put(2, 2);
EXPECT_EQ(cache.Size(), 2u);
cache.Put(1, 11); // Update
EXPECT_EQ(cache.Size(), 2u);
cache.Erase(2);
EXPECT_EQ(cache.Size(), 1u);
}
// Test with a max_Size of 0
TEST(LRUCache, ZeroSizeCache) {
LRUCache<int, int> cache(0);
cache.Put(1, 1);
EXPECT_FALSE(cache.Exists(1));
EXPECT_EQ(cache.Size(), 0u);
}
// Test with a max_Size of 1
TEST(LRUCache, OneSizeCache) {
LRUCache<int, int> cache(1);
cache.Put(1, 1);
EXPECT_TRUE(cache.Exists(1));
EXPECT_EQ(cache.Size(), 1u);
cache.Put(2, 2);
EXPECT_FALSE(cache.Exists(1));
EXPECT_TRUE(cache.Exists(2));
EXPECT_EQ(cache.Size(), 1u);
}
// Test with complex key and value types
TEST(LRUCache, ComplexTypes) {
LRUCache<std::string, std::vector<int>> cache(2);
std::vector<int> vec1 = {1, 2, 3};
std::vector<int> vec2 = {4, 5, 6};
std::vector<int> vec3 = {7, 8, 9};
cache.Put("vec1", vec1);
cache.Put("vec2", vec2);
EXPECT_EQ(cache.Get("vec1"), vec1);
EXPECT_EQ(cache.Get("vec2"), vec2);
cache.Put("vec3", vec3);
EXPECT_FALSE(cache.Exists("vec1"));
EXPECT_TRUE(cache.Exists("vec2"));
EXPECT_TRUE(cache.Exists("vec3"));
}

View File

@ -0,0 +1,102 @@
'use strict';
require('../common');
const assert = require('assert');
const { DatabaseSync } = require('node:sqlite');
const { test, beforeEach } = require('node:test');
const db = new DatabaseSync(':memory:');
const sql = db.createTagStore(10);
beforeEach(() => {
db.exec('DROP TABLE IF EXISTS foo');
db.exec('CREATE TABLE foo (id INTEGER PRIMARY KEY, text TEXT)');
sql.clear();
});
test('sql.run inserts data', () => {
assert.strictEqual(sql.run`INSERT INTO foo (text) VALUES (${'bob'})`.changes, 1);
assert.strictEqual(sql.run`INSERT INTO foo (text) VALUES (${'mac'})`.changes, 1);
assert.strictEqual(sql.run`INSERT INTO foo (text) VALUES (${'alice'})`.changes, 1);
const count = db.prepare('SELECT COUNT(*) as count FROM foo').get().count;
assert.strictEqual(count, 3);
});
test('sql.get retrieves a single row', () => {
assert.strictEqual(sql.run`INSERT INTO foo (text) VALUES (${'bob'})`.changes, 1);
const first = sql.get`SELECT * FROM foo ORDER BY id ASC`;
assert.ok(first);
assert.strictEqual(first.text, 'bob');
assert.strictEqual(first.id, 1);
assert.strictEqual(Object.getPrototypeOf(first), null);
});
test('sql.all retrieves all rows', () => {
assert.strictEqual(sql.run`INSERT INTO foo (text) VALUES (${'bob'})`.changes, 1);
assert.strictEqual(sql.run`INSERT INTO foo (text) VALUES (${'mac'})`.changes, 1);
assert.strictEqual(sql.run`INSERT INTO foo (text) VALUES (${'alice'})`.changes, 1);
const all = sql.all`SELECT * FROM foo ORDER BY id ASC`;
assert.strictEqual(Array.isArray(all), true);
assert.strictEqual(all.length, 3);
for (const row of all) {
assert.strictEqual(Object.getPrototypeOf(row), null);
}
assert.deepStrictEqual(all.map((r) => r.text), ['bob', 'mac', 'alice']);
});
test('sql.iterate retrieves rows via iterator', () => {
assert.strictEqual(sql.run`INSERT INTO foo (text) VALUES (${'bob'})`.changes, 1);
assert.strictEqual(sql.run`INSERT INTO foo (text) VALUES (${'mac'})`.changes, 1);
assert.strictEqual(sql.run`INSERT INTO foo (text) VALUES (${'alice'})`.changes, 1);
const iter = sql.iterate`SELECT * FROM foo ORDER BY id ASC`;
const iterRows = [];
for (const row of iter) {
assert.ok(row);
assert.strictEqual(Object.getPrototypeOf(row), null);
iterRows.push(row.text);
}
assert.deepStrictEqual(iterRows, ['bob', 'mac', 'alice']);
});
test('queries with no results', () => {
const none = sql.get`SELECT * FROM foo WHERE text = ${'notfound'}`;
assert.strictEqual(none, undefined);
const empty = sql.all`SELECT * FROM foo WHERE text = ${'notfound'}`;
assert.deepStrictEqual(empty, []);
let count = 0;
// eslint-disable-next-line no-unused-vars
for (const _ of sql.iterate`SELECT * FROM foo WHERE text = ${'notfound'}`) {
count++;
}
assert.strictEqual(count, 0);
});
test('TagStore capacity, size, and clear', () => {
assert.strictEqual(sql.capacity, 10);
assert.strictEqual(sql.size(), 0);
assert.strictEqual(sql.run`INSERT INTO foo (text) VALUES (${'one'})`.changes, 1);
assert.strictEqual(sql.size(), 1);
assert.ok(sql.get`SELECT * FROM foo WHERE text = ${'one'}`);
assert.strictEqual(sql.size(), 2);
// Using the same template string shouldn't increase the size
assert.strictEqual(sql.get`SELECT * FROM foo WHERE text = ${'two'}`, undefined);
assert.strictEqual(sql.size(), 2);
assert.strictEqual(sql.all`SELECT * FROM foo`.length, 1);
assert.strictEqual(sql.size(), 3);
sql.clear();
assert.strictEqual(sql.size(), 0);
assert.strictEqual(sql.capacity, 10);
});
test('sql.db returns the associated DatabaseSync instance', () => {
assert.strictEqual(sql.db, db);
});

View File

@ -96,6 +96,8 @@ const customTypesMap = {
'EncapsulatedBits': 'webcrypto.html#class-encapsulatedbits', 'EncapsulatedBits': 'webcrypto.html#class-encapsulatedbits',
'EncapsulatedKey': 'webcrypto.html#class-encapsulatedkey', 'EncapsulatedKey': 'webcrypto.html#class-encapsulatedkey',
'SubtleCrypto': 'webcrypto.html#class-subtlecrypto', 'SubtleCrypto': 'webcrypto.html#class-subtlecrypto',
'Template Literal':
`${jsDocPrefix}Reference/Template_literals`,
'RsaOaepParams': 'webcrypto.html#class-rsaoaepparams', 'RsaOaepParams': 'webcrypto.html#class-rsaoaepparams',
'AesCtrParams': 'webcrypto.html#class-aesctrparams', 'AesCtrParams': 'webcrypto.html#class-aesctrparams',
'AesCbcParams': 'webcrypto.html#class-aescbcparams', 'AesCbcParams': 'webcrypto.html#class-aescbcparams',
@ -234,6 +236,7 @@ const customTypesMap = {
'Session': 'sqlite.html#class-session', 'Session': 'sqlite.html#class-session',
'StatementSync': 'sqlite.html#class-statementsync', 'StatementSync': 'sqlite.html#class-statementsync',
'SQLTagStore': 'sqlite.html#class-sqltagstore',
'Stream': 'stream.html#stream', 'Stream': 'stream.html#stream',
'stream.Duplex': 'stream.html#class-streamduplex', 'stream.Duplex': 'stream.html#class-streamduplex',