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
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
|