Skip to content

sqlite: add tagged template #58748

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
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
158 changes: 158 additions & 0 deletions doc/api/sqlite.md
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,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 @@ -490,6 +537,117 @@ 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. This class cannot be instantiated via its constructor.
Instead, instances are created via the `database.createSQLTagStore()` method.
All APIs exposed by this class execute synchronously.

The store saves the prepared statement in respect to the SQL query provided, and
returns the same prepared statement when it sees it again. It has a `maxSize`
which defaults to 1000 but can also be passed in
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's worth mentioning that all the methods do the automatic parameter binding

Copy link
Contributor Author

@0hmX 0hmX Jul 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see what you mean. I figured 'prepared statements' as it implied we are doing automatic parameter, but I don't mind making it more explicit. I'll change it!

`database.createSQLTagStore(100)`.

### `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