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