sequel 3.43.0 → 3.44.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 (73) hide show
  1. data/CHANGELOG +32 -0
  2. data/doc/association_basics.rdoc +10 -3
  3. data/doc/release_notes/3.37.0.txt +1 -1
  4. data/doc/release_notes/3.44.0.txt +152 -0
  5. data/lib/sequel/adapters/ado.rb +0 -6
  6. data/lib/sequel/adapters/db2.rb +0 -3
  7. data/lib/sequel/adapters/dbi.rb +0 -6
  8. data/lib/sequel/adapters/ibmdb.rb +0 -3
  9. data/lib/sequel/adapters/jdbc.rb +8 -12
  10. data/lib/sequel/adapters/jdbc/as400.rb +2 -18
  11. data/lib/sequel/adapters/jdbc/derby.rb +10 -0
  12. data/lib/sequel/adapters/jdbc/h2.rb +10 -0
  13. data/lib/sequel/adapters/jdbc/hsqldb.rb +10 -0
  14. data/lib/sequel/adapters/jdbc/sqlite.rb +5 -0
  15. data/lib/sequel/adapters/jdbc/sqlserver.rb +2 -4
  16. data/lib/sequel/adapters/mock.rb +5 -0
  17. data/lib/sequel/adapters/mysql2.rb +4 -13
  18. data/lib/sequel/adapters/odbc.rb +0 -5
  19. data/lib/sequel/adapters/oracle.rb +3 -5
  20. data/lib/sequel/adapters/postgres.rb +2 -1
  21. data/lib/sequel/adapters/shared/access.rb +10 -0
  22. data/lib/sequel/adapters/shared/cubrid.rb +9 -0
  23. data/lib/sequel/adapters/shared/db2.rb +10 -0
  24. data/lib/sequel/adapters/shared/mssql.rb +10 -0
  25. data/lib/sequel/adapters/shared/mysql.rb +14 -0
  26. data/lib/sequel/adapters/shared/oracle.rb +15 -0
  27. data/lib/sequel/adapters/shared/postgres.rb +26 -2
  28. data/lib/sequel/adapters/shared/sqlite.rb +15 -0
  29. data/lib/sequel/adapters/tinytds.rb +5 -32
  30. data/lib/sequel/adapters/utils/emulate_offset_with_row_number.rb +2 -37
  31. data/lib/sequel/core.rb +3 -3
  32. data/lib/sequel/database/misc.rb +40 -4
  33. data/lib/sequel/database/query.rb +1 -1
  34. data/lib/sequel/database/schema_methods.rb +33 -12
  35. data/lib/sequel/dataset/actions.rb +51 -2
  36. data/lib/sequel/dataset/features.rb +0 -6
  37. data/lib/sequel/dataset/sql.rb +1 -1
  38. data/lib/sequel/exceptions.rb +22 -7
  39. data/lib/sequel/extensions/columns_introspection.rb +30 -5
  40. data/lib/sequel/extensions/pg_auto_parameterize.rb +9 -0
  41. data/lib/sequel/model/associations.rb +50 -37
  42. data/lib/sequel/model/base.rb +30 -1
  43. data/lib/sequel/plugins/eager_each.rb +17 -21
  44. data/lib/sequel/plugins/identity_map.rb +2 -1
  45. data/lib/sequel/plugins/many_through_many.rb +1 -1
  46. data/lib/sequel/plugins/single_table_inheritance.rb +2 -2
  47. data/lib/sequel/plugins/tactical_eager_loading.rb +1 -1
  48. data/lib/sequel/sql.rb +4 -2
  49. data/lib/sequel/version.rb +1 -1
  50. data/spec/adapters/postgres_spec.rb +32 -2
  51. data/spec/adapters/sqlite_spec.rb +20 -0
  52. data/spec/core/database_spec.rb +40 -0
  53. data/spec/core/dataset_spec.rb +91 -4
  54. data/spec/core/mock_adapter_spec.rb +2 -1
  55. data/spec/core/schema_generator_spec.rb +4 -0
  56. data/spec/core/schema_spec.rb +9 -3
  57. data/spec/extensions/association_dependencies_spec.rb +3 -3
  58. data/spec/extensions/columns_introspection_spec.rb +28 -2
  59. data/spec/extensions/eager_each_spec.rb +0 -1
  60. data/spec/extensions/identity_map_spec.rb +3 -2
  61. data/spec/extensions/migration_spec.rb +6 -0
  62. data/spec/extensions/prepared_statements_associations_spec.rb +2 -2
  63. data/spec/extensions/rcte_tree_spec.rb +2 -2
  64. data/spec/extensions/single_table_inheritance_spec.rb +3 -0
  65. data/spec/extensions/tactical_eager_loading_spec.rb +1 -1
  66. data/spec/extensions/validation_class_methods_spec.rb +8 -0
  67. data/spec/integration/associations_test.rb +4 -4
  68. data/spec/integration/database_test.rb +68 -20
  69. data/spec/integration/dataset_test.rb +48 -0
  70. data/spec/integration/schema_test.rb +25 -1
  71. data/spec/model/associations_spec.rb +21 -8
  72. data/spec/model/dataset_methods_spec.rb +58 -18
  73. metadata +4 -2
