sequel 3.30.0 → 3.31.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 (45) hide show
  1. data/CHANGELOG +40 -0
  2. data/Rakefile +12 -2
  3. data/doc/association_basics.rdoc +28 -0
  4. data/doc/dataset_filtering.rdoc +8 -0
  5. data/doc/opening_databases.rdoc +1 -0
  6. data/doc/release_notes/3.31.0.txt +146 -0
  7. data/lib/sequel/adapters/jdbc.rb +7 -6
  8. data/lib/sequel/adapters/jdbc/derby.rb +5 -0
  9. data/lib/sequel/adapters/jdbc/h2.rb +6 -1
  10. data/lib/sequel/adapters/mock.rb +21 -2
  11. data/lib/sequel/adapters/shared/db2.rb +10 -0
  12. data/lib/sequel/adapters/shared/mssql.rb +40 -5
  13. data/lib/sequel/adapters/shared/mysql.rb +19 -2
  14. data/lib/sequel/adapters/shared/oracle.rb +13 -1
  15. data/lib/sequel/adapters/shared/postgres.rb +52 -8
  16. data/lib/sequel/adapters/shared/sqlite.rb +4 -3
  17. data/lib/sequel/adapters/utils/stored_procedures.rb +1 -11
  18. data/lib/sequel/database/schema_generator.rb +9 -2
  19. data/lib/sequel/dataset/actions.rb +37 -19
  20. data/lib/sequel/dataset/features.rb +10 -0
  21. data/lib/sequel/dataset/prepared_statements.rb +0 -10
  22. data/lib/sequel/dataset/query.rb +13 -1
  23. data/lib/sequel/dataset/sql.rb +6 -1
  24. data/lib/sequel/model/associations.rb +14 -4
  25. data/lib/sequel/model/base.rb +10 -0
  26. data/lib/sequel/plugins/serialization.rb +82 -43
  27. data/lib/sequel/version.rb +1 -1
  28. data/spec/adapters/mssql_spec.rb +46 -0
  29. data/spec/adapters/mysql_spec.rb +3 -0
  30. data/spec/adapters/postgres_spec.rb +61 -24
  31. data/spec/core/database_spec.rb +31 -18
  32. data/spec/core/dataset_spec.rb +90 -13
  33. data/spec/core/mock_adapter_spec.rb +37 -0
  34. data/spec/extensions/instance_filters_spec.rb +1 -0
  35. data/spec/extensions/nested_attributes_spec.rb +1 -1
  36. data/spec/extensions/serialization_spec.rb +49 -5
  37. data/spec/extensions/sharding_spec.rb +1 -1
  38. data/spec/integration/associations_test.rb +15 -0
  39. data/spec/integration/dataset_test.rb +71 -0
  40. data/spec/integration/prepared_statement_test.rb +8 -0
  41. data/spec/model/association_reflection_spec.rb +27 -0
  42. data/spec/model/associations_spec.rb +18 -3
  43. data/spec/model/base_spec.rb +20 -0
  44. data/spec/model/eager_loading_spec.rb +21 -0
  45. metadata +4 -2
@@ -345,6 +345,7 @@ module Sequel
345
345
  INSERT = Dataset::INSERT
346
346
  COMMA = Dataset::COMMA
347
347
  LIMIT = Dataset::LIMIT
348
+ GROUP_BY = Dataset::GROUP_BY
348
349
  REGEXP = 'REGEXP'.freeze
349
350
  LIKE = 'LIKE'.freeze
350
351
  BINARY = 'BINARY '.freeze
@@ -361,6 +362,7 @@ module Sequel
361
362
  ON_DUPLICATE_KEY_UPDATE = " ON DUPLICATE KEY UPDATE ".freeze
362
363
  EQ_VALUES = '=VALUES('.freeze
363
364
  EQ = '='.freeze
365
+ WITH_ROLLUP = ' WITH ROLLUP'.freeze
364
366
 
365
367
  # MySQL specific syntax for LIKE/REGEXP searches, as well as
366
368
  # string concatenation.
@@ -423,8 +425,9 @@ module Sequel
423
425
  end
424
426
 
425
427
  # MySQL specific full text search syntax.
