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.
Files changed (93) hide show
  1. data/CHANGELOG +84 -0
  2. data/Rakefile +1 -1
  3. data/doc/cheat_sheet.rdoc +5 -2
  4. data/doc/opening_databases.rdoc +2 -0
  5. data/doc/release_notes/3.5.0.txt +510 -0
  6. data/lib/sequel/adapters/ado.rb +3 -1
  7. data/lib/sequel/adapters/ado/mssql.rb +2 -2
  8. data/lib/sequel/adapters/do.rb +2 -11
  9. data/lib/sequel/adapters/do/mysql.rb +7 -0
  10. data/lib/sequel/adapters/do/postgres.rb +2 -2
  11. data/lib/sequel/adapters/firebird.rb +3 -3
  12. data/lib/sequel/adapters/informix.rb +3 -3
  13. data/lib/sequel/adapters/jdbc/h2.rb +3 -3
  14. data/lib/sequel/adapters/jdbc/mssql.rb +7 -0
  15. data/lib/sequel/adapters/mysql.rb +60 -21
  16. data/lib/sequel/adapters/odbc.rb +1 -1
  17. data/lib/sequel/adapters/openbase.rb +3 -3
  18. data/lib/sequel/adapters/oracle.rb +1 -5
  19. data/lib/sequel/adapters/postgres.rb +3 -3
  20. data/lib/sequel/adapters/shared/mssql.rb +142 -33
  21. data/lib/sequel/adapters/shared/mysql.rb +54 -31
  22. data/lib/sequel/adapters/shared/oracle.rb +17 -6
  23. data/lib/sequel/adapters/shared/postgres.rb +7 -7
  24. data/lib/sequel/adapters/shared/progress.rb +3 -3
  25. data/lib/sequel/adapters/shared/sqlite.rb +3 -17
  26. data/lib/sequel/connection_pool.rb +4 -6
  27. data/lib/sequel/core.rb +29 -113
  28. data/lib/sequel/database.rb +14 -12
  29. data/lib/sequel/dataset.rb +8 -21
  30. data/lib/sequel/dataset/convenience.rb +1 -1
  31. data/lib/sequel/dataset/graph.rb +9 -2
  32. data/lib/sequel/dataset/sql.rb +170 -104
  33. data/lib/sequel/exceptions.rb +3 -0
  34. data/lib/sequel/extensions/looser_typecasting.rb +21 -0
  35. data/lib/sequel/extensions/named_timezones.rb +61 -0
  36. data/lib/sequel/extensions/schema_dumper.rb +7 -1
  37. data/lib/sequel/extensions/sql_expr.rb +122 -0
  38. data/lib/sequel/extensions/string_date_time.rb +4 -4
  39. data/lib/sequel/extensions/thread_local_timezones.rb +48 -0
  40. data/lib/sequel/model/associations.rb +105 -45
  41. data/lib/sequel/model/base.rb +37 -28
  42. data/lib/sequel/plugins/active_model.rb +35 -0
  43. data/lib/sequel/plugins/association_dependencies.rb +96 -0
  44. data/lib/sequel/plugins/class_table_inheritance.rb +214 -0
  45. data/lib/sequel/plugins/force_encoding.rb +61 -0
  46. data/lib/sequel/plugins/many_through_many.rb +32 -11
  47. data/lib/sequel/plugins/nested_attributes.rb +7 -2
  48. data/lib/sequel/plugins/subclasses.rb +45 -0
  49. data/lib/sequel/plugins/touch.rb +118 -0
  50. data/lib/sequel/plugins/typecast_on_load.rb +61 -0
  51. data/lib/sequel/sql.rb +31 -30
  52. data/lib/sequel/timezones.rb +161 -0
  53. data/lib/sequel/version.rb +1 -1
  54. data/spec/adapters/mssql_spec.rb +262 -0
  55. data/spec/adapters/mysql_spec.rb +46 -8
  56. data/spec/adapters/postgres_spec.rb +6 -3
  57. data/spec/adapters/spec_helper.rb +21 -0
  58. data/spec/adapters/sqlite_spec.rb +1 -1
  59. data/spec/core/connection_pool_spec.rb +1 -1
  60. data/spec/core/database_spec.rb +27 -1
  61. data/spec/core/dataset_spec.rb +63 -1
  62. data/spec/core/object_graph_spec.rb +1 -1
  63. data/spec/core/schema_spec.rb +1 -0
  64. data/spec/extensions/active_model_spec.rb +47 -0
  65. data/spec/extensions/association_dependencies_spec.rb +108 -0
  66. data/spec/extensions/class_table_inheritance_spec.rb +252 -0
  67. data/spec/extensions/force_encoding_spec.rb +75 -0
  68. data/spec/extensions/looser_typecasting_spec.rb +39 -0
  69. data/spec/extensions/many_through_many_spec.rb +60 -2
  70. data/spec/extensions/named_timezones_spec.rb +72 -0
  71. data/spec/extensions/nested_attributes_spec.rb +29 -1
  72. data/spec/extensions/schema_dumper_spec.rb +10 -0
  73. data/spec/extensions/spec_helper.rb +1 -1
  74. data/spec/extensions/sql_expr_spec.rb +89 -0
  75. data/spec/extensions/subclasses_spec.rb +52 -0
  76. data/spec/extensions/thread_local_timezones_spec.rb +45 -0
  77. data/spec/extensions/touch_spec.rb +155 -0
  78. data/spec/extensions/typecast_on_load_spec.rb +60 -0
  79. data/spec/integration/database_test.rb +8 -0
  80. data/spec/integration/dataset_test.rb +9 -9
  81. data/spec/integration/plugin_test.rb +139 -0
  82. data/spec/integration/schema_test.rb +7 -7
  83. data/spec/integration/spec_helper.rb +32 -1
  84. data/spec/integration/timezone_test.rb +3 -3
  85. data/spec/integration/transaction_test.rb +1 -1
  86. data/spec/integration/type_test.rb +6 -6
  87. data/spec/model/association_reflection_spec.rb +18 -0
  88. data/spec/model/associations_spec.rb +169 -9
  89. data/spec/model/base_spec.rb +2 -0
  90. data/spec/model/eager_loading_spec.rb +82 -2
  91. data/spec/model/model_spec.rb +8 -1
  92. data/spec/model/record_spec.rb +52 -9
  93. metadata +33 -23