data/lib/sequel/core.rb CHANGED
@@ -380,14 +380,14 @@ module Sequel
380
380
  end
381
381
 
382
382
  # If the supplied block takes a single argument,
383
- # yield a new <tt>SQL::VirtualRow</tt> instance to the block
384
- # argument. Otherwise, evaluate the block in the context of a new
383
+ # yield an <tt>SQL::VirtualRow</tt> instance to the block
384
+ # argument. Otherwise, evaluate the block in the context of a
385
385
  # <tt>SQL::VirtualRow</tt> instance.
386
386
  #
387
387
  # Sequel.virtual_row{a} # Sequel::SQL::Identifier.new(:a)
388
388
  # Sequel.virtual_row{|o| o.a{}} # Sequel::SQL::Function.new(:a)
389
389
  def self.virtual_row(&block)
390
- vr = SQL::VirtualRow.new
390
+ vr = VIRTUAL_ROW
391
391
  case block.arity
392
392
  when -1, 0
393
393
  vr.instance_exec(&block)
@@ -14,6 +14,10 @@ module Sequel
14
14
  # instances.
15
15
  DEFAULT_STRING_COLUMN_SIZE = 255
16
16
 
17
+ # Empty exception regexp to class map, used by default if Sequel doesn't
18
+ # have specific support for the database in use.
19
+ DEFAULT_DATABASE_ERROR_REGEXPS = {}.freeze
20
+
17
21
  # Register an extension callback for Database objects. ext should be the
18
22
  # extension name symbol, and mod should either be a Module that the
19
23
  # database is extended with, or a callable object called with the database
@@ -316,27 +320,59 @@ module Sequel
316
320
  obj.respond_to?(:empty?) ? obj.empty? : false
317
321
  end
318
322
  end
319
-
323
+
320
324
  # Which transaction errors to translate, blank by default.
321
325
  def database_error_classes
322
326
  []
323
327
  end
324
328
 
329
+ # A hash with regexp keys and exception class values, used
330
+ # to match against underlying driver exception messages in
331
+ # order to raise a more specific Sequel::Error subclass.
332
+ def database_error_regexps
333
+ DEFAULT_DATABASE_ERROR_REGEXPS
334
+ end
335
+
336
+ # Return the Sequel::DatabaseError subclass to wrap the given
337
+ # exception in.
338
+ def database_error_class(exception, opts)
339
+ database_specific_error_class(exception, opts) || DatabaseError
340
+ end
341
+
342
+ # Return a specific Sequel::DatabaseError exception class if
343
+ # one is appropriate for the underlying exception,
344
+ # or nil if there is no specific exception class.
345
+ def database_specific_error_class(exception, opts)
346
+ return DatabaseDisconnectError if disconnect_error?(exception, opts)
347
+
348
+ database_error_regexps.each do |regexp, klass|
349
+ return klass if exception.message =~ regexp
350
+ end
351
+
352
+ nil
353
+ end
354
+
325
355
  # Return true if exception represents a disconnect error, false otherwise.
326
356
  def disconnect_error?(exception, opts)
327
357
  opts[:disconnect]
328
358
  end
329
359
 
330
- # Convert the given exception to a DatabaseError, keeping message
331
- # and traceback.
360
+ # Convert the given exception to an appropriate Sequel::DatabaseError
361
+ # subclass, keeping message and traceback.
332
362
  def raise_error(exception, opts={})
333
363
  if !opts[:classes] || Array(opts[:classes]).any?{|c| exception.is_a?(c)}
334
- raise Sequel.convert_exception_class(exception, disconnect_error?(exception, opts) ? DatabaseDisconnectError : DatabaseError)
364
+ raise Sequel.convert_exception_class(exception, database_error_class(exception, opts))
335
365
  else
336
366
  raise exception
337
367
  end
338
368
  end
339
369
 
370
+ # Whether the database supports CREATE OR REPLACE VIEW. If not, support
371
+ # will be emulated by dropping the view first. false by default.
372
+ def supports_create_or_replace_view?
373
+ false
374
+ end
375
+
340
376
  # Typecast the value to an SQL::Blob
341
377
  def typecast_value_blob(value)
342
378
  value.is_a?(Sequel::SQL::Blob) ? value : Sequel::SQL::Blob.new(value)
