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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 161 additions & 0 deletions doc/api/sqlite.md
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,53 @@ added: v22.5.0
Compiles a SQL statement into a [prepared statement][]. This method is a wrapper
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])`

<!-- YAML
Expand Down Expand Up @@ -504,6 +551,120 @@ times with different bound values. Parameters also offer protection against
[SQL injection][] attacks. For these reasons, prepared statements are preferred
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])`

<!-- YAML
Expand Down
69 changes: 69 additions & 0 deletions src/lru_cache-inl.h
Original file line number Diff line number Diff line change
@@ -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_
Loading
Loading