mohair
Version:
sql query builder
184 lines (135 loc) • 5.48 kB
text/coffeescript
assert = require 'assert'
mysql =
getPlaceholderGenerator: ->
-> '?'
quote: (string) -> string.split('.').map((part) -> "`#{part}`").join('.')
postgres =
getPlaceholderGenerator: ->
i = 1
-> "$#{i++}"
quote: (string) -> string.split('.').map((part) -> "\"#{part}\"").join('.')
values = (obj) -> Object.keys(obj).map (key) -> obj[key]
Mohair = class
constructor: (@options = mysql) ->
@getNextPlaceholder = @options.getPlaceholderGenerator()
@quote = @options.quote
@_sql = ''
@_params = []
@_queryModifiers =
$or: (q) => @parens => @subqueryByOp 'OR', q
$and: (q) => @subqueryByOp 'AND', q
$not: (q) => @not => @query q
$nor: (q) => @not => @subqueryByOp 'OR', q
@_tests =
$in: (x) => @before ' IN ', => @array x
$nin: (x) => @before ' NOT IN ', => @array x
comparisons =
'$eq': ' = '
'$ne': ' != '
'$lt': ' < '
'$lte': ' <= '
'$gt': ' > '
'$gte': ' >= '
for key, operator of comparisons
do (key, operator) =>
@_tests[key] = (x) => @before operator, => @callOrBind x
# Core
# ====
sql: -> @_sql
params: -> @_params
raw: (sql, params...) ->
@_sql += sql
@_params.push params...
# Helpers
# =======
array: (xs) -> @parens => @intersperse ', ', xs, @callOrBind
not: (inner) -> @before 'NOT ', => @parens inner
quoted: (string) -> @raw "'#{string}'"
intersperse: (string, obj, f) ->
first = true
for key, value of obj
do (key, value) =>
if first then first = false else @raw string
f value, key
around: (start, end, inner) ->
@raw start
inner() if inner?
@raw end
before: (start, inner) -> @around start, '', inner
parens: (inner) -> @around '(', ')', inner
command: (start, inner) -> @around start, ';\n', inner
callOrQuery: (f) -> if typeof f is 'function' then f() else @where f
callOrBind: (f) =>
if typeof f is 'function' then f() else @raw @getNextPlaceholder(), f
# {key1} = {value1()}, {key2} = {value2()}, ...
assignments: (obj) ->
@intersperse ', ', obj, (value, column) =>
@before "#{@quote(column)} = ", => @callOrBind value
# Interface
# =========
insert: (table, objects, updates) ->
throw new Error 'second argument missing in insert' if not objects?
objects = if Array.isArray objects then objects else [objects]
return @command "INSERT INTO #{@quote table} () VALUES ()" if objects.length is 0
keys = Object.keys objects[0]
columnString = keys.map(@quote).join(', ')
@command "INSERT INTO #{@quote table} (#{columnString}) VALUES ", =>
@intersperse ', ', objects, (object, index) =>
assert.deepEqual keys, Object.keys(object),
'objects must have the same keys'
@array values object
if updates?
throw new Error 'empty updates object' if Object.keys(updates).length is 0
@raw ' ON DUPLICATE KEY UPDATE '
@assignments updates
update: (table, changes, f) ->
@command "UPDATE #{@quote table} SET ", =>
@assignments changes
@callOrQuery f
delete: (table, f) -> @command "DELETE FROM #{@quote table}", => @callOrQuery f
select: (table, columns, f) ->
if not f?
f = columns
columns = '*'
columns = columns.join(', ') if Array.isArray columns
@command "SELECT #{columns} FROM #{@quote table}", =>
@callOrQuery f if f?
transaction: (inner) -> @around 'BEGIN;\n', 'COMMIT;\n', inner
# Select inner
# ------------
where: (f) -> @before " WHERE ", =>
if typeof f is 'function' then f() else @query f
_join: (prefix, table, left, right) ->
@raw if not left?
"#{prefix} JOIN #{table}"
else
"#{prefix} JOIN #{@quote table} ON #{@quote left} = #{@quote right}"
join: (args...) -> @_join '', args...
leftJoin: (args...) -> @_join ' LEFT', args...
rightJoin: (args...) -> @_join ' RIGHT', args...
innerJoin: (args...) -> @_join ' INNER', args...
groupBy: (column) -> @raw " GROUP BY #{@quote column}"
orderBy: (column, direction) -> @raw " ORDER BY #{@quote column} #{direction}"
limit: (count) ->
@raw " LIMIT "
@callOrBind count
skip: (count) ->
@raw " SKIP "
@callOrBind count
# Query
# =====
query: (query) ->
@intersperse ' AND ', query, (value, key) =>
return @_queryModifiers[key] value if @_queryModifiers[key]?
@raw @quote(key)
isTest = typeof value is 'object'
test = @_tests[if isTest then Object.keys(value)[0] else '$eq']
test if isTest then values(value)[0] else value
subqueryByOp: (op, list) ->
if not Array.isArray list
msg = "array expected as argument to #{op} query but #{list} given"
throw new Error msg
@intersperse " #{op} ", list, (x) => @query x
module.exports = (options) -> new Mohair options
module.exports.mysql = -> new Mohair mysql
module.exports.postgres = -> new Mohair postgres