sequel 3.4.0 → 3.5.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 +84 -0
- data/Rakefile +1 -1
- data/doc/cheat_sheet.rdoc +5 -2
- data/doc/opening_databases.rdoc +2 -0
- data/doc/release_notes/3.5.0.txt +510 -0
- data/lib/sequel/adapters/ado.rb +3 -1
- data/lib/sequel/adapters/ado/mssql.rb +2 -2
- data/lib/sequel/adapters/do.rb +2 -11
- data/lib/sequel/adapters/do/mysql.rb +7 -0
- data/lib/sequel/adapters/do/postgres.rb +2 -2
- data/lib/sequel/adapters/firebird.rb +3 -3
- data/lib/sequel/adapters/informix.rb +3 -3
- data/lib/sequel/adapters/jdbc/h2.rb +3 -3
- data/lib/sequel/adapters/jdbc/mssql.rb +7 -0
- data/lib/sequel/adapters/mysql.rb +60 -21
- data/lib/sequel/adapters/odbc.rb +1 -1
- data/lib/sequel/adapters/openbase.rb +3 -3
- data/lib/sequel/adapters/oracle.rb +1 -5
- data/lib/sequel/adapters/postgres.rb +3 -3
- data/lib/sequel/adapters/shared/mssql.rb +142 -33
- data/lib/sequel/adapters/shared/mysql.rb +54 -31
- data/lib/sequel/adapters/shared/oracle.rb +17 -6
- data/lib/sequel/adapters/shared/postgres.rb +7 -7
- data/lib/sequel/adapters/shared/progress.rb +3 -3
- data/lib/sequel/adapters/shared/sqlite.rb +3 -17
- data/lib/sequel/connection_pool.rb +4 -6
- data/lib/sequel/core.rb +29 -113
- data/lib/sequel/database.rb +14 -12
- data/lib/sequel/dataset.rb +8 -21
- data/lib/sequel/dataset/convenience.rb +1 -1
- data/lib/sequel/dataset/graph.rb +9 -2
- data/lib/sequel/dataset/sql.rb +170 -104
- data/lib/sequel/exceptions.rb +3 -0
- data/lib/sequel/extensions/looser_typecasting.rb +21 -0
- data/lib/sequel/extensions/named_timezones.rb +61 -0
- data/lib/sequel/extensions/schema_dumper.rb +7 -1
- data/lib/sequel/extensions/sql_expr.rb +122 -0
- data/lib/sequel/extensions/string_date_time.rb +4 -4
- data/lib/sequel/extensions/thread_local_timezones.rb +48 -0
- data/lib/sequel/model/associations.rb +105 -45
- data/lib/sequel/model/base.rb +37 -28
- data/lib/sequel/plugins/active_model.rb +35 -0
- data/lib/sequel/plugins/association_dependencies.rb +96 -0
- data/lib/sequel/plugins/class_table_inheritance.rb +214 -0
- data/lib/sequel/plugins/force_encoding.rb +61 -0
- data/lib/sequel/plugins/many_through_many.rb +32 -11
- data/lib/sequel/plugins/nested_attributes.rb +7 -2
- data/lib/sequel/plugins/subclasses.rb +45 -0
- data/lib/sequel/plugins/touch.rb +118 -0
- data/lib/sequel/plugins/typecast_on_load.rb +61 -0
- data/lib/sequel/sql.rb +31 -30
- data/lib/sequel/timezones.rb +161 -0
- data/lib/sequel/version.rb +1 -1
- data/spec/adapters/mssql_spec.rb +262 -0
- data/spec/adapters/mysql_spec.rb +46 -8
- data/spec/adapters/postgres_spec.rb +6 -3
- data/spec/adapters/spec_helper.rb +21 -0
- data/spec/adapters/sqlite_spec.rb +1 -1
- data/spec/core/connection_pool_spec.rb +1 -1
- data/spec/core/database_spec.rb +27 -1
- data/spec/core/dataset_spec.rb +63 -1
- data/spec/core/object_graph_spec.rb +1 -1
- data/spec/core/schema_spec.rb +1 -0
- data/spec/extensions/active_model_spec.rb +47 -0
- data/spec/extensions/association_dependencies_spec.rb +108 -0
- data/spec/extensions/class_table_inheritance_spec.rb +252 -0
- data/spec/extensions/force_encoding_spec.rb +75 -0
- data/spec/extensions/looser_typecasting_spec.rb +39 -0
- data/spec/extensions/many_through_many_spec.rb +60 -2
- data/spec/extensions/named_timezones_spec.rb +72 -0
- data/spec/extensions/nested_attributes_spec.rb +29 -1
- data/spec/extensions/schema_dumper_spec.rb +10 -0
- data/spec/extensions/spec_helper.rb +1 -1
- data/spec/extensions/sql_expr_spec.rb +89 -0
- data/spec/extensions/subclasses_spec.rb +52 -0
- data/spec/extensions/thread_local_timezones_spec.rb +45 -0
- data/spec/extensions/touch_spec.rb +155 -0
- data/spec/extensions/typecast_on_load_spec.rb +60 -0
- data/spec/integration/database_test.rb +8 -0
- data/spec/integration/dataset_test.rb +9 -9
- data/spec/integration/plugin_test.rb +139 -0
- data/spec/integration/schema_test.rb +7 -7
- data/spec/integration/spec_helper.rb +32 -1
- data/spec/integration/timezone_test.rb +3 -3
- data/spec/integration/transaction_test.rb +1 -1
- data/spec/integration/type_test.rb +6 -6
- data/spec/model/association_reflection_spec.rb +18 -0
- data/spec/model/associations_spec.rb +169 -9
- data/spec/model/base_spec.rb +2 -0
- data/spec/model/eager_loading_spec.rb +82 -2
- data/spec/model/model_spec.rb +8 -1
- data/spec/model/record_spec.rb +52 -9
- metadata +33 -23
data/lib/sequel/exceptions.rb
CHANGED
|
@@ -2,6 +2,9 @@ module Sequel
|
|
|
2
2
|
# The default exception class for exceptions raised by Sequel.
|
|
3
3
|
# All exception classes defined by Sequel are descendants of this class.
|
|
4
4
|
class Error < ::StandardError
|
|
5
|
+
# If this exception wraps an underlying exception, the underlying
|
|
6
|
+
# exception is held here.
|
|
7
|
+
attr_accessor :wrapped_exception
|
|
5
8
|
end
|
|
6
9
|
|
|
7
10
|
# Raised when the adapter requested doesn't exist or can't be loaded.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# The LooserTypecasting extension changes the float and integer typecasting to
|
|
2
|
+
# use the looser .to_f and .to_i instead of the more strict Kernel.Float and
|
|
3
|
+
# Kernel.Integer. To use it, you should extend the database with the
|
|
4
|
+
# Sequel::LooserTypecasting module after loading the extension:
|
|
5
|
+
#
|
|
6
|
+
# Sequel.extension :looser_typecasting
|
|
7
|
+
# DB.extend(Sequel::LooserTypecasting)
|
|
8
|
+
|
|
9
|
+
module Sequel
|
|
10
|
+
module LooserTypecasting
|
|
11
|
+
# Typecast the value to a Float using to_f instead of Kernel.Float
|
|
12
|
+
def typecast_value_float(value)
|
|
13
|
+
value.to_f
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Typecast the value to an Integer using to_i instead of Kernel.Integer
|
|
17
|
+
def typecast_value_integer(value)
|
|
18
|
+
value.to_i
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Allows the use of named timezones via TZInfo (requires tzinfo).
|
|
2
|
+
# Forces the use of DateTime as Sequel's datetime_class, since
|
|
3
|
+
# ruby's Time class doesn't support timezones other than local
|
|
4
|
+
# and UTC.
|
|
5
|
+
#
|
|
6
|
+
# This allows you to either pass strings or TZInfo::Timezone
|
|
7
|
+
# instance to Sequel.database_timezone=, application_timezone=, and
|
|
8
|
+
# typecast_timezone=. If a string is passed, it is converted to a
|
|
9
|
+
# TZInfo::Timezone using TZInfo::Timezone.get.
|
|
10
|
+
#
|
|
11
|
+
# Let's say you have the database server in New York and the
|
|
12
|
+
# application server in Los Angeles. For historical reasons, data
|
|
13
|
+
# is stored in local New York time, but the application server only
|
|
14
|
+
# services clients in Los Angeles, so you want to use New York
|
|
15
|
+
# time in the database and Los Angeles time in the application. This
|
|
16
|
+
# is easily done via:
|
|
17
|
+
#
|
|
18
|
+
# Sequel.database_timezone = 'America/New_York'
|
|
19
|
+
# Sequel.application_timezone = 'America/Los_Angeles'
|
|
20
|
+
#
|
|
21
|
+
# Then, before data is stored in the database, it is converted to New
|
|
22
|
+
# York time. When data is retrieved from the database, it is
|
|
23
|
+
# converted to Los Angeles time.
|
|
24
|
+
|
|
25
|
+
require 'tzinfo'
|
|
26
|
+
|
|
27
|
+
module Sequel
|
|
28
|
+
self.datetime_class = DateTime
|
|
29
|
+
|
|
30
|
+
module NamedTimezones
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
# Assume the given DateTime has a correct time but a wrong timezone. It is
|
|
34
|
+
# currently in UTC timezone, but it should be converted to the input_timzone.
|
|
35
|
+
# Keep the time the same but convert the timezone to the input_timezone.
|
|
36
|
+
# Expects the input_timezone to be a TZInfo::Timezone instance.
|
|
37
|
+
def convert_input_datetime_other(v, input_timezone)
|
|
38
|
+
local_offset = input_timezone.period_for_local(v).utc_total_offset_rational
|
|
39
|
+
(v - local_offset).new_offset(local_offset)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Convert the given DateTime to use the given output_timezone.
|
|
43
|
+
# Expects the output_timezone to be a TZInfo::Timezone instance.
|
|
44
|
+
def convert_output_datetime_other(v, output_timezone)
|
|
45
|
+
# TZInfo converts times, but expects the given DateTime to have an offset
|
|
46
|
+
# of 0 and always leaves the timezone offset as 0
|
|
47
|
+
v = output_timezone.utc_to_local(v.new_offset(0))
|
|
48
|
+
local_offset = output_timezone.period_for_local(v).utc_total_offset_rational
|
|
49
|
+
# Convert timezone offset from UTC to the offset for the output_timezone
|
|
50
|
+
(v - local_offset).new_offset(local_offset)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Convert the timezone setter argument. Returns argument given by default,
|
|
54
|
+
# exists for easier overriding in extensions.
|
|
55
|
+
def convert_timezone_setter_arg(tz)
|
|
56
|
+
tz.is_a?(String) ? TZInfo::Timezone.get(tz) : super
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
extend NamedTimezones
|
|
61
|
+
end
|
|
@@ -73,7 +73,7 @@ END_MIG
|
|
|
73
73
|
# method modified so that .lit is always appended after it, only if the
|
|
74
74
|
# :same_db option is used.
|
|
75
75
|
def column_schema_to_ruby_default_fallback(default, options)
|
|
76
|
-
if options[:same_db] &&
|
|
76
|
+
if default.is_a?(String) && options[:same_db] && use_column_schema_to_ruby_default_fallback?
|
|
77
77
|
default = default.to_s
|
|
78
78
|
def default.inspect
|
|
79
79
|
"#{super}.lit"
|
|
@@ -165,6 +165,12 @@ END_MIG
|
|
|
165
165
|
h[:unique] = true if index_opts[:unique]
|
|
166
166
|
h
|
|
167
167
|
end
|
|
168
|
+
|
|
169
|
+
# Don't use the "...".lit fallback on MySQL, since the defaults it uses aren't
|
|
170
|
+
# valid literal SQL values.
|
|
171
|
+
def use_column_schema_to_ruby_default_fallback?
|
|
172
|
+
database_type != :mysql
|
|
173
|
+
end
|
|
168
174
|
end
|
|
169
175
|
|
|
170
176
|
module Schema
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# The sql_expr extension adds the sql_expr method to every object, which
|
|
2
|
+
# returns an object that works nicely with Sequel's DSL. This is
|
|
3
|
+
# best shown by example:
|
|
4
|
+
#
|
|
5
|
+
# 1.sql_expr < :a # 1 < a
|
|
6
|
+
# false.sql_expr & :a # FALSE AND a
|
|
7
|
+
# true.sql_expr | :a # TRUE OR a
|
|
8
|
+
# ~nil.sql_expr # NOT NULL
|
|
9
|
+
# "a".sql_expr + "b" # 'a' || 'b'
|
|
10
|
+
|
|
11
|
+
module Sequel
|
|
12
|
+
module SQL
|
|
13
|
+
# The GenericComplexExpression acts like a
|
|
14
|
+
# GenericExpression in terms of methods,
|
|
15
|
+
# but has an internal structure of a
|
|
16
|
+
# ComplexExpression. It is used by Object#sql_expr.
|
|
17
|
+
# Since we don't know what specific type of object
|
|
18
|
+
# we are dealing with it, we treat it similarly to
|
|
19
|
+
# how we treat symbols or literal strings, allowing
|
|
20
|
+
# many different types of methods.
|
|
21
|
+
class GenericComplexExpression < ComplexExpression
|
|
22
|
+
include AliasMethods
|
|
23
|
+
include BooleanMethods
|
|
24
|
+
include CastMethods
|
|
25
|
+
include ComplexExpressionMethods
|
|
26
|
+
include InequalityMethods
|
|
27
|
+
include NumericMethods
|
|
28
|
+
include OrderMethods
|
|
29
|
+
include StringMethods
|
|
30
|
+
include SubscriptMethods
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
class Object
|
|
36
|
+
# Return a copy of the object wrapped in a
|
|
37
|
+
# Sequel::SQL::GenericComplexExpression. Allows easy use
|
|
38
|
+
# of the Object with Sequel's DSL. You'll probably have
|
|
39
|
+
# to make sure that Sequel knows how to literalize the
|
|
40
|
+
# object properly, though.
|
|
41
|
+
def sql_expr
|
|
42
|
+
Sequel::SQL::GenericComplexExpression.new(:NOOP, self)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
class FalseClass
|
|
47
|
+
# Returns a copy of the object wrapped in a
|
|
48
|
+
# Sequel::SQL::BooleanExpression, allowing easy use
|
|
49
|
+
# of Sequel's DSL:
|
|
50
|
+
#
|
|
51
|
+
# false.sql_expr & :a # FALSE AND a
|
|
52
|
+
def sql_expr
|
|
53
|
+
Sequel::SQL::BooleanExpression.new(:NOOP, self)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
class NilClass
|
|
58
|
+
# Returns a copy of the object wrapped in a
|
|
59
|
+
# Sequel::SQL::BooleanExpression, allowing easy use
|
|
60
|
+
# of Sequel's DSL:
|
|
61
|
+
#
|
|
62
|
+
# ~nil.sql_expr # NOT NULL
|
|
63
|
+
def sql_expr
|
|
64
|
+
Sequel::SQL::BooleanExpression.new(:NOOP, self)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
class Numeric
|
|
69
|
+
# Returns a copy of the object wrapped in a
|
|
70
|
+
# Sequel::SQL::NumericExpression, allowing easy use
|
|
71
|
+
# of Sequel's DSL:
|
|
72
|
+
#
|
|
73
|
+
# 1.sql_expr < :a # 1 < a
|
|
74
|
+
def sql_expr
|
|
75
|
+
Sequel::SQL::NumericExpression.new(:NOOP, self)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
class Proc
|
|
80
|
+
# Evaluates the proc as a virtual row block.
|
|
81
|
+
# If a hash or array of two element arrays is returned,
|
|
82
|
+
# they are converted to a Sequel::SQL::BooleanExpression. Otherwise,
|
|
83
|
+
# unless the object returned is already an Sequel::SQL::Expression,
|
|
84
|
+
# convert the object to an Sequel::SQL::GenericComplexExpression.
|
|
85
|
+
#
|
|
86
|
+
# proc{a(b)}.sql_expr + 1 # a(b) + 1
|
|
87
|
+
# proc{{a=>b}}.sql_expr | true # (a = b) OR TRUE
|
|
88
|
+
# proc{1}.sql_expr + :a # 1 + a
|
|
89
|
+
def sql_expr
|
|
90
|
+
o = Sequel.virtual_row(&self)
|
|
91
|
+
if Sequel.condition_specifier?(o)
|
|
92
|
+
Sequel::SQL::BooleanExpression.from_value_pairs(o, :AND)
|
|
93
|
+
elsif o.is_a?(Sequel::SQL::Expression)
|
|
94
|
+
o
|
|
95
|
+
else
|
|
96
|
+
Sequel::SQL::GenericComplexExpression.new(:NOOP, o)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
class String
|
|
102
|
+
# Returns a copy of the object wrapped in a
|
|
103
|
+
# Sequel::SQL::StringExpression, allowing easy use
|
|
104
|
+
# of Sequel's DSL:
|
|
105
|
+
#
|
|
106
|
+
# "a".sql_expr + :a # 'a' || a
|
|
107
|
+
def sql_expr
|
|
108
|
+
Sequel::SQL::StringExpression.new(:NOOP, self)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
class TrueClass
|
|
113
|
+
# Returns a copy of the object wrapped in a
|
|
114
|
+
# Sequel::SQL::BooleanExpression, allowing easy use
|
|
115
|
+
# of Sequel's DSL:
|
|
116
|
+
#
|
|
117
|
+
# true.sql_expr | :a # TRUE OR a
|
|
118
|
+
def sql_expr
|
|
119
|
+
Sequel::SQL::BooleanExpression.new(:NOOP, self)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
@@ -8,7 +8,7 @@ class String
|
|
|
8
8
|
begin
|
|
9
9
|
Date.parse(self, Sequel.convert_two_digit_years)
|
|
10
10
|
rescue => e
|
|
11
|
-
raise Sequel::InvalidValue
|
|
11
|
+
raise Sequel.convert_exception_class(e, Sequel::InvalidValue)
|
|
12
12
|
end
|
|
13
13
|
end
|
|
14
14
|
|
|
@@ -17,7 +17,7 @@ class String
|
|
|
17
17
|
begin
|
|
18
18
|
DateTime.parse(self, Sequel.convert_two_digit_years)
|
|
19
19
|
rescue => e
|
|
20
|
-
raise Sequel::InvalidValue
|
|
20
|
+
raise Sequel.convert_exception_class(e, Sequel::InvalidValue)
|
|
21
21
|
end
|
|
22
22
|
end
|
|
23
23
|
|
|
@@ -31,7 +31,7 @@ class String
|
|
|
31
31
|
Sequel.datetime_class.parse(self)
|
|
32
32
|
end
|
|
33
33
|
rescue => e
|
|
34
|
-
raise Sequel
|
|
34
|
+
raise Sequel.convert_exception_class(e, Sequel::InvalidValue)
|
|
35
35
|
end
|
|
36
36
|
end
|
|
37
37
|
|
|
@@ -40,7 +40,7 @@ class String
|
|
|
40
40
|
begin
|
|
41
41
|
Time.parse(self)
|
|
42
42
|
rescue => e
|
|
43
|
-
raise Sequel::InvalidValue
|
|
43
|
+
raise Sequel.convert_exception_class(e, Sequel::InvalidValue)
|
|
44
44
|
end
|
|
45
45
|
end
|
|
46
46
|
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# The thread_local_timezones extension allows you to set a per-thread timezone that
|
|
2
|
+
# will override the default global timezone while the thread is executing. The
|
|
3
|
+
# main use case is for web applications that execute each request in its own thread,
|
|
4
|
+
# and want to set the timezones based on the request. The most common example
|
|
5
|
+
# is having the database always store time in UTC, but have the application deal
|
|
6
|
+
# with the timezone of the current user. That can be done with:
|
|
7
|
+
#
|
|
8
|
+
# Sequel.database_timezone = :utc
|
|
9
|
+
# # In each thread:
|
|
10
|
+
# Sequel.thread_application_timezone = current_user.timezone
|
|
11
|
+
#
|
|
12
|
+
# This extension is designed to work with the named_timezones extension.
|
|
13
|
+
#
|
|
14
|
+
# This extension adds the thread_application_timezone=, thread_database_timezone=,
|
|
15
|
+
# and thread_typecast_timezone= methods to the Sequel module. It overrides
|
|
16
|
+
# the application_timezone, database_timezone, and typecast_timezone
|
|
17
|
+
# methods to check the related thread local timezone first, and use it if present.
|
|
18
|
+
# If the related thread local timezone is not present, it falls back to the
|
|
19
|
+
# default global timezone.
|
|
20
|
+
#
|
|
21
|
+
# There is one special case of note. If you have a default global timezone
|
|
22
|
+
# and you want to have a nil thread local timezone, you have to set the thread
|
|
23
|
+
# local value to :nil instead of nil:
|
|
24
|
+
#
|
|
25
|
+
# Sequel.application_timezone = :utc
|
|
26
|
+
# Sequel.thread_application_timezone = nil
|
|
27
|
+
# Sequel.application_timezone # => :utc
|
|
28
|
+
# Sequel.thread_application_timezone = :nil
|
|
29
|
+
# Sequel.application_timezone # => nil
|
|
30
|
+
|
|
31
|
+
module Sequel
|
|
32
|
+
module ThreadLocalTimezones
|
|
33
|
+
%w'application database typecast'.each do |t|
|
|
34
|
+
class_eval("def thread_#{t}_timezone=(tz); Thread.current[:#{t}_timezone] = convert_timezone_setter_arg(tz); end", __FILE__, __LINE__)
|
|
35
|
+
class_eval(<<END, __FILE__, __LINE__)
|
|
36
|
+
def #{t}_timezone
|
|
37
|
+
if tz = Thread.current[:#{t}_timezone]
|
|
38
|
+
tz unless tz == :nil
|
|
39
|
+
else
|
|
40
|
+
super
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
END
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
extend ThreadLocalTimezones
|
|
48
|
+
end
|
|
@@ -101,9 +101,9 @@ module Sequel
|
|
|
101
101
|
def reciprocal
|
|
102
102
|
return self[:reciprocal] if include?(:reciprocal)
|
|
103
103
|
r_type = reciprocal_type
|
|
104
|
-
|
|
104
|
+
keys = self[:keys]
|
|
105
105
|
associated_class.all_association_reflections.each do |assoc_reflect|
|
|
106
|
-
if assoc_reflect[:type] == r_type && assoc_reflect[:
|
|
106
|
+
if assoc_reflect[:type] == r_type && assoc_reflect[:keys] == keys && assoc_reflect.associated_class == self[:model]
|
|
107
107
|
return self[:reciprocal] = assoc_reflect[:name]
|
|
108
108
|
end
|
|
109
109
|
end
|
|
@@ -174,10 +174,15 @@ module Sequel
|
|
|
174
174
|
self[:key]
|
|
175
175
|
end
|
|
176
176
|
|
|
177
|
-
# The column in the associated table that the key in the current table references.
|
|
177
|
+
# The column(s) in the associated table that the key in the current table references (either a symbol or an array).
|
|
178
178
|
def primary_key
|
|
179
179
|
self[:primary_key] ||= associated_class.primary_key
|
|
180
180
|
end
|
|
181
|
+
|
|
182
|
+
# The columns in the associated table that the key in the current table references (always an array).
|
|
183
|
+
def primary_keys
|
|
184
|
+
self[:primary_keys] ||= Array(primary_key)
|
|
185
|
+
end
|
|
181
186
|
|
|
182
187
|
# Whether this association returns an array of objects instead of a single object,
|
|
183
188
|
# false for a many_to_one association.
|
|
@@ -245,9 +250,10 @@ module Sequel
|
|
|
245
250
|
self[:join_table]
|
|
246
251
|
end
|
|
247
252
|
|
|
248
|
-
# The default associated key alias
|
|
253
|
+
# The default associated key alias(es) to use when eager loading
|
|
254
|
+
# associations via eager.
|
|
249
255
|
def default_associated_key_alias
|
|
250
|
-
:x_foreign_key_x
|
|
256
|
+
self[:uses_left_composite_keys] ? (0...self[:left_keys].length).map{|i| :"x_foreign_key_#{i}_x"} : :x_foreign_key_x
|
|
251
257
|
end
|
|
252
258
|
|
|
253
259
|
# Default name symbol for the join table.
|
|
@@ -286,12 +292,12 @@ module Sequel
|
|
|
286
292
|
# Returns the reciprocal association symbol, if one exists.
|
|
287
293
|
def reciprocal
|
|
288
294
|
return self[:reciprocal] if include?(:reciprocal)
|
|
289
|
-
|
|
290
|
-
|
|
295
|
+
left_keys = self[:left_keys]
|
|
296
|
+
right_keys = self[:right_keys]
|
|
291
297
|
join_table = self[:join_table]
|
|
292
298
|
associated_class.all_association_reflections.each do |assoc_reflect|
|
|
293
|
-
if assoc_reflect[:type] == :many_to_many && assoc_reflect[:
|
|
294
|
-
assoc_reflect[:
|
|
299
|
+
if assoc_reflect[:type] == :many_to_many && assoc_reflect[:left_keys] == right_keys &&
|
|
300
|
+
assoc_reflect[:right_keys] == left_keys && assoc_reflect[:join_table] == join_table &&
|
|
295
301
|
assoc_reflect.associated_class == self[:model]
|
|
296
302
|
return self[:reciprocal] = assoc_reflect[:name]
|
|
297
303
|
end
|
|
@@ -299,10 +305,15 @@ module Sequel
|
|
|
299
305
|
self[:reciprocal] = nil
|
|
300
306
|
end
|
|
301
307
|
|
|
302
|
-
# The primary key column to use in the associated table.
|
|
308
|
+
# The primary key column(s) to use in the associated table (can be symbol or array).
|
|
303
309
|
def right_primary_key
|
|
304
310
|
self[:right_primary_key] ||= associated_class.primary_key
|
|
305
311
|
end
|
|
312
|
+
|
|
313
|
+
# The primary key columns to use in the associated table (always array).
|
|
314
|
+
def right_primary_keys
|
|
315
|
+
self[:right_primary_keys] ||= Array(right_primary_key)
|
|
316
|
+
end
|
|
306
317
|
|
|
307
318
|
# The columns to select when loading the association, associated_class.table_name.* by default.
|
|
308
319
|
def select
|
|
@@ -473,15 +484,19 @@ module Sequel
|
|
|
473
484
|
# use this option, but beware that the join table attributes can clash with
|
|
474
485
|
# attributes from the model table, so you should alias any attributes that have
|
|
475
486
|
# the same name in both the join table and the associated table.
|
|
487
|
+
# - :validate - Set to false to not validate when implicitly saving any associated object.
|
|
476
488
|
# * :many_to_one:
|
|
477
489
|
# - :key - foreign_key in current model's table that references
|
|
478
|
-
# associated model's primary key, as a symbol. Defaults to :"#{name}_id".
|
|
490
|
+
# associated model's primary key, as a symbol. Defaults to :"#{name}_id". Can use an
|
|
491
|
+
# array of symbols for a composite key association.
|
|
479
492
|
# - :primary_key - column in the associated table that :key option references, as a symbol.
|
|
480
|
-
# Defaults to the primary key of the associated table.
|
|
493
|
+
# Defaults to the primary key of the associated table. Can use an
|
|
494
|
+
# array of symbols for a composite key association.
|
|
481
495
|
# * :one_to_many:
|
|
482
496
|
# - :key - foreign key in associated model's table that references
|
|
483
497
|
# current model's primary key, as a symbol. Defaults to
|
|
484
|
-
# :"#{self.name.underscore}_id".
|
|
498
|
+
# :"#{self.name.underscore}_id". Can use an
|
|
499
|
+
# array of symbols for a composite key association.
|
|
485
500
|
# - :one_to_one: Create a getter and setter similar to those of many_to_one
|
|
486
501
|
# associations. The getter returns a singular matching record, or raises an
|
|
487
502
|
# error if multiple records match. The setter updates the record given and removes
|
|
@@ -492,7 +507,8 @@ module Sequel
|
|
|
492
507
|
# table instead of the current table. Note that using this option still requires
|
|
493
508
|
# you to use a plural name when creating and using the association (e.g. for reflections, eager loading, etc.).
|
|
494
509
|
# - :primary_key - column in the current table that :key option references, as a symbol.
|
|
495
|
-
# Defaults to primary key of the current table.
|
|
510
|
+
# Defaults to primary key of the current table. Can use an
|
|
511
|
+
# array of symbols for a composite key association.
|
|
496
512
|
# * :many_to_many:
|
|
497
513
|
# - :graph_join_table_block - The block to pass to join_table for
|
|
498
514
|
# the join table when eagerly loading the association via eager_graph.
|
|
@@ -511,13 +527,17 @@ module Sequel
|
|
|
511
527
|
# of current model and name of associated model, pluralized,
|
|
512
528
|
# underscored, sorted, and joined with '_'.
|
|
513
529
|
# - :left_key - foreign key in join table that points to current model's
|
|
514
|
-
# primary key, as a symbol. Defaults to :"#{self.name.underscore}_id".
|
|
530
|
+
# primary key, as a symbol. Defaults to :"#{self.name.underscore}_id".
|
|
531
|
+
# Can use an array of symbols for a composite key association.
|
|
515
532
|
# - :left_primary_key - column in current table that :left_key points to, as a symbol.
|
|
516
|
-
# Defaults to primary key of current table.
|
|
533
|
+
# Defaults to primary key of current table. Can use an
|
|
534
|
+
# array of symbols for a composite key association.
|
|
517
535
|
# - :right_key - foreign key in join table that points to associated
|
|
518
536
|
# model's primary key, as a symbol. Defaults to Defaults to :"#{name.to_s.singularize}_id".
|
|
537
|
+
# Can use an array of symbols for a composite key association.
|
|
519
538
|
# - :right_primary_key - column in associated table that :right_key points to, as a symbol.
|
|
520
|
-
# Defaults to primary key of the associated table.
|
|
539
|
+
# Defaults to primary key of the associated table. Can use an
|
|
540
|
+
# array of symbols for a composite key association.
|
|
521
541
|
# - :uniq - Adds a after_load callback that makes the array of objects unique.
|
|
522
542
|
def associate(type, name, opts = {}, &block)
|
|
523
543
|
raise(Error, 'invalid association type') unless assoc_class = ASSOCIATION_TYPES[type]
|
|
@@ -580,8 +600,13 @@ module Sequel
|
|
|
580
600
|
if opts[:eager_graph]
|
|
581
601
|
ds = ds.eager_graph(opts[:eager_graph])
|
|
582
602
|
ds = ds.add_graph_aliases(opts.associated_key_alias=>[opts.associated_class.table_name, opts.associated_key_alias, SQL::QualifiedIdentifier.new(opts.associated_key_table, opts.associated_key_column)]) if opts.eager_loading_use_associated_key?
|
|
583
|
-
|
|
584
|
-
ds
|
|
603
|
+
elsif opts.eager_loading_use_associated_key?
|
|
604
|
+
ds = if opts[:uses_left_composite_keys]
|
|
605
|
+
t = opts.associated_key_table
|
|
606
|
+
ds.select_more(*opts.associated_key_alias.zip(opts.associated_key_column).map{|a, c| SQL::AliasedExpression.new(SQL::QualifiedIdentifier.new(t, c), a)})
|
|
607
|
+
else
|
|
608
|
+
ds.select_more(SQL::AliasedExpression.new(SQL::QualifiedIdentifier.new(opts.associated_key_table, opts.associated_key_column), opts.associated_key_alias))
|
|
609
|
+
end
|
|
585
610
|
end
|
|
586
611
|
ds = ds.eager(associations) unless Array(associations).empty?
|
|
587
612
|
ds = opts[:eager_block].call(ds) if opts[:eager_block]
|
|
@@ -649,22 +674,36 @@ module Sequel
|
|
|
649
674
|
name = opts[:name]
|
|
650
675
|
model = self
|
|
651
676
|
left = (opts[:left_key] ||= opts.default_left_key)
|
|
677
|
+
lcks = opts[:left_keys] = Array(left)
|
|
652
678
|
right = (opts[:right_key] ||= opts.default_right_key)
|
|
679
|
+
rcks = opts[:right_keys] = Array(right)
|
|
653
680
|
left_pk = (opts[:left_primary_key] ||= self.primary_key)
|
|
681
|
+
lcpks = opts[:left_primary_keys] = Array(left_pk)
|
|
682
|
+
raise(Error, 'mismatched number of left composite keys') unless lcks.length == lcpks.length
|
|
683
|
+
raise(Error, 'mismatched number of right composite keys') if opts[:right_primary_key] && rcks.length != Array(opts[:right_primary_key]).length
|
|
684
|
+
uses_lcks = opts[:uses_left_composite_keys] = lcks.length > 1
|
|
685
|
+
uses_rcks = opts[:uses_right_composite_keys] = rcks.length > 1
|
|
654
686
|
opts[:cartesian_product_number] ||= 1
|
|
655
687
|
join_table = (opts[:join_table] ||= opts.default_join_table)
|
|
656
688
|
left_key_alias = opts[:left_key_alias] ||= opts.default_associated_key_alias
|
|
657
689
|
graph_jt_conds = opts[:graph_join_table_conditions] = opts[:graph_join_table_conditions] ? opts[:graph_join_table_conditions].to_a : []
|
|
658
690
|
opts[:graph_join_table_join_type] ||= opts[:graph_join_type]
|
|
659
691
|
opts[:after_load].unshift(:array_uniq!) if opts[:uniq]
|
|
660
|
-
opts[:dataset] ||= proc{opts.associated_class.inner_join(join_table,
|
|
692
|
+
opts[:dataset] ||= proc{opts.associated_class.inner_join(join_table, rcks.zip(opts.right_primary_keys) + lcks.zip(lcpks.map{|k| send(k)}))}
|
|
661
693
|
database = db
|
|
662
694
|
|
|
663
695
|
opts[:eager_loader] ||= proc do |key_hash, records, associations|
|
|
664
696
|
h = key_hash[left_pk]
|
|
665
697
|
records.each{|object| object.associations[name] = []}
|
|
666
|
-
|
|
667
|
-
|
|
698
|
+
r = uses_rcks ? rcks.zip(opts.right_primary_keys) : [[right, opts.right_primary_key]]
|
|
699
|
+
l = uses_lcks ? [[lcks.map{|k| SQL::QualifiedIdentifier.new(join_table, k)}, SQL::SQLArray.new(h.keys)]] : [[left, h.keys]]
|
|
700
|
+
model.eager_loading_dataset(opts, opts.associated_class.inner_join(join_table, r + l), Array(opts.select), associations).all do |assoc_record|
|
|
701
|
+
hash_key = if uses_lcks
|
|
702
|
+
left_key_alias.map{|k| assoc_record.values.delete(k)}
|
|
703
|
+
else
|
|
704
|
+
assoc_record.values.delete(left_key_alias)
|
|
705
|
+
end
|
|
706
|
+
next unless objects = h[hash_key]
|
|
668
707
|
objects.each{|object| object.associations[name].push(assoc_record)}
|
|
669
708
|
end
|
|
670
709
|
end
|
|
@@ -680,8 +719,8 @@ module Sequel
|
|
|
680
719
|
jt_join_type = opts[:graph_join_table_join_type]
|
|
681
720
|
jt_graph_block = opts[:graph_join_table_block]
|
|
682
721
|
opts[:eager_grapher] ||= proc do |ds, assoc_alias, table_alias|
|
|
683
|
-
ds = ds.graph(join_table, use_jt_only_conditions ? jt_only_conditions :
|
|
684
|
-
ds.graph(opts.associated_class, use_only_conditions ? only_conditions :
|
|
722
|
+
ds = ds.graph(join_table, use_jt_only_conditions ? jt_only_conditions : lcks.zip(lcpks) + graph_jt_conds, :select=>false, :table_alias=>ds.send(:eager_unique_table_alias, ds, join_table), :join_type=>jt_join_type, :implicit_qualifier=>table_alias, :from_self_alias=>ds.opts[:eager_graph][:master], &jt_graph_block)
|
|
723
|
+
ds.graph(opts.associated_class, use_only_conditions ? only_conditions : opts.right_primary_keys.zip(rcks) + conditions, :select=>select, :table_alias=>assoc_alias, :join_type=>join_type, &graph_block)
|
|
685
724
|
end
|
|
686
725
|
|
|
687
726
|
def_association_dataset_methods(opts)
|
|
@@ -689,13 +728,16 @@ module Sequel
|
|
|
689
728
|
return if opts[:read_only]
|
|
690
729
|
|
|
691
730
|
association_module_private_def(opts._add_method) do |o|
|
|
692
|
-
|
|
731
|
+
h = {}
|
|
732
|
+
lcks.zip(lcpks).each{|k, pk| h[k] = send(pk)}
|
|
733
|
+
rcks.zip(opts.right_primary_keys).each{|k, pk| h[k] = o.send(pk)}
|
|
734
|
+
database.dataset.from(join_table).insert(h)
|
|
693
735
|
end
|
|
694
736
|
association_module_private_def(opts._remove_method) do |o|
|
|
695
|
-
database.dataset.from(join_table).filter(
|
|
737
|
+
database.dataset.from(join_table).filter(lcks.zip(lcpks.map{|k| send(k)}) + rcks.zip(opts.right_primary_keys.map{|k| o.send(k)})).delete
|
|
696
738
|
end
|
|
697
739
|
association_module_private_def(opts._remove_all_method) do
|
|
698
|
-
database.dataset.from(join_table).filter(
|
|
740
|
+
database.dataset.from(join_table).filter(lcks.zip(lcpks.map{|k| send(k)})).delete
|
|
699
741
|
end
|
|
700
742
|
|
|
701
743
|
def_add_method(opts)
|
|
@@ -708,10 +750,13 @@ module Sequel
|
|
|
708
750
|
model = self
|
|
709
751
|
opts[:key] = opts.default_key unless opts.include?(:key)
|
|
710
752
|
key = opts[:key]
|
|
753
|
+
cks = opts[:keys] = Array(opts[:key])
|
|
754
|
+
raise(Error, 'mismatched number of composite keys') if opts[:primary_key] && cks.length != Array(opts[:primary_key]).length
|
|
755
|
+
uses_cks = opts[:uses_composite_keys] = cks.length > 1
|
|
711
756
|
opts[:cartesian_product_number] ||= 0
|
|
712
757
|
opts[:dataset] ||= proc do
|
|
713
758
|
klass = opts.associated_class
|
|
714
|
-
klass.filter(SQL::QualifiedIdentifier.new(klass.table_name,
|
|
759
|
+
klass.filter(opts.primary_keys.map{|k| SQL::QualifiedIdentifier.new(klass.table_name, k)}.zip(cks.map{|k| send(k)}))
|
|
715
760
|
end
|
|
716
761
|
opts[:eager_loader] ||= proc do |key_hash, records, associations|
|
|
717
762
|
h = key_hash[key]
|
|
@@ -722,8 +767,9 @@ module Sequel
|
|
|
722
767
|
# Skip eager loading if no objects have a foreign key for this association
|
|
723
768
|
unless keys.empty?
|
|
724
769
|
klass = opts.associated_class
|
|
725
|
-
model.eager_loading_dataset(opts, klass.filter(SQL::QualifiedIdentifier.new(klass.table_name, opts.primary_key)=>keys), opts.select, associations).all do |assoc_record|
|
|
726
|
-
|
|
770
|
+
model.eager_loading_dataset(opts, klass.filter(uses_cks ? {opts.primary_keys.map{|k| SQL::QualifiedIdentifier.new(klass.table_name, k)}=>SQL::SQLArray.new(keys)} : {SQL::QualifiedIdentifier.new(klass.table_name, opts.primary_key)=>keys}), opts.select, associations).all do |assoc_record|
|
|
771
|
+
hash_key = uses_cks ? opts.primary_keys.map{|k| assoc_record.send(k)} : assoc_record.send(opts.primary_key)
|
|
772
|
+
next unless objects = h[hash_key]
|
|
727
773
|
objects.each{|object| object.associations[name] = assoc_record}
|
|
728
774
|
end
|
|
729
775
|
end
|
|
@@ -736,14 +782,14 @@ module Sequel
|
|
|
736
782
|
conditions = opts[:graph_conditions]
|
|
737
783
|
graph_block = opts[:graph_block]
|
|
738
784
|
opts[:eager_grapher] ||= proc do |ds, assoc_alias, table_alias|
|
|
739
|
-
ds.graph(opts.associated_class, use_only_conditions ? only_conditions :
|
|
785
|
+
ds.graph(opts.associated_class, use_only_conditions ? only_conditions : opts.primary_keys.zip(cks) + conditions, :select=>select, :table_alias=>assoc_alias, :join_type=>join_type, :implicit_qualifier=>table_alias, :from_self_alias=>ds.opts[:eager_graph][:master], &graph_block)
|
|
740
786
|
end
|
|
741
787
|
|
|
742
788
|
def_association_dataset_methods(opts)
|
|
743
789
|
|
|
744
790
|
return if opts[:read_only]
|
|
745
791
|
|
|
746
|
-
association_module_private_def(opts._setter_method){|o| send(:"#{
|
|
792
|
+
association_module_private_def(opts._setter_method){|o| cks.zip(opts.primary_keys).each{|k, pk| send(:"#{k}=", (o.send(pk) if o))}}
|
|
747
793
|
association_module_def(opts.setter_method){|o| set_associated_object(opts, o)}
|
|
748
794
|
end
|
|
749
795
|
|
|
@@ -752,18 +798,23 @@ module Sequel
|
|
|
752
798
|
name = opts[:name]
|
|
753
799
|
model = self
|
|
754
800
|
key = (opts[:key] ||= opts.default_key)
|
|
801
|
+
cks = opts[:keys] = Array(key)
|
|
755
802
|
primary_key = (opts[:primary_key] ||= self.primary_key)
|
|
803
|
+
cpks = opts[:primary_keys] = Array(primary_key)
|
|
804
|
+
raise(Error, 'mismatched number of composite keys') unless cks.length == cpks.length
|
|
805
|
+
uses_cks = opts[:uses_composite_keys] = cks.length > 1
|
|
756
806
|
opts[:dataset] ||= proc do
|
|
757
807
|
klass = opts.associated_class
|
|
758
|
-
klass.filter(SQL::QualifiedIdentifier.new(klass.table_name,
|
|
808
|
+
klass.filter(cks.map{|k| SQL::QualifiedIdentifier.new(klass.table_name, k)}.zip(cpks.map{|k| send(k)}))
|
|
759
809
|
end
|
|
760
810
|
opts[:eager_loader] ||= proc do |key_hash, records, associations|
|
|
761
811
|
h = key_hash[primary_key]
|
|
762
812
|
records.each{|object| object.associations[name] = []}
|
|
763
813
|
reciprocal = opts.reciprocal
|
|
764
814
|
klass = opts.associated_class
|
|
765
|
-
model.eager_loading_dataset(opts, klass.filter(SQL::QualifiedIdentifier.new(klass.table_name, key)=>h.keys), opts.select, associations).all do |assoc_record|
|
|
766
|
-
|
|
815
|
+
model.eager_loading_dataset(opts, klass.filter(uses_cks ? {cks.map{|k| SQL::QualifiedIdentifier.new(klass.table_name, k)}=>SQL::SQLArray.new(h.keys)} : {SQL::QualifiedIdentifier.new(klass.table_name, key)=>h.keys}), opts.select, associations).all do |assoc_record|
|
|
816
|
+
hash_key = uses_cks ? cks.map{|k| assoc_record.send(k)} : assoc_record.send(key)
|
|
817
|
+
next unless objects = h[hash_key]
|
|
767
818
|
objects.each do |object|
|
|
768
819
|
object.associations[name].push(assoc_record)
|
|
769
820
|
assoc_record.associations[reciprocal] = object if reciprocal
|
|
@@ -779,7 +830,7 @@ module Sequel
|
|
|
779
830
|
opts[:cartesian_product_number] ||= 1
|
|
780
831
|
graph_block = opts[:graph_block]
|
|
781
832
|
opts[:eager_grapher] ||= proc do |ds, assoc_alias, table_alias|
|
|
782
|
-
ds = ds.graph(opts.associated_class, use_only_conditions ? only_conditions :
|
|
833
|
+
ds = ds.graph(opts.associated_class, use_only_conditions ? only_conditions : cks.zip(cpks) + conditions, :select=>select, :table_alias=>assoc_alias, :join_type=>join_type, :implicit_qualifier=>table_alias, :from_self_alias=>ds.opts[:eager_graph][:master], &graph_block)
|
|
783
834
|
# We only load reciprocals for one_to_many associations, as other reciprocals don't make sense
|
|
784
835
|
ds.opts[:eager_graph][:reciprocals][assoc_alias] = opts.reciprocal
|
|
785
836
|
ds
|
|
@@ -787,20 +838,24 @@ module Sequel
|
|
|
787
838
|
|
|
788
839
|
def_association_dataset_methods(opts)
|
|
789
840
|
|
|
841
|
+
ck_nil_hash ={}
|
|
842
|
+
cks.each{|k| ck_nil_hash[k] = nil}
|
|
843
|
+
|
|
790
844
|
unless opts[:read_only]
|
|
845
|
+
validate = opts[:validate]
|
|
791
846
|
association_module_private_def(opts._add_method) do |o|
|
|
792
|
-
o.send(:"#{
|
|
793
|
-
o.save || raise(Sequel::Error, "invalid associated object, cannot save")
|
|
847
|
+
cks.zip(cpks).each{|k, pk| o.send(:"#{k}=", send(pk))}
|
|
848
|
+
o.save(:validate=>validate) || raise(Sequel::Error, "invalid associated object, cannot save")
|
|
794
849
|
end
|
|
795
850
|
def_add_method(opts)
|
|
796
851
|
|
|
797
852
|
unless opts[:one_to_one]
|
|
798
853
|
association_module_private_def(opts._remove_method) do |o|
|
|
799
|
-
o.send(:"#{
|
|
800
|
-
o.save || raise(Sequel::Error, "invalid associated object, cannot save")
|
|
854
|
+
cks.each{|k| o.send(:"#{k}=", nil)}
|
|
855
|
+
o.save(:validate=>validate) || raise(Sequel::Error, "invalid associated object, cannot save")
|
|
801
856
|
end
|
|
802
857
|
association_module_private_def(opts._remove_all_method) do
|
|
803
|
-
opts.associated_class.filter(
|
|
858
|
+
opts.associated_class.filter(cks.zip(cpks.map{|k| send(k)})).update(ck_nil_hash)
|
|
804
859
|
end
|
|
805
860
|
def_remove_methods(opts)
|
|
806
861
|
end
|
|
@@ -820,7 +875,7 @@ module Sequel
|
|
|
820
875
|
klass = opts.associated_class
|
|
821
876
|
update_database = lambda do
|
|
822
877
|
send(opts.add_method, o)
|
|
823
|
-
klass.filter(
|
|
878
|
+
klass.filter(cks.zip(cpks.map{|k| send(k)})).exclude(o.pk_hash).update(ck_nil_hash)
|
|
824
879
|
end
|
|
825
880
|
use_transactions ? db.transaction(opts){update_database.call} : update_database.call
|
|
826
881
|
end
|
|
@@ -873,7 +928,7 @@ module Sequel
|
|
|
873
928
|
else
|
|
874
929
|
if !opts[:key]
|
|
875
930
|
send(opts.dataset_method).all.first
|
|
876
|
-
elsif
|
|
931
|
+
elsif opts[:keys].all?{|k| send(k)}
|
|
877
932
|
send(opts.dataset_method).first
|
|
878
933
|
end
|
|
879
934
|
end
|
|
@@ -883,7 +938,7 @@ module Sequel
|
|
|
883
938
|
def add_associated_object(opts, o, *args)
|
|
884
939
|
raise(Sequel::Error, "model object #{model} does not have a primary key") unless pk
|
|
885
940
|
if opts.need_associated_primary_key?
|
|
886
|
-
o.save if o.new?
|
|
941
|
+
o.save(:validate=>opts[:validate]) if o.new?
|
|
887
942
|
raise(Sequel::Error, "associated object #{o.model} does not have a primary key") unless o.pk
|
|
888
943
|
end
|
|
889
944
|
return if run_association_callbacks(opts, :before_add, o) == false
|
|
@@ -1348,7 +1403,12 @@ module Sequel
|
|
|
1348
1403
|
# Associate each object with every key being monitored
|
|
1349
1404
|
a.each do |rec|
|
|
1350
1405
|
key_hash.each do |key, id_map|
|
|
1351
|
-
|
|
1406
|
+
case key
|
|
1407
|
+
when Array
|
|
1408
|
+
id_map[key.map{|k| rec[k]}] << rec if key.all?{|k| rec[k]}
|
|
1409
|
+
when Symbol
|
|
1410
|
+
id_map[rec[key]] << rec if rec[key]
|
|
1411
|
+
end
|
|
1352
1412
|
end
|
|
1353
1413
|
end
|
|
1354
1414
|
|