sequel 3.5.0 → 3.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. data/CHANGELOG +108 -0
  2. data/README.rdoc +25 -14
  3. data/Rakefile +20 -1
  4. data/doc/advanced_associations.rdoc +61 -64
  5. data/doc/cheat_sheet.rdoc +16 -7
  6. data/doc/opening_databases.rdoc +3 -3
  7. data/doc/prepared_statements.rdoc +1 -1
  8. data/doc/reflection.rdoc +2 -1
  9. data/doc/release_notes/3.6.0.txt +366 -0
  10. data/doc/schema.rdoc +19 -14
  11. data/lib/sequel/adapters/amalgalite.rb +5 -27
  12. data/lib/sequel/adapters/jdbc.rb +13 -3
  13. data/lib/sequel/adapters/jdbc/h2.rb +17 -0
  14. data/lib/sequel/adapters/jdbc/mysql.rb +20 -7
  15. data/lib/sequel/adapters/mysql.rb +4 -3
  16. data/lib/sequel/adapters/oracle.rb +1 -1
  17. data/lib/sequel/adapters/postgres.rb +87 -28
  18. data/lib/sequel/adapters/shared/mssql.rb +47 -6
  19. data/lib/sequel/adapters/shared/mysql.rb +12 -31
  20. data/lib/sequel/adapters/shared/postgres.rb +15 -12
  21. data/lib/sequel/adapters/shared/sqlite.rb +18 -0
  22. data/lib/sequel/adapters/sqlite.rb +1 -16
  23. data/lib/sequel/connection_pool.rb +1 -1
  24. data/lib/sequel/core.rb +1 -1
  25. data/lib/sequel/database.rb +1 -1
  26. data/lib/sequel/database/schema_generator.rb +2 -0
  27. data/lib/sequel/database/schema_sql.rb +1 -1
  28. data/lib/sequel/dataset.rb +5 -179
  29. data/lib/sequel/dataset/actions.rb +123 -0
  30. data/lib/sequel/dataset/convenience.rb +18 -10
  31. data/lib/sequel/dataset/features.rb +65 -0
  32. data/lib/sequel/dataset/prepared_statements.rb +29 -23
  33. data/lib/sequel/dataset/query.rb +429 -0
  34. data/lib/sequel/dataset/sql.rb +67 -435
  35. data/lib/sequel/model/associations.rb +77 -13
  36. data/lib/sequel/model/base.rb +30 -8
  37. data/lib/sequel/model/errors.rb +4 -4
  38. data/lib/sequel/plugins/caching.rb +38 -15
  39. data/lib/sequel/plugins/force_encoding.rb +18 -7
  40. data/lib/sequel/plugins/hook_class_methods.rb +4 -0
  41. data/lib/sequel/plugins/many_through_many.rb +1 -1
  42. data/lib/sequel/plugins/nested_attributes.rb +40 -11
  43. data/lib/sequel/plugins/serialization.rb +17 -3
  44. data/lib/sequel/plugins/validation_helpers.rb +65 -18
  45. data/lib/sequel/sql.rb +23 -1
  46. data/lib/sequel/version.rb +1 -1
  47. data/spec/adapters/mssql_spec.rb +96 -10
  48. data/spec/adapters/mysql_spec.rb +19 -0
  49. data/spec/adapters/postgres_spec.rb +65 -2
  50. data/spec/adapters/sqlite_spec.rb +10 -0
  51. data/spec/core/core_sql_spec.rb +9 -0
  52. data/spec/core/database_spec.rb +8 -4
  53. data/spec/core/dataset_spec.rb +122 -29
  54. data/spec/core/expression_filters_spec.rb +17 -0
  55. data/spec/extensions/caching_spec.rb +43 -3
  56. data/spec/extensions/force_encoding_spec.rb +43 -1
  57. data/spec/extensions/nested_attributes_spec.rb +55 -2
  58. data/spec/extensions/validation_helpers_spec.rb +71 -0
  59. data/spec/integration/associations_test.rb +281 -0
  60. data/spec/integration/dataset_test.rb +383 -9
  61. data/spec/integration/eager_loader_test.rb +0 -65
  62. data/spec/integration/model_test.rb +110 -0
  63. data/spec/integration/plugin_test.rb +306 -0
  64. data/spec/integration/prepared_statement_test.rb +32 -0
  65. data/spec/integration/schema_test.rb +8 -3
  66. data/spec/integration/spec_helper.rb +1 -25
  67. data/spec/model/association_reflection_spec.rb +38 -0
  68. data/spec/model/associations_spec.rb +184 -8
  69. data/spec/model/eager_loading_spec.rb +23 -0
  70. data/spec/model/model_spec.rb +8 -0
  71. data/spec/model/record_spec.rb +84 -1
  72. metadata +9 -2
@@ -7,6 +7,17 @@ module Sequel
7
7
  clauses.map{|clause| :"#{type}_#{clause}_sql"}.freeze