426
- def full_text_sql(cols, term, opts = {})
427
- "MATCH #{literal(Array(cols))} AGAINST (#{literal(Array(term).join(' '))}#{" IN BOOLEAN MODE" if opts[:boolean]})"
428
+ def full_text_sql(cols, terms, opts = {})
429
+ terms = terms.join(' ') if terms.is_a?(Array)
430
+ SQL::PlaceholderLiteralString.new("MATCH ? AGAINST (?#{" IN BOOLEAN MODE" if opts[:boolean]})", [Array(cols), terms], true)
428
431
  end
429
432
 
430
433
  # MySQL allows HAVING clause on ungrouped datasets.
@@ -518,6 +521,11 @@ module Sequel
518
521
  true
519
522
  end
520
523
 
524
+ # MySQL supports GROUP BY WITH ROLLUP (but not CUBE)
525
+ def supports_group_rollup?
526
+ true
527
+ end
528
+
521
529
  # MySQL does not support INTERSECT or EXCEPT
522
530
  def supports_intersect_except?
523
531
  false
@@ -661,6 +669,15 @@ module Sequel
661
669
  SELECT_CLAUSE_METHODS
662
670
  end
663
671
 
672
+ # MySQL supports ROLLUP via nonstandard SQL syntax
673
+ def select_group_sql(sql)
674
+ if group = @opts[:group]
675
+ sql << GROUP_BY
676
+ expression_list_append(sql, group)
677
+ sql << WITH_ROLLUP if @opts[:group_options] == :rollup
678
+ end
679
+ end
680
+
664
681
  # Support FOR SHARE locking when using the :share lock style.
665
682
  def select_lock_sql(sql)
666
683
  @opts[:lock] == :share ? (sql << FOR_SHARE) : super
@@ -247,8 +247,10 @@ module Sequel
247
247
  compound_clone(:minus, dataset, opts)
248
248
  end
249
249
 
250
+ # Use a custom expression with EXISTS to determine whether a dataset
251
+ # is empty.
250
252
  def empty?
251
- db[:dual].where(unordered.exists).get(1) == nil
253
+ db[:dual].where(@opts[:offset] ? exists : unordered.exists).get(1) == nil
252
254
  end
253
255
 
254
256
  # Oracle requires SQL standard datetimes
@@ -283,6 +285,16 @@ module Sequel
283
285
  true
284
286
  end
285
287
 
288
+ # Oracle supports GROUP BY CUBE
289
+ def supports_group_cube?
290
+ true
291
+ end
292
+
293
+ # Oracle supports GROUP BY ROLLUP
294
+ def supports_group_rollup?
295
+ true
296
+ end
297
+
286
298
  # Oracle does not support INTERSECT ALL or EXCEPT ALL
287
299
  def supports_intersect_except_all?
288
300
  false
@@ -345,8 +345,11 @@ module Sequel
345
345
  (conn.server_version rescue nil) if conn.respond_to?(:server_version)
346
346
  end
347
347
  unless @server_version
348
- m = /PostgreSQL (\d+)\.(\d+)(?:(?:rc\d+)|\.(\d+))?/.match(fetch('SELECT version()').single_value)
349
- @server_version = (m[1].to_i * 10000) + (m[2].to_i * 100) + m[3].to_i
348
+ @server_version = if m = /PostgreSQL (\d+)\.(\d+)(?:(?:rc\d+)|\.(\d+))?/.match(fetch('SELECT version()').single_value)
349
+ (m[1].to_i * 10000) + (m[2].to_i * 100) + m[3].to_i
350
+ else
351
+ 0
352
+ end
350
353
  end
351
354
  @server_version
352
355
  end
@@ -493,7 +496,7 @@ module Sequel
493
496
  filter = " WHERE #{filter_expr(filter)}" if filter
494
497
  case index_type
495
498
  when :full_text
496
- expr = "(to_tsvector(#{literal(index[:language] || 'simple')}, #{dataset.send(:full_text_string_join, cols)}))"
499
+ expr = "(to_tsvector(#{literal(index[:language] || 'simple')}, #{literal(dataset.send(:full_text_string_join, cols))}))"
497
500
  index_type = :gin
498
501
  when :spatial
499
502
  index_type = :gist
@@ -577,7 +580,8 @@ module Sequel
577
580
  SQL::Function.new(:format_type, :pg_type__oid, :pg_attribute__atttypmod).as(:db_type),
578
581
  SQL::Function.new(:pg_get_expr, :pg_attrdef__adbin, :pg_class__oid).as(:default),
579
582
  SQL::BooleanExpression.new(:NOT, :pg_attribute__attnotnull).as(:allow_null),
