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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f38441c60755b6d8a052a675725904ef2d7694955b81908790c31a1f33a314fd
|
4
|
+
data.tar.gz: f7a0cb8679e3c0798f626781daac68c17c5de0aca907323628e26a1b73645d73
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 94a69f5cdd6e83f504d2bf3536e5616b1cce54589a450dc25440ead3576b93def237be2e51e74cdffb9671775bd22b5f695c7f0db92fd5d8cde482744442b84e
|
7
|
+
data.tar.gz: 1e3859cfb9f114e90b8748862a30db06e41c57c54754bb1c0d6c768c11cab065dd2d75fcc013494490ec0f44defda21e4e5d52fbb2920baebb1ca32b548b5962
|
data/.gitignore
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
/.config
|
4
|
+
/coverage/
|
5
|
+
/InstalledFiles
|
6
|
+
/pkg/
|
7
|
+
/spec/reports/
|
8
|
+
/spec/examples.txt
|
9
|
+
/test/tmp/
|
10
|
+
/test/version_tmp/
|
11
|
+
/tmp/
|
12
|
+
|
13
|
+
# Used by dotenv library to load environment variables.
|
14
|
+
# .env
|
15
|
+
|
16
|
+
## Specific to RubyMotion:
|
17
|
+
.dat*
|
18
|
+
.repl_history
|
19
|
+
build/
|
20
|
+
*.bridgesupport
|
21
|
+
build-iPhoneOS/
|
22
|
+
build-iPhoneSimulator/
|
23
|
+
|
24
|
+
## Specific to RubyMotion (use of CocoaPods):
|
25
|
+
#
|
26
|
+
# We recommend against adding the Pods directory to your .gitignore. However
|
27
|
+
# you should judge for yourself, the pros and cons are mentioned at:
|
28
|
+
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
29
|
+
#
|
30
|
+
# vendor/Pods/
|
31
|
+
|
32
|
+
## Documentation cache and generated files:
|
33
|
+
/.yardoc/
|
34
|
+
/_yardoc/
|
35
|
+
/doc/
|
36
|
+
/rdoc/
|
37
|
+
|
38
|
+
## Environment normalization:
|
39
|
+
/.bundle/
|
40
|
+
/vendor/bundle
|
41
|
+
/lib/bundler/man/
|
42
|
+
|
43
|
+
# for a library or gem, you might want to ignore these files since the code is
|
44
|
+
# intended to run in multiple environments; otherwise, check them in:
|
45
|
+
# Gemfile.lock
|
46
|
+
# .ruby-version
|
47
|
+
# .ruby-gemset
|
48
|
+
|
49
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
50
|
+
.rvmrc
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,16 @@
|
|
1
|
+
* Rename `#_q` to `#_l` (for literal)
|
2
|
+
* Add `#_i` method for creating identifier
|
3
|
+
|
4
|
+
0.5 2019-01-25
|
5
|
+
--------------
|
6
|
+
|
7
|
+
* Implement query combination: `union`, `intersect`, `except`
|
8
|
+
* Implement `#not_in` method
|
9
|
+
* Implement case expression (using `#cond`)
|
10
|
+
* Implement not in expression
|
11
|
+
* Implement in operator
|
12
|
+
* Implement cast operator (using either `#cast` or `#^`)
|
13
|
+
|
1
14
|
0.4 2019-01-21
|
2
15
|
--------------
|
3
16
|
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
eno (0.5)
|
5
|
+
modulation (= 0.18)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
modulation (0.18)
|
11
|
+
pg (1.1.3)
|
12
|
+
sqlite3 (1.3.13)
|
13
|
+
|
14
|
+
PLATFORMS
|
15
|
+
ruby
|
16
|
+
|
17
|
+
DEPENDENCIES
|
18
|
+
eno!
|
19
|
+
pg (= 1.1.3)
|
20
|
+
sqlite3 (= 1.3.13)
|
21
|
+
|
22
|
+
BUNDLED WITH
|
23
|
+
2.1.4
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2019 digital-fabric
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
CHANGED
@@ -30,7 +30,7 @@ like ActiveRecord's `where`:
|
|
30
30
|
Client.where(order_count: [1, 3, 5])
|
31
31
|
```
|
32
32
|
|
33
|
-
And Sequel is a bit more flexible:
|
33
|
+
And Sequel is (quite) a bit more flexible:
|
34
34
|
|
35
35
|
```ruby
|
36
36
|
Client.where { order_count > 10 }
|
@@ -98,15 +98,90 @@ Q {
|
|
98
98
|
}.to_sql #=> "select a, b from c"
|
99
99
|
```
|
100
100
|
|
101
|
-
##
|
101
|
+
## Expressions
|
102
102
|
|
103
|
-
|
104
|
-
can mix
|
103
|
+
Eno lets you build arbitrarily complex expressions once inside the query block.
|
104
|
+
You can freely mix identifiers and literals, use most operators (with certain
|
105
|
+
caveats) and make function calls.
|
106
|
+
|
107
|
+
### Identifiers
|
108
|
+
|
109
|
+
An identifier is referenced simply using its name:
|
110
|
+
|
111
|
+
```ruby
|
112
|
+
Q {
|
113
|
+
select foo
|
114
|
+
} #=> select foo
|
115
|
+
```
|
116
|
+
|
117
|
+
Identifiers can be qualified by using dot-notation:
|
118
|
+
|
119
|
+
```ruby
|
120
|
+
Q {
|
121
|
+
select foo.bar
|
122
|
+
} #=> select foo.bar
|
123
|
+
```
|
124
|
+
|
125
|
+
### Literals
|
126
|
+
|
127
|
+
Literals can be specified as literals
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
Q {
|
131
|
+
select x * 10
|
132
|
+
} #=> select x * 10
|
133
|
+
```
|
134
|
+
|
135
|
+
However, if the first argument of an expression is a literal, it will need to be
|
136
|
+
wrapped in a call to `#_q`:
|
137
|
+
|
138
|
+
```ruby
|
139
|
+
Q {
|
140
|
+
select _q(2) + 2
|
141
|
+
} #=> select 2 + 2
|
142
|
+
```
|
143
|
+
|
144
|
+
### Operators
|
145
|
+
|
146
|
+
Eno supports the following mathematical operators:
|
147
|
+
|
148
|
+
operator | description
|
149
|
+
---------|------------
|
150
|
+
`+` | addition
|
151
|
+
`-` | subtraction
|
152
|
+
`*` | multiplication
|
153
|
+
`/` | division
|
154
|
+
`%` | modulo (remainder)
|
155
|
+
|
156
|
+
Logical operators are supported using the following operators:
|
157
|
+
|
158
|
+
operator | description
|
159
|
+
---------|------------
|
160
|
+
`&` | logical and
|
161
|
+
`\|` | logical or
|
162
|
+
`!` | logical not
|
163
|
+
|
164
|
+
The following comparison operators are supported:
|
165
|
+
|
166
|
+
operator | description
|
167
|
+
---------|------------
|
168
|
+
`==` | equal
|
169
|
+
`!=` | not equal
|
170
|
+
`<` | less than
|
171
|
+
`>` | greater than
|
172
|
+
`<=` | less than or equal
|
173
|
+
`>=` | greater than or equal
|
174
|
+
|
175
|
+
An example involving multiple operators:
|
105
176
|
|
106
177
|
```ruby
|
107
|
-
Q {
|
178
|
+
Q {
|
179
|
+
select (a + b) & (c * d), e >= f
|
180
|
+
} #=> select (a + b) and (c * d), e >= f
|
108
181
|
```
|
109
182
|
|
183
|
+
### functions
|
184
|
+
|
110
185
|
You can also use SQL functions:
|
111
186
|
|
112
187
|
```ruby
|
@@ -117,6 +192,80 @@ Q {
|
|
117
192
|
}
|
118
193
|
```
|
119
194
|
|
195
|
+
## SQL clauses
|
196
|
+
|
197
|
+
Eno supports the following clauses:
|
198
|
+
|
199
|
+
### Select
|
200
|
+
|
201
|
+
The `#select` method is used to specify the list of selected expressions for a
|
202
|
+
`select` statement. The `select` method accepts a list of expressions:
|
203
|
+
|
204
|
+
```ruby
|
205
|
+
Q { select a, b + c, d.as(e) } #=> select a, b + c, d as e
|
206
|
+
```
|
207
|
+
|
208
|
+
The `#select` method can also accept a hash mapping aliases to expressions:
|
209
|
+
|
210
|
+
```ruby
|
211
|
+
Q { select c: a + b, f: d(e) } #=> select a + b as c, d(e) as f
|
212
|
+
```
|
213
|
+
|
214
|
+
Columns can be qualified using dot-notation:
|
215
|
+
|
216
|
+
```ruby
|
217
|
+
Q { select a.b, c.d.e } #=> select a.b, c.d.e
|
218
|
+
```
|
219
|
+
|
220
|
+
Note: if `#select` is not called within a query block, a `select *` is assumed:
|
221
|
+
|
222
|
+
```ruby
|
223
|
+
Q { from mytable } #=> select * from mytable
|
224
|
+
```
|
225
|
+
|
226
|
+
### From
|
227
|
+
|
228
|
+
The `#from` method is used to specify one or more sources for the query. Usually
|
229
|
+
this would be a table name, a subquery, a CTE name (specified using `#with`):
|
230
|
+
|
231
|
+
```ruby
|
232
|
+
Q { from a, b, c } #=> select * from a, b, c
|
233
|
+
Q { from a.as b } #=> select * from a as b
|
234
|
+
```
|
235
|
+
|
236
|
+
Subqueries can also be used in `#from`:
|
237
|
+
|
238
|
+
```ruby
|
239
|
+
Q {
|
240
|
+
select sum(foo.score)
|
241
|
+
from Q { select * from scores }.as(foo)
|
242
|
+
} #=> select sum(foo.score) from (select score from scores) as foo
|
243
|
+
```
|
244
|
+
|
245
|
+
### Where
|
246
|
+
|
247
|
+
The `#where` method is used to specify a record filter:
|
248
|
+
|
249
|
+
```ruby
|
250
|
+
Q {
|
251
|
+
from users
|
252
|
+
where name == 'John Doe' & age > 30
|
253
|
+
} #=> select * from users where (name = 'John Doe') and (age > 30)
|
254
|
+
```
|
255
|
+
|
256
|
+
Where clauses can be of arbitrary complexity (as shown [above](#expressions)),
|
257
|
+
and can also be chained in order to mutate and further filter query:
|
258
|
+
|
259
|
+
```ruby
|
260
|
+
query = Q {
|
261
|
+
from users
|
262
|
+
where state == 'CA'
|
263
|
+
}
|
264
|
+
query.where { age >= 25 } #=> select * from users where (state = 'CA') and (age >= 25)
|
265
|
+
```
|
266
|
+
|
267
|
+
|
268
|
+
|
120
269
|
## Hooking up Eno to your database
|
121
270
|
|
122
271
|
In and of itself, Eno is just an engine for building SQL queries. To actually
|
data/eno.gemspec
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require_relative './lib/eno/version'
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = 'eno'
|
5
|
+
s.version = Eno::VERSION
|
6
|
+
s.licenses = ['MIT']
|
7
|
+
s.summary = 'Eno: Eno is Not an ORM'
|
8
|
+
s.author = 'Sharon Rosner'
|
9
|
+
s.email = 'ciconia@gmail.com'
|
10
|
+
s.files = `git ls-files`.split
|
11
|
+
s.homepage = 'http://github.com/digital-fabric/eno'
|
12
|
+
s.metadata = {
|
13
|
+
"source_code_uri" => "https://github.com/digital-fabric/eno"
|
14
|
+
}
|
15
|
+
s.rdoc_options = ["--title", "eno", "--main", "README.md"]
|
16
|
+
s.extra_rdoc_files = ["README.md"]
|
17
|
+
s.require_paths = ["lib"]
|
18
|
+
|
19
|
+
s.add_runtime_dependency 'modulation', '0.18'
|
20
|
+
s.add_development_dependency 'pg', '1.1.3'
|
21
|
+
s.add_development_dependency 'sqlite3', '1.3.13'
|
22
|
+
end
|
data/examples/basic.rb
ADDED
File without changes
|
data/lib/eno.rb
CHANGED
@@ -2,485 +2,16 @@
|
|
2
2
|
|
3
3
|
require 'modulation/gem'
|
4
4
|
|
5
|
-
|
5
|
+
export_default :Eno
|
6
6
|
|
7
7
|
module ::Kernel
|
8
8
|
def Q(**ctx, &block)
|
9
|
-
Query.new(**ctx, &block)
|
9
|
+
Eno::Query.new(**ctx, &block)
|
10
10
|
end
|
11
11
|
end
|
12
12
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
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
|
13
|
+
module Eno
|
14
|
+
include_from('./eno/expressions')
|
15
|
+
SQL = import('./eno/sql')::SQL
|
16
|
+
Query = import('./eno/query')::Query
|
17
|
+
end
|