UNPKG

criterion

Version:

criterion allows you to work with (build, combine, reuse, ...) SQL-where-conditions ('x = 5 AND y IS NOT NULL'...) as data (goodbye string-concatenation) and compile them to SQL: it has a succinct mongodb-like query-language, a simple and elegant function

1,036 lines (769 loc) 23.2 kB
# criterion [![NPM version](https://badge.fury.io/js/criterion.svg)](http://badge.fury.io/js/criterion) [![Build Status](https://travis-ci.org/snd/criterion.svg?branch=master)](https://travis-ci.org/snd/criterion/branches) [![Dependencies](https://david-dm.org/snd/criterion.svg)](https://david-dm.org/snd/criterion) #### ABOUT THIS VERSION ! **this is the readme for criterion version `0.4.0` - a work in progress. release candidate `0.4.0-rc.1` is published to npm: the api is stable, the implementation complete, the tests numerous and passing, the documentation (this readme) still needs some love. to see the readme for `0.3.3` [click here](https://github.com/snd/criterion/tree/0808d66443fd72aaece2f3e5134f49d3af0bf72e) ! to see what has changed in `0.4.0` [click here](#changelog).** > criterion allows you to work with (build, combine, reuse, ...) SQL-where-conditions > (`x = 5 AND y IS NOT NULL`...) as data > (goodbye string-concatenation) > and compile them to SQL: > it has a *succinct* [mongodb-like query-language](#condition-objects), > a *simple* and *elegant* functional API, > is [reasily extended](#the-sql-fragment-interface) > just by implementing 2 functions > and *gets out of your way*: you can always drop down to [raw-sql](#raw-sql). - [background](#background) - [introduction](#get-started) - [install (`npm install criterion`)](#install) - [require (`var criterion = require('criterion');`)](#require) - [condition-objects (`var c = criterion({x: 7, y: {$lt: 5}});`)](#condition-objects) - [raw-sql (`var c = criterion('LOG(y, ?)', 5);`)](#raw-sql) - [the sql-fragment interface](#the-sql-fragment-interface) - [for users of mesa and mohair](#for-users-of-mesa-and-mohair) - [reference by example](#reference-by-example) - [how to read this reference](#how-to-read-this-reference) - [comparisons](#comparisons) - [equal (`{x: 1}` -> `x = ?`)](#equal) - [not equal (`{x: {$ne: 1}}` -> `x != ?`)](#not-equal) - [lower than (`{x: {$lt: 1}}` -> `x < ?`, `{x: {$lte: 1}}` -> `x <= ?`)](#lower-than) - [greater than (`$gt` -> `>`, `$gte` -> `>=`](#greater-than) - [null (`{$null: true}` -> `IS NULL`)](#null) - [not null (`{$null: false}` -> `IS NOT NULL`)](#not-null) - [boolean operations](#boolean-operations) - [and (`{x: 1, y: {$lt: 2}}` -> `(x = ?) AND (y < ?)`)](#and) - [or (`{$or: {x: 1, y: {$lt: 2}}}` -> `(x = ?) OR (y < ?)`)](#or) - [not (`{$not: {x: 1}}` -> `NOT (x = ?)`)](#not) - [lists of scalar expressions](#lists-of-scalar-expressions) - [in list (`{x: [1, 2, 3]}` -> `x IN (?, ?, ?)`)](#in-list) - [not in list (`{x: {$nin: [1, 2, 3]}}` -> `x NOT IN (?, ?, ?)`)](#not-in-list) - [subqueries](#subqueries) - [in subquery (`{x: subquery}` -> `x IN subquery`)](#in-subquery) - [not in subquery (`{x: {$nin: subquery}}` -> `x NOT IN subquery`)](#not-in-subquery) - [exists - whether subquery returns any rows](#exists-whether-subquery-returns-any-rows) - [row-wise comparison with subqueries](#row-wise-comparison-with-subqueries) - [advanced topics](#advanced-topics) - [combining criteria with `.and()`](#combining-criteria-with-and) - [combining criteria with `.or()`](#combining-criteria-with-or) - [negating criteria with `.not()`](#negating-criteria-with-not) - [escaping column names with `.sql(escape)`](#escaping-column-names) - [param array explosion](#param-array-explosion) - [changelog](#changelog) - [license: MIT](#license-mit) ## background criterion is part of three libraries for nodejs that make SQL with nodejs: - [simple](http://www.infoq.com/presentations/Simple-Made-Easy), - DRY - extendable - well documented > make SQL with Nodejs > succinct, > DRY, > functional > data-driven > composable > flexible - free - close to the metal (sql, database, database-driver) - and FUN ! - succinct - FUN ! used in production short code high quality - few lines of high quality code well tested philosophy #### [CRITERION](http://github.com/snd/criterion) <- you are looking at it parses SQL-where-conditions from a mongodb-like query-language into objects which it can compile to SQL #### [MOHAIR](http://github.com/snd/mohair) a powerful SQL-query-builder with a fluent, functional, side-effect-free API. uses [criterion](http://github.com/snd/criterion) to build and combine its SQL-where-clauses. #### [MESA](http://github.com/snd/mesa) helps as much as possible with the construction, composition and execution of SQL-queries while not restricting full access to the database in any way. is not an ORM ! uses [mohair](http://github.com/snd/mohair) to build its SQL-queries. uses [criterion](http://github.com/snd/criterion) (through [mohair](http://github.com/snd/mohair)) to build and combine its SQL-where-clauses. ## introduction ### install ``` npm install criterion ``` ### require ``` js var criterion = require('criterion'); ``` criterion exports a single function `criterion()` which can be called either with a [condition-object](#condition-objects) or with [raw-sql](#raw-sql): ### condition-objects a *condition-object* describes an SQL-where-condition as data using a *query-language* that is inspired by the [mongodb query language](http://docs.mongodb.org/manual/tutorial/query-documents/). let's make a *condition-object*: ``` js var condition = { a: 7, b: {$lt: 5}, $not: { $or: { c: [1, 2, 3], d: {$null: false} } } }; ``` you see that the *query-language* uses special *modifier-keys* to model comparisons (`$lt`), boolean operations (`$not`, `$or`) and [much much more](#reference-by-example) (not unlike the [mongodb query language](http://docs.mongodb.org/manual/tutorial/query-documents/)). now we can make a *criterion* from the *condition-object*: ``` js var c = criterion(condition); ``` we can then compile the *criterion* to SQL: ``` js c.sql(); // -> // '(a = ?) // AND // (b < ?) // AND // NOT ( // (c IN (?, ?, ?)) // OR // (d IS NOT NULL) // )' ``` we can also get the bound parameters of the *criterion*: ```js c.params(); // -> [7, 5, 1, 2, 3] ``` [see the reference below for examples on how to model almost every SQL-where-condition using *condition-objects* !](#reference-by-example) ### raw-sql *raw-sql* is a string of SQL followed by some optional parameter bindings. use *raw-sql* for those rare cases where condition-objects and you have to fall back to using strings. note that [*condition-objects* and *raw-sql* can be mixed](#mixing-condition-objects-and-sql-fragments) to keep *raw-sql* to a minimum. ``` js var c = criterion('LOG(y, ?)', 4); ``` a criterion made from *raw-sql* behaves exactly like one made from a *condition-object*: you can get the sql: ```js c.sql(); // -> 'LOG(y, ?)' ``` ...and the bound parameters: ```js c.params(); // -> [4] ``` in fact both the criterion made from *raw-sql* and one made from a *condition-object* are *sql-fragments*: ### the sql-fragment interface in [mesa](http://github.com/snd/mesa), [mohair](http://github.com/snd/mohair) and [criterion](http://github.com/snd/criterion) every object that has a `.sql()` method and a `.params()` method is an implements the [sql-fragment](#the-sql-fragment-interface) interface and is an [sql-fragment](#the-sql-fragment-interface). more precisely: the `.sql()` method should return a string of valid SQL. the `.sql()` method might be called with a single argument: a function `escape()` which takes a string and returns a string. when the `escape()` function is present then the `.sql()` method should call it to transform table- and column-names in the returned SQL. if `.sql()` constructs the SQL on-the-fly that should be straightforward. in the case of *raw-sql* escaping is complex, ambigous and not worth the effort. the `.params()` method takes no arguments and must return an array. #### things that are sql-fragments (already) - EVERY *criterion*: - `criterion({x: 7})` - `criterion('LOG(y, ?)', 4)` - EVERY [mesa](http://github.com/snd/mesa)-query or [mohair](http://github.com/snd/mohair)-query: - `mesa.table('post')` - `mesa.table('post').where({id: 7})` - `mohair.table('host')` - `mohair.table('host').select('name').where({created_at: {$lt: new Date()}})` - EVERY return value of [mesa's](http://github.com/snd/mesa) or [mohair's](http://github.com/snd/mohair) `.raw()` method: - `mesa.raw('LOG(y, ?)', 4)` - `mohair.raw('LOG(y, ?)', 4)` - EVERY object you create that implements the [sql-fragment interface](#sql-fragment-interface) #### mixing condition-objects and sql-fragments now to the FUN part ! **ANY** *sql-fragment* can be used in place of **ANY** value in a [condition-object](#condition-objects): ``` js var c = criterion({ x: criterion('crypt(?, gen_salt(?, ?))', 'password', 'bf', 4) }); c.sql(); // -> 'x = (crypt(?, gen_salt(?, ?)))' c.params(); // -> ['password', 'bf', 4] ``` or ``` js var c = criterion({x: {$ne: criterion('LOG(y, ?)', 4)}}); c.sql(); // -> 'x != LOG(y, ?)' c.params(); // -> [4] ``` you see how this allows mixing *condition-objects* with arbitrary sql: use it to keep [raw-sql](#raw-sql) to a minimum ! *sql-fragments* can be mixed with *condition-objects* inside boolean operations: ``` js var c = criterion({ $or: [ criterion('x BETWEEN ? AND ?', 5, 10), {y: {$ne: 12}} [ criterion('x != LOG(y, ?)', 4)}}), {x: {$lt: 10}} ] ] }); c.sql(); // -> // '(x BETWEEN ? AND ?) // OR // (y != ?) // OR // ( // (x != LOG(y, ?)) // AND // (x < ?) // )' c.params(); // -> [5, 10, 12, 4, 10] ``` last but not least: the fact that [mohair](http://github.com/snd/mohair)/[mesa](http://github.com/snd/mesa)-queries are *sql-fragments* allows you to model subqueries with mohair/mesa and then use them directly in *condition-objects*. this makes the creation of SQL-where-conditions that contain subqueries quite elegant: [see the examples !](#subqueries) #### making your own fragments if we wanted to support [see also](http://www.postgresql.org/docs/9.4/static/functions-json.html) ``` js var pgJsonGet = function(left, right) { var leftF = criterion.coerceToSqlFragment(left); var rightF = criterion.coerceToSqlFragment(right); return { sql: function(escape) { return left.sql(escape) + }, params: function() { } }; }; ``` and use them like this ``` js var c = criterion({ $or: [ criterion('x BETWEEN ? AND ?', 5, 10), {y: {$ne: 12}} [ criterion('x != LOG(y, ?)', 4)}}), {x: {$lt: 10}} ] ] }); ``` there is a library that does that for you. ## for users of mesa and mohair [EVERYTHING possible with criterion](http://github.com/snd/criterion#reference-by-example) is possible for the where conditions in [mesa](http://github.com/snd/mesa) and [mohair](http://github.com/snd/mohair) ! the [criterion reference](http://github.com/snd/criterion#reference-by-example) completes mesa's and mohair's documentation ! here's why: the criterion module exports a single function: `var criterion = require('criterion')` [mesa's](http://github.com/snd/mesa) and [mohair's](http://github.com/snd/mohair) fluent `.where()` methods call `criterion()` under the hood and forward all their arguments **unmodifed** to `criterion()`. this means that all arguments supported by `criterion()` are supported by `.where()` ! ``` js // same condition-object var condition = {x: 7}; // criterion var criterion = require('criterion'); var c = criterion(condition); c.sql(); // -> 'x = ?' c.params(); // -> [7] // mohair var mohair = require('mohair'); var query = mohair .table('post') .where(condition); query.sql(); // -> 'SELECT * FROM post WHERE x = ?' query.params(); // -> [7] ``` if `.where()` is called more than once the resulting criteria are [ANDed](#combining-criteria-with-and) together: ``` js var mohair = require('mohair'); var postTable = mohair.table('post') var queryAlpha = postTable.where({x: 7}); var queryBravo = queryAlpha.where('y IN (?)', [1, 2]); postTable.sql(); // -> 'SELECT * FROM post' postTable.params(); // -> [] queryAlpha.sql(); // -> 'SELECT * FROM post WHERE x = ?' queryAlpha.params(); // -> [7] queryBravo.sql(); // -> 'SELECT * FROM post WHERE x = ? AND y IN (?, ?)' queryBravo.params(); // -> [7, 1, 2] ``` calling methods on does not but refines this is one of the nice properties of mohair and mesa. ## reference by example ### how to read this reference *for each section several examples are given and seperated by "or". the criteria created in the examples behave identical. the first example in each section uses condition-objects and is always the preferred way of doing things !* ### comparisons #### equal where `x = 7`: ``` js var c = criterion({x: 7}); c.sql(); // -> 'x = ?' c.params(); // -> [7] ``` or raw: ``` js var c = criterion('x = ?', 7); ``` or functional: ``` js var c = criterion.eq(criterion.escape(x), 7); ``` #### not equal where `x != 3`: ``` js var c = criterion({x: {$ne: 3}}); c.sql(); // -> 'x != ?' c.params(); // -> [3] ``` or raw: ``` js var c = criterion('x != ?', 3); ``` or functional: ``` js var c = criterion.ne(criterion.escape(x), 3); ``` #### lower than where `x < 3` and `y <= 4`: ``` js var c = criterion({x: {$lt: 3}, y: {$lte: 4}}); c.sql(); // -> 'x < ? AND y <= ?' c.params(); // -> [3, 4] ``` or raw: ``` js var c = criterion('x < ? AND y <= ?', 3, 4); ``` or functional: ``` js var c = criterion.and( criterion.lt(criterion.escape('x'), 3), criterion.lte(criterion.escape('y'), 4) ); ``` #### greater than where `x > 3` and `y >= 4`: ``` js var c = criterion({x: {$gt: 3}, y: {$gte: 4}}); c.sql(); // -> 'x > ? AND y >= ?' c.params(); // -> [3, 4] ``` or raw: ``` js var c = criterion('x > ? AND y >= ?', 3, 4); ``` or functional: ``` js var c = criterion.and( criterion.gt(criterion.escape('x'), 3), criterion.gte(criterion.escape('y'), 4) ); ``` #### null where `x` is `null` ``` js var c = criterion({x: {$null: true}); c.sql(); // -> 'x IS NULL' c.params(); // -> [] ``` or raw: ``` js var c = criterion('x IS NULL'); ``` or functional: ``` js var c = criterion.null(criterion.escape('x'), true); // true is default ``` #### not null where `x` is not `null`: ``` js var c = criterion({x: {$null: false}}); c.sql(); // -> 'x IS NOT NULL' c.params(); // -> [] ``` or raw: ``` js var c = criterion('x IS NOT NULL'); ``` or functional: ``` js var c = criterion.null(criterion.escape('x'), false); ``` ### boolean operations `$or`, `$and` and `$not` can be nested arbitrarily. #### and where `x = 7` and `y = 'a'`: ``` js var c = criterion({x: 7, y: 'a'}); c.sql(); // -> 'x = ? AND y = ?' c.params(); // -> [7, 'a'] ``` or using an array: ``` js var c = criterion([{x: 7}, {y: 'a'}]); ``` or more verbose: ``` js var c = criterion({$and: {x: 7, y: 'a'}}); ``` or more verbose using an array: ``` js var c = criterion({$and: [{x: 7}, {y: 'a'}]}); ``` or raw: ``` js var c = criterion('x = ? AND y = ?', 7, 'a'); ``` or functional: ``` js var c = criterion.and( criterion.eq(criterion.escape('x'), 7), criterion.eq(criterion.escape('y'), 'a') ); ``` #### or where `x = 7` or `y = 6`: ``` js var c = criterion({$or: {x: 7, y: 6}}); c.sql(); // -> 'x = ? OR y = ?' c.params(); // -> [7, 6] ``` or using an array: ``` js var c = criterion({$or: [{x: 7}, {y: 6}]}); ``` or raw: ``` js var c = criterion('x = ? OR y = ?', 7, 6); ``` or functional: ``` js var c = criterion.or( criterion.eq(criterion.escape('x'), 7), criterion.eq(criterion.escape('y'), 6) ); ``` #### not where not (`x > 3` and `y >= 4`): ``` js var c = criterion({$not: {x: {$gt: 3}, y: {$gte: 4}}}); c.sql(); // -> 'NOT (x > ? AND y >= ?)' c.params(); // -> [3, 4] ``` or raw: ``` js var c = criterion('NOT (x > ? AND y >= ?)', 3, 4); ``` or functional: ``` js var c = criterion.not( criterion.and( criterion.eq(criterion.escape('x'), 3), criterion.eq(criterion.escape('y'), 4) ) ); ``` `$or`, `$and` and `$not` can be nested arbitrarily. ### lists of scalar expressions [see also the postgres documentation on row and array comparisons](http://www.postgresql.org/docs/9.3/static/functions-comparisons.html) #### in list where `x` is in `[1, 2, 3]` ``` js var c = criterion({x: [1, 2, 3]}); c.sql(); // -> 'x IN (?, ?, ?)' c.params(); // -> [1,2,3] ``` or more verbose: ``` js var c = criterion({x: {$in: [1, 2, 3]}}); ``` or raw: ``` js var c = criterion('x IN (?)', [1, 2, 3]); ``` or functional: ``` js var c = criterion.in(criterion.escape('x'), [1, 2, 3]); ``` #### not in list where `x` is not in `[1, 2, 3]` ``` js var c = criterion({x: {$nin: [1, 2, 3]}}); c.sql(); // -> 'x NOT IN (?, ?, ?)' c.params(); // -> [1,2,3] ``` or raw: ``` js var c = criterion('x NOT IN (?)', [1, 2, 3]); ``` or functional: ``` js var c = criterion.nin(criterion.escape('x'), [1, 2, 3]); ``` ### subqueries `var subquery` in the examples below can be any [sql-fragment](#the-sql-fragment-interface). the fact that [mohair](http://github.com/snd/mohair)/[mesa](http://github.com/snd/mesa)-queries are *sql-fragments* allows you to model subqueries with mohair/mesa and then use them directly in *condition-objects*. this makes the creation of SQL-where-conditions that contain subqueries quite elegant. [see also the postgres documentation on row and array comparisons](http://www.postgresql.org/docs/9.3/static/functions-comparisons.html) #### in subquery where `x` is in subquery: ``` js var subquery = mohair .table('post') .where({is_published: true}) .select('id'); var c = criterion({x: {$in: subquery}}); c.sql(); // -> 'x IN (SELECT id FROM post WHERE is_published = ?)' c.params(); // -> [true] ``` or functional: ``` js var c = criterion.in(criterion.escape('x'), subquery); ``` #### not in subquery where `x` is not in subquery: ``` js var subquery = mohair .table('post') .where({is_published: true}) .select('id'); var c = criterion({x: {$nin: subquery}}); c.sql(); // -> 'x NOT IN (SELECT id FROM post WHERE is_published = ?)' c.params(); // -> [true] ``` or functional: ``` js var c = criterion.nin(criterion.escape('x'), subquery); ``` #### subquery returns any rows ``` js # TODO this isnt right var subquery = mohair .table('post') .where({is_published: false}) .where({user_id: mohair.raw('id')}) var c = criterion({$exists: subquery}) c.sql(); // -> 'EXISTS (SELECT * FROM post WHERE is_published = ?)' c.params(); // -> [true] ``` or functional: ``` js var c = criterion.exists(subquery); ``` #### compare to any/all in subquery ``` js var subquery = mohair .table('post') .select('id') .where({is_published: false}) var c = criterion({x: {$any: subquery}}) c.sql(); // -> 'x = ANY (SELECT * FROM post WHERE is_published = ?)' c.params(); // -> [true] ``` or functional: ``` js var c = criterion.any(criterion.escape('x'), subquery); ``` criterion supports #### row-wise comparison with subqueries find published posts that were created strictly-before the user with `id = 1` was created: ``` js var mohair = require('mohair'); var creationDateOfUserWithId1 = mohair .table('user') .where({id: 1}) .select('created_at'); var postsCreatedBeforeUser = mohair .table('post') .where({is_published: true}) .where({created_at: {$lt: creationDateOfUserWithId1}}); postsCreatedBeforeUser.sql(); // -> // 'SELECT * // FROM post // WHERE is_published = ? // AND created_at < (SELECT created_at FROM user WHERE id = ?)' postsCreatedBeforeUser.params(); // -> [true, 1] ``` ## advanced topics ### combining criteria with `.and()` ``` js var alpha = criterion({x: 7, y: 'a'}); var bravo = criterion('z = ?', true); alpha.and(bravo).sql(); // -> '(x = ?) AND (y = ?) AND (z = ?)' alpha.and(bravo).params(); // -> [7, 'a', true] ``` `and()`, `or()` and `not()` return new objects. no method ever changes the object it is called on. ### combining criteria with `.or()` ``` js var alpha = criterion({x: 7, y: 'a'}); var bravo = criterion('z = ?', true); bravo.or(alpha).sql(); // -> '(z = ?) OR (x = ? AND y = ?)' bravo.or(alpha).params(); // -> [true, 7, 'a'] ``` `and()`, `or()` and `not()` return new objects. no method ever changes the object it is called on. ### negating criteria with `.not()` ``` js var c = criterion({x: 7, y: 'a'}); c.not().sql(); // -> 'NOT ((x = ?) AND (y = ?))' c.not().params(); // -> [7, 'a'] ``` double negations are removed: ``` js var c = criterion({x: 7, y: 'a'}); c.not().not().sql(); // -> '(x = ?) AND (y = ?)' c.not().not().params(); // -> [7, 'a'] ``` ### escaping column names you can pass a function into any `sql()` method to escape column names: ``` js var c = criterion({x: 7, y: 8}); var escape = function(x) { return '"' + x + '"'; }; c.sql(escape); // -> '"x" = ? AND "y" = ?' <- x and y are escaped ! c.params(); // -> [7, 8] ``` ### param array explosion if a parameter binding for raw sql is an array then the corresponding binding `?` is exploded into a list of `?`: ``` js var c = criterion('x = ? AND y IN (?)', 7, [8, 9, 10]); c.sql(); // -> 'x = ? AND y IN (?, ?, ?)' c.params(); // -> [7, 8, 9, 10] ``` ``` js var c = criterion('x = ? AND (y && ARRAY[?])', 7, [8, 9, 10]); c.sql(); // -> 'x = ? AND (y && ARRAY[?, ?, ?])' c.params(); // -> [7, 8, 9, 10] ``` ## changelog ### 0.4.0 - to escape column names in the resulting SQL an escape function can now be passed as an argument into any `sql()` method - sql fragments are now always wrapped in parentheses before pasting them into a query. - doesn't break anything and makes subqueries work without further changes. - added `$exists` which can be used with mesa/mohair queries (or any object that responds to an `sql()` method): `criterion({$exists: mohair.table('post').where({id: 7})})` - `$in` and `$nin` now support not just lists of values but also subqueries: - `criterion({id: {$in: mohair.table('post').where({is_active: true}).select('id')}})` - added modifiers `$any`, `$neAny`, `$ltAny`, `$gtAny`, `$gteAny`, `$all`, `$neAll`, `$ltAll`, `$lteAll`, `$gtAll`, `$gteAll` to be used with subqueries: - `criterion({created_at: {$gteAll: mohair.table('post').where({is_active: true}).select('updated_at')}})` - sql-fragments can now be used in more places... - where the value would normally go in a comparison: `{$lt: criterion('5 + 8')}` - this makes row-wise comparisons with subqueries possible - in the arrays passed to `$or` and `$and`: `{$or [{a: 7}, criterion('b < ?', 5)]}` - ... - bugfixes - made some (exotic) condition-objects work which didn't work before - improved implementation and based everything on a DSL which is also exposed - major improvements to - code quality - tests - terminology - documentation ## [license: MIT](LICENSE) ## TODO - test dsl - document dsl - read through the code again - often the left side of an operation is just a column or a table qualified column - atoms are treated as values - dontWrap - says how outer fragments should handle this fragment - things are only wrapped when inside of something - finish the readme - test left operands - reverse operands