580
- SQL::Function.new(:COALESCE, SQL::BooleanExpression.from_value_pairs(:pg_attribute__attnum => SQL::Function.new(:ANY, :pg_index__indkey)), false).as(:primary_key)).
583
+ SQL::Function.new(:COALESCE, SQL::BooleanExpression.from_value_pairs(:pg_attribute__attnum => SQL::Function.new(:ANY, :pg_index__indkey)), false).as(:primary_key),
584
+ :pg_namespace__nspname).
581
585
  from(:pg_class).
582
586
  join(:pg_attribute, :attrelid=>:oid).
583
587
  join(:pg_type, :oid=>:atttypid).
@@ -589,7 +593,16 @@ module Sequel
589
593
  filter(:pg_class__relname=>m2.call(table_name)).
590
594
  order(:pg_attribute__attnum)
591
595
  ds = filter_schema(ds, opts)
596
+ current_schema = nil
592
597
  ds.map do |row|
598
+ sch = row.delete(:nspname)
599
+ if current_schema
600
+ if sch != current_schema
601
+ raise Error, "columns from tables in two separate schema were returned (please specify a schema): #{current_schema.inspect}, #{sch.inspect}"
602
+ end
603
+ else
604
+ current_schema = sch
605
+ end
593
606
  row[:default] = nil if blank_object?(row[:default])
594
607
  row[:type] = schema_column_type(row[:db_type])
595
608
  [m.call(row.delete(:name)), row]
@@ -746,7 +759,8 @@ module Sequel
746
759
  # in 8.3 by default, and available for earlier versions as an add-on).
747
760
  def full_text_search(cols, terms, opts = {})
748
761
  lang = opts[:language] || 'simple'
749
- filter("to_tsvector(#{literal(lang)}, #{full_text_string_join(cols)}) @@ to_tsquery(#{literal(lang)}, #{literal(Array(terms).join(' | '))})")
762
+ terms = terms.join(' | ') if terms.is_a?(Array)
763
+ filter("to_tsvector(?, ?) @@ to_tsquery(?, ?)", lang, full_text_string_join(cols), lang, terms)
750
764
  end
751
765
 
752
766
  # Insert given values into the database.
@@ -756,7 +770,14 @@ module Sequel
756
770
  elsif !@opts[:sql] && supports_insert_select?
757
771
  returning(insert_pk).insert(*values){|r| return r.values.first}
758
772
  elsif (f = opts[:from]) && !f.empty?
759
- execute_insert(insert_sql(*values), :table=>f.first, :values=>values.size == 1 ? values.first : values)
773
+ v = if values.size == 1
774
+ values.first
775
+ elsif values.size == 2 && values.all?{|v0| v0.is_a?(Array)}
776
+ Hash[*values.first.zip(values.last).flatten]
777
+ else
778
+ values
779
+ end
780
+ execute_insert(insert_sql(*values), :table=>f.first, :values=>v)
760
781
  else
761
782
  super
762
783
  end
@@ -812,7 +833,7 @@ module Sequel
812
833
  if type == :insert
813
834
  server_version >= 80200 && !opts[:disable_insert_returning]
814
835
  else
815
- server_version >= 90100
836
+ server_version >= 80200
816
837
  end
817
838
  end
818
839
 
@@ -831,6 +852,29 @@ module Sequel
831
852
  clone(:window=>(@opts[:window]||[]) + [[name, SQL::Window.new(opts)]])
832
853
  end
833
854
 
855
+ protected
856
+
857
+ # If returned primary keys are requested, use RETURNING unless already set on the
858
+ # dataset. If RETURNING is already set, use existing returning values. If RETURNING
859
+ # is only set to return a single columns, return an array of just that column.
860
+ # Otherwise, return an array of hashes.
861
+ def _import(columns, values, opts={})
862
+ if server_version >= 80200
863
+ if opts[:return] == :primary_key && !@opts[:returning]
864
+ returning(insert_pk)._import(columns, values, opts)
865
+ elsif @opts[:returning]
866
+ statements = multi_insert_sql(columns, values)
867
+ @db.transaction(opts.merge(:server=>@opts[:server])) do
868
+ statements.map{|st| returning_fetch_rows(st)}
869
+ end.first.map{|v| v.length == 1 ? v.values.first : v}
870
+ else
871
+ super
872
+ end
873
+ else
874
+ super
875
+ end
876
+ end
877
+
834
878
  private
