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.
Files changed (57) hide show
  1. data/CHANGELOG +1003 -0
  2. data/COPYING +18 -0
  3. data/README +81 -0
  4. data/Rakefile +176 -0
  5. data/bin/sequel +41 -0
  6. data/lib/sequel_core.rb +59 -0
  7. data/lib/sequel_core/adapters/adapter_skeleton.rb +68 -0
  8. data/lib/sequel_core/adapters/ado.rb +100 -0
  9. data/lib/sequel_core/adapters/db2.rb +158 -0
  10. data/lib/sequel_core/adapters/dbi.rb +126 -0
  11. data/lib/sequel_core/adapters/informix.rb +87 -0
  12. data/lib/sequel_core/adapters/jdbc.rb +108 -0
  13. data/lib/sequel_core/adapters/mysql.rb +269 -0
  14. data/lib/sequel_core/adapters/odbc.rb +145 -0
  15. data/lib/sequel_core/adapters/odbc_mssql.rb +93 -0
  16. data/lib/sequel_core/adapters/openbase.rb +90 -0
  17. data/lib/sequel_core/adapters/oracle.rb +99 -0
  18. data/lib/sequel_core/adapters/postgres.rb +519 -0
  19. data/lib/sequel_core/adapters/sqlite.rb +192 -0
  20. data/lib/sequel_core/array_keys.rb +296 -0
  21. data/lib/sequel_core/connection_pool.rb +152 -0
  22. data/lib/sequel_core/core_ext.rb +59 -0
  23. data/lib/sequel_core/core_sql.rb +191 -0
  24. data/lib/sequel_core/database.rb +433 -0
  25. data/lib/sequel_core/dataset.rb +409 -0
  26. data/lib/sequel_core/dataset/convenience.rb +321 -0
  27. data/lib/sequel_core/dataset/sequelizer.rb +354 -0
  28. data/lib/sequel_core/dataset/sql.rb +586 -0
  29. data/lib/sequel_core/exceptions.rb +45 -0
  30. data/lib/sequel_core/migration.rb +191 -0
  31. data/lib/sequel_core/model.rb +8 -0
  32. data/lib/sequel_core/pretty_table.rb +73 -0
  33. data/lib/sequel_core/schema.rb +8 -0
  34. data/lib/sequel_core/schema/schema_generator.rb +131 -0
  35. data/lib/sequel_core/schema/schema_sql.rb +131 -0
  36. data/lib/sequel_core/worker.rb +58 -0
  37. data/spec/adapters/informix_spec.rb +139 -0
  38. data/spec/adapters/mysql_spec.rb +330 -0
  39. data/spec/adapters/oracle_spec.rb +130 -0
  40. data/spec/adapters/postgres_spec.rb +189 -0
  41. data/spec/adapters/sqlite_spec.rb +345 -0
  42. data/spec/array_keys_spec.rb +679 -0
  43. data/spec/connection_pool_spec.rb +356 -0
  44. data/spec/core_ext_spec.rb +67 -0
  45. data/spec/core_sql_spec.rb +301 -0
  46. data/spec/database_spec.rb +812 -0
  47. data/spec/dataset_spec.rb +2381 -0
  48. data/spec/migration_spec.rb +261 -0
  49. data/spec/pretty_table_spec.rb +66 -0
  50. data/spec/rcov.opts +4 -0
  51. data/spec/schema_generator_spec.rb +86 -0
  52. data/spec/schema_spec.rb +230 -0
  53. data/spec/sequelizer_spec.rb +448 -0
  54. data/spec/spec.opts +5 -0
  55. data/spec/spec_helper.rb +44 -0
  56. data/spec/worker_spec.rb +96 -0
  57. 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