eno 0.4 → 0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +50 -0
- data/CHANGELOG.md +13 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +23 -0
- data/LICENSE +21 -0
- data/README.md +154 -5
- data/eno.gemspec +22 -0
- data/examples/basic.rb +0 -0
- data/lib/eno.rb +7 -476
- data/lib/eno/expressions.rb +555 -0
- data/lib/eno/pg.rb +25 -0
- data/lib/eno/query.rb +72 -0
- data/lib/eno/sql.rb +151 -0
- data/lib/eno/version.rb +1 -1
- data/test/adapters/test_pg.rb +32 -0
- data/test/ext.rb +11 -0
- data/test/test.rb +3 -0
- data/test/test_clauses.rb +346 -0
- data/test/test_expressions.rb +144 -0
- data/test/test_query.rb +321 -0
- metadata +47 -3
@@ -0,0 +1,555 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
export :Expression,
|
4
|
+
|
5
|
+
:Alias,
|
6
|
+
:Case,
|
7
|
+
:Cast,
|
8
|
+
:CastShorthand,
|
9
|
+
:Desc,
|
10
|
+
:FunctionCall,
|
11
|
+
:Identifier,
|
12
|
+
:In,
|
13
|
+
:IsNotNull,
|
14
|
+
:IsNull,
|
15
|
+
:Join,
|
16
|
+
:Literal,
|
17
|
+
:Operator,
|
18
|
+
:Over,
|
19
|
+
:Not,
|
20
|
+
:NotIn,
|
21
|
+
:WindowExpression,
|
22
|
+
|
23
|
+
:Combination,
|
24
|
+
:From,
|
25
|
+
:Limit,
|
26
|
+
:OrderBy,
|
27
|
+
:Select,
|
28
|
+
:Where,
|
29
|
+
:Window,
|
30
|
+
:With
|
31
|
+
|
32
|
+
Query = import('./query')
|
33
|
+
|
34
|
+
class Expression
|
35
|
+
attr_reader :members, :props
|
36
|
+
|
37
|
+
def initialize(*members, **props)
|
38
|
+
@members = members
|
39
|
+
@props = props
|
40
|
+
end
|
41
|
+
|
42
|
+
def as(sym = nil, &block)
|
43
|
+
if sym
|
44
|
+
Alias.new(self, sym)
|
45
|
+
else
|
46
|
+
Alias.new(self, Query::Query.new(&block))
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def desc
|
51
|
+
Desc.new(self)
|
52
|
+
end
|
53
|
+
|
54
|
+
def over(sym = nil, &block)
|
55
|
+
Over.new(self, sym || WindowExpression.new(&block))
|
56
|
+
end
|
57
|
+
|
58
|
+
S_EQ = '='
|
59
|
+
S_TILDE = '~'
|
60
|
+
S_NEQ = '<>'
|
61
|
+
S_LT = '<'
|
62
|
+
S_GT = '>'
|
63
|
+
S_LTE = '<='
|
64
|
+
S_GTE = '>='
|
65
|
+
S_AND = 'and'
|
66
|
+
S_OR = 'or'
|
67
|
+
S_PLUS = '+'
|
68
|
+
S_MINUS = '-'
|
69
|
+
S_MUL = '*'
|
70
|
+
S_DIV = '/'
|
71
|
+
S_MOD = '%'
|
72
|
+
|
73
|
+
def ==(expr2)
|
74
|
+
Operator.new(S_EQ, self, expr2)
|
75
|
+
end
|
76
|
+
|
77
|
+
def =~(expr2)
|
78
|
+
Operator.new(S_TILDE, self, expr2)
|
79
|
+
end
|
80
|
+
|
81
|
+
def !=(expr2)
|
82
|
+
Operator.new(S_NEQ, self, expr2)
|
83
|
+
end
|
84
|
+
|
85
|
+
def <(expr2)
|
86
|
+
Operator.new(S_LT, self, expr2)
|
87
|
+
end
|
88
|
+
|
89
|
+
def >(expr2)
|
90
|
+
Operator.new(S_GT, self, expr2)
|
91
|
+
end
|
92
|
+
|
93
|
+
def <=(expr2)
|
94
|
+
Operator.new(S_LTE, self, expr2)
|
95
|
+
end
|
96
|
+
|
97
|
+
def >=(expr2)
|
98
|
+
Operator.new(S_GTE, self, expr2)
|
99
|
+
end
|
100
|
+
|
101
|
+
def &(expr2)
|
102
|
+
Operator.new(S_AND, self, expr2)
|
103
|
+
end
|
104
|
+
|
105
|
+
def |(expr2)
|
106
|
+
Operator.new(S_OR, self, expr2)
|
107
|
+
end
|
108
|
+
|
109
|
+
def +(expr2)
|
110
|
+
Operator.new(S_PLUS, self, expr2)
|
111
|
+
end
|
112
|
+
|
113
|
+
def -(expr2)
|
114
|
+
Operator.new(S_MINUS, self, expr2)
|
115
|
+
end
|
116
|
+
|
117
|
+
def *(expr2)
|
118
|
+
Operator.new(S_MUL, self, expr2)
|
119
|
+
end
|
120
|
+
|
121
|
+
def /(expr2)
|
122
|
+
Operator.new(S_DIV, self, expr2)
|
123
|
+
end
|
124
|
+
|
125
|
+
def %(expr2)
|
126
|
+
Operator.new(S_MOD, self, expr2)
|
127
|
+
end
|
128
|
+
|
129
|
+
def ^(expr2)
|
130
|
+
CastShorthand.new(self, expr2)
|
131
|
+
end
|
132
|
+
|
133
|
+
# not
|
134
|
+
def !@
|
135
|
+
Not.new(self)
|
136
|
+
end
|
137
|
+
|
138
|
+
def null?
|
139
|
+
IsNull.new(self)
|
140
|
+
end
|
141
|
+
|
142
|
+
def not_null?
|
143
|
+
IsNotNull.new(self)
|
144
|
+
end
|
145
|
+
|
146
|
+
def join(sym, **props)
|
147
|
+
Join.new(self, sym, **props)
|
148
|
+
end
|
149
|
+
|
150
|
+
def inner_join(sym, **props)
|
151
|
+
join(sym, props.merge(type: :inner))
|
152
|
+
end
|
153
|
+
|
154
|
+
def cast(sym)
|
155
|
+
Cast.new(self, sym)
|
156
|
+
end
|
157
|
+
|
158
|
+
def in(*args)
|
159
|
+
In.new(self, *args)
|
160
|
+
end
|
161
|
+
|
162
|
+
def not_in(*args)
|
163
|
+
NotIn.new(self, *args)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
############################################################
|
168
|
+
|
169
|
+
S_COMMA = ', '
|
170
|
+
|
171
|
+
class Alias < Expression
|
172
|
+
S_AS = '%s as %s'
|
173
|
+
def to_sql(sql)
|
174
|
+
S_AS % [sql.quote(@members[0]), sql.quote(@members[1])]
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
class Case < Expression
|
179
|
+
def initialize(conditions)
|
180
|
+
@props = conditions
|
181
|
+
end
|
182
|
+
|
183
|
+
S_WHEN = 'when %s then %s'
|
184
|
+
S_ELSE = 'else %s'
|
185
|
+
S_CASE = 'case %s end'
|
186
|
+
S_SPACE = ' '
|
187
|
+
|
188
|
+
def to_sql(sql)
|
189
|
+
conditions = @props.inject([]) { |a, (k, v)|
|
190
|
+
if k.is_a?(Symbol) && k == :default
|
191
|
+
a
|
192
|
+
else
|
193
|
+
a << (S_WHEN % [sql.quote(k), sql.quote(v)])
|
194
|
+
end
|
195
|
+
}
|
196
|
+
if default = @props[:default]
|
197
|
+
conditions << (S_ELSE % sql.quote(default))
|
198
|
+
end
|
199
|
+
|
200
|
+
S_CASE % conditions.join(S_SPACE)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
class Cast < Expression
|
205
|
+
S_CAST = 'cast (%s as %s)'
|
206
|
+
|
207
|
+
def to_sql(sql)
|
208
|
+
S_CAST % [sql.quote(@members[0]), sql.quote(@members[1])]
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
class CastShorthand < Expression
|
213
|
+
S_CAST = '%s::%s'
|
214
|
+
|
215
|
+
def to_sql(sql)
|
216
|
+
S_CAST % [sql.quote(@members[0]), sql.quote(@members[1])]
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
class Desc < Expression
|
221
|
+
S_DESC = '%s desc'
|
222
|
+
|
223
|
+
def to_sql(sql)
|
224
|
+
S_DESC % sql.quote(@members[0])
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
class FunctionCall < Expression
|
229
|
+
S_FUN_NO_ARGS = '%s()'
|
230
|
+
S_FUN = '%s(%s)'
|
231
|
+
|
232
|
+
def to_sql(sql)
|
233
|
+
fun = @members[0]
|
234
|
+
if @members.size == 2 && Identifier === @members.last && @members.last._empty_placeholder?
|
235
|
+
S_FUN_NO_ARGS % fun
|
236
|
+
else
|
237
|
+
S_FUN % [
|
238
|
+
fun,
|
239
|
+
@members[1..-1].map { |a| sql.quote(a) }.join(S_COMMA)
|
240
|
+
]
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
class Identifier < Expression
|
246
|
+
def to_sql(sql)
|
247
|
+
sql.quote(@members[0].to_sym)
|
248
|
+
end
|
249
|
+
|
250
|
+
def method_missing(sym)
|
251
|
+
super if sym == :to_hash
|
252
|
+
Identifier.new("#{@members[0]}.#{sym}")
|
253
|
+
end
|
254
|
+
|
255
|
+
def _empty_placeholder?
|
256
|
+
m = @members[0]
|
257
|
+
Symbol === m && m == :_
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
class In < Expression
|
262
|
+
S_IN = '%s in (%s)'
|
263
|
+
|
264
|
+
def to_sql(sql)
|
265
|
+
S_IN % [
|
266
|
+
sql.quote(@members[0]),
|
267
|
+
@members[1..-1].map { |m| sql.quote(m) }.join(S_COMMA)
|
268
|
+
]
|
269
|
+
end
|
270
|
+
|
271
|
+
def !@
|
272
|
+
NotIn.new(*@members)
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
class IsNotNull < Expression
|
277
|
+
S_NOT_NULL = '(%s is not null)'
|
278
|
+
def to_sql(sql)
|
279
|
+
S_NOT_NULL % sql.quote(@members[0])
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
class IsNull < Expression
|
284
|
+
S_NULL = '(%s is null)'
|
285
|
+
|
286
|
+
def to_sql(sql)
|
287
|
+
S_NULL % sql.quote(@members[0])
|
288
|
+
end
|
289
|
+
|
290
|
+
def !@
|
291
|
+
IsNotNull.new(@members[0])
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
class Join < Expression
|
296
|
+
H_JOIN_TYPES = {
|
297
|
+
nil: 'join',
|
298
|
+
inner: 'inner join',
|
299
|
+
outer: 'outer join'
|
300
|
+
}
|
301
|
+
|
302
|
+
S_JOIN = '%s %s %s %s'
|
303
|
+
S_ON = 'on %s'
|
304
|
+
S_USING = 'using (%s)'
|
305
|
+
|
306
|
+
def to_sql(sql)
|
307
|
+
(S_JOIN % [
|
308
|
+
sql.quote(@members[0]),
|
309
|
+
H_JOIN_TYPES[@props[:type]],
|
310
|
+
sql.quote(@members[1]),
|
311
|
+
condition_sql(sql)
|
312
|
+
]).strip
|
313
|
+
end
|
314
|
+
|
315
|
+
def condition_sql(sql)
|
316
|
+
if @props[:on]
|
317
|
+
S_ON % sql.quote(@props[:on])
|
318
|
+
elsif using_fields = @props[:using]
|
319
|
+
fields = using_fields.is_a?(Array) ? using_fields : [using_fields]
|
320
|
+
S_USING % fields.map { |f| sql.quote(f) }.join(S_COMMA)
|
321
|
+
else
|
322
|
+
nil
|
323
|
+
end
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
class Literal < Expression
|
328
|
+
def to_sql(sql)
|
329
|
+
sql.quote(@members[0])
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
class Operator < Expression
|
334
|
+
attr_reader :op
|
335
|
+
|
336
|
+
def initialize(op, *members, **props)
|
337
|
+
if Operator === members[0] && op == members[0].op
|
338
|
+
members = members[0].members + members[1..-1]
|
339
|
+
end
|
340
|
+
if Operator === members.last && op == members.last.op
|
341
|
+
members = members[0..-2] + members.last.members
|
342
|
+
end
|
343
|
+
|
344
|
+
super(*members, **props)
|
345
|
+
@op = op
|
346
|
+
end
|
347
|
+
|
348
|
+
S_OP = ' %s '
|
349
|
+
S_OP_EXPR = '(%s)'
|
350
|
+
|
351
|
+
def to_sql(sql)
|
352
|
+
op_s = S_OP % @op
|
353
|
+
S_OP_EXPR % @members.map { |m| sql.quote(m) }.join(op_s)
|
354
|
+
end
|
355
|
+
|
356
|
+
INVERSE_OP = {
|
357
|
+
'=' => '<>',
|
358
|
+
'<>' => '=',
|
359
|
+
'<' => '>=',
|
360
|
+
'>' => '<=',
|
361
|
+
'<=' => '>',
|
362
|
+
'>=' => '<'
|
363
|
+
}
|
364
|
+
|
365
|
+
def !@
|
366
|
+
inverse = INVERSE_OP[@op]
|
367
|
+
inverse ? Operator.new(inverse, *@members) : super
|
368
|
+
end
|
369
|
+
end
|
370
|
+
|
371
|
+
class Over < Expression
|
372
|
+
S_OVER = '%s over %s'
|
373
|
+
|
374
|
+
def to_sql(sql)
|
375
|
+
S_OVER % [sql.quote(@members[0]), sql.quote(@members[1])]
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
class Not < Expression
|
380
|
+
S_NOT = '(not %s)'
|
381
|
+
|
382
|
+
def to_sql(sql)
|
383
|
+
S_NOT % sql.quote(@members[0])
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
class NotIn < Expression
|
388
|
+
S_NOT_IN = '%s not in (%s)'
|
389
|
+
|
390
|
+
def to_sql(sql)
|
391
|
+
S_NOT_IN % [
|
392
|
+
sql.quote(@members[0]),
|
393
|
+
@members[1..-1].map { |m| sql.quote(m) }.join(S_COMMA)
|
394
|
+
]
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
class WindowExpression < Expression
|
399
|
+
def initialize(&block)
|
400
|
+
instance_eval(&block)
|
401
|
+
end
|
402
|
+
|
403
|
+
def partition_by(*args)
|
404
|
+
@partition_by = args
|
405
|
+
end
|
406
|
+
|
407
|
+
def order_by(*args)
|
408
|
+
@order_by = args
|
409
|
+
end
|
410
|
+
|
411
|
+
S_UNBOUNDED = 'between unbounded preceding and unbounded following'
|
412
|
+
S_WINDOW = '(%s)'
|
413
|
+
S_PARTITION_BY = 'partition by %s '
|
414
|
+
S_ORDER_BY = 'order by %s '
|
415
|
+
S_RANGE = 'range %s '
|
416
|
+
|
417
|
+
def range_unbounded
|
418
|
+
@range = S_UNBOUNDED
|
419
|
+
end
|
420
|
+
|
421
|
+
def to_sql(sql)
|
422
|
+
S_WINDOW % [
|
423
|
+
_partition_by_clause(sql),
|
424
|
+
_order_by_clause(sql),
|
425
|
+
_range_clause(sql)
|
426
|
+
].join.strip
|
427
|
+
end
|
428
|
+
|
429
|
+
def _partition_by_clause(sql)
|
430
|
+
return nil unless @partition_by
|
431
|
+
S_PARTITION_BY % @partition_by.map { |e| sql.quote(e) }.join(S_COMMA)
|
432
|
+
end
|
433
|
+
|
434
|
+
def _order_by_clause(sql)
|
435
|
+
return nil unless @order_by
|
436
|
+
S_ORDER_BY % @order_by.map { |e| sql.quote(e) }.join(S_COMMA)
|
437
|
+
end
|
438
|
+
|
439
|
+
def _range_clause(sql)
|
440
|
+
return nil unless @range
|
441
|
+
S_RANGE % @range
|
442
|
+
end
|
443
|
+
|
444
|
+
def method_missing(sym)
|
445
|
+
super if sym == :to_hash
|
446
|
+
Identifier.new(sym)
|
447
|
+
end
|
448
|
+
end
|
449
|
+
|
450
|
+
############################################################
|
451
|
+
|
452
|
+
class Combination < Expression
|
453
|
+
S_COMBINATION = ' %s '
|
454
|
+
S_COMBINATION_ALL = ' %s all '
|
455
|
+
|
456
|
+
def to_sql(sql)
|
457
|
+
union = (@props[:all] ? S_COMBINATION_ALL : S_COMBINATION) % @props[:kind]
|
458
|
+
@members.map { |m| sql.quote(m) }.join(union)
|
459
|
+
end
|
460
|
+
end
|
461
|
+
|
462
|
+
class From < Expression
|
463
|
+
S_FROM = 'from %s'
|
464
|
+
S_T1 = '%s t1'
|
465
|
+
S_ALIAS = '%s %s'
|
466
|
+
|
467
|
+
def to_sql(sql)
|
468
|
+
S_FROM % @members.map { |m| member_sql(m, sql) }.join(S_COMMA)
|
469
|
+
end
|
470
|
+
|
471
|
+
def member_sql(member, sql)
|
472
|
+
if Query::Query === member
|
473
|
+
S_T1 % sql.quote(member)
|
474
|
+
elsif Alias === member && Query::Query === member.members[0]
|
475
|
+
S_ALIAS % [sql.quote(member.members[0]), sql.quote(member.members[1])]
|
476
|
+
else
|
477
|
+
sql.quote(member)
|
478
|
+
end
|
479
|
+
end
|
480
|
+
end
|
481
|
+
|
482
|
+
class Limit < Expression
|
483
|
+
S_LIMIT = 'limit %d'
|
484
|
+
|
485
|
+
def to_sql(sql)
|
486
|
+
S_LIMIT % @members[0]
|
487
|
+
end
|
488
|
+
end
|
489
|
+
|
490
|
+
class OrderBy < Expression
|
491
|
+
S_ORDER_BY = 'order by %s'
|
492
|
+
|
493
|
+
def to_sql(sql)
|
494
|
+
S_ORDER_BY % @members.map { |e| sql.quote(e) }.join(S_COMMA)
|
495
|
+
end
|
496
|
+
end
|
497
|
+
|
498
|
+
class Select < Expression
|
499
|
+
S_SELECT = 'select %s%s'
|
500
|
+
S_DISTINCT = 'distinct '
|
501
|
+
S_DISTINCT_ON = 'distinct on (%s) '
|
502
|
+
S_DISTINCT_ON_SINGLE = 'distinct on %s '
|
503
|
+
|
504
|
+
def to_sql(sql)
|
505
|
+
S_SELECT % [
|
506
|
+
distinct_clause(sql), @members.map { |e| sql.quote(e) }.join(S_COMMA)
|
507
|
+
]
|
508
|
+
end
|
509
|
+
|
510
|
+
def distinct_clause(sql)
|
511
|
+
case (on = @props[:distinct])
|
512
|
+
when nil
|
513
|
+
nil
|
514
|
+
when true
|
515
|
+
S_DISTINCT
|
516
|
+
when Array
|
517
|
+
S_DISTINCT_ON % on.map { |e| sql.quote(e) }.join(S_COMMA)
|
518
|
+
else
|
519
|
+
S_DISTINCT_ON_SINGLE % sql.quote(on)
|
520
|
+
end
|
521
|
+
end
|
522
|
+
end
|
523
|
+
|
524
|
+
class Where < Expression
|
525
|
+
S_WHERE = 'where %s'
|
526
|
+
S_AND = ' and '
|
527
|
+
|
528
|
+
def to_sql(sql)
|
529
|
+
S_WHERE % @members.map { |e| sql.quote(e) }.join(S_AND)
|
530
|
+
end
|
531
|
+
end
|
532
|
+
|
533
|
+
class Window < Expression
|
534
|
+
def initialize(sym, &block)
|
535
|
+
super(sym)
|
536
|
+
@block = block
|
537
|
+
end
|
538
|
+
|
539
|
+
S_WINDOW = 'window %s as %s'
|
540
|
+
|
541
|
+
def to_sql(sql)
|
542
|
+
S_WINDOW % [
|
543
|
+
sql.quote(@members.first),
|
544
|
+
WindowExpression.new(&@block).to_sql(sql)
|
545
|
+
]
|
546
|
+
end
|
547
|
+
end
|
548
|
+
|
549
|
+
class With < Expression
|
550
|
+
S_WITH = 'with %s'
|
551
|
+
|
552
|
+
def to_sql(sql)
|
553
|
+
S_WITH % @members.map { |e| sql.quote(e) }.join(S_COMMA)
|
554
|
+
end
|
555
|
+
end
|