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,144 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './ext'
|
4
|
+
|
5
|
+
class ExpressionTest < T
|
6
|
+
def test_aliases
|
7
|
+
assert_sql('select 1 as c') { select _l(1).as c }
|
8
|
+
assert_sql('select a as b') { select a.as b }
|
9
|
+
assert_sql('select a as b, c as d') { select (a.as b), (c.as d) }
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_aliased_function_expressions
|
13
|
+
assert_sql('select pg_sleep(1) as s, pg_sleep(2) as s2') {
|
14
|
+
select pg_sleep(1).as(s), pg_sleep(2).as(s2)
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_that_aliases_can_be_expressed_with_symbols
|
19
|
+
assert_sql('select pg_sleep(1) as s, pg_sleep(2) as s2') {
|
20
|
+
select pg_sleep(1).as(:s), pg_sleep(2).as(:s2)
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_qualified_names
|
25
|
+
assert_sql('select a.b, c.d as e') {
|
26
|
+
select a.b, c.d.as(e)
|
27
|
+
}
|
28
|
+
|
29
|
+
assert_sql('select a.b.c.d') {
|
30
|
+
select a.b.c.d
|
31
|
+
}
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class OpTest < T
|
36
|
+
def test_comparison_operators
|
37
|
+
assert_sql('select (a = b)') { select a == b }
|
38
|
+
assert_sql('select (a = b)') { select !(a != b) }
|
39
|
+
assert_sql('select (a = (b + c))') { select a == (b + c) }
|
40
|
+
|
41
|
+
assert_sql('select (a <> b)') { select (a != b) }
|
42
|
+
assert_sql('select (a <> b)') { select !(a == b) }
|
43
|
+
|
44
|
+
assert_sql('select (a > b), (c < d)') { select (a > b), (c < d) }
|
45
|
+
assert_sql('select (a >= b), (c <= d)') { select !(a < b), !(c > d) }
|
46
|
+
|
47
|
+
assert_sql('select (a >= b), (c <= d)') { select (a >= b), (c <= d) }
|
48
|
+
assert_sql('select (a > b), (c < d)') { select !(a <= b), !(c >= d) }
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_math_operators
|
52
|
+
assert_sql('select (a + b), (c - d)') { select a + b, c - d }
|
53
|
+
assert_sql('select (a * b), (c / d), (e % f)') { select a * b, c / d, e % f }
|
54
|
+
|
55
|
+
assert_sql('select (a + (b * c))') { select a + b * c }
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_logical_operators
|
59
|
+
assert_sql('select (a and b), (c or d)') { select a & b, c | d }
|
60
|
+
assert_sql('select (a and (not b))') { select a & !b }
|
61
|
+
assert_sql('select (not (a or b))') { select !(a | b) }
|
62
|
+
end
|
63
|
+
|
64
|
+
def test_cast_shorthand_operator
|
65
|
+
assert_sql('select a::integer') { select a^integer }
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
class CastTest < T
|
70
|
+
def test_that_cast_is_correctly_formatted
|
71
|
+
assert_sql('select cast (a as b)') { select a.cast(b) }
|
72
|
+
assert_sql('select cast (123 as float)') { select _l(123).cast(float) }
|
73
|
+
assert_sql("select cast ('123' as integer)") { select _l('123').cast(integer) }
|
74
|
+
end
|
75
|
+
|
76
|
+
def test_that_cast_shorthand_is_correctly_formatted
|
77
|
+
assert_sql('select a::b') { select a^b }
|
78
|
+
assert_sql('select 123::float') { select _l(123)^float }
|
79
|
+
assert_sql("select '2019-01-01 00:00+00'::timestamptz") {
|
80
|
+
select _l('2019-01-01 00:00+00')^timestamptz
|
81
|
+
}
|
82
|
+
end
|
83
|
+
|
84
|
+
def test_that_cast_works_wih_symbols
|
85
|
+
assert_sql('select cast (a as b)') { select a.cast(:b) }
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
class InTest < T
|
90
|
+
def test_that_in_is_correctly_formatted
|
91
|
+
assert_sql('select * where a in (1, 2, 3)') { where a.in 1, 2, 3 }
|
92
|
+
assert_sql('select * where a not in (1, 2, 3)') { where !a.in(1, 2, 3) }
|
93
|
+
end
|
94
|
+
|
95
|
+
def test_that_not_in_is_correcly_formatted
|
96
|
+
assert_sql('select * where a not in (1, 2, 3)') { where a.not_in 1, 2, 3 }
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
class LiteralTest < T
|
101
|
+
def test_that_numbers_are_correctly_quoted
|
102
|
+
assert_sql('select 123') { select 123 }
|
103
|
+
assert_sql('select 123') { select _l(123) }
|
104
|
+
assert_sql('select (2 + 2)') { select _l(2) + _l(2) }
|
105
|
+
end
|
106
|
+
|
107
|
+
def test_that_strings_are_correctly_quoted
|
108
|
+
assert_sql("select 'abc'") { select 'abc' }
|
109
|
+
end
|
110
|
+
|
111
|
+
def test_that_null_literal_is_correctly_quoted
|
112
|
+
assert_sql('select null') { select null }
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
class ConditionalTest < T
|
117
|
+
def test_that_cond_expression_is_correctly_formatted
|
118
|
+
assert_sql('select case when (a < b) then c else d end') {
|
119
|
+
select cond(
|
120
|
+
(a < b) => c,
|
121
|
+
default => d
|
122
|
+
)
|
123
|
+
}
|
124
|
+
end
|
125
|
+
|
126
|
+
def test_that_cond_expression_can_be_nested
|
127
|
+
assert_sql("select case when quality not in (1, 4, 5) then null when (datatype = 3) then case when unformatted_value::boolean then 1 else 0 end when (unformatted_value ~ '^[+-]?([0-9]*[.])?[0-9]+$') then unformatted_value::float else null end as value_float") {
|
128
|
+
select cond(
|
129
|
+
!quality.in(1, 4, 5) => null,
|
130
|
+
datatype == 3 => cond(
|
131
|
+
unformatted_value^boolean => 1,
|
132
|
+
default => 0
|
133
|
+
),
|
134
|
+
unformatted_value =~ '^[+-]?([0-9]*[.])?[0-9]+$' => unformatted_value^float,
|
135
|
+
default => null
|
136
|
+
).as value_float
|
137
|
+
}
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
class AliasTest < T
|
142
|
+
def test_that_alias_is_escaped_as_identifier
|
143
|
+
end
|
144
|
+
end
|
data/test/test_query.rb
ADDED
@@ -0,0 +1,321 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './ext'
|
4
|
+
|
5
|
+
class ContextTest < T
|
6
|
+
def test_that_context_passed_can_be_used_in_query
|
7
|
+
query = Q(tbl: :nodes, field: :sample_rate, value: 42) {
|
8
|
+
select a, b
|
9
|
+
from tbl
|
10
|
+
where field < value
|
11
|
+
}
|
12
|
+
assert_equal(
|
13
|
+
'select a, b from nodes where (sample_rate < 42)',
|
14
|
+
query.to_sql
|
15
|
+
)
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_that_context_can_be_passed_in_to_sql_method
|
19
|
+
query = Q {
|
20
|
+
select a, b
|
21
|
+
from tbl
|
22
|
+
where field < value
|
23
|
+
}
|
24
|
+
assert_equal(
|
25
|
+
'select a, b from nodes where (sample_rate < 43)',
|
26
|
+
query.to_sql(tbl: :nodes, field: :sample_rate, value: 43)
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_that_to_sql_overrides_initial_context
|
31
|
+
query = Q(tbl: :nodes, field: :deadband) {
|
32
|
+
select a, b
|
33
|
+
from tbl
|
34
|
+
where field < value
|
35
|
+
}
|
36
|
+
assert_equal(
|
37
|
+
'select a, b from nodes where (sample_rate < 42)',
|
38
|
+
query.to_sql(field: :sample_rate, value: 42)
|
39
|
+
)
|
40
|
+
|
41
|
+
assert_equal(
|
42
|
+
'select a, b from nodes where (deadband < 42)',
|
43
|
+
query.to_sql(value: 42)
|
44
|
+
)
|
45
|
+
end
|
46
|
+
|
47
|
+
def test_that_context_is_accessible_for_sub_query
|
48
|
+
q1 = Q { select a }
|
49
|
+
q2 = Q { select b; from q1.as t1 }
|
50
|
+
assert_equal('select 3 from (select 2) t1', q2.to_sql(a: 2, b: 3))
|
51
|
+
assert_equal('select 3 from (select 2) tbl', q2.to_sql(a: 2, b: 3, t1: :tbl))
|
52
|
+
end
|
53
|
+
|
54
|
+
def test_that_to_sql_context_overrides_initialized_context
|
55
|
+
q1 = Q(t1: :tbl1) { select a from t1 }
|
56
|
+
q2 = Q(t2: :tbl2) { select b; from q1.as t2 }
|
57
|
+
assert_equal('select 3 from (select 2 from tbl1) tbl2', q2.to_sql(a: 2, b: 3))
|
58
|
+
end
|
59
|
+
|
60
|
+
def test_that_context_method_gives_context
|
61
|
+
q = Q { from users; where name == context[:user_name] }
|
62
|
+
assert_equal("select * from users where (name = 'foo')", q.to_sql(user_name: 'foo'))
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class MutationTest < T
|
67
|
+
def test_that_query_can_further_refined_with_where_clause
|
68
|
+
q = Q {
|
69
|
+
select a, b
|
70
|
+
}
|
71
|
+
assert_equal('select a, b', q.to_sql)
|
72
|
+
|
73
|
+
q2 = q.where { c < d}
|
74
|
+
assert(q != q2)
|
75
|
+
assert_equal('select a, b', q.to_sql)
|
76
|
+
assert_equal('select a, b where (c < d)', q2.to_sql)
|
77
|
+
|
78
|
+
q = Q {
|
79
|
+
where _l(2) + _l(2) == _l(5)
|
80
|
+
}
|
81
|
+
assert_equal('select * where ((2 + 2) = 5)', q.to_sql)
|
82
|
+
|
83
|
+
q2 = q.where { _l('up') == _l('down') }
|
84
|
+
assert_equal("select * where ((2 + 2) = 5) and ('up' = 'down')", q2.to_sql)
|
85
|
+
end
|
86
|
+
|
87
|
+
def test_that_mutated_query_can_change_arbitrary_clauses
|
88
|
+
q = Q { select a; from b }
|
89
|
+
assert_equal('select a from b', q.to_sql)
|
90
|
+
|
91
|
+
q2 = q.mutate { from c }
|
92
|
+
assert_equal('select a from b', q.to_sql)
|
93
|
+
assert_equal('select a from c', q2.to_sql)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
class ConvenienceVariablesTest < T
|
98
|
+
def test_that_convenience_variables_do_not_change_query
|
99
|
+
assert_sql('select unformatted_value::boolean, unformatted_value::float') {
|
100
|
+
uv = unformatted_value
|
101
|
+
select uv^boolean, uv^float
|
102
|
+
}
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
class CustomFunctionTest < T
|
107
|
+
class Eno::SQL
|
108
|
+
FLOAT_RE = '^[+-]?([0-9]*[.])?[0-9]+$'.freeze
|
109
|
+
|
110
|
+
def cast_value
|
111
|
+
uv = unformatted_value
|
112
|
+
cond(
|
113
|
+
quality.not_in(1, 4, 5) => null,
|
114
|
+
(datatype == 3) => cond(uv^boolean => 1, default => 0),
|
115
|
+
(uv =~ FLOAT_RE) => uv^float,
|
116
|
+
default => null
|
117
|
+
)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def test_that_custom_function_can_be_used_normally
|
122
|
+
assert_sql("select case when quality not in (1, 4, 5) then null when (datatype = 3) then case when unformatted_value::boolean then 1 else 0 end when (unformatted_value ~ '^[+-]?([0-9]*[.])?[0-9]+$') then unformatted_value::float else null end as value_float") {
|
123
|
+
select cast_value.as value_float
|
124
|
+
}
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
class CombinationTest < T
|
129
|
+
def test_union
|
130
|
+
query = Q { select a }.union { select b}
|
131
|
+
assert_equal("(select a) union (select b)", query.to_sql)
|
132
|
+
|
133
|
+
q1 = Q { select a }
|
134
|
+
q2 = Q { select b }
|
135
|
+
assert_equal("(select a) union (select b)", q1.union(q2).to_sql)
|
136
|
+
|
137
|
+
q3 = q1.union(q2).union { select c }
|
138
|
+
assert_equal("((select a) union (select b)) union (select c)", q3.to_sql)
|
139
|
+
|
140
|
+
q4 = q1.union(Q { select b}, Q { select c })
|
141
|
+
assert_equal("(select a) union (select b) union (select c)", q4.to_sql)
|
142
|
+
|
143
|
+
assert_sql("(select a) union (select b)") {
|
144
|
+
union q1, q2
|
145
|
+
}
|
146
|
+
end
|
147
|
+
|
148
|
+
def test_union_all
|
149
|
+
q1 = Q { select a }.union(all: true) { select b }
|
150
|
+
assert_equal("(select a) union all (select b)", q1.to_sql)
|
151
|
+
|
152
|
+
q2 = Q { select a }.union_all(Q { select b}, Q { select c })
|
153
|
+
assert_equal("(select a) union all (select b) union all (select c)", q2.to_sql)
|
154
|
+
end
|
155
|
+
|
156
|
+
def test_union_shorthand
|
157
|
+
q1 = Q { select a }
|
158
|
+
q2 = Q { select b }
|
159
|
+
q3 = Q { select c }
|
160
|
+
assert_equal(
|
161
|
+
'((select a) union (select b)) union (select c)',
|
162
|
+
(q1 | q2 | q3).to_sql
|
163
|
+
)
|
164
|
+
end
|
165
|
+
|
166
|
+
def test_intersect
|
167
|
+
query = Q { select a }.intersect { select b}
|
168
|
+
assert_equal("(select a) intersect (select b)", query.to_sql)
|
169
|
+
|
170
|
+
q1 = Q { select a }
|
171
|
+
q2 = Q { select b }
|
172
|
+
assert_equal("(select a) intersect (select b)", q1.intersect(q2).to_sql)
|
173
|
+
|
174
|
+
q3 = q1.intersect(q2).intersect { select c }
|
175
|
+
assert_equal("((select a) intersect (select b)) intersect (select c)", q3.to_sql)
|
176
|
+
|
177
|
+
q4 = q1.intersect(Q { select b}, Q { select c })
|
178
|
+
assert_equal("(select a) intersect (select b) intersect (select c)", q4.to_sql)
|
179
|
+
|
180
|
+
assert_sql("(select a) intersect (select b)") {
|
181
|
+
intersect q1, q2
|
182
|
+
}
|
183
|
+
end
|
184
|
+
|
185
|
+
def test_intersect_all
|
186
|
+
q1 = Q { select a }.intersect(all: true) { select b }
|
187
|
+
assert_equal("(select a) intersect all (select b)", q1.to_sql)
|
188
|
+
|
189
|
+
q2 = Q { select a }.intersect_all(Q { select b}, Q { select c })
|
190
|
+
assert_equal("(select a) intersect all (select b) intersect all (select c)", q2.to_sql)
|
191
|
+
end
|
192
|
+
|
193
|
+
def test_intersect_shorthand
|
194
|
+
q1 = Q { select a }
|
195
|
+
q2 = Q { select b }
|
196
|
+
q3 = Q { select c }
|
197
|
+
assert_equal(
|
198
|
+
'((select a) intersect (select b)) intersect (select c)',
|
199
|
+
(q1 & q2 & q3).to_sql
|
200
|
+
)
|
201
|
+
end
|
202
|
+
|
203
|
+
def test_except
|
204
|
+
query = Q { select a }.except { select b}
|
205
|
+
assert_equal("(select a) except (select b)", query.to_sql)
|
206
|
+
|
207
|
+
q1 = Q { select a }
|
208
|
+
q2 = Q { select b }
|
209
|
+
assert_equal("(select a) except (select b)", q1.except(q2).to_sql)
|
210
|
+
|
211
|
+
q3 = q1.except(q2).except { select c }
|
212
|
+
assert_equal("((select a) except (select b)) except (select c)", q3.to_sql)
|
213
|
+
|
214
|
+
q4 = q1.except(Q { select b}, Q { select c })
|
215
|
+
assert_equal("(select a) except (select b) except (select c)", q4.to_sql)
|
216
|
+
|
217
|
+
assert_sql("(select a) except (select b)") {
|
218
|
+
except q1, q2
|
219
|
+
}
|
220
|
+
end
|
221
|
+
|
222
|
+
def test_except_all
|
223
|
+
q1 = Q { select a }.except(all: true) { select b }
|
224
|
+
assert_equal("(select a) except all (select b)", q1.to_sql)
|
225
|
+
|
226
|
+
q2 = Q { select a }.except_all(Q { select b}, Q { select c })
|
227
|
+
assert_equal("(select a) except all (select b) except all (select c)", q2.to_sql)
|
228
|
+
end
|
229
|
+
|
230
|
+
def test_except_shorthand
|
231
|
+
q1 = Q { select a }
|
232
|
+
q2 = Q { select b }
|
233
|
+
q3 = Q { select c }
|
234
|
+
assert_equal(
|
235
|
+
'((select a) except (select b)) except (select c)',
|
236
|
+
(q1 ^ q2 ^ q3).to_sql
|
237
|
+
)
|
238
|
+
end
|
239
|
+
|
240
|
+
def test_combination
|
241
|
+
q1 = Q { select a }
|
242
|
+
q2 = Q { select b }
|
243
|
+
q3 = Q { select c }
|
244
|
+
assert_equal(
|
245
|
+
'((select a) union (select b)) intersect (select c)',
|
246
|
+
((q1 | q2) & q3).to_sql
|
247
|
+
)
|
248
|
+
|
249
|
+
assert_equal(
|
250
|
+
'((select 1) union (select 2)) intersect (select 1)',
|
251
|
+
((q1 | q2) & q3).to_sql(a: 1, b: 2, c: 1)
|
252
|
+
)
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
class UseCasesTest < T
|
257
|
+
class Eno::SQL
|
258
|
+
def extract_epoch_from(sym)
|
259
|
+
ExtractEpoch.new(sym)
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
class ExtractEpoch < Eno::Expression
|
264
|
+
def to_sql(sql)
|
265
|
+
"extract (epoch from #{sql.quote(@members[0])})::integer"
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
def test_1
|
270
|
+
assert_sql("select extract (epoch from stamp)::integer as stamp, quality, value, unformatted_value, datatype from states where ((path = '/r1') and (stamp >= '2019-01-01 00:00:00+00') and (stamp < '2019-01-02 00:00:00+00')) order by stamp desc limit 1") {
|
271
|
+
select extract_epoch_from(stamp).as(stamp), quality, value, unformatted_value, datatype
|
272
|
+
from states
|
273
|
+
where (path == '/r1') & (stamp >= '2019-01-01 00:00:00+00') & (stamp < '2019-01-02 00:00:00+00')
|
274
|
+
order_by stamp.desc
|
275
|
+
limit 1
|
276
|
+
}
|
277
|
+
end
|
278
|
+
|
279
|
+
def test_2
|
280
|
+
# http://www.postgresqltutorial.com/postgresql-window-function/
|
281
|
+
assert_sql('select product_name, price, group_name, avg(price) over (partition by group_name) from products inner join product_groups using (group_id)') {
|
282
|
+
select product_name,
|
283
|
+
price,
|
284
|
+
group_name,
|
285
|
+
avg(price).over { partition_by group_name }
|
286
|
+
from products.inner_join(product_groups, using: group_id)
|
287
|
+
}
|
288
|
+
end
|
289
|
+
|
290
|
+
def test_3
|
291
|
+
# http://www.postgresqltutorial.com/postgresql-window-function/
|
292
|
+
assert_sql('select product_name, group_name, price, row_number() over (partition by group_name order by price) from products inner join product_groups using (group_id)') {
|
293
|
+
select product_name,
|
294
|
+
group_name,
|
295
|
+
price,
|
296
|
+
row_number(_).over {
|
297
|
+
partition_by group_name
|
298
|
+
order_by price
|
299
|
+
}
|
300
|
+
from products.inner_join(product_groups, using: group_id)
|
301
|
+
}
|
302
|
+
end
|
303
|
+
|
304
|
+
def test_4
|
305
|
+
# http://www.postgresqltutorial.com/postgresql-window-function/
|
306
|
+
assert_sql('select product_name, group_name, price, lag(price, 1) over (partition by group_name order by price) as prev_price, (price - lag(price, 1) over (partition by group_name order by price)) as cur_prev_diff from products inner join product_groups using (group_id)') {
|
307
|
+
select product_name,
|
308
|
+
group_name,
|
309
|
+
price,
|
310
|
+
lag(price, 1).over {
|
311
|
+
partition_by group_name
|
312
|
+
order_by price
|
313
|
+
}.as(prev_price),
|
314
|
+
(price - lag(price, 1).over {
|
315
|
+
partition_by group_name
|
316
|
+
order_by price
|
317
|
+
}).as(cur_prev_diff)
|
318
|
+
from products.inner_join product_groups, using: group_id
|
319
|
+
}
|
320
|
+
end
|
321
|
+
end
|