GitHub - felixfbecker/node-sql-template-strings: ES6 tagged template strings for prepared SQL statements 📋 (original) (raw)
SQL Template Strings
A simple yet powerful module to allow you to use ES6 tagged template strings for prepared/escaped statements.
Works with mysql, mysql2, postgres and sequelize.
Example for escaping queries (callbacks omitted):
const SQL = require('sql-template-strings')
const book = 'harry potter' const author = 'J. K. Rowling'
// mysql:
mysql.query('SELECT author FROM books WHERE name = ? AND author = ?', [book, author])
// is equivalent to
mysql.query(SQLSELECT author FROM books WHERE name = <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mi>b</mi><mi>o</mi><mi>o</mi><mi>k</mi></mrow><mi>A</mi><mi>N</mi><mi>D</mi><mi>a</mi><mi>u</mi><mi>t</mi><mi>h</mi><mi>o</mi><mi>r</mi><mo>=</mo></mrow><annotation encoding="application/x-tex">{book} AND author = </annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord"><span class="mord mathnormal">b</span><span class="mord mathnormal">oo</span><span class="mord mathnormal" style="margin-right:0.03148em;">k</span></span><span class="mord mathnormal">A</span><span class="mord mathnormal" style="margin-right:0.10903em;">N</span><span class="mord mathnormal" style="margin-right:0.02778em;">D</span><span class="mord mathnormal">a</span><span class="mord mathnormal">u</span><span class="mord mathnormal">t</span><span class="mord mathnormal">h</span><span class="mord mathnormal" style="margin-right:0.02778em;">or</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span></span></span></span>{author}
)
// postgres:
pg.query('SELECT author FROM books WHERE name = 1ANDauthor=1 AND author = 1ANDauthor=2', [book, author])
// is equivalent to
pg.query(SQLSELECT author FROM books WHERE name = <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mi>b</mi><mi>o</mi><mi>o</mi><mi>k</mi></mrow><mi>A</mi><mi>N</mi><mi>D</mi><mi>a</mi><mi>u</mi><mi>t</mi><mi>h</mi><mi>o</mi><mi>r</mi><mo>=</mo></mrow><annotation encoding="application/x-tex">{book} AND author = </annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord"><span class="mord mathnormal">b</span><span class="mord mathnormal">oo</span><span class="mord mathnormal" style="margin-right:0.03148em;">k</span></span><span class="mord mathnormal">A</span><span class="mord mathnormal" style="margin-right:0.10903em;">N</span><span class="mord mathnormal" style="margin-right:0.02778em;">D</span><span class="mord mathnormal">a</span><span class="mord mathnormal">u</span><span class="mord mathnormal">t</span><span class="mord mathnormal">h</span><span class="mord mathnormal" style="margin-right:0.02778em;">or</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span></span></span></span>{author}
)
// sequelize:
sequelize.query('SELECT author FROM books WHERE name = ? AND author = ?', {replacements: [book, author]})
// is equivalent to
sequelize.query(SQLSELECT author FROM books WHERE name = <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mi>b</mi><mi>o</mi><mi>o</mi><mi>k</mi></mrow><mi>A</mi><mi>N</mi><mi>D</mi><mi>a</mi><mi>u</mi><mi>t</mi><mi>h</mi><mi>o</mi><mi>r</mi><mo>=</mo></mrow><annotation encoding="application/x-tex">{book} AND author = </annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord"><span class="mord mathnormal">b</span><span class="mord mathnormal">oo</span><span class="mord mathnormal" style="margin-right:0.03148em;">k</span></span><span class="mord mathnormal">A</span><span class="mord mathnormal" style="margin-right:0.10903em;">N</span><span class="mord mathnormal" style="margin-right:0.02778em;">D</span><span class="mord mathnormal">a</span><span class="mord mathnormal">u</span><span class="mord mathnormal">t</span><span class="mord mathnormal">h</span><span class="mord mathnormal" style="margin-right:0.02778em;">or</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span></span></span></span>{author}
)
This might not seem like a big deal, but when you do an INSERT with a lot columns writing all the placeholders becomes a nightmare:
db.query(
'INSERT INTO books (name, author, isbn, category, recommended_age, pages, price) VALUES (?, ?, ?, ?, ?, ?, ?)',
[name, author, isbn, category, recommendedAge, pages, price]
)
// is better written as
db.query(SQL INSERT INTO books (name, author, isbn, category, recommended_age, pages, price) VALUES (${name}, <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mi>a</mi><mi>u</mi><mi>t</mi><mi>h</mi><mi>o</mi><mi>r</mi></mrow><mo separator="true">,</mo></mrow><annotation encoding="application/x-tex">{author}, </annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord"><span class="mord mathnormal">a</span><span class="mord mathnormal">u</span><span class="mord mathnormal">t</span><span class="mord mathnormal">h</span><span class="mord mathnormal" style="margin-right:0.02778em;">or</span></span><span class="mpunct">,</span></span></span></span>{isbn}, <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mi>c</mi><mi>a</mi><mi>t</mi><mi>e</mi><mi>g</mi><mi>o</mi><mi>r</mi><mi>y</mi></mrow><mo separator="true">,</mo></mrow><annotation encoding="application/x-tex">{category}, </annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8095em;vertical-align:-0.1944em;"></span><span class="mord"><span class="mord mathnormal">c</span><span class="mord mathnormal">a</span><span class="mord mathnormal">t</span><span class="mord mathnormal">e</span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span><span class="mord mathnormal" style="margin-right:0.03588em;">ory</span></span><span class="mpunct">,</span></span></span></span>{recommendedAge}, <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mi>p</mi><mi>a</mi><mi>g</mi><mi>e</mi><mi>s</mi></mrow><mo separator="true">,</mo></mrow><annotation encoding="application/x-tex">{pages}, </annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.625em;vertical-align:-0.1944em;"></span><span class="mord"><span class="mord mathnormal">p</span><span class="mord mathnormal">a</span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span><span class="mord mathnormal">es</span></span><span class="mpunct">,</span></span></span></span>{price})
)
Also template strings support line breaks, while normal strings do not.
How it works
The SQL template string tag transforms the template string and returns an object that is understood by both mysql and postgres:
const query = SQLSELECT author FROM books WHERE name = <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mi>b</mi><mi>o</mi><mi>o</mi><mi>k</mi></mrow><mi>A</mi><mi>N</mi><mi>D</mi><mi>a</mi><mi>u</mi><mi>t</mi><mi>h</mi><mi>o</mi><mi>r</mi><mo>=</mo></mrow><annotation encoding="application/x-tex">{book} AND author = </annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord"><span class="mord mathnormal">b</span><span class="mord mathnormal">oo</span><span class="mord mathnormal" style="margin-right:0.03148em;">k</span></span><span class="mord mathnormal">A</span><span class="mord mathnormal" style="margin-right:0.10903em;">N</span><span class="mord mathnormal" style="margin-right:0.02778em;">D</span><span class="mord mathnormal">a</span><span class="mord mathnormal">u</span><span class="mord mathnormal">t</span><span class="mord mathnormal">h</span><span class="mord mathnormal" style="margin-right:0.02778em;">or</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span></span></span></span>{author}
typeof query // => 'object'
query.text // => 'SELECT author FROM books WHERE name = 1ANDauthor=1 AND author = 1ANDauthor=2'
query.sql // => 'SELECT author FROM books WHERE name = ? AND author = ?'
query.values // => ['harry potter', 'J. K. Rowling']
Building complex queries with append()
It is also possible to build queries by appending another query or a string with the append()
method (returns this
for chaining):
query.append(SQLAND genre = ${genre}
).append(' ORDER BY rating')
query.text // => 'SELECT author FROM books WHERE name = 1ANDauthor=1 AND author = 1ANDauthor=2 AND genre = $3 ORDER BY rating'
query.sql // => 'SELECT author FROM books WHERE name = ? AND author = ? AND genre = ? ORDER BY rating'
query.values // => ['harry potter', 'J. K. Rowling', 'Fantasy'] ORDER BY rating
This allows you to build complex queries without having to care about the placeholder index or the values array:
const query = SQLSELECT * FROM books
if (params.name) {
query.append(SQL WHERE name = ${params.name}
)
}
query.append(SQL LIMIT 10 OFFSET ${params.offset || 0}
)
Raw values
Some values cannot be replaced by placeholders in prepared statements, like table names.append()
replaces the SQL.raw()
syntax from version 1, just pass a string and it will get appended raw.
Please note that when inserting raw values, you are responsible for quoting and escaping these values with proper escaping functions first if they come from user input (E.g.
mysql.escapeId()
andpg.escapeIdentifier()
). Also, executing many prepared statements with changing raw values in a loop will quickly overflow the prepared statement buffer (and destroy their performance benefit), so be careful.
const table = 'books' const order = 'DESC' const column = 'author'
db.query(SQLSELECT * FROM "
.append(table).append(SQL" WHERE author = <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mi>a</mi><mi>u</mi><mi>t</mi><mi>h</mi><mi>o</mi><mi>r</mi></mrow><mi>O</mi><mi>R</mi><mi>D</mi><mi>E</mi><mi>R</mi><mi>B</mi><mi>Y</mi></mrow><annotation encoding="application/x-tex">{author} ORDER BY </annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord"><span class="mord mathnormal">a</span><span class="mord mathnormal">u</span><span class="mord mathnormal">t</span><span class="mord mathnormal">h</span><span class="mord mathnormal" style="margin-right:0.02778em;">or</span></span><span class="mord mathnormal" style="margin-right:0.00773em;">OR</span><span class="mord mathnormal" style="margin-right:0.02778em;">D</span><span class="mord mathnormal" style="margin-right:0.05017em;">ERB</span><span class="mord mathnormal" style="margin-right:0.22222em;">Y</span></span></span></span>{column}
).append(order))
// escape user input manually
mysql.query(SQLSELECT * FROM
.append(mysql.escapeId(someUserInput)).append(SQLWHERE name = <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mi>b</mi><mi>o</mi><mi>o</mi><mi>k</mi></mrow><mi>O</mi><mi>R</mi><mi>D</mi><mi>E</mi><mi>R</mi><mi>B</mi><mi>Y</mi></mrow><annotation encoding="application/x-tex">{book} ORDER BY </annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord"><span class="mord mathnormal">b</span><span class="mord mathnormal">oo</span><span class="mord mathnormal" style="margin-right:0.03148em;">k</span></span><span class="mord mathnormal" style="margin-right:0.00773em;">OR</span><span class="mord mathnormal" style="margin-right:0.02778em;">D</span><span class="mord mathnormal" style="margin-right:0.05017em;">ERB</span><span class="mord mathnormal" style="margin-right:0.22222em;">Y</span></span></span></span>{column}
).append(order))
pg.query(SQLSELECT * FROM
.append(pg.escapeIdentifier(someUserInput)).append(SQLWHERE name = <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mi>b</mi><mi>o</mi><mi>o</mi><mi>k</mi></mrow><mi>O</mi><mi>R</mi><mi>D</mi><mi>E</mi><mi>R</mi><mi>B</mi><mi>Y</mi></mrow><annotation encoding="application/x-tex">{book} ORDER BY </annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord"><span class="mord mathnormal">b</span><span class="mord mathnormal">oo</span><span class="mord mathnormal" style="margin-right:0.03148em;">k</span></span><span class="mord mathnormal" style="margin-right:0.00773em;">OR</span><span class="mord mathnormal" style="margin-right:0.02778em;">D</span><span class="mord mathnormal" style="margin-right:0.05017em;">ERB</span><span class="mord mathnormal" style="margin-right:0.22222em;">Y</span></span></span></span>{column}
).append(order))
Binding Arrays
To bind the array dynamically as a parameter use ANY (PostgreSQL only):
const authors = ['J. K. Rowling', 'J. R. R. Tolkien']
const query = SQLSELECT name FROM books WHERE author = ANY(${authors})
query.text // => 'SELECT name FROM books WHERE author = ANY($1)'
query.values // => ['J. K. Rowling', 'J. R. R. Tolkien']
Named Prepared Statements in Postgres
Postgres has the option of naming prepared statements, which allows parsing and other work to be reused (and requires the SQL associated with the name to stay the same, with only the parameters changing). You can set the name with the setName()
method:
// old way pg.query({name: 'my_query', text: 'SELECT author FROM books WHERE name = $1', values: [book]})
// with template strings
pg.query(SQLSELECT author FROM books WHERE name = ${book}
.setName('my_query'))
You can also set the name property on the statement object directly or use Object.assign()
.
Bound Statements in sequelize
By default, Sequelize will escape replacements on the client. To switch to using a bound statement in Sequelize, call useBind()
. The boolean parameter defaults to true
. Like all methods, returns this
for chaining. Please note that as long as the bound mode is active, the statement object only supports Sequelize, not the other drivers.
// old way sequelize.query('SELECT author FROM books WHERE name = ? AND author = ?', {bind: [book, author]})
// with template strings
sequelize.query(SQLSELECT author FROM books WHERE name = ${book}
.useBind(true))
sequelize.query(SQLSELECT author FROM books WHERE name = ${book}
.useBind()) // the same
// works with append (you can call useBind at any time)
const query = SQLSELECT * FROM books
.useBind(true)
if (params.name) {
query.append(SQL WHERE name = ${params.name}
)
}
query.append(SQL LIMIT 10 OFFSET ${params.offset || 0}
)
Editor Integration
- Sublime Text: javascript-sql-sublime-syntax
- Vim: vim-javascript-sql
Contributing
- Tests are written using mocha
- You can use
npm test
to run the tests and check coding style - Since this module is only compatible with ES6 versions of node anyway, use all the ES6 goodies
- Pull requests are welcome :)