sequel_core 1.0
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.
- data/CHANGELOG +1003 -0
- data/COPYING +18 -0
- data/README +81 -0
- data/Rakefile +176 -0
- data/bin/sequel +41 -0
- data/lib/sequel_core.rb +59 -0
- data/lib/sequel_core/adapters/adapter_skeleton.rb +68 -0
- data/lib/sequel_core/adapters/ado.rb +100 -0
- data/lib/sequel_core/adapters/db2.rb +158 -0
- data/lib/sequel_core/adapters/dbi.rb +126 -0
- data/lib/sequel_core/adapters/informix.rb +87 -0
- data/lib/sequel_core/adapters/jdbc.rb +108 -0
- data/lib/sequel_core/adapters/mysql.rb +269 -0
- data/lib/sequel_core/adapters/odbc.rb +145 -0
- data/lib/sequel_core/adapters/odbc_mssql.rb +93 -0
- data/lib/sequel_core/adapters/openbase.rb +90 -0
- data/lib/sequel_core/adapters/oracle.rb +99 -0
- data/lib/sequel_core/adapters/postgres.rb +519 -0
- data/lib/sequel_core/adapters/sqlite.rb +192 -0
- data/lib/sequel_core/array_keys.rb +296 -0
- data/lib/sequel_core/connection_pool.rb +152 -0
- data/lib/sequel_core/core_ext.rb +59 -0
- data/lib/sequel_core/core_sql.rb +191 -0
- data/lib/sequel_core/database.rb +433 -0
- data/lib/sequel_core/dataset.rb +409 -0
- data/lib/sequel_core/dataset/convenience.rb +321 -0
- data/lib/sequel_core/dataset/sequelizer.rb +354 -0
- data/lib/sequel_core/dataset/sql.rb +586 -0
- data/lib/sequel_core/exceptions.rb +45 -0
- data/lib/sequel_core/migration.rb +191 -0
- data/lib/sequel_core/model.rb +8 -0
- data/lib/sequel_core/pretty_table.rb +73 -0
- data/lib/sequel_core/schema.rb +8 -0
- data/lib/sequel_core/schema/schema_generator.rb +131 -0
- data/lib/sequel_core/schema/schema_sql.rb +131 -0
- data/lib/sequel_core/worker.rb +58 -0
- data/spec/adapters/informix_spec.rb +139 -0
- data/spec/adapters/mysql_spec.rb +330 -0
- data/spec/adapters/oracle_spec.rb +130 -0
- data/spec/adapters/postgres_spec.rb +189 -0
- data/spec/adapters/sqlite_spec.rb +345 -0
- data/spec/array_keys_spec.rb +679 -0
- data/spec/connection_pool_spec.rb +356 -0
- data/spec/core_ext_spec.rb +67 -0
- data/spec/core_sql_spec.rb +301 -0
- data/spec/database_spec.rb +812 -0
- data/spec/dataset_spec.rb +2381 -0
- data/spec/migration_spec.rb +261 -0
- data/spec/pretty_table_spec.rb +66 -0
- data/spec/rcov.opts +4 -0
- data/spec/schema_generator_spec.rb +86 -0
- data/spec/schema_spec.rb +230 -0
- data/spec/sequelizer_spec.rb +448 -0
- data/spec/spec.opts +5 -0
- data/spec/spec_helper.rb +44 -0
- data/spec/worker_spec.rb +96 -0
- metadata +162 -0
@@ -0,0 +1,354 @@
|
|
1
|
+
class Sequel::Dataset
|
2
|
+
# The Sequelizer module includes methods for translating Ruby expressions
|
3
|
+
# into SQL expressions, making it possible to specify dataset filters using
|
4
|
+
# blocks, e.g.:
|
5
|
+
#
|
6
|
+
# DB[:items].filter {:price < 100}
|
7
|
+
# DB[:items].filter {:category == 'ruby' && :date < 3.days.ago}
|
8
|
+
#
|
9
|
+
# Block filters can refer to literals, variables, constants, arguments,
|
10
|
+
# instance variables or anything else in order to create parameterized
|
11
|
+
# queries. Block filters can also refer to other dataset objects as
|
12
|
+
# sub-queries. Block filters are pretty much limitless!
|
13
|
+
#
|
14
|
+
# Block filters are based on ParseTree. If you do not have the ParseTree
|
15
|
+
# gem installed, block filters will raise an error.
|
16
|
+
#
|
17
|
+
# To enable full block filter support make sure you have both ParseTree and
|
18
|
+
# Ruby2Ruby installed:
|
19
|
+
#
|
20
|
+
# sudo gem install parsetree
|
21
|
+
# sudo gem install ruby2ruby
|
22
|
+
module Sequelizer
|
23
|
+
# Formats an comparison expression involving a left value and a right
|
24
|
+
# value. Comparison expressions differ according to the class of the right
|
25
|
+
# value. The stock implementation supports Range (inclusive and exclusive),
|
26
|
+
# Array (as a list of values to compare against), Dataset (as a subquery to
|
27
|
+
# compare against), or a regular value.
|
28
|
+
#
|
29
|
+
# dataset.compare_expr('id', 1..20) #=>
|
30
|
+
# "(id >= 1 AND id <= 20)"
|
31
|
+
# dataset.compare_expr('id', [3,6,10]) #=>
|
32
|
+
# "(id IN (3, 6, 10))"
|
33
|
+
# dataset.compare_expr('id', DB[:items].select(:id)) #=>
|
34
|
+
# "(id IN (SELECT id FROM items))"
|
35
|
+
# dataset.compare_expr('id', nil) #=>
|
36
|
+
# "(id IS NULL)"
|
37
|
+
# dataset.compare_expr('id', 3) #=>
|
38
|
+
# "(id = 3)"
|
39
|
+
def compare_expr(l, r)
|
40
|
+
case r
|
41
|
+
when Range
|
42
|
+
r.exclude_end? ? \
|
43
|
+
"(#{literal(l)} >= #{literal(r.begin)} AND #{literal(l)} < #{literal(r.end)})" : \
|
44
|
+
"(#{literal(l)} >= #{literal(r.begin)} AND #{literal(l)} <= #{literal(r.end)})"
|
45
|
+
when Array
|
46
|
+
"(#{literal(l)} IN (#{literal(r)}))"
|
47
|
+
when Sequel::Dataset
|
48
|
+
"(#{literal(l)} IN (#{r.sql}))"
|
49
|
+
when NilClass
|
50
|
+
"(#{literal(l)} IS NULL)"
|
51
|
+
when Regexp
|
52
|
+
match_expr(l, r)
|
53
|
+
else
|
54
|
+
"(#{literal(l)} = #{literal(r)})"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Formats a string matching expression. The stock implementation supports
|
59
|
+
# matching against strings only using the LIKE operator. Specific adapters
|
60
|
+
# can override this method to provide support for regular expressions.
|
61
|
+
def match_expr(l, r)
|
62
|
+
case r
|
63
|
+
when String
|
64
|
+
"(#{literal(l)} LIKE #{literal(r)})"
|
65
|
+
else
|
66
|
+
raise Sequel::Error, "Unsupported match pattern class (#{r.class})."
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Evaluates a method call. This method is used to evaluate Ruby expressions
|
71
|
+
# referring to indirect values, e.g.:
|
72
|
+
#
|
73
|
+
# dataset.filter {:category => category.to_s}
|
74
|
+
# dataset.filter {:x > y[0..3]}
|
75
|
+
#
|
76
|
+
# This method depends on the Ruby2Ruby gem. If you do not have Ruby2Ruby
|
77
|
+
# installed, this method will raise an error.
|
78
|
+
def ext_expr(e, b, opts)
|
79
|
+
eval(RubyToRuby.new.process(e), b)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Translates a method call parse-tree to SQL expression. The following
|
83
|
+
# operators are recognized and translated to SQL expressions: >, <, >=, <=,
|
84
|
+
# ==, =~, +, -, *, /, %:
|
85
|
+
#
|
86
|
+
# :x == 1 #=> "(x = 1)"
|
87
|
+
# (:x + 100) < 200 #=> "((x + 100) < 200)"
|
88
|
+
#
|
89
|
+
# The in, in?, nil and nil? method calls are intercepted and passed to
|
90
|
+
# #compare_expr.
|
91
|
+
#
|
92
|
+
# :x.in [1, 2, 3] #=> "(x IN (1, 2, 3))"
|
93
|
+
# :x.in?(DB[:y].select(:z)) #=> "(x IN (SELECT z FROM y))"
|
94
|
+
# :x.nil? #=> "(x IS NULL)"
|
95
|
+
#
|
96
|
+
# The like and like? method calls are intercepted and passed to #match_expr.
|
97
|
+
#
|
98
|
+
# :x.like? 'ABC%' #=> "(x LIKE 'ABC%')"
|
99
|
+
#
|
100
|
+
# The method also supports SQL functions by invoking Symbol#[]:
|
101
|
+
#
|
102
|
+
# :avg[:x] #=> "avg(x)"
|
103
|
+
# :substring[:x, 5] #=> "substring(x, 5)"
|
104
|
+
#
|
105
|
+
# All other method calls are evaulated as normal Ruby code.
|
106
|
+
def call_expr(e, b, opts)
|
107
|
+
case op = e[2]
|
108
|
+
when :>, :<, :>=, :<=
|
109
|
+
l = eval_expr(e[1], b, opts)
|
110
|
+
r = eval_expr(e[3][1], b, opts)
|
111
|
+
if l.is_one_of?(Symbol, Sequel::LiteralString, Sequel::SQL::Expression) || \
|
112
|
+
r.is_one_of?(Symbol, Sequel::LiteralString, Sequel::SQL::Expression)
|
113
|
+
"(#{literal(l)} #{op} #{literal(r)})"
|
114
|
+
else
|
115
|
+
ext_expr(e, b, opts)
|
116
|
+
end
|
117
|
+
when :==
|
118
|
+
l = eval_expr(e[1], b, opts)
|
119
|
+
r = eval_expr(e[3][1], b, opts)
|
120
|
+
compare_expr(l, r)
|
121
|
+
when :=~
|
122
|
+
l = eval_expr(e[1], b, opts)
|
123
|
+
r = eval_expr(e[3][1], b, opts)
|
124
|
+
match_expr(l, r)
|
125
|
+
when :+, :-, :*, :%, :/
|
126
|
+
l = eval_expr(e[1], b, opts)
|
127
|
+
r = eval_expr(e[3][1], b, opts)
|
128
|
+
if l.is_one_of?(Symbol, Sequel::LiteralString, Sequel::SQL::Expression) || \
|
129
|
+
r.is_one_of?(Symbol, Sequel::LiteralString, Sequel::SQL::Expression)
|
130
|
+
"(#{literal(l)} #{op} #{literal(r)})".lit
|
131
|
+
else
|
132
|
+
ext_expr(e, b, opts)
|
133
|
+
end
|
134
|
+
when :<<
|
135
|
+
l = eval_expr(e[1], b, opts)
|
136
|
+
r = eval_expr(e[3][1], b, opts)
|
137
|
+
"#{literal(l)} = #{literal(r)}".lit
|
138
|
+
when :|
|
139
|
+
l = eval_expr(e[1], b, opts)
|
140
|
+
r = eval_expr(e[3][1], b, opts)
|
141
|
+
if l.is_one_of?(Symbol, Sequel::SQL::Subscript)
|
142
|
+
l|r
|
143
|
+
else
|
144
|
+
ext_expr(e, b, opts)
|
145
|
+
end
|
146
|
+
when :in, :in?
|
147
|
+
# in/in? operators are supported using two forms:
|
148
|
+
# :x.in([1, 2, 3])
|
149
|
+
# :x.in(1, 2, 3) # variable arity
|
150
|
+
l = eval_expr(e[1], b, opts)
|
151
|
+
r = eval_expr((e[3].size == 2) ? e[3][1] : e[3], b, opts)
|
152
|
+
compare_expr(l, r)
|
153
|
+
when :nil, :nil?
|
154
|
+
l = eval_expr(e[1], b, opts)
|
155
|
+
compare_expr(l, nil)
|
156
|
+
when :like, :like?
|
157
|
+
l = eval_expr(e[1], b, opts)
|
158
|
+
r = eval_expr(e[3][1], b, opts)
|
159
|
+
match_expr(l, r)
|
160
|
+
else
|
161
|
+
if (op == :[]) && (e[1][0] == :lit) && (Symbol === e[1][1])
|
162
|
+
# SQL Functions, e.g.: :sum[:x]
|
163
|
+
if e[3]
|
164
|
+
e[1][1][*eval_expr(e[3], b, opts)]
|
165
|
+
else
|
166
|
+
e[1][1][]
|
167
|
+
end
|
168
|
+
else
|
169
|
+
# external code
|
170
|
+
ext_expr(e, b, opts)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def fcall_expr(e, b, opts) #:nodoc:
|
176
|
+
ext_expr(e, b, opts)
|
177
|
+
end
|
178
|
+
|
179
|
+
def vcall_expr(e, b, opts) #:nodoc:
|
180
|
+
eval(e[1].to_s, b)
|
181
|
+
end
|
182
|
+
|
183
|
+
def iter_expr(e, b, opts) #:nodoc:
|
184
|
+
if e[1][0] == :call && e[1][2] == :each
|
185
|
+
unfold_each_expr(e, b, opts)
|
186
|
+
elsif e[1] == [:fcall, :proc]
|
187
|
+
eval_expr(e[3], b, opts) # inline proc
|
188
|
+
else
|
189
|
+
ext_expr(e, b, opts) # method call with inline proc
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def replace_dvars(a, values)
|
194
|
+
a.map do |i|
|
195
|
+
if i.is_a?(Array) && (i[0] == :dvar)
|
196
|
+
if v = values[i[1]]
|
197
|
+
value_to_parse_tree(v)
|
198
|
+
else
|
199
|
+
i
|
200
|
+
end
|
201
|
+
elsif Array === i
|
202
|
+
replace_dvars(i, values)
|
203
|
+
else
|
204
|
+
i
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
def value_to_parse_tree(value)
|
210
|
+
c = Class.new
|
211
|
+
c.class_eval("def m; #{value.inspect}; end")
|
212
|
+
ParseTree.translate(c, :m)[2][1][2]
|
213
|
+
end
|
214
|
+
|
215
|
+
def unfold_each_expr(e, b, opts) #:nodoc:
|
216
|
+
source = eval_expr(e[1][1], b, opts)
|
217
|
+
block_dvars = []
|
218
|
+
if e[2][0] == :dasgn_curr
|
219
|
+
block_dvars << e[2][1]
|
220
|
+
elsif e[2][0] == :masgn
|
221
|
+
e[2][1].each do |i|
|
222
|
+
if i.is_a?(Array) && i[0] == :dasgn_curr
|
223
|
+
block_dvars << i[1]
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
new_block = [:block]
|
228
|
+
|
229
|
+
source.each do |*dvars|
|
230
|
+
iter_values = (Array === dvars[0]) ? dvars[0] : dvars
|
231
|
+
values = block_dvars.inject({}) {|m, i| m[i] = iter_values.shift; m}
|
232
|
+
iter = replace_dvars(e[3], values)
|
233
|
+
new_block << iter
|
234
|
+
end
|
235
|
+
|
236
|
+
pt_expr(new_block, b, opts)
|
237
|
+
end
|
238
|
+
|
239
|
+
# Evaluates a parse-tree into an SQL expression.
|
240
|
+
def eval_expr(e, b, opts)
|
241
|
+
case e[0]
|
242
|
+
when :call # method call
|
243
|
+
call_expr(e, b, opts)
|
244
|
+
when :fcall
|
245
|
+
fcall_expr(e, b, opts)
|
246
|
+
when :vcall
|
247
|
+
vcall_expr(e, b, opts)
|
248
|
+
when :ivar, :cvar, :dvar, :const, :gvar # local ref
|
249
|
+
eval(e[1].to_s, b)
|
250
|
+
when :nth_ref
|
251
|
+
eval("$#{e[1]}", b)
|
252
|
+
when :lvar # local context
|
253
|
+
if e[1] == :block
|
254
|
+
pr = eval(e[1].to_s, b)
|
255
|
+
"#{proc_to_sql(pr)}"
|
256
|
+
else
|
257
|
+
eval(e[1].to_s, b)
|
258
|
+
end
|
259
|
+
when :lit, :str # literal
|
260
|
+
e[1]
|
261
|
+
when :dot2 # inclusive range
|
262
|
+
eval_expr(e[1], b, opts)..eval_expr(e[2], b, opts)
|
263
|
+
when :dot3 # exclusive range
|
264
|
+
eval_expr(e[1], b, opts)...eval_expr(e[2], b, opts)
|
265
|
+
when :colon2 # qualified constant ref
|
266
|
+
eval_expr(e[1], b, opts).const_get(e[2])
|
267
|
+
when :false
|
268
|
+
false
|
269
|
+
when :true
|
270
|
+
true
|
271
|
+
when :nil
|
272
|
+
nil
|
273
|
+
when :array
|
274
|
+
# array
|
275
|
+
e[1..-1].map {|i| eval_expr(i, b, opts)}
|
276
|
+
when :match3
|
277
|
+
# =~/!~ operator
|
278
|
+
l = eval_expr(e[2], b, opts)
|
279
|
+
r = eval_expr(e[1], b, opts)
|
280
|
+
compare_expr(l, r)
|
281
|
+
when :iter
|
282
|
+
iter_expr(e, b, opts)
|
283
|
+
when :dasgn, :dasgn_curr
|
284
|
+
# assignment
|
285
|
+
l = e[1]
|
286
|
+
r = eval_expr(e[2], b, opts)
|
287
|
+
raise Sequel::Error::InvalidExpression, "#{l} = #{r}. Did you mean :#{l} == #{r}?"
|
288
|
+
when :if, :dstr
|
289
|
+
ext_expr(e, b, opts)
|
290
|
+
else
|
291
|
+
raise Sequel::Error::InvalidExpression, "Invalid expression tree: #{e.inspect}"
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
JOIN_AND = " AND ".freeze
|
296
|
+
JOIN_COMMA = ", ".freeze
|
297
|
+
|
298
|
+
def pt_expr(e, b, opts = {}) #:nodoc:
|
299
|
+
case e[0]
|
300
|
+
when :not # negation: !x, (x != y), (x !~ y)
|
301
|
+
if (e[1][0] == :lit) && (Symbol === e[1][1])
|
302
|
+
# translate (!:x) into (x = 'f')
|
303
|
+
compare_expr(e[1][1], false)
|
304
|
+
else
|
305
|
+
"(NOT #{pt_expr(e[1], b, opts)})"
|
306
|
+
end
|
307
|
+
when :and # x && y
|
308
|
+
"(#{e[1..-1].map {|i| pt_expr(i, b, opts)}.join(JOIN_AND)})"
|
309
|
+
when :or # x || y
|
310
|
+
"(#{pt_expr(e[1], b, opts)} OR #{pt_expr(e[2], b, opts)})"
|
311
|
+
when :call, :vcall, :iter, :match3 # method calls, blocks
|
312
|
+
eval_expr(e, b, opts)
|
313
|
+
when :block # block of statements
|
314
|
+
if opts[:comma_separated]
|
315
|
+
"#{e[1..-1].map {|i| pt_expr(i, b, opts)}.join(JOIN_COMMA)}"
|
316
|
+
else
|
317
|
+
"(#{e[1..-1].map {|i| pt_expr(i, b, opts)}.join(JOIN_AND)})"
|
318
|
+
end
|
319
|
+
else # literals
|
320
|
+
if e == [:lvar, :block]
|
321
|
+
eval_expr(e, b, opts)
|
322
|
+
else
|
323
|
+
literal(eval_expr(e, b, opts))
|
324
|
+
end
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
# Translates a Ruby block into an SQL expression.
|
329
|
+
def proc_to_sql(proc, opts = {})
|
330
|
+
c = Class.new {define_method(:m, &proc)}
|
331
|
+
pt_expr(ParseTree.translate(c, :m)[2][2], proc.binding, opts)
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
begin
|
337
|
+
require 'parse_tree'
|
338
|
+
rescue Exception
|
339
|
+
module Sequel::Dataset::Sequelizer
|
340
|
+
def proc_to_sql(*args)
|
341
|
+
raise Sequel::Error, "You must have the ParseTree gem installed in order to use block filters."
|
342
|
+
end
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
begin
|
347
|
+
require 'ruby2ruby'
|
348
|
+
rescue Exception
|
349
|
+
module Sequel::Dataset::Sequelizer
|
350
|
+
def ext_expr(*args)
|
351
|
+
raise Sequel::Error, "You must have the Ruby2Ruby gem installed in order to use this block filter."
|
352
|
+
end
|
353
|
+
end
|
354
|
+
end
|
@@ -0,0 +1,586 @@
|
|
1
|
+
module Sequel
|
2
|
+
class Dataset
|
3
|
+
# The Dataset SQL module implements all the dataset methods concerned with
|
4
|
+
# generating SQL statements for retrieving and manipulating records.
|
5
|
+
module SQL
|
6
|
+
# Adds quoting to column references. This method is just a stub and can
|
7
|
+
# be overriden in adapters in order to provide correct column quoting
|
8
|
+
# behavior.
|
9
|
+
def quote_column_ref(name); name.to_s; end
|
10
|
+
|
11
|
+
ALIASED_REGEXP = /^(.*)\s(.*)$/.freeze
|
12
|
+
QUALIFIED_REGEXP = /^(.*)\.(.*)$/.freeze
|
13
|
+
|
14
|
+
# Returns a qualified column name (including a table name) if the column
|
15
|
+
# name isn't already qualified.
|
16
|
+
def qualified_column_name(column, table)
|
17
|
+
s = literal(column)
|
18
|
+
if s =~ QUALIFIED_REGEXP
|
19
|
+
return column
|
20
|
+
else
|
21
|
+
if (table =~ ALIASED_REGEXP)
|
22
|
+
table = $2
|
23
|
+
end
|
24
|
+
Sequel::SQL::QualifiedColumnRef.new(table, column)
|
25
|
+
# "#{table}.#{column}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
WILDCARD = '*'.freeze
|
30
|
+
COMMA_SEPARATOR = ", ".freeze
|
31
|
+
|
32
|
+
# Converts an array of column names into a comma seperated string of
|
33
|
+
# column names. If the array is empty, a wildcard (*) is returned.
|
34
|
+
def column_list(columns)
|
35
|
+
if columns.empty?
|
36
|
+
WILDCARD
|
37
|
+
else
|
38
|
+
m = columns.map do |i|
|
39
|
+
i.is_a?(Hash) ? i.map {|kv| "#{literal(kv[0])} AS #{kv[1]}"} : literal(i)
|
40
|
+
end
|
41
|
+
m.join(COMMA_SEPARATOR)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Converts an array of sources names into into a comma separated list.
|
46
|
+
def source_list(source)
|
47
|
+
if source.nil? || source.empty?
|
48
|
+
raise Error, 'No source specified for query'
|
49
|
+
end
|
50
|
+
auto_alias_count = 0
|
51
|
+
m = source.map do |i|
|
52
|
+
case i
|
53
|
+
when Dataset
|
54
|
+
auto_alias_count += 1
|
55
|
+
i.to_table_reference(auto_alias_count)
|
56
|
+
when Hash
|
57
|
+
i.map {|k, v| "#{k.is_a?(Dataset) ? k.to_table_reference : k} #{v}"}.
|
58
|
+
join(COMMA_SEPARATOR)
|
59
|
+
else
|
60
|
+
i
|
61
|
+
end
|
62
|
+
end
|
63
|
+
m.join(COMMA_SEPARATOR)
|
64
|
+
end
|
65
|
+
|
66
|
+
NULL = "NULL".freeze
|
67
|
+
TIMESTAMP_FORMAT = "TIMESTAMP '%Y-%m-%d %H:%M:%S'".freeze
|
68
|
+
DATE_FORMAT = "DATE '%Y-%m-%d'".freeze
|
69
|
+
TRUE = "'t'".freeze
|
70
|
+
FALSE = "'f'".freeze
|
71
|
+
|
72
|
+
# Returns a literal representation of a value to be used as part
|
73
|
+
# of an SQL expression. The stock implementation supports literalization
|
74
|
+
# of String (with proper escaping to prevent SQL injections), numbers,
|
75
|
+
# Symbol (as column references), Array (as a list of literalized values),
|
76
|
+
# Time (as an SQL TIMESTAMP), Date (as an SQL DATE), Dataset (as a
|
77
|
+
# subquery) and nil (AS NULL).
|
78
|
+
#
|
79
|
+
# dataset.literal("abc'def") #=> "'abc''def'"
|
80
|
+
# dataset.literal(:items__id) #=> "items.id"
|
81
|
+
# dataset.literal([1, 2, 3]) => "(1, 2, 3)"
|
82
|
+
# dataset.literal(DB[:items]) => "(SELECT * FROM items)"
|
83
|
+
#
|
84
|
+
# If an unsupported object is given, an exception is raised.
|
85
|
+
def literal(v)
|
86
|
+
case v
|
87
|
+
when LiteralString
|
88
|
+
v
|
89
|
+
when String
|
90
|
+
"'#{v.gsub(/'/, "''")}'"
|
91
|
+
when Integer, Float
|
92
|
+
v.to_s
|
93
|
+
when BigDecimal
|
94
|
+
v.to_s("F")
|
95
|
+
when NilClass
|
96
|
+
NULL
|
97
|
+
when TrueClass
|
98
|
+
TRUE
|
99
|
+
when FalseClass
|
100
|
+
FALSE
|
101
|
+
when Symbol
|
102
|
+
v.to_column_ref(self)
|
103
|
+
when Sequel::SQL::Expression
|
104
|
+
v.to_s(self)
|
105
|
+
when Array
|
106
|
+
v.empty? ? NULL : v.map {|i| literal(i)}.join(COMMA_SEPARATOR)
|
107
|
+
when Time
|
108
|
+
v.strftime(TIMESTAMP_FORMAT)
|
109
|
+
when Date
|
110
|
+
v.strftime(DATE_FORMAT)
|
111
|
+
when Dataset
|
112
|
+
"(#{v.sql})"
|
113
|
+
else
|
114
|
+
raise Error, "can't express #{v.inspect} as a SQL literal"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
AND_SEPARATOR = " AND ".freeze
|
119
|
+
QUESTION_MARK = '?'.freeze
|
120
|
+
|
121
|
+
# Formats a where clause. If parenthesize is true, then the whole
|
122
|
+
# generated clause will be enclosed in a set of parentheses.
|
123
|
+
def expression_list(expr, parenthesize = false)
|
124
|
+
case expr
|
125
|
+
when Hash
|
126
|
+
parenthesize = false if expr.size == 1
|
127
|
+
fmt = expr.map {|i| compare_expr(i[0], i[1])}.join(AND_SEPARATOR)
|
128
|
+
when Array
|
129
|
+
fmt = expr.shift.gsub(QUESTION_MARK) {literal(expr.shift)}
|
130
|
+
when Proc
|
131
|
+
fmt = proc_to_sql(expr)
|
132
|
+
else
|
133
|
+
# if the expression is compound, it should be parenthesized in order for
|
134
|
+
# things to be predictable (when using #or and #and.)
|
135
|
+
parenthesize |= expr =~ /\).+\(/
|
136
|
+
fmt = expr
|
137
|
+
end
|
138
|
+
parenthesize ? "(#{fmt})" : fmt
|
139
|
+
end
|
140
|
+
|
141
|
+
# Returns a copy of the dataset with the source changed.
|
142
|
+
def from(*source)
|
143
|
+
clone_merge(:from => source)
|
144
|
+
end
|
145
|
+
|
146
|
+
# Returns a copy of the dataset with the selected columns changed.
|
147
|
+
def select(*columns)
|
148
|
+
clone_merge(:select => columns)
|
149
|
+
end
|
150
|
+
|
151
|
+
# Returns a copy of the dataset with the distinct option.
|
152
|
+
def uniq(*args)
|
153
|
+
clone_merge(:distinct => args)
|
154
|
+
end
|
155
|
+
alias_method :distinct, :uniq
|
156
|
+
|
157
|
+
# Returns a copy of the dataset with the order changed.
|
158
|
+
def order(*order)
|
159
|
+
clone_merge(:order => order)
|
160
|
+
end
|
161
|
+
|
162
|
+
alias_method :order_by, :order
|
163
|
+
|
164
|
+
# Returns a copy of the dataset with the order reversed. If no order is
|
165
|
+
# given, the existing order is inverted.
|
166
|
+
def reverse_order(*order)
|
167
|
+
order(*invert_order(order.empty? ? @opts[:order] : order))
|
168
|
+
end
|
169
|
+
|
170
|
+
# Inverts the given order by breaking it into a list of column references
|
171
|
+
# and inverting them.
|
172
|
+
#
|
173
|
+
# dataset.invert_order([:id.desc]]) #=> [:id]
|
174
|
+
# dataset.invert_order(:category, :price.desc]) #=>
|
175
|
+
# [:category.desc, :price]
|
176
|
+
def invert_order(order)
|
177
|
+
new_order = []
|
178
|
+
order.map do |f|
|
179
|
+
if f.is_a?(Sequel::SQL::ColumnExpr) && (f.op == Sequel::SQL::ColumnMethods::DESC)
|
180
|
+
f.l
|
181
|
+
else
|
182
|
+
f.desc
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
# Returns a copy of the dataset with the results grouped by the value of
|
188
|
+
# the given columns
|
189
|
+
def group(*columns)
|
190
|
+
clone_merge(:group => columns)
|
191
|
+
end
|
192
|
+
|
193
|
+
alias_method :group_by, :group
|
194
|
+
|
195
|
+
# Returns a copy of the dataset with the given conditions imposed upon it.
|
196
|
+
# If the query has been grouped, then the conditions are imposed in the
|
197
|
+
# HAVING clause. If not, then they are imposed in the WHERE clause. Filter
|
198
|
+
# accepts a Hash (formated into a list of equality expressions), an Array
|
199
|
+
# (formatted ala ActiveRecord conditions), a String (taken literally), or
|
200
|
+
# a block that is converted into expressions.
|
201
|
+
#
|
202
|
+
# dataset.filter(:id => 3).sql #=>
|
203
|
+
# "SELECT * FROM items WHERE (id = 3)"
|
204
|
+
# dataset.filter('price < ?', 100).sql #=>
|
205
|
+
# "SELECT * FROM items WHERE price < 100"
|
206
|
+
# dataset.filter('price < 100').sql #=>
|
207
|
+
# "SELECT * FROM items WHERE price < 100"
|
208
|
+
# dataset.filter {price < 100}.sql #=>
|
209
|
+
# "SELECT * FROM items WHERE (price < 100)"
|
210
|
+
#
|
211
|
+
# Multiple filter calls can be chained for scoping:
|
212
|
+
#
|
213
|
+
# software = dataset.filter(:category => 'software')
|
214
|
+
# software.filter {price < 100}.sql #=>
|
215
|
+
# "SELECT * FROM items WHERE (category = 'software') AND (price < 100)"
|
216
|
+
def filter(*cond, &block)
|
217
|
+
clause = (@opts[:group] ? :having : :where)
|
218
|
+
cond = cond.first if cond.size == 1
|
219
|
+
if cond === true || cond === false
|
220
|
+
raise Error::InvalidFilter, "Invalid filter specified. Did you mean to supply a block?"
|
221
|
+
end
|
222
|
+
parenthesize = !(cond.is_a?(Hash) || cond.is_a?(Array))
|
223
|
+
filter = cond.is_a?(Hash) && cond
|
224
|
+
if @opts[clause]
|
225
|
+
l = expression_list(@opts[clause])
|
226
|
+
r = expression_list(block || cond, parenthesize)
|
227
|
+
clone_merge(clause => "#{l} AND #{r}")
|
228
|
+
else
|
229
|
+
clone_merge(:filter => cond, clause => expression_list(block || cond))
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
# Adds an alternate filter to an existing filter using OR. If no filter
|
234
|
+
# exists an error is raised.
|
235
|
+
def or(*cond, &block)
|
236
|
+
clause = (@opts[:group] ? :having : :where)
|
237
|
+
cond = cond.first if cond.size == 1
|
238
|
+
parenthesize = !(cond.is_a?(Hash) || cond.is_a?(Array))
|
239
|
+
if @opts[clause]
|
240
|
+
l = expression_list(@opts[clause])
|
241
|
+
r = expression_list(block || cond, parenthesize)
|
242
|
+
clone_merge(clause => "#{l} OR #{r}")
|
243
|
+
else
|
244
|
+
raise Error::NoExistingFilter, "No existing filter found."
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
# Adds an further filter to an existing filter using AND. If no filter
|
249
|
+
# exists an error is raised. This method is identical to #filter except
|
250
|
+
# it expects an existing filter.
|
251
|
+
def and(*cond, &block)
|
252
|
+
clause = (@opts[:group] ? :having : :where)
|
253
|
+
unless @opts[clause]
|
254
|
+
raise Error::NoExistingFilter, "No existing filter found."
|
255
|
+
end
|
256
|
+
filter(*cond, &block)
|
257
|
+
end
|
258
|
+
|
259
|
+
# Performs the inverse of Dataset#filter.
|
260
|
+
#
|
261
|
+
# dataset.exclude(:category => 'software').sql #=>
|
262
|
+
# "SELECT * FROM items WHERE NOT (category = 'software')"
|
263
|
+
def exclude(*cond, &block)
|
264
|
+
clause = (@opts[:group] ? :having : :where)
|
265
|
+
cond = cond.first if cond.size == 1
|
266
|
+
parenthesize = !(cond.is_a?(Hash) || cond.is_a?(Array))
|
267
|
+
if @opts[clause]
|
268
|
+
l = expression_list(@opts[clause])
|
269
|
+
r = expression_list(block || cond, parenthesize)
|
270
|
+
cond = "#{l} AND (NOT #{r})"
|
271
|
+
else
|
272
|
+
cond = "(NOT #{expression_list(block || cond, true)})"
|
273
|
+
end
|
274
|
+
clone_merge(clause => cond)
|
275
|
+
end
|
276
|
+
|
277
|
+
# Returns a copy of the dataset with the where conditions changed. Raises
|
278
|
+
# if the dataset has been grouped. See also #filter.
|
279
|
+
def where(*cond, &block)
|
280
|
+
if @opts[:group]
|
281
|
+
raise Error, "Can't specify a WHERE clause once the dataset has been grouped"
|
282
|
+
else
|
283
|
+
filter(*cond, &block)
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
# Returns a copy of the dataset with the having conditions changed. Raises
|
288
|
+
# if the dataset has not been grouped. See also #filter
|
289
|
+
def having(*cond, &block)
|
290
|
+
unless @opts[:group]
|
291
|
+
raise Error, "Can only specify a HAVING clause on a grouped dataset"
|
292
|
+
else
|
293
|
+
filter(*cond, &block)
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
# Adds a UNION clause using a second dataset object. If all is true the
|
298
|
+
# clause used is UNION ALL, which may return duplicate rows.
|
299
|
+
def union(dataset, all = false)
|
300
|
+
clone_merge(:union => dataset, :union_all => all)
|
301
|
+
end
|
302
|
+
|
303
|
+
# Adds an INTERSECT clause using a second dataset object. If all is true
|
304
|
+
# the clause used is INTERSECT ALL, which may return duplicate rows.
|
305
|
+
def intersect(dataset, all = false)
|
306
|
+
clone_merge(:intersect => dataset, :intersect_all => all)
|
307
|
+
end
|
308
|
+
|
309
|
+
# Adds an EXCEPT clause using a second dataset object. If all is true the
|
310
|
+
# clause used is EXCEPT ALL, which may return duplicate rows.
|
311
|
+
def except(dataset, all = false)
|
312
|
+
clone_merge(:except => dataset, :except_all => all)
|
313
|
+
end
|
314
|
+
|
315
|
+
JOIN_TYPES = {
|
316
|
+
:left_outer => 'LEFT OUTER JOIN'.freeze,
|
317
|
+
:right_outer => 'RIGHT OUTER JOIN'.freeze,
|
318
|
+
:full_outer => 'FULL OUTER JOIN'.freeze,
|
319
|
+
:inner => 'INNER JOIN'.freeze
|
320
|
+
}
|
321
|
+
|
322
|
+
# Returns a join clause based on the specified join type and condition.
|
323
|
+
def join_expr(type, table, expr)
|
324
|
+
join_type = JOIN_TYPES[type || :inner]
|
325
|
+
unless join_type
|
326
|
+
raise Error::InvalidJoinType, "Invalid join type: #{type}"
|
327
|
+
end
|
328
|
+
|
329
|
+
join_conditions = {}
|
330
|
+
expr.each do |k, v|
|
331
|
+
k = qualified_column_name(k, table) if k.is_a?(Symbol)
|
332
|
+
v = qualified_column_name(v, @opts[:last_joined_table] || @opts[:from].first) if v.is_a?(Symbol)
|
333
|
+
join_conditions[k] = v
|
334
|
+
end
|
335
|
+
" #{join_type} #{table} ON #{expression_list(join_conditions)}"
|
336
|
+
end
|
337
|
+
|
338
|
+
# Returns a joined dataset with the specified join type and condition.
|
339
|
+
def join_table(type, table, expr)
|
340
|
+
unless expr.is_a?(Hash)
|
341
|
+
expr = {expr => :id}
|
342
|
+
end
|
343
|
+
clause = join_expr(type, table, expr)
|
344
|
+
join = @opts[:join] ? @opts[:join] + clause : clause
|
345
|
+
clone_merge(:join => join, :last_joined_table => table)
|
346
|
+
end
|
347
|
+
|
348
|
+
# Returns a LEFT OUTER joined dataset.
|
349
|
+
def left_outer_join(table, expr); join_table(:left_outer, table, expr); end
|
350
|
+
|
351
|
+
# Returns a RIGHT OUTER joined dataset.
|
352
|
+
def right_outer_join(table, expr); join_table(:right_outer, table, expr); end
|
353
|
+
|
354
|
+
# Returns an OUTER joined dataset.
|
355
|
+
def full_outer_join(table, expr); join_table(:full_outer, table, expr); end
|
356
|
+
|
357
|
+
# Returns an INNER joined dataset.
|
358
|
+
def inner_join(table, expr); join_table(:inner, table, expr); end
|
359
|
+
alias join inner_join
|
360
|
+
|
361
|
+
# Inserts multiple values. If a block is given it is invoked for each
|
362
|
+
# item in the given array before inserting it.
|
363
|
+
def insert_multiple(array, &block)
|
364
|
+
if block
|
365
|
+
array.each {|i| insert(block[i])}
|
366
|
+
else
|
367
|
+
array.each {|i| insert(i)}
|
368
|
+
end
|
369
|
+
end
|
370
|
+
|
371
|
+
# Formats a SELECT statement using the given options and the dataset
|
372
|
+
# options.
|
373
|
+
def select_sql(opts = nil)
|
374
|
+
opts = opts ? @opts.merge(opts) : @opts
|
375
|
+
|
376
|
+
if sql = opts[:sql]
|
377
|
+
return sql
|
378
|
+
end
|
379
|
+
|
380
|
+
columns = opts[:select]
|
381
|
+
select_columns = columns ? column_list(columns) : WILDCARD
|
382
|
+
|
383
|
+
if distinct = opts[:distinct]
|
384
|
+
distinct_clause = distinct.empty? ? "DISTINCT" : "DISTINCT ON (#{column_list(distinct)})"
|
385
|
+
sql = "SELECT #{distinct_clause} #{select_columns}"
|
386
|
+
else
|
387
|
+
sql = "SELECT #{select_columns}"
|
388
|
+
end
|
389
|
+
|
390
|
+
if opts[:from]
|
391
|
+
sql << " FROM #{source_list(opts[:from])}"
|
392
|
+
end
|
393
|
+
|
394
|
+
if join = opts[:join]
|
395
|
+
sql << join
|
396
|
+
end
|
397
|
+
|
398
|
+
if where = opts[:where]
|
399
|
+
sql << " WHERE #{where}"
|
400
|
+
end
|
401
|
+
|
402
|
+
if group = opts[:group]
|
403
|
+
sql << " GROUP BY #{column_list(group)}"
|
404
|
+
end
|
405
|
+
|
406
|
+
if order = opts[:order]
|
407
|
+
sql << " ORDER BY #{column_list(order)}"
|
408
|
+
end
|
409
|
+
|
410
|
+
if having = opts[:having]
|
411
|
+
sql << " HAVING #{having}"
|
412
|
+
end
|
413
|
+
|
414
|
+
if limit = opts[:limit]
|
415
|
+
sql << " LIMIT #{limit}"
|
416
|
+
if offset = opts[:offset]
|
417
|
+
sql << " OFFSET #{offset}"
|
418
|
+
end
|
419
|
+
end
|
420
|
+
|
421
|
+
if union = opts[:union]
|
422
|
+
sql << (opts[:union_all] ? \
|
423
|
+
" UNION ALL #{union.sql}" : " UNION #{union.sql}")
|
424
|
+
elsif intersect = opts[:intersect]
|
425
|
+
sql << (opts[:intersect_all] ? \
|
426
|
+
" INTERSECT ALL #{intersect.sql}" : " INTERSECT #{intersect.sql}")
|
427
|
+
elsif except = opts[:except]
|
428
|
+
sql << (opts[:except_all] ? \
|
429
|
+
" EXCEPT ALL #{except.sql}" : " EXCEPT #{except.sql}")
|
430
|
+
end
|
431
|
+
|
432
|
+
sql
|
433
|
+
end
|
434
|
+
alias_method :sql, :select_sql
|
435
|
+
|
436
|
+
# Formats an INSERT statement using the given values. If a hash is given,
|
437
|
+
# the resulting statement includes column names. If no values are given,
|
438
|
+
# the resulting statement includes a DEFAULT VALUES clause.
|
439
|
+
#
|
440
|
+
# dataset.insert_sql() #=> 'INSERT INTO items DEFAULT VALUES'
|
441
|
+
# dataset.insert_sql(1,2,3) #=> 'INSERT INTO items VALUES (1, 2, 3)'
|
442
|
+
# dataset.insert_sql(:a => 1, :b => 2) #=>
|
443
|
+
# 'INSERT INTO items (a, b) VALUES (1, 2)'
|
444
|
+
def insert_sql(*values)
|
445
|
+
if values.empty?
|
446
|
+
"INSERT INTO #{@opts[:from]} DEFAULT VALUES"
|
447
|
+
else
|
448
|
+
values = values[0] if values.size == 1
|
449
|
+
case values
|
450
|
+
when Sequel::Model
|
451
|
+
insert_sql(values.values)
|
452
|
+
when Array
|
453
|
+
if values.empty?
|
454
|
+
"INSERT INTO #{@opts[:from]} DEFAULT VALUES"
|
455
|
+
elsif values.keys
|
456
|
+
fl = values.keys.map {|f| literal(f.to_sym)}
|
457
|
+
vl = @transform ? transform_save(values.values) : values.values
|
458
|
+
vl.map! {|v| literal(v)}
|
459
|
+
"INSERT INTO #{@opts[:from]} (#{fl.join(COMMA_SEPARATOR)}) VALUES (#{vl.join(COMMA_SEPARATOR)})"
|
460
|
+
else
|
461
|
+
"INSERT INTO #{@opts[:from]} VALUES (#{literal(values)})"
|
462
|
+
end
|
463
|
+
when Hash
|
464
|
+
values = transform_save(values) if @transform
|
465
|
+
if values.empty?
|
466
|
+
"INSERT INTO #{@opts[:from]} DEFAULT VALUES"
|
467
|
+
else
|
468
|
+
fl, vl = [], []
|
469
|
+
values.each {|k, v| fl << literal(k.to_sym); vl << literal(v)}
|
470
|
+
"INSERT INTO #{@opts[:from]} (#{fl.join(COMMA_SEPARATOR)}) VALUES (#{vl.join(COMMA_SEPARATOR)})"
|
471
|
+
end
|
472
|
+
when Dataset
|
473
|
+
"INSERT INTO #{@opts[:from]} #{literal(values)}"
|
474
|
+
else
|
475
|
+
"INSERT INTO #{@opts[:from]} VALUES (#{literal(values)})"
|
476
|
+
end
|
477
|
+
end
|
478
|
+
end
|
479
|
+
|
480
|
+
# Formats an UPDATE statement using the given values.
|
481
|
+
#
|
482
|
+
# dataset.update_sql(:price => 100, :category => 'software') #=>
|
483
|
+
# "UPDATE items SET price = 100, category = 'software'"
|
484
|
+
def update_sql(values = {}, opts = nil, &block)
|
485
|
+
opts = opts ? @opts.merge(opts) : @opts
|
486
|
+
|
487
|
+
if opts[:group]
|
488
|
+
raise Error::InvalidOperation, "A grouped dataset cannot be updated"
|
489
|
+
elsif (opts[:from].size > 1) or opts[:join]
|
490
|
+
raise Error::InvalidOperation, "A joined dataset cannot be updated"
|
491
|
+
end
|
492
|
+
|
493
|
+
sql = "UPDATE #{@opts[:from]} SET "
|
494
|
+
if block
|
495
|
+
sql << proc_to_sql(block, :comma_separated => true)
|
496
|
+
else
|
497
|
+
# check if array with keys
|
498
|
+
values = values.to_hash if values.is_a?(Array) && values.keys
|
499
|
+
if values.is_a?(Hash)
|
500
|
+
# get values from hash
|
501
|
+
values = transform_save(values) if @transform
|
502
|
+
set = values.map do |k, v|
|
503
|
+
# convert string key into symbol
|
504
|
+
k = k.to_sym if String === k
|
505
|
+
"#{literal(k)} = #{literal(v)}"
|
506
|
+
end.join(COMMA_SEPARATOR)
|
507
|
+
else
|
508
|
+
# copy values verbatim
|
509
|
+
set = values
|
510
|
+
end
|
511
|
+
sql << set
|
512
|
+
end
|
513
|
+
if where = opts[:where]
|
514
|
+
sql << " WHERE #{where}"
|
515
|
+
end
|
516
|
+
|
517
|
+
sql
|
518
|
+
end
|
519
|
+
|
520
|
+
# Formats a DELETE statement using the given options and dataset options.
|
521
|
+
#
|
522
|
+
# dataset.filter {price >= 100}.delete_sql #=>
|
523
|
+
# "DELETE FROM items WHERE (price >= 100)"
|
524
|
+
def delete_sql(opts = nil)
|
525
|
+
opts = opts ? @opts.merge(opts) : @opts
|
526
|
+
|
527
|
+
if opts[:group]
|
528
|
+
raise Error::InvalidOperation, "Grouped datasets cannot be deleted from"
|
529
|
+
elsif opts[:from].is_a?(Array) && opts[:from].size > 1
|
530
|
+
raise Error::InvalidOperation, "Joined datasets cannot be deleted from"
|
531
|
+
end
|
532
|
+
|
533
|
+
sql = "DELETE FROM #{opts[:from]}"
|
534
|
+
|
535
|
+
if where = opts[:where]
|
536
|
+
sql << " WHERE #{where}"
|
537
|
+
end
|
538
|
+
|
539
|
+
sql
|
540
|
+
end
|
541
|
+
|
542
|
+
# Returns a table reference for use in the FROM clause. If the dataset has
|
543
|
+
# only a :from option refering to a single table, only the table name is
|
544
|
+
# returned. Otherwise a subquery is returned.
|
545
|
+
def to_table_reference(idx = nil)
|
546
|
+
if opts.keys == [:from] && opts[:from].size == 1
|
547
|
+
opts[:from].first.to_s
|
548
|
+
else
|
549
|
+
idx ? "(#{sql}) t#{idx}" : "(#{sql})"
|
550
|
+
end
|
551
|
+
end
|
552
|
+
|
553
|
+
# Returns an EXISTS clause for the dataset.
|
554
|
+
#
|
555
|
+
# dataset.exists #=> "EXISTS (SELECT 1 FROM items)"
|
556
|
+
def exists(opts = nil)
|
557
|
+
"EXISTS (#{sql({:select => [1]}.merge(opts || {}))})"
|
558
|
+
end
|
559
|
+
|
560
|
+
# If given an integer, the dataset will contain only the first l results.
|
561
|
+
# If given a range, it will contain only those at offsets within that
|
562
|
+
# range. If a second argument is given, it is used as an offset.
|
563
|
+
def limit(l, o = nil)
|
564
|
+
if l.is_a? Range
|
565
|
+
lim = (l.exclude_end? ? l.last - l.first : l.last + 1 - l.first)
|
566
|
+
clone_merge(:limit => lim, :offset=>l.first)
|
567
|
+
elsif o
|
568
|
+
clone_merge(:limit => l, :offset => o)
|
569
|
+
else
|
570
|
+
clone_merge(:limit => l)
|
571
|
+
end
|
572
|
+
end
|
573
|
+
|
574
|
+
STOCK_COUNT_OPTS = {:select => ["COUNT(*)".lit], :order => nil}.freeze
|
575
|
+
|
576
|
+
# Returns the number of records in the dataset.
|
577
|
+
def count
|
578
|
+
opts = @opts[:sql] ? \
|
579
|
+
{:sql => "SELECT COUNT(*) FROM (#{@opts[:sql]}) AS c", :order => nil} : \
|
580
|
+
STOCK_COUNT_OPTS
|
581
|
+
|
582
|
+
single_value(opts).to_i
|
583
|
+
end
|
584
|
+
end
|
585
|
+
end
|
586
|
+
end
|