8
8
  end
9
9
 
10
+ # These symbols have _join methods created (e.g. inner_join) that
11
+ # call join_table with the symbol, passing along the arguments and
12
+ # block from the method call.
13
+ CONDITIONED_JOIN_TYPES = [:inner, :full_outer, :right_outer, :left_outer, :full, :right, :left]
14
+
15
+ # These symbols have _join methods created (e.g. natural_join) that
16
+ # call join_table with the symbol. They only accept a single table
17
+ # argument which is passed to join_table, and they raise an error
18
+ # if called with a block.
19
+ UNCONDITIONED_JOIN_TYPES = [:natural, :natural_left, :natural_right, :natural_full, :cross]
20
+
10
21
  AND_SEPARATOR = " AND ".freeze
11
22
  BOOL_FALSE = "'f'".freeze
12
23
  BOOL_TRUE = "'t'".freeze
@@ -15,14 +26,12 @@ module Sequel
15
26
  COLUMN_REF_RE3 = /\A([\w ]+)__([\w ]+)\z/.freeze
16
27
  COUNT_FROM_SELF_OPTS = [:distinct, :group, :sql, :limit, :compounds]
17
28
  DATASET_ALIAS_BASE_NAME = 't'.freeze
18
- FROM_SELF_KEEP_OPTS = [:graph, :eager_graph, :graph_aliases]
19
29
  IS_LITERALS = {nil=>'NULL'.freeze, true=>'TRUE'.freeze, false=>'FALSE'.freeze}.freeze
20
30
  IS_OPERATORS = ::Sequel::SQL::ComplexExpression::IS_OPERATORS
21
31
  N_ARITY_OPERATORS = ::Sequel::SQL::ComplexExpression::N_ARITY_OPERATORS
22
32
  NULL = "NULL".freeze
23
33
  QUALIFY_KEYS = [:select, :where, :having, :order, :group]
24
34
  QUESTION_MARK = '?'.freeze
25
- STOCK_COUNT_OPTS = {:select => [SQL::AliasedExpression.new(LiteralString.new("COUNT(*)").freeze, :count)], :order => nil}.freeze
26
35
  DELETE_CLAUSE_METHODS = clause_methods(:delete, %w'from where')
27
36
  INSERT_CLAUSE_METHODS = clause_methods(:insert, %w'into columns values')
28
37
  SELECT_CLAUSE_METHODS = clause_methods(:select, %w'with distinct columns from join where group having compounds order limit')
@@ -33,16 +42,6 @@ module Sequel
33
42
  WILDCARD = '*'.freeze
34
43
  SQL_WITH = "WITH ".freeze
35
44
 
36
- # Adds an further filter to an existing filter using AND. If no filter
37
- # exists an error is raised. This method is identical to #filter except
38
- # it expects an existing filter.
39
- #
40
- # ds.filter(:a).and(:b) # SQL: WHERE a AND b
41
- def and(*cond, &block)
42
- raise(InvalidOperation, "No existing filter found.") unless @opts[:having] || @opts[:where]
43
- filter(*cond, &block)
44
- end
45
-
46
45
  # SQL fragment for the aliased expression
47
46
  def aliased_expression_sql(ae)
48
47
  as_sql(literal(ae.expression), ae.aliaz)
@@ -86,6 +85,14 @@ module Sequel
86
85
  else
87
86
  complex_expression_sql(:OR, [SQL::BooleanExpression.new(:"!=", *args), SQL::BooleanExpression.new(:IS, args.at(0), nil)])
88
87
  end
88
+ when :IN, :"NOT IN"
89
+ cols = args.at(0)
90
+ if !supports_multiple_column_in? && cols.is_a?(Array)
91
+ expr = SQL::BooleanExpression.new(:OR, *args.at(1).to_a.map{|vals| SQL::BooleanExpression.from_value_pairs(cols.zip(vals).map{|col, val| [col, val]})})
92
+ literal(op == :IN ? expr : ~expr)
93
+ else
94
+ "(#{literal(cols)} #{op} #{literal(args.at(1))})"
95
+ end
89
96
  when *TWO_ARITY_OPERATORS
90
97
  "(#{literal(args.at(0))} #{op} #{literal(args.at(1))})"
91
98
  when *N_ARITY_OPERATORS
@@ -108,7 +115,7 @@ module Sequel
108
115
 
109
116
  # Returns the number of records in the dataset.
110
117
  def count
111
- options_overlap(COUNT_FROM_SELF_OPTS) ? from_self.count : clone(STOCK_COUNT_OPTS).single_value.to_i
118
+ aggregate_dataset.get{COUNT(:*){}.as(count)}.to_i
112
119
  end
113
120
 
114
121
  # Formats a DELETE statement using the given options and dataset options.
@@ -121,50 +128,6 @@ module Sequel
121
128
  clause_sql(:delete)
122
129
  end
123
130
 