@@ -585,7 +585,7 @@ module Sequel
585
585
 
586
586
  # Remove the cached schema for the given schema name
587
587
  def remove_cached_schema(table)
588
- @schemas.delete(quote_schema_table(table)) if @schemas
588
+ Sequel.synchronize{@schemas.delete(quote_schema_table(table))} if @schemas
589
589
  end
590
590
 
591
591
  # Remove the current thread from the list of active transactions
@@ -202,24 +202,34 @@ module Sequel
202
202
  create_table_generator_class.new(self, &block)
203
203
  end
204
204
 
205
- # Creates a view, replacing it if it already exists:
205
+ # Creates a view, replacing a view with the same name if one already exists.
206
206
  #
207
- # DB.create_or_replace_view(:cheap_items, "SELECT * FROM items WHERE price < 100")
208
- # DB.create_or_replace_view(:ruby_items, DB[:items].filter(:category => 'ruby'))
209
- def create_or_replace_view(name, source)
210
- source = source.sql if source.is_a?(Dataset)
211
- execute_ddl("CREATE OR REPLACE VIEW #{quote_schema_table(name)} AS #{source}")
212
- remove_cached_schema(name)
213
- nil
207
+ # DB.create_or_replace_view(:some_items, "SELECT * FROM items WHERE price < 100")
208
+ # DB.create_or_replace_view(:some_items, DB[:items].filter(:category => 'ruby'))
209
+ #
210
+ # For databases where replacing a view is not natively supported, support
211
+ # is emulated by dropping a view with the same name before creating the view.
212
+ def create_or_replace_view(name, source, options = {})
213
+ if supports_create_or_replace_view?
214
+ options = options.merge(:replace=>true)
215
+ else
216
+ drop_view(name) rescue nil
217
+ end
218
+
219
+ create_view(name, source, options)
214
220
  end
215
221
 
216
222
  # Creates a view based on a dataset or an SQL string:
217
223
  #
218
224
  # DB.create_view(:cheap_items, "SELECT * FROM items WHERE price < 100")
219
225
  # DB.create_view(:ruby_items, DB[:items].filter(:category => 'ruby'))
220
- def create_view(name, source)
221
- source = source.sql if source.is_a?(Dataset)
222
- execute_ddl("CREATE VIEW #{quote_schema_table(name)} AS #{source}")
226
+ #
227
+ # PostgreSQL/SQLite specific option:
228
+ # :temp :: Create a temporary view, automatically dropped on disconnect.
229
+ def create_view(name, source, options = {})
230
+ execute_ddl(create_view_sql(name, source, options))
231
+ remove_cached_schema(name)
232
+ nil
223
233
  end
224
234
 
225
235
  # Removes a column from the specified table:
@@ -581,11 +591,22 @@ module Sequel
581
591
  "#{create_table_prefix_sql(name, options)} AS #{sql}"
582
592
  end
583
593
 
584
- # DDL statement for creating a table with the given name, columns, and options
594
+ # DDL fragment for initial part of CREATE TABLE statement
585
595
  def create_table_prefix_sql(name, options)
586
596
  "CREATE #{temporary_table_sql if options[:temp]}TABLE#{' IF NOT EXISTS' if options[:if_not_exists]} #{options[:temp] ? quote_identifier(name) : quote_schema_table(name)}"
587
597
  end
588
598
 
599
+ # DDL statement for creating a view.
600
+ def create_view_sql(name, source, options)
601
+ source = source.sql if source.is_a?(Dataset)
602
+ "#{create_view_prefix_sql(name, options)} AS #{source}"
603
+ end
604
+
605
+ # DDL fragment for initial part of CREATE VIEW statement
606
+ def create_view_prefix_sql(name, options)
607
+ "CREATE #{'OR REPLACE 'if options[:replace]}VIEW #{quote_schema_table(name)}"
608
+ end
609
+
589
610
  # Default index name for the table and columns, may be too long
590
611
  # for certain databases.
591
612
  def default_index_name(table_name, columns)
@@ -11,7 +11,7 @@ module Sequel
11
11
  ACTION_METHODS = (<<-METHS).split.map{|x| x.to_sym}
12
12
  << [] []= all avg count columns columns! delete each
13
13
  empty? fetch_rows first get import insert insert_multiple interval last
14
- map max min multi_insert range select_hash select_hash_groups select_map select_order_map
14
+ map max min multi_insert paged_each range select_hash select_hash_groups select_map select_order_map
15
15
  set single_record single_value sum to_csv to_hash to_hash_groups truncate update
16
16
  METHS
17
17
 
@@ -80,7 +80,7 @@ module Sequel
80
80
  # # => [:id, :name]
81
81
  def columns
82
82
  return @columns if @columns
