├── .travis.yml ├── logo.png ├── .gitignore ├── .github ├── funding.yml ├── workflows │ └── main.yml ├── bug_report.md └── feature_request.md ├── rollup.config.js ├── .editorconfig ├── src ├── index.js ├── utils.js ├── constants.js ├── literal.js └── query.js ├── example.js ├── package.json ├── license ├── test └── index.js └── readme.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 6 4 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terkelg/sqliterally/HEAD/logo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | packge-lock.json 4 | *.lock 5 | *.log 6 | dist 7 | -------------------------------------------------------------------------------- /.github/funding.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: terkelg 4 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import pkg from './package.json'; 2 | 3 | export default [{ 4 | input: 'src/index.js', 5 | output: [ 6 | { file: pkg.main, format: 'cjs' }, 7 | { file: pkg.module, format: 'es' } 8 | ] 9 | }]; 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [{package.json,*.yml}] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Query from './query'; 2 | import Literal from './literal'; 3 | import {startingClauses} from './constants'; 4 | 5 | export const query = new Query(startingClauses); 6 | export const sql = (pieces, ...values) => new Literal(pieces, values); 7 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | export const mergeAdjecent = (arr, i, before = 0, after = 0) => 2 | arr.splice( 3 | i - before, 4 | before + 1 + after, 5 | [...arr.slice(i - before, i), arr[i], ...arr.slice(i + 1, i + 1 + after)].join('') 6 | ); 7 | 8 | export const copy = o => 9 | Object.keys(o).reduce((newObject, key) => ((newObject[key] = o[key].slice()), newObject), {}); 10 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | name: Node.js v${{ matrix.nodejs }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | nodejs: [8, 10, 12] 12 | 13 | steps: 14 | - uses: actions/checkout@master 15 | with: 16 | fetch-depth: 1 17 | - uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.nodejs }} 20 | 21 | - name: Install 22 | run: | 23 | npm install 24 | - name: Test 25 | run: npm test 26 | -------------------------------------------------------------------------------- /.github/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Describe the bug 11 | A clear and concise description of what the bug is. 12 | 13 | ### To Reproduce 14 | Steps to reproduce the behavior: 15 | 16 | 1. Step 1 17 | 2. Step 2 18 | 3. ... 19 | 20 | ### Expected behavior 21 | A clear and concise description of what you expected to happen. 22 | 23 | ### System 24 | - OS: [e.g. MacOS 10.14.3] 25 | - Node version: `node -v` 26 | 27 | ### Additional context 28 | Add any other context about the problem here. 29 | -------------------------------------------------------------------------------- /.github/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Is your feature request related to a problem? 11 | Please describe. A clear and concise description of what the problem is. 12 | Ex. I'm always frustrated when [...] 13 | 14 | ### Describe the solution you'd like 15 | A clear and concise description of what you want to happen. 16 | 17 | ### Describe alternatives you've considered 18 | A clear and concise description of any alternative solutions or features you've considered. 19 | 20 | ### Additional context 21 | Add any other context or screenshots about the feature request here. 22 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | const {query, sql} = require('./dist/sqliterally.js'); 2 | 3 | const number = 34; 4 | const name = 'Potter'; 5 | 6 | let result, sub; 7 | 8 | // Simple query 9 | result = sql`SELECT * FROM movies WHERE title = ${name}`; 10 | console.log(result.text, result.values); 11 | 12 | // Append string to query 13 | result = sql`SELECT * FROM movies WHERE title = ${name}`.append(` LIMIT 5`); 14 | console.log(result.text, result.values); 15 | 16 | // Append subquery 17 | sub = sql` AND age < ${number}`; 18 | result = sql`SELECT * FROM movies WHERE title = ${name}`.append(sub); 19 | console.log(result.text, result.values); 20 | 21 | // Query 22 | result = query 23 | .select`title` 24 | .select`id` 25 | .from`movies` 26 | .where`age < ${number}` 27 | .where`name = ${name}` 28 | .limit`5` 29 | .build(); 30 | 31 | console.log(result.text, result.values); // postgres 32 | console.log(result.sql, result.values); // mysql -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sqliterally", 3 | "version": "1.0.3", 4 | "repository": "terkelg/sqliterally", 5 | "description": "Lightweight SQL query builder", 6 | "module": "dist/sqliterally.mjs", 7 | "main": "dist/sqliterally.js", 8 | "license": "MIT", 9 | "author": { 10 | "name": "Terkel Gjervig", 11 | "email": "terkel@terkel.com", 12 | "url": "https://terkel.com" 13 | }, 14 | "engines": { 15 | "node": ">=8" 16 | }, 17 | "scripts": { 18 | "build": "rimraf dist && rollup -c", 19 | "pretest": "npm run build", 20 | "test": "tape test/*.js | tap-spec", 21 | "prepublish": "npm run build" 22 | }, 23 | "files": [ 24 | "dist" 25 | ], 26 | "keywords": [ 27 | "db", 28 | "sql", 29 | "orm", 30 | "mysql", 31 | "query", 32 | "builder", 33 | "strings", 34 | "postgres", 35 | "database", 36 | "template", 37 | "prepared", 38 | "statements" 39 | ], 40 | "devDependencies": { 41 | "rimraf": "^3.0.2", 42 | "rollup": "^2.1.0", 43 | "tap-spec": "5.0.0", 44 | "tape": "4.13.2" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | export const ADDTOCLAUSE = Symbol('addToClause'); 2 | export const STRINGIFY = Symbol('stringify'); 3 | 4 | export const clauseOrder = [ 5 | `select`, 6 | `insert`, 7 | `delete`, 8 | `values`, 9 | `update`, 10 | `set`, 11 | `from`, 12 | `join`, 13 | `where`, 14 | `onDuplicate`, 15 | `groupBy`, 16 | `having`, 17 | `orderBy`, 18 | `limit`, 19 | `returning`, 20 | `lock` 21 | ]; 22 | 23 | export const startingClauses = { 24 | select: [], 25 | insert: [], 26 | onDuplicate: [], 27 | values: [], 28 | update: [], 29 | set: [], 30 | from: [], 31 | join: [], 32 | where: [], 33 | groupBy: [], 34 | having: [], 35 | orderBy: [], 36 | limit: [], 37 | delete: [], 38 | returning: [], 39 | lock: [] 40 | }; 41 | 42 | export const clauseStrings = { 43 | select: 'SELECT ', 44 | insert: 'INSERT INTO ', 45 | onDuplicate: 'ON DUPLICATE KEY UPDATE', 46 | values: 'VALUES ', 47 | update: 'UPDATE ', 48 | set: 'SET ', 49 | from: 'FROM ', 50 | join: '', 51 | where: 'WHERE ', 52 | groupBy: 'GROUP BY ', 53 | having: 'HAVING ', 54 | orderBy: 'ORDER BY ', 55 | limit: 'LIMIT ', 56 | delete: 'DELETE ', 57 | returning: 'RETURNING ', 58 | lock: '' 59 | }; 60 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Terkel Gjervig (terkel.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/literal.js: -------------------------------------------------------------------------------- 1 | import {mergeAdjecent} from './utils'; 2 | import {STRINGIFY, ADDTOCLAUSE} from './constants'; 3 | 4 | export default class Literal { 5 | constructor(pieces = [''], values = [], delimiter = '') { 6 | this.pieces = [...pieces]; 7 | this.values = [...values]; 8 | this.delimiter = delimiter; 9 | 10 | for (let i = 0, j = 1, k = 0; i < values.length; i++, j++, k++) { 11 | let val = values[i]; 12 | if (val && val[ADDTOCLAUSE]) val = val.build(' '); 13 | if (val instanceof Literal) { 14 | this.values.splice(k, 1); 15 | 16 | if (val.pieces.length === 0) { 17 | mergeAdjecent(this.pieces, j, 1); 18 | continue; 19 | } 20 | 21 | this.pieces.splice(j, 0, ...val.pieces); 22 | mergeAdjecent(this.pieces, j, 1, 0); 23 | mergeAdjecent(this.pieces, j + val.pieces.length - 2, 0, 1); 24 | 25 | this.values.splice(k, 0, ...val.values); 26 | j += val.pieces.length; 27 | k += val.values.length; 28 | i += val.values.length; 29 | } 30 | } 31 | } 32 | 33 | append(literal, delimiter = '') { 34 | const clone = this.clone(); 35 | 36 | if (typeof literal === 'string') { 37 | clone.pieces[clone.pieces.length - 1] += `${delimiter}${literal}`; 38 | return clone; 39 | } 40 | 41 | clone.pieces[clone.pieces.length - 1] += `${delimiter || literal.delimiter}${literal.pieces[0]}`; 42 | const [_, ...pieces] = literal.pieces; 43 | clone.pieces.push(...pieces); 44 | clone.values.push(...literal.values); 45 | 46 | return clone; 47 | } 48 | 49 | prefix(string = '') { 50 | const clone = this.clone(); 51 | clone.pieces[0] = `${string}${this.pieces[0]}`; 52 | return clone; 53 | } 54 | 55 | suffix(string = '') { 56 | const clone = this.clone(); 57 | clone.pieces[clone.pieces.length] += string; 58 | return clone; 59 | } 60 | 61 | clone() { 62 | return new Literal(this.pieces, this.values, this.delimiter); 63 | } 64 | 65 | [STRINGIFY](type = 'pg') { 66 | return this.pieces.reduce((acc, part, i) => acc + (type == 'pg' ? '$' + i : '?') + part); 67 | } 68 | 69 | get text() { 70 | return this[STRINGIFY]('pg'); 71 | } 72 | 73 | get sql() { 74 | return this[STRINGIFY]('mysql'); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/query.js: -------------------------------------------------------------------------------- 1 | import {copy} from './utils'; 2 | import Literal from './literal'; 3 | import {clauseOrder, clauseStrings, ADDTOCLAUSE} from './constants'; 4 | 5 | export default class Query { 6 | constructor(clauses) { 7 | this.clauses = copy(clauses); 8 | } 9 | 10 | [ADDTOCLAUSE](key, literal, override) { 11 | const state = copy(this.clauses); 12 | override ? (state[key] = [literal]) : state[key].push(literal); 13 | return new Query(state); 14 | } 15 | 16 | build(delimiter = '\n') { 17 | return clauseOrder 18 | .map(key => ({key, expressions: this.clauses[key]})) 19 | .filter(clause => clause.expressions && clause.expressions.length > 0) 20 | .map(({expressions, key}) => 21 | expressions.reduce((acc, literal) => acc.append(literal)).prefix(clauseStrings[key]) 22 | ) 23 | .reduce((acc, query) => acc.append(query, delimiter)); 24 | } 25 | 26 | select(pieces, ...values) { 27 | return this[ADDTOCLAUSE]('select', new Literal(pieces, values, ', ')); 28 | } 29 | 30 | update(pieces, ...values) { 31 | return this[ADDTOCLAUSE]('update', new Literal(pieces, values), true); 32 | } 33 | 34 | set(pieces, ...values) { 35 | return this[ADDTOCLAUSE]('set', new Literal(pieces, values, ', ')); 36 | } 37 | 38 | from(pieces, ...values) { 39 | return this[ADDTOCLAUSE]('from', new Literal(pieces, values), true); 40 | } 41 | 42 | join(pieces, ...values) { 43 | return this[ADDTOCLAUSE]('join', new Literal(pieces, values, '\n').prefix('JOIN ')); 44 | } 45 | 46 | leftJoin(pieces, ...values) { 47 | return this[ADDTOCLAUSE]('join', new Literal(pieces, values, '\n').prefix('LEFT JOIN ')); 48 | } 49 | 50 | where(pieces, ...values) { 51 | return this[ADDTOCLAUSE]('where', new Literal(pieces, values, ' AND ')); 52 | } 53 | 54 | orWhere(pieces, ...values) { 55 | return this[ADDTOCLAUSE]('where', new Literal(pieces, values, ' OR ')); 56 | } 57 | 58 | having(pieces, ...values) { 59 | return this[ADDTOCLAUSE]('having', new Literal(pieces, values, ' AND ')); 60 | } 61 | 62 | orHaving(pieces, ...values) { 63 | return this[ADDTOCLAUSE]('having', new Literal(pieces, values, ' OR ')); 64 | } 65 | 66 | groupBy(pieces, ...values) { 67 | return this[ADDTOCLAUSE]('groupBy', new Literal(pieces, values, ', ')); 68 | } 69 | 70 | orderBy(pieces, ...values) { 71 | return this[ADDTOCLAUSE]('orderBy', new Literal(pieces, values, ', ')); 72 | } 73 | 74 | update(pieces, ...values) { 75 | return this[ADDTOCLAUSE]('update', new Literal(pieces, values), true); 76 | } 77 | 78 | limit(pieces, ...values) { 79 | return this[ADDTOCLAUSE]('limit', new Literal(pieces, values), true); 80 | } 81 | 82 | returning(pieces, ...values) { 83 | return this[ADDTOCLAUSE]('returning', new Literal(pieces, values, ', ')); 84 | } 85 | 86 | get lockInShareMode() { 87 | return this[ADDTOCLAUSE](`lock`, new Literal([`LOCK IN SHARE MODE`]), true); 88 | } 89 | 90 | get forUpdate() { 91 | return this[ADDTOCLAUSE](`lock`, new Literal([`FOR UPDATE`]), true); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const test = require('tape'); 2 | const {sql, query} = require('../dist/sqliterally'); 3 | 4 | test('sqliterally', t => { 5 | t.is(typeof sql, 'function', 'exports object'); 6 | t.is(typeof query , 'object', 'exports object'); 7 | t.end(); 8 | }); 9 | 10 | 11 | test('sql', t => { 12 | let x = sql`SELECT * FROM animals`; 13 | t.is(x.text, 'SELECT * FROM animals'); 14 | t.is(x.sql, 'SELECT * FROM animals'); 15 | t.same(x.values, []); 16 | t.end(); 17 | }); 18 | 19 | test('sql: parameterized', t => { 20 | let movie = 'Memento'; 21 | let x = sql`SELECT director FROM movies WHERE title = ${movie}`; 22 | t.is(x.text, 'SELECT director FROM movies WHERE title = $1'); 23 | t.is(x.sql, 'SELECT director FROM movies WHERE title = ?'); 24 | t.same(x.values, ['Memento']); 25 | 26 | x = sql`SELECT ${'lion'}, ${'zebra'} FROM animals WHERE age < ${2}`; 27 | t.is(x.text, 'SELECT $1, $2 FROM animals WHERE age < $3'); 28 | t.is(x.sql, 'SELECT ?, ? FROM animals WHERE age < ?'); 29 | t.same(x.values, ['lion', 'zebra', 2]); 30 | 31 | x = sql``; 32 | t.is(x.text, ''); 33 | t.same(x.values, []); 34 | t.end(); 35 | }); 36 | 37 | test('sql: append', t => { 38 | let x = sql`SELECT * FROM animals`.append(` WHERE x = y`); 39 | t.is(x.text, 'SELECT * FROM animals WHERE x = y'); 40 | t.is(x.sql, 'SELECT * FROM animals WHERE x = y'); 41 | t.same(x.values, []); 42 | 43 | let sub = sql` WHERE id = ${1}`; 44 | x = sql`SELECT * FROM animals`.append(sub); 45 | t.is(x.text, 'SELECT * FROM animals WHERE id = $1'); 46 | t.is(x.sql, 'SELECT * FROM animals WHERE id = ?'); 47 | t.same(x.values, [1]); 48 | 49 | t.end(); 50 | }); 51 | 52 | test('sql: empty input', t => { 53 | let x = sql``; 54 | t.is(x.text, ''); 55 | t.same(x.values, []); 56 | t.end(); 57 | }); 58 | 59 | test('sql: null and undefined input', t => { 60 | let x = sql`INSERT INTO test VALUES (${undefined}, ${null})`; 61 | t.is(x.text, 'INSERT INTO test VALUES ($1, $2)'); 62 | t.same(x.values, [undefined, null]); 63 | t.end(); 64 | }); 65 | 66 | test('sql: nested', t => { 67 | let field = 'cust_name', value = 5000; 68 | let sub = sql`SELECT DISTINCT cust_id, ${field} FROM orders WHERE order_value > ${value}`; 69 | let x = sql`SELECT * FROM customers WHERE cust_id IN (${sub})`; 70 | t.is(x.text, 'SELECT * FROM customers WHERE cust_id IN (SELECT DISTINCT cust_id, $1 FROM orders WHERE order_value > $2)'); 71 | t.is(x.sql, 'SELECT * FROM customers WHERE cust_id IN (SELECT DISTINCT cust_id, ? FROM orders WHERE order_value > ?)'); 72 | t.same(x.values, [field, value]); 73 | t.end(); 74 | }); 75 | 76 | test('query', t => { 77 | let x = query.select`*`.from`animals`; 78 | let clauseMethods = [ 79 | 'select', 'from', 'join', 'leftJoin', 'where', 'orWhere', 80 | 'having', 'orHaving', 'groupBy', 'orderBy', 'update', 81 | 'limit', 'lockInShareMode', 'forUpdate', 'returning' 82 | ] 83 | clauseMethods.forEach(clause => t.is(clause in query, true)) 84 | t.end(); 85 | }); 86 | 87 | test('query: build', t => { 88 | let x = query.select`*`.from`animals`.build(); 89 | t.is(x.text, 'SELECT *\nFROM animals'); 90 | t.same(x.values, []); 91 | t.end(); 92 | }); 93 | 94 | test('query: build custom delimiter', t => { 95 | let x = query.select`*`.from`animals`.build(' '); 96 | t.is(x.text, 'SELECT * FROM animals'); 97 | t.same(x.values, []); 98 | t.end(); 99 | }); 100 | 101 | test('query: parameterized', t => { 102 | let name = 'dumbo'; 103 | let column = 'name'; 104 | let x = query 105 | .select`${column}` 106 | .from`animals` 107 | .where`name = ${name}` 108 | .build(); 109 | 110 | t.is(x.text, 'SELECT $1\nFROM animals\nWHERE name = $2'); 111 | t.is(x.sql, 'SELECT ?\nFROM animals\nWHERE name = ?'); 112 | t.same(x.values, [column, name]); 113 | t.end(); 114 | }); 115 | 116 | test('query: order clauses', t => { 117 | let x = query 118 | .where`a = ${1}` 119 | .from`table` 120 | .select`*` 121 | .forUpdate 122 | .where`b = ${2}` 123 | .build() 124 | t.is(x.text, 'SELECT *\nFROM table\nWHERE a = $1 AND b = $2\nFOR UPDATE'); 125 | t.is(x.sql, 'SELECT *\nFROM table\nWHERE a = ? AND b = ?\nFOR UPDATE'); 126 | t.same(x.values, [1, 2]); 127 | 128 | x = query 129 | .set`kind = Dramatic` 130 | .update`films` 131 | .set`duration = 120` 132 | .build() 133 | t.is(x.text, 'UPDATE films\nSET kind = Dramatic, duration = 120'); 134 | t.same(x.values, []); 135 | 136 | t.end(); 137 | }); 138 | 139 | test('query: select clause', t => { 140 | let x = query 141 | .select`a, b, ${'c'}` 142 | .select`x` 143 | .select`y` 144 | .select`z` 145 | .build(); 146 | 147 | t.is(x.text, 'SELECT a, b, $1, x, y, z'); 148 | t.is(x.sql, 'SELECT a, b, ?, x, y, z'); 149 | t.same(x.values, ['c']); 150 | t.end(); 151 | }); 152 | 153 | test('query: from clause', t => { 154 | let x = query 155 | .from`animals` 156 | .from`humans` 157 | .from`aliens` 158 | .build(); 159 | 160 | t.is(x.text, 'FROM aliens'); 161 | t.is(x.sql, 'FROM aliens'); 162 | t.same(x.values, []); 163 | t.end(); 164 | }); 165 | 166 | test('query: where clause', t => { 167 | let x = query 168 | .where`a > b` 169 | .where`z = y` 170 | .where`c = ${11}` 171 | .build(); 172 | t.is(x.text, 'WHERE a > b AND z = y AND c = $1'); 173 | t.is(x.sql, 'WHERE a > b AND z = y AND c = ?'); 174 | t.same(x.values, [11]); 175 | 176 | x = query 177 | .orWhere`a > b` 178 | .orWhere`z = y` 179 | .orWhere`c = ${11}` 180 | .build(); 181 | t.is(x.text, 'WHERE a > b OR z = y OR c = $1'); 182 | t.is(x.sql, 'WHERE a > b OR z = y OR c = ?'); 183 | t.same(x.values, [11]); 184 | 185 | x = query 186 | .where`a > b` 187 | .orWhere`z = y` 188 | .where`c = ${11}` 189 | .where`(q > 1 AND q < 10)` 190 | .build(); 191 | t.is(x.text, 'WHERE a > b OR z = y AND c = $1 AND (q > 1 AND q < 10)'); 192 | t.is(x.sql, 'WHERE a > b OR z = y AND c = ? AND (q > 1 AND q < 10)'); 193 | t.same(x.values, [11]); 194 | 195 | t.end(); 196 | }); 197 | 198 | test('query: update', t => { 199 | let x = query 200 | .update`films` 201 | .set`kind = Dramatic` 202 | .set`duration = 120` 203 | .where`id = 2` 204 | .returning`*` 205 | .build(); 206 | t.is(x.text, 'UPDATE films\nSET kind = Dramatic, duration = 120\nWHERE id = 2\nRETURNING *'); 207 | t.same(x.values, []); 208 | 209 | x = query 210 | .update`not me` 211 | .update`films` 212 | .set`kind = Dramatic` 213 | .returning`title` 214 | .returning`duration` 215 | .build(); 216 | t.is(x.text, 'UPDATE films\nSET kind = Dramatic\nRETURNING title, duration'); 217 | t.same(x.values, []); 218 | 219 | t.end(); 220 | }); 221 | 222 | test('query: join', t => { 223 | let x = query 224 | .join`a ON b.id = a.id` 225 | .join`c ON d` 226 | .build(); 227 | t.is(x.text, 'JOIN a ON b.id = a.id\nJOIN c ON d'); 228 | t.same(x.values, []); 229 | 230 | x = query 231 | .leftJoin`a ON b.id = a.id` 232 | .leftJoin`c ON d` 233 | .build(); 234 | t.is(x.text, 'LEFT JOIN a ON b.id = a.id\nLEFT JOIN c ON d'); 235 | t.same(x.values, []); 236 | 237 | x = query 238 | .join`a ON b.id = a.id` 239 | .leftJoin`c ON d` 240 | .build(); 241 | t.is(x.text, 'JOIN a ON b.id = a.id\nLEFT JOIN c ON d'); 242 | t.same(x.values, []); 243 | 244 | t.end(); 245 | }); 246 | 247 | test('query: having', t => { 248 | let x = query 249 | .having`MAX (list_price) > 4000` 250 | .having`MIN (list_price) < 500` 251 | .build(); 252 | t.is(x.text, 'HAVING MAX (list_price) > 4000 AND MIN (list_price) < 500'); 253 | t.same(x.values, []); 254 | 255 | x = query 256 | .having`MAX (list_price) > 4000` 257 | .orHaving`MIN (list_price) < 500` 258 | .build(); 259 | t.is(x.text, 'HAVING MAX (list_price) > 4000 OR MIN (list_price) < 500'); 260 | t.same(x.values, []); 261 | 262 | t.end(); 263 | }); 264 | 265 | test('query: group by', t => { 266 | let x = query 267 | .groupBy`a, b` 268 | .groupBy`c` 269 | .groupBy`d` 270 | .build(); 271 | 272 | t.is(x.text, 'GROUP BY a, b, c, d'); 273 | t.same(x.values, []); 274 | t.end(); 275 | }); 276 | 277 | test('query: order by', t => { 278 | let x = query 279 | .orderBy`a, b` 280 | .orderBy`COUNT(c) DESC` 281 | .orderBy`d` 282 | .build(); 283 | 284 | t.is(x.text, 'ORDER BY a, b, COUNT(c) DESC, d'); 285 | t.same(x.values, []); 286 | t.end(); 287 | }); 288 | 289 | test('query: limit', t => { 290 | let x = query 291 | .limit`10` 292 | .limit`5` 293 | .build(); 294 | 295 | t.is(x.text, 'LIMIT 5'); 296 | t.same(x.values, []); 297 | t.end(); 298 | }); 299 | 300 | test('query: returning', t => { 301 | let x = query 302 | .returning`name` 303 | .returning`email` 304 | .build(); 305 | 306 | t.is(x.text, 'RETURNING name, email'); 307 | t.same(x.values, []); 308 | t.end(); 309 | }); 310 | 311 | test('query: lock share mode', t => { 312 | let x = query 313 | .lockInShareMode 314 | .build(); 315 | t.is(x.text, 'LOCK IN SHARE MODE'); 316 | t.same(x.values, []); 317 | 318 | x = query 319 | .lockInShareMode 320 | .lockInShareMode 321 | .build(); 322 | t.is(x.text, 'LOCK IN SHARE MODE'); 323 | t.end(); 324 | }); 325 | 326 | test('query: for update', t => { 327 | let x = query 328 | .forUpdate 329 | .build(); 330 | t.is(x.text, 'FOR UPDATE'); 331 | t.same(x.values, []); 332 | 333 | x = query 334 | .forUpdate 335 | .forUpdate 336 | .build(); 337 | t.is(x.text, 'FOR UPDATE'); 338 | t.end(); 339 | }); 340 | 341 | test('query: build twice', t => { 342 | let x = query.select`*`.from`users`.where`id = ${123}`; 343 | x.build(); 344 | x = x.build(' '); 345 | t.is(x.text, 'SELECT * FROM users WHERE id = $1'); 346 | t.is(x.sql, 'SELECT * FROM users WHERE id = ?'); 347 | t.same(x.values , [123]); 348 | t.end(); 349 | }); 350 | 351 | test('query: nested', t => { 352 | let sub = query.select`*`.from`users`.where`id = ${123}`; 353 | let main = query.select`*`.from`posts`.where`user = (${sub})`.build(); 354 | t.is(main.text, 'SELECT *\nFROM posts\nWHERE user = (SELECT * FROM users WHERE id = $1)'); 355 | t.is(main.sql, 'SELECT *\nFROM posts\nWHERE user = (SELECT * FROM users WHERE id = ?)'); 356 | t.same(main.values , [123]); 357 | t.end(); 358 | }); 359 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |
2 | sqliterally 3 |
4 | 5 |
6 | 7 | version 8 | 9 | 10 | integration status 11 | 12 | 13 | downloads 14 | 15 |
16 | 17 |