124
- # Returns a copy of the dataset with the SQL DISTINCT clause.
125
- # The DISTINCT clause is used to remove duplicate rows from the
126
- # output. If arguments are provided, uses a DISTINCT ON clause,
127
- # in which case it will only be distinct on those columns, instead
128
- # of all returned columns. Raises an error if arguments
129
- # are given and DISTINCT ON is not supported.
130
- #
131
- # dataset.distinct # SQL: SELECT DISTINCT * FROM items
132
- # dataset.order(:id).distinct(:id) # SQL: SELECT DISTINCT ON (id) * FROM items ORDER BY id
133
- def distinct(*args)
134
- raise(InvalidOperation, "DISTINCT ON not supported") if !args.empty? && !supports_distinct_on?
135
- clone(:distinct => args)
136
- end
137
-
138
- # Adds an EXCEPT clause using a second dataset object.
139
- # An EXCEPT compound dataset returns all rows in the current dataset
140
- # that are not in the given dataset.
141
- # Raises an InvalidOperation if the operation is not supported.
142
- # Options:
143
- # * :all - Set to true to use EXCEPT ALL instead of EXCEPT, so duplicate rows can occur
144
- # * :from_self - Set to false to not wrap the returned dataset in a from_self, use with care.
145
- #
146
- # DB[:items].except(DB[:other_items]).sql
147
- # #=> "SELECT * FROM items EXCEPT SELECT * FROM other_items"
148
- def except(dataset, opts={})
149
- opts = {:all=>opts} unless opts.is_a?(Hash)
150
- raise(InvalidOperation, "EXCEPT not supported") unless supports_intersect_except?
151
- raise(InvalidOperation, "EXCEPT ALL not supported") if opts[:all] && !supports_intersect_except_all?
152
- compound_clone(:except, dataset, opts)
153
- end
154
-
155
- # Performs the inverse of Dataset#filter.
156
- #
157
- # dataset.exclude(:category => 'software').sql #=>
158
- # "SELECT * FROM items WHERE (category != 'software')"
159
- def exclude(*cond, &block)
160
- clause = (@opts[:having] ? :having : :where)
161
- cond = cond.first if cond.size == 1
162
- cond = filter_expr(cond, &block)
163
- cond = SQL::BooleanExpression.invert(cond)
164
- cond = SQL::BooleanExpression.new(:AND, @opts[clause], cond) if @opts[clause]
165
- clone(clause => cond)
166
- end
167
-
168
131
  # Returns an EXISTS clause for the dataset as a LiteralString.
169
132
  #
170
133
  # DB.select(1).where(DB[:items].exists).sql
@@ -173,57 +136,6 @@ module Sequel
173
136
  LiteralString.new("EXISTS (#{select_sql})")
174
137
  end
175
138
 
176
- # Returns a copy of the dataset with the given conditions imposed upon it.
177
- # If the query already has a HAVING clause, then the conditions are imposed in the
178
- # HAVING clause. If not, then they are imposed in the WHERE clause.
179
- #
180
- # filter accepts the following argument types:
181
- #
182
- # * Hash - list of equality/inclusion expressions
183
- # * Array - depends:
184
- # * If first member is a string, assumes the rest of the arguments
185
- # are parameters and interpolates them into the string.
186
- # * If all members are arrays of length two, treats the same way
187
- # as a hash, except it allows for duplicate keys to be
188
- # specified.
189
- # * String - taken literally
190
- # * Symbol - taken as a boolean column argument (e.g. WHERE active)
191
- # * Sequel::SQL::BooleanExpression - an existing condition expression,
192
- # probably created using the Sequel expression filter DSL.
193
- #
194
- # filter also takes a block, which should return one of the above argument
195
- # types, and is treated the same way. This block yields a virtual row object,
196
- # which is easy to use to create identifiers and functions.
197
- #
198
- # If both a block and regular argument
199
- # are provided, they get ANDed together.
200
- #
201
- # Examples:
202
- #
203
- # dataset.filter(:id => 3).sql #=>
204
- # "SELECT * FROM items WHERE (id = 3)"
205
- # dataset.filter('price < ?', 100).sql #=>
206
- # "SELECT * FROM items WHERE price < 100"
207
- # dataset.filter([[:id, (1,2,3)], [:id, 0..10]]).sql #=>
208
- # "SELECT * FROM items WHERE ((id IN (1, 2, 3)) AND ((id >= 0) AND (id <= 10)))"
209
- # dataset.filter('price < 100').sql #=>
210
- # "SELECT * FROM items WHERE price < 100"
211
- # dataset.filter(:active).sql #=>
212
- # "SELECT * FROM items WHERE :active
213
- # dataset.filter{|o| o.price < 100}.sql #=>
214
- # "SELECT * FROM items WHERE (price < 100)"
215
- #
216
- # Multiple filter calls can be chained for scoping:
217
- #
218
- # software = dataset.filter(:category => 'software')
219
- # software.filter{|o| o.price < 100}.sql #=>
220
- # "SELECT * FROM items WHERE ((category = 'software') AND (price < 100))"
221
- #
222
- # See doc/dataset_filters.rdoc for more examples and details.
223
- def filter(*cond, &block)
224
- _filter(@opts[:having] ? :having : :where, *cond, &block)
225
- end
226
-
227
139
  # The first source (primary table) for this dataset. If the dataset doesn't