83
- ds = unfiltered.unordered.clone(:distinct => nil, :limit => 1, :offset=>nil)
83
+ ds = unfiltered.unordered.naked.clone(:distinct => nil, :limit => 1, :offset=>nil)
84
84
  ds.each{break}
85
85
  @columns = ds.instance_variable_get(:@columns)
86
86
  @columns || []
@@ -461,6 +461,55 @@ module Sequel
461
461
  import(columns, hashes.map{|h| columns.map{|c| h[c]}}, opts)
462
462
  end
463
463
 
464
+ # Yields each row in the dataset, but interally uses multiple queries as needed with
465
+ # limit and offset to process the entire result set without keeping all
466
+ # rows in the dataset in memory, even if the underlying driver buffers all
467
+ # query results in memory.
468
+ #
469
+ # Because this uses multiple queries internally, in order to remain consistent,
470
+ # it also uses a transaction internally. Additionally, to make sure that all rows
471
+ # in the dataset are yielded and none are yielded twice, the dataset must have an
472
+ # unambiguous order. Sequel requires that datasets using this method have an
473
+ # order, but it cannot ensure that the order is unambiguous.
474
+ #
475
+ # Options:
476
+ # :rows_per_fetch :: The number of rows to fetch per query. Defaults to 1000.
477
+ def paged_each(opts={})
478
+ unless @opts[:order]
479
+ raise Sequel::Error, "Dataset#paged_each requires the dataset be ordered"
480
+ end
481
+
482
+ total_limit = @opts[:limit]
483
+ offset = @opts[:offset] || 0
484
+
485
+ if server = @opts[:server]
486
+ opts = opts.merge(:server=>server)
487
+ end
488
+
489
+ rows_per_fetch = opts[:rows_per_fetch] || 1000
490
+ num_rows_yielded = rows_per_fetch
491
+ total_rows = 0
492
+
493
+ db.transaction(opts) do
494
+ while num_rows_yielded == rows_per_fetch && (total_limit.nil? || total_rows < total_limit)
495
+ if total_limit && total_rows + rows_per_fetch > total_limit
496
+ rows_per_fetch = total_limit - total_rows
497
+ end
498
+
499
+ num_rows_yielded = 0
500
+ limit(rows_per_fetch, offset).each do |row|
501
+ num_rows_yielded += 1
502
+ total_rows += 1 if total_limit
503
+ yield row
504
+ end
505
+
506
+ offset += rows_per_fetch
507
+ end
508
+ end
509
+
510
+ self
511
+ end
512
+
464
513
  # Returns a +Range+ instance made from the minimum and maximum values for the
465
514
  # given column/expression. Uses a virtual row block if no argument is given.
466
515
  #
@@ -159,12 +159,6 @@ module Sequel
159
159
  true
160
160
  end
161
161
 
162
- # Whether using an offset returns an extra row number column that should be
163
- # eliminated, false by default.
164
- def offset_returns_row_number_column?
165
- false
166
- end
167
-
168
162
  # Whether the RETURNING clause is used for the given dataset.
169
163
  # +type+ can be :insert, :update, or :delete.
170
164
  def uses_returning?(type)
@@ -654,7 +654,7 @@ module Sequel
654
654
  when SQL::Identifier
655
655
  [sch, table_name.value.to_s]
656
656
  when String
657
- [sch, table_name.to_s]
657
+ [sch, table_name]
658
658
  else
659
659
  raise Error, 'table_name should be a Symbol, SQL::QualifiedIdentifier, SQL::Identifier, or String'
660
660
  end
@@ -19,32 +19,47 @@ module Sequel
19
19
  # connection parameters it was given.
20
20
  class DatabaseConnectionError < DatabaseError; end
21
21
 
22
- # Error that should be raised by adapters when they determine that the connection
22
+ # Error raised by adapters when they determine that the connection
23
23
  # to the database has been lost. Instructs the connection pool code to
24
24
  # remove that connection from the pool so that other connections can be acquired
25
25
  # automatically.
26
26
  class DatabaseDisconnectError < DatabaseError; end
27
27
 
28
- # Raised on an invalid operation, such as trying to update or delete
28
+ # Generic error raised when Sequel determines a database constraint has been violated.
29
+ class ConstraintViolation < DatabaseError; end
30
+
31
+ # Error raised when Sequel determines a database check constraint has been violated.
32
+ class CheckConstraintViolation < ConstraintViolation; end
33
+
34
+ # Error raised when Sequel determines a database foreign key constraint has been violated.
35
+ class ForeignKeyConstraintViolation < ConstraintViolation; end
36
+
37
+ # Error raised when Sequel determines a database NOT NULL constraint has been violated.
38
+ class NotNullConstraintViolation < ConstraintViolation; end
39
+
40
+ # Error raised when Sequel determines a database unique constraint has been violated.
41
+ class UniqueConstraintViolation < ConstraintViolation; end
42
+
43
+ # Error raised on an invalid operation, such as trying to update or delete
29
44
  # a joined or grouped dataset.
