UNPKG

21.3 kBtext/coffeescriptView Raw
1###
2These are the classes that represent nodes in the AST for a SQL statement.
3Application code should very rarely have to deal with these classes directly;
4Instead, the APIs exposed by the various query manager classes are intended to
5cover the majority of use-cases.
6
7However, in the spirit of "making hard things possible", all of AST nodes are
8exported from this module so you can constructed and assemble them manually if
9you need to.
10###
11
12class Node
13 ### (Empty) base Node class ###
14 compile: (dialect) ->
15 message = "#{@constructor} has no compile method. Parents: #{dialect.path}"
16 throw new Error message
17
18
19class ValueNode extends Node
20 ### A ValueNode is a literal string that should be printed unescaped. ###
21 constructor: (@value) ->
22 if @value?
23 throw new Error("Invalid #{@constructor.name}: #{@value}") unless @valid()
24 copy: -> new @constructor @value
25 valid: -> true
26 compile: -> @value
27
28class IntegerNode extends ValueNode
29 ### A :class:`nodes::ValueNode` that validates it's input is an integer. ###
30 valid: -> not isNaN @value = parseInt @value
31
32class Identifier extends ValueNode
33 ###
34 An identifier is a column or relation name that may need to be quoted.
35 ###
36 compile: (dialect) ->
37 dialect.quote(@value)
38
39CONST_NODES = {}
40CONST_NODES[name] = new ValueNode(name.replace('_', ' ')) for name in [
41 'DEFAULT', 'NULL', 'IS_NULL', 'IS_NOT_NULL'
42]
43
44class JoinType extends ValueNode
45
46JOIN_TYPES = {}
47JOIN_TYPES[name] = new JoinType(name.replace('_', ' ')) for name in [
48 'LEFT', 'RIGHT', 'INNER',
49 'LEFT_OUTER', 'RIGHT_OUTER', 'FULL_OUTER'
50 'NATURAL', 'CROSS'
51]
52
53class NodeSet extends Node
54 ### A set of nodes joined together by ``@glue`` ###
55 constructor: (nodes, glue=' ') ->
56 ###
57 :param @nodes: A list of child nodes.
58 :param glue: A string that will be used to join the nodes when compileing
59 ###
60 @nodes = []
61 @addNode(node) for node in nodes if nodes
62 @glue ?= glue
63
64 copy: ->
65 ###
66 Make a deep copy of this node and it's children
67 ###
68 c = new @constructor @nodes.map(copy), @glue
69 return c
70
71 addNode: (node) ->
72 ### Add a new Node to the end of this set ###
73 @nodes.push node
74
75 compile: (dialect) ->
76 compile = dialect.compile.bind(dialect)
77 @nodes.map(compile).filter(Boolean).join(@glue)
78
79
80class FixedNodeSet extends NodeSet
81 # A NodeSet that disables the ``addNode`` method after construction.
82 constructor: ->
83 super
84 @addNode = null
85
86class Statement extends Node
87 # A Statement lazily constructs child nodes.
88 @prefix = ''
89
90 # Define the names and type of each lazily built child node
91 @structure = (structure) ->
92 @_nodeOrder = []
93 structure.forEach ([k, type]) =>
94 @_nodeOrder.push k
95 @::__defineGetter__ k, -> @_private[k] or= new type
96 @::__defineSetter__ k, (v) -> @_private[k] = v
97
98 constructor: (opts) ->
99 @_private = {}
100 @initialize(opts) if opts
101
102 initialize: (opts) ->
103 @initialize = null
104
105 copy: ->
106 c = new @constructor
107 for k, node of @_private
108 c[k] = copy node
109 c.initialize = null
110 return c
111
112 compile: (dialect) ->
113 parts = for k in @constructor._nodeOrder when node = @_private[k]
114 dialect.compile(node)
115 if parts.length
116 @constructor.prefix + parts.join(' ')
117 else
118 return ""
119
120class ParenthesizedNodeSet extends NodeSet
121 ### A NodeSet wrapped in parenthesis. ###
122 compile: ->
123 "(" + super + ")"
124
125class AbstractAlias extends Node
126 constructor: (@obj, @alias) ->
127 copy: -> new @constructor copy(@obj), @alias
128 ref: -> @alias
129 compile: (dialect) ->
130 dialect.maybeParens(dialect.compile(@obj)) + " AS " + dialect.quote(@alias)
131
132# End of generic base classes
133
134class TextNode extends Node
135 constructor: (@text, @bindVals=[]) ->
136
137 paramRegexp = /\$([\w]+)\b/g
138
139 compile: (dialect) ->
140 @text.replace paramRegexp, (_, name) =>
141 if name of @bindVals
142 dialect.parameter(@bindVals[name])
143 else
144 throw new Error "Parameter #{name} not present in #{JSON.stringify(@bindVals)}"
145
146 as: (alias) ->
147 new Alias @, alias
148
149 copy: -> new @constructor(@text, copy(@bindVals))
150
151 @Alias = class Alias extends AbstractAlias
152
153
154class SqlFunction extends Node
155 ### Includes :class:`nodes::ComparableMixin` ###
156 constructor: (@name, @arglist) ->
157 ref: -> @name
158 copy: -> new @constructor @name, copy(@arglist)
159 compile: (dialect) ->
160 "#{@name}#{dialect.compile @arglist}"
161 as: (alias) -> new Alias @, alias
162
163 @Alias = class Alias extends AbstractAlias
164 shouldRenderFull = (parents) ->
165 return false if parents.some((it) -> it instanceof Column)
166 parents.some (node) ->
167 node instanceof ColumnSet or node instanceof RelationSet
168
169 compile: (dialect, parents) ->
170 if shouldRenderFull(parents)
171 dialect.compile(@obj) + " AS " + dialect.quote(@alias)
172 else
173 dialect.quote(@alias)
174
175class Parameter extends ValueNode
176 ###
177 Like a ValueNode, but will render as a bound parameter place-holder
178 (e.g. ``$1``) and it's value will be collected by the dialect when compiling.
179 ###
180 compile: (dialect) ->
181 dialect.parameter(@value)
182
183class Relation extends Identifier
184 ###
185 A relation node represents a table name or alias in a statement.
186 ###
187 ref: ->
188 ###
189 Return the table name. Aliased tables return the alias name.
190 ###
191 @value
192
193 project: (field) ->
194 ### Return a new :class:`nodes::Column` of `field` from this table. ###
195 new Column @, toField(field)
196
197 as: (alias) ->
198 new Alias @, alias
199
200 @Alias = class Alias extends AbstractAlias
201 ### An aliased :class:`nodes::Relation` ###
202 project: (field) -> Relation::project.call @, field
203 compile: (dialect, parents) ->
204 if parents.some((n) -> n instanceof Column)
205 dialect.quote(@alias)
206 else
207 super
208
209class Field extends Identifier
210 ### A column name ###
211
212class Column extends FixedNodeSet
213 ###
214 Includes :class:`nodes::ComparableMixin`
215 ###
216 constructor: (@source, @field) -> super [@source, @field], '.'
217 rel: -> @source
218 copy: -> new @constructor copy(@source), copy(@field)
219 as: (alias) ->
220 ### Return an aliased version of this column. ###
221 new Alias @, alias
222
223 @Alias = class Alias extends AbstractAlias
224 ### An aliased :class:`nodes::Column` ###
225 rel: -> @obj.rel()
226 compile: (dialect, parents) ->
227 for node in parents when node instanceof ColumnSet
228 return dialect.compile(@obj) + " AS " + dialect.quote(@alias)
229 dialect.quote(@alias)
230
231class Limit extends IntegerNode
232 compile: ->
233 if @value then "LIMIT #{@value}" else ""
234
235class Offset extends IntegerNode
236 compile: ->
237 if @value then "OFFSET #{@value}" else ""
238
239class Binary extends FixedNodeSet
240 constructor: (@left, @op, @right) -> super [@left, @op, @right], ' '
241 copy: -> new @constructor copy(@left), @op, copy(@right)
242
243 and: (args...) ->
244 new And [@, args...]
245
246 or: (args...) ->
247 new Or [@, args...]
248
249 compile: (dialect) ->
250 [ dialect.compile(@left)
251 dialect.operator(@op)
252 dialect.compile(@right)
253 ].join(' ')
254
255class Tuple extends ParenthesizedNodeSet
256 glue: ', '
257
258class ColumnSet extends NodeSet
259 ### The list of projected columns in a query ###
260 glue: ', '
261
262class Returning extends ColumnSet
263 @extend = (klazz) ->
264 klazz::addReturning = (cols) ->
265 @returning.addNode(toField(col)) for col in cols
266 null
267
268 compile: ->
269 if string = super then "RETURNING #{string}" else ""
270
271class Distinct extends ColumnSet
272 constructor: (@enable=false) -> super
273
274 copy: -> new @constructor @enable, copy(@nodes)
275
276 compile: (dialect) ->
277 if not @enable
278 ''
279 else if @nodes.length
280 "DISTINCT(#{super})"
281 else
282 'DISTINCT'
283
284class SelectColumnSet extends ColumnSet
285 prune: (predicate) ->
286 ###
287 Recurse over child nodes, removing all Column nodes that match the
288 predicate.
289 ###
290 @nodes = @nodes.filter((n) -> not predicate(n))
291
292 compile: (dialect) ->
293 if not @nodes.length
294 '*'
295 else
296 super
297
298
299#######
300class RelationSet extends NodeSet
301 ###
302 Manages a set of relations and exposes methods to find them by alias.
303 ###
304 addNode: (node) ->
305 unless @first
306 @relsByName = {}
307 @nodes.push node
308 @first = @active = @relsByName[node.ref()] = node
309 else
310 super
311 @active = @relsByName[node.ref()] = node.relation
312
313 copy: ->
314 c = super
315 if @active?.ref?() isnt c.active?.ref?()
316 c.switch(@active.ref())
317 return c
318
319 get: (name, strict=true) ->
320 name = name.ref() unless 'string' == typeof name
321 found = @relsByName[name]
322 if strict and not found
323 throw new Error "No such relation #{name} in #{Object.keys @relsByName}"
324 return found
325
326 switch: (name) ->
327 @active = @get(name)
328
329 compile: (dialect) ->
330 if string = super then "FROM #{string}" else ""
331
332class Join extends FixedNodeSet
333 JOIN = new ValueNode 'JOIN'
334 ON = new ValueNode 'ON'
335
336 constructor: (@type, @relation) ->
337 nodes = [@type, JOIN, @relation]
338 super nodes
339
340 on: (clause) ->
341 if @nodes.length < 4
342 @nodes.push ON
343 @nodes.push clause
344
345 ref: ->
346 @relation.ref()
347
348 copy: ->
349 c = new @constructor copy(@type), copy(@relation)
350 for clause in @nodes.slice(4)
351 c.on(clause)
352 return c
353
354
355class Where extends NodeSet
356 glue: ' AND '
357 compile: (dialect) ->
358 if string = super then "WHERE #{string}" else ""
359
360class Or extends ParenthesizedNodeSet
361 glue: ' OR '
362
363 and: (args...) ->
364 new And [@, args...]
365
366 or: (args...) ->
367 ret = @copy()
368 ret.addNode(arg) for arg in args
369 return ret
370
371
372class And extends ParenthesizedNodeSet
373 glue: ' AND '
374
375 and: (args...) ->
376 ret = @copy()
377 ret.addNode(arg) for arg in args
378 return ret
379
380 or: (args...) ->
381 new Or [@, args...]
382
383class GroupBy extends NodeSet
384 glue: ', '
385 compile: (dialect) ->
386 if string = super then "GROUP BY #{string}" else ""
387
388class Having extends NodeSet
389 glue: ' AND '
390 compile: (dialect) ->
391 if string = super then "HAVING #{string}" else ""
392
393class OrderBy extends NodeSet
394 constructor: (orderings) -> super orderings, ', '
395 compile: (dialect) ->
396 if string = super then "ORDER BY #{string}" else ""
397
398class Ordering extends FixedNodeSet
399 constructor: (projection, direction) ->
400 direction = new ValueNode(direction) if typeof direction is 'string'
401 super [projection, direction]
402
403class Select extends Statement
404 ###
405 The root node of a SELECT query
406 ###
407 @prefix = 'SELECT '
408
409 @structure [
410 ['distinct', Distinct]
411 ['projections', SelectColumnSet]
412 ['relations', RelationSet]
413 ['where', Where]
414 ['groupBy', GroupBy]
415 ['having', Having]
416 ['orderBy', OrderBy]
417 ['limit', Limit]
418 ['offset', Offset]
419 ]
420
421 initialize: (opts) ->
422 @projections # ensure we have an (empty) projection set
423 if opts.table
424 @relations.addNode toRelation(opts.table)
425
426class Update extends Statement
427 ###
428 The root node of an UPDATE query
429 ###
430
431 class UpdateSet extends NodeSet
432 # must be pre-defined for the call to @structure below
433 constructor: (nodes) -> super nodes, ', '
434 compile: (dialect) ->
435 if string = super then "SET #{string}" else ""
436
437 @prefix = 'UPDATE '
438
439 @structure [
440 ['relation', Relation]
441 ['updates', UpdateSet]
442 ['orderBy', OrderBy]
443 ['limit', Limit]
444 ['fromList', RelationSet] # Optional FROM portion
445 ['where', Where]
446 ['returning', Returning]
447 ]
448
449 Returning.extend(@)
450
451 initialize: (opts) ->
452 @relation = toRelation(opts.table)
453
454
455class Insert extends Statement
456 ###
457 The root node of an INSERT query
458 ###
459
460 class InsertData extends NodeSet
461 # must be pre-defined for the call to @structure below
462 glue: ', '
463 compile: (dialect) ->
464 if string = super then "VALUES #{string}" else ""
465
466 @ColumnList = class ColumnList extends Tuple
467
468 @prefix = 'INSERT INTO '
469
470 @structure [
471 ['relation', Relation]
472 ['columns', ColumnList]
473 ['data', InsertData]
474 ['returning', Returning]
475 ]
476
477 Returning.extend(@)
478
479 initialize: (opts) ->
480 unless opts.fields?.length
481 throw new Error "Column list is required when constructing an INSERT"
482 @columns = new ColumnList opts.fields.map(toField)
483 @relation = toRelation(opts.table)
484
485 addRow: (row) ->
486 if @data instanceof Select
487 throw new Error "Cannot add rows when inserting from a SELECT"
488 if Array.isArray(row) then @addRowArray row
489 else @addRowObject row
490
491 addRowArray: (row) ->
492 if not count = @columns.nodes.length
493 throw new Error "Must set column list before inserting arrays"
494 if row.length != count
495 message = "Wrong number of values in array, expected #{@columns.nodes}"
496 throw new Error message
497
498 params = for v in row
499 if v instanceof Node then v else new Parameter v
500
501 @data.addNode new Tuple params
502
503 addRowObject: (row) ->
504 ###
505 Add a row from an object. This will set the column list of the query if it
506 isn't set yet. If it `is` set, then only keys matching the existing column
507 list will be inserted.
508 ###
509 @addRowArray @columns.nodes.map(valOrDefault.bind(row))
510
511 valOrDefault = (field) ->
512 key = field.value
513 if @hasOwnProperty(key) then @[key] else CONST_NODES.DEFAULT
514
515 from: (query) ->
516 unless query instanceof Select
517 throw new Error "Can only insert from a SELECT"
518 @data = query
519
520
521class Delete extends Statement
522 ###
523 The root node of a DELETE query
524 ###
525 @prefix = 'DELETE '
526 @structure [
527 ['relations', RelationSet]
528 ['where', Where]
529 ['orderBy', OrderBy]
530 ['limit', Limit]
531 ['returning', Returning]
532 ]
533
534 Returning.extend(@)
535
536 initialize: (opts) ->
537 @relations.addNode(toRelation(opts.table))
538
539class ComparableMixin
540 ###
541 A mixin that adds comparison methods to a class. Each of these comparison
542 methods will yield a new AST node comparing the invocant to the argument.
543 ###
544 eq: (other) ->
545 ### ``this = other`` ###
546 @compare '=', other
547 ne: (other) ->
548 ### ``this != other`` ###
549 @compare '!=', other
550 gt: (other) ->
551 ### ``this > other`` ###
552 @compare '>', other
553 lt: (other) ->
554 ### ``this < other`` ###
555 @compare '<', other
556 lte: (other) ->
557 ### ``this <= other`` ###
558 @compare '<=', other
559 gte: (other) ->
560 ### ``this >= other`` ###
561 @compare '>=', other
562 like: (other) ->
563 @compare 'LIKE', other
564 is: (other) ->
565 @compare 'IS', other ? CONST_NODES.NULL
566 isNot: (other) ->
567 @compare 'IS NOT', other ? CONST_NODES.NULL
568 in: (other) ->
569 @compare 'IN', other
570 notIn: (other) ->
571 @compare 'NOT IN', other
572 compare: (op, other) ->
573 ### ``this op other`` **DANGER** `op` is **NOT** escaped! ###
574 new Binary @, op, toParam other
575
576for k, v of ComparableMixin::
577 TextNode::[k] = v
578 SqlFunction::[k] = v
579 SqlFunction.Alias::[k] = v
580 Column::[k] = v
581 Column.Alias::[k] = v
582 Tuple::[k] = v
583
584toParam = (it) ->
585 ###
586 Return a Node that can be used as a parameter.
587
588 * :class:`queries/select::SelectQuery` instances will be treated as
589 un-named sub queries,
590 * Node instances will be returned unchanged.
591 * Arrays will be turned into a :class:`nodes::Tuple` instance.
592
593 All other types will be wrapped in a :class:`nodes::Parameter` instance.
594 ###
595 SelectQuery = require './queries/select'
596 if it?.constructor is SelectQuery then new Tuple([it.q])
597 else if it instanceof Node then it
598 else if Array.isArray it then new Tuple(it.map toParam)
599 else new Parameter it
600
601toRelation = (it) ->
602 ###
603 Transform ``it`` into a :class:`nodes::Relation` instance.
604
605 This accepts `strings, `Relation`` and ``Alias`` instances, and objects with
606 a single key-value pair, which will be turned into an ``Alias`` instance.
607
608 Examples::
609
610 toRelation('table1') == new Relation('table1')
611 toRelation(t1: 'table1') == new Alias(new Relation('table1'), 't1')
612
613 **Throws Errors** if the input is not valid.
614 ###
615 switch it.constructor
616 when Relation, Relation.Alias, SqlFunction, SqlFunction.Alias then it
617 when String then new Relation it
618 when Object
619 if alias = getAlias it
620 toRelation(it[alias]).as(alias)
621 else
622 throw new Error "Can't make relation out of #{it}"
623 else
624 throw new Error "Can't make relation out of #{it}"
625
626
627toField = (it) ->
628 if typeof it is 'string'
629 new Field it
630 else if it instanceof Field
631 it
632 else
633 throw new Error "Can't make a field out of #{it}"
634
635
636toColumn = (relation, field) ->
637 ###
638 Create a new :class:`nodes::Column` instance.
639
640 The first argument is optional and specifies a table (or alias) name.
641 Alternatively, you can specify the relation name and field with a single
642 dot-separated string::
643
644 toColumn('departments.name') == toColumn('departments', 'name')
645
646 Either argument can be an pre-constructed node object (of the correct type).
647 ###
648 if field?
649 return new Column(toRelation(relation), toField(field))
650 else if typeof relation is 'string'
651 parts = relation.split('.')
652 if parts.length is 2
653 return new Column(toRelation(parts[0]), toField(parts[1]))
654 throw new Error("Can't make projection from object: #{relation}")
655
656toProjection = toColumn
657
658sqlFunction = (name, args) ->
659 ###
660 Create a new SQL function call node. For example::
661
662 count = g.sqlFunction('count', [g.text('*')])
663
664 ###
665 new SqlFunction name, new Tuple(args.map(toParam))
666
667func = (name) ->
668 ###
669 Create a factory for calling the given SQL function. Example::
670
671 count = g.func('count')
672 count(g.text('*'))
673
674 The returned factory accepts any number of parameters::
675
676 substringIndex = g.func('SUBSTRING_INDEX')
677 substringIndex(g.text('mycol'), '-', 1) # SUBSTRING_INDEX(mycol, '-', 1)
678 ###
679 (args...) ->
680 sqlFunction name, args
681
682getAlias = (o) ->
683 ###
684 Check if ``o`` is an object literal representing an alias, and return the
685 alias name if it is.
686 ###
687 if 'object' == typeof o
688 keys = Object.keys(o)
689 return keys[0] if keys.length == 1
690 return null
691
692text = (rawSQL, bindVals) ->
693 ###
694 Construct a node with a raw SQL string and (optionally) parameters. Useful for
695 when you want to construct a query that is difficult or impossible with the
696 normal APIs. [#]_
697
698 To use bound parameters in the SQL string, use ``$`` prefixed names, and
699 pass a ``bindVals`` argument with corresponding property names. For example,
700 :meth:`~queries/sud::SUDQuery.where` doesn't (currently) support the SQL
701 ``BETWEEN`` operator, but if you needed it, you could use ``text``::
702
703 function peopleInWeightRange (min, max, callback) {
704 return select('people')
705 .where(text("weight BETWEEN $min AND $max", {min: min, max: max}))
706 .execute(callback)
707 }
708
709 Because javascript doesn't distinguish between array indexing and property
710 access, it can be more clear to use numbered parameters for such short
711 snippets::
712
713 function peopleInWeightRange (min, max, callback) {
714 return select('people')
715 .where(text("weight BETWEEN $0 AND $1", [min, max]))
716 .execute(callback)
717 }
718
719 If you find yourself using this function often, please consider opening an
720 issue on `Github <https://github.com/BetSmartMedia/gesundheit>`_ with details
721 on your use case so gesundheit can support it more elegantly.
722 ###
723 new TextNode(rawSQL, bindVals)
724
725binaryOp = (left, op, right) ->
726 ###
727 Create a new :class:`nodes::Binary` node::
728
729 binaryOp('hstore_column', '->', toParam(y))
730 # hstore_column -> ?
731
732 This is for special cases, normally you want to use the methods from
733 :class:`nodes::ComparableMixin`.
734 ###
735 new Binary left, op, right
736
737class Prefixed extends ValueNode
738 constructor: (@prefix, @node) ->
739 compile: -> @prefix + @node.compile.apply(@node, arguments)
740
741exists = (subquery) ->
742 ### Create an ``EXISTS (<subquery>)`` node for `where` ###
743 new Prefixed('EXISTS ', new Tuple([subquery.q or subquery]))
744
745notExists = (subquery) ->
746 ### Create a ``NOT EXISTS (<subquery>)`` node for `where` ###
747 new Prefixed('NOT EXISTS ', new Tuple([subquery.q or subquery]))
748
749tuple = (input) ->
750 ###
751 Create a new Tuple from an array of nodes. Any item in the array that is
752 **not** an `instanceof Node` will be turned into a parameter with
753 :func:`nodes::toParam`.
754 ###
755 new Tuple input.map(toParam)
756
757module.exports = {
758 CONST_NODES
759 JOIN_TYPES
760
761 binaryOp
762 exists
763 func
764 getAlias
765 notExists
766 sqlFunction
767 text
768 toField
769 toParam
770 toColumn
771 toProjection
772 toRelation
773 tuple
774
775 Node
776 ValueNode
777 IntegerNode
778 Identifier
779 JoinType
780 NodeSet
781 FixedNodeSet
782 Statement
783 ParenthesizedNodeSet
784 TextNode
785 SqlFunction
786 Parameter
787 Relation
788 Field
789 Column
790 Limit
791 Offset
792 Binary
793 Tuple
794 ColumnSet
795 Returning
796 Distinct
797 SelectColumnSet
798 RelationSet
799 Join
800 Where
801 Or
802 And
803 GroupBy
804 OrderBy
805 Ordering
806 Select
807 Update
808 Insert
809 Delete
810 ComparableMixin
811}
812
813copy = (it) ->
814 # Return a deep copy of ``it``.
815 if not it then return it
816 switch it.constructor
817 when String, Number, Boolean then it
818 when Array then it.map copy
819 when Object
820 c = {}
821 for k, v of it
822 c[k] = copy v
823 else
824 if it.copy? then it.copy()
825 else throw new Error "Don't know how to copy #{it}"