228
140
  # have a table, raises an error. If the table is aliased, returns the aliased name.
229
141
  def first_source_alias
@@ -243,87 +155,12 @@ module Sequel
243
155
  end
244
156
  alias first_source first_source_alias
245
157
 
246
- # Returns a copy of the dataset with the source changed.
247
- #
248
- # dataset.from # SQL: SELECT *
249
- # dataset.from(:blah) # SQL: SELECT * FROM blah
250
- # dataset.from(:blah, :foo) # SQL: SELECT * FROM blah, foo
251
- def from(*source)
252
- table_alias_num = 0
253
- sources = []
254
- source.each do |s|
255
- case s
256
- when Hash
257
- s.each{|k,v| sources << SQL::AliasedExpression.new(k,v)}
258
- when Dataset
259
- sources << SQL::AliasedExpression.new(s, dataset_alias(table_alias_num+=1))
260
- when Symbol
261
- sch, table, aliaz = split_symbol(s)
262
- if aliaz
263
- s = sch ? SQL::QualifiedIdentifier.new(sch.to_sym, table.to_sym) : SQL::Identifier.new(table.to_sym)
264
- sources << SQL::AliasedExpression.new(s, aliaz.to_sym)
265
- else
266
- sources << s
267
- end
268
- else
269
- sources << s
270
- end
271
- end
272
- o = {:from=>sources.empty? ? nil : sources}
273
- o[:num_dataset_sources] = table_alias_num if table_alias_num > 0
274
- clone(o)
275
- end
276
-
277
- # Returns a dataset selecting from the current dataset.
278
- # Supplying the :alias option controls the name of the result.
279
- #
280
- # ds = DB[:items].order(:name).select(:id, :name)
281
- # ds.sql #=> "SELECT id,name FROM items ORDER BY name"
282
- # ds.from_self.sql #=> "SELECT * FROM (SELECT id, name FROM items ORDER BY name) AS 't1'"
283
- # ds.from_self(:alias=>:foo).sql #=> "SELECT * FROM (SELECT id, name FROM items ORDER BY name) AS 'foo'"
284
- def from_self(opts={})
285
- fs = {}
286
- @opts.keys.each{|k| fs[k] = nil unless FROM_SELF_KEEP_OPTS.include?(k)}
287
- clone(fs).from(opts[:alias] ? as(opts[:alias]) : self)
288
- end
289
-
290
158
  # SQL fragment specifying an SQL function call
291
159
  def function_sql(f)
292
160
  args = f.args
293
161
  "#{f.f}#{args.empty? ? '()' : literal(args)}"
294
162
  end
295
163
 
296
- # Pattern match any of the columns to any of the terms. The terms can be
297
- # strings (which use LIKE) or regular expressions (which are only supported
298
- # in some databases). See Sequel::SQL::StringExpression.like. Note that the
299
- # total number of pattern matches will be cols.length * terms.length,
300
- # which could cause performance issues.
301
- #
302
- # dataset.grep(:a, '%test%') # SQL: SELECT * FROM items WHERE a LIKE '%test%'
303
- # dataset.grep([:a, :b], %w'%test% foo') # SQL: SELECT * FROM items WHERE a LIKE '%test%' OR a LIKE 'foo' OR b LIKE '%test%' OR b LIKE 'foo'
304
- def grep(cols, terms)
305
- filter(SQL::BooleanExpression.new(:OR, *Array(cols).collect{|c| SQL::StringExpression.like(c, *terms)}))
306
- end
307
-
308
- # Returns a copy of the dataset with the results grouped by the value of
309
- # the given columns.
310
- #
311
- # dataset.group(:id) # SELECT * FROM items GROUP BY id
312
- # dataset.group(:id, :name) # SELECT * FROM items GROUP BY id, name
313
- def group(*columns)
314
- clone(:group => (columns.compact.empty? ? nil : columns))
315
- end
316
- alias group_by group
317
-
318
- # Returns a copy of the dataset with the HAVING conditions changed. Raises
319
- # an error if the dataset has not been grouped. See #filter for argument types.
320
- #
321
- # dataset.group(:sum).having(:sum=>10) # SQL: SELECT * FROM items GROUP BY sum HAVING sum = 10
322
- def having(*cond, &block)
323
- raise(InvalidOperation, "Can only specify a HAVING clause on a grouped dataset") unless @opts[:group]
324
- _filter(:having, *cond, &block)
325
- end
326
-
327
164
  # Inserts multiple values. If a block is given it is invoked for each