30
45
  class InvalidOperation < Error; end
31
46
 
32
- # Raised when attempting an invalid type conversion.
47
+ # Error raised when attempting an invalid type conversion.
33
48
  class InvalidValue < Error ; end
34
49
 
35
- # Raised when the adapter adapter hasn't implemented a method such as +tables+:
50
+ # Error raised when the adapter adapter hasn't implemented a method such as +tables+:
36
51
  class NotImplemented < Error; end
37
52
 
38
- # Raised when the connection pool cannot acquire a database connection
53
+ # Error raised when the connection pool cannot acquire a database connection
39
54
  # before the timeout.
40
55
  class PoolTimeout < Error ; end
41
56
 
42
57
  # Exception that you should raise to signal a rollback of the current transaction.
43
58
  # The transaction block will catch this exception, rollback the current transaction,
44
- # and won't reraise it.
59
+ # and won't reraise it (unless a reraise is requested).
45
60
  class Rollback < Error ; end
46
61
 
47
- # Exception that occurs when unbinding a dataset that has multiple different values
62
+ # Error raised when unbinding a dataset that has multiple different values
48
63
  # for a given variable.
49
64
  class UnbindDuplicate < Error; end
50
65
 
@@ -2,8 +2,10 @@
2
2
  # selected columns for a dataset before issuing a query. If it
3
3
  # thinks it can guess correctly at the columns the query will use,
4
4
  # it will return the columns without issuing a database query.
5
+ #
5
6
  # This method is not fool-proof, it's possible that some databases
6
- # will use column names that Sequel does not expect.
7
+ # will use column names that Sequel does not expect. Also, it
8
+ # may not correctly handle all cases.
7
9
  #
8
10
  # To attempt to introspect columns for a single dataset:
9
11
  #
@@ -27,15 +29,38 @@ module Sequel
27
29
  # SQL::AliasedExpressions.
28
30
  def columns
29
31
  return @columns if @columns
30
- return columns_without_introspection unless cols = opts[:select] and !cols.empty?
31
- probable_columns = cols.map{|c| probable_column_name(c)}
32
- if probable_columns.all?
33
- @columns = probable_columns
32
+ if (pcs = probable_columns) && pcs.all?
33
+ @columns = pcs
34
34
  else
35
35
  columns_without_introspection
36
36
  end
37
37
  end
38
38
 
39
+ protected
40
+
41
+ # Return an array of probable column names for the dataset, or
42
+ # nil if it is not possible to determine that through
43
+ # introspection.
44
+ def probable_columns
45
+ if (cols = opts[:select]) && !cols.empty?
46
+ cols.map{|c| probable_column_name(c)}
47
+ elsif !opts[:join] && !opts[:with] && (from = opts[:from]) && from.length == 1 && (from = from.first)
48
+ if from.is_a?(SQL::AliasedExpression)
49
+ from = from.expression
50
+ end
51
+
52
+ case from
53
+ when Dataset
54
+ from.probable_columns
55
+ when Symbol, SQL::Identifier, SQL::QualifiedIdentifier
56
+ schemas = db.instance_variable_get(:@schemas)
57
+ if schemas && (sch = Sequel.synchronize{schemas[literal(from)]})
58
+ sch.map{|c,_| c}
59
+ end
60
+ end
61
+ end
62
+ end
63
+
39
64
  private
40
65
 
41
66
  # Return the probable name of the column, or nil if one
@@ -102,6 +102,15 @@ module Sequel
102
102
  end
103
103
  super
104
104
  end
105
+
106
+ private
107
+
108
+ def create_view_sql(name, source, options)
109
+ if source.is_a?(DatasetMethods)
110
+ source = source.no_auto_parameterize
111
+ end
112
+ super
113
+ end
105
114
  end
106
115
 
107
116
  module DatasetMethods
@@ -22,11 +22,6 @@ module Sequel
22
22
  :"_add_#{singularize(self[:name])}"
23
23
  end
24
24
 
25
- # Name symbol for the _dataset association method
26
- def _dataset_method
27
- :"_#{self[:name]}_dataset"
28
- end
29
-
30
25
  # Name symbol for the _remove_all internal association method
31
26
  def _remove_all_method
32
27
  :"_remove_all_#{self[:name]}"
@@ -56,6 +51,29 @@ module Sequel
56
51
  def associated_class
57
52
  cached_fetch(:class){constantize(self[:class_name])}
58
53
  end
