eno 0.4 → 0.5
Sign up to get free protection for your applications and to get access to all the features.
- 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
data/lib/eno/pg.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pg'
|
4
|
+
|
5
|
+
class PG::Connection
|
6
|
+
ESCAPER = ->(expr) {
|
7
|
+
case expr
|
8
|
+
when Symbol
|
9
|
+
quote_ident(expr.to_s)
|
10
|
+
when String
|
11
|
+
"'#{escape(expr)}'"
|
12
|
+
else
|
13
|
+
nil # use default quoting
|
14
|
+
end
|
15
|
+
}
|
16
|
+
|
17
|
+
def q(query = nil, **ctx, &block)
|
18
|
+
query ||= Eno::Query.new(&block)
|
19
|
+
exec(query_to_sql(query, **ctx))
|
20
|
+
end
|
21
|
+
|
22
|
+
def query_to_sql(query, **ctx)
|
23
|
+
query.to_sql(escape_proc: ESCAPER, **ctx)
|
24
|
+
end
|
25
|
+
end
|
data/lib/eno/query.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
export :Query
|
4
|
+
|
5
|
+
Expressions = import('./expressions')
|
6
|
+
SQL = import('./sql')
|
7
|
+
|
8
|
+
class Query
|
9
|
+
def initialize(**ctx, &block)
|
10
|
+
@ctx = ctx
|
11
|
+
@block = block
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_sql(escape_proc: nil, **ctx)
|
15
|
+
r = SQL::SQL.new(escape_proc: escape_proc, **@ctx.merge(ctx))
|
16
|
+
r.to_sql(&@block)
|
17
|
+
end
|
18
|
+
|
19
|
+
def as(sym)
|
20
|
+
Expressions::Alias.new(self, sym)
|
21
|
+
end
|
22
|
+
|
23
|
+
def where(&block)
|
24
|
+
old_block = @block
|
25
|
+
Query.new(@ctx) {
|
26
|
+
instance_eval(&old_block)
|
27
|
+
where instance_eval(&block)
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
def mutate(&block)
|
32
|
+
old_block = @block
|
33
|
+
Query.new(@ctx) {
|
34
|
+
instance_eval(&old_block)
|
35
|
+
instance_eval(&block)
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
def union(*queries, **props, &block)
|
40
|
+
q1 = self
|
41
|
+
queries << Query.new(&block) if queries.empty?
|
42
|
+
Query.new(@ctx) { union q1, *queries, **props }
|
43
|
+
end
|
44
|
+
alias_method :|, :union
|
45
|
+
|
46
|
+
def union_all(*queries, &block)
|
47
|
+
union(*queries, all: true, &block)
|
48
|
+
end
|
49
|
+
|
50
|
+
def intersect(*queries, **props, &block)
|
51
|
+
q1 = self
|
52
|
+
queries << Query.new(&block) if queries.empty?
|
53
|
+
Query.new(@ctx) { intersect q1, *queries, **props }
|
54
|
+
end
|
55
|
+
alias_method :&, :intersect
|
56
|
+
|
57
|
+
def intersect_all(*queries, &block)
|
58
|
+
intersect(*queries, all: true, &block)
|
59
|
+
end
|
60
|
+
|
61
|
+
def except(*queries, **props, &block)
|
62
|
+
q1 = self
|
63
|
+
queries << Query.new(&block) if queries.empty?
|
64
|
+
Query.new(@ctx) { except q1, *queries, **props }
|
65
|
+
end
|
66
|
+
alias_method :"^", :except
|
67
|
+
|
68
|
+
def except_all(*queries, &block)
|
69
|
+
except(*queries, all: true, &block)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
data/lib/eno/sql.rb
ADDED
@@ -0,0 +1,151 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Expressions = import('./expressions')
|
4
|
+
Query = import('./query')
|
5
|
+
|
6
|
+
export :SQL
|
7
|
+
|
8
|
+
S_SPACE = ' '
|
9
|
+
S_PARENS = '(%s)'
|
10
|
+
S_QUOTES = "'%s'"
|
11
|
+
S_ALL = '*'
|
12
|
+
S_QUALIFIED_ALL = '%s.*'
|
13
|
+
|
14
|
+
class SQL
|
15
|
+
def initialize(escape_proc: nil, **ctx)
|
16
|
+
@escape_proc = escape_proc
|
17
|
+
@ctx = ctx
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_sql(&block)
|
21
|
+
instance_eval(&block)
|
22
|
+
|
23
|
+
return @combination.to_sql(self) if @combination
|
24
|
+
|
25
|
+
[
|
26
|
+
@with,
|
27
|
+
@select || default_select,
|
28
|
+
@from,
|
29
|
+
@where,
|
30
|
+
@window,
|
31
|
+
@order_by,
|
32
|
+
@limit
|
33
|
+
].compact.map { |c| c.to_sql(self) }.join(S_SPACE)
|
34
|
+
end
|
35
|
+
|
36
|
+
def quote(expr)
|
37
|
+
if @escape_proc
|
38
|
+
value = @escape_proc.(expr)
|
39
|
+
return value if value
|
40
|
+
end
|
41
|
+
|
42
|
+
case expr
|
43
|
+
when Query::Query
|
44
|
+
S_PARENS % expr.to_sql(@ctx).strip
|
45
|
+
when Expressions::Expression
|
46
|
+
expr.to_sql(self)
|
47
|
+
when Symbol
|
48
|
+
expr.to_s
|
49
|
+
when String
|
50
|
+
S_QUOTES % expr
|
51
|
+
else
|
52
|
+
expr.inspect
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def context
|
57
|
+
@ctx
|
58
|
+
end
|
59
|
+
|
60
|
+
def _l(value)
|
61
|
+
Expressions::Literal.new(value)
|
62
|
+
end
|
63
|
+
|
64
|
+
def _i(value)
|
65
|
+
Expressions::Identifier.new(value)
|
66
|
+
end
|
67
|
+
|
68
|
+
def default_select
|
69
|
+
Expressions::Select.new(:*)
|
70
|
+
end
|
71
|
+
|
72
|
+
def method_missing(sym, *args)
|
73
|
+
if @ctx.has_key?(sym)
|
74
|
+
value = @ctx[sym]
|
75
|
+
return Symbol === value ? Expressions::Identifier.new(value) : value
|
76
|
+
end
|
77
|
+
|
78
|
+
super if sym == :to_hash
|
79
|
+
if args.empty?
|
80
|
+
Expressions::Identifier.new(sym)
|
81
|
+
else
|
82
|
+
Expressions::FunctionCall.new(sym, *args)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def with(*members, **props)
|
87
|
+
@with = Expressions::With.new(*members, **props)
|
88
|
+
end
|
89
|
+
|
90
|
+
H_EMPTY = {}.freeze
|
91
|
+
|
92
|
+
def select(*members, **props)
|
93
|
+
if members.empty? && !props.empty?
|
94
|
+
members = props.map { |k, v| Expressions::Alias.new(v, k) }
|
95
|
+
props = {}
|
96
|
+
end
|
97
|
+
@select = Expressions::Select.new(*members, **props)
|
98
|
+
end
|
99
|
+
|
100
|
+
def from(*members, **props)
|
101
|
+
@from = Expressions::From.new(*members, **props)
|
102
|
+
end
|
103
|
+
|
104
|
+
def where(expr)
|
105
|
+
if @where
|
106
|
+
@where.members << expr
|
107
|
+
else
|
108
|
+
@where = Expressions::Where.new(expr)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def window(sym, &block)
|
113
|
+
@window = Expressions::Window.new(sym, &block)
|
114
|
+
end
|
115
|
+
|
116
|
+
def order_by(*members, **props)
|
117
|
+
@order_by = Expressions::OrderBy.new(*members, **props)
|
118
|
+
end
|
119
|
+
|
120
|
+
def limit(*members)
|
121
|
+
@limit = Expressions::Limit.new(*members)
|
122
|
+
end
|
123
|
+
|
124
|
+
def all(sym = nil)
|
125
|
+
if sym
|
126
|
+
Expressions::Identifier.new(S_QUALIFIED_ALL % sym)
|
127
|
+
else
|
128
|
+
Expressions::Identifier.new(S_ALL)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def cond(props)
|
133
|
+
Expressions::Case.new(props)
|
134
|
+
end
|
135
|
+
|
136
|
+
def default
|
137
|
+
:default
|
138
|
+
end
|
139
|
+
|
140
|
+
def union(*queries, **props)
|
141
|
+
@combination = Expressions::Combination.new(*queries, kind: :union, **props)
|
142
|
+
end
|
143
|
+
|
144
|
+
def intersect(*queries, **props)
|
145
|
+
@combination = Expressions::Combination.new(*queries, kind: :intersect, **props)
|
146
|
+
end
|
147
|
+
|
148
|
+
def except(*queries, **props)
|
149
|
+
@combination = Expressions::Combination.new(*queries, kind: :except, **props)
|
150
|
+
end
|
151
|
+
end
|
data/lib/eno/version.rb
CHANGED
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../ext'
|
4
|
+
import('../../lib/eno/pg')
|
5
|
+
|
6
|
+
require 'pp'
|
7
|
+
|
8
|
+
class PostgresTest < T
|
9
|
+
DB = PG.connect(
|
10
|
+
host: '/tmp',
|
11
|
+
user: 'reality',
|
12
|
+
password: nil,
|
13
|
+
dbname: 'reality',
|
14
|
+
sslmode: 'require'
|
15
|
+
)
|
16
|
+
|
17
|
+
def test_correct_escaping
|
18
|
+
results = DB.q {
|
19
|
+
select _l("abc def 'ghi'\n").as(:"jkl mno")
|
20
|
+
}.to_a
|
21
|
+
assert_equal(1, results.size)
|
22
|
+
assert_equal(['jkl mno'], results.first.keys)
|
23
|
+
assert_equal(["abc def 'ghi'\n"], results.first.values)
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_identifier_escaping
|
27
|
+
query = Q {
|
28
|
+
select _i("abc'def\"ghi")
|
29
|
+
}
|
30
|
+
assert_equal("select \"abc'def\"\"ghi\"", DB.query_to_sql(query))
|
31
|
+
end
|
32
|
+
end
|
data/test/ext.rb
ADDED
data/test/test.rb
ADDED
@@ -0,0 +1,346 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './ext'
|
4
|
+
|
5
|
+
class SelectTest < T
|
6
|
+
def test_that_no_from_select_is_supported
|
7
|
+
assert_sql('select 1') { select 1 }
|
8
|
+
assert_sql('select pg_sleep(1)') { select pg_sleep(1) }
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_default_select
|
12
|
+
assert_sql('select *') { }
|
13
|
+
assert_sql('select * from t') { from t}
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_that_select_with_hash_is_supported
|
17
|
+
assert_sql('select 1 as a, 2 as b') { select a: 1, b: 2 }
|
18
|
+
assert_sql('select pg_sleep(1) as s, pg_sleep(2) as s2') {
|
19
|
+
select s: pg_sleep(1), s2: pg_sleep(2)
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_select_distinct
|
24
|
+
assert_sql('select distinct a, b') {
|
25
|
+
select a, b, distinct: true
|
26
|
+
}
|
27
|
+
|
28
|
+
assert_sql('select distinct on (a + b) a, b') {
|
29
|
+
select a, b, distinct: a + b
|
30
|
+
}
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class FromTest < T
|
35
|
+
def test_that_from_accepts_table_name
|
36
|
+
assert_sql('select * from abc') {
|
37
|
+
from abc
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_that_expression_can_be_aliased
|
42
|
+
assert_sql('select * from abc as a') {
|
43
|
+
from abc.as(a)
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
def test_that_from_accepts_sub_query
|
48
|
+
query = Q { select _l(1).as a }
|
49
|
+
assert_sql('select a from (select 1 as a) t1') {
|
50
|
+
select a
|
51
|
+
from query
|
52
|
+
}
|
53
|
+
|
54
|
+
assert_sql('select a from (select 1 as a) t3') {
|
55
|
+
select a
|
56
|
+
from query.as t3
|
57
|
+
}
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class WithTest < T
|
62
|
+
def test_that_with_accepts_sub_queries
|
63
|
+
assert_sql('with t1 as (select 1 as a), t2 as (select 2 as b) select * from b') {
|
64
|
+
with t1.as { select _l(1).as a }, t2.as { select _l(2).as b }
|
65
|
+
select all
|
66
|
+
from b
|
67
|
+
}
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
class WhereTest < T
|
72
|
+
def test_that_where_accepts_boolean_const
|
73
|
+
assert_sql('select * from a where true') {
|
74
|
+
from a
|
75
|
+
where true
|
76
|
+
}
|
77
|
+
end
|
78
|
+
|
79
|
+
def test_that_where_accepts_comparison_expression
|
80
|
+
assert_sql('select * from a where (b = 1)') {
|
81
|
+
from a
|
82
|
+
where b == 1
|
83
|
+
}
|
84
|
+
|
85
|
+
assert_sql('select * from a where ((b = 1) and (c = 2))') {
|
86
|
+
from a
|
87
|
+
where (b == 1) & (c == 2)
|
88
|
+
}
|
89
|
+
|
90
|
+
assert_sql('select * from a where ((b > 1) and (c < 2))') {
|
91
|
+
from a
|
92
|
+
where (b > 1) & (c < 2)
|
93
|
+
}
|
94
|
+
|
95
|
+
assert_sql('select * from a where ((b <= 1) and (c <> 2))') {
|
96
|
+
from a
|
97
|
+
where (b <= 1) & (c != 2)
|
98
|
+
}
|
99
|
+
end
|
100
|
+
|
101
|
+
def test_that_where_accepts_logical_expression
|
102
|
+
assert_sql('select * from a where (b and c)') {
|
103
|
+
from a
|
104
|
+
where b & c
|
105
|
+
}
|
106
|
+
|
107
|
+
assert_sql('select * from a where (b or c)') {
|
108
|
+
from a
|
109
|
+
where b | c
|
110
|
+
}
|
111
|
+
|
112
|
+
assert_sql('select * from a where (b and (not c))') {
|
113
|
+
from a
|
114
|
+
where b & !c
|
115
|
+
}
|
116
|
+
|
117
|
+
assert_sql('select * from a where (not (b or c))') {
|
118
|
+
from a
|
119
|
+
where !(b | c)
|
120
|
+
}
|
121
|
+
end
|
122
|
+
|
123
|
+
def test_that_where_accepts_nil
|
124
|
+
assert_sql('select * from a where (b is null)') {
|
125
|
+
from a
|
126
|
+
where b.null?
|
127
|
+
}
|
128
|
+
|
129
|
+
assert_sql('select * from a where (b is not null)') {
|
130
|
+
from a
|
131
|
+
where !b.null?
|
132
|
+
}
|
133
|
+
|
134
|
+
assert_sql('select * from a where (b is not null)') {
|
135
|
+
from a
|
136
|
+
where !b.null?
|
137
|
+
}
|
138
|
+
end
|
139
|
+
|
140
|
+
def test_that_where_accepts_arithmetic_operators
|
141
|
+
assert_sql('select * from a where ((b + c) = 42)') {
|
142
|
+
from a
|
143
|
+
where b + c == 42
|
144
|
+
}
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
class DSLTest < T
|
149
|
+
class Eno::Identifier
|
150
|
+
def [](sym)
|
151
|
+
case sym
|
152
|
+
when Symbol
|
153
|
+
Eno::Alias.new(JSONBExpression.new(self, sym), sym)
|
154
|
+
else
|
155
|
+
JSONBExpression.new(self, sym)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
class JSONBExpression < Eno::Expression
|
161
|
+
def to_sql(sql)
|
162
|
+
"#{sql.quote(@members[0])}->>'#{sql.quote(@members[1])}'"
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def test_that_dsl_can_be_extended
|
167
|
+
assert_sql("select attributes->>'path'") {
|
168
|
+
select attributes[path]
|
169
|
+
}
|
170
|
+
|
171
|
+
assert_sql("select attributes->>'path' as path") {
|
172
|
+
select attributes[:path]
|
173
|
+
}
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
class WindowTest < T
|
178
|
+
def test_that_over_is_supported
|
179
|
+
assert_sql('select last_value(q) over w as q_last, last_value(v) over w as v_last') {
|
180
|
+
select last_value(q).over(w).as(q_last),
|
181
|
+
last_value(v).over(w).as(v_last)
|
182
|
+
}
|
183
|
+
end
|
184
|
+
|
185
|
+
def test_that_over_supports_inline_window
|
186
|
+
assert_sql('select group_name, avg(price) over (partition by group_name) from products') {
|
187
|
+
select group_name, (avg(price).over { partition_by group_name })
|
188
|
+
from products
|
189
|
+
}
|
190
|
+
end
|
191
|
+
|
192
|
+
def test_named_windows
|
193
|
+
assert_sql('select last_value(q) over w as q_last, last_value(v) over w as v_last from t1 window w as (partition by stamp_aligned order by stamp range between unbounded preceding and unbounded following)') {
|
194
|
+
select last_value(q).over(w).as(q_last),
|
195
|
+
last_value(v).over(w).as(v_last)
|
196
|
+
from t1
|
197
|
+
window(w) {
|
198
|
+
partition_by stamp_aligned
|
199
|
+
order_by stamp
|
200
|
+
range_unbounded
|
201
|
+
}
|
202
|
+
}
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
class ContextTest < T
|
207
|
+
def test_that_context_passed_can_be_used_in_query
|
208
|
+
query = Q(tbl: :nodes, field: :sample_rate, value: 42) {
|
209
|
+
select a, b
|
210
|
+
from tbl
|
211
|
+
where field < value
|
212
|
+
}
|
213
|
+
assert_equal(
|
214
|
+
'select a, b from nodes where (sample_rate < 42)',
|
215
|
+
query.to_sql
|
216
|
+
)
|
217
|
+
end
|
218
|
+
|
219
|
+
def test_that_context_can_be_passed_in_to_sql_method
|
220
|
+
query = Q {
|
221
|
+
select a, b
|
222
|
+
from tbl
|
223
|
+
where field < value
|
224
|
+
}
|
225
|
+
assert_equal(
|
226
|
+
'select a, b from nodes where (sample_rate < 43)',
|
227
|
+
query.to_sql(tbl: :nodes, field: :sample_rate, value: 43)
|
228
|
+
)
|
229
|
+
end
|
230
|
+
|
231
|
+
def test_that_to_sql_overrides_initial_context
|
232
|
+
query = Q(tbl: :nodes, field: :deadband) {
|
233
|
+
select a, b
|
234
|
+
from tbl
|
235
|
+
where field < value
|
236
|
+
}
|
237
|
+
assert_equal(
|
238
|
+
'select a, b from nodes where (sample_rate < 42)',
|
239
|
+
query.to_sql(field: :sample_rate, value: 42)
|
240
|
+
)
|
241
|
+
|
242
|
+
assert_equal(
|
243
|
+
'select a, b from nodes where (deadband < 42)',
|
244
|
+
query.to_sql(value: 42)
|
245
|
+
)
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
class MutationTest < T
|
250
|
+
def test_that_query_can_further_refined_with_where_clause
|
251
|
+
q = Q {
|
252
|
+
select a, b
|
253
|
+
}
|
254
|
+
assert_equal('select a, b', q.to_sql)
|
255
|
+
|
256
|
+
q2 = q.where { c < d}
|
257
|
+
assert(q != q2)
|
258
|
+
assert_equal('select a, b', q.to_sql)
|
259
|
+
assert_equal('select a, b where (c < d)', q2.to_sql)
|
260
|
+
|
261
|
+
q = Q {
|
262
|
+
where _l(2) + _l(2) == _l(5)
|
263
|
+
}
|
264
|
+
assert_equal('select * where ((2 + 2) = 5)', q.to_sql)
|
265
|
+
|
266
|
+
q2 = q.where { _l('up') == _l('down') }
|
267
|
+
assert_equal("select * where ((2 + 2) = 5) and ('up' = 'down')", q2.to_sql)
|
268
|
+
end
|
269
|
+
|
270
|
+
def test_that_mutated_query_can_change_arbitrary_clauses
|
271
|
+
q = Q { select a; from b }
|
272
|
+
assert_equal('select a from b', q.to_sql)
|
273
|
+
|
274
|
+
q2 = q.mutate { from c }
|
275
|
+
assert_equal('select a from b', q.to_sql)
|
276
|
+
assert_equal('select a from c', q2.to_sql)
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
class CastTest < T
|
281
|
+
def test_that_cast_is_correctly_formatted
|
282
|
+
assert_sql('select cast (a as b)') { select a.cast(b) }
|
283
|
+
assert_sql('select cast (123 as float)') { select _l(123).cast(float) }
|
284
|
+
assert_sql("select cast ('123' as integer)") { select _l('123').cast(integer) }
|
285
|
+
end
|
286
|
+
|
287
|
+
def test_that_cast_shorthand_is_correctly_formatted
|
288
|
+
assert_sql('select a::b') { select a^b }
|
289
|
+
assert_sql('select 123::float') { select _l(123)^float }
|
290
|
+
assert_sql("select '2019-01-01 00:00+00'::timestamptz") {
|
291
|
+
select _l('2019-01-01 00:00+00')^timestamptz
|
292
|
+
}
|
293
|
+
end
|
294
|
+
|
295
|
+
def test_that_cast_works_wih_symbols
|
296
|
+
assert_sql('select cast (a as b)') { select a.cast(:b) }
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
class InTest < T
|
301
|
+
def test_that_in_is_correctly_formatted
|
302
|
+
assert_sql('select * where a in (1, 2, 3)') { where a.in 1, 2, 3 }
|
303
|
+
assert_sql('select * where a not in (1, 2, 3)') { where !a.in(1, 2, 3) }
|
304
|
+
end
|
305
|
+
|
306
|
+
def test_that_not_in_is_correcly_formatted
|
307
|
+
assert_sql('select * where a not in (1, 2, 3)') { where a.not_in 1, 2, 3 }
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
class LiteralTest < T
|
312
|
+
def test_that_numbers_are_correctly_quoted
|
313
|
+
assert_sql('select 123') { select 123 }
|
314
|
+
assert_sql('select 123') { select _l(123) }
|
315
|
+
assert_sql('select (2 + 2)') { select _l(2) + _l(2) }
|
316
|
+
end
|
317
|
+
|
318
|
+
def test_that_strings_are_correctly_quoted
|
319
|
+
assert_sql("select 'abc'") { select 'abc' }
|
320
|
+
end
|
321
|
+
|
322
|
+
def test_that_null_literal_is_correctly_quoted
|
323
|
+
assert_sql('select null') { select null }
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
class ConvenienceVariablesTest < T
|
328
|
+
def test_that_convenience_variables_do_not_change_query
|
329
|
+
assert_sql('select unformatted_value::boolean, unformatted_value::float') {
|
330
|
+
uv = unformatted_value
|
331
|
+
select uv^boolean, uv^float
|
332
|
+
}
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
class ExtractEpoch < Eno::Expression
|
337
|
+
class Eno::SQL
|
338
|
+
def extract_epoch_from(sym)
|
339
|
+
ExtractEpoch.new(sym)
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
def to_sql(sql)
|
344
|
+
"extract (epoch from #{sql.quote(@members[0])})::integer"
|
345
|
+
end
|
346
|
+
end
|