328
165
  # item in the given array before inserting it. See #multi_insert as
329
166
  # a possible faster version that inserts multiple records in one
@@ -390,36 +227,6 @@ module Sequel
390
227
  clone(:columns=>columns, :values=>values)._insert_sql
391
228
  end
392
229
 
393
- # Adds an INTERSECT clause using a second dataset object.
394
- # An INTERSECT compound dataset returns all rows in both the current dataset
395
- # and the given dataset.
396
- # Raises an InvalidOperation if the operation is not supported.
397
- # Options:
398
- # * :all - Set to true to use INTERSECT ALL instead of INTERSECT, so duplicate rows can occur
399
- # * :from_self - Set to false to not wrap the returned dataset in a from_self, use with care.
400
- #
401
- # DB[:items].intersect(DB[:other_items]).sql
402
- # #=> "SELECT * FROM items INTERSECT SELECT * FROM other_items"
403
- def intersect(dataset, opts={})
404
- opts = {:all=>opts} unless opts.is_a?(Hash)
405
- raise(InvalidOperation, "INTERSECT not supported") unless supports_intersect_except?
406
- raise(InvalidOperation, "INTERSECT ALL not supported") if opts[:all] && !supports_intersect_except_all?
407
- compound_clone(:intersect, dataset, opts)
408
- end
409
-
410
- # Inverts the current filter
411
- #
412
- # dataset.filter(:category => 'software').invert.sql #=>
413
- # "SELECT * FROM items WHERE (category != 'software')"
414
- def invert
415
- having, where = @opts[:having], @opts[:where]
416
- raise(Error, "No current filter") unless having || where
417
- o = {}
418
- o[:having] = SQL::BooleanExpression.invert(having) if having
419
- o[:where] = SQL::BooleanExpression.invert(where) if where
420
- clone(o)
421
- end
422
-
423
230
  # SQL fragment specifying a JOIN clause without ON or USING.
424
231
  def join_clause_sql(jc)
425
232
  table = jc.table
@@ -470,6 +277,13 @@ module Sequel
470
277
  # the table alias/name for the last joined (or first table), and an array of previous
471
278
  # SQL::JoinClause.
472
279
  def join_table(type, table, expr=nil, options={}, &block)
280
+ using_join = expr.is_a?(Array) && !expr.empty? && expr.all?{|x| x.is_a?(Symbol)}
281
+ if using_join && !supports_join_using?
282
+ h = {}
283
+ expr.each{|s| h[s] = s}
284
+ return join_table(type, table, h, options)
285
+ end
286
+
473
287
  if [Symbol, String].any?{|c| options.is_a?(c)}
474
288
  table_alias = options
475
289
  last_alias = nil
@@ -490,7 +304,7 @@ module Sequel
490
304
 
491
305
  join = if expr.nil? and !block_given?
492
306
  SQL::JoinClause.new(type, table, table_alias)
493
- elsif Array === expr and !expr.empty? and expr.all?{|x| Symbol === x}
307
+ elsif using_join
494
308
  raise(Sequel::Error, "can't use a block if providing an array of symbols as expr") if block_given?
495
309
  SQL::JoinUsingClause.new(expr, type, table, table_alias)
496
310
  else
@@ -514,30 +328,6 @@ module Sequel
514
328
  clone(opts)
515
329
  end
516
330
 
517
- # If given an integer, the dataset will contain only the first l results.
518
- # If given a range, it will contain only those at offsets within that
519
- # range. If a second argument is given, it is used as an offset.
520
- #
521
- # dataset.limit(10) # SQL: SELECT * FROM items LIMIT 10
522
- # dataset.limit(10, 20) # SQL: SELECT * FROM items LIMIT 10 OFFSET 20
523
- def limit(l, o = nil)
524
- return from_self.limit(l, o) if @opts[:sql]
525
-
526
- if Range === l
527
- o = l.first
528
- l = l.last - l.first + (l.exclude_end? ? 0 : 1)
529
- end
530
- l = l.to_i
531
- raise(Error, 'Limits must be greater than or equal to 1') unless l >= 1
532
- opts = {:limit => l}
533
- if o
534
- o = o.to_i
535
- raise(Error, 'Offsets must be greater than or equal to 0') unless o >= 0
536
- opts[:offset] = o
537
- end
538
- clone(opts)
539
- end
540
-
541
331
  # Returns a literal representation of a value to be used as part
542
332
  # of an SQL expression.
543
333
  #
@@ -596,46 +386,6 @@ module Sequel
596
386
  values.map{|r| insert_sql(columns, r)}
597
387
  end
598
388
 