54
+
55
+ # The dataset associated via this association, with the non-instance specific
56
+ # changes already applied.
57
+ def associated_dataset
58
+ cached_fetch(:_dataset){apply_dataset_changes(associated_class.dataset.clone)}
59
+ end
60
+
61
+ # Apply all non-instance specific changes to the given dataset and return it.
62
+ def apply_dataset_changes(ds)
63
+ ds.extend(AssociationDatasetMethods)
64
+ ds.association_reflection = self
65
+ self[:extend].each{|m| ds.extend(m)}
66
+ ds = ds.select(*select) if select
67
+ if c = self[:conditions]
68
+ ds = (c.is_a?(Array) && !Sequel.condition_specifier?(c)) ? ds.where(*c) : ds.where(c)
69
+ end
70
+ ds = ds.order(*self[:order]) if self[:order]
71
+ ds = ds.limit(*self[:limit]) if self[:limit]
72
+ ds = ds.limit(1) if !returns_array? && self[:key]
73
+ ds = ds.eager(*self[:eager]) if self[:eager]
74
+ ds = ds.distinct if self[:distinct]
75
+ ds
76
+ end
59
77
 
60
78
  # Whether this association can have associated objects, given the current
61
79
  # object. Should be false if obj cannot have associated objects because
@@ -688,7 +706,7 @@ module Sequel
688
706
  def apply_association_dataset_opts(opts, ds)
689
707
  ds = ds.select(*opts.select) if opts.select
690
708
  if c = opts[:conditions]
691
- ds = (c.is_a?(Array) && !Sequel.condition_specifier?(c)) ? ds.filter(*c) : ds.filter(c)
709
+ ds = (c.is_a?(Array) && !Sequel.condition_specifier?(c)) ? ds.where(*c) : ds.where(c)
692
710
  end
693
711
  ds = ds.order(*opts[:order]) if opts[:order]
694
712
  ds = ds.eager(opts[:eager]) if opts[:eager]
@@ -747,9 +765,10 @@ module Sequel
747
765
  # :clone :: Merge the current options and block into the options and block used in defining
748
766
  # the given association. Can be used to DRY up a bunch of similar associations that
749
767
  # all share the same options such as :class and :key, while changing the order and block used.
750
- # :conditions :: The conditions to use to filter the association, can be any argument passed to filter.
751
- # :dataset :: A proc that is instance_evaled to get the base dataset
752
- # to use for the _dataset method (before the other options are applied).
768
+ # :conditions :: The conditions to use to filter the association, can be any argument passed to where.
769
+ # :dataset :: A proc that is instance_execed to get the base dataset to use (before the other
770
+ # options are applied). If the proc accepts an argument, it is passed the related
771
+ # association reflection.
753
772
  # :distinct :: Use the DISTINCT clause when selecting associating object, both when
754
773
  # lazy loading and eager loading via .eager (but not when using .eager_graph).
755
774
  # :eager :: The associations to eagerly load via +eager+ when loading the associated object(s).
@@ -1047,7 +1066,7 @@ module Sequel
1047
1066
  else
1048
1067
  SQL::QualifiedIdentifier.new(table, pk)
1049
1068
  end
1050
- ds.filter(pk=>subquery)
1069
+ ds.where(pk=>subquery)
1051
1070
  end
1052
1071
 
1053
1072
  # Use a window function to limit the results of the eager loading dataset.
@@ -1091,7 +1110,6 @@ module Sequel
1091
1110
 
1092
1111
  # Adds the association dataset methods to the association methods module.
1093
1112
  def def_association_dataset_methods(opts)
1094
- association_module_private_def(opts._dataset_method, opts, &opts[:dataset])
1095
1113
  association_module_def(opts.dataset_method, opts){_dataset(opts)}
1096
1114
  def_association_method(opts)
1097
1115
  end
@@ -1127,7 +1145,7 @@ module Sequel
1127
1145
  graph_jt_conds = opts[:graph_join_table_conditions] = opts.fetch(:graph_join_table_conditions, []).to_a
1128
1146
  opts[:graph_join_table_join_type] ||= opts[:graph_join_type]
1129
1147
  opts[:after_load].unshift(:array_uniq!) if opts[:uniq]
1130
- opts[:dataset] ||= proc{opts.associated_class.inner_join(join_table, rcks.zip(opts.right_primary_keys) + opts.predicate_keys.zip(lcpks.map{|k| send(k)}), :qualify=>:deep)}
1148
+ opts[:dataset] ||= proc{opts.associated_dataset.inner_join(join_table, rcks.zip(opts.right_primary_keys) + opts.predicate_keys.zip(lcpks.map{|k| send(k)}), :qualify=>:deep)}
1131
1149
 
1132
1150
  opts[:eager_loader] ||= proc do |eo|