835
879
 
836
880
  # PostgreSQL allows deleting from joined datasets
@@ -948,7 +992,7 @@ module Sequel
948
992
  cols = Array(cols).map{|x| SQL::Function.new(:COALESCE, x, EMPTY_STRING)}
949
993
  cols = cols.zip([SPACE] * cols.length).flatten
950
994
  cols.pop
951
- literal(SQL::StringExpression.new(:'||', *cols))
995
+ SQL::StringExpression.new(:'||', *cols)
952
996
  end
953
997
 
954
998
  # PostgreSQL splits the main table from the joined tables
@@ -405,7 +405,7 @@ module Sequel
405
405
  AS = Dataset::AS
406
406
  APOS = Dataset::APOS
407
407
  EXTRACT_OPEN = "CAST(strftime(".freeze
408
- EXRACT_CLOSE = ') AS '.freeze
408
+ EXTRACT_CLOSE = ') AS '.freeze
409
409
  NUMERIC = 'NUMERIC'.freeze
410
410
  INTEGER = 'INTEGER'.freeze
411
411
  BACKTICK = '`'.freeze
@@ -435,13 +435,14 @@ module Sequel
435
435
  raise(Sequel::Error, "unsupported extract argument: #{part.inspect}") unless format = EXTRACT_MAP[part]
436
436
  sql << EXTRACT_OPEN << format << COMMA
437
437
  literal_append(sql, args.at(1))
438
- sql << EXRACT_CLOSE << (part == :second ? NUMERIC : INTEGER) << PAREN_CLOSE
438
+ sql << EXTRACT_CLOSE << (part == :second ? NUMERIC : INTEGER) << PAREN_CLOSE
439
439
  else
440
440
  super
441
441
  end
442
442
  end
443
443
 
444
- # MSSQL doesn't support the SQL standard CURRENT_DATE or CURRENT_TIME
444
+ # SQLite has CURRENT_TIMESTAMP and related constants in UTC instead
445
+ # of in localtime, so convert those constants to local time.
445
446
  def constant_sql_append(sql, constant)
446
447
  if c = CONSTANT_MAP[constant]
447
448
  sql << c
@@ -1,9 +1,6 @@
1
1
  module Sequel
2
2
  class Dataset
3
3
  module StoredProcedureMethods
4
- SQL_QUERY_TYPE = Hash.new{|h,k| h[k] = k}
5
- SQL_QUERY_TYPE[:first] = SQL_QUERY_TYPE[:all] = :select
6
-
7
4
  # The name of the stored procedure to call
8
5
  attr_accessor :sproc_name
9
6
 
@@ -44,14 +41,7 @@ module Sequel
44
41
  # ignored anyway).
45
42
  def sproc_type=(type)
46
43
  @sproc_type = type
47
- meta_def("#{sql_query_type}_sql"){|*a| ''}
48
- end
49
-
50
- private
51
-
52
- # The type of query (:select, :insert, :delete, :update).
53
- def sql_query_type
54
- SQL_QUERY_TYPE[@sproc_type]
44
+ @opts[:sql] = ''
55
45
  end
56
46
  end
57
47
 
@@ -125,6 +125,13 @@ module Sequel
125
125
  # foreign_key(:artist_id) # artist_id INTEGER
126
126
  # foreign_key(:artist_id, :artists) # artist_id INTEGER REFERENCES artists
127
127
  # foreign_key(:artist_id, :artists, :key=>:id) # artist_id INTEGER REFERENCES artists(id)
128
+ #
129
+ # If you want a foreign key constraint without adding a column (usually because it is a
130
+ # composite foreign key), you can provide an array of columns as the first argument, and
131
+ # you can provide the :name option to name the constraint:
132
+ #
133
+ # foreign_key([:artist_name, :artist_location], :artists, :name=>:artist_fk)
134
+ # # ADD CONSTRAINT artist_fk FOREIGN KEY (artist_name, artist_location) REFERENCES artists
128
135
  def foreign_key(name, table=nil, opts = {})
129
136
  opts = case table
130
137
  when Hash
@@ -282,8 +289,8 @@ module Sequel
282
289
  # to the DDL for the table. See Generator#column for the available options.
283
290
  #
284
291
  # You can also pass an array of column names for creating composite foreign
285
- # keys. In this case, it will assume the columns exists and will only add
286
- # the constraint.
292
+ # keys. In this case, it will assume the columns exist and will only add
293
+ # the constraint. You can provide a :name option to name the constraint.
287
294
  #
288
295
  # NOTE: If you need to add a foreign key constraint to a single existing column
289
296
  # use the composite key syntax even if it is only one column.
@@ -137,10 +137,10 @@ module Sequel
137
137
 
138
138
  # Returns true if no records exist in the dataset, false otherwise
139
139
  #
140
- # DB[:table].empty? # SELECT 1 FROM table LIMIT 1
140
+ # DB[:table].empty? # SELECT 1 AS one FROM table LIMIT 1
141
141
  # # => false
142
142
  def empty?
143
- get(1).nil?
143
+ get(Sequel::SQL::AliasedExpression.new(1, :one)).nil?
144
144
  end
145
145
 
146
146
  # Executes a select query and fetches records, yielding each record to the
@@ -229,31 +229,33 @@ module Sequel
229
229
  # DB[:table].import([:x, :y], DB[:table2].select(:a, :b))
230
230
  # # INSERT INTO table (x, y) SELECT a, b FROM table2
231
231
  #
232
- # The method also accepts a :slice or :commit_every option that specifies
233
- # the number of records to insert per transaction. This is useful especially
234
- # when inserting a large number of records, e.g.:
235
- #
236
- # # this will commit every 50 records
237
- # dataset.import([:x, :y], [[1, 2], [3, 4], ...], :slice => 50)
232
+ # Options:
233
+ # :commit_every :: Open a new transaction for every given number of records.
234
+ # For example, if you provide a value of 50, will commit
235
+ # after every 50 records.
236
+ # :server :: Set the server/shard to use for the transaction and insert
237
+ # queries.
238
+ # :slice :: Same as :commit_every, :commit_every takes precedence.
238
239
  def import(columns, values, opts={})
239
240
  return @db.transaction{insert(columns, values)} if values.is_a?(Dataset)
240
241
 
241
242
  return if values.empty?
242
243
  raise(Error, IMPORT_ERROR_MSG) if columns.empty?
244
+ ds = opts[:server] ? server(opts[:server]) : self
243
245
 
244
246
  if slice_size = opts[:commit_every] || opts[:slice]
245
247
  offset = 0
246
- loop do
247
- @db.transaction(opts){multi_insert_sql(columns, values[offset, slice_size]).each{|st| execute_dui(st)}}
248
+ rows = []
249
+ while offset < values.length
250
+ rows << ds._import(columns, values[offset, slice_size], opts)
248
251
  offset += slice_size
249
- break if offset >= values.length
250
252
  end
253
+ rows.flatten
251
254
  else
252
- statements = multi_insert_sql(columns, values)
253
- @db.transaction{statements.each{|st| execute_dui(st)}}
255
+ ds._import(columns, values, opts)
254
256
  end
255
257
  end
256
-
258
+
257
259
  # Inserts values into the associated table. The returned value is generally
258
260
  # the value of the primary key for the inserted row, but that is adapter dependent.
259
261
  #
@@ -300,21 +302,24 @@ module Sequel
300
302
 
301
303
  # Inserts multiple values. If a block is given it is invoked for each
302
304
  # item in the given array before inserting it. See +multi_insert+ as
303
- # a possible faster version that inserts multiple records in one
304
- # SQL statement.
305
+ # a possibly faster version that may be able to insert multiple
306
+ # records in one SQL statement (if supported by the database).
307
+ # Returns an array of primary keys of inserted rows.
305
308
  #
306
309
  # DB[:table].insert_multiple([{:x=>1}, {:x=>2}])
310
+ # # => [4, 5]
307
311
  # # INSERT INTO table (x) VALUES (1)
308
312
  # # INSERT INTO table (x) VALUES (2)
309
313
  #
310
314
  # DB[:table].insert_multiple([{:x=>1}, {:x=>2}]){|row| row[:y] = row[:x] * 2}
315
+ # # => [6, 7]
311
316
  # # INSERT INTO table (x, y) VALUES (1, 2)
312
317
  # # INSERT INTO table (x, y) VALUES (2, 4)
313
318
  def insert_multiple(array, &block)
314
319
  if block
315
- array.each {|i| insert(block[i])}
320
+ array.map{|i| insert(block.call(i))}
316
321
  else
317
- array.each {|i| insert(i)}
322
+ array.map{|i| insert(i)}
318
323
  end
319
324
  end
320
325
 
@@ -397,7 +402,7 @@ module Sequel
397
402
  # otherwise some columns could be missed or set to null instead of to default
398
403
  # values.
399
404
  #
400
- # You can also use the :slice or :commit_every option that import accepts.
405
+ # This respects the same options as #import.
401
406
  def multi_insert(hashes, opts={})
402
407
  return if hashes.empty?
403
408
  columns = hashes.first.keys
@@ -603,6 +608,19 @@ module Sequel
603
608
 
604
609
  protected
605
610
 
611
+ # Internals of #import. If primary key values are requested, use
612
+ # separate insert commands for each row. Otherwise, call #multi_insert_sql
613
+ # and execute each statement it gives separately.
614
+ def _import(columns, values, opts)
615
+ trans_opts = opts.merge(:server=>@opts[:server])
616
+ if opts[:return] == :primary_key
617
+ @db.transaction(trans_opts){values.map{|v| insert(columns, v)}}
618
+ else
619
+ stmts = multi_insert_sql(columns, values)
620
+ @db.transaction(trans_opts){stmts.each{|st| execute_dui(st)}}
621
+ end
622
+ end
623
+
606
624
  # Return an array of arrays of values given by the symbols in ret_cols.
607
625
  def _select_map_multiple(ret_cols)
608
626
  map{|r| r.values_at(*ret_cols)}
@@ -61,6 +61,16 @@ module Sequel
61
61
  false
62
62
  end
63
63
 
64
+ # Whether the dataset supports CUBE with GROUP BY.
65
+ def supports_group_cube?
66
+ false
67
+ end
68
+
69
+ # Whether the dataset supports ROLLUP with GROUP BY.
70
+ def supports_group_rollup?
71
+ false
72
+ end
73
+
64
74
  # Whether this dataset supports the +insert_select+ method for returning all columns values
65
75
  # directly from an insert query.
66
76
  def supports_insert_select?
@@ -12,9 +12,6 @@ module Sequel
12
12
  # native database support for bind variables and prepared
13
13
  # statements (as opposed to the emulated ones used by default).
14
14
  module ArgumentMapper
15
- SQL_QUERY_TYPE = Hash.new{|h,k| h[k] = k}
16
- SQL_QUERY_TYPE[:first] = SQL_QUERY_TYPE[:all] = :select
17
-
18
15
  # The name of the prepared statement, if any.
19
16
  attr_accessor :prepared_statement_name
20
17
 
@@ -38,13 +35,6 @@ module Sequel
38
35
  @opts[:sql] = @prepared_sql
39
36
  @prepared_sql
40
37
  end
41
-
42
- private
43
-
44
- # The type of query (:select, :insert, :delete, :update).
45
- def sql_query_type
46
- SQL_QUERY_TYPE[@prepared_type]
47
- end
48
38
  end
49
39
 
50
40
  # Backbone of the prepared statement support. Grafts bind variable
@@ -218,7 +218,7 @@ module Sequel
218
218
  when Symbol
219
219
  sch, table, aliaz = split_symbol(s)
220
220
  if aliaz
221
- s = sch ? SQL::QualifiedIdentifier.new(sch.to_sym, table.to_sym) : SQL::Identifier.new(table.to_sym)
221
+ s = sch ? SQL::QualifiedIdentifier.new(sch, table) : SQL::Identifier.new(table)
222
222
  sources << SQL::AliasedExpression.new(s, aliaz.to_sym)
223
223
  else
224
224
  sources << s
@@ -337,6 +337,18 @@ module Sequel
337
337
  select_group(*columns, &block).select_more(COUNT_OF_ALL_AS_COUNT)
338
338
  end
339
339
 
340
+ # Adds the appropriate CUBE syntax to GROUP BY.
341
+ def group_cube
342
+ raise Error, "GROUP BY CUBE not supported on #{db.database_type}" unless supports_group_cube?
343
+ clone(:group_options=>:cube)
344
+ end
345
+
346
+ # Adds the appropriate ROLLUP syntax to GROUP BY.
347
+ def group_rollup
348
+ raise Error, "GROUP BY ROLLUP not supported on #{db.database_type}" unless supports_group_rollup?
349
+ clone(:group_options=>:rollup)
350
+ end
351
+
340
352
  # Returns a copy of the dataset with the HAVING conditions changed. See #filter for argument types.
341
353
  #
342
354
  # DB[:items].group(:sum).having(:sum=>10)