sequel_core 1.0

Sign up to get free protection for your applications and to get access to all the features.
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