Skip to content

Commit 97e55f8

Browse files
authored
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: #58748 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Edy Silva <edigleyssonsilva@gmail.com>
1 parent 65bee02 commit 97e55f8

File tree

7 files changed

+1065
-116
lines changed

7 files changed

+1065
-116
lines changed

doc/api/sqlite.md

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,53 @@ added: v22.5.0
361361
Compiles a SQL statement into a [prepared statement][]. This method is a wrapper
362362
around [`sqlite3_prepare_v2()`][].
363363

364+
### `database.createSQLTagStore([maxSize])`
365+
366+
<!-- YAML
367+
added: REPLACEME
368+
-->
369+
370+
* `maxSize` {integer} The maximum number of prepared statements to cache.
371+
**Default:** `1000`.
372+
* Returns: {SQLTagStore} A new SQL tag store for caching prepared statements.
373+
374+
Creates a new `SQLTagStore`, which is an LRU (Least Recently Used) cache for
375+
storing prepared statements. This allows for the efficient reuse of prepared
376+
statements by tagging them with a unique identifier.
377+
378+
When a tagged SQL literal is executed, the `SQLTagStore` checks if a prepared
379+
statement for that specific SQL string already exists in the cache. If it does,
380+
the cached statement is used. If not, a new prepared statement is created,
381+
executed, and then stored in the cache for future use. This mechanism helps to
382+
avoid the overhead of repeatedly parsing and preparing the same SQL statements.
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 id = 1;
399+
const user = sql.get`SELECT * FROM users WHERE id = ${id}`;
400+
console.log(user); // { id: 1, name: 'Alice' }
401+
402+
// Using the 'all' method to retrieve all rows.
403+
const allUsers = sql.all`SELECT * FROM users ORDER BY id`;
404+
console.log(allUsers);
405+
// [
406+
// { id: 1, name: 'Alice' },
407+
// { id: 2, name: 'Bob' }
408+
// ]
409+
```
410+
364411
### `database.createSession([options])`
365412

366413
<!-- YAML
@@ -504,6 +551,120 @@ times with different bound values. Parameters also offer protection against
504551
[SQL injection][] attacks. For these reasons, prepared statements are preferred
505552
over hand-crafted SQL strings when handling user input.
506553

554+
## Class: `SQLTagStore`
555+
556+
<!-- YAML
557+
added: REPLACEME
558+
-->
559+
560+
This class represents a single LRU (Least Recently Used) cache for storing
561+
prepared statements.
562+
563+
Instances of this class are created via the database.createSQLTagStore() method,
564+
not by using a constructor. The store caches prepared statements based on the
565+
provided SQL query string. When the same query is seen again, the store
566+
retrieves the cached statement and safely applies the new values through
567+
parameter binding, thereby preventing attacks like SQL injection.
568+
569+
The cache has a maxSize that defaults to 1000 statements, but a custom size can
570+
be provided (e.g., database.createSQLTagStore(100)). All APIs exposed by this
571+
class execute synchronously.
572+
573+
### `sqlTagStore.all(sqlTemplate[, ...values])`
574+
575+
<!-- YAML
576+
added: REPLACEME
577+
-->
578+
579+
* `sqlTemplate` {Template Literal} A template literal containing the SQL query.
580+
* `...values` {any} Values to be interpolated into the template literal.
581+
* Returns: {Array} An array of objects representing the rows returned by the query.
582+
583+
Executes the given SQL query and returns all resulting rows as an array of objects.
584+
585+
### `sqlTagStore.get(sqlTemplate[, ...values])`
586+
587+
<!-- YAML
588+
added: REPLACEME
589+
-->
590+
591+
* `sqlTemplate` {Template Literal} A template literal containing the SQL query.
592+
* `...values` {any} Values to be interpolated into the template literal.
593+
* Returns: {Object | undefined} An object representing the first row returned by
594+
the query, or `undefined` if no rows are returned.
595+
596+
Executes the given SQL query and returns the first resulting row as an object.
597+
598+
### `sqlTagStore.iterate(sqlTemplate[, ...values])`
599+
600+
<!-- YAML
601+
added: REPLACEME
602+
-->
603+
604+
* `sqlTemplate` {Template Literal} A template literal containing the SQL query.
605+
* `...values` {any} Values to be interpolated into the template literal.
606+
* Returns: {Iterator} An iterator that yields objects representing the rows returned by the query.
607+
608+
Executes the given SQL query and returns an iterator over the resulting rows.
609+
610+
### `sqlTagStore.run(sqlTemplate[, ...values])`
611+
612+
<!-- YAML
613+
added: REPLACEME
614+
-->
615+
616+
* `sqlTemplate` {Template Literal} A template literal containing the SQL query.
617+
* `...values` {any} Values to be interpolated into the template literal.
618+
* Returns: {Object} An object containing information about the execution, including `changes` and `lastInsertRowid`.
619+
620+
Executes the given SQL query, which is expected to not return any rows (e.g., INSERT, UPDATE, DELETE).
621+
622+
### `sqlTagStore.size()`
623+
624+
<!-- YAML
625+
added: REPLACEME
626+
-->
627+
628+
* Returns: {integer} The number of prepared statements currently in the cache.
629+
630+
A read-only property that returns the number of prepared statements currently in the cache.
631+
632+
### `sqlTagStore.capacity`
633+
634+
<!-- YAML
635+
added: REPLACEME
636+
-->
637+
638+
* Returns: {integer} The maximum number of prepared statements the cache can hold.
639+
640+
A read-only property that returns the maximum number of prepared statements the cache can hold.
641+
642+
### `sqlTagStore.db`
643+
644+
<!-- YAML
645+
added: REPLACEME
646+
-->
647+
648+
* {DatabaseSync} The `DatabaseSync` instance that created this `SQLTagStore`.
649+
650+
A read-only property that returns the `DatabaseSync` object associated with this `SQLTagStore`.
651+
652+
### `sqlTagStore.reset()`
653+
654+
<!-- YAML
655+
added: REPLACEME
656+
-->
657+
658+
Resets the LRU cache, clearing all stored prepared statements.
659+
660+
### `sqlTagStore.clear()`
661+
662+
<!-- YAML
663+
added: REPLACEME
664+
-->
665+
666+
An alias for `sqlTagStore.reset()`.
667+
507668
### `statement.all([namedParameters][, ...anonymousParameters])`
508669

509670
<!-- YAML

src/lru_cache-inl.h

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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+
using const_iterator = typename std::list<key_value_pair_t>::const_iterator;
14+
15+
const_iterator begin() const { return lru_list_.begin(); }
16+
const_iterator end() const { return lru_list_.end(); }
17+
18+
explicit LRUCache(size_t capacity) : capacity_(capacity) {}
19+
20+
void Put(const key_t& key, const value_t& value) {
21+
auto it = lookup_map_.find(key);
22+
if (it != lookup_map_.end()) {
23+
lru_list_.erase(it->second);
24+
lookup_map_.erase(it);
25+
}
26+
27+
lru_list_.push_front(std::make_pair(key, value));
28+
lookup_map_[key] = lru_list_.begin();
29+
30+
if (lookup_map_.size() > capacity_) {
31+
auto last = lru_list_.end();
32+
last--;
33+
lookup_map_.erase(last->first);
34+
lru_list_.pop_back();
35+
}
36+
}
37+
38+
value_t& Get(const key_t& key) {
39+
auto it = lookup_map_.find(key);
40+
lru_list_.splice(lru_list_.begin(), lru_list_, it->second);
41+
return it->second->second;
42+
}
43+
44+
void Erase(const key_t& key) {
45+
auto it = lookup_map_.find(key);
46+
if (it != lookup_map_.end()) {
47+
lru_list_.erase(it->second);
48+
lookup_map_.erase(it);
49+
}
50+
}
51+
52+
bool Exists(const key_t& key) const { return lookup_map_.count(key) > 0; }
53+
54+
size_t Size() const { return lookup_map_.size(); }
55+
56+
size_t Capacity() const { return capacity_; }
57+
58+
void Clear() {
59+
lru_list_.clear();
60+
lookup_map_.clear();
61+
}
62+
63+
private:
64+
std::list<key_value_pair_t> lru_list_;
65+
std::unordered_map<key_t, iterator> lookup_map_;
66+
size_t capacity_;
67+
};
68+
69+
#endif // SRC_LRU_CACHE_INL_H_

0 commit comments

Comments
 (0)