├── .gitignore ├── .travis.yml ├── build-logic.js ├── changelog.md ├── clause-handlers.js ├── combinable-logic.js ├── constants.js ├── index.d.ts ├── index.js ├── package-lock.json ├── package.json ├── query-object.js ├── readme.md ├── tagged-template.js └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | install: 3 | - npm i 4 | - npm i git+https://git@github.com/jkroso/v8-argv.git#1.1.1 5 | node_js: 6 | - "8" 7 | - "10" 8 | - "12" 9 | - "14" 10 | -------------------------------------------------------------------------------- /build-logic.js: -------------------------------------------------------------------------------- 1 | import { clauseOrder, clauseKeyToString } from './constants.js' 2 | 3 | function build(clauses, joinedBy = `\n`) { 4 | const built = clauseOrder.map( 5 | key => ({ 6 | key, 7 | ary: clauses[key], 8 | }), 9 | ) 10 | .filter(clause => clause.ary && clause.ary.length > 0) 11 | .map(clause => reduceClauseArray(clause.ary, clauseKeyToString[clause.key])) 12 | .reduce((part1, part2) => combine(joinedBy, part1, part2)) 13 | 14 | return { 15 | sql: built.sql, 16 | values: built.values, 17 | } 18 | } 19 | 20 | function reduceClauseArray(clause, clauseQueryString) { 21 | const reducedClause = clause.reduce((combinedClause, clausePart) => { 22 | if (clausePart.values) { 23 | combinedClause.values = combinedClause.values.concat(clausePart.values) 24 | } 25 | 26 | const joinedBy = (combinedClause.sql && clausePart.joinedBy) ? clausePart.joinedBy : ` ` 27 | 28 | combinedClause.sql = (combinedClause.sql + joinedBy + clausePart.sql).trim() 29 | 30 | return combinedClause 31 | }, { 32 | values: [], 33 | sql: ``, 34 | }) 35 | 36 | return { 37 | values: reducedClause.values, 38 | sql: (`${ clauseQueryString } ${ reducedClause.sql }`).trim(), 39 | } 40 | } 41 | 42 | function combine(joinCharacter, part1, part2) { 43 | return { 44 | values: part1.values.concat(part2.values), 45 | sql: part1.sql + joinCharacter + part2.sql, 46 | } 47 | } 48 | 49 | export { build } 50 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # 4.0.1 2 | 3 | - fix a typo in the type definition 4 | 5 | # 4.0.0 6 | 7 | - breaking: convert to ESM 8 | - feature: add type definition 9 | 10 | # 3.1.0 11 | 12 | - feature: add `union` and `unionAll` methods [#28](https://github.com/TehShrike/sql-concat/pull/28) 13 | 14 | # 3.0.0 15 | 16 | - breaking: drop `str` and `params` properties from the built object [#26](https://github.com/TehShrike/sql-concat/pull/26) 17 | - breaking: drop node 6.0.0 support 18 | - feature: built objects can be used inside the template tag [#25](https://github.com/TehShrike/sql-concat/pull/25) 19 | 20 | # 2.3.1 21 | 22 | - expand the documentation a bit 23 | 24 | # 2.3.0 25 | 26 | - feature: added `toString` method to the query object [#21](https://github.com/TehShrike/sql-concat/pull/21) 27 | - feature: support WHERE/HAVING clauses with only a single argument (no value to escape) [#22](https://github.com/TehShrike/sql-concat/pull/22) 28 | -------------------------------------------------------------------------------- /clause-handlers.js: -------------------------------------------------------------------------------- 1 | const makeOn = on => on ? ` ON ${ on }` : `` 2 | 3 | const expressionToObject = expression => { 4 | if (expression && typeof expression === `object` && expression.values) { 5 | return expression 6 | } else { 7 | return { 8 | sql: expression, 9 | values: [], 10 | } 11 | } 12 | } 13 | 14 | const joinExpressions = (expressions, joinedBy) => { 15 | const values = [] 16 | const sqls = [] 17 | 18 | expressions.forEach(expression => { 19 | const object = expressionToObject(expression) 20 | values.push(...object.values) 21 | sqls.push(object.sql) 22 | }) 23 | 24 | return { 25 | sql: sqls.join(joinedBy), 26 | values, 27 | } 28 | } 29 | 30 | const getComparison = (like, comparison) => like ? `LIKE` : (comparison || `=`) 31 | function getComparisonAndParameterString(value, like, comparison) { 32 | if (value === null) { 33 | return `${ (comparison || `IS`) } ?` 34 | } else if (Array.isArray(value)) { 35 | return `${ (comparison || `IN`) }(?)` 36 | } else { 37 | return `${ getComparison(like, comparison) } ?` 38 | } 39 | } 40 | 41 | function hasABuildFunction(q) { 42 | return typeof q.build === `function` 43 | } 44 | 45 | function combineWithAlias(str, alias) { 46 | return alias ? (`${ str } AS ${ alias }`) : str 47 | } 48 | 49 | export { 50 | staticText, 51 | whateverTheyPutIn, 52 | tableNameOrSubquery, 53 | columnParam, 54 | joinClauseHandler, 55 | } 56 | 57 | function staticText(text) { 58 | return { 59 | sql: text, 60 | } 61 | } 62 | 63 | function whateverTheyPutIn(clausePartsJoinedBy, partsJoinedBy, ...expressions) { 64 | const { sql, values } = joinExpressions(expressions, partsJoinedBy) 65 | return { 66 | sql, 67 | values, 68 | joinedBy: clausePartsJoinedBy, 69 | } 70 | } 71 | 72 | function tableNameOrSubquery(table, alias) { 73 | if (hasABuildFunction(table)) { 74 | const result = table.build(`\n\t`) 75 | 76 | return { 77 | sql: combineWithAlias(`(\n\t${ result.sql }\n)`, alias), 78 | values: result.values, 79 | } 80 | } else { 81 | return { 82 | sql: combineWithAlias(table, alias), 83 | } 84 | } 85 | } 86 | 87 | function columnParam(joinedBy, opts, expression, comparator, value) { 88 | opts = opts || {} 89 | 90 | const expressionObject = expressionToObject(expression) 91 | 92 | if (comparator === undefined) { 93 | if (opts.like) { 94 | throw new Error(`You can't use a "like" comparison without passing in a value`) 95 | } 96 | return { joinedBy, ...expressionObject } 97 | } else if (value === undefined) { 98 | value = comparator 99 | comparator = undefined 100 | } 101 | 102 | const valueIsObject = (value && typeof value === `object` && value.values && typeof value.values === `object`) 103 | 104 | const valueParams = valueIsObject 105 | ? value.values 106 | : [ value ] 107 | 108 | const values = [ ...expressionObject.values, ...valueParams ] 109 | 110 | const comparatorAndValue = valueIsObject 111 | ? getComparison(opts.like, comparator) + ` ` + value.sql 112 | : getComparisonAndParameterString(value, opts.like, comparator) 113 | 114 | return { 115 | sql: `${ expressionObject.sql } ${ comparatorAndValue }`, 116 | values, 117 | joinedBy, 118 | } 119 | } 120 | 121 | function joinClauseHandler(type, table, alias, on) { 122 | if (!on) { 123 | on = alias 124 | alias = undefined 125 | } 126 | 127 | function joinString() { 128 | return `${ type }JOIN ` 129 | } 130 | 131 | let onParams = [] 132 | let onString = `` 133 | 134 | if (on) { 135 | if (on.values) { 136 | onParams = on.values 137 | onString = makeOn(on.sql) 138 | } else { 139 | onString = makeOn(on) 140 | } 141 | } 142 | 143 | if (hasABuildFunction(table)) { 144 | const result = table.build(`\n\t`) 145 | 146 | return { 147 | sql: joinString() + combineWithAlias(`(\n\t${ result.sql }\n)`, alias) + onString, 148 | values: [ ...result.values, ...onParams ], 149 | joinedBy: `\n`, 150 | } 151 | } else { 152 | return { 153 | sql: joinString() + combineWithAlias(table, alias) + onString, 154 | values: onParams, 155 | joinedBy: `\n`, 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /combinable-logic.js: -------------------------------------------------------------------------------- 1 | import sqlString from 'sqlstring' 2 | import { build } from './build-logic.js' 3 | 4 | const combinableType = { 5 | text: `text`, 6 | clauses: `clauses`, 7 | } 8 | 9 | const buildCombinable = (combinableArray, joinedBy = `\n`) => combinableArray.map(({ type, clauses, text }) => { 10 | if (type === combinableType.text) { 11 | return { sql: text, values: [] } 12 | } else if (type === combinableType.clauses) { 13 | return build(clauses, joinedBy) 14 | } 15 | 16 | throw new Error(`TehShrike messed up and somehow referenced a combinable type that isn't supported: "${type}"`) 17 | }).reduce((combined, { sql, values }) => { 18 | if (combined) { 19 | return { 20 | sql: combined.sql + joinedBy + sql, 21 | values: [ 22 | ...combined.values, 23 | ...values, 24 | ], 25 | } 26 | } else { 27 | return { 28 | sql, 29 | values, 30 | } 31 | } 32 | }, null) 33 | 34 | const makeCombinableQueries = combinableArray => ({ 35 | union: ({ getClauses }) => makeCombinableQueries([ 36 | ...combinableArray, 37 | { text: `UNION`, type: combinableType.text }, 38 | { clauses: getClauses(), type: combinableType.clauses }, 39 | ]), 40 | unionAll: ({ getClauses }) => makeCombinableQueries([ 41 | ...combinableArray, 42 | { text: `UNION ALL`, type: combinableType.text }, 43 | { clauses: getClauses(), type: combinableType.clauses }, 44 | ]), 45 | build: joinedBy => buildCombinable(combinableArray, joinedBy), 46 | toString: joinedBy => { 47 | const { sql, values } = buildCombinable(combinableArray, joinedBy) 48 | return sqlString.format(sql, values) 49 | }, 50 | }) 51 | 52 | const combineClauses = (clausesA, combineText, clausesB) => makeCombinableQueries([ 53 | { clauses: clausesA, type: combinableType.clauses }, 54 | { text: combineText, type: combinableType.text }, 55 | { clauses: clausesB, type: combinableType.clauses }, 56 | ]) 57 | 58 | export { combineClauses } 59 | -------------------------------------------------------------------------------- /constants.js: -------------------------------------------------------------------------------- 1 | const clauseKeyToString = { 2 | select: `SELECT`, 3 | insert: `INSERT INTO`, 4 | onDuplicate: `ON DUPLICATE KEY UPDATE`, 5 | values: `VALUES`, 6 | update: `UPDATE`, 7 | set: `SET`, 8 | from: `FROM`, 9 | join: ``, 10 | where: `WHERE`, 11 | groupBy: `GROUP BY`, 12 | having: `HAVING`, 13 | orderBy: `ORDER BY`, 14 | limit: `LIMIT`, 15 | delete: `DELETE`, 16 | lock: ``, 17 | } 18 | 19 | const clauseOrder = [ 20 | `select`, 21 | `insert`, 22 | `delete`, 23 | `values`, 24 | `update`, 25 | `set`, 26 | `from`, 27 | `join`, 28 | `where`, 29 | `onDuplicate`, 30 | `groupBy`, 31 | `having`, 32 | `orderBy`, 33 | `limit`, 34 | `lock`, 35 | ] 36 | 37 | const startingClauses = { 38 | select: [], 39 | insert: [], 40 | onDuplicate: [], 41 | values: [], 42 | update: [], 43 | set: [], 44 | from: [], 45 | join: [], 46 | where: [], 47 | groupBy: [], 48 | having: [], 49 | orderBy: [], 50 | limit: [], 51 | delete: [], 52 | lock: [], 53 | } 54 | 55 | export { 56 | clauseOrder, 57 | clauseKeyToString, 58 | startingClauses, 59 | } 60 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'sql-concat' { 2 | type Buildable = { 3 | build(): { sql: string, values: Value[] } 4 | toString(): string 5 | } 6 | 7 | type StringOrSubquery = string | Buildable 8 | 9 | type Unionable = { 10 | union(query: Buildable): Unionable 11 | unionAll(query: Buildable): Unionable 12 | } & Buildable 13 | 14 | type Value = string | number | null | object 15 | 16 | type Tag = (sql: TemplateStringsArray, ...values: any[]) => string 17 | 18 | export type Query = { 19 | select(...sql_expression: string[]): Query 20 | from(string_or_subquery: StringOrSubquery, alias?: string): Query 21 | join(string_or_subquery: StringOrSubquery, on_expression: string): Query 22 | join(string_or_subquery: StringOrSubquery, alias: string, on_expression: string): Query 23 | leftJoin(string_or_subquery: StringOrSubquery, on_expression: string): Query 24 | leftJoin(string_or_subquery: StringOrSubquery, alias: string, on_expression: string): Query 25 | where(sql_expression: string): Query 26 | where(sql_expression: string, value: Value): Query 27 | where(sql_expression: string, comparator: string, value: Value): Query 28 | orWhere(sql_expression: string): Query 29 | orWhere(sql_expression: string, value: Value): Query 30 | orWhere(sql_expression: string, comparator: string, value: Value): Query 31 | whereLike(sql_expression: string, value: Value): Query 32 | orWhereLike(sql_expression: string, value: Value): Query 33 | having(sql_expression: string): Query 34 | having(sql_expression: string, value: Value): Query 35 | having(sql_expression: string, comparator: string, value: Value): Query 36 | orHaving(sql_expression: string): Query 37 | orHaving(sql_expression: string, value: Value): Query 38 | orHaving(sql_expression: string, comparator: string, value: Value): Query 39 | groupBy(...sql_expression: string[]): Query 40 | orderBy(...sql_expression: string[]): Query 41 | limit(row_count: number): Query 42 | limit(offset: number, row_count: number): Query 43 | forUpdate(): Query 44 | lockInShareMode(): Query 45 | } & Buildable & Unionable & Tag 46 | 47 | const query: Query 48 | 49 | export default query 50 | } 51 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import q from './query-object.js' 2 | import { startingClauses } from './constants.js' 3 | import taggedTemplate from './tagged-template.js' 4 | 5 | export default Object.assign(taggedTemplate, q(startingClauses)) 6 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sql-concat", 3 | "version": "4.0.1", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "sql-concat", 9 | "version": "4.0.1", 10 | "license": "WTFPL", 11 | "dependencies": { 12 | "sqlstring": "^2.3.1" 13 | } 14 | }, 15 | "node_modules/sqlstring": { 16 | "version": "2.3.1", 17 | "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz", 18 | "integrity": "sha1-R1OT/56RR5rqYtyvDKPRSYOn+0A=", 19 | "engines": { 20 | "node": ">= 0.6" 21 | } 22 | } 23 | }, 24 | "dependencies": { 25 | "sqlstring": { 26 | "version": "2.3.1", 27 | "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz", 28 | "integrity": "sha1-R1OT/56RR5rqYtyvDKPRSYOn+0A=" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sql-concat", 3 | "version": "4.0.1", 4 | "description": "It's just fancy string concatenation", 5 | "type": "module", 6 | "main": "index.js", 7 | "types": "index.d.ts", 8 | "scripts": { 9 | "test": "node --test test.js" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git@github.com:TehShrike/sql-concat.git" 14 | }, 15 | "keywords": [ 16 | "sql", 17 | "mysql" 18 | ], 19 | "author": "TehShrike", 20 | "license": "WTFPL", 21 | "bugs": { 22 | "url": "https://github.com/TehShrike/sql-concat/issues" 23 | }, 24 | "homepage": "https://github.com/TehShrike/sql-concat", 25 | "dependencies": { 26 | "sqlstring": "^2.3.1" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /query-object.js: -------------------------------------------------------------------------------- 1 | import { 2 | whateverTheyPutIn, 3 | tableNameOrSubquery, 4 | joinClauseHandler, 5 | columnParam, 6 | staticText, 7 | } from './clause-handlers.js' 8 | import sqlString from 'sqlstring' 9 | import { combineClauses } from './combinable-logic.js' 10 | import { build } from './build-logic.js' 11 | 12 | const q = clauses => ({ 13 | select: addToClause(clauses, `select`, (...args) => whateverTheyPutIn(`, `, `, `, ...args)), 14 | from: addToClause(clauses, `from`, tableNameOrSubquery), 15 | join: addToClause(clauses, `join`, (...args) => joinClauseHandler(``, ...args)), 16 | leftJoin: addToClause(clauses, `join`, (...args) => joinClauseHandler(`LEFT `, ...args)), 17 | where: addToClause(clauses, `where`, (...args) => columnParam(` AND `, { like: false }, ...args)), 18 | whereLike: addToClause(clauses, `where`, (...args) => columnParam(` AND `, { like: true }, ...args)), 19 | orWhere: addToClause(clauses, `where`, (...args) => columnParam(` OR `, { like: false }, ...args)), 20 | orWhereLike: addToClause(clauses, `where`, (...args) => columnParam(` OR `, { like: true }, ...args)), 21 | having: addToClause(clauses, `having`, (...args) => columnParam(` AND `, { like: false }, ...args)), 22 | orHaving: addToClause(clauses, `having`, (...args) => columnParam(` OR `, { like: false }, ...args)), 23 | groupBy: addToClause(clauses, `groupBy`, (...args) => whateverTheyPutIn(`, `, `, `, ...args)), 24 | orderBy: addToClause(clauses, `orderBy`, (...args) => whateverTheyPutIn(`, `, `, `, ...args)), 25 | limit: addToClause(clauses, `limit`, (...args) => whateverTheyPutIn(`, `, `, `, ...args)), 26 | forUpdate: addToClause(clauses, `lock`, () => staticText(`FOR UPDATE`)), 27 | lockInShareMode: addToClause(clauses, `lock`, () => staticText(`LOCK IN SHARE MODE`)), 28 | build: joinedBy => build(clauses, joinedBy), 29 | getClauses: () => copy(clauses), 30 | toString: joinedBy => { 31 | const { sql, values } = build(clauses, joinedBy) 32 | return sqlString.format(sql, values) 33 | }, 34 | union: query => combineClauses(clauses, `UNION`, query.getClauses()), 35 | unionAll: query => combineClauses(clauses, `UNION ALL`, query.getClauses()), 36 | }) 37 | 38 | function addToClause(clauses, key, stringBuilder) { 39 | return (...args) => { 40 | const newClauses = copy(clauses) 41 | newClauses[key].push(stringBuilder(...args)) 42 | return q(newClauses) 43 | } 44 | } 45 | 46 | function copy(o) { 47 | return Object.keys(o).reduce((newObject, key) => { 48 | newObject[key] = o[key].slice() 49 | return newObject 50 | }, {}) 51 | } 52 | 53 | export default q 54 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # sql-concat 2 | 3 | A MySQL query builder. 4 | 5 | ```node 6 | import q from 'sql-concat' 7 | ``` 8 | 9 | The only "breaking" change from 1.x to 2.x is that support for versions of node older than 6 was dropped. 10 | 11 | ## Designed to... 12 | 13 | - Build queries programmatically 14 | - Allow simple combining of query parts and their associated parameters (as opposed to writing a long query string followed by a long array of parameter values) 15 | - Build queries for the [mysqljs/mysql](https://github.com/mysqljs/mysql) library (specifically, by expecting its [rules for query values](https://github.com/mysqljs/mysql#escaping-query-values) instead of MySQL's stored procedure parameters) 16 | 17 | ## Features 18 | 19 | - Easily compose query parts - the query-builder object is immutable, so you can build up a base query and re-use it over and over again with small modifications (for example, with conditional where clauses or joins) 20 | - Not as overblown as [knex](http://knexjs.org/), and allows more freedom in using string literals within query chunks 21 | - Queries should look good when printed out (newlines between clauses, subqueries indented with tabs) 22 | 23 | ## Looks like 24 | 25 | ```node 26 | import q from 'sql-concat' 27 | ``` 28 | 29 | 32 | 33 | ```js 34 | const minNumber = 0 35 | const result = q.select('table1.some_boring_id, table2.something_interesting, mystery_table.surprise', q`LEAST(table1.whatever, ${minNumber}) AS whatever`) 36 | .from('table1') 37 | .join('table2', 'table1.some_boring_id = table2.id') 38 | .leftJoin('mystery_table', 'mystery_table.twister_reality = table2.probably_null_column') 39 | .where('table1.pants', 'fancy') 40 | .where('table1.britches', '>', 99) 41 | .build() 42 | 43 | const expectedQuery = 'SELECT table1.some_boring_id, table2.something_interesting, mystery_table.surprise, LEAST(table1.whatever, ?) AS whatever\n' 44 | + 'FROM table1\n' 45 | + 'JOIN table2 ON table1.some_boring_id = table2.id\n' 46 | + 'LEFT JOIN mystery_table ON mystery_table.twister_reality = table2.probably_null_column\n' 47 | + 'WHERE table1.pants = ? AND table1.britches > ?' 48 | 49 | result.sql // => expectedQuery 50 | 51 | result.values // => [ 0, 'fancy', 99 ] 52 | 53 | ``` 54 | 55 | ## A cooler example 56 | 57 | Showing off the composability/reusability of the query objects, plus some dynamic query building: 58 | 59 | ```js 60 | 61 | // A partial query that we can just leave here to reuse later: 62 | const MOST_RECENT_SALE = q.select('item_sale.item_id, MAX(item_sale.date) AS `date`') 63 | .from('item_sale') 64 | .groupBy('item_sale.item_id') 65 | 66 | function mostRecentSalePricesQuery(taxable, itemType) { 67 | const subquery = MOST_RECENT_SALE.where('taxable', taxable) 68 | 69 | let query = q.select('item.item_id, item.description, item.type, latest_sale.date AS latest_sale_date, latest_sale.price') 70 | .from('item') 71 | .join(subquery, 'latest_sale', 'latest_sale.item_id = item.item_id') 72 | 73 | // Dynamically add new clauses to the query as needed 74 | if (itemType) { 75 | query = query.where('item.item_type', itemType) 76 | } 77 | 78 | return query.build() 79 | } 80 | 81 | // Build those dynamic queries: 82 | 83 | const taxableSpecialQuery = mostRecentSalePricesQuery(true, 'special') 84 | 85 | const expectedTaxableSpecialQuery = ['SELECT item.item_id, item.description, item.type, latest_sale.date AS latest_sale_date, latest_sale.price', 86 | 'FROM item', 87 | 'JOIN (', 88 | '\tSELECT item_sale.item_id, MAX(item_sale.date) AS `date`', 89 | '\tFROM item_sale', 90 | '\tWHERE taxable = ?', 91 | '\tGROUP BY item_sale.item_id', 92 | ') AS latest_sale ON latest_sale.item_id = item.item_id', 93 | 'WHERE item.item_type = ?'].join('\n') 94 | 95 | taxableSpecialQuery.sql // => expectedTaxableSpecialQuery 96 | taxableSpecialQuery.values // => [ true, 'special' ] 97 | 98 | const nonTaxableQuery = mostRecentSalePricesQuery(false) 99 | 100 | const expectedNonTaxableQuery = ['SELECT item.item_id, item.description, item.type, latest_sale.date AS latest_sale_date, latest_sale.price', 101 | 'FROM item', 102 | 'JOIN (', 103 | '\tSELECT item_sale.item_id, MAX(item_sale.date) AS `date`', 104 | '\tFROM item_sale', 105 | '\tWHERE taxable = ?', 106 | '\tGROUP BY item_sale.item_id', 107 | ') AS latest_sale ON latest_sale.item_id = item.item_id'].join('\n') 108 | 109 | nonTaxableQuery.sql // => expectedNonTaxableQuery 110 | nonTaxableQuery.values // => [ false ] 111 | 112 | ``` 113 | 114 | ## API 115 | 116 | Because the [mysql](https://github.com/mysqljs/mysql) package already makes inserting so easy, this module is focused on `SELECT` queries. I've implemented new clauses as I've needed them, and it's pretty well fleshed out at the moment. 117 | 118 | If you need a clause added that is not implemented yet, feel free to open a pull request. If you're not sure what the API should look like, open an issue and we can talk it through. 119 | 120 | ### Clauses 121 | 122 | Every clause method returns a new immutable `q` query object. 123 | 124 | - `q.select(expression1, expression2, etc)` 125 | - `q.from(tablename | subquery, alias)` 126 | - `q.join(tablename | subquery, [alias], on_expression)` 127 | - `q.leftJoin(tablename | subquery, [alias], on_expression)` 128 | - `q.where(expression, [[comparator], value])` 129 | - `q.orWhere(expression, [[comparator], value])` 130 | - `q.whereLike(expression, value)` 131 | - `q.orWhereLike(expression, value)` 132 | - `q.having(expression, [[comparator], value])` 133 | - `q.orHaving(expression, [[comparator], value])` 134 | - `q.groupBy(expression1, expression2, etc)` 135 | - `q.orderBy(expression1, expression2, etc)` 136 | - `q.limit(offset, [row_count])` 137 | - `q.forUpdate()` 138 | - `q.lockInShareMode()` 139 | 140 | `expression` strings are inserted without being parameterized, but you can also pass in [tagged template strings](#tagged-template-strings) to do anything special. 141 | 142 | All `value`s will be automatically escaped. If a `value` is `NULL` it will be automatically compared with `IS`, and if it's an array it will be automatically compared with `IN()`. Otherwise, it will be compared with `=`. 143 | 144 | ```js 145 | const whereInResult = q.select('fancy') 146 | .from('table') 147 | .where('table.pants', [ 'fancy', 'boring' ]) 148 | .build() 149 | 150 | const whereInQuery = 'SELECT fancy\n' 151 | + 'FROM table\n' 152 | + 'WHERE table.pants IN(?)' 153 | 154 | whereInResult.sql // => whereInQuery 155 | 156 | whereInResult.values // => [ [ 'fancy', 'boring' ] ] 157 | ``` 158 | 159 | Put another way, calling `q.select('column1, column2')` is just as acceptable as calling `q.select('column1', 'column2')` and you should use whichever you prefer. 160 | 161 | #### Clause order 162 | 163 | Clauses are returned in the [correct order](https://github.com/TehShrike/sql-concat/blob/master/constants.js#L19-L35) no matter what order you call the methods in. 164 | 165 | ```js 166 | q.from('table').select('column').toString() // `SELECT column\nFROM table`` 167 | ``` 168 | 169 | However, if you call a method multiple times, the values are concatenated in the same order you called them. 170 | 171 | ```js 172 | q.from('nifty') 173 | .select('snazzy') 174 | .select('spiffy') 175 | .select('sizzle') 176 | .toString() // `SELECT snazzy, spiffy, sizzle\nFROM nifty`` 177 | ``` 178 | 179 | ### `q.union(query)` and `q.unionAll(query)` 180 | 181 | The `union` and `unionAll` methods return a query object that only contains `union` and `unionAll` methods – once you start unioning queries together, you can keep unioning more queries, but you can't add any other clauses to them. 182 | 183 | ### `q.build()` 184 | 185 | Returns an object with these properties: 186 | 187 | - `sql`: a string containing the query, with question marks `?` where escaped values should be inserted. 188 | - `values`: an array of values to be used with the query. 189 | 190 | You can pass this object directly to the `query` method of the [`mysql`](https://github.com/mysqljs/mysql#performing-queries) library: 191 | 192 | ```node 193 | mysql.query( 194 | q.select('Cool!').build(), 195 | (err, result) => { 196 | console.log(result) 197 | } 198 | ) 199 | ``` 200 | 201 | ```js 202 | q.select('column') 203 | .where('id', 3) 204 | .build() // { sql: `SELECT column\nWHERE id = ?`, values: [ 3 ]} 205 | ``` 206 | 207 | ### `q.toString()` 208 | 209 | Returns a string with values escaped by [`sqlstring`](https://github.com/mysqljs/sqlstring#formatting-queries). 210 | 211 | ```js 212 | q.select('fancy') 213 | .from('table') 214 | .where('table.pants', [ 'what\'s up', 'boring' ]) 215 | .toString() // => `SELECT fancy\nFROM table\nWHERE table.pants IN('what\\'s up', 'boring')` 216 | ``` 217 | 218 | ### Tagged template strings 219 | 220 | sql-concat is also a template tag: 221 | 222 | ```js 223 | const rainfall = 3 224 | const templateTagResult = q`SELECT galoshes FROM puddle WHERE rain > ${ rainfall }` 225 | 226 | templateTagResult.sql // => `SELECT galoshes FROM puddle WHERE rain > ?` 227 | templateTagResult.values // => [ 3 ] 228 | ``` 229 | 230 | You can pass these results into any method as a value. This allows you to properly parameterize function calls: 231 | 232 | ```js 233 | const shoeSize = 9 234 | const functionCallResult = q.select('rubbers') 235 | .from('puddle') 236 | .where('rain', '>', 4) 237 | .where('size', q`LPAD(${ shoeSize }, 2, '0')`) 238 | .build() 239 | 240 | const functionCallQuery = `SELECT rubbers\n` 241 | + `FROM puddle\n` 242 | + `WHERE rain > ? AND size = LPAD(?, 2, '0')` 243 | 244 | functionCallResult.sql // => functionCallQuery 245 | 246 | functionCallResult.values // => [ 4, 9 ] 247 | ``` 248 | 249 | ## Long-shot feature 250 | 251 | Some syntax for generating nested clauses conditionally would be nice, so you could easily generate something like this dynamically: 252 | 253 | ```sql 254 | WHERE important = ? AND (your_column = ? OR your_column = ? OR something_else LIKE ?) 255 | ``` 256 | 257 | Maybe something like: 258 | 259 | ```node 260 | const whereCondition = q.parenthetical('OR') 261 | .equal('your_column', true) 262 | .equal('your_column', randomVariable) 263 | .like('something_else', anotherVariable) 264 | 265 | const query = q.select('everything') 266 | .from('table') 267 | .where('important', true) 268 | .where(whereCondition) 269 | ``` 270 | 271 | You can discuss this feature in [Issue 3](https://github.com/TehShrike/sql-concat/issues/3) if you're interested. 272 | 273 | ## Running the tests 274 | 275 | 1. clone the repo 276 | 2. navigate to the cloned directory 277 | 3. `npm install` 278 | 4. `npm test` 279 | 280 | ## License 281 | 282 | [WTFPL](http://wtfpl2.com) 283 | -------------------------------------------------------------------------------- /tagged-template.js: -------------------------------------------------------------------------------- 1 | export default (queryParts, ...values) => { 2 | return queryParts.reduce( 3 | (queryObject, queryPart, i) => { 4 | queryObject.sql += queryPart 5 | 6 | if (i < values.length) { 7 | const nextValue = values[i] 8 | 9 | if (nextValue 10 | && typeof nextValue === `object` 11 | && typeof nextValue.sql === `string` 12 | && Array.isArray(nextValue.values) 13 | ) { 14 | queryObject.sql += `(${ nextValue.sql })` 15 | queryObject.values.push(...nextValue.values) 16 | } else { 17 | queryObject.sql += `?` 18 | queryObject.values.push(nextValue) 19 | } 20 | } 21 | 22 | return queryObject 23 | }, 24 | { sql: ``, values: [] }, 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test' 2 | import assert from 'node:assert' 3 | import q from './index.js' 4 | 5 | test('first query', () => { 6 | q.select('WRONG') 7 | 8 | const result = q.select('butt') 9 | .from('pants') 10 | .build() 11 | 12 | assert.equal(result.sql, [ 13 | 'SELECT butt', 14 | 'FROM pants', 15 | ].join('\n')) 16 | }) 17 | 18 | test('select/from/where with params', () => { 19 | const result = q.select('butt') 20 | .from('pants') 21 | .where('touching', true) 22 | .where('hugging', true) 23 | .where('feeling', false) 24 | .build() 25 | 26 | assert.equal(result.sql, [ 27 | 'SELECT butt', 28 | 'FROM pants', 29 | 'WHERE touching = ? AND hugging = ? AND feeling = ?', 30 | ].join('\n')) 31 | assert.deepEqual(result.values, [ true, true, false ]) 32 | 33 | assert.equal(result.sql, [ 34 | 'SELECT butt', 35 | 'FROM pants', 36 | 'WHERE touching = ? AND hugging = ? AND feeling = ?', 37 | ].join('\n')) 38 | assert.deepEqual(result.values, [ true, true, false ]) 39 | }) 40 | 41 | test('some joins', () => { 42 | const result = q.select('wat') 43 | .from('meh') 44 | .join('no_in_fact_u', 'no_in_fact_u.meh_id = meh.id') 45 | .leftJoin('who', 'who.no_in_fact_u_id = no_in_fact_u.id') 46 | .build() 47 | 48 | assert.equal(result.sql, [ 49 | 'SELECT wat', 50 | 'FROM meh', 51 | 'JOIN no_in_fact_u ON no_in_fact_u.meh_id = meh.id', 52 | 'LEFT JOIN who ON who.no_in_fact_u_id = no_in_fact_u.id', 53 | ].join('\n')) 54 | }) 55 | 56 | test('WHERE a OR b', () => { 57 | const result = q.select('wat') 58 | .from('whatever') 59 | .where('a', 1) 60 | .orWhere('b', 2) 61 | .orWhere('c', [ 3, 4 ]) 62 | .build() 63 | 64 | assert.equal(result.sql, [ 65 | 'SELECT wat', 66 | 'FROM whatever', 67 | 'WHERE a = ? OR b = ? OR c IN(?)', 68 | ].join('\n')) 69 | 70 | assert.deepEqual(result.values, [ 1, 2, [ 3, 4 ] ]) 71 | }) 72 | 73 | test('multiple select values', () => { 74 | assert.equal(q.select('a', 'b', 'c').from('blah').build().sql, 'SELECT a, b, c\nFROM blah') 75 | }) 76 | 77 | test('from clause with alias', () => { 78 | assert.equal( 79 | q.select('whatever').from('blah', 'a_longer_name_for_no_reason').build().sql, 80 | 'SELECT whatever\nFROM blah AS a_longer_name_for_no_reason', 81 | ) 82 | }) 83 | 84 | test('subqueries', () => { 85 | const subquery = q.select('thing').from('place').where('column', 'value') 86 | 87 | let result = q.select('a') 88 | .from(subquery, 'subquery_alias') 89 | .having('subquery_alias.thing', 2) 90 | .where('subquery_alias.thing', 1) 91 | .build() 92 | 93 | assert.equal(result.sql, [ 94 | 'SELECT a', 95 | 'FROM (', 96 | '\tSELECT thing', 97 | '\tFROM place', 98 | '\tWHERE column = ?', 99 | ') AS subquery_alias', 100 | 'WHERE subquery_alias.thing = ?', 101 | 'HAVING subquery_alias.thing = ?', 102 | ].join('\n')) 103 | 104 | assert.deepEqual(result.values, [ 'value', 1, 2 ]) 105 | 106 | result = q.select('dumb_column') 107 | .from('dumb_table', 'dumb_alias') 108 | .leftJoin(subquery, 'dumb_subquery', 'dumb_subquery.column = dumb_alias.column') 109 | .build() 110 | 111 | assert.equal(result.sql, [ 112 | 'SELECT dumb_column', 113 | 'FROM dumb_table AS dumb_alias', 114 | 'LEFT JOIN (', 115 | '\tSELECT thing', 116 | '\tFROM place', 117 | '\tWHERE column = ?', 118 | ') AS dumb_subquery ON dumb_subquery.column = dumb_alias.column', 119 | ].join('\n')) 120 | 121 | assert.deepEqual(result.values, [ 'value' ]) 122 | }) 123 | 124 | test('group by', () => { 125 | assert.equal( 126 | q.groupBy('a', 'b').from('wat').select('lol').where('butts', 13).build().sql, 127 | [ 'SELECT lol', 'FROM wat', 'WHERE butts = ?', 'GROUP BY a, b' ].join('\n'), 128 | ) 129 | }) 130 | 131 | test('where like', () => { 132 | const result = q.select('lol').from('butt').whereLike('column', 'starts with%').build() 133 | assert.equal(result.sql, [ 'SELECT lol', 'FROM butt', 'WHERE column LIKE ?' ].join('\n')) 134 | assert.deepEqual(result.values, [ 'starts with%' ]) 135 | 136 | const result2 = q.select('lol').from('butt').whereLike('column', 'starts with%').orWhereLike('other_column', '%ends with').build() 137 | assert.equal(result2.sql, [ 'SELECT lol', 'FROM butt', 'WHERE column LIKE ? OR other_column LIKE ?' ].join('\n')) 138 | assert.deepEqual(result2.values, [ 'starts with%', '%ends with' ]) 139 | }) 140 | 141 | test('order by', () => { 142 | const result = q.select('lol').from('butt').orderBy('column1', 'column2').build() 143 | assert.equal(result.sql, [ 'SELECT lol', 'FROM butt', 'ORDER BY column1, column2' ].join('\n')) 144 | assert.deepEqual(result.values, [ ]) 145 | }) 146 | 147 | test('limit', () => { 148 | const result = q.select('lol').from('butt').limit(10).build() 149 | assert.equal(result.sql, [ 'SELECT lol', 'FROM butt', 'LIMIT 10' ].join('\n')) 150 | assert.deepEqual(result.values, [ ]) 151 | }) 152 | 153 | test('limit+row count', () => { 154 | const result = q.select('lol').from('butt').limit(10, 15).build() 155 | assert.equal(result.sql, [ 'SELECT lol', 'FROM butt', 'LIMIT 10, 15' ].join('\n')) 156 | assert.deepEqual(result.values, [ ]) 157 | }) 158 | 159 | test('null in a where clause', () => { 160 | const result = q.select('whatever').from('meh').where('something', null).where('thingy', 'whatevs').build() 161 | 162 | assert.equal(result.sql, [ 'SELECT whatever', 'FROM meh', 'WHERE something IS ? AND thingy = ?' ].join('\n')) 163 | assert.deepEqual(result.values, [ null, 'whatevs' ]) 164 | }) 165 | 166 | test('null in a where clause with comparator', () => { 167 | const result = q.select('whatever').from('meh').where('something', 'IS NOT', null).where('thingy', 'whatevs').build() 168 | 169 | assert.equal(result.sql, [ 'SELECT whatever', 'FROM meh', 'WHERE something IS NOT ? AND thingy = ?' ].join('\n')) 170 | assert.deepEqual(result.values, [ null, 'whatevs' ]) 171 | }) 172 | 173 | test('where in(array)', () => { 174 | const result = q.select('whatever').from('meh').where('something', [ 1, 2, 3 ]).build() 175 | 176 | assert.equal(result.sql, [ 'SELECT whatever', 'FROM meh', 'WHERE something IN(?)' ].join('\n')) 177 | assert.deepEqual(result.values, [ [ 1, 2, 3 ] ]) 178 | }) 179 | 180 | test('lock in shared mode', () => { 181 | const result = q.select('whatever').from('meh').where('something', 22).lockInShareMode().build() 182 | 183 | assert.equal(result.sql, [ 'SELECT whatever', 'FROM meh', 'WHERE something = ?', 'LOCK IN SHARE MODE' ].join('\n')) 184 | assert.deepEqual(result.values, [ 22 ]) 185 | }) 186 | 187 | test('select for update', () => { 188 | const result = q.select('whatever').from('meh').where('something', 22).forUpdate().build() 189 | 190 | assert.equal(result.sql, [ 'SELECT whatever', 'FROM meh', 'WHERE something = ?', 'FOR UPDATE' ].join('\n')) 191 | assert.deepEqual(result.values, [ 22 ]) 192 | }) 193 | 194 | test('WHERE gt/lt/gte/lte AND', () => { 195 | const result = q.select('wat') 196 | .from('whatever') 197 | .where('a', '>', 1) 198 | .where('b', '>=', 2) 199 | .where('c', '<', 3) 200 | .where('d', '<=', 4) 201 | .build() 202 | 203 | assert.equal(result.sql, [ 204 | 'SELECT wat', 205 | 'FROM whatever', 206 | 'WHERE a > ? AND b >= ? AND c < ? AND d <= ?', 207 | ].join('\n')) 208 | 209 | assert.deepEqual(result.values, [ 1, 2, 3, 4 ]) 210 | }) 211 | 212 | test('WHERE gt/lt/gte/lte OR', () => { 213 | const result = q.select('wat') 214 | .from('whatever') 215 | .orWhere('a', '>', 1) 216 | .orWhere('b', '>=', 2) 217 | .orWhere('c', '<', 3) 218 | .orWhere('d', '<=', 4) 219 | .build() 220 | 221 | assert.equal(result.sql, [ 222 | 'SELECT wat', 223 | 'FROM whatever', 224 | 'WHERE a > ? OR b >= ? OR c < ? OR d <= ?', 225 | ].join('\n')) 226 | 227 | assert.deepEqual(result.values, [ 1, 2, 3, 4 ]) 228 | }) 229 | 230 | test('HAVING gt/lt/gte/lte AND', () => { 231 | const result = q.select('wat') 232 | .from('whatever') 233 | .having('a', '>', 1) 234 | .having('b', '>=', 2) 235 | .having('c', '<', 3) 236 | .having('d', '<=', 4) 237 | .build() 238 | 239 | assert.equal(result.sql, [ 240 | 'SELECT wat', 241 | 'FROM whatever', 242 | 'HAVING a > ? AND b >= ? AND c < ? AND d <= ?', 243 | ].join('\n')) 244 | 245 | assert.deepEqual(result.values, [ 1, 2, 3, 4 ]) 246 | }) 247 | 248 | test('HAVING gt/lt/gte/lte OR', () => { 249 | const result = q.select('wat') 250 | .from('whatever') 251 | .orHaving('a', '>', 1) 252 | .orHaving('b', '>=', 2) 253 | .orHaving('c', '<', 3) 254 | .orHaving('d', '<=', 4) 255 | .build() 256 | 257 | assert.equal(result.sql, [ 258 | 'SELECT wat', 259 | 'FROM whatever', 260 | 'HAVING a > ? OR b >= ? OR c < ? OR d <= ?', 261 | ].join('\n')) 262 | 263 | assert.deepEqual(result.values, [ 1, 2, 3, 4 ]) 264 | }) 265 | 266 | test('Tagged template string', () => { 267 | const result = q`SELECT wat FROM a WHERE foo = ${ 4 } AND bar IN(${ [ 1, 2 ] })` 268 | 269 | assert.equal(result.sql, 'SELECT wat FROM a WHERE foo = ? AND bar IN(?)') 270 | assert.deepEqual(result.values, [ 4, [ 1, 2 ] ]) 271 | }) 272 | 273 | test('Query object in a tagged template string', () => { 274 | const subquery = q.select('sub').from('other').where('three', 3).build() 275 | const result = q`SELECT wat FROM a WHERE foo = ${ subquery } AND bar IN(${ [ 1, 2 ] })` 276 | 277 | assert.equal(result.sql, 'SELECT wat FROM a WHERE foo = (SELECT sub\nFROM other\nWHERE three = ?) AND bar IN(?)') 278 | assert.deepEqual(result.values, [ 3, [ 1, 2 ] ]) 279 | }) 280 | 281 | test('Passing a str/params object as a value', () => { 282 | const { sql, values } = q.select('howdy') 283 | .from('meh') 284 | .where('a', 1) 285 | .where('tag', { 286 | sql: 'FANCY(?, ?)', 287 | values: [ 'pants', 'butts' ], 288 | }) 289 | .where('b', 2) 290 | .build() 291 | 292 | assert.equal(sql, 'SELECT howdy\nFROM meh\nWHERE a = ? AND tag = FANCY(?, ?) AND b = ?') 293 | assert.deepEqual(values, [ 1, 'pants', 'butts', 2 ]) 294 | }) 295 | 296 | test('Passing a str/params object in an "on" clause', () => { 297 | const { sql, values } = q.select('howdy') 298 | .from('meh') 299 | .where('a', 1) 300 | .join('tag', { 301 | sql: 'something_cool = FANCY(?, ?)', 302 | values: [ 'pants', 'butts' ], 303 | }) 304 | .where('b', 2) 305 | .build() 306 | 307 | assert.equal(sql, 'SELECT howdy\nFROM meh\nJOIN tag ON something_cool = FANCY(?, ?)\nWHERE a = ? AND b = ?') 308 | assert.deepEqual(values, [ 'pants', 'butts', 1, 2 ]) 309 | }) 310 | 311 | test('Integration: passing a tagged template string result as an argument', () => { 312 | const { sql, values } = q.where('tag', q`FANCY(${ 'pants' }, ${ 'butts' })`).build() 313 | 314 | assert.equal(sql, 'WHERE tag = FANCY(?, ?)') 315 | assert.deepEqual(values, [ 'pants', 'butts' ]) 316 | }) 317 | 318 | test('Passing str/params into every clause', () => { 319 | const assertLegit = (query, expectedStr) => { 320 | const { sql, values } = query.build() 321 | 322 | assert.equal(sql, expectedStr, expectedStr) 323 | assert.deepEqual(values, [ 1, 2 ]) 324 | } 325 | 326 | assertLegit( 327 | q.select(q`FOO(${ 1 })`, q`BAR(${ 2 })`), 328 | 'SELECT FOO(?), BAR(?)', 329 | ) 330 | assertLegit( 331 | q.join('table', q`FOO(${ 1 }) = BAR(${ 2 })`), 332 | 'JOIN table ON FOO(?) = BAR(?)', 333 | ) 334 | assertLegit( 335 | q.leftJoin('table', q`FOO(${ 1 }) = BAR(${ 2 })`), 336 | 'LEFT JOIN table ON FOO(?) = BAR(?)', 337 | ) 338 | assertLegit( 339 | q.where(q`FOO(${ 1 })`, q`BAR(${ 2 })`), 340 | 'WHERE FOO(?) = BAR(?)', 341 | ) 342 | assertLegit( 343 | q.whereLike(q`FOO(${ 1 })`, q`BAR(${ 2 })`), 344 | 'WHERE FOO(?) LIKE BAR(?)', 345 | ) 346 | assertLegit( 347 | q.having(q`FOO(${ 1 })`, q`BAR(${ 2 })`), 348 | 'HAVING FOO(?) = BAR(?)', 349 | ) 350 | assertLegit( 351 | q.groupBy(q`FOO(${ 1 })`, q`BAR(${ 2 })`), 352 | 'GROUP BY FOO(?), BAR(?)', 353 | ) 354 | assertLegit( 355 | q.orderBy(q`FOO(${ 1 })`, q`BAR(${ 2 })`), 356 | 'ORDER BY FOO(?), BAR(?)', 357 | ) 358 | }) 359 | 360 | test('sql/values are legit with mulitple clauses', () => { 361 | const result = q.select('table1.some_boring_id, table2.something_interesting, mystery_table.surprise', q`LEAST(table1.whatever, ?) AS whatever`) 362 | .from('table1') 363 | .join('table2', 'table1.some_boring_id = table2.id') 364 | .leftJoin('mystery_table', 'mystery_table.twister_reality = table2.probably_null_column') 365 | .where('table1.pants', 'fancy') 366 | .where('table1.britches', '>', 99) 367 | .build() 368 | 369 | assert.equal(result.sql, [ 370 | 'SELECT table1.some_boring_id, table2.something_interesting, mystery_table.surprise, LEAST(table1.whatever, ?) AS whatever', 371 | 'FROM table1', 372 | 'JOIN table2 ON table1.some_boring_id = table2.id', 373 | 'LEFT JOIN mystery_table ON mystery_table.twister_reality = table2.probably_null_column', 374 | 'WHERE table1.pants = ? AND table1.britches > ?', 375 | ].join('\n')) 376 | 377 | assert.deepEqual(result.values, [ 'fancy', 99 ]) 378 | }) 379 | 380 | test('custom comparator', () => { 381 | const input = 'whatever' 382 | 383 | const result = q.select('myColumn') 384 | .from('table1') 385 | .where('MATCH(myColumn)', ' ', q`AGAINST(${ input })`) 386 | .build() 387 | 388 | assert.equal(result.sql, [ 389 | 'SELECT myColumn', 390 | 'FROM table1', 391 | 'WHERE MATCH(myColumn) AGAINST(?)', 392 | ].join('\n')) 393 | 394 | assert.deepEqual(result.values, [ 'whatever' ]) 395 | }) 396 | 397 | test('no where value', () => { 398 | const input = 'whatever' 399 | 400 | const result = q.select('myColumn') 401 | .from('table1') 402 | .where(q`MATCH(myColumn) AGAINST(${ input })`) 403 | .where('someOtherColumn = somethingUnrelated') 404 | .build() 405 | 406 | assert.equal(result.sql, [ 407 | 'SELECT myColumn', 408 | 'FROM table1', 409 | 'WHERE MATCH(myColumn) AGAINST(?) AND someOtherColumn = somethingUnrelated', 410 | ].join('\n')) 411 | 412 | assert.deepEqual(result.values, [ 'whatever' ]) 413 | }) 414 | 415 | test('Throws an error if you call whereLike without a value', () => { 416 | assert.throws(() => { 417 | q.whereLike('nuthin') 418 | }, /like/i) 419 | }) 420 | 421 | test('toString', () => { 422 | const result = q.select('myColumn') 423 | .from('table1') 424 | .where('foo', '!=', 'baz') 425 | .toString() 426 | 427 | assert.equal(result, 'SELECT myColumn\nFROM table1\nWHERE foo != \'baz\'') 428 | }) 429 | 430 | test('toString custom separator', () => { 431 | const result = q.select('myColumn') 432 | .from('table1') 433 | .where('foo', '!=', 'baz') 434 | .toString(' ') 435 | 436 | assert.equal(result, 'SELECT myColumn FROM table1 WHERE foo != \'baz\'') 437 | }) 438 | 439 | test('union + unionAll', () => { 440 | const query = q.select('myColumn') 441 | .from('table1') 442 | .where('foo', '!=', 'baz') 443 | .union(q.select('wat').from('bar').where('biz', true)) 444 | .unionAll(q.select('huh').from('baz').having('wat', 'whatever')) 445 | 446 | const buildResult = query.build() 447 | 448 | assert.equal(buildResult.sql, [ 449 | 'SELECT myColumn', 450 | 'FROM table1', 451 | 'WHERE foo != ?', 452 | 'UNION', 453 | 'SELECT wat', 454 | 'FROM bar', 455 | 'WHERE biz = ?', 456 | 'UNION ALL', 457 | 'SELECT huh', 458 | 'FROM baz', 459 | 'HAVING wat = ?' 460 | ].join('\n')) 461 | 462 | assert.deepEqual(buildResult.values, [ 'baz', true, 'whatever' ]) 463 | 464 | assert.equal(query.toString(), [ 465 | 'SELECT myColumn', 466 | 'FROM table1', 467 | 'WHERE foo != \'baz\'', 468 | 'UNION', 469 | 'SELECT wat', 470 | 'FROM bar', 471 | 'WHERE biz = true', 472 | 'UNION ALL', 473 | 'SELECT huh', 474 | 'FROM baz', 475 | 'HAVING wat = \'whatever\'' 476 | ].join('\n')) 477 | 478 | assert.equal(query.toString('\n\n'), [ 479 | 'SELECT myColumn', 480 | '', 481 | 'FROM table1', 482 | '', 483 | 'WHERE foo != \'baz\'', 484 | '', 485 | 'UNION', 486 | '', 487 | 'SELECT wat', 488 | '', 489 | 'FROM bar', 490 | '', 491 | 'WHERE biz = true', 492 | '', 493 | 'UNION ALL', 494 | '', 495 | 'SELECT huh', 496 | '', 497 | 'FROM baz', 498 | '', 499 | 'HAVING wat = \'whatever\'' 500 | ].join('\n')) 501 | }) 502 | 503 | test('union query as subquery', () => { 504 | const subquery = q.select('1 AS wat') 505 | .from('table1') 506 | .where('foo', false) 507 | .unionAll(q.select('2').from('bar').where('biz', true)) 508 | 509 | const buildResult = q.select('wat').from(subquery, 'meh').build() 510 | 511 | assert.equal(buildResult.sql, [ 512 | 'SELECT wat', 513 | 'FROM (', 514 | '\tSELECT 1 AS wat', 515 | '\tFROM table1', 516 | '\tWHERE foo = ?', 517 | '\tUNION ALL', 518 | '\tSELECT 2', 519 | '\tFROM bar', 520 | '\tWHERE biz = ?', 521 | ') AS meh' 522 | ].join('\n')) 523 | 524 | assert.deepEqual(buildResult.values, [ false, true ]) 525 | }) 526 | --------------------------------------------------------------------------------