Skip to content

Commit e7d49d2

Browse files
committed
sqlite: add tagged template support
Adding tagged template and LRU cache for prepared statements in SQLite.
1 parent 5f7dbf4 commit e7d49d2

File tree

7 files changed

+921
-0
lines changed

7 files changed

+921
-0
lines changed

doc/api/sqlite.md

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,66 @@ added: v22.5.0
347347
Compiles a SQL statement into a [prepared statement][]. This method is a wrapper
348348
around [`sqlite3_prepare_v2()`][].
349349

350+
### `database.createSqlTagStore([maxSize])`
351+
352+
<!-- YAML
353+
added: v24.3.0
354+
-->
355+
356+
* `maxSize` {integer} The maximum number of prepared statements to cache.
357+
**Default:** `1000`.
358+
* Returns: {SqlTagStore} A new SQL tag store for caching prepared statements.
359+
360+
Creates a new `SqlTagStore`, which is an LRU (Least Recently Used) cache for
361+
storing prepared statements. This allows for the efficient reuse of prepared
362+
statements by tagging them with a unique identifier.
363+
364+
When a tagged SQL literal is executed, the `SqlTagStore` checks if a prepared
365+
statement for that specific SQL string already exists in the cache. If it does,
366+
the cached statement is used. If not, a new prepared statement is created,
367+
executed, and then stored in the cache for future use. This mechanism helps to
368+
avoid the overhead of repeatedly parsing and preparing the same SQL statements.
369+
370+
The `maxSize` parameter determines the maximum number of prepared statements
371+
that can be held in the LRU cache at any given time. When the cache is full, the
372+
least recently used statement is evicted to make room for a new one.
373+
374+
The returned `SqlTagStore` object has the following methods, which correspond to
375+
the methods on the `StatementSync` class:
376+
377+
* `all`
378+
* `get`
379+
* `iterate`
380+
* `run`
381+
382+
Each of these methods takes a tagged SQL literal as its argument.
383+
384+
```mjs
385+
import { DatabaseSync } from 'node:sqlite';
386+
387+
const db = new DatabaseSync(':memory:');
388+
const sql = db.createSqlTagStore();
389+
390+
db.exec('CREATE TABLE users (id INT, name TEXT)');
391+
392+
// Using the 'run' method to insert data.
393+
// The tagged literal is used to identify the prepared statement.
394+
sql.run`INSERT INTO users VALUES (1, 'Alice')`;
395+
sql.run`INSERT INTO users VALUES (2, 'Bob')`;
396+
397+
// Using the 'get' method to retrieve a single row.
398+
const user = sql.get`SELECT * FROM users WHERE id = 1`;
399+
console.log(user); // { id: 1, name: 'Alice' }
400+
401+
// Using the 'all' method to retrieve all rows.
402+
const allUsers = sql.all`SELECT * FROM users ORDER BY id`;
403+
console.log(allUsers);
404+
// [
405+
// { id: 1, name: 'Alice' },
406+
// { id: 2, name: 'Bob' }
407+
// ]
408+
```
409+
350410
### `database.createSession([options])`
351411

352412
<!-- YAML
@@ -490,6 +550,107 @@ times with different bound values. Parameters also offer protection against
490550
[SQL injection][] attacks. For these reasons, prepared statements are preferred
491551
over hand-crafted SQL strings when handling user input.
492552