@@ -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] && default.is_a?(String)
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, "Invalid Date value '#{self}' (#{e.message})"
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, "Invalid DateTime value '#{self}' (#{e.message})"
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::InvalidValue, "Invalid #{Sequel.datetime_class} value '#{self}' (#{e.message})"
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, "Invalid Time value '#{self}' (#{e.message})"
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
- key = self[:key]
104
+ keys = self[:keys]
105
105
  associated_class.all_association_reflections.each do |assoc_reflect|
106
- if assoc_reflect[:type] == r_type && assoc_reflect[:key] == key && assoc_reflect.associated_class == self[:model]
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
- left_key = self[:left_key]
290
- right_key = self[:right_key]
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[:left_key] == right_key &&
294
- assoc_reflect[:right_key] == left_key && assoc_reflect[:join_table] == join_table &&
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
- else
584
- ds.select_more(SQL::AliasedExpression.new(SQL::QualifiedIdentifier.new(opts.associated_key_table, opts.associated_key_column), opts.associated_key_alias)) if opts.eager_loading_use_associated_key?
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, [[right, opts.right_primary_key], [left, send(left_pk)]])}
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
- model.eager_loading_dataset(opts, opts.associated_class.inner_join(join_table, [[right, opts.right_primary_key], [left, h.keys]]), Array(opts.select), associations).all do |assoc_record|
667
- next unless objects = h[assoc_record.values.delete(left_key_alias)]
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 : [[left, left_pk]] + 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, &jt_graph_block)
684
- ds.graph(opts.associated_class, use_only_conditions ? only_conditions : [[opts.right_primary_key, right]] + conditions, :select=>select, :table_alias=>assoc_alias, :join_type=>join_type, &graph_block)
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
- database.dataset.from(join_table).insert(left=>send(left_pk), right=>o.send(opts.right_primary_key))
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([[left, send(left_pk)], [right, o.send(opts.right_primary_key)]]).delete
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(left=>send(left_pk)).delete
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, opts.primary_key)=>send(key))
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
- next unless objects = h[assoc_record.send(opts.primary_key)]
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 : [[opts.primary_key, key]] + conditions, :select=>select, :table_alias=>assoc_alias, :join_type=>join_type, :implicit_qualifier=>table_alias, &graph_block)
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(:"#{key}=", (o.send(opts.primary_key) if o))}
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, key) => send(primary_key))
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
- next unless objects = h[assoc_record[key]]
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 : [[key, primary_key]] + conditions, :select=>select, :table_alias=>assoc_alias, :join_type=>join_type, :implicit_qualifier=>table_alias, &graph_block)
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(:"#{key}=", send(primary_key))
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(:"#{key}=", nil)
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(key=>send(primary_key)).update(key=>nil)
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(Sequel::SQL::BooleanExpression.new(:AND, {key=>send(primary_key)}, SQL::BooleanExpression.new(:'!=', klass.primary_key, o.pk))).update(key=>nil)
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 send(opts[:key])
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
- id_map[rec[key]] << rec if rec[key]
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