18 | Composable and safe parameterized queries using tagged template literals 19 |

20 | 21 |
22 | 23 | SQLiterally makes it easy to compose safe parameterized SQL queries using template literals. Clauses are automatically arranged which means you can re-use, subquery and append new clauses as you like – order doesn't matter. All queries are well formatted and ready to be passed directly to [`node-pg`](https://github.com/brianc/node-postgres) and [`mysql`](https://github.com/mysqljs/mysql). 24 | 25 | Use SQLiterally as a lightweight alternative to extensive query builders like [`Knex.js`](http://knexjs.org/) or when big ORMs are over-kill. 26 | 27 | > **OBS**: _SQLiterally provides a lot of freedom by design and it's not meant to reduce the SQL learning curve. It won't prevent you from writing incorrect queries._ 28 | 29 | ## Features 30 | 31 | * Build queries programmatically 32 | * Works directly with [`node-pg`](https://github.com/brianc/node-postgres) and [`mysql`](https://github.com/mysqljs/mysql) 33 | * Supports nested sub-queries 34 | * Queries are parametrized to protect against SQL injections 35 | * Write SQL as you like with no restrictions using string literals 36 | * Produces well-formatted queries with line breaks 37 | * Lightweight with **no dependencies**! 38 | 39 | This module exposes two module definitions: 40 | 41 | * **ES Module**: `dist/sqliterally.mjs` 42 | * **CommonJS**: `dist/sqliterally.js` 43 | 44 | 45 | ## Installation 46 | 47 | ``` 48 | npm install sqliterally --save 49 | ``` 50 | 51 | ## Usage 52 | 53 | The module exposes two functions: 54 | * [**sql**](#sqlstring): Use this to construct any query. Useful for complex SQL scripts or when you know the full query and all you need is a parameterized query object. 55 | * [**query**](#query): Use this to programmatically compose parameterized queries. Useful for constructing queries as you go. 56 | 57 | ```js 58 | import {sql, query} from 'sqliterally'; 59 | 60 | let movie = 'Memento', year = 2001; 61 | 62 | sql`SELECT director FROM movies WHERE title = ${movie}`; 63 | // => { 64 | // text: 'SELECT director FROM movies WHERE title = $1' 65 | // sql => 'SELECT director FROM movies WHERE title = ?' 66 | // values => ['Memento'] 67 | // } 68 | 69 | let q = query 70 | .select`director` 71 | .select`year` 72 | .from`movies` 73 | .where`title = ${movie}` 74 | .limit`5`; 75 | 76 | if (year) q = q.where`year >= ${year}`; 77 | if (writers) q = q.select`writers`; 78 | 79 | q.build(); 80 | // => { 81 | // text: `SELECT director, year FROM movies WHERE title = $1 AND year >= $2 LIMIT 5' 82 | // sql => 'SELECT director, year FROM movies WHERE title = ? AND year >= ? LIMIT 5' 83 | // values => ['Memento', 2001] 84 | // } 85 | ``` 86 | 87 | ## API 88 | 89 | ### sql\`string\` 90 | 91 | Returns: `Object` 92 | 93 | The string can contain nested SQLiterally `query` and `sql` objects. 94 | Indexes and values are taken care of automatically. 95 | 96 | You can pass this directly to [`node-pg`](https://github.com/brianc/node-postgres) and [`mysql`](https://github.com/mysqljs/mysql). 97 | 98 | ```js 99 | let name = 'Harry Potter'; 100 | let max = 10, min = 0; 101 | 102 | sub = sql`age > ${min} AND age < ${max}`; 103 | sql`SELECT * FROM x WHERE name = ${name} OR (${sub}) LIMIT 2`; 104 | // => { 105 | // text: 'SELECT * FROM x WHERE name = $1 OR (age > $2 OR age < $3) LIMIT 2', 106 | // sql: 'SELECT * FROM x WHERE name = ? OR (age > ? OR age < ?) LIMIT 2', 107 | // values: ['Harry Potter', 0, 10] 108 | // } 109 | 110 | let script = sql` 111 | CREATE OR REPLACE FUNCTION update_modified_column() 112 | RETURNS TRIGGER AS $$ 113 | BEGIN 114 | NEW.modified = now(); 115 | RETURN NEW; 116 | END; 117 | $$ language 'plpgsql'; 118 | ` 119 | // => { text: 'CREATE OR REPL...', sql: 'CREATE OR REPL...' values: [] } 120 | ``` 121 | 122 | #### text 123 | 124 | Type: `String` 125 | 126 | Getter that returns the parameterized string for [Postgres](https://github.com/brianc/node-postgres). 127 | 128 | 129 | #### sql 130 | 131 | Type: `String` 132 | 133 | Getter that returns the parameterized string for [MySQL](https://github.com/mysqljs/mysql). 134 | 135 | 136 | #### values 137 | 138 | Type: `Array` 139 | 140 | Getter that returns the corresponding values in order. 141 | 142 | 143 | ### query 144 | 145 | Build a query by adding clauses. The order in which clauses are added doesn't matter. The final output is sorted and returned in the correct order no matter what order you call the methods in. 146 | 147 | You can nest as many `query` and `sql` as you like. You don't have to build sub-queries before nesting them. 148 | 149 | `query` is immutable and all method calls return a new instance. This means you can build up a base query and re-use it. For example, with conditional where clauses or joins. 150 | 151 | > **OBS:** If you call a method multiple times, the values are concatenated in the same order you called them. 152 | 153 | ```js 154 | let age = 13, limit = 10, page = 1, paginate = false; 155 | 156 | let sub = query 157 | .select`id` 158 | .from`customers` 159 | .where`salary > 45000`; 160 | 161 | let main = query 162 | .select`*` 163 | .from`customers` 164 | .where`age > '${age}'` 165 | .where`id IN (${sub})`; 166 | 167 | main = paginate ? main.limit`${limit} OFFSET ${limit * page}` : main; 168 | 169 | main.build(); 170 | ``` 171 | 172 | #### build(delimiter?) 173 | 174 | Constructs the final query and returns a [`sql`](#sql) query object ready for [`node-pg`](https://github.com/brianc/node-postgres) and [`mysql`](https://github.com/mysqljs/mysql). 175 | 176 | > You can still append to the returned `sql` object or use it as a sub-query. You don't have to call `.build()` when nesting queries – there's no reason to call build before you need the parameterized string and values. 177 | 178 | ##### delimiter 179 | 180 | Type: `String`
181 | Default: `\n` 182 | 183 | Change the delimiter used to combine clauses. The default is a line break. 184 | 185 | #### select\`string\` 186 | 187 | Returns: `query` 188 | 189 | All `.select` calls get reduced and joined with `, ` on `.build()`. 190 | 191 | ```js 192 | query.select`*`.build() 193 | // => SELECT * 194 | query.select`cat`.select`zebra`.build() 195 | // => SELECT cat, zebra 196 | query.select`cat, dog`.select`zebra`.build() 197 | // => SELECT cat, dog, zebra 198 | query.select`something`.select`5 * 3 AS result`.build() 199 | // => SELECT something, 5 * 3 AS result 200 | ``` 201 | 202 | 203 | #### update\`string\` 204 | 205 | Returns: `query` 206 | 207 | Calling `.update` more than once result in the clause being overwritten. 208 | 209 | ```js 210 | query.update`film`.build() 211 | // => UPDATE film 212 | query.update`film`.update`books`.build() 213 | // => UPDATE books 214 | ``` 215 | 216 | #### set\`string\` 217 | 218 | Returns: `query` 219 | 220 | All `.set` calls get reduced and joined with `, ` on `.build()`. 221 | 222 | ```js 223 | query.set`a = b`.build() 224 | // => SET a = b 225 | query.set`a = b`.set`z = y`.build() 226 | // => SET a = b, z = y 227 | ``` 228 | 229 | #### from\`string\` 230 | 231 | Returns: `query` 232 | 233 | Calling `.from` more than once result in the clause being overwritten. 234 | 235 | ```js 236 | query.from`film`.build() 237 | // => FROM film 238 | query.from`film AS f`.build() 239 | // => FROM film AS f 240 | query.from`film`.from`books`.build() 241 | // => FROM books 242 | ``` 243 | 244 | #### join\`string\` 245 | 246 | Returns: `query` 247 | 248 | ```js 249 | query.join`c ON d`.build() 250 | // => JOIN c ON d 251 | query.join`a ON b.id`.join`c ON d`.build() 252 | // => JOIN a ON b.id\nJOIN c ON d 253 | ``` 254 | 255 | #### leftJoin\`string\` 256 | 257 | ```js 258 | query.leftJoin`c ON d`.build() 259 | // => LEFT JOIN c ON d 260 | query.leftJoin`a ON b.id`.leftJoin`c ON d`.build() 261 | // => LEFT JOIN a ON b.id\nLEFT JOIN c ON d 262 | ``` 263 | 264 | #### where\`string\` 265 | 266 | Returns: `query` 267 | 268 | All `.where` calls get reduced and joined with ` AND ` on `.build()`. 269 | 270 | ```js 271 | query.where`a < b`.build() 272 | // => WHERE a < b 273 | query.where`a < b`.where`z = y`.build() 274 | // => WHERE a < b AND z = y 275 | query.where`a = z OR a = y`.build() 276 | // => WHERE a = z OR a = y 277 | ``` 278 | 279 | #### orWhere\`string\` 280 | 281 | Returns: `query` 282 | 283 | All `.orWhere` calls get reduced and joined with ` OR ` on `.build()`. 284 | 285 | ```js 286 | query.orWhere`a < b`.build() 287 | // => WHERE a < b 288 | query.orWhere`a < b`.orWhere`z = y`.build() 289 | // => WHERE a < b OR z = y 290 | ``` 291 | 292 | #### having\`string\` 293 | 294 | Returns: `query` 295 | 296 | All `.having` calls get reduced and joined with ` AND ` on `.build()`. 297 | 298 | ```js 299 | query.having`MAX (list_price) > 4000` 300 | // => HAVING MAX (list_price) > 4000 301 | query.having`MAX (list_price) > 4000`.having`MIN (list_price) < 500` 302 | // => HAVING MAX (list_price) > 4000 AND MIN (list_price) < 500' 303 | ``` 304 | 305 | #### orHaving\`string\` 306 | 307 | Returns: `query` 308 | 309 | All `.orHaving` calls get reduced and joined with ` OR ` on `.build()`. 310 | 311 | ```js 312 | query.orHaving`MAX (list_price) > 4000` 313 | // => HAVING MAX (list_price) > 4000 314 | query.orHaving`MAX (list_price) > 4000`.orHaving`MIN (list_price) < 500` 315 | // => HAVING MAX (list_price) > 4000 OR MIN (list_price) < 500' 316 | ``` 317 | 318 | #### groupBy\`string\` 319 | 320 | Returns: `query` 321 | 322 | All `.groupBy` calls get reduced and joined with `, ` on `.build()`. 323 | 324 | ```js 325 | query.groupBy`a, b`.groupBy`c`.groupBy`d`.build() 326 | // => GROUP BY a, b, c, d 327 | ``` 328 | 329 | #### orderBy\`string\` 330 | 331 | Returns: `query` 332 | 333 | All `.orderBy` calls get reduced and joined with `, ` on `.build()`. 334 | 335 | ```js 336 | query.orderBy`a, b`.orderBy`COUNT(c) DESC`.orderBy`d`.build() 337 | // => ORDER BY a, b, COUNT(c) DESC, d 338 | ``` 339 | 340 | #### limit\`string\` 341 | 342 | Returns: `query` 343 | 344 | Calling `.limit` more than once result on the clause being overwritten. 345 | 346 | ```js 347 | query.limit`5`.build() 348 | // => LIMIT 5 349 | query.limit`5 OFFSET 2`.build() 350 | // => LIMIT 5 OFFSET 2 351 | query.limit`5`.limit`10`.build() 352 | // => LIMIT 10 353 | ``` 354 | 355 | #### returning\`string\` 356 | 357 | Returns: `query` 358 | 359 | All `.returning` calls get reduced and joined with `, ` on `.build()`. 360 | 361 | ```js 362 | query.returning`a, b`.returning`c`.returning`d`.build() 363 | // => RETURNING a, b, c, d 364 | ``` 365 | 366 | #### lockInShareMode 367 | 368 | Returns: `query` 369 | 370 | Getter method. Multiple invocations get ignored. 371 | 372 | ```js 373 | query.lockInShareMode.build() 374 | // => LOCK IN SHARE MODE 375 | query.select`*`.from`x`.lockInShareMode.build() 376 | // => SELECT * FROM x LOCK IN SHARE MODE 377 | ``` 378 | 379 | #### forUpdate 380 | 381 | Returns: `query` 382 | 383 | Getter method. Multiple invocations get ignored. 384 | 385 | ```js 386 | query.forUpdate.build() 387 | // => FOR UPDATE 388 | query.select`*`.from`x`.forUpdate.build() 389 | // => SELECT * FROM x FOR UPDATE 390 | query.select`*`.from`x`.lockInShareMode.forUpdate.build() 391 | // => SELECT * FROM x LOCK IN SHARE MODE FOR UPDATE 392 | ``` 393 | 394 | ## Credit 395 | 396 | This module is inspired by [sql-concat](https://github.com/TehShrike/sql-concat) but with a different implementation, support for Postgres, single queries and with a reduced API. 397 | 398 | The `sql` function and merge algorithm are based on [prepare-sql](https://github.com/hyperdivision/prepare-sql). 399 | 400 | 401 | ## License 402 | 403 | MIT © [Terkel Gjervig](https://terkel.com) 404 | --------------------------------------------------------------------------------