553+
## Class: `SqlTagStore`
554+
555+
<!-- YAML
556+
added: v25.0.0
557+
-->
558+
559+
This class represents a single \[LRU (Least Recently Used) cache]\[] for storing
560+
prepared statements. This class cannot be instantiated via its constructor.
561+
Instead, instances are created via the `database.createSqlTagStore()` method.
562+
All APIs exposed by this class execute synchronously.
563+
564+
The store saves the prepared statement in respect to the SQL query provided, and
565+
returns the same prepared statement when it sees it again. It has a `maxSize`
566+
which defaults to 1000 but can also be passed in
567+
`database.createSqlTagStore(100)`.
568+
569+
### `sqlTagStore.all(sqlTemplate[, ...values])`
570+
571+
<!-- YAML
572+
added: v25.0.0
573+
-->
574+
575+
* `sqlTemplate` {Template Literal} A template literal containing the SQL query.
576+
* `...values` {any} Values to be interpolated into the template literal.
577+
* Returns: {Array} An array of objects representing the rows returned by the query.
578+
579+
Executes the given SQL query and returns all resulting rows as an array of objects.
580+
581+
### `sqlTagStore.get(sqlTemplate[, ...values])`
582+
583+
<!-- YAML
584+
added: v25.0.0
585+
-->
586+
587+
* `sqlTemplate` {Template Literal} A template literal containing the SQL query.
588+
* `...values` {any} Values to be interpolated into the template literal.
589+
* Returns: {Object | undefined} An object representing the first row returned by
590+
the query, or `undefined` if no rows are returned.
591+
592+
Executes the given SQL query and returns the first resulting row as an object.
593+
594+
### `sqlTagStore.iterate(sqlTemplate[, ...values])`
595+
596+
<!-- YAML
597+
added: v25.0.0
598+
-->
599+
600+
* `sqlTemplate` {Template Literal} A template literal containing the SQL query.
601+
* `...values` {any} Values to be interpolated into the template literal.
602+
* Returns: {Iterator} An iterator that yields objects representing the rows returned by the query.
603+
604+
Executes the given SQL query and returns an iterator over the resulting rows.
605+
606+
### `sqlTagStore.run(sqlTemplate[, ...values])`
607+
608+
<!-- YAML
609+
added: v25.0.0
610+
-->
611+
612+
* `sqlTemplate` {Template Literal} A template literal containing the SQL query.
613+
* `...values` {any} Values to be interpolated into the template literal.
614+
* Returns: {Object} An object containing information about the execution, including `changes` and `lastInsertRowid`.
615+
616+
Executes the given SQL query, which is expected to not return any rows (e.g., INSERT, UPDATE, DELETE).
617+
618+
### `sqlTagStore.size`
619+
620+
<!-- YAML
621+
added: v25.0.0
622+
-->
623+
624+
* Returns: {integer} The number of prepared statements currently in the cache.
625+
626+
A read-only property that returns the number of prepared statements currently in the cache.
627+
628+
### `sqlTagStore.capacity`
629+
630+
<!-- YAML
631+
added: v25.0.0
632+
-->
633+
634+
* Returns: {integer} The maximum number of prepared statements the cache can hold.
635+
636+
A read-only property that returns the maximum number of prepared statements the cache can hold.
637+
638+
### `sqlTagStore.reset()`
639+
640+
<!-- YAML
641+
added: v25.0.0
642+
-->
643+
644+
Resets the LRU cache, clearing all stored prepared statements.
645+
646+
### `sqlTagStore.clear()`
647+
648+
<!-- YAML
649+
added: v25.0.0
650+
-->
651+
652+
An alias for `sqlTagStore.reset()`.
653+
493654
### `statement.all([namedParameters][, ...anonymousParameters])`
494655

495656
<!-- YAML

src/lru_cache-inl.h

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
#ifndef SRC_LRU_CACHE_INL_H_
2+
#define SRC_LRU_CACHE_INL_H_
3+
4+
#include <list>
5+
#include <unordered_map>
6+
#include <utility>
7+
8+
template <typename key_t, typename value_t>
9+
class LruCache {
10+
public:
11+
using key_value_pair_t = typename std::pair<key_t, value_t>;
12+
using iterator = typename std::list<key_value_pair_t>::iterator;
13+
14+
explicit LruCache(size_t capacity) : capacity_(capacity) {}
15+
16+
void put(const key_t& key, const value_t& value) {
17+
auto it = lookup_map_.find(key);
18+
if (it != lookup_map_.end()) {
19+
lru_list_.erase(it->second);
20+
lookup_map_.erase(it);
21+
}
22+
23+
lru_list_.push_front(std::make_pair(key, value));
24+
lookup_map_[key] = lru_list_.begin();
25+
26+
if (lookup_map_.size() > capacity_) {
27+
auto last = lru_list_.end();
28+
last--;
29+
lookup_map_.erase(last->first);
30+
lru_list_.pop_back();
31+
}
32+
}
33+
34+
value_t& get(const key_t& key) {
35+
auto it = lookup_map_.find(key);
36+
lru_list_.splice(lru_list_.begin(), lru_list_,
37+
it->second);
38+
return it->second->second;
39+
}
40+
41+
void erase(const key_t& key) {
42+
auto it = lookup_map_.find(key);
43+
if (it != lookup_map_.end()) {
44+
lru_list_.erase(it->second);
45+
lookup_map_.erase(it);
46+
}
47+
}
48+
49+
bool exists(const key_t& key) const {
50+
return lookup_map_.count(key) > 0;
51+
}
52+
53+
size_t size() const {
54+
return lookup_map_.size();
55+
}
56+
57+
size_t capacity() const { return capacity_; }
58+
59+
void clear() {
60+
lru_list_.clear();
61+
lookup_map_.clear();
62+
}
63+
64+
private:
65+
std::list<key_value_pair_t> lru_list_;
66+
std::unordered_map<key_t, iterator> lookup_map_;
67+
size_t capacity_;
68+
};
69+
70+
#endif // SRC_LRU_CACHE_INL_H_
71+

0 commit comments

Comments
 (0)