599
- # Adds an alternate filter to an existing filter using OR. If no filter
600
- # exists an error is raised.
601
- #
602
- # dataset.filter(:a).or(:b) # SQL: SELECT * FROM items WHERE a OR b
603
- def or(*cond, &block)
604
- clause = (@opts[:having] ? :having : :where)
605
- raise(InvalidOperation, "No existing filter found.") unless @opts[clause]
606
- cond = cond.first if cond.size == 1
607
- clone(clause => SQL::BooleanExpression.new(:OR, @opts[clause], filter_expr(cond, &block)))
608
- end
609
-
610
- # Returns a copy of the dataset with the order changed. If a nil is given
611
- # the returned dataset has no order. This can accept multiple arguments
612
- # of varying kinds, and even SQL functions. If a block is given, it is treated
613
- # as a virtual row block, similar to filter.
614
- #
615
- # ds.order(:name).sql #=> 'SELECT * FROM items ORDER BY name'
616
- # ds.order(:a, :b).sql #=> 'SELECT * FROM items ORDER BY a, b'
617
- # ds.order('a + b'.lit).sql #=> 'SELECT * FROM items ORDER BY a + b'
618
- # ds.order(:a + :b).sql #=> 'SELECT * FROM items ORDER BY (a + b)'
619
- # ds.order(:name.desc).sql #=> 'SELECT * FROM items ORDER BY name DESC'
620
- # ds.order(:name.asc).sql #=> 'SELECT * FROM items ORDER BY name ASC'
621
- # ds.order{|o| o.sum(:name)}.sql #=> 'SELECT * FROM items ORDER BY sum(name)'
622
- # ds.order(nil).sql #=> 'SELECT * FROM items'
623
- def order(*columns, &block)
624
- columns += Array(Sequel.virtual_row(&block)) if block
625
- clone(:order => (columns.compact.empty?) ? nil : columns)
626
- end
627
- alias_method :order_by, :order
628
-
629
- # Returns a copy of the dataset with the order columns added
630
- # to the existing order.
631
- #
632
- # ds.order(:a).order(:b).sql #=> 'SELECT * FROM items ORDER BY b'
633
- # ds.order(:a).order_more(:b).sql #=> 'SELECT * FROM items ORDER BY a, b'
634
- def order_more(*columns, &block)
635
- columns = @opts[:order] + columns if @opts[:order]
636
- order(*columns, &block)
637
- end
638
-
639
389
  # SQL fragment for the ordered expression, used in the ORDER BY
640
390
  # clause.
641
391
  def ordered_expression_sql(oe)
@@ -644,8 +394,14 @@ module Sequel
644
394
 
645
395
  # SQL fragment for a literal string with placeholders
646
396
  def placeholder_literal_string_sql(pls)
