Gesundheit generates SQL, it does this using a sugary API for managing the abstract syntax tree of a SQL statement. After building your statement programmatically, gesundheit can compile it to a string or execute it against your database for you, using proper bound parameters and allowing for streaming of results.
Contents:
There are a few subsystems that make up gesundheit, but the majority of use cases will be covered by using the following properties of the main module:
- gesundheit.{Select, SELECT, select}
- Function for creating new SelectQuery instances.
- gesundheit.{Update, UPDATE, update}
- Function for creating new UpdateQuery instances.
- gesundheit.{Delete, DELETE, delete}
- Function for creating new DeleteQuery instances.
- gesundheit.{Insert, INSERT, insert}
- Function for creating new InsertQuery instances.
- gesundheit.engines.{mysql, postgres}
- Functions for creating new engines.
- gesundheit.defaultEngine
- The engine that will be used for queries that aren’t explicitly bound. This is set to a no-op engine that you will want to replace either using the gesundheit.engines functions or by implementing the engine interface yourself.
- Join types
- Constant nodes for use with SUDQuery.join. ‘LEFT’, ‘RIGHT’, ‘INNER’, ‘LEFT_OUTER’, ‘RIGHT_OUTER’, ‘FULL_OUTER’ ‘NATURAL’, ‘CROSS’
- AST helper functions
These come from the nodes module and are often useful when constructing queries that the query manager classes don’t cover as well:
- toParam
- toRelation
- binaryOp
- sqlFunction
- text
If you are implementing support for a different database engine or constructing particularly unusual SQL statements, you might also want to make use of these:
- gesundheit.nodes
- The nodes <Nodes> module.
- gesundheit.dialects
- The dialects <Dialects> module.
The main interface for building queries with gesundheit are the query manager classes. They provide an API designed to make most query building operations concise and fluent, while under the hood they manage an abstract syntax tree for the query.
All of the query managers are created with functions named after the query type that take a table (or alias) as their first parameter. To demonstrate we will create a simple select query:
select = require('gesundheit').select
departments = select('departments')
This creates a new SelectQuery query instance that generates the SQL string SELECT * FROM departments. To refine the field list we call SelectQuery.fields:
departments.fields('name', 'manager_id')
It’s important to note that all of the query manager methods modify the query in-place [1] so departments will now render to SELECT name, manager_id FROM departments.
To turn the query object into a SQL string and array of bind parameters, we .compile the query:
assert.deepEqual(
departments.compile(),
[ 'SELECT name, manager_id FROM departments', [] ]
)
(there are no bind parameters in our query yet)
Most often you don’t really care about the SQL string and params themselves, but want result of performing the query on an actual database. In that case you simply use the .execute method:
query.execute (err, res) -> console.log {err, res}
... but gesundheit can’t know about how to find and connect to your database all on it’s own! To execute with a real connection you will need to bind the query object to an engine or client. You can bind a query by passing an engine/client as the first parameter to those methods that require a binding, or assigning a new defaultEngine for implicit binding.
The first step in either case is to create an engine:
gesundheit = require('gesundheit')
# The parameter to mysql() is the same as for mysql.createClient()
engine = gesundheit.engines.mysql({database: 'test'})
Then we can pass it, or a client we returned by it’s .connect method, to query.execute:
query.execute engine, (err, res) -> console.log {err, res}
engine.connect (err, client) ->
throw err if err
query.execute client, (err, res) -> console.log {err, res}
Finally, you can also set a new defaultEngine for implicit binding:
gesundheit.defaultEngine = engine
query.execute (err, res) -> console.log {err, res}
Now BaseQuery.execute and other methods that need a database client will get one from the engine automatically.
Any function that accepts a table or field parameter will accept a string, an instance of the appropriate AST node type, or an alias object. Alias objects are objects with a single key-value pair where the key is an alias name and the value is the object to be aliased. So the alias object {p: 'people'} will generate the SQL string people AS p. Here is an example of aliasing table and field names:
# SELECT manager_id AS m_id FROM departments AS d;
select({d: 'departments'}, [{m_id: 'manager_id'}])
(This example also shows passing a list of fields to select as the second parameter).
Footnotes
[1] | Use BaseQuery.copy if you want to generate multiple independent refinements from a single query instance. |
The query manager classes use the following inheritance hierarchy:
- BaseQuery
- InsertQuery
- SUDQuery
- SelectQuery
- UpdateQuery
- DeleteQuery
The following functions for creating *Query class instances are re-exported by the main gesundheit module:
Create a new InsertQuery that will add rows to table.
The fields parameter is required to be an array of column names that will be inserted.
Create a new SelectQuery selecting from table.
Parameters: |
|
---|
Create a new UpdateQuery that will update table.
Create a new DeleteQuery that will delete rows from table.
The base class for all queries. While this class itself is not part of gesundheits public API, the methods defined on it are.
Parameters: |
|
---|
Instantiate a new query with a deep copy of this ones AST
Call the given function in the context of this query. This is mostly useful in coffeescript where you can use it as a sort-of-DSL:
queryObject.visit ->
@where x: val
@orderBy x: 'ASC'
The current query is also given as the first parameter to the query in case you need it.
If called before .render(), then resulting SQL will be sent to stdout via console.log()
Bind this query object to a bindable object (engine or client). If no argument is given the query will be bound to the default engine.
Render the query to SQL.
Parameters: |
|
---|
Compile this query object, returning a SQL string and parameter array.
Parameters: |
|
---|
Execute the query and buffer all results.
Parameters: |
|
---|
Execute the query and stream the results.
Parameters: |
|
---|
Decorates a method so that it can accept a bindable object as it’s first argument, and will always call @bind() before the method itself.
Insert queries are much simpler than most query types: they cannot join multiple tables.
Add multiple rows of data to the insert statement.
Add a single row
Insert from a select query.
SUDQuery is the base class for SELECT, UPDATE, and DELETE queries. It adds logic to BaseQuery for dealing with WHERE clauses and ordering.
Add a WHERE clause to the query. Can optionally take a table/alias name as the first parameter, otherwise the clause is added using the last table added to the query.
The where clause itself can be a comparison node, such as those produced by the ComparableMixin methods:
q.where(q.project('table','field1').eq(42))
q.where(q.project('table','field2').gt(42))
... Or an object literal where each key is a field name (or field name alias) and each value is a constraint:
q.where('table', {field1: 42, field2: {gt: 42}})
Constraints values can also be other projected fields:
p = q.project.bind(q, 'table')
q.where('table', p('field1').gt(p('field2')))
Add one or more WHERE clauses, all joined by the OR operator.
Add an ORDER BY to the query. Currently this always uses the “active” table of the query. (See SelectQuery.from)
Each ordering can either be a string, in which case it must be a valid-ish SQL snippet like ‘some_field DESC’, (the field name and direction will still be normalized) or an object, in which case each key will be treated as a field and each value as a direction.
Set the LIMIT on this query
Set the OFFSET of this query
Start a select query with exports.select:
light_recliners = select('chairs', ['chair_type', 'size'])
.where({chair_type: 'recliner', weight: {lt: 25}})
Join another table with SelectQuery.join:
men_with_light_recliners = light_recliners.copy()
.join("people", {
on: {chair_id: query.project('chairs', 'id')},
fields: ['name']
})
.where({gender: 'M'})
Note that joining a table “focuses” it, so “gender” in .where({gender: 'M'}) refers to the people.gender column. To add more conditions on an earlier table refocus it with SelectQuery.focus:
men_with_light_recliners.focus('chairs')
Ordering and limits are added with methods of the same name:
men_with_light_recliners
.order(weight: 'ASC)
.limit(5)
The entire query can also be written using BaseQuery.visit (and less punctuation) like so:
men_with_light_recliners = select('chairs', ['chair_type', 'size']).visit ->
@where chair_type: 'recliner', weight: {lt: 25}
@join "people",
on: {chair_id: @project('chairs', 'id')},
fields: ['name']
@where gender: 'M'
@focus 'chairs'
@order weight: 'ASC
@limit 5
Adds a number of SELECT-specific methods to SUDQuery, such as fields and groupBy
Adds one or more fields to the query. If the second argument is an array, the first argument is treated as a table (in the same way that join understands tables) and the second argument as the list of fields to select/update from that table. The table must already be joined for this to work.
If the second argument is not an array, then each argument is treated as an individual field to be projected from the currently focused table.
Adds one or more aggregated fields to the query
Parameters: |
|
---|
Example:
select('t1').agg('count', 'id') # SELECT count(id) FROM t1
Make this query DISTINCT on all fields.
Join another table to the query.
Parameters: |
|
---|
The same as join, but will only join tbl if it is not joined already.
A shorthand way to get a relation by (alias) name
Return a Projection node representing <alias>.<field>.
This node has a number methods from ComparableMixin that can create new comparison nodes usable in join conditions and where clauses:
# Find developers over the age of 45
s = select('people', ['name'])
dep_id = s.project('people', 'department_id')
s.join('departments', on: {id: dep_id})
s.where(s.project('departments', 'name').eq('development'))
s.where(s.project('people', 'age').gte(45))
Make a different table “focused”, this will use that table as the default for the fields, order and where methods.
Parameters: |
|
---|
Add a GROUP BY to the query.
Updating rows that match a condition:
update('tweeters') # UPDATE tweeters
.set(influential: true) # SET tweeters.influential = true
.where(followers: gt: 1000) # WHERE tweeters.followers > 1000;
.execute (err, res) ->
throw err if err
# Woohoo
The update query is a little underpowered right now, and can only handle simple updates of a single table.
Add fields to the SET portion of this query.
Parameters: |
|
---|
Directly push one or more nodes into the SET portion of this query
A gesundheit query must be “bound” to an “engine” to render and/or execute. For apps that deal with a single database, you can simply create an engine instance during application startup, assign it to gesundheit.defaultEngine and not have to think about binding after that.
For more complicated scenarios where you need control over the exact connections used (e.g. transactions) you will need to understand the engine/binding system.
An engine is any object that implements the following API:
- render(query)
- Render the given query instance to a SQL string. This method must be synchronous, and will usually just delegate to a subclass of BaseDialect.
- connect(callback)
- Call callback(err, client) where client is an object with a query method that works the same as those of the pg and mysql driver clients. The client must also have an .engine property that points back to the engine instance that created it.
- stream([client,] query, cb)
- If client is not given, get one by calling connect. Then execute the query, calling cb(err, row) for each row in the result.
- execute([client,] query, cb)
- If client is not given, get one by calling connect. Then execute the query, calling cb(err, result) with the full query results.
Gesundheit exports factory functions for creating postgres and mysql engines:
Create a new Postgres engine using a DSN compatible with require('pg').connect(dsn). This generally has the from:
<scheme>://<user>@<host>[:<port>]/<database>
This will require('pg') and attempt to use the ‘native’ interface if it’s available, so that module must be installed for this to work.
Create a new MySQL engine using an object that is compatible with require('node-mysql').createClient(opts),
Additionally, you can specify extra options for generic-pool by including them as an object in opts.pool. The create and destroy pool functions will be created for you.
This will require('mysql') and require('generic-pool') so those modules must be installed and loadable for this function to work.
Create a no-op engine that simply returns the compiled SQL and parameter array to the result callback. This will be the default until you over-ride with gesundheit.defaultEngine = myAppEngine.
Decorate a method so that it will be called with a connected client prepended to the argument list. The method must receive an object bound to an engine as it’s first argument.
The render, compile, stream, and execute methods of BaseQuery all require an engine to do their work. Rather than requiring the engine to be passed to each of these methods, the query can be “bound” to an engine or client object.
Queries are bound to such “bindable” objects in one of 3 ways:
- Using BaseQuery.bind.
- A bindable object can be given as the first parameter to methods that require the query to be bound (e.g. BaseQuery.execute).
- If a method that requires a binding is called on an unbound query (and no bindable is given) the value of gesundheit.defaultEngine will be used.
Dialects are responsible for rendering an AST to a SQL string compatible with a particular DBMS. They are rarely used directly, instead a query is usually bound to an engine that will delegate rendering to a private dialect instance.
A dialect that isn’t specific to a particular DBMS, but used as a base for both MySQL and Postgres.
Specialization of BaseDialect for MySQL
These are the classes that represent nodes in the AST for a SQL statement. Application code should very rarely have to deal with these classes directly; Instead, the APIs exposed by the various query manager classes are intended to cover the majority of use-cases. However, in the spirit of “making hard things possible”, the various AST nodes can be constructed and assembled manually if you so desire.
A ValueNode is a literal string that should be printed unescaped.
A set of nodes joined together by @glue
Parameters: |
|
---|
Make a deep copy of this node and it’s children
Add a new Node to the end of this set
Recurse over nested NodeSet instances, collecting parameter values.
A FixedNodeSet that instantiates a set of nodes defined by the class member @structure when it it instantiated.
See Select for example.
Includes ComparableMixin
Example:
table = new Relation(‘my_table_with_a_long_name’) alias = new Alias(table, ‘mtwaln’)
A relation node represents a table name in a statement.
Return the table name. This is a common interface with nodes:Alias.
Return a new Projection of field from this table.
An alias for Relation.project.
A tuple node. e.g. (col1, col2)
Includes ComparableMixin
Manages a set of relation and exposes methods to find them by alias.
A mixin that adds comparison methods to a class. Each of these comparison methods will yield a new AST node comparing the invocant to the argument.
this = other
this <> other
this > other
this < other
this <= other
this >= other
this op other
Return a Node that can be used as a parameter.
- SelectQuery instances will be treated as un-named sub queries,
- Node instances will be returned unchanged.
- Arrays will be turned into a Tuple instance.
All other types will be wrapped in a Parameter instance.
Create a new SQL function call node. For example:
count = sqlFunction(‘count’, new ValueNode(‘*’))
Check if o is an object literal representing an alias, and return the alias name if it is.
Construct a node with a raw SQL string and (optionally) parameters.
Parameter arguments are assumed to be values for placeholders in the raw string. Be careful: the number and types of these parameters will not be checked, so it is very easy to create invalid statements with this.
Create a new Binary node:
binaryOp('hstore_column', '->', toParam(y))
# hstore_column -> ?
See also
Return a deep copy of it.