eno 0.4
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 +7 -0
- data/CHANGELOG.md +13 -0
- data/README.md +164 -0
- data/lib/eno.rb +486 -0
- data/lib/eno/version.rb +5 -0
- metadata +66 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: eb697b829ee2858e69995a4485607463b4b2c0740cb78f2c984bd25ffa188108
|
4
|
+
data.tar.gz: 40850710021d688e6a916e0f8c21ee5511990012f015c8c9541e9e700f815b59
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: cf6d28da3a66cc2db0121914f2ae29c578ec67f9d5ea72bc9acb9fc0d0c85215145f424092095b73abc6d68b094661ef5a73a318f84d3a70d2dba91c1f75a4b7
|
7
|
+
data.tar.gz: dec76ece5872f0f4688d1654ff2894809680e870f728165269516167101828be9e8379f77ef643f137604f4cf082a73d03c88ce0a05be34248350f32b40081b7
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
0.4 2019-01-21
|
2
|
+
--------------
|
3
|
+
|
4
|
+
* Implement query mutation
|
5
|
+
* Can now use `!` as `not` operator
|
6
|
+
* Clauses as real expressions
|
7
|
+
* Implement context injection
|
8
|
+
* Refactor and simplify code
|
9
|
+
|
10
|
+
0.1 2019-01-16
|
11
|
+
--------------
|
12
|
+
|
13
|
+
* First working version
|
data/README.md
ADDED
@@ -0,0 +1,164 @@
|
|
1
|
+
# Eno is Not an ORM
|
2
|
+
|
3
|
+
[INSTALL](#installing-eno) |
|
4
|
+
[TUTORIAL](#getting-started) |
|
5
|
+
[EXAMPLES](examples)
|
6
|
+
|
7
|
+
## What is Eno?
|
8
|
+
|
9
|
+
Eno is an experimental Ruby gem for working with SQL databases. Eno provides
|
10
|
+
tools for writing SQL queries using plain Ruby and specifically for querying
|
11
|
+
PostgreSQL and SQLite databases.
|
12
|
+
|
13
|
+
Eno provides the following features:
|
14
|
+
|
15
|
+
- Compose `SELECT` statements using plain Ruby syntax
|
16
|
+
- Create arbitrarily complex `WHERE` clauses
|
17
|
+
- Support for common table expressions (CTE) and joins
|
18
|
+
- Compose queries and sub-queries
|
19
|
+
- Create parametric queries using context variables
|
20
|
+
- Reusable queries can be further refined and mutated
|
21
|
+
|
22
|
+
## What is it good for?
|
23
|
+
|
24
|
+
So why would anyone want to compose queries in Ruby instead of in plain SQL?
|
25
|
+
That's actually a very good question. Libraries like ActiveRecord and Sequel
|
26
|
+
already provide tools for querying relational databases. There's usage patterns
|
27
|
+
like ActiveRecord's `where`:
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
Client.where(order_count: [1, 3, 5])
|
31
|
+
```
|
32
|
+
|
33
|
+
And Sequel is a bit more flexible:
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
Client.where { order_count > 10 }
|
37
|
+
```
|
38
|
+
|
39
|
+
But both stumble when it comes to putting together more complex queries.
|
40
|
+
ActiveRecord queries in particular aren't really composable, making it actually
|
41
|
+
easier to filter and manipulate records inside your app code than in your
|
42
|
+
database.
|
43
|
+
|
44
|
+
With both ActiveRecord and Sequel you'll need to eventually provide snippets of
|
45
|
+
literal SQL. This is time-consuming, prevents your queries from being composable
|
46
|
+
and makes it easy to expose your app to SQL injection.
|
47
|
+
|
48
|
+
## Installing eno
|
49
|
+
|
50
|
+
Using bundler:
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
gem 'eno'
|
54
|
+
```
|
55
|
+
|
56
|
+
Or manually:
|
57
|
+
|
58
|
+
```bash
|
59
|
+
$ gem install eno
|
60
|
+
```
|
61
|
+
|
62
|
+
## Getting started
|
63
|
+
|
64
|
+
To use eno in your code just require it:
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
require 'eno'
|
68
|
+
```
|
69
|
+
|
70
|
+
Alternatively, you can import it using [Modulation](https://github.com/digital-fabric/modulation):
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
Eno = import('eno')
|
74
|
+
```
|
75
|
+
|
76
|
+
## Putting together queries
|
77
|
+
|
78
|
+
Eno makes it easy to compose SQL queries using plain Ruby syntax. It takes care
|
79
|
+
of formatting table and column identifiers and literals, and allows you to
|
80
|
+
compose multiple queries into a single `SELECT` statement.
|
81
|
+
|
82
|
+
To compose a query use the `Kernel#Q` method, providing a block in which the
|
83
|
+
query is built:
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
Q {
|
87
|
+
select a, b
|
88
|
+
from c
|
89
|
+
}
|
90
|
+
```
|
91
|
+
|
92
|
+
To turn the query into SQL, use the `#to_sql` method:
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
Q {
|
96
|
+
select a, b
|
97
|
+
from c
|
98
|
+
}.to_sql #=> "select a, b from c"
|
99
|
+
```
|
100
|
+
|
101
|
+
## Using expressions
|
102
|
+
|
103
|
+
Once inside the query block, you can build arbitrarily complex expressions. You
|
104
|
+
can mix logical and arithmetic operators:
|
105
|
+
|
106
|
+
```ruby
|
107
|
+
Q { select (a + b) & (c * d) }.to_sql #=> select (a + b) and (c * d)
|
108
|
+
```
|
109
|
+
|
110
|
+
You can also use SQL functions:
|
111
|
+
|
112
|
+
```ruby
|
113
|
+
Q {
|
114
|
+
select user_id, max(score)
|
115
|
+
from exams
|
116
|
+
group_by user_id
|
117
|
+
}
|
118
|
+
```
|
119
|
+
|
120
|
+
## Hooking up Eno to your database
|
121
|
+
|
122
|
+
In and of itself, Eno is just an engine for building SQL queries. To actually
|
123
|
+
run your queries, you'll need to hook Eno to your database. Here's an example
|
124
|
+
of how to open a connection to a PostgreSQL database and then easily issue
|
125
|
+
queries to it:
|
126
|
+
|
127
|
+
```ruby
|
128
|
+
require 'pg'
|
129
|
+
|
130
|
+
DB = PG.connect(host: '/tmp', dbname: 'myapp', user: 'myuser')
|
131
|
+
def DB.q(**ctx, &block)
|
132
|
+
query(**ctx, &block).to_a
|
133
|
+
end
|
134
|
+
|
135
|
+
# issue a query
|
136
|
+
DB.q {
|
137
|
+
from users
|
138
|
+
select
|
139
|
+
}
|
140
|
+
```
|
141
|
+
|
142
|
+
Another way to issue queries is by defining methods on Eno::Query:
|
143
|
+
|
144
|
+
```ruby
|
145
|
+
def Eno::Query.each(**ctx, &block)
|
146
|
+
DB.query(to_sql(**ctx)).each(&block)
|
147
|
+
end
|
148
|
+
```
|
149
|
+
|
150
|
+
## Roadmap
|
151
|
+
|
152
|
+
Eno is intended as a complete solution for eventually expressing *any* SQL query
|
153
|
+
in Ruby (including `INSERT`, `UPDATE` and `DELETE` and `ALTER TABLE`
|
154
|
+
statements).
|
155
|
+
|
156
|
+
In the future, Eno could be used to manipulate queries in other ways:
|
157
|
+
|
158
|
+
- `EXPLAIN` your queries.
|
159
|
+
- Introspect different parts of a query (for example look at results of
|
160
|
+
subqueries or CTE's).
|
161
|
+
- Transform CTE's into subqueries (for example to overcome optimization
|
162
|
+
boundaries).
|
163
|
+
- Create views from queries.
|
164
|
+
- Compose data manipulation statements using `SELECT` subqueries.
|
data/lib/eno.rb
ADDED
@@ -0,0 +1,486 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'modulation/gem'
|
4
|
+
|
5
|
+
export :Query, :SQL, :Expression, :Identifier, :Alias
|
6
|
+
|
7
|
+
module ::Kernel
|
8
|
+
def Q(**ctx, &block)
|
9
|
+
Query.new(**ctx, &block)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class Expression
|
14
|
+
def self.quote(expr)
|
15
|
+
case expr
|
16
|
+
when Query
|
17
|
+
"(#{expr.to_sql.strip})"
|
18
|
+
when Expression
|
19
|
+
expr.to_sql
|
20
|
+
when Symbol
|
21
|
+
expr.to_s
|
22
|
+
when String
|
23
|
+
"'#{expr}'"
|
24
|
+
else
|
25
|
+
expr.inspect
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
attr_reader :members, :props
|
30
|
+
|
31
|
+
def initialize(*members, **props)
|
32
|
+
@members = members
|
33
|
+
@props = props
|
34
|
+
end
|
35
|
+
|
36
|
+
def as(sym = nil, &block)
|
37
|
+
if sym
|
38
|
+
Alias.new(self, sym)
|
39
|
+
else
|
40
|
+
Alias.new(self, Query.new(&block))
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def desc
|
45
|
+
Desc.new(self)
|
46
|
+
end
|
47
|
+
|
48
|
+
def over(sym = nil, &block)
|
49
|
+
Over.new(self, sym || WindowExpression.new(&block))
|
50
|
+
end
|
51
|
+
|
52
|
+
def ==(expr2)
|
53
|
+
Operator.new('=', self, expr2)
|
54
|
+
end
|
55
|
+
|
56
|
+
def !=(expr2)
|
57
|
+
Operator.new('<>', self, expr2)
|
58
|
+
end
|
59
|
+
|
60
|
+
def <(expr2)
|
61
|
+
Operator.new('<', self, expr2)
|
62
|
+
end
|
63
|
+
|
64
|
+
def >(expr2)
|
65
|
+
Operator.new('>', self, expr2)
|
66
|
+
end
|
67
|
+
|
68
|
+
def <=(expr2)
|
69
|
+
Operator.new('<=', self, expr2)
|
70
|
+
end
|
71
|
+
|
72
|
+
def >=(expr2)
|
73
|
+
Operator.new('>=', self, expr2)
|
74
|
+
end
|
75
|
+
|
76
|
+
def &(expr2)
|
77
|
+
Operator.new('and', self, expr2)
|
78
|
+
end
|
79
|
+
|
80
|
+
def |(expr2)
|
81
|
+
Operator.new('or', self, expr2)
|
82
|
+
end
|
83
|
+
|
84
|
+
def +(expr2)
|
85
|
+
Operator.new('+', self, expr2)
|
86
|
+
end
|
87
|
+
|
88
|
+
def -(expr2)
|
89
|
+
Operator.new('-', self, expr2)
|
90
|
+
end
|
91
|
+
|
92
|
+
def *(expr2)
|
93
|
+
Operator.new('*', self, expr2)
|
94
|
+
end
|
95
|
+
|
96
|
+
def /(expr2)
|
97
|
+
Operator.new('/', self, expr2)
|
98
|
+
end
|
99
|
+
|
100
|
+
def %(expr2)
|
101
|
+
Operator.new('%', self, expr2)
|
102
|
+
end
|
103
|
+
|
104
|
+
# not
|
105
|
+
def !@
|
106
|
+
Not.new(self)
|
107
|
+
end
|
108
|
+
|
109
|
+
def null?
|
110
|
+
IsNull.new(self)
|
111
|
+
end
|
112
|
+
|
113
|
+
def not_null?
|
114
|
+
IsNotNull.new(self)
|
115
|
+
end
|
116
|
+
|
117
|
+
def join(sym, **props)
|
118
|
+
Join.new(self, sym, **props)
|
119
|
+
end
|
120
|
+
|
121
|
+
def inner_join(sym, **props)
|
122
|
+
join(sym, props.merge(type: :inner))
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
class Operator < Expression
|
127
|
+
def initialize(*members, **props)
|
128
|
+
op = members[0]
|
129
|
+
if Operator === members[1] && op == members[1].op
|
130
|
+
members = [op] + members[1].members[1..-1] + members[2..-1]
|
131
|
+
end
|
132
|
+
if Operator === members[2] && op == members[2].op
|
133
|
+
members = members[0..1] + members[2].members[1..-1]
|
134
|
+
end
|
135
|
+
|
136
|
+
super(*members, **props)
|
137
|
+
end
|
138
|
+
|
139
|
+
def op
|
140
|
+
@members[0]
|
141
|
+
end
|
142
|
+
|
143
|
+
def to_sql
|
144
|
+
op = " #{@members[0]} "
|
145
|
+
"(%s)" % @members[1..-1].map { |m| Expression.quote(m) }.join(op)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
class Desc < Expression
|
150
|
+
def to_sql
|
151
|
+
"#{Expression.quote(@members[0])} desc"
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
class Over < Expression
|
156
|
+
def to_sql
|
157
|
+
"#{Expression.quote(@members[0])} over #{Expression.quote(@members[1])}"
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
class Not < Expression
|
162
|
+
def to_sql
|
163
|
+
"(not #{Expression.quote(@members[0])})"
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
class IsNull < Expression
|
168
|
+
def to_sql
|
169
|
+
"(#{Expression.quote(@members[0])} is null)"
|
170
|
+
end
|
171
|
+
|
172
|
+
def !@
|
173
|
+
IsNotNull.new(members[0])
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
class IsNotNull < Expression
|
178
|
+
def to_sql
|
179
|
+
"(#{Expression.quote(@members[0])} is not null)"
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
class WindowExpression < Expression
|
184
|
+
def initialize(&block)
|
185
|
+
instance_eval(&block)
|
186
|
+
end
|
187
|
+
|
188
|
+
def partition_by(*args)
|
189
|
+
@partition_by = args
|
190
|
+
end
|
191
|
+
|
192
|
+
def order_by(*args)
|
193
|
+
@order_by = args
|
194
|
+
end
|
195
|
+
|
196
|
+
def range_unbounded
|
197
|
+
@range = 'between unbounded preceding and unbounded following'
|
198
|
+
end
|
199
|
+
|
200
|
+
def to_sql
|
201
|
+
"(%s)" % [
|
202
|
+
_partition_by_clause,
|
203
|
+
_order_by_clause,
|
204
|
+
_range_clause
|
205
|
+
].join.strip
|
206
|
+
end
|
207
|
+
|
208
|
+
def _partition_by_clause
|
209
|
+
return nil unless @partition_by
|
210
|
+
"partition by %s " % @partition_by.map { |e| Expression.quote(e) }.join(', ')
|
211
|
+
end
|
212
|
+
|
213
|
+
def _order_by_clause
|
214
|
+
return nil unless @order_by
|
215
|
+
"order by %s " % @order_by.map { |e| Expression.quote(e) }.join(', ')
|
216
|
+
end
|
217
|
+
|
218
|
+
def _range_clause
|
219
|
+
return nil unless @range
|
220
|
+
"range #{@range} "
|
221
|
+
end
|
222
|
+
|
223
|
+
def method_missing(sym)
|
224
|
+
super if sym == :to_hash
|
225
|
+
Identifier.new(sym)
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
class QuotedExpression < Expression
|
230
|
+
def to_sql
|
231
|
+
Expression.quote(@members[0])
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
class Identifier < Expression
|
236
|
+
def to_sql
|
237
|
+
@members[0].to_s
|
238
|
+
end
|
239
|
+
|
240
|
+
def method_missing(sym)
|
241
|
+
super if sym == :to_hash
|
242
|
+
Identifier.new("#{@members[0]}.#{sym}")
|
243
|
+
end
|
244
|
+
|
245
|
+
def _empty_placeholder?
|
246
|
+
m = @members[0]
|
247
|
+
Symbol === m && m == :_
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
class Alias < Expression
|
252
|
+
def to_sql
|
253
|
+
"#{Expression.quote(@members[0])} as #{Expression.quote(@members[1])}"
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
class FunctionCall < Expression
|
258
|
+
def to_sql
|
259
|
+
fun = @members[0]
|
260
|
+
if @members.size == 2 && Identifier === @members.last && @members.last._empty_placeholder?
|
261
|
+
"#{fun}()"
|
262
|
+
else
|
263
|
+
"#{fun}(#{@members[1..-1].map { |a| Expression.quote(a) }.join(', ')})"
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
class Join < Expression
|
269
|
+
H_JOIN_TYPES = {
|
270
|
+
nil: 'join',
|
271
|
+
inner: 'inner join',
|
272
|
+
outer: 'outer join'
|
273
|
+
}
|
274
|
+
|
275
|
+
def to_sql
|
276
|
+
("%s %s %s %s" % [
|
277
|
+
Expression.quote(@members[0]),
|
278
|
+
H_JOIN_TYPES[@props[:type]],
|
279
|
+
Expression.quote(@members[1]),
|
280
|
+
condition_sql
|
281
|
+
]).strip
|
282
|
+
end
|
283
|
+
|
284
|
+
def condition_sql
|
285
|
+
if @props[:on]
|
286
|
+
'on %s' % Expression.quote(@props[:on])
|
287
|
+
elsif using_fields = @props[:using]
|
288
|
+
fields = using_fields.is_a?(Array) ? using_fields : [using_fields]
|
289
|
+
'using (%s)' % fields.map { |f| Expression.quote(f) }.join(', ')
|
290
|
+
else
|
291
|
+
nil
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
class From < Expression
|
297
|
+
def to_sql
|
298
|
+
"from %s" % @members.map { |m| member_sql(m) }.join(', ')
|
299
|
+
end
|
300
|
+
|
301
|
+
def member_sql(member)
|
302
|
+
if Query === member
|
303
|
+
"%s t1" % Expression.quote(member)
|
304
|
+
elsif Alias === member && Query === member.members[0]
|
305
|
+
"%s %s" % [Expression.quote(member.members[0]), Expression.quote(member.members[1])]
|
306
|
+
else
|
307
|
+
Expression.quote(member)
|
308
|
+
end
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
class With < Expression
|
313
|
+
def to_sql
|
314
|
+
"with %s" % @members.map { |e| Expression.quote(e) }.join(', ')
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
class Select < Expression
|
319
|
+
def to_sql
|
320
|
+
"select %s%s" % [distinct_clause, @members.map { |e| Expression.quote(e) }.join(', ')]
|
321
|
+
end
|
322
|
+
|
323
|
+
def distinct_clause
|
324
|
+
case (on = @props[:distinct])
|
325
|
+
when nil
|
326
|
+
nil
|
327
|
+
when true
|
328
|
+
"distinct "
|
329
|
+
when Array
|
330
|
+
"distinct on (%s) " % on.map { |e| Expression.quote(e) }.join(', ')
|
331
|
+
else
|
332
|
+
"distinct on %s " % Expression.quote(on)
|
333
|
+
end
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
class Where < Expression
|
338
|
+
def to_sql
|
339
|
+
"where %s" % @members.map { |e| Expression.quote(e) }.join(' and ')
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
class Window < Expression
|
344
|
+
def initialize(sym, &block)
|
345
|
+
super(sym)
|
346
|
+
@block = block
|
347
|
+
end
|
348
|
+
|
349
|
+
def to_sql
|
350
|
+
"window %s as %s" % [
|
351
|
+
Expression.quote(@members.first),
|
352
|
+
WindowExpression.new(&@block).to_sql
|
353
|
+
]
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
class OrderBy < Expression
|
358
|
+
def to_sql
|
359
|
+
"order by %s" % @members.map { |e| Expression.quote(e) }.join(', ')
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
class Limit < Expression
|
364
|
+
def to_sql
|
365
|
+
"limit %d" % @members[0]
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
class Query
|
370
|
+
def initialize(**ctx, &block)
|
371
|
+
@ctx = ctx
|
372
|
+
@block = block
|
373
|
+
end
|
374
|
+
|
375
|
+
def to_sql(**ctx)
|
376
|
+
r = SQL.new(@ctx.merge(ctx))
|
377
|
+
r.to_sql(&@block)
|
378
|
+
end
|
379
|
+
|
380
|
+
def as(sym)
|
381
|
+
Alias.new(self, sym)
|
382
|
+
end
|
383
|
+
|
384
|
+
def where(&block)
|
385
|
+
old_block = @block
|
386
|
+
Query.new(@ctx) {
|
387
|
+
instance_eval(&old_block)
|
388
|
+
where instance_eval(&block)
|
389
|
+
}
|
390
|
+
end
|
391
|
+
|
392
|
+
def mutate(&block)
|
393
|
+
old_block = @block
|
394
|
+
Query.new(@ctx) {
|
395
|
+
instance_eval(&old_block)
|
396
|
+
instance_eval(&block)
|
397
|
+
}
|
398
|
+
end
|
399
|
+
end
|
400
|
+
|
401
|
+
class SQL
|
402
|
+
def initialize(ctx)
|
403
|
+
@ctx = ctx
|
404
|
+
end
|
405
|
+
|
406
|
+
def to_sql(&block)
|
407
|
+
instance_eval(&block)
|
408
|
+
[
|
409
|
+
@with,
|
410
|
+
@select || default_select,
|
411
|
+
@from,
|
412
|
+
@where,
|
413
|
+
@window,
|
414
|
+
@order_by,
|
415
|
+
@limit
|
416
|
+
].compact.map { |c| c.to_sql }.join(' ')
|
417
|
+
end
|
418
|
+
|
419
|
+
def _q(expr)
|
420
|
+
QuotedExpression.new(expr)
|
421
|
+
end
|
422
|
+
|
423
|
+
def default_select
|
424
|
+
Select.new(:*)
|
425
|
+
end
|
426
|
+
|
427
|
+
def method_missing(sym, *args)
|
428
|
+
if @ctx.has_key?(sym)
|
429
|
+
value = @ctx[sym]
|
430
|
+
return Symbol === value ? Identifier.new(value) : value
|
431
|
+
end
|
432
|
+
|
433
|
+
super if sym == :to_hash
|
434
|
+
if args.empty?
|
435
|
+
Identifier.new(sym)
|
436
|
+
else
|
437
|
+
FunctionCall.new(sym, *args)
|
438
|
+
end
|
439
|
+
end
|
440
|
+
|
441
|
+
def with(*members, **props)
|
442
|
+
@with = With.new(*members, **props)
|
443
|
+
end
|
444
|
+
|
445
|
+
H_EMPTY = {}.freeze
|
446
|
+
|
447
|
+
def select(*members, **props)
|
448
|
+
if members.empty? && !props.empty?
|
449
|
+
members = props.map { |k, v| Alias.new(v, k) }
|
450
|
+
props = {}
|
451
|
+
end
|
452
|
+
@select = Select.new(*members, **props)
|
453
|
+
end
|
454
|
+
|
455
|
+
def from(*members, **props)
|
456
|
+
@from = From.new(*members, **props)
|
457
|
+
end
|
458
|
+
|
459
|
+
def where(expr)
|
460
|
+
if @where
|
461
|
+
@where.members << expr
|
462
|
+
else
|
463
|
+
@where = Where.new(expr)
|
464
|
+
end
|
465
|
+
end
|
466
|
+
|
467
|
+
def window(sym, &block)
|
468
|
+
@window = Window.new(sym, &block)
|
469
|
+
end
|
470
|
+
|
471
|
+
def order_by(*members, **props)
|
472
|
+
@order_by = OrderBy.new(*members, **props)
|
473
|
+
end
|
474
|
+
|
475
|
+
def limit(*members)
|
476
|
+
@limit = Limit.new(*members)
|
477
|
+
end
|
478
|
+
|
479
|
+
def all(sym = nil)
|
480
|
+
if sym
|
481
|
+
Identifier.new("#{sym}.*")
|
482
|
+
else
|
483
|
+
Identifier.new('*')
|
484
|
+
end
|
485
|
+
end
|
486
|
+
end
|
data/lib/eno/version.rb
ADDED
metadata
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: eno
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.4'
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Sharon Rosner
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-01-21 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: modulation
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.18'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.18'
|
27
|
+
description:
|
28
|
+
email: ciconia@gmail.com
|
29
|
+
executables: []
|
30
|
+
extensions: []
|
31
|
+
extra_rdoc_files:
|
32
|
+
- README.md
|
33
|
+
files:
|
34
|
+
- CHANGELOG.md
|
35
|
+
- README.md
|
36
|
+
- lib/eno.rb
|
37
|
+
- lib/eno/version.rb
|
38
|
+
homepage: http://github.com/digital-fabric/eno
|
39
|
+
licenses:
|
40
|
+
- MIT
|
41
|
+
metadata:
|
42
|
+
source_code_uri: https://github.com/digital-fabric/eno
|
43
|
+
post_install_message:
|
44
|
+
rdoc_options:
|
45
|
+
- "--title"
|
46
|
+
- eno
|
47
|
+
- "--main"
|
48
|
+
- README.md
|
49
|
+
require_paths:
|
50
|
+
- lib
|
51
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
requirements: []
|
62
|
+
rubygems_version: 3.0.1
|
63
|
+
signing_key:
|
64
|
+
specification_version: 4
|
65
|
+
summary: 'Eno: Eno is Not an ORM'
|
66
|
+
test_files: []
|