1133
1151
  h = eo[:id_map]
@@ -1190,10 +1208,10 @@ module Sequel
1190
1208
  _join_table_dataset(opts).insert(h)
1191
1209
  end
1192
1210
  association_module_private_def(opts._remove_method, opts) do |o|
1193
- _join_table_dataset(opts).filter(lcks.zip(lcpks.map{|k| send(k)}) + rcks.zip(opts.right_primary_key_methods.map{|k| o.send(k)})).delete
1211
+ _join_table_dataset(opts).where(lcks.zip(lcpks.map{|k| send(k)}) + rcks.zip(opts.right_primary_key_methods.map{|k| o.send(k)})).delete
1194
1212
  end
1195
1213
  association_module_private_def(opts._remove_all_method, opts) do
1196
- _join_table_dataset(opts).filter(lcks.zip(lcpks.map{|k| send(k)})).delete
1214
+ _join_table_dataset(opts).where(lcks.zip(lcpks.map{|k| send(k)})).delete
1197
1215
  end
1198
1216
 
1199
1217
  def_add_method(opts)
@@ -1219,8 +1237,7 @@ module Sequel
1219
1237
  qualify = opts[:qualify] != false
1220
1238
  opts[:cartesian_product_number] ||= 0
1221
1239
  opts[:dataset] ||= proc do
1222
- klass = opts.associated_class
1223
- klass.filter(opts.predicate_keys.zip(cks.map{|k| send(k)}))
1240
+ opts.associated_dataset.where(opts.predicate_keys.zip(cks.map{|k| send(k)}))
1224
1241
  end
1225
1242
  opts[:eager_loader] ||= proc do |eo|
1226
1243
  h = eo[:id_map]
@@ -1231,7 +1248,7 @@ module Sequel
1231
1248
  # Skip eager loading if no objects have a foreign key for this association
1232
1249
  unless keys.empty?
1233
1250
  klass = opts.associated_class
1234
- model.eager_loading_dataset(opts, klass.filter(opts.predicate_key=>keys), nil, eo[:associations], eo).all do |assoc_record|
1251
+ model.eager_loading_dataset(opts, klass.where(opts.predicate_key=>keys), nil, eo[:associations], eo).all do |assoc_record|
1235
1252
  hash_key = uses_cks ? opts.primary_key_methods.map{|k| assoc_record.send(k)} : assoc_record.send(opts.primary_key_method)
1236
1253
  next unless objects = h[hash_key]
1237
1254
  objects.each{|object| object.associations[name] = assoc_record}
@@ -1276,7 +1293,7 @@ module Sequel
1276
1293
  raise(Error, "mismatched number of keys: #{cks.inspect} vs #{cpks.inspect}") unless cks.length == cpks.length
1277
1294
  uses_cks = opts[:uses_composite_keys] = cks.length > 1
1278
1295
  opts[:dataset] ||= proc do
1279
- opts.associated_class.filter(opts.predicate_keys.zip(cpks.map{|k| send(k)}))
1296
+ opts.associated_dataset.where(opts.predicate_keys.zip(cpks.map{|k| send(k)}))
1280
1297
  end
1281
1298
  opts[:eager_loader] ||= proc do |eo|
1282
1299
  h = eo[:id_map]
@@ -1289,7 +1306,7 @@ module Sequel
1289
1306
  reciprocal = opts.reciprocal
1290
1307
  klass = opts.associated_class
1291
1308
  filter_keys = opts.predicate_key
1292
- ds = model.eager_loading_dataset(opts, klass.filter(filter_keys=>h.keys), nil, eo[:associations], eo)
1309
+ ds = model.eager_loading_dataset(opts, klass.where(filter_keys=>h.keys), nil, eo[:associations], eo)
1293
1310
  case opts.eager_limit_strategy
1294
1311
  when :distinct_on
1295
1312
  ds = ds.distinct(*filter_keys).order_prepend(*filter_keys)
@@ -1351,7 +1368,7 @@ module Sequel
1351
1368
 
1352
1369
  if one_to_one
1353
1370
  association_module_private_def(opts._setter_method, opts) do |o|
1354
- up_ds = _apply_association_options(opts, opts.associated_class.filter(cks.zip(cpks.map{|k| send(k)})))
1371
+ up_ds = _apply_association_options(opts, opts.associated_dataset.where(cks.zip(cpks.map{|k| send(k)})))
1355
1372
  if o
1356
1373
  up_ds = up_ds.exclude(o.pk_hash) unless o.new?
1357
1374
  cks.zip(cpks).each{|k, pk| o.send(:"#{k}=", send(pk))}
@@ -1374,7 +1391,7 @@ module Sequel
1374
1391
  o.save(:validate=>validate) || raise(Sequel::Error, "invalid associated object, cannot save")
