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.
- data/CHANGELOG +32 -0
- data/doc/association_basics.rdoc +10 -3
- data/doc/release_notes/3.37.0.txt +1 -1
- data/doc/release_notes/3.44.0.txt +152 -0
- data/lib/sequel/adapters/ado.rb +0 -6
- data/lib/sequel/adapters/db2.rb +0 -3
- data/lib/sequel/adapters/dbi.rb +0 -6
- data/lib/sequel/adapters/ibmdb.rb +0 -3
- data/lib/sequel/adapters/jdbc.rb +8 -12
- data/lib/sequel/adapters/jdbc/as400.rb +2 -18
- data/lib/sequel/adapters/jdbc/derby.rb +10 -0
- data/lib/sequel/adapters/jdbc/h2.rb +10 -0
- data/lib/sequel/adapters/jdbc/hsqldb.rb +10 -0
- data/lib/sequel/adapters/jdbc/sqlite.rb +5 -0
- data/lib/sequel/adapters/jdbc/sqlserver.rb +2 -4
- data/lib/sequel/adapters/mock.rb +5 -0
- data/lib/sequel/adapters/mysql2.rb +4 -13
- data/lib/sequel/adapters/odbc.rb +0 -5
- data/lib/sequel/adapters/oracle.rb +3 -5
- data/lib/sequel/adapters/postgres.rb +2 -1
- data/lib/sequel/adapters/shared/access.rb +10 -0
- data/lib/sequel/adapters/shared/cubrid.rb +9 -0
- data/lib/sequel/adapters/shared/db2.rb +10 -0
- data/lib/sequel/adapters/shared/mssql.rb +10 -0
- data/lib/sequel/adapters/shared/mysql.rb +14 -0
- data/lib/sequel/adapters/shared/oracle.rb +15 -0
- data/lib/sequel/adapters/shared/postgres.rb +26 -2
- data/lib/sequel/adapters/shared/sqlite.rb +15 -0
- data/lib/sequel/adapters/tinytds.rb +5 -32
- data/lib/sequel/adapters/utils/emulate_offset_with_row_number.rb +2 -37
- data/lib/sequel/core.rb +3 -3
- data/lib/sequel/database/misc.rb +40 -4
- data/lib/sequel/database/query.rb +1 -1
- data/lib/sequel/database/schema_methods.rb +33 -12
- data/lib/sequel/dataset/actions.rb +51 -2
- data/lib/sequel/dataset/features.rb +0 -6
- data/lib/sequel/dataset/sql.rb +1 -1
- data/lib/sequel/exceptions.rb +22 -7
- data/lib/sequel/extensions/columns_introspection.rb +30 -5
- data/lib/sequel/extensions/pg_auto_parameterize.rb +9 -0
- data/lib/sequel/model/associations.rb +50 -37
- data/lib/sequel/model/base.rb +30 -1
- data/lib/sequel/plugins/eager_each.rb +17 -21
- data/lib/sequel/plugins/identity_map.rb +2 -1
- data/lib/sequel/plugins/many_through_many.rb +1 -1
- data/lib/sequel/plugins/single_table_inheritance.rb +2 -2
- data/lib/sequel/plugins/tactical_eager_loading.rb +1 -1
- data/lib/sequel/sql.rb +4 -2
- data/lib/sequel/version.rb +1 -1
- data/spec/adapters/postgres_spec.rb +32 -2
- data/spec/adapters/sqlite_spec.rb +20 -0
- data/spec/core/database_spec.rb +40 -0
- data/spec/core/dataset_spec.rb +91 -4
- data/spec/core/mock_adapter_spec.rb +2 -1
- data/spec/core/schema_generator_spec.rb +4 -0
- data/spec/core/schema_spec.rb +9 -3
- data/spec/extensions/association_dependencies_spec.rb +3 -3
- data/spec/extensions/columns_introspection_spec.rb +28 -2
- data/spec/extensions/eager_each_spec.rb +0 -1
- data/spec/extensions/identity_map_spec.rb +3 -2
- data/spec/extensions/migration_spec.rb +6 -0
- data/spec/extensions/prepared_statements_associations_spec.rb +2 -2
- data/spec/extensions/rcte_tree_spec.rb +2 -2
- data/spec/extensions/single_table_inheritance_spec.rb +3 -0
- data/spec/extensions/tactical_eager_loading_spec.rb +1 -1
- data/spec/extensions/validation_class_methods_spec.rb +8 -0
- data/spec/integration/associations_test.rb +4 -4
- data/spec/integration/database_test.rb +68 -20
- data/spec/integration/dataset_test.rb +48 -0
- data/spec/integration/schema_test.rb +25 -1
- data/spec/model/associations_spec.rb +21 -8
- data/spec/model/dataset_methods_spec.rb +58 -18
- 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
|
384
|
-
# argument. Otherwise, evaluate the block in the context of a
|
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 =
|
390
|
+
vr = VIRTUAL_ROW
|
391
391
|
case block.arity
|
392
392
|
when -1, 0
|
393
393
|
vr.instance_exec(&block)
|
data/lib/sequel/database/misc.rb
CHANGED
@@ -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
|
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,
|
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
|
205
|
+
# Creates a view, replacing a view with the same name if one already exists.
|
206
206
|
#
|
207
|
-
# DB.create_or_replace_view(:
|
208
|
-
# DB.create_or_replace_view(:
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
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
|
-
|
221
|
-
|
222
|
-
|
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
|
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)
|
data/lib/sequel/dataset/sql.rb
CHANGED
@@ -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
|
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
|
data/lib/sequel/exceptions.rb
CHANGED
@@ -19,32 +19,47 @@ module Sequel
|
|
19
19
|
# connection parameters it was given.
|
20
20
|
class DatabaseConnectionError < DatabaseError; end
|
21
21
|
|
22
|
-
# Error
|
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
|
-
#
|
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
|
-
#
|
47
|
+
# Error raised when attempting an invalid type conversion.
|
33
48
|
class InvalidValue < Error ; end
|
34
49
|
|
35
|
-
#
|
50
|
+
# Error raised when the adapter adapter hasn't implemented a method such as +tables+:
|
36
51
|
class NotImplemented < Error; end
|
37
52
|
|
38
|
-
#
|
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
|
-
#
|
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
|
-
|
31
|
-
|
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.
|
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
|
751
|
-
# :dataset :: A proc that is
|
752
|
-
#
|
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.
|
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.
|
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).
|
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).
|
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
|
-
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
1434
|
-
|
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 =
|
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
|
-
|
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).
|
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.
|
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.
|
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)
|