647
- args = pls.args.dup
648
- s = pls.str.gsub(QUESTION_MARK){literal(args.shift)}
397
+ args = pls.args
398
+ s = if args.is_a?(Hash)
399
+ re = /:(#{args.keys.map{|k| Regexp.escape(k.to_s)}.join('|')})\b/
400
+ pls.str.gsub(re){literal(args[$1.to_sym])}
401
+ else
402
+ i = -1
403
+ pls.str.gsub(QUESTION_MARK){literal(args.at(i+=1))}
404
+ end
649
405
  s = "(#{s})" if pls.parens
650
406
  s
651
407
  end
@@ -710,13 +466,6 @@ module Sequel
710
466
  "\"#{name.to_s.gsub('"', '""')}\""
711
467
  end
712
468
 
713
- # Returns a copy of the dataset with the order reversed. If no order is
714
- # given, the existing order is inverted.
715
- def reverse_order(*order)
716
- order(*invert_order(order.empty? ? @opts[:order] : order))
717
- end
718
- alias reverse reverse_order
719
-
720
469
  # Split the schema information from the table
721
470
  def schema_and_table(table_name)
722
471
  sch = db.default_schema if db
@@ -735,39 +484,6 @@ module Sequel
735
484
  end
736
485
  end
737
486
 
738
- # Returns a copy of the dataset with the columns selected changed
739
- # to the given columns. This also takes a virtual row block,
740
- # similar to filter.
741
- #
742
- # dataset.select(:a) # SELECT a FROM items
743
- # dataset.select(:a, :b) # SELECT a, b FROM items
744
- # dataset.select{|o| o.a, o.sum(:b)} # SELECT a, sum(b) FROM items
745
- def select(*columns, &block)
746
- columns += Array(Sequel.virtual_row(&block)) if block
747
- m = []
748
- columns.map do |i|
749
- i.is_a?(Hash) ? m.concat(i.map{|k, v| SQL::AliasedExpression.new(k,v)}) : m << i
750
- end
751
- clone(:select => m)
752
- end
753
-
754
- # Returns a copy of the dataset selecting the wildcard.
755
- #
756
- # dataset.select(:a).select_all # SELECT * FROM items
757
- def select_all
758
- clone(:select => nil)
759
- end
760
-
761
- # Returns a copy of the dataset with the given columns added
762
- # to the existing selected columns.
763
- #
764
- # dataset.select(:a).select(:b) # SELECT b FROM items
765
- # dataset.select(:a).select_more(:b) # SELECT a, b FROM items
766
- def select_more(*columns, &block)
767
- columns = @opts[:select] + columns if @opts[:select]
768
- select(*columns, &block)
769
- end
770
-
771
487
  # Formats a SELECT statement
772
488
  #
773
489
  # dataset.select_sql # => "SELECT * FROM items"
@@ -797,48 +513,6 @@ module Sequel
797
513
  end
798
514
  end
799
515
 
800
- # Returns a copy of the dataset with no filters (HAVING or WHERE clause) applied.
801
- #
802
- # dataset.group(:a).having(:a=>1).where(:b).unfiltered # SELECT * FROM items GROUP BY a
803
- def unfiltered
804
- clone(:where => nil, :having => nil)
805
- end
806
-
807
- # Returns a copy of the dataset with no grouping (GROUP or HAVING clause) applied.
808
- #
809
- # dataset.group(:a).having(:a=>1).where(:b).ungrouped # SELECT * FROM items WHERE b
810
- def ungrouped
811
- clone(:group => nil, :having => nil)
812
- end
813
-
814
- # Adds a UNION clause using a second dataset object.
815
- # A UNION compound dataset returns all rows in either the current dataset
816
- # or the given dataset.
817
- # Options:
818
- # * :all - Set to true to use UNION ALL instead of UNION, so duplicate rows can occur
819
- # * :from_self - Set to false to not wrap the returned dataset in a from_self, use with care.
820
- #
821
- # DB[:items].union(DB[:other_items]).sql
822
- # #=> "SELECT * FROM items UNION SELECT * FROM other_items"
823
- def union(dataset, opts={})
824
- opts = {:all=>opts} unless opts.is_a?(Hash)
825
- compound_clone(:union, dataset, opts)
826
- end
827
-
828
- # Returns a copy of the dataset with no limit or offset.
829
- #
830
- # dataset.limit(10, 20).unlimited # SELECT * FROM items
831
- def unlimited
832
- clone(:limit=>nil, :offset=>nil)
833
- end
834
-
835
- # Returns a copy of the dataset with no order.
836
- #
837
- # dataset.order(:a).unordered # SELECT * FROM items
838
- def unordered
839
- order(nil)
840
- end
841
-
842
516
  # Formats an UPDATE statement using the given values.
843
517
  #
844
518
  # dataset.update_sql(:price => 100, :category => 'software') #=>
@@ -913,9 +587,12 @@ module Sequel
913
587
  clone(:sql=>sql)
914
588
  end
915
589
 
916
- [:inner, :full_outer, :right_outer, :left_outer].each do |jtype|
590
+ CONDITIONED_JOIN_TYPES.each do |jtype|
917
591
  class_eval("def #{jtype}_join(*args, &block); join_table(:#{jtype}, *args, &block) end", __FILE__, __LINE__)
918
592
  end
593
+ UNCONDITIONED_JOIN_TYPES.each do |jtype|
594
+ class_eval("def #{jtype}_join(table); raise(Sequel::Error, '#{jtype}_join does not accept join table blocks') if block_given?; join_table(:#{jtype}, table) end", __FILE__, __LINE__)
595
+ end
919
596
  alias join inner_join
920
597
 
921
598
  protected
@@ -938,20 +615,20 @@ module Sequel
938
615
 
939
616
  private
940
617
 
941
- # Internal filter method so it works on either the having or where clauses.
942
- def _filter(clause, *cond, &block)
943
- cond = cond.first if cond.size == 1
944
- cond = filter_expr(cond, &block)
945
- cond = SQL::BooleanExpression.new(:AND, @opts[clause], cond) if @opts[clause]
946
- clone(clause => cond)
947
- end
948
-
949
618
  # Formats the truncate statement. Assumes the table given has already been
950
619
  # literalized.
951
620
  def _truncate_sql(table)
952
621
  "TRUNCATE TABLE #{table}"
953
622
  end
954
623
 
624
+ # Clone of this dataset usable in aggregate operations. Does
625
+ # a from_self if dataset contains any parameters that would
626
+ # affect normal aggregation, or just removes an existing
627
+ # order if not.
628
+ def aggregate_dataset
629
+ options_overlap(COUNT_FROM_SELF_OPTS) ? from_self : unordered
630
+ end
631
+
955
632
  # Do a simple join of the arguments (which should be strings or symbols) separated by commas
956
633
  def argument_list(args)
957
634
  args.join(COMMA_SEPARATOR)
@@ -982,12 +659,6 @@ module Sequel
982
659
  (columns.nil? || columns.empty?) ? WILDCARD : expression_list(columns)
983
660
  end
984
661
 
985
- # Add the dataset to the list of compounds
986
- def compound_clone(type, dataset, opts)
987
- ds = compound_from_self.clone(:compounds=>Array(@opts[:compounds]).map{|x| x.dup} + [[type, dataset.compound_from_self, opts[:all]]])
988
- opts[:from_self] == false ? ds : ds.from_self
989
- end
990
-
991
662
  # The alias to use for datasets, takes a number to make sure the name is unique.
992
663
  def dataset_alias(number)
993
664
  :"#{DATASET_ALIAS_BASE_NAME}#{number}"
@@ -1009,40 +680,6 @@ module Sequel
1009
680
  requires_sql_standard_datetimes? ? STANDARD_TIMESTAMP_FORMAT : TIMESTAMP_FORMAT
1010
681
  end
1011
682
 
1012
- # SQL fragment based on the expr type. See #filter.
1013
- def filter_expr(expr = nil, &block)
1014
- expr = nil if expr == []
1015
- if expr && block
1016
- return SQL::BooleanExpression.new(:AND, filter_expr(expr), filter_expr(block))
1017
- elsif block
1018
- expr = block
1019
- end
1020
- case expr
1021
- when Hash
1022
- SQL::BooleanExpression.from_value_pairs(expr)
1023
- when Array
1024
- if String === expr[0]
1025
- SQL::PlaceholderLiteralString.new(expr.shift, expr, true)
1026
- elsif Sequel.condition_specifier?(expr)
1027
- SQL::BooleanExpression.from_value_pairs(expr)
1028
- else
1029
- SQL::BooleanExpression.new(:AND, *expr.map{|x| filter_expr(x)})
1030
- end
1031
- when Proc
1032
- filter_expr(Sequel.virtual_row(&expr))
1033
- when SQL::NumericExpression, SQL::StringExpression
1034
- raise(Error, "Invalid SQL Expression type: #{expr.inspect}")
1035
- when Symbol, SQL::Expression
1036
- expr
1037
- when TrueClass, FalseClass
1038
- SQL::BooleanExpression.new(:NOOP, expr)
1039
- when String
1040
- LiteralString.new("(#{expr})")
1041
- else
1042
- raise(Error, 'Invalid filter argument')
1043
- end
1044
- end
1045
-
1046
683
  # Format the timestamp based on the default_timestamp_format, with a couple
1047
684
  # of modifiers. First, allow %N to be used for fractions seconds (if the
1048
685
  # database supports them), and override %z to always use a numeric offset
@@ -1111,25 +748,6 @@ module Sequel
1111
748
  end
1112
749
  end
1113
750
 
1114
- # Inverts the given order by breaking it into a list of column references
1115
- # and inverting them.
1116
- #
1117
- # dataset.invert_order([:id.desc]]) #=> [:id]
1118
- # dataset.invert_order(:category, :price.desc]) #=>
1119
- # [:category.desc, :price]
1120
- def invert_order(order)
1121
- return nil unless order
1122
- new_order = []
1123
- order.map do |f|
1124
- case f
1125
- when SQL::OrderedExpression
1126
- f.invert
1127
- else
1128
- SQL::OrderedExpression.new(f)
1129
- end
1130
- end
1131
- end
1132
-
1133
751
  # SQL fragment specifying a JOIN type, converts underscores to
1134
752
  # spaces and upcases.
1135
753
  def join_type_sql(join_type)
@@ -1192,11 +810,18 @@ module Sequel
1192
810
  v.to_s
1193
811
  end
1194
812
 
1195
- # SQL fragmento for a type of object not handled by Dataset#literal.
1196
- # Raises an error. If a database specific type is allowed,
1197
- # this should be overriden in a subclass.
813
+ # SQL fragment for a type of object not handled by Dataset#literal.
814
+ # Calls sql_literal if object responds to it, otherwise raises an error.
815
+ # Classes implementing sql_literal should call a class-specific method on the dataset
816
+ # provided and should add that method to Sequel::Dataset, allowing for adapters
817
+ # to provide customized literalizations.
818
+ # If a database specific type is allowed, this should be overriden in a subclass.
1198
819
  def literal_other(v)
1199
- raise Error, "can't express #{v.inspect} as a SQL literal"
820
+ if v.respond_to?(:sql_literal)
821
+ v.sql_literal(self)
822
+ else
823
+ raise Error, "can't express #{v.inspect} as a SQL literal"
824
+ end
1200
825
  end
1201
826
 
1202
827
  # SQL fragment for String. Doubles \ and ' by default.
@@ -1293,7 +918,14 @@ module Sequel
1293
918
  o[:order] = qualified_expression(o[:order], table) if o[:order]
1294
919
  SQL::Window.new(o)
1295
920
  when SQL::PlaceholderLiteralString
1296
- SQL::PlaceholderLiteralString.new(e.str, qualified_expression(e.args, table), e.parens)
921
+ args = if e.args.is_a?(Hash)
922
+ h = {}
923
+ e.args.each{|k,v| h[k] = qualified_expression(v, table)}
924
+ h
925
+ else
926
+ qualified_expression(e.args, table)
927
+ end
928
+ SQL::PlaceholderLiteralString.new(e.str, args, e.parens)
1297
929
  else
1298
930
  e
1299
931
  end