1375
1392
  end
1376
1393
  association_module_private_def(opts._remove_all_method, opts) do
1377
- _apply_association_options(opts, opts.associated_class.filter(cks.zip(cpks.map{|k| send(k)}))).update(ck_nil_hash)
1394
+ _apply_association_options(opts, opts.associated_dataset.where(cks.zip(cpks.map{|k| send(k)}))).update(ck_nil_hash)
1378
1395
  end
1379
1396
  def_remove_methods(opts)
1380
1397
  end
@@ -1430,19 +1447,10 @@ module Sequel
1430
1447
 
1431
1448
  # Apply the association options such as :order and :limit to the given dataset, returning a modified dataset.
1432
1449
  def _apply_association_options(opts, ds)
1433
- ds.extend(AssociationDatasetMethods)
1434
- ds.model_object = self
1435
- ds.association_reflection = opts
1436
- opts[:extend].each{|m| ds.extend(m)}
1437
- ds = ds.select(*opts.select) if opts.select
1438
- if c = opts[:conditions]
1439
- ds = (c.is_a?(Array) && !Sequel.condition_specifier?(c)) ? ds.filter(*c) : ds.filter(c)
1450
+ unless ds.kind_of?(AssociationDatasetMethods)
1451
+ ds = opts.apply_dataset_changes(ds)
1440
1452
  end
1441
- ds = ds.order(*opts[:order]) if opts[:order]
1442
- ds = ds.limit(*opts[:limit]) if opts[:limit]
1443
- ds = ds.limit(1) if !opts.returns_array? && opts[:key]
1444
- ds = ds.eager(*opts[:eager]) if opts[:eager]
1445
- ds = ds.distinct if opts[:distinct]
1453
+ ds.model_object = self
1446
1454
  ds = ds.eager_graph(opts[:eager_graph]) if opts[:eager_graph] && opts.eager_graph_lazy_dataset?
1447
1455
  ds = instance_exec(ds, &opts[:block]) if opts[:block]
1448
1456
  ds
@@ -1460,7 +1468,12 @@ module Sequel
1460
1468
  # Return an association dataset for the given association reflection
1461
1469
  def _dataset(opts)
1462
1470
  raise(Sequel::Error, "model object #{inspect} does not have a primary key") if opts.dataset_need_primary_key? && !pk
1463
- _apply_association_options(opts, send(opts._dataset_method))
1471
+ ds = if opts[:dataset].arity == 1
1472
+ instance_exec(opts, &opts[:dataset])
1473
+ else
1474
+ instance_exec(&opts[:dataset])
1475
+ end
1476
+ _apply_association_options(opts, ds)
1464
1477
  end
1465
1478
 
1466
1479
  # Dataset for the join table of the given many to many association reflection
@@ -1601,7 +1614,7 @@ module Sequel
1601
1614
  o = remove_check_existing_object_from_pk(opts, o, *args)
1602
1615
  elsif !o.is_a?(klass)
1603
1616
  raise(Sequel::Error, "associated object #{o.inspect} not of correct type #{klass}")
1604
- elsif opts.remove_should_check_existing? && send(opts.dataset_method).filter(o.pk_hash).empty?
1617
+ elsif opts.remove_should_check_existing? && send(opts.dataset_method).where(o.pk_hash).empty?
1605
1618
  raise(Sequel::Error, "associated object #{o.inspect} is not currently associated to #{inspect}")
1606
1619
  end
1607
1620
  raise(Sequel::Error, "model object #{inspect} does not have a primary key") unless pk
@@ -1723,7 +1736,7 @@ module Sequel
1723
1736
  # allows you to customize it at association definition time. For example,
1724
1737
  # if you wanted artists with their albums since 1990:
1725
1738
  #
1726
- # Artist.eager(:albums => proc{|ds| ds.filter{year > 1990}})
1739
+ # Artist.eager(:albums => proc{|ds| ds.where{year > 1990}})
1727
1740
  #
1728
1741
  # Or if you needed albums and their artist's name only, using a single query:
1729
1742
  #
@@ -1734,7 +1747,7 @@ module Sequel
1734
1747
  # the cascaded associations as the value. This will load artists with their albums
1735
1748
  # since 1990, and also the tracks on those albums and the genre for those tracks:
1736
1749
  #
1737
- # Artist.eager(:albums => {proc{|ds| ds.filter{year > 1990}}=>{:tracks => :genre}})
1750
+ # Artist.eager(:albums => {proc{|ds| ds.where{year > 1990}}=>{:tracks => :genre}})
1738
1751
  module DatasetMethods
1739
1752
  # Add the <tt>eager!</tt> and <tt>eager_graph!</tt> mutation methods to the dataset.
1740
1753
  def self.extended(obj)