sequel 3.30.0